⚡【C++要笑着学】(7) 默认成员函数:构造函数 | 析构函数 | 拷贝构造函数

🔥 订阅量破千的火热 C++ 教程

👉 火速订阅《C++要笑着学》 

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 ← 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

  本篇博客全站综合热榜最高排名:7

👑 本篇博客是本专栏最受欢迎的一篇 👑

  • 💭 写在斜面:朋友们好啊,我是亦优叶子,今天终于更新了。本章将继续讲解C++中的面向对象的知识点,本篇主要讲解默认成员函数中的构造函数、析构函数和拷贝构造函数。还是和以前一样,我们将由浅入深地去讲解,以 "初学者" 的角度去探索式地学习。会一步步地推进讲解,而不是直接把枯燥的知识点倒出来,应该会有不错的阅读体验。保证文章非常有意思!不信你可以读一读。本章内容全是干货,是 C++ 面向对象的重要章节! 如果觉得不错,可以 "一键三连" 支持一下博主!你们的关注就是我更新的最大动力!
    • 【2023.9.27 更新】评论区对于此问题也有不少提问,这里我再做一个补充说明:这两个问题实际上都是在问析构顺序的,只要把这块知识点搞明白就会很好理解(讲解在本章的 Ⅲ 0x03)

    📜 本章目录:

    目录

    Ⅰ.  默认成员函数(Default member function)

    Ⅱ. 构造函数(Constructor)

    0x00 引入:为什么要有构造函数?

    0x01 构造函数的概念

    0x02 构造函数的特性

    0x03 默认构造函数

    0x04 构造函数的特性的测试

    Ⅲ. 析构函数(Destructor)

    0x00 引入:专门 “擦屁股” 的析构函数

    0x01 析构函数的概念

    0x02 析构函数的特性

    0x03 析构顺序问题

    0x04 析构函数的特性的测试

    Ⅳ.  拷贝构造函数(Copy Constructor)

    0x00 引入:可以帮我拷贝吗?可以!

    0x01 拷贝构造函数的概念

    0x02 拷贝构造函数的特性

    0x03 关于默认生成的拷贝构造

    Ⅴ.  总结(构造&析构&拷贝构造)

    0x00 构造函数 

    0x01 析构函数

    0x02 拷贝构造


    Ⅰ.  默认成员函数(Default member function)

    如果一个类中什么成员都没有,我们称之为 "空类" 。

    但是空类中真的什么都没有吗?答案是否定的!

     类有六个默认成员函数,特殊的点非常多,我们本章就来好好学学。

     对于 默认成员函数,如果我们不主动实现,编译器会自己生成一份。

    ❓ 它们有什么用呢?举个例子:比如我们在上一章里举过的一个 Stack 的例子。

    如果需要初始化和清理,"构造函数" 和 "析构函数" 就可以帮助我们完成。

    构造函数就类似于 Init,而析构函数就类似于 Destroy。

      没错,就是这么的爽!

    还是和以前一样,我们将先由浅入深地进行学习,我们先从最简单的 "构造函数" 开始讲起。

    Ⅱ. 构造函数(Constructor)

    0x00 引入:为什么要有构造函数?

    打开宇宙第一编辑器,一起敲一敲看看 ~

    为了能够更好地讲解,我们来写一个简单的日期类,通过日期类来讲解!

    💬 代码演示:Date.cpp

    #include class Date {
    public:
        void SetDate(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        void Print() {
            printf("%d-%d-%d\n", _year, _month, _day);
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    int main(void)
    {
        Date d1;
        d1.SetDate(2022, 3, 8);
        d1.Print();
        Date d2;
        d2.SetDate(2022, 3, 12);
        d2.Print();
        return 0;
    }

     对于 Date 类,我们可以通过我们写的成员函数 SetDate 给对象设置内容。

    但是每次创建对象都要调用这个 SetDate ,是不是太鸡儿烦了?

    ❓ 那有没有什么办法能在创建对象时,自动将我们要传递的内容放置进去呢?

     有!下面我们来隆重介绍一下 构造函数!

    0x01 构造函数的概念

     构造函数是一个特殊的成员函数:

    • 名字与类名相同(写起来简单又草率)
    • 创建类类型对象时由编译器自动调用(全自动工具人)
    • 能够保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。

       构造函数的意义:能够保证对象被 初始化 (init) 。

      构造函数是特殊的成员函数,主要任务是初始化,而不是开空间(虽然构造函数的名字叫构造)。

      0x02 构造函数的特性

       构造函数是特殊的成员函数,主要特征如下:

      • 构造函数的函数名和类名是相同的 (比如类名是 Date,构造函数名就是 Date)
      • 构造函数无返回值 (它不具有返回类型,因此不能直接返回值)
      • 构造函数支持重载 (仔细看下面的例子)
      • 会在对象实例化时自动调用对象定义出来

        比如下面的代码只需要 Date d1,就会自动调用构造,保证了对象一定是被初始化过的!

        "还有这种好事?!"

        这也太好了吧?我们直接来看看它是怎么用的! 

        💬 代码演示:构造函数的用法 (无参构造函数 和 带参构造函数)

        #include class Date {
        public:
            /* 无参构造函数 */
            Date() {
                _year = 0;
                _month = 1;
                _day = 1;
            }
            /* 带参构造函数 */
            Date(int year, int month, int day) {
                _year = year;
                _month = month;
                _day = day;
            }
            void Print() {
                printf("%d-%d-%d\n", _year, _month, _day);
            }
        private:
            int _year;
            int _month;
            int _day;
        };
        int main(void)
        {
            Date d1;   // 对象实例化,此时触发构造,调用无参构造函数
            d1.Print();
            Date d2(2022, 3, 9);   // 对象实例化,此时触发构造,调用带参构造函数
            d2.Print();
            return 0;
        }

        🚩 运行结果如下:

         🔑 解读:不给参数时就会调用 无参构造函数,给参数则会调用 带参构造函数。

        📌 注意事项:

        ① 构造函数是特殊的,不是常规的成员函数,不能直接调  。

        #include class Date {
        public:
            Date(int year = 1, int month = 0, int day = 0) {
                _year = year;
                _month = month;
                _day = day;
            }
        private:
            int _year;
            int _month;
            int _day;
        };
        int main(void)
        {
            Date d1;
            d1.Date(); // 不能这么去调,构造函数是特殊的,不是常规的成员函数!
            return 0;
        }

        🚩 运行结果:(报错)

        ② 如果通过无参构造函数创建对象,对象后面不用跟括号,否则就成了函数声明。

        #include class Date {
        public:
            Date(int year = 1, int month = 0, int day = 0) {
                _year = year;
                _month = month;
                _day = day;
            }
            void Print() {
                printf("%d-%d-%d\n", _year, _month, _day);
            }
        private:
            int _year;
            int _month;
            int _day;
        };
        int main(void)
        {
            //带参这么调:加括号(),在括号中加参数列表
            Date d2(2022, 3, 9);
            Date d3();   // 这样可以吗? 
                         // 既然代参的调用加括号,在括号中加参数列表。
                         // 那我不带参,可不可以加括号呢?
                         ❌ 仍然不可以。
                         // 这个对象实际上没有被定义出来,这里会报错。 
                         // 编译器不会识别,所以不传参数就老老实实地  
                         // Date d3; 不要 Date d3();   
            // 主要是编译器没法识别,所以这里记住不能这么干就行了。
            return 0;
        }

        ③ 这里如果调用带参构造函数,我们需要传递三个参数(这里我们没设缺省) 。

        ④ 如果你没有自己定义构造函数(类中未显式定义),C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。

        #include class Date {
        public:
            /* 如果用户显式定义了构造函数,编译器将不再生成
            Date(int year, int month, int day) {
                _year = year;
                _month = month;
                _day = day;
            }
            */
            void Print() {
                printf("%d-%d-%d\n", _year, _month, _day);
            }
        private:
            int _year;
            int _month;
            int _day;
        };
        int main(void)
        {
            Date d1;  // 这里调用的是默认生成的无参的构造函数
            d1.Print();
            return 0;
        }

        🚩 运行结果如下:

        🔑 没有定义构造函数,对象也可以创建成功,因此此处调用的是 编译器默认生成的构造函数。

        0x03 默认构造函数

         无参构造函数、全缺省构造函数、自动生成的构造函数都被称为 默认构造函数。

        并且 默认构造函数只能有一个!

        class Date {
        public:
            /* 全缺省构造函数 - 默认构造函数 */
            Date(int year = 1970, int month = 1, int day = 1) {
                _year = year;
                _month = month;
                _day = day;
            }
        private:
            int _year;
            int _month;
            int _day;
        };

        📌 注意事项:

        语法上无参和全缺省可以同时存在,但如果同时存在会引发二义性:

        #include class Date {
        public:
            Date() {
                _year = 1970;
                _month = 1;
                _day = 1;
            }
            Date(int year = 1970, int month = 1, int day = 1) {
                _year = year;
                _month = month;
                _day = day;
            }
            
        private:
            int _year;
            int _month;
            int _day;
        };
        int main(void)
        {
            Date d1; ❌
            return 0;
        }

        🚩 运行结果如下:(报错)

        🔑 解读:无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,虽然语法上允许它们们两个可以同时存在,但是如果有对象定义去调用就会报错。

          强烈推荐实现全缺省或者半缺省,因为真的很好用:

        #include class Date {
            public:
                /* 全缺省 */
                Date(int year = 0, int month = 1, int day = 1) {
                    _year = year;
                    _month = month;
                    _day = day;
                }
            
                void Print() {
                    printf("%d-%d-%d\n", _year, _month, _day);
                } 
            private:
                int _year;
                int _month;
                int _day;
        };
        int main(void)
        {
            Date d1; // 如果不传,就是缺省值
            Date d2(2022, 1, 15);
            Date d3(2009);
            Date d4(2012, 4);
            d1.Print();  // 0-1-1
            d2.Print();  // 2022-1-15
            d3.Print();  // 2009-1-1
            d4.Print();  // 2012-4-1
            return 0;
        }

        🚩 运行结果如下:

        0x04 构造函数的特性的测试

        通过刚才的讲解我们知道了任何一个类的默认构造函数,只有三种:

        • 无参的构造函数
        • 全缺省的构造函数
        • 我们不写,编译器自己生成的构造函数

          如果你没有自己定义构造函数(类中未显式定义),C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。

          💬 代码演示:让编译器自己生成一个

          #include class Date {
          public:
              // 让编译器自己生成一个
              void Print() {
                  printf("%d-%d-%d\n", _year, _month, _day);
              }
          private:
              int _year;
              int _month;
              int _day;
          };
          int main(void)
          {
              Date d1;  // 这里调用的是默认生成的无参的构造函数
              d1.Print();
              return 0;
          }

          🚩 运行结果如下:

            好了,我们来好好探讨探讨这个问题!

          在我们不是先构造函数的情况下,编译器生成的默认构造函数。

          "似乎这看起来没有什么鸟用啊,这不就是一堆随机值嘛……"

          d1 对象调用了编译器生成的默认函数,但 d1 对象 year / month / day 依旧是随机值,

          也就是说这里编译器生成的默认构造函数好像并没有什么卵用。

          🔑 解答:C++ 把类型分成内置类型(基本类型)和自定义类型。

          • 内置类型就是语法已经定义好的类型:如 int / char...
          • 自定义类型就是我们使用 class / struct / union / 自己定义的类型。

            观察下面的程序,你就会发现编译器生成默认的构造函数,会对自定类型成员 _t 调用的它的默认成员函数:

            #include using namespace std;
            class Time {
            public:
            	Time()
            	{
            		cout << "Time()" << endl;
            		_hour = 0;
            		_minute = 0;
            		_second = 0;
            	}
            private:
            	int _hour;
            	int _minute;
            	int _second;
            };
            class Date {
            private:
            	// 基本类型(内置类型)
            	int _year;
            	int _month;
            	int _day;
            	// 自定义类型
            	Time _t;
            };
            int main()
            {
            	Date d;
            	return 0;
            }

            🚩 运行结果如下: 

            💬 测试:对自定义类型处理,会调用默认构造函数(不用参数就可以调的函数)

            #includeusing namespace std;
            class A {
            public:
                // 默认构造函数(不用参数就可以调的)
                A() {
                    cout << " A() " << endl;
                    _a = 0;
                }
            private:
                int _a;
            };
            class Date {
            public:
            private:
                int _year;
                int _month;
                int _day;
                A _aa;   // 对自定义类型处理,此时会调用默认构造函数 A() {...}
            };
            int main(void)
            {
                Date d1;
                return 0;
            }

            🚩 运行结果如下:

            C++ 里面把类型分为两类:内置类型(基本类型)和 自定义类型。

            C++ 规定:我们不写 编译器默认生成构造函数对于内置类型的成员变量不做初始化处理。

            但是对于自定义类型的成员变量会去调用它的默认构造函数(不用参数就可以调的)初始化。

            如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!

            "你要写就写好了,要么就别写,不写我默认生成的能保底"

            💬 为了验证,这里我们故意写个带参的默认构造函数,让编译器不默认生成:

            #include using namespace std;
            class A {
            public:
                // 如果没有默认的构造函数,会报错。
                A(int a) {    // 故意给个参
                    cout << " A() " << endl;
                    _a = 0;
                }
            private:
                int _a;
            };
            class Date {
            public:
            private:
                // 如果没有默认构造函数就会报错
                int _year;
                int _month;
                int _day;
                A _aa;
            };
            int main(void)
            {
                Date d1;
                return 0;
            }

            🚩 运行结果如下(报错)

            💬 我们不写,让编译器默认生成一个:

            #includeusing namespace std;
            class A {
            public:
                // 让编译器默认生成
            private:
                int _a;
            };
            class Date {
            public:
            private:
                int _year;
                int _month;
                int _day;
                A _aa;
            };
            int main(void)
            {
                Date d1;
                return 0;
            }

             是随机值没错,但是这是一种对自定义类型的 "处理",这就是随机值的原因 。

             这里说个题外话,个人认为 C++里,我们不写构造函数编译器会默认生成的这个特性设计得不好(狗头保命)……因为没有对内置类型和自定义类型统一处理,不处理内置类型成员变量,只处理自定义类型成员变量。

            Ⅲ. 析构函数(Destructor)

            0x00 引入:专门 “擦屁股” 的析构函数

             通过前面构造函数的学习,我们知道了一个对象是怎么来的了,

            ❓ 那一个对象又是怎么没的呢?既然构造函数的本质是初始化,那清理的工作交给谁来干呢?

            当然是交给专门擦屁股的 —— 析构函数 (destructor) !

            以前我们玩数据结构的时候经常忘记调用 destroy 函数,但是现在我们有析构函数了!!!

            (非常炸裂…… 多么振奋人心啊!话不多说让我们开始讲解!!!)

            0x01 析构函数的概念

             析构函数与构造函数的功能相反。

            构造函数是特殊的成员函数,主要任务是初始化,而不是开空间;

            析构函数也一样,主要任务是清理,而不是做对象销毁的工作。

            (局部对象销毁工作是由编译器完成的)

            📚 概念:对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作。

            构造函数和析构函数的名词翻译趣谈

            首先,构造函数的英文是 Constructor,意思为建造者/制造商。析构函数是 Destructor,意思是破坏者/垃圾焚烧炉,诶,这里的垃圾焚烧炉,完成对象的资源清理工作,非常的合理啊。下面我们来看看 "构造函数" 和 "析构函数" 的其他翻译方式!

            [繁] 中国台湾地区将 Constructor 翻译为 "建構函式",表示建造和构成,Destructor 翻译为 "解構函式",意思应该是 "解除构造" 的意思。

            [韩] 韩语翻译的就有意思了,本人在韩国留学,这边的 Constructor 叫  생성자 (生成者) Destructor 叫 소멸자 (消灭者),非常的简单直白,但一开始在教材上读到 "消灭者" 的时候一直没想到说的是析构函数,感觉非常中二……  怎么不叫 종결자 (终结者) 呢?

            0x02 析构函数的特性

             构造函数是特殊的成员函数,主要特征如下:

            • 析构函数名是在类名前面加上字符 ~ 
            • 析构函数既没有参数也没有返回值(因为没有参数,所以也不会构成重载问题)
            • 一个类的析构函数有且仅有一个(如果不写系统会默认生成一个析构函数)
            • 析构函数在对象生命周期结束后,会自动调用。

              (和构造函数是对应的构造函数是在对象实例化的时候自动调用)

              💬 代码演示:为了演示自动调用,我们来让析构函数被调用时 "吱" 一声:

              #include using namespace std;
              class Date {
              public:
                  Date(int year = 1, int month = 0, int day = 0) {
                      _year = year;
                      _month = month;
                      _day = day;
                  }
                  void Print() {
                      printf("%d-%d-%d\n", _year, _month, _day);
                  }
                  ~Date() {
                      // Date 类没有资源需要清理,所以Date不实现析构函都是可以的
                      cout << "~Date() 吱~ " << endl;  // 测试一下,让他吱一声
                  }
              private:
                  int _year;
                  int _month;
                  int _day;
              };
              int main(void)
              {
                  Date d1;
                  Date d2(2022, 3, 9);
                  return 0;
              }

              🚩 运行结果如下:

              (这里的析构顺序问题我们下面会讲,不要着急)

              额,之前举得日期类的例子没法很好地展示析构函数的 "魅力" ……

              就像本段开头说情景,我们拿 Stack 来举个例子,这就很贴切了。

              我们知道,栈是需要 destroy 清理开辟的内存空间的。

               这里我们让析构函数来干这个活,简直美滋滋!

              💬 代码演示:析构函数的用法

              #include#includeusing namespace std;
              typedef int StackDataType;
              class Stack {
              public:
                  /* 构造函数 - StackInit */
                  Stack(int capacity = 4) {  // 这里只需要一个capacity就够了,默认给4(利用缺省参数)
                      _array = (StackDataType*)malloc(sizeof(StackDateType) * capacity);
                      if (_array == NULL) {
                          cout << "Malloc Failed!" << endl;
                          exit(-1);
                      }
                      _top = 0;
                      _capacity = capacity;
                  }
                  /* 析构函数 - StackDestroy */
                  ~Stack() {   // 这里就用的上析构函数了,我们需要清理开辟的内存空间(防止内存泄漏)
                      free(_array);
                      _array = nullptr;
                      _top = _capacity = 0;
                  }
              private:
                  int* _array;
                  size_t _top;
                  size_t _capacity;
              };
              int main(void)
              {
                  Stack s1;
                  Stack s2(20); // s2 栈 初始capacity给的是20(可以理解为"客制化")
                  return 0;
              }
              

              🔑 解读:我们在设置栈的构造函数时,定义容量 capacity 时利用缺省参数默认给个4的容量,这样用的时候默认就是4,如果不想要4可以自己传。

              如此一来,就可以保证了栈被定义出来就一定被初始化,用完后会自动销毁。以后就不会有忘记调用 destroy 而导致内存泄露的惨案了,这里的析构函数就可以充当销毁的作用。

              0x03 析构顺序问题

              💬 先看代码:这是我们刚才举的析构函数的例子

              class Date {
              public:
                  Date(int year = 1, int month = 0, int day = 0) {...}
                  void Print() {...}
                  ~Date() {
                      cout << "~Date() 吱~ " << endl;  // 测试一下,让他吱一声
                  }
              private: 
                  {...}
              };
              int main(void)
              {
                  Date d1;                    // 谁先析构?
                  Date d2(2022, 3, 9);        
                  return 0;
              }

              ❓ 问一个比较有意思的问题:这个例子中是先析构 s1 还是先析构 s2?

               

              或者这么问:打印结果的两个吱分别都是谁叫的?

              💡 答案:先析构 s2,再析构 s1 ;第一个吱是 s2 叫的,第二个吱是 s1 叫的。

               刚才例子的解析图上,我就写着第一个 吱是 d2 的了:

              因为析构的顺序在局部的栈中是相反的,栈帧销毁清理资源时 s2 先清理,然后再清理 s1!

              所以 先构造的后析构,后构造的先析构,所以是 d2 先吱。

              (不信的话可以去监视一下 this 观察下成员变量)

              【2023.9.27 更新】评论区对于此问题也有不少提问,这里我再做一个补充说明:

              这两个问题实际上都是在问析构顺序的,只要把这块知识点搞明白就会很好理解。

              0x04 析构函数的特性的测试

              又到了测试环节,上号!

              我们知道了,如果没写析构函数编译器会自动生成一个。

              那默认生成的析构函数会做什么事情呢?它会帮我们 destroy 嘛?

               hhh,哪有这种好事,不能什么都帮你做啊!

              我们刚才讲了(我们回顾下,串联一下知识点):

              📌 如果不自己写构造函数,让编译器自动生成,那么这个自动生成的 默认构造函数:

              • 对于 "内置类型" 的成员变量:不会做初始化处理。
              • 对于 "自定义类型" 的成员变量:会调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!

                 而我们的析构函数也是这样的,一个德行!

                📌 如果我们不自己写析构函数,让编译器自动生成,那么这个 默认析构函数:

                • 对于 "内置类型" 的成员变量:不作处理 (不会帮你清理的.)
                • 对于 "自定义类型" 的成员变量:会调用它对应的析构函数 (已经仁至义尽了) 。

                  " 编译器:哈哈哈,给你默认生成个用用就不错了,你都懒得写了,不要挑三拣四滴!"

                  💬 代码演示:

                  #include#includeusing namespace std;
                  typedef int StackDataType;
                  class Stack {
                  public:
                      Stack(int capacity = 4) {
                          _array = (StackDataType*)malloc(sizeof(int*) * capacity);
                          if (_array == NULL) {
                              cout << "Malloc Failed!" << endl;
                              exit(-1);
                          }
                          _top = 0;
                          _capacity = capacity;
                      }
                      // ~Stack() {
                      //     free(_array);
                      //     _array = nullptr;
                      //     _top = _capacity = 0;
                      // }
                  private:
                      int* _array;
                      size_t _top;
                      size_t _capacity;
                  };
                  int main(void)
                  {
                      Stack s1;
                      Stack s2(20);
                      return 0;
                  }

                  难道就不能帮我把这些事都干了吗?帮我都销毁掉不就好了?

                  不不不,举个最简单的例子,迭代器,析构的时候是不释放的,因为不需要他来管,

                  所以默认不对内置类型处理是正常的,万一误杀了怎么办,对吧。 

                  有人可能又要说了,这么一来默认生成的析构函数不就没有用了吗?

                  有用!他对内置类型的成员类型不作处理,会在一些情况下非常的有用!

                  比如说: 两个栈实现一个队列(LeetCode232) ,用 C++ 可以非常的爽。

                  💬 代码演示:自定义类型的成员变量调用它的析构函数

                  #include using namespace std;
                  class String {
                  public:
                  	String(const char* str = "jack") {
                  		_str = (char*)malloc(strlen(str) + 1);
                  		strcpy(_str, str);
                  	}
                  	~String() {
                  		cout << "~String()" << endl;
                  		free(_str);
                  	}
                  private:
                  	char* _str;
                  };
                  class Person {
                  private:
                  	String _name;
                  	int _age;
                  };
                  int main()
                  {
                  	Person p;
                  	return 0;
                  }
                  

                  🚩 运行结果如下:

                  Ⅳ.  拷贝构造函数(Copy Constructor)

                  0x00 引入:可以帮我拷贝吗?可以!

                  我们在创建对象的时候,能不能创建一个与某一个对象一模一样的新对象呢?

                  Date d1(2022, 3, 9);    
                  d1.Print();
                  Date d2(d1);    // 照着d1的模子做一个d2
                  d2.Print();

                  当然可以,这时我们就可以用拷贝构造函数。

                  (但是要警惕无穷递归,一定要加引用&,这个我们下面重点讲解!)

                  0x01 拷贝构造函数的概念

                  📚 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用 const 修饰),

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

                  0x02 拷贝构造函数的特性

                  它也是一个特殊的成员函数,所以他符合构造函数的一些特性:

                  ① 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。

                  ② 拷贝构造函数的参数只有一个,并且 必须要使用引用传参!

                        使用传值方式会引发无穷递归调用!

                  "拷贝构造函数的引用是必不可少的!"

                  类名(const 类名& 形参);

                  💬 代码演示:拷贝构造函数的用法

                  #include class Date {
                  public:
                      Date(int year = 0, int month = 1, int day = 1) {
                          _year = year;
                          _month = month;
                          _day = day;
                      }
                      /* Date d2(d1); */
                      Date(Date& d) {         // 这里要用引用,否则就会无穷递归下去
                          _year = d._year;
                          _month = d._month;
                          _day = d._day;
                      }
                      void Print() {
                          printf("%d-%d-%d\n", _year, _month, _day);
                      }
                  private:
                      int _year;
                      int _month;
                      int _day;
                  };
                  int main(void)
                  {
                      Date d1(2022, 3, 9);
                      Date d2(d1);          // 拷贝复制
                      // 看看拷贝成功没
                      d1.Print();
                      d2.Print();
                      return 0;
                  }

                  🚩 运行结果如下:

                  ❓ 为什么必须使用引用传参呢?

                  调用拷贝构造,需要先穿参数,传值传参又是一个拷贝构造。

                  调用拷贝构造,需要先穿参数,传值传参又是一个拷贝构造。

                  调用拷贝构造,需要先穿参数,传值传参又是一个拷贝构造。

                  ……

                  一直在传参这里出不去了,所以这个递归是一个无穷无尽的。

                  💬 我们来验证一下:

                  error: invalid constructor; you probably meant 'Date (const Date&)'

                  这里不是加不加 const 的问题,而是没有用引用导致的问题。

                  不用引用,他就会在传参那无线套娃递归,至于为什么我们继续往下看。

                  💬 拷贝构造函数加 const:如果函数内不需要改变,建议把 const 也给它加上!

                  class Date {
                  public:
                      Date(int year = 0, int month = 1, int day = 1) {
                          _year = year;
                          _month = month;
                          _day = day;
                      }
                      /* Date d2(d1); */
                      Date(const Date& d) {    // 如果内部不需要改变,建议加上const
                          _year = d._year;
                          _month = d._month;
                          _day = d._day;
                      }
                      void Print() {
                          printf("%d-%d-%d\n", _year, _month, _day);
                      }
                  private:
                      int _year;
                      int _month;
                      int _day;
                  };

                  第一个原因:怕出错,万一你一不小心写反了怎么办?

                  /* Date d2(d1); */
                  Date(Date& d) {
                      d._year = _year;
                      d._month = _month;
                      d._day = _day;
                  }

                  这样会产生一个很诡异的问题,这一个可以被编译出来的 BUG ,结果会变为随机值。

                  所以,这里加一个 const 就安全多了,这些错误就会被检查出来了。

                  第二个原因:以后再讲,因为涉及一些临时对象的概念。

                  🔺 反正,不想深究的话就记住:如果函数体内不需要改变,建议把 const 加上 就完事了。

                  "社会上的事情少打听,拷贝构造直接  X(const& x)  就行了"

                  0x03 关于默认生成的拷贝构造

                   这里比较特殊,我们单独领出来讲。 

                  📚 默认生成拷贝构造:

                  ① 内置类型的成员,会完成按字节序的拷贝(把每个字节依次拷贝过去)。

                  ② 自定义类型成员,会再调用它的拷贝构造。

                  💬 拷贝构造我们不写生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理的细节是不一样的,这个跟构造和析构是不一样的!

                  #includeusing namespace std;
                  class Date {
                      public:
                          Date(int year = 0, int month = 1, int day = 1) {
                              _year = year;
                              _month = month;
                              _day = day;
                          }
                          // Date(Date& d) {
                          //     _year = d._year;
                          //     _month = d._month;
                          //     _day = d._day;
                          // }
                          void Print() {
                              printf("%d-%d-%d\n", _year, _month, _day);
                          } 
                      
                      private:
                          int _year;
                          int _month;
                          int _day;
                  };
                  int main(void)
                  {
                      Date d1(2002, 4, 8);
                      // 拷贝复制
                      Date d2(d1);
                      // 没有写拷贝构造,但是也拷贝成功了
                      d1.Print();
                      d2.Print();
                      return 0;
                  }

                  🚩 运行结果如下:

                  🔑 他这和之前几个不同了,这个他还真给我解决了。

                  所以为什么要写拷贝构造?写他有什么意义?没有什么意义。

                   默认生成的一般就够用了!

                   当然,这并不意味着我们都不用写了,有些情况还是不可避免要写的

                  比如实现栈的时候,栈的结构问题,导致这里如果用默认的 拷贝构造,会翻车。

                  按字节把所有东西都拷过来会产生问题,如果 Stack st1 拷贝出另一个 Stack st2(st1) 

                  会导致他们都指向那块开辟的内存空间,导致他们指向的空间被析构两次,导致程序崩溃

                  然而问题不止这些……

                   其实这里的字节序拷贝是浅拷贝,下面几章我会详细讲一下深浅拷贝,这里的深拷贝和浅拷贝先做一个大概的了解。

                  🔺 总结:对于常见的类,比如日期类,默认生成的拷贝构造能用。但是对于栈这样的类,默认生成的拷贝构造不能用。

                  Ⅴ.  总结(构造&析构&拷贝构造)

                  默认成员函数有六只,本篇只介绍了三只,剩下的我们后面讲。

                  类和对象部分知识很重要,所以我们来做一个简单的总结 ~

                  0x00 构造函数 

                  初始化,在对象实例化时候自动调用,保证实例化对象一定被初始化。

                  构造函数是默认成员函数,我们不写编译器会自己生成一份,我们写了编译器就不会生成。

                  我们不写内置类型成员变量不处理。

                  对于内置类型成员变量不处理。

                  对于自定义类型的成员变量会调用它的默认构造函数。

                  // 我们需要自己实现构造函数
                  class Date {
                      int _year;
                      int _month;
                      int _day;
                  };
                          
                  // 我们不需要自己实现构造函数,默认生成的就可以
                  class MyQueue {
                      Stack _pushST;
                      Stack _popST;
                  };

                  0x01 析构函数

                   完成对象中自愿的清理。如果类对象需要资源清理,才需要自己实现析构函数。

                  析构函数在对象生命周期到了以后自动调用,如果你正确实现了析构函数,保证了类对象中的资源被清理。

                  什么时候生命周期到了?如果是局部变量,出了作用域。全局和静态变量,整个程序结束。

                  我们不写编译器会默认生成析构函数,我们实现了,编译器就不会实现了。

                  对于内置类型成员变量不处理。

                  对于自定义类型的成员变量会调用它的析构函数。

                  // 没有资源需要清理,不徐需要自己实现析构函数
                  class Date {
                      int _year;
                      int _month;
                      int _day;
                  };
                  // 需要自己实现析构函数,清理资源。
                  class Stack {
                      int* _a;
                      int  _top;
                      int  _capacity;
                  };

                  0x02 拷贝构造

                  使用同类型的对象去初始化实例对象。

                  参数必须是引用!不然会导致无穷递归。

                  如果我们不实现,编译器会默认生成一份默认的拷贝构造函数。

                  默认生成的拷贝构造:

                  ① 内置类型完成按子继续的值拷贝。 —— 浅拷贝

                  ② 自定义类型的成员变量,会去调用它的拷贝构造。

                  // 不需要自己实现,默认生成的拷贝构造,完成浅拷贝就能满足需求
                  class Date {
                      int _year;
                      int _month;
                      int _day;
                  };
                  // 需要自己实现,因为默认生成的浅拷贝不能满足需求。
                  // 我们需要自己实现深拷贝的拷贝构造,深拷贝我们后面会用专门的章节去讲解。        
                  class Stack {
                      int* _a;
                      int  _top;
                      int  _capacity;
                  };
                  #include using namespace std;
                  class Date {
                  public:
                      Date(int year = 1, int month = 0, int day = 0) {
                          _year = year;
                          _month = month;
                          _day = day;
                      }
                      void Print() {
                          printf("%d-%d-%d\n", _year, _month, _day);
                      }
                      ~Date() {
                          cout << "&Date()" << endl;
                      }
                  private:
                      int _year;
                      int _month;
                      int _day;
                  };
                  int main(void)
                  {
                      Date d1;
                      d1.Print();
                      Date d2(2002);
                      d2.Print();
                      Date d3(2022, 3);
                      d3.Print();
                      Date d4(2022, 3, 9);
                      d4.Print();
                      return 0;
                  }

                   🚩 运行结果如下:

                   ​​​

                  📌 [ 笔者 ]   王亦优
                  📃 [ 更新 ]   2022.3.15 | 2023.9.27(重制)
                  ❌ [ 勘误 ]   Star丶北辰:拿栈举例时malloc空间 sizeof 有误(已修正)
                  📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
                                本人也很想知道这些错误,恳望读者批评指正!

                  📜 参考资料 

                  Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

                  百度百科[EB/OL]. []. https://baike.baidu.com/.

                  比特科技. C++[EB/OL]. 2021[2021.8.31]