[C++クラスとオブジェクト]6つのデフォルトのメンバー関数がすべてを食べる

この記事は、「新人クリエーションセレモニー」イベントに参加し、一緒にゴールドクリエーションの道を歩み始めました。

1.コンストラクター


【機能】:オブジェクトの初期化を完了します

【特徴】:

  • 戻り値なし、パラメータありまたはなし

  • ②関数名はクラス名と同じです

     class Date
     {
     public:
         Date()  // 注意不带参,void也不要加上
         {
             _year = 2022;
            _month = 7;
            _day = 2;
         }
     ​
     private:
         int _year;
         int _month;
         int _day;
     };
     ​
    
  • 関数のオーバーロードを構成し、デフォルト値を持つことができます。一般的に、完全なデフォルトが最も一般的に使用されます。ただし、コンパイラはどちらを呼び出すべきかを判断できないため、パラメータと完全なデフォルトを同時に表示することはできないことに注意してください。

     // ……
         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;
         }
     // ……
    
  • オブジェクトがインスタンス化されると、コンストラクターが自動的に呼び出されます。コンストラクターにパラメーターがない場合は、オブジェクトの作成時に書き込む必要がありますが、関数宣言として解釈される可能性があるため、Date d1記述できません(関数名はd1、戻り値はDate)。Date d1()

     int main()
     {
        Date d1;          // 无参的
        Date d2(2022, 7, 2)   // 有参的
        return 0;   
     }
    
  • ⑤⭐デフォルトコンストラクターの役割

    コンストラクターが明示的に記述されていない場合、C++コンパイラーは引数のないデフォルトのコンストラクターを自動的に生成します。引数なしのコンストラクター、完全なデフォルトコンストラクター、およびコンパイラーによってデフォルトで生成されるコンストラクターは、デフォルトコンストラクターと見なすことができます。本質的に、パラメーターを渡さずに呼び出すことができる関数は、デフォルトコンストラクターです。

    【質問】コンパイラがデフォルトで生成したコンストラクタが効果がないように見えるのはなぜですか?

    コンパイラーがデフォルトで生成するコンストラクターは、初期化時にカスタム型専用です。処理方法はコンストラクターを呼び出すことですが、組み込み型は初期化されません。

    在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

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

おすすめ

転載: juejin.im/post/7117559743807225870