C++面向对象编程之二:构造函数、拷贝构造函数、析构函数

构造函数和析构函数

C++利用构造函数和析构函数,完成对象的初始化和清理工作。

对象的初始化和清理工作,是编译器强制我们要做的事情,如果我们不提供构造函数和析构函数,编译器会提供3个函数:

  1. 默认无参构造函数

  1. 默认拷贝构造函数

  1. 默认析构函数

构造函数:在对象初始化时,对对象的成员属性赋初始值。构造函数由编译器自动调用,不用手动调用。

拷贝构造函数:在对象初始化时,将一个已有的对象的所有成员属性拷贝到这个被创建的对象上。拷贝构造函数由编译器自动调用,不用手动调用。

析构函数:在对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:
类名()
{
}
  1. 构造函数没有返回值,也不用写void。

  1. 构造函数的函数名跟类名相同。

  1. 构造函数可以有参数,因此可以发生重载。

  1. 构造函数会在程序在创建对象的时候,被自动调用,不用手动调用,而且创建该对象只会调用一次。

拷贝构造函数语法:
类名(const 类名 &obj)
{
}
  1. 拷贝构造函数没有返回值,也不用写void。

  1. 拷贝构造函数的函数名和类名相同。

  1. 拷贝构造函数的参数是固定的,并且只有一个参数,这个参数为:const 类名 &obj。

  1. 拷贝构造函数被调用的时机

  1. 在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用。

  1. 将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用。

  1. 函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。

析构函数语法:
~类名()
{
}
  1. 析构函数没有返回值,也不用写void。

  1. 析构函数的函数名跟类名相同,并且在函数名之前加上~。

  1. 析构函数不可以有参数,因此不能发生重载。

  1. 析构函数在对象销毁前会自动被调用,不用手动调用,而且只会调用一次。

example:设计一个怪物类,并测试打印无参构造函数,有参构造函数,拷贝构造函数,析构函数被调用的时机

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster()
    {
        cout << "Monster()无参构造函数被调用" << endl;
    }
    Monster(const Monster &m)
    {
        cout << "Monster(const Monster &m)拷贝构造函数被调用" << endl;
    }
    Monster(const int monsterId)
    {
        m_monsterId = monsterId;
        cout << "Monster(const int monsterId)有参构造函数被调用" << endl;
    }
    ~Monster()
    {
        cout << "~Monster()析构函数被调用" << endl;
    }
    private:
    int m_monsterId; //怪物id
};

int main(int argc, char *argv[])
{
    Monster m1; //无参构造函数被调用
    Monster m2(10001); //有参构造函数被调用
    Monster m3(m2); //拷贝构造函数被调用
    
    return 0;
}

对象被创建的三种方法
  1. 括号法

//括号法
Monster m1; //注意:不是 Monster m1(); 写成这样子编译器会不知道这个是创建一个对象,还是函数声明
Monster m2(10001); //有参构造函数被调用
Monster m3(m2); //拷贝构造函数被调用
  1. 等号法

//等号法
Monster m4; //注意:不是Monster m4 = Monster();写成这样子相当于手动调用无参构造函数,
            //但构造函数是没有返回值的,这时m4 = void; 并没有创建一个对象
Monster m5 = Monster(10001); //有参构造函数被调用
Monster m6 = Monster(m5); //拷贝构造函数被调用
Monster(10002); //匿名对象,当前行执行完毕,系统会立即回收掉这个匿名对象(即这时析构函数会被调用)
//Monster(m5); //错误:不要利用拷贝构造函数初始化匿名对象,
               //编译器会认为:Monster(m5); == Monster m5;
  1. 隐式等号法

//隐式等号法
Monster m7 = 10001; //相当于:Monster m7 = Monster(10001);
Monster m8 = m7; //相当于:Monster m8 = Monster(m7);

注意:

Monster m1; //注意:不是 Monster m1(); 写成这样子编译器会不知道这个是创建一个对象,还是函数声明

Monster m4; //注意:不是Monster m4 = Monster(); 写成这样子相当于手动调用无参构造函数,

//但构造函数是没有返回值的,这时m4 = void; 并没有创建一个对象

Monster(10002); //匿名对象,当前行执行完毕,系统会立即回收掉这个匿名对象(即这时析构函数会被调用)

//Monster(m5); //错误:不要利用拷贝构造函数初始化匿名对象,编译器会认为:Monster(m5); == Monster m5;

example:验证对象被创建的三种方法

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster()
    {
        cout << "Monster()无参构造函数被调用" << endl;
    }
    Monster(const Monster &m)
    {
        cout << "Monster(const Monster &m)拷贝构造函数被调用" << endl;
    }
    Monster(const int monsterId)
    {
        m_monsterId = monsterId;
        cout << "Monster(const int monsterId)有参构造函数被调用" << endl;
    }
    ~Monster()
    {
        cout << "~Monster()析构函数被调用" << endl;
    }
    private:
    int m_monsterId; //怪物id
};

int main(int argc, char *argv[])
{
    //括号法
    Monster m1; //注意:不是 Monster m1(); 写成这样子编译器会不知道这个是创建一个对象,还是函数声明
    Monster m2(10001); //有参构造函数被调用
    Monster m3(m2); //拷贝构造函数被调用

    //等号法
    Monster m4; //注意:不是Monster m4 = Monster(); 写成这样子相当于手动调用无参构造函数,
                //但构造函数是没有返回值的,这时m4 = void; 并没有创建一个对象
    Monster m5 = Monster(10001); //有参构造函数被调用
    Monster m6 = Monster(m5); //拷贝构造函数被调用
    Monster(10002); //匿名对象,当前行执行完毕,系统会立即回收掉这个匿名对象(即这时析构函数会被调用)
    //Monster(m5); //错误:不要利用拷贝构造函数初始化匿名对象,编译器会认为:Monster(m5); == Monster m5;

    //隐式等号法
    Monster m7 = 10001; //相当于:Monster m7 = Monster(10001);
    Monster m8 = m7; //相当于:Monster m8 = Monster(m7);

    return 0;
}
拷贝构造函数
  1. 拷贝构造函数被调用的时机:

  1. 在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用。

  1. 将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用。

  1. 函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。

example:验证拷贝构造函数被调用的时机

#include <iostream>
using namespace std;

int line = 0;

class Monster
{
    public:
    Monster()
    {
        m_monsterId = 0;
        m_blood = 0;
        line++;
        cout << line << "行:Monster()无参构造函数被调用" << endl;
    }
    Monster(const int monsterId, const int blood)
    {
        m_monsterId = monsterId;
        m_blood = blood;
        line++;
        cout << line << "行:Monster(const int monsterId, const int blood)有参构造函数被调用" << endl;
    }
    Monster(const Monster &m)
    {
        m_monsterId = m.m_monsterId;
        m_blood = m.m_blood;
        line++;
        cout << line << "行:Monster(const Monster &m)拷贝构造函数被调用" << endl;
    }
    ~Monster()
    {
        line++;
        cout << line << "行:~Monster()析构函数被调用" << endl;
    }

    void setMonsterId(const int monsterId)
    {
        m_monsterId = monsterId;
    }
    int getMonsterId()
    {
        return m_monsterId;
    }

    void setBlood(const int blood)
    {
        m_blood = blood;
    }
    int getBlood()
    {
        return m_blood;
    }

    private:
    int m_monsterId; //怪物id
    int m_blood; //血量
};

void subMonsterBlood(Monster m, const int val)
{
    int blood = m.getBlood() - val;
    if (blood < 0)
        blood = 0;
    
    m.setBlood(blood);
}

Monster getTempMonster(const int monsterId, const int blood)
{
    Monster m(monsterId, blood);
    return m;
}

int main(int argc, char *argv[])
{
    Monster m1(10001, 1000); //有参构造函数被调用
    Monster m2(m1); //在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用

    subMonsterBlood(m1, 500); //将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用

    Monster m3 = getTempMonster(10002, 15000); //函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝
                                               //构造函数,拷贝一个临时的对象进行返回
    
    return 0;
}

g++ monster_copy_constructor.cpp -o monster_copy_constructor 编译链接生成可执行文件

根据打印结果进行代码分析:

main函数内:
Monster m1(10001, 1000); 
(打印第1行)有参构造函数被调用

Monster m2(m1);
在创建对象时,用一个已有对象来初始化这个被创建的对象时,(打印第2行)拷贝构造函数被调用

subMonsterBlood(m1, 500); 
将一个已有的对象,作为函数的实参,进行值传递时,此时,(打印第3行)拷贝构造函数被调用,拷
贝所有成员属性给形参,这个函数调用执行结束后,形参被析构,所以(打印第4行)析构函数被调用

Monster m3 = getTempMonster(10002, 15000);
调用getTempMonster函数,这个函数体内执行:Monster m(monsterId, blood);
所以(打印第5行)有参构造函数被调用。按照getTempMonster函数的返回值为对象,并且将一个
对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。但程序并没有打印
拷贝构造函数被调用,这是为什么呢?


main函数执行结束
m3被释放,所以(打印第6行)析构函数被调用
m2被释放,所以(打印第7行)析构函数被调用
m1被释放,所以(打印第8行)析构函数被调用

程序调用getTempMonster函数返回一个对象,程序并没有打印拷贝构造函数被调用,这是为什么呢?

其原因是:RVO(return value optimization),被G++进行值返回时优化了,具体的RVO的相关技术,可以百度。

我们可以将RVO优化关闭,可以对g++增加选项-fno-elide-constructors,重新编绎之后

g++ monster_copy_constructor.cpp -fno-elide-constructors -o monster_copy_constructor

接下来我们再根据打印结果进行代码分析:

main函数内:
Monster m1(10001, 1000); 
(打印第1行)有参构造函数被调用

Monster m2(m1);
在创建对象时,用一个已有对象来初始化这个被创建的对象时,(打印第2行)拷贝构造函数被调用

subMonsterBlood(m1, 500); 
将一个已有的对象,作为函数的实参,进行值传递时,此时,(打印第3行)拷贝构造函数被调用,拷
贝所有成员属性给形参,这个函数调用执行结束后,形参被析构,所以(打印第4行)析构函数被调用

Monster m3 = getTempMonster(10002, 15000);
调用getTempMonster函数,这个函数体内执行:Monster m(monsterId, blood);
所以(打印第5行)有参构造函数被调用。getTempMonster函数的返回值为对象,并且将一个
对象返回时,其实这时编译器会(打印第6行)调用拷贝构造函数创建一个临时的对象进行返回。
此时,getTempMonster函数执行结束,函数体内创建的临时对象m会被释放,所以
(打印第7行)析构函数被调用
回到main函数,将返回的临时的对象利用隐式等号法赋值给m3,相当于执行:
Monster m3 = Monster(temp);所以所以(打印第8行)拷贝构造函数被调用
当这句代码执行结束后,临时对象temp被释放,所以(打印第9行)析构函数被调用

main函数执行结束
m3被释放,所以(打印第10行)析构函数被调用
m2被释放,所以(打印第11行)析构函数被调用
m1被释放,所以(打印第12行)析构函数被调用
  1. 浅拷贝与深拷贝

浅拷贝:对成员属性进行简单的赋值操作的拷贝构造函数,编译器提供的默认的拷贝构造函数就是浅拷贝。

深拷贝:对于可以简单赋值的成员属性进行简单的赋值操作,对于在堆区的成员属性,在堆区重新申请空间,进行拷贝操作。

example:验证浅拷贝会导致程序崩掉的情况,以及应该用深拷贝进行避免因浅拷贝出现的问题

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster()
    {
        m_monsterId = 0;
        mp_blood = new int(0);
    }

    Monster(const int monsterId, const int blood)
    {
        m_monsterId = monsterId;
        mp_blood = new int(blood);
    }

    /*浅拷贝*/
    // Monster(const Monster &m)
    // {
    //     m_monsterId = m.m_monsterId;
    //     mp_blood = m.mp_blood; //浅拷贝,正确的做法:用深拷贝,mp_blood这个成员变量是在堆中申请的空间,
    //                            //我们应该在堆中重新申请空间,进行拷贝操作
    // }
    
    /*深拷贝*/
    Monster(const Monster &m)
    {
        m_monsterId = m.m_monsterId;
        mp_blood = new int(*m.mp_blood); //深拷贝,mp_blood这个成员变量是在堆中申请的空间,我们在堆中
                                         //重新申请空间,进行拷贝操作
    }

    ~Monster()
    {
        if (mp_blood != NULL) //如果用浅拷贝,会出现mp_blood空间多次被重复释放的,导致程序崩掉
        {
            delete mp_blood;
            mp_blood = NULL;
        }
    }

    void print_monster_info()
    {
        cout << "怪物id = " << m_monsterId << ",怪物血量 = " << *mp_blood << endl; 
    }

    private:
    int m_monsterId; //怪物id
    int *mp_blood; //血量
};

int main(int argc, char *argv[])
{
    Monster m1(10001, 1000);
    m1.print_monster_info();

    Monster m2(m1);
    m2.print_monster_info();
    
    return 0;
}

浅拷贝时出错打印输出:代码中将浅拷贝实现打开,深拷贝实现注释掉

深拷贝时,程序正确输出:

对象的初始化和清理工作,是编译器强制我们要做的事情:

  1. 如果我们不提供构造函数和析构函数,编译器会提供3个函数:

  1. 默认无参构造函数,函数体是空实现

  1. 默认拷贝构造函数,函数体是值拷贝(浅拷贝)

  1. 默认析构函数,函数体是空实现

  1. 如果我们提供了有参构造函数,编译器会提供2个函数

  1. 默认拷贝构造函数,函数体是值拷贝(浅拷贝)

  1. 默认析构函数,函数体是空实现

  1. 如果我们提供了拷贝构造函数,那么编译器只提供1个函数

  1. 默认析构函数,函数体是空实现

example:验证我们提供了有参构造函数,编译器不会再提供默认无参构造函数

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster(const int monsterId)
    {
        m_monsterId = monsterId;
    }

    void print_monster_info()
    {
        cout << "怪物id = " << m_monsterId << endl;
    }
    private:
    int m_monsterId;
};

int main(int argc, char *argv[])
{
    //Monster m1; //错误:Monster类只提供了有参构造函数,那么编译器就不会提供默认无参构造函数了
    Monster m2(10001);
    Monster m3(m2); //正确:Monster类只提供了有参构造函数,那么编译器就会提供默认拷贝构造函数
    m3.print_monster_info();
    return 0;
}

example:验证我们提供了拷贝构造函数,编译器就不再提供默认无参构造函数

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster(const Monster &m)
    {
        m_monsterId = m.m_monsterId;
    }

    void print_monster_info()
    {
        cout << "怪物id = " << m_monsterId << endl;
    }
    private:
    int m_monsterId;
};

int main(int argc, char *argv[])
{
    //Monster m1; //错误:Monster类只提供了拷贝构造函数,那么编译器就不会提供默认无参构造函数了
    //m1.print_monster_info();

    return 0;
}

好了,关于C++面向对象编程之二:构造函数、拷贝构造函数、析构函数,先写到这。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值