[C++ classes and objects] Six default member functions eat everything

This article has participated in the "Newcomer Creation Ceremony" event to start the road of gold creation together.

1. Constructor


[Function]: Complete the initialization of the object

【feature】:

  • ①No return value , with or without parameters

  • ② The function name is the same as the class name

     class Date
     {
     public:
         Date()  // 注意不带参,void也不要加上
         {
             _year = 2022;
            _month = 7;
            _day = 2;
         }
     ​
     private:
         int _year;
         int _month;
         int _day;
     };
     ​
    
  • Function overloading can be formed , and there can be default values. Generally speaking, full default is the most commonly used. But it should be noted that no parameter and full default cannot appear at the same time , because the compiler can't tell which one should be called

     // ……
         Date()  // 无参
         {
             _year = 2022;
             _month = 7;
             _day = 2;
         }
     ​
         Date(int year = 2022, int month = 7, int day = 2) // 全缺省
         {
             _year = year;
             _month = month;
             _day = day;
         }
     ​
         Date(int year, int month = 7, int day = 2)   // 半缺省
         {
             _year = year;
             _month = month;
             _day = day;
         }
     // ……
    
  • ④ The constructor is automatically called when the object is instantiated . If the constructor is parameterless, it should be written when the object is created , but it cannot be written , because this may be interpreted as a function declaration (the function name is d1 and the return value is Date)Date d1Date d1()

     int main()
     {
        Date d1;          // 无参的
        Date d2(2022, 7, 2)   // 有参的
        return 0;   
     }
    
  • ⑤ ⭐The role of the default constructor

    If no constructor is explicitly written, the C++ compiler will automatically generate a default constructor with no arguments . No-argument constructors, full default constructors, and constructors generated by default by the compiler can be considered as default constructors. In essence, functions that can be called without passing parameters are default constructors.

    [Question] Why does the constructor generated by the compiler by default seem to have no effect?

    The constructor generated by the compiler by default is only for the custom type when it is initialized. The way of processing is to call its constructor, but the built-in type will not be initialized .

    在C++中只有少数几种情况推荐使用编译器的默认构造函数:

    • 所有的成员变量都是自定义类型,并且都提供了默认构造函数

    • 内置类型的成量给了缺省值。C++11标准引入了内置类型成员变量“缺省值”的概念:

      image-20220703093433693

     class Date
     {
     public:
         Date(int year = 2022, int month = 7, int day = 2)
         {
             _year = year;
             _month = month;
             _day = day;
         }
     ​
     private:
         int _year = 2022;  // 这就是缺省值
         int _month = 7;
         int _day = 2;
     };
     ​
     class Time
     {
     private:
         int a;     // 内置类型不处理
         Date date;  // 自定义类型调用自定义类型的构造函数
     }; 
     ​
     int main()
     {
         Time t;
         return 0;
     }
    

    image-20220703092737929

    编译器默认生成的构造函数的特点是只会初始化自定义类型,初始化的方式是调用其默认构造函数(只有上面提到的三种,如半缺省就不行)。

    如果没有默认构造函数(例如将上面的全缺省改成半缺省),就会出现找不到默认构造函数的错误。

二、析构函数


【作用】:完成类的资源清理(不是完成对象的销毁,对象销毁由编译器完成)

【特征】:

  • ① 对象生命周期结束后自动调用

  • ② 析构函数名和是在类型前加上 “~”

  • ③ 无参数无返回值

     class Stack 
     {
     public: 
         ~Stack()
         {
             free(arr);
             arr = nullptr;
         }
     ​
     private:
         int* arr;
         int sz;
         int capacity;
     };
    
  • ④ 若没有显式的写出析构函数,则C++编译器会自动生成一个无参的默认析构函数。编译器默认生成的构造函数只会处理自定义类型,处理的方式是调用其默认构造函数

  • ⑤ 先构造的后析构

     C c;
     int main()
     {
         A a;
         B b;
         static D d;
         return 0;
     }
     // A B C D 四个类析构的顺序是 B A D C
    

三、拷贝构造函数


【背景】

在函数进行传值传参的时候,对于内置类型来说,形参是实参的直接拷贝,但是自定义类型的变量在传参的时候必须要调用其拷贝构造函数

从本质上讲,当类的对象需要拷贝时,拷贝构造函数将会被调用,所以函数的返回值是对象时,也会调用相应的拷贝构造函数。

【作用】创建一个与原对象一模一样的的新对象

【特征】

  • 拷贝构造函数是构造函数的一个重载形式

  • 已存在类的对象创建新对象时由编译器自动调用

  • 只有单个形参,该形参是对同类对象引用(一般常用const修饰) 。

  • 必须采用引用传参,采用传值调用会引发无穷递归。因为传值调用在拷贝形参时仍会调用其拷贝构造函数,如此不断递归下去,程序就会崩溃。而采用引用传参则不需要拷贝,因此也就不需要调用拷贝函数。

     class Date
     {
         Date (const Date& d) // const Date d (X)
         {
             _year = d._year;
             _month = d._month;
             _day = d._day;
         }
     ​
     private:
         int _year;
         int _month;
         int _day;
     };
    
  • 若未显式定义,编译器会生成默认的拷贝构造函数。默认拷贝构造函数的作用是对于内置类型和自定义类型的处理是不同的:

    • 对于自定义类型——调用其拷贝构造函数
    • 对于内置类型,按字节序完成拷贝(相当于memcpy的作用),这种拷贝我们叫做浅拷贝,或者值拷贝。

【总结】一般的类,使用默认生成的拷贝构造函数就够用了,但是需要直接管理内存资源的的类,例如自己写的栈,那我们就需要自己写拷贝构造函数了(需要实现深拷贝)。

否则拷贝后,两个栈所指向的空间也会是一样的,会有以下两个弊端:

  • 修改数据时会相互影响
  • 在析构时,同一块空间有可能被释放两次

四、赋值运算符重载


① 什么是运算符重载

【背景】

假设我们要判断两个日期类对象是否相等,虽然可以通过自定义函数来实现,但是如果我们可以用运算符直接比较,那么代码的可读性会大大提高。

而运算符重载的出现就是为了解决自定义类型不能使用各种运算符的问题。

【作用】增强代码的可读性

【特征】:

  • ① 运算符重载本质是具有特殊函数名的函数。函数原型为返回值类型 + operator + 需要重载的运算符符号 + 参数列表

  • ② 由于类外的函数不能访问私有成员变量,所以最好的解决办法是将运算符重载函数写在类里面(或者使用友元、或者写一个函数返回成员变量的值,但都不推荐)

     class Date
     {
     public: 
         Date(int month = 7, int day = 3)
         {
             _month = month;
             _day = day;
         }
         int operator==(const Date& d1, const Date& d2)
         {
             if (d1._month == d2._month && d1._day == d2._day)
                 return 1;
             else
                 return 0;
         }
     private:
         int _month;
         int _day;
     };
    
  • ③ 上面运算符重载的写法仍然是有问题的,错误提示是参数过多。原因在于每个非静态成员函数都有一个隐藏参数 this 指针(用于指向当前对象),所以正确的写法应该是:

    int operator==(const Date& d2) //为了避免拷贝,推荐使用引用,最好再加上const
      {
          if (_month == d2._month && _day == d2._day)
              return 1;
          else
              return 0;
      }
    
  • ④ 有了运算符重载就可以直接使用运算符比较自定义类型了

    int main()
    {
    	Date d1;
    	Date d2;
    	if (d1 == d2)   // 等价于 d1.operator== (d2)
    		cout << "==" << endl;
    	return 0;
    }
    
  • ⑤ 优先在类里面找对应运算符重载,再到全局里搜索

  • ⑥ 有五个运算符不能重载: ***** :: sizeof ?: .

② 赋值运算符重载
class Date
{
public:  // ……
 Date& operator= (const Date& d2)
	{
		if (&d2 != this)
		{
			_month = d2._month;
			_day = d2._day;
		}
    return *this;
	}
		// …… 
};

【注意点】

  • 考虑到连续赋值的情况(d1 = d2 = d3),我们需要给赋值运算符重载设计返回值。为了减少拷贝,返回值采用引用的方式

  • 为了避免自己给自己赋值的情况,最好先进行判断

  • 如果没有显式的定义赋值运算符符重载,编译器默认生成的赋值运算符重载会完成对象按字节序的值拷贝 (所以涉及对内存直接管理的类,我们有必要自己写运算符重载)

  • 与拷贝构造的区别:

    拷贝构造是使用一个存在的对象去初始化一个正在创建的对象,赋值重载是两个存在的对象之间的赋值

五、取地址及const取地址操作符重载

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

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

【注意】

  • 取地址及const取地址操作符重载编译器默认生成的就够用了(下图是默认生成的取地址操作符重载)

image-20220706225930962

  • 只有极少数的情况需要自己写取地址操作符重载,例如不想让人得到对象的真实地址。

Guess you like

Origin juejin.im/post/7117559743807225870