C++利用构造函数和析构函数,完成对象的初始化和清理工作。
对象的初始化和清理工作,是编译器强制我们要做的事情,如果我们不提供构造函数和析构函数,编译器会提供3个函数:
默认无参构造函数
默认拷贝构造函数
默认析构函数
构造函数:在对象初始化时,对对象的成员属性赋初始值。构造函数由编译器自动调用,不用手动调用。
拷贝构造函数:在对象初始化时,将一个已有的对象的所有成员属性拷贝到这个被创建的对象上。拷贝构造函数由编译器自动调用,不用手动调用。
析构函数:在对象销毁前系统自动调用,执行一些清理工作。
类名()
{
}
构造函数没有返回值,也不用写void。
构造函数的函数名跟类名相同。
构造函数可以有参数,因此可以发生重载。
构造函数会在程序在创建对象的时候,被自动调用,不用手动调用,而且创建该对象只会调用一次。
类名(const 类名 &obj)
{
}
拷贝构造函数没有返回值,也不用写void。
拷贝构造函数的函数名和类名相同。
拷贝构造函数的参数是固定的,并且只有一个参数,这个参数为:const 类名 &obj。
拷贝构造函数被调用的时机
在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用。
将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用。
函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。
~类名()
{
}
析构函数没有返回值,也不用写void。
析构函数的函数名跟类名相同,并且在函数名之前加上~。
析构函数不可以有参数,因此不能发生重载。
析构函数在对象销毁前会自动被调用,不用手动调用,而且只会调用一次。
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;
}
括号法
//括号法
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);
注意:
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;
}
拷贝构造函数被调用的时机:
在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用。
将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用。
函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。
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; //怪物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行)析构函数被调用
浅拷贝与深拷贝
浅拷贝:对成员属性进行简单的赋值操作的拷贝构造函数,编译器提供的默认的拷贝构造函数就是浅拷贝。
深拷贝:对于可以简单赋值的成员属性进行简单的赋值操作,对于在堆区的成员属性,在堆区重新申请空间,进行拷贝操作。
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; //怪物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;
}
浅拷贝时出错打印输出:代码中将浅拷贝实现打开,深拷贝实现注释掉
深拷贝时,程序正确输出:
对象的初始化和清理工作,是编译器强制我们要做的事情:
如果我们不提供构造函数和析构函数,编译器会提供3个函数:
默认无参构造函数,函数体是空实现
默认拷贝构造函数,函数体是值拷贝(浅拷贝)
默认析构函数,函数体是空实现
如果我们提供了有参构造函数,编译器会提供2个函数
默认拷贝构造函数,函数体是值拷贝(浅拷贝)
默认析构函数,函数体是空实现
如果我们提供了拷贝构造函数,那么编译器只提供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++面向对象编程之二:构造函数、拷贝构造函数、析构函数,先写到这。