C++面向对象编程之二:构造函数、拷贝构造函数、析构函数
迪丽瓦拉
2024-05-30 23:46:00
0

构造函数和析构函数

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 
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 
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 
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; //怪物idint 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 
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; //怪物idint *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 
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 
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++面向对象编程之二:构造函数、拷贝构造函数、析构函数,先写到这。

相关内容