问题源于看剑指offer上的一道面试题,题目如下:
class A{
private:
int value;
public:
A(int n){ value = n; }
A(A other){ value = other.value; }
void Print() {cout<<value<<endl; }
};
int main(void)
{
A a = 10;
A b = a;
b.Print();
return 0;
}
对上面这段代码进行分析编译运行的结果是:
A、编译错误 B、编译成功,运行时程序崩溃 C、编译运行正常,输出10
答案:A、编译错误。复制构造函数A(Aother)传入的参数是A的一个实例。由于是传值参数,我们把形参复制到实参会调用复制构造函数。因此如果允许复制构造函数传值,就会在复制构造函数内调用复制构造函数,就会形成永无休止的递归调用从而导致栈溢出。
敲黑板划重点:C++的标准不允许复制构造函数传值参数,只能将构造函数修改为A(const A& other),也就是把传值参数改为常量引用。(注意:传指针也是不可以的,只能改为引用)。
一开始不理解为什么会无限循环递归下去这个过程。于是在网上查了一下。用下面这个例子来深入理解一下这个过程。
#include<iostream>
usingnamespace std;
class CExample
{
private:
int m_nTest;
public:
CExample(int x) : m_nTest(x) //带参数构造函数
{
cout << "constructor withargument"<<endl;
}
// 拷贝构造函数,参数中的const不是严格必须的,但引用符号是必须的
CExample(const CExample & ex) //拷贝构造函数
{
m_nTest = ex.m_nTest;
cout << "copyconstructor"<<endl;
}
CExample& operator = (const CExample&ex) //赋值函数(赋值运算符重载)
{
cout << "assignmentoperator"<<endl;
m_nTest = ex.m_nTest;
return *this;
}
void myTestFunc(CExample ex)
{
}
};
int main(void)
{
CExample aaa(2);
CExample bbb(3);
bbb = aaa;
CExample ccc = aaa;
bbb.myTestFunc(aaa);
return 0;
}
这段代码运行后输出如下:
constructorwith argument
constructorwith argument
assignmentoperator
copyconstructor
copyconstructor
程序分解分析:
第一个constructorwith argument,来源于CExample aaa(2);因为要对aaa实例化并初始化,所以要调用带参数的构造函数
第二个constructorwith argument,来源于CExample bbb(3);同理。
assignmentoperator,来源于bbb =aaa;
第一个copyconstructor,来源于CExample ccc = aaa;这个和上面为什么有区别呢?原因是因为bbb对象已经实例化了,不需要构造,所以将aaa赋值给bbb,只会调用赋值函数。但是ccc还没有实例化,因此调用的是拷贝构造函数,构造出ccc,而不是赋值函数。
第二个copyconstructor,来源于bbb.myTestFunc(aaa);。过程是这样:首先将aaa作为参数传递给bbb.myTestFunc(CExample ex),即CExample ex = aaa;和上面一样,所以还是拷贝赋值函数。
如果拷贝构造函数是传值而不是传引用,当调用ccc.CExample(aaa)时,aaa作为参数传值给ccc.CExample(CExample ex),即CExample ex = aaa,因为ex没有被初始化,所以会继续调用拷贝构造函数,接下来是构造ex,也就是ex.CExample(aaa),必然又会有aaa传给CExample(CExample ex),即CExample ex = aaa;那么又会触发拷贝构造函数,这样就无限递归下去了。
所以,拷贝构造函数的参数使用引用类型不是为了减少一次内存拷贝,而是避免拷贝构造函数无限制的递归下去。
下面这几种情况下会调用拷贝构造函数
(1)显式或隐式地用同类型的一个对象来初始化另外一个对象。如上例中的CExample ccc = aaa;
(2)作为实参传递给一个函数。如上例中的bbb.myTestFunc(aaa);
(3)在函数体内返回一个对象时,也会调用返回值类型的拷贝构造函数
(4)初始化序列容器中的元素时。比如vector<string> svec(5),string的缺省构造函数和拷贝构造函数都会被调用。
(5)用列表的方式初始化数组元素时。string a[] = {string(“hello”),string(“world”)};会调用string的拷贝构造函数。