构造函数与析构函数二

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

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

    测试一下代码:

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

    因为这个对象空间已经生成好了,也就是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个字节,其实也很好理解,如果没有一点大小,还能怎么去生成对象呢?

posted on 2015-12-28 22:29  cexo  阅读(272)  评论(0编辑  收藏  举报

导航