类类型的拷贝构造函数与析构函数,成员中含有动态内存指针时的处理,C++11标准
- 以下讨论的符号约定
- 得出以下结论的实验代码(建议先看后面的内容)
- tips:
- 什么是拷贝构造
- 为什么要自定义析构函数
- 为什么要自定义拷贝构造函数
- 拷贝构造函数与析构函数的联系
- 成员是普通指针的情况
- 自定义的析构函数该怎么写
- 自定义拷贝构造函数该怎么写
以下讨论的符号约定
假设定义类类型
class A{
成员a
成员b
成员c//指向动态内存的指针
成员d//普通指针(指向静态内存)
};
及一个A的变量 e
~~~c
A e;
下面讨论的是通过e构造新的A的变量f的一些相关问题
得出以下结论的实验代码(建议先看后面的内容)
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
A();
A(const A& l):a(l.a),b(l.b),c(l.c),d(l.d){
c = new int(*l.c);
}
public:
int a;
string b;
int* c;//动态内存成员
//int* c= new int;这种形式也可以
int* d;//普通指针成员
~A(){
delete c;
}
};
A::A()
{
c = new int;//c的定义为int* c或int* c= new int时都可以这么写
}
//A::A(){}//仅当c的定义为int* c= new int时可以这么写
int main( int argc, char** argv )
{
for(int i= 1;i != 2;++i)
{
int x = 2;
int y = 8;
A e;
e.a =5;
e.b ="dhsu";
*e.c =y;
e.d =&x;
A f(e) ;
cout <<"e: "<<e.a<<" "<<e.b<<" "<<*e.c<<" "<<*e.d<<endl;
cout <<"f: "<<f.a<<" "<<f.b<<" "<<*f.c<<" "<<*f.d<<endl;
e.a =8;
e.b ="kkkk";
*e.c = 3;
*e.d =4;
cout <<"e: "<<e.a<<" "<<e.b<<" "<<*e.c<<" "<<*e.d<<endl;
cout <<"f: "<<f.a<<" "<<f.b<<" "<<*f.c<<" "<<*f.d<<endl;
}
return 0;
}
tips:
1, new开辟的内存称为动态内存
2, delete 的作用是释放动态(new)内存,delete静态(非new)内存是未定义的行为.
3,同一块动态内存只能delete一次,指向同一动态内存的多个指针,当一个指针被delete后,其他指针将无效,调用时将发生未知错误
什么是拷贝构造
拷贝构造是指当采用A f(e);或者A f=e;的形式借助已有的e构造新的变量f时发生的构造行为.这种构造行为将通过拷贝构造函数(合成的或自己定义的)完成.
注意拷贝构造函数以及其他构造函数都是在构造时调用的,就是说在A f…;这一行命令中调用的,其他位置不会发生.
1,将一个对象做为实参传给函数的非引用形参.
2,以及一个返回类型为非引用类型的函数返回一个对象.
3,以及对标准库容器调用insert以及push.
以上三点会调用拷贝构造函数,但本质上也是在A f…;这一行命令中调用的.
<<c++ primer>>还描述了其他不常用的情况,没有细究,但本质上应该都是A f…;这一命令行中才会调用.
拷贝构造:
A f(e);
或 A f=e;
如果代码写成A f; f=e;A f; f(e);则不会调用拷贝构造函数
A f; 调用的是A()形式的构造函数(默认或者自定义的)
f=e; 调用的是运算符=,默认的或是自定义的
A f;调用的是A()形式的构造函数(默认或者自定义的)
f(e);以上调用的是A(A)形式的函数,很像拷贝构造函数,但却不是,而且都不是构造函数.(此处可能有误,没有细究)
为什么要自定义析构函数
当类类型的成员是指向动态内存的指针时,编译器自动合成(以下简称:合成的)析构函数不能释放该动态内存.因此需要自己定义析构函数,显式的释放动态内存(使用delete)
为什么要自定义拷贝构造函数
若类类型的成员中有的是指向动态内存的指针,借助已有的e构造新的变量f时,编译器自动合成(以下简称:合成的)拷贝构造函数,会将f和e中相对应的动态内存指针成员指向同一个内存,而不是为f中的指向动态内存的指针开辟新的成员.
很多时候我们希望两者的动态内存是各自独有的,因此要自定义拷贝构造函数实现这个目的.
拷贝构造函数与析构函数的联系
当类类型的成员中有的是指向动态内存的指针时,为了释放其内存,必然要自己定义析构函数,但是如果采用编译器自己合成的拷贝构造函数的话,e和f的动态内存成员指向的内存是同一个,当e或f内存释放后,另一方的相应成员指向的内存也就被释放了,因此会发生错误.
为保证不发生此类错误,e和f的动态内存成员指向的内存不能是同一个,所以要采用自定义的拷贝构造函数
这也就是<<c++ primer>>中说”如果定义了析构函数,那么几乎一定要定义拷贝构造函数”的原因,几乎之外的例外是,你的代码中不会用到拷贝构造.
总结一下:
逻辑就是,因为有动态内存成员,所以要自定义析构函数,所以要自定义拷贝构造函数.
成员是普通指针的情况
以上一系列工作都是因为类类型有成员是指向动态内存的指针,那我们很自然的会好奇,如果含有int* pt 这样的普通指针成员时,会怎样?
结果是整洁的.
对于普通指针,无论在是否使用默认的还是定义的拷贝构造函数,都会共享内存,无论使用默认的还是定义的析构函数都不能释放其内存(其以普通指针的形式释放内存,因此不用担心内存泄漏,但要记得会共享内存)
这里的逻辑是:
因为普通指针会自动且恰当的解决内存管理问题,所以就不需要析构,后面的一些列麻烦自然也就不存在了
那当我们不希望e和f的普通指针成员共享内存时,要怎么做呢,答案是:不要用拷贝构造!
自定义的析构函数该怎么写
只谈怎么释放动态内存指针成员的内存,方法是显然的.直接在析构函数函数体内delete掉就可以了.
~A()
{
delete c;
}
自定义拷贝构造函数该怎么写
形参要为该类类型的const 引用,例如A(const s& l ).
定义的拷贝构造函数中,对于动态分配内存的成员,应当在函数体中为其显式的开辟内存,然后赋值,即
A(const A& l):a(l.a),b(l.b){
c = new int(*l.c);
}
而如果采用
A(const A& l):a(l.a),b(l.b),(c(l.c)) {}
或者
A(const A& l):a(l.a),b(l.b){
c =l.c;
}
则与默认拷贝构造函数没有差别,动态分配内存的成员将共享同一块内存.
正确形式:成员c不会共用内存
A(const A& l):a(l.a),b(l.b),d(l.d)
{
c = new int(*l.c);
}
错误形式1:成员c会共用内存
A(const A& l):a(l.a),b(l.b),(c(l.c)),d(l.d)
{
}
错误形式2:成员c会共用内存
A(const A& l):a(l.a),b(l.b),d(l.d)
{
c =l.c ;
}