【类和对象(中)】六大默认成员函数

简介: 前言本文继类和对象上,开始讲述默认成员函数。默认成员函数是:我们不具体写,编译器会自动生成的函数叫默认成员函数。一、🌺构造函数(重点🌺)构造函数是类的一个默认成员函数,它虽然叫构造函数,但它的作用并不是构造一个对象,而是初始化一个对象。它与Init函数不同, 每次实例化一个新的对象都要调用 Init函数来完成对象的初始化,比较麻烦,而这个构造函数,可以解决这个问题。

前言

本文继类和对象上,开始讲述默认成员函数。

默认成员函数是:我们不具体写,编译器会自动生成的函数叫默认成员函数。

一、🌺构造函数(重点🌺)

构造函数是类的一个默认成员函数,它虽然叫构造函数,但它的作用并不是构造一个对象,而是初始化一个对象。

它与Init函数不同, 每次实例化一个新的对象都要调用 Init函数来完成对象的初始化,比较麻烦,而这个构造函数,可以解决这个问题。

1.构造函数的特性

1.函数名与类名相同

2.没有返回值

3.编译器实例化时会自动调用该构造函数

4.构造函数可以重载

stack::stack(int default_capapacity)
{
  cout << "stack(int default_capapacity = 4)" << endl;
  _a = (int*)malloc(sizeof(int) * default_capapacity);
  if (nullptr == _a)
  {
    perror("malloc fail\n");
    exit(-1);
  }
  _capacity = default_capapacity;
  _top = 0;
  cout << default_capapacity << endl;
}
stack::stack()
{
  cout << "stack(int default_capapacity = 4)" << endl;
  int default_capapacity = 4;
  _a = (int*)malloc(sizeof(int) * default_capapacity);
  if (nullptr == _a)
  {
    perror("malloc fail\n");
    exit(-1);
  }
  _capacity = default_capapacity;
  _top = 0;
  cout << default_capapacity << endl;
}

这两个构造函数可以构成函数重载,并且一个是全缺省的构造函数,一个是无参的构造函数,这两个构造函数都是默认构造函数,对于编译器来讲,默认构造函数只能存在一个。

所以如果两个都写,编译器会报一个错误:调用不明确。

c97e06c8f23c4ab3ab23bc02bca8588c.png

一般写构造函数最好写全缺省的默认构造函数

注意:默认构造函数的声明和定义不能同时给缺省值。

不能使用下面的方法调用构造函数:

  stack s1();
  //这样看还不想函数声明
  stack func();//这样看就像是函数声明

这样调用构造函数会给编译器造成困扰,编译器不知道这样到底是调用默认构造函数还是一个函数的声明。

构造函数比普通的函数更特殊,它也有自己的调用方法。

正确的调用方法:

  stack s1;
  //上面这中是不给参数,编译器就会默认使用缺省值
  //两种都可以,下面这种是自己给参数
  stack s1(4);

5.如果类中没有显式定义一个默认构造函数,那么编译器会自动生成一个无参数的默认构造函数。一旦我们自己定义构造函数,编译器就不会再自动生成。


6. 编译器自己生成的默认构造函数不会对内置类型做处理,对自定义类型会调用自定义类型自己的默认构造函数。

所以:有内置类型就必须要自己写构造函数,不能用编译器自己生成的默认构造函数。

如果全部都是自定义类型成员,则可以考虑让编译器自己生成。


但是在C++11的标准发布之后,打了一个补丁:在成员变量声明的时候可以自己给缺省值。注意:不是初始化,只是声明。

缺省值的意义在于:如果我们没有自己给指定的值,编译器就会用缺省值,如果我们自己给了,编译器就不会用缺省值了。

7.无参的构造函数和全缺省的构造函数都可以叫默认构造函数,还有编译器自己生成的构造函数也可以叫做默认构造函数。这几个默认构造函数只能存在一个。

对于编译器自己生成的默认构造函数中,有一个特点:
内置类型不做处理,自定义类型会调用它自己的默认构造函数。

所以我们自己实现默认构造函数的时候,如果有内置类型,最好写成全缺省的构造函数,这样编译器就会调用该构造函数。

不需要我们自己实现默认构造函数的两种情况:

1.内置类型成员都有缺省值,且缺省值符合我们的要求。

2.全是自定义类型的构造,且这些自定义类型都定义默认构造函数。

比如:两个栈实现队列,两个栈都是内置类型,不需要自己写构造函数,会直接调用自定义类型的默认构造函数

二、🌺析构函数(重点🌺)

析构函数同样是类的一个默认成员函数,析构函数的功能是不是销毁对象本身的,销毁对象本身是编译器完成的,析构函数是销毁对象中申请的资源空间。

析构函数会在对象销毁时自动调用。

1.析构函数的特性

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
~stack();

这是一个典型的析构函数的声明。

  1. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

注意:(1)析构函数不能重载.
(2)默认生成的析构函数:对内置类型不做处理,对自定义类型则会调用它的默认析构函数

两种不需要写析构函数的情况:

(1).当成员都是内置类型时(没有动态申请资源的),不需要写析构函数。

(2).需要释放资源的都是自定义类型的,也不需要写析构函数。(对于自定义类型,编译器会自动调用它的析构函数)

  1. 类对象生命周期结束时,C++编译系统系统自动调用析构函数。

三、🌺拷贝构造函数 (重点🌺)

拷贝构造函数是特殊的构造函数,是构造函数的重载形式。
它的作用是用已存在的类对象来初始化一个新的类对象。

与下面所讲的赋值运算符重载区别,注意区分。

1.拷贝构造函数特性

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。(另一个参数是this指针) 因为C++规定,自定义类型在传参时会调用它的拷贝构造函数。


907ad70e59074c29b12bcede7a5ee4ae.png

因为把d1传给d时,需要调用d的拷贝构造函数,才能传给d,调用d的拷贝构造函数后,又需要传参,传d1给d,而传参又会调用d的拷贝构造函数,这样会引发无穷递归。


所以拷贝构造函数需要传引用,这样传引用就不会调用他的拷贝构造函数了,因为d 就是d1的引用。


注意:拷贝构造函数的参数最好加一个const修饰,因为可能会错写成下面这样:

7e7a82e13ba344eeac3648592fc783e4.png

这样就写反了。


若未显式定义,编译器会生成默认的拷贝构造函数。

默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。


与构造和析构函数不同,拷贝构造函数会对内置类型和在自定义类型都处理。


内置类型完成的拷贝构造会完成值拷贝(浅拷贝),

自定义类型会调用它的拷贝构造函数。


C++规定:调用函数传参时:内置类型直接拷贝

自定义类型必须调用它的拷贝构造函数。


但是不能因为编译器会处理,就不写拷贝构造函数,因为编译器仅仅是完成浅拷贝。

比如下面的案例:

class stack
{
public:
stack(int default_capapacity =4 )
{
  cout << "stack(int default_capapacity = 4)" << endl;
  _a = (int*)malloc(sizeof(int) * default_capapacity);
  if (nullptr == _a)
  {
    perror("malloc fail\n");
    exit(-1);
  }
  _capacity = default_capapacity;
  _top = 0;
  cout << default_capapacity << endl;
}
~stack()
{
  cout << "stack::~stack()" << endl;
  if (_a)
  {
    free(_a);
    _a = NULL;
    _capacity = _top = 0;
  }
}
private:
  int _capacity;
  int _top;
  int *_a;
};
int main()
{
  stack s1;
  stack s2(s1);
  return 0;
}

当我们实例化一个类对象时,会自动调用它的构造函数,然后实例化一个类对象s2(这个s2是已经存在的!)时,调用的是拷贝构造函数。


16a8505a50a44af083e65903104bf8df.png

可以看到,s2完完全全拷贝了s1的内容,此时的情况是这样的:


17e1e24314934dd2ae53f24988dc6bcb.png

所以此时s1的_a和s2的_a指向的是同一块空间。

那么在s2生命周期结束的时候会调用它的析构函数,释放了_a指向的空间,在s1生命周期结束的时候也会调用它的析构函数,此时就会重复释放_a指向的空间,会出问题。


注意:是s2先调用析构函数,即后进先出原则!!!

注意:是s2先调用析构函数,即后进先出原则!!!

注意:是s2先调用析构函数,即后进先出原则!!!


所以:像这样有动态申请空间的不能直接让编译器生成默认拷贝构造函数,需要自己实现深拷贝构造函数。


上面的例子中正确的实现拷贝构造函数的方法如下:

  stack(const stack& st)
  {
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (nullptr == _a)
    {
      perror("malloc fail\n");
      exit(-1);
    }
    // destination, source,num
    memcpy(_a, st._a, sizeof(int) * st._top);
    _capacity = st._capacity;
    _top = st._top;
  }

总结:浅拷贝会出现两个问题:

1、如果析构两次,会出现重复释放空间的问题。

2、一个地方修改会影响到另一个地方。


构造函数和拷贝构造函数的区别和联系

联系:构造函数和拷贝构造函数都属于构造函数。


区别:构造函数是初始化一个类对象,

而拷贝构造函数是两个已经存在的对象之间的拷贝。

四、🌺赋值运算符重载(重点🌺)

🌺1.什么是运算符重载

运算符重载是具有特殊函数名的函数,(运算符重载的关键字为operator)也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

1.1运算符重载存在的意义

运算符重载的存在是为了让自定义类型能够像内置类型一样可以使用赋值,比较,等操作符。

1.2运算符重载的一些注意事项

函数名字为:关键字operator后面接需要重载的运算符符号。

函数的原型:返回值类型 + operator+ 操作符(参数列表)

比如:重载一个赋值运算符,则该赋值运算符的函数原型为:

Date& operator=(const Date& d);

重点注意(🌺)


不能通过连接其他符号来创建新的操作符:比如operator@


重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义


作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this


.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

🌺2.赋值运算符重载的特性

特性1. 赋值运算符重载格式

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

检测是否自己给自己赋值

返回*this :要复合连续赋值的含义

Date& Date::operator=(const Date& d)
{
  if (this != &d)
  {
    this->_year = d._year;
    this->_month = d._month;
    this->_day = d._day;
  }
  return *this;
}

以日期类为例:赋值运算符重载的正确写法如上:

(1)返回引用:可以返回引用的原因是:this是一个形参指针,生命周期就在这个栈区,但是*this是外面的对象, 生命周期远超过这个作用域,返回 *this没有影响,还是同一个对象。


(2)加const,既然是赋值运算符,右值不可以修改,为了防止像拷贝构造一样写反,最好加上一个const。


(3)防止自己给自己赋值,可以加一个判断。

(注意&d的&是取地址的意思)


特性2.

赋值运算符只能重载成类的成员函数不能重载成全局函数。


原因:如果类中没有赋值运算符,编译器会自己实现,此时如果再在类外面自己定义一个全局的赋值运算符重载,就和编译器自动生成的产生冲突,所以赋值运算符只能实现在类里面。


特性3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。(浅拷贝)


注意:对于内置类型来说,编译器自己生成的赋值运算符会进行浅拷贝(拷贝值)

对于自定义类型来说,编译器会调用它的赋值运算符重载。

五、🌺取地址运算符重载(了解)

以下两个重载均以日期类为例:

const Date* operator&()
{
  return this;
}

六、🌺const取地址运算符重载(了解)

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针。

表明在该成员函数中不能对类的任何成员进行修改。

const Date* Date::operator&() const
{
  return this;
}

const Date* Date::operator&() const

等价于:

const Date* Date::operator&(const Date * this)


因为前面我们说过,this指针不能在形参和实参部分显式地使用,但是可以在成员函数内部直接使用。


取地址运算符和const修饰的取地址运算符构成重载,因为

一个是Date* this ,

一个是const Date* this。

总结

本文讲述了六大默认成员函数的具体特性,以及他们的使用场景还有各种细节。

相关文章
|
2月前
|
存储 Serverless 数据安全/隐私保护
C++ 类的成员函数和数据成员的技术性探讨
C++ 类的成员函数和数据成员的技术性探讨
30 0
|
19天前
|
编译器 C++ 存储
【C++语言】类和对象--默认成员函数 (中)
【C++语言】类和对象--默认成员函数 (中)
【C++语言】类和对象--默认成员函数 (中)
|
2月前
|
安全 编译器 程序员
类与对象(二)--类的六个默认成员函数超详细讲解
类与对象(二)--类的六个默认成员函数超详细讲解
类与对象(二)--类的六个默认成员函数超详细讲解
|
2月前
|
C++
C++ 类的初始化列表与构造函数初始化的技术性探讨
C++ 类的初始化列表与构造函数初始化的技术性探讨
14 0
|
2月前
|
编译器 C++
【C++成长记】C++入门 | 类和对象(中) |类的6个默认成员函数、构造函数、析构函数
【C++成长记】C++入门 | 类和对象(中) |类的6个默认成员函数、构造函数、析构函数
|
2月前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(上) |类的作用域、类的实例化、类的对象大小的计算、类成员函数的this指针
【C++成长记】C++入门 | 类和对象(上) |类的作用域、类的实例化、类的对象大小的计算、类成员函数的this指针
|
2月前
|
存储 安全 编译器
【C++】类的六大默认成员函数及其特性(万字详解)
【C++】类的六大默认成员函数及其特性(万字详解)
48 3
|
2月前
|
存储 编译器 C语言
【C++练级之路】【Lv.2】类和对象(上)(类的定义,访问限定符,类的作用域,类的实例化,类的对象大小,this指针)
【C++练级之路】【Lv.2】类和对象(上)(类的定义,访问限定符,类的作用域,类的实例化,类的对象大小,this指针)
|
2月前
|
存储 编译器 程序员
【C++学习】类和对象(中)一招带你彻底了解六大默认成员函数
【C++学习】类和对象(中)一招带你彻底了解六大默认成员函数
|
2月前
|
存储 编译器 C语言
【C++初阶】第三站:类和对象(中) -- 类的6个默认成员函数-2
【C++初阶】第三站:类和对象(中) -- 类的6个默认成员函数-2