zoukankan      html  css  js  c++  java
  • 构造函数与析构函数二

    • 推荐在构造函数初始化列表中进行初始化
      什么是初始化列表呢?还是以上节中的时钟类的构造来说明:

      这是原来的构造函数的写法,而这里改成用构造函数初始化列表来进行成员的初始化,如下:

      测试一下代码:

      编译运行:
    • 构造函数的执行分为两个阶段:初始化段和普通计算段
      实际上对于这种形式不属于初始化了:

      因为这个对象空间已经生成好了,也就是hour_、second_已经存在了,然后再将参数赋值给它们,所以是属于赋值阶段,也就是普通计算段,就比如:
      int n = 10;这是初始化,

      int n;
      n = 10;这是赋值操作。
      所以建议成员的初始化放到初始化列表中。

     用代码进行说明:

    #include <iostream>
    
    using namespace std;
    
    class Object {
    public:
        Object() {
            cout<<"Object"<<endl;
        }
        ~Object() {
            cout<<"~Object"<<endl;
        }
    };
    
    class Container {
    public:
        Container() {
            cout<<"Container"<<endl;
        }
    
        ~Container() {
            cout<<"~Container"<<endl;
        }
    private:
        Object obj;
    };
    
    
    int main(void) {
        Container c;
    
        return 0;
    }

    看一下打印结果:

    从中可以发现析构的顺序跟初始化的顺序是相关的,下面继续修改代码:

    #include <iostream>
    
    using namespace std;
    
    class Object {
    public:
        Object(int num) : num_(num) {//这里只提供一个带参的构造函数,而没提供默认构造
            cout<<"Object"<<endl;
        }
        ~Object() {
            cout<<"~Object"<<endl;
        }
    private:
        int num_;
    };
    
    class Container {
    public:
        Container() {
            cout<<"Container"<<endl;
        }
    
        ~Container() {
            cout<<"~Container"<<endl;
        }
    private:
        Object obj;
    };
    
    
    int main(void) {
        Container c;
    
        return 0;
    }

    这时编译运行:

    因为在生成Container之前需要生成Object,而在初始化Object成员默认调用的默认构造函数,可以这么解决它:

    #include <iostream>
    
    using namespace std;
    
    class Object {
    public:
        Object(int num) : num_(num) {
            cout<<"Object"<<endl;
        }
        ~Object() {
            cout<<"~Object"<<endl;
        }
    private:
        int num_;
    };
    
    class Container {
    public:
        Container():obj(0) {
            cout<<"Container"<<endl;
        }
    
        ~Container() {
            cout<<"~Container"<<endl;
        }
    private:
        Object obj;
    };
    
    
    int main(void) {
        Container c;
    
        return 0;
    }

    那如果有多个Object成员,初始化顺序又会是怎样呢?

    #include <iostream>
    
    using namespace std;
    
    class Object {
    public:
        Object(int num) : num_(num) {
            cout<<"Object "<<num_<<"..."<<endl;
        }
        ~Object() {
            cout<<"~Object "<<num_<<"..."<<endl;
        }
    private:
        int num_;
    };
    
    class Container {
    public:
        Container(int obj1, int obj2):obj(obj1),obj2(obj2) {
            cout<<"Container"<<endl;
        }
    
        ~Container() {
            cout<<"~Container"<<endl;
        }
    private:
        Object obj;
        Object obj2;
    };
    
    
    int main(void) {
        Container c(10, 20);
    
        return 0;
    }

    编译运行:

    那如果这样修改呢?

    编译运行发现结果还是一样,下面继续修改:

    编译运行:

    以上说明构造的顺序跟定义的顺序有关,而跟它在列表中的构造顺序是无关的。

    新建一个cpp文件来说明:

    编译运行:

    常量的初始化必须在初始化列表中进行,所以改造代码如下:

    编译运行:

    另外还有一个也只能在初始化列表进行------对象成员(对象所对应的类没有默认构造函数)。

    将其打印出来论证一下:

    看效果:

    如果希望针对所有对象都是常量那怎么办呢?只能通过枚举来实现,具体如下:

    #include <iostream>
    using namespace std;
    
    class Object {
    public:
        enum E_TYPE {//定义一个枚举类型常量
            TYPE_A = 100,
            TYPE_B = 200
        };
    public:
        Object(int num=0) : num_(num),kNum(num),refNum(num_) {
            //kNum = 100;        ERROR,const成员的初始化只能够在构造函数初始化列表中进行
            cout<<"Object "<<num_<<"..."<<endl;
        }
        ~Object() {
            cout<<"~Object "<<num_<<"..."<<endl;
        }
    
        void displayKNum() {
            cout<<"knum="<<kNum<<endl;
        }
    
    private:
        int num_;
        const int kNum;
        int& refNum;
    };
    
    
    int main(void) {
        Object obj1(10);
        obj1.displayKNum();
        Object obj2(20);
        obj2.displayKNum();
        //打印不同对象的枚举,看输出:
        cout<<"TYPE="<<obj1.TYPE_A<<endl;
        cout<<"TYPE="<<Object::TYPE_A<<endl;
        cout<<"TYPE="<<obj2.TYPE_A<<endl;
        return 0;
    }

    编译运行:

    所以说如果想要一个常量适用于任何一个对象,则必须用枚举常量,而不能用const常量!

    • 功能:使用一个已经存在的对象来初始化一个新的同一类型的对象。
      下面用代码来说明,还是借用之前实验中的Test类:
      Test.h:
      #ifndef _TEST_H_
      #define _TEST_H_
      
      class Test
      {
      public:
          // 如果类不提供任何一个构造函数,系统将为我们提供一个不带参数的
          // 默认的构造函数
          Test();
          /*explicit*/ Test(int num);
          void Display();
      
          Test& operator=(const Test& other);//等号运算符重载
      
          ~Test();
      private:
          int num_;
      };
      #endif // _TEST_H_

      Test.cpp:

      #include "Test.h"
      #include <iostream>
      using namespace std;
      
      // 不带参数的构造函数称为默认构造函数
      Test::Test()
      {
          num_ = 0;
          cout<<"Initializing Default"<<endl;
      }
      
      Test::Test(int num)
      {
          num_ = num;
          cout<<"Initializing "<<num_<<endl;
      }
      
      Test::~Test()
      {
          cout<<"Destroy "<<num_<<endl;
      }
      
      void Test::Display()
      {
          cout<<"num="<<num_<<endl;
      }
      
      Test& Test::operator=(const Test& other) {
          cout<<"Test::operator="<<endl;
          if(this == &other)
              return *this;
          num_ = other.num_;
          return *this;
      }


      编译运行:

      接下来用现有对象来初始化一个新对象,这里就会涉及到拷贝构造函数了,如下:

      编译运行:

    • 声明:只有一个参数并且参数为该类对象的引用。
      为了论证确确实实调用了拷贝构造函数,这里重写一下:



      编译运行:

      另外有个等价的写法:
    • 如果类中没有说明拷贝构造函数,则系统自动生成一个缺省复制构造函数,作为该类的公有成员
      对于拷贝构造函数的参数是对象的引用,那如果不是对象的引用又会怎样呢?


      编译运行:

      直接报错了,思考一下为什么这种情况会编译不过呢?当实参要初始化other对象,则会调用Test的拷贝构造函数,如果other不是引用,那它是值传递,会分配内存将实参初始化形参other,又要调用拷贝构造函数,就会出现递归的死循环,而引用不会分配内存,它是跟实参共享内存的,不会再构造一个对象出来,也就不会调用拷贝构造函数了,关于这点下面会有说明。

    • 当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。理所当然也调用拷贝构造函数。
      下面用代码来说明下:

      编译运行:


      编译运行:

      从结果来看,调用testFun2中并未调用拷贝构造函数,因为传递的是引用,也就是形参不会构造一个对象分配一个内存出来,跟实现是共享内一块内存,另外在参数传递时尽量用引用,可以减少内存的复制
    • 当函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。
      具体用代码来明说,下面的实验会比较绕,需细细体会:

      编译运行:


      编译运行:

      那下面再来对其进行改变,看结果:


      解释:对于这种情况由于是对t2进行初始化,而不是赋值,所以就不会调用=运算符重载函数了,另外为啥没有及时销毁呢?可以这样理解:临时对象变为了有名对象,所以说当调用testFun3的临时不会马上消失,因为有t2对象接管了,那下面这种情况呢?

      结果跟上面一样,临时对象也不会立刻被销毁,上面的细微差别需要细细体会,下面继续研究:

      编译运行:

      它跟“Test t2 = testFun3(t);”是同样的输出结果,但是含义是不一样的:
      “Test t2 = testFun3(t);”:表示调用函数返回对象的时候要调用拷贝构造函数。
      "Test t2 = testFun4(t);"表示当调用函数返回对象,并未调用拷贝构造函数,返回一个对象的引用,然后将这个结象初始化到t2,这里才调用拷贝构造函数。
      关于上面这点,其实可以用下面的实验来进一步理解:

      编译运行:

      从结果来看是没有调用拷贝构造函数,从上面的实验来看情况比较多,其实只要记住一点:如果一个对象初始化另外一个对象,就会调用拷贝构造函数。

     

     先来编写一个字符串类来进一步说明它:

    String.h:

    #ifndef _STRING_H
    #define _STRING_H
    
    class String
    {
    public:
        String(char* str="");
        ~String();
    
        void display();
    private:
        char* str_;
    };
    
    #endif//_STRING_H

    String.cpp:

    #include "String.h"
    #include <string.h>
    #include <iostream>
    using namespace std;
    
    String::String(char* str/* ="" */) {
        int len = strlen(str) + 1;
        str_ = new char[len];
        memset(str_,0, len);
        strcpy(str_, str);
    }
    
    String::~String()
    {
        delete[] str_;
    }
    
    void String::display() {
        cout<<str_<<endl;
    }

    这里来使用一下它:

    #include "String.h"
    
    int main(void) {
        String s1("AAA");
        s1.display();
        return 0;
    }

    编译运行:

    这个代码很简单,先说明一个细节:

    接下来继续编写,会引出此次要学的东东:

    用之前学的经验我们可以很容易知道这个会触发String的拷贝构造函数,因为是对象初始化对象,那看看运行结果吧:

    居然报错了,这是为什么呢?这是由于调用的是系统默认的拷贝构造函数,而默认拷贝构造函数实施的是浅拷贝,也就是类似于:s2.str_=s1.str_,但是str_是个指针,也就是s2中的str_并未申请新的空间,而是跟s1.str_指向的是同一个空间,一个空间被两个指针同时使用是不允许的,所以就报错了,而要解决此问题就必须采用深拷贝来解决,其实有个java经验的对于深浅拷贝应该不难理解,所以下面来实现自己的拷贝构造函数实现深拷贝来解决此问题:

    那深拷贝如何写呢?其实跟第一个参数的构造很类似,如下:

    编译运行:

    这时就没报错了,这就是所谓的深拷贝,对于这两个函数的代码基本一样,应该将代码抽取一下:

    再次运行结果当然也一样喽,但是代码要变得精简多了,这也是一个好习惯的点滴实施。

    【提示】:关于两指针共享同一块内存的操作也是可以实现的,这个在之后会进行学习,先记录抛出来~

     其实对于目前这个程序还是有些问题的,继续编写测试代码如下:

    这个代码的结果应该很容易想出来,如下:

    但是再进行下面这一步:

    报错了,这是为啥呢?这是因为这句话调用的是等号运算符,而系统提供的默认等号运算符实施的也是浅拷贝:s3.str_=s2.str_,所以要解决这个问题,需要实现自己的等号运算符,如下:

    再编译运行,看问题解决没?

    成功解决~所以说赋值也得实现深拷贝。

    这是一个新的话题,对于世界上有些对象是独一无二的,对于这样的对象是不应该去拷贝的,那如果要禁止拷贝,该怎么做呢?其实很简单:

    因为拷贝构造函数默认是公有的。

    编译运行:

    直接就编译出错了,对于错语能在编译期间报出来的就应该提到编译期间。知道这点既可,还是先将代码还原,因为还得接下来的实验。

     

     

    现在看不出什么意思,下面debug一下来看下p和e的地址是否一样:

    另外它会自动调用取地址函数,也很好理解。

    接下来还有一个默认成员:const Empty* operator&() const;,下面也来看下:

    编译运行:

    说到空类,另外问一下,这个空类的大小是多小呢?是不是0个字节呢?

    编译运行:

     并非0个字节,而是1个字节,其实也很好理解,如果没有一点大小,还能怎么去生成对象呢?

  • 相关阅读:
    stylelint 安装配置
    使用 jest 测试 react component 的配置,踩坑。
    互联网媒体类型 MIME Type
    react-router 父子路由同时要接收 params 的写法
    fixed 相对于父容器定位
    react 点击空白处隐藏弹出层
    canvas 使用 isPointInPath() 判断鼠标位置是否在绘制的元素上
    JavaScript 缓存基本原理
    简单说明 Virtual DOM 为啥快
    通过阻止 touchstart 事件,解决移动端的 BUG
  • 原文地址:https://www.cnblogs.com/webor2006/p/5084247.html
Copyright © 2011-2022 走看看