C++拷贝构造函数的细节
构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。
每个类只有一个析构函数 和一个赋值函数 ,但可以有多个构造函数 (包含一个拷贝构造函数 ,其它的称为普通构造函数 )。对于任意一个类 A ,如果不想编写上述函数, C++ 编译器将自动为 A 产生四个缺省的函数(也只是在需要的时候才会产生) ,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?
原因如下:
( 1 )如果使用 “ 缺省的无参数构造函数 ” 和 “ 缺省的析构函数 ” ,等于放弃了自主 “ 初始化 ” 和 “ 清除 ” 的机会, C++发明人 Stroustrup 的好心好意白费了。
( 2 ) “ 缺省的拷贝构造函数 ” 和 “ 缺省的赋值函数 ” 均采用 “ 位拷贝 ” 而非 “ 值拷贝 ” 的方式来实现,倘若类中含有指针变量,这两个函数注定将出错 。
对于那些没有吃够苦头的 C++ 程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。
什么时候系统会为我们定义无参数构造函数
答案:
首先,编译器“可能”为我们定义一个无参的构造函数,一个拷贝构造函数
当自己没有定义任何构造函数(包括无参数,有参数的普通构造函数,还有拷贝构造函数)的时候
系统才定义一个 无参数的空函数体的 构造函数
如果没有定义一个拷贝构造函数,系统也会为我们定义一个拷贝构造函数,完成简单的位(bit)复制
第2:
拷贝构造函数的深度复制,类中包括指针变量,然后指向相同的一块内存区域,可能由于一个对象释放了内存,另一个不知道已经释放,极有可能导致程序崩溃
VC6.0++ 可以通过改变配置,可以暂时运行程序,而不导致程序运行崩溃的临时解决方法,但是其实还是我们的程序不健壮,具体设置方法可以看下面的参考
第3:
C++拷贝构造函数的几个问题
class CopyConstructorTest
{
public:
CopyConstructorTest() {}
CopyConstructorTest(const CopyConstructorTest& cct) {}
};
问题1:const 是不是必须的?
不是必须的,但是你省去了const,下面的代码就会出错:
const CopyConstructorTest cct1;
CopyConstructorTest cct2(cct1);
所以,如果希望const对象可以被拷贝,那么写上const,否则不写,那么记住const对象不能再出现副本,有时这是有用的。
问题2:&是不是必须的,YES OR NO?
答案是YES!否则,呵呵,会出现一个编译错误:
error C2652: 'CopyConstructorTest' : illegal copy constructor: first parameter must not be a 'CopyConstructorTest'
为什么拷贝构造函数必须为引用传递,不能是值传递?
拷贝构造函数的标准写法如下:
class Base{
public:
Base(){}
Base(const Base &b){..}
//
}
上述写法见得最多,甚至你认为理所当然。
那么如果我们不写成引用传递呢,而是值传递,那么会怎样?
class Base
{
public:
Base(){}
Base(const Base b){ }
//
}编译出错:error C2652: 'Base' : illegal copy constructor: first parameter must not be a 'Base'
事实上,你可以从这个小小的问题认真搞清楚2件事:
1) 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。其实,个人认为不应该叫这些constructor(default constructor, copy constructor....)为构造函数,更佳的名字应该是"初始化函数"(见我的另一片文章).
2) 参数传递过程到底发生了什么?
将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!
i)值传递:
对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用
ii)引用传递:
无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).
上述1) 2)回答了为什么拷贝构造函数使用值传递会产生无限递归调用...
3). 如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,而且在一般的情况下运行的也很好。但是在遇到类有指针数据成员时就出现问题了:因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。
上述3)回答了在类中有指针数据成员时,拷贝构造函数使用值传递等于白显式定义了拷贝构造函数,因为默认的拷贝构造函数就是这么干的...
一 拷贝构造函数是C++最基础的概念之一,大家自认为对拷贝构造函数了解么?请大家先回答一下三个问题:
1. 以下函数哪个是拷贝构造函数,为什么?
- X::X(const X&);
- X::X(X);
- X::X(X&, int a=1);
- X::X(X&, int a=1,int b=2);
2. 一个类中可以存在多于一个的拷贝构造函数吗?
3. 写出以下程序段的输出结果, 并说明为什么? 如果你都能回答无误的话,那么你已经对拷贝构造函数有了相当的了解。
- #include <iostream>
#include <string>
using namespace std;
class X {
public:
template<typename T>
X( T& ) { std::cout << "X( T& )" << std::endl; }
template<typename T>
X& operator=( T& ) { std::cout << "X& operator=( T& )" << std::endl; }
};
int main() {
int m =5, n=10.5;
X a(m); //X( T& )
X b(n); //X( T& )
X c = a; //VC6这里只调用系统默认的拷贝构造函数,没有调用X( T& )
X d(b); //VC6这里只调用系统默认的拷贝构造函数,没有调用X( T& )
return 0;
}
解答如下:
1. 对于一个类X,如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
- X::X(const X&); //是拷贝构造函数
- X::X(X&, int=1); //是拷贝构造函数
- X::X(X&, int a=1,int b=2); //是拷贝构造函数
2.类中可以存在超过一个拷贝构造函数,
- class X {
- public:
- X(const X&);
- X(X&); // OK
- };
注意,如果一个类中只存在一个参数为X&的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
- class X {
- public:
- X();
- X(X&);
- };
- const X cx;
- X x = cx; // error
如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数.
这个默认的参数可能为X::X(const X&)或X::X(X&),由编译器根据上下文决定选择哪一个.
默认拷贝构造函数的行为如下:
默认的拷贝构造函数执行的顺序与其他用户定义的构造函数相同,执行先父类后子类的构造.
拷贝构造函数对类中每一个数据成员执行成员拷贝(memberwise Copy)的动作.
a)如果数据成员为某一个类的实例,那么调用此类的拷贝构造函数.
b)如果数据成员是一个数组,对数组的每一个执行按位拷贝.
c)如果数据成员是一个数量,如int,double,那么调用系统内建的赋值运算符对其进行赋值.
3. 拷贝构造函数不能由成员函数模版生成.
- #include <iostream>
#include <string>
using namespace std;
class X {
public:
template<typename T>
X( T& ) { std::cout << "X( T& )" << std::endl; }
template<typename T>
X& operator=( T& ) { std::cout << "X& operator=( T& )" << std::endl; }
};
int main() {
int m =5, n=10.5;
X a(m);//X( T& ) ,这里会调用拷贝构造函数
X b(n); //X( T& ),这里会调用拷贝构造函数
//下面2个,VC6这里只调用系统默认的拷贝构造函数,没有调用自己的拷贝构造函数 - //linux下如何?需要再测试
- X c = a;
X d(b);
return 0;
}
原因很简单, 成员函数模版并不改变语言的规则,而语言的规则说,如果程序需要一个拷贝构造函数而你没有声明它,那么编译器会为你自动生成一个. 所以成员函数模版并不会阻止编译器生成拷贝构造函数, 赋值运算符重载也遵循同样的规则.(参见Effective C++ 3edition, Item45)
#include "stdio.h"
#include <iostream>
#include <string>
using namespace std;
/*
Test1(int i)
Test1(const Test1& test)
Test1(const Test1& test)
Test1(const Test1& test)
Test1& operator = (const Test1& test)
Test2(int num)
Test2(const Test2& test)
Test2( Test2& test)
Test2()
Test2& operator = (const Test2& test)
*/
struct Test1
{
Test1() { cout<<"Test1()"<<endl; }
Test1(int i) { id = i; cout<<"Test1(int i)"<<endl; }
Test1(const Test1& test)
{
id = test.id;
cout<<"Test1(const Test1& test)"<<endl;
}
Test1& operator = (const Test1& test)
{
cout<<"Test1& operator = (const Test1& test)"<<endl;
if(this == &test)
return *this;
id = test.id;
return *this;
}
int id;
};
class Test2
{
public:
Test2(){ m_pChar = NULL; cout<<"Test2()"<<endl; }
Test2(char *pChar) { m_pChar = pChar;cout<<"Test2(char *pChar) "<<endl; }
Test2(int num)
{
cout<<" Test2(int num) "<<endl;
m_pChar = new char[num];
for(int i = 0; i< num; ++i)
m_pChar[i] = 'a';
m_pChar[num-1] = '\0';
}
Test2(const Test2& test)
{
cout<<" Test2(const Test2& test) "<<endl;
char *pCharT = m_pChar;
m_pChar = new char[strlen(test.m_pChar)];
strcpy(m_pChar, test.m_pChar);
if(!pCharT)
delete []pCharT;
}
//注意这里没有const关键字,说明const可以参与函数重载的区分
Test2(Test2 &test)
{
cout<<" Test2( Test2& test) "<<endl;
char *pCharT = m_pChar;
m_pChar = new char[strlen(test.m_pChar)];
strcpy(m_pChar, test.m_pChar);
if(!pCharT)
delete []pCharT;
}
Test2& operator = (const Test2& test)
{
cout<<" Test2& operator = (const Test2& test) "<<endl;
if(this == &test) //取别名的地址,相同则为同一个对象
return *this; //*this 表示指针中的内容,就是当前对象首地址
char *pCharT = m_pChar; //定义一个临时变量
m_pChar = new char[strlen(test.m_pChar)];
strcpy(m_pChar, test.m_pChar);
if(!pCharT)
delete []pCharT; //删除char *的方法 ,不要忘记[]
return *this;
}
private:
char *m_pChar;
};
int main(int argc, char* argv[])
{
//const 普通对象和const对象指针 必须在定义的时候进行初始化
const Test1 ts(1); // Test1()
const Test1 ts2(ts); //Test(const Test1& test)
const Test1 ts3 = ts; //Test(const Test1& test)
Test1 ts4 = ts;
ts4 = ts; //Test1& operator = (const Test1& test)
cout<<endl;
/*
Test2 t4 ; //调用默认的构造函数
Test2 t4 = t ; //定义的同时 并且 用另外一个对象 初始化则调用复制(拷贝)构造函数,没有调用 operator = 运算符函数函数
t4 = t //定义之后的赋值调用operator = 运算符函数,没有调用 复制(拷贝)构造函数
Test2 t4 = t ; 这个方式:定义且赋值 如果我们自己没有定义复制(拷贝)构造函数且只定义了operator = 函数,编译器将自己我们定义一个复制(拷贝)构造函数
*/
const Test2 t(5); //
Test2 t2(t); //由于参数为const,所以调用Test2(const Test2& test)
Test2 t3 = t2; //由于参数为非const,所以调用 Test2( Test2& test)
Test2 t4 ;
//如果未定义operator = 函数,系统也不会调用自定义的复制(拷贝)构造函数.
//会调用编译器自己的默认赋值函数 operator = ()
t4 = t; //Test2& operator = (const Test2& test)
return 0;
}