一.什么是拷贝构造函数?
1.1 概念
同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或称拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员是共用的(只有一份拷贝)。
在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为拷贝构造函数( Copy Constructor)。
拷贝构造函数的参数必须采用引用类型,但并不限制为const,一般普遍的会加上const限制。如果以类对象作为参数传递到拷贝构造函数,会引起无穷递归。
1.2 代码示例
代码示例如下:
#include <iostream>
using namespace std;
class CStudent
{
public:
CStudent(int age = 0,int score = 0);
~CStudent();
//拷贝构造函数
CStudent(const CStudent &stu);
private:
int age;
int score;
};
CStudent::CStudent(int age,int score)
{
cout<<"Constructor!"<<endl;
this->age = age;
this->score = score;
}
CStudent::~CStudent()
{
cout<<"Desconstructor!"<<endl;
}
CStudent::CStudent(const CStudent &stu)
{
cout<<"Copy constuctor!"<<endl;
this->age = stu.age;
this->score = stu.score;
}
二.如何实现?
2.1 缺省拷贝构造函数
2.1.1 概念
如果类中没有给出定义,系统会自动提供缺省拷贝构造函数。
缺省的拷贝构造函数会按成员语义,依次拷贝每个类成员,亦称为缺省的按成员初始化。
按成员作拷贝是通过依次拷贝每个数据成员实现的,而不是对整个类对象按位拷贝。
2.1.2 代码示例
示例代码如下:
#include <iostream>
using namespace std;
class CStudent
{
public:
CStudent(int age = 0,int score = 0);
~CStudent();
void print_info(void);
private:
int age;
int score;
};
CStudent::CStudent(int age,int score)
{
cout<<"Constructor! "<<this<<endl;
this->age = age;
this->score = score;
}
CStudent::~CStudent()
{
cout<<"Desconstructor! "<<this<<endl;
}
void CStudent::print_info(void)
{
cout<<"age("<<this<<"): "<<age<<endl;
cout<<"score("<<this<<"): "<<score<<endl;
}
int main(int argc, char** argv)
{
CStudent stu1(8,90);
CStudent stu2(stu1);
stu2.print_info();
return 0;
}
运行结果如下图所示。
由上图可知:
(1)只调用了一次普通构造函数,用来构造对象stu1。表明,在构造stu2时调用了一个缺省的构造函数,这个函数就是拷贝构造函数。
(2)对象stu2的所有数据成员被初始化为stu1对应数据成员的值。
(3)最后,调用了两次析构函数,用于析构stu1和stu2。
2.2 自定义拷贝构造函数
2.2.1 概念
通常按成员语义支持已经足够。但在某些情况下,它对类与对象的安全性和处理的正确性还不够,这时就要求类的设计者提供特殊的拷贝构造函数定义。
2.2.2 代码示例
示例代码如下:
#include <iostream>
using namespace std;
class CStudent
{
public:
CStudent(int age = 0,int score = 0);
~CStudent();
//拷贝构造函数
CStudent(const CStudent &stu);
void print_info(void);
private:
int age;
int score;
};
CStudent::CStudent(int age,int score)
{
cout<<"Constructor!"<<endl;
this->age = age;
this->score = score;
}
CStudent::~CStudent()
{
cout<<"Desconstructor!"<<endl;
}
CStudent::CStudent(const CStudent &stu)
{
cout<<"Copy constuctor!"<<endl;
this->age = stu.age;
this->score = stu.score;
}
void CStudent::print_info(void)
{
cout<<"age: "<<age<<endl;
cout<<"score: "<<score<<endl;
}
int main(int argc, char** argv)
{
CStudent stu1(8,90);
CStudent stu2(stu1);
stu2.print_info();
return 0;
}
运行结果如下图所示。
由上图可知:
(1)构造stu2对象时,调用了一次自定义的拷贝构造函数。
(2)关注一下自定义构造函数代码,发现在函数域内可通过引用对象访问私有数据成员age和score。
从逻辑上讲,每个对象有自己的成员函数,访问同类其他对象的私有数据成员应通过该对象的公有函数,不能直接访问。但在物理上只有一个成员函数拷贝,所以直接访问是合理的。
即,C++有个原则:类的成员函数可以访问私有数据成员。
CStudent::CStudent(const CStudent &stu)
{
cout<<"Copy constuctor!"<<endl;
this->age = stu.age;
this->score = stu.score;
}
三.何时调用?
3.1 用对象初始化对象
以下两种形式都是用已存在的对象初始化对象:
CStudent stu1(8,90);
CStudent stu2(stu1);
或者
CStudent stu2 = stu1;
以上两种形式是等价的,只是写法上不同。
3.2 给函数传递类的对象参数
当函数的形参是类的对象时, 一旦调用函数,要在内存新建立一个局部对象,并把实参拷贝到新的对象中。
代码示例(部分)如下:
void func(CStudent stu)
{
cout<<"func"<<endl;
}
int main(int argc, char** argv)
{
CStudent stu1(8,90);
func(stu1);
return 0;
}
运行结果如下图所示。
由上图可知。调用func函数时,会调用拷贝构造函数构造一个临时对象传给func。
3.3 函数返回类的对象(部分编译器)
很多资料提到:如果函数的返回值是类的对象,那么函数执行完成后,返回调用者时会调用拷贝构造函数。其实这不严谨。
有些编译器在函数返回类的对象时,不会调用拷贝构造函数。下面单独一节详细分析。
四.函数返回类的对象但不调用拷贝构造函数
本次实验使用64位TDM-GCC 4.9.2编译器。
4.1 示例代码
#include <iostream>
using namespace std;
class CStudent
{
public:
CStudent(int age = 0,int score = 0);
~CStudent();
//拷贝构造函数
CStudent(const CStudent &stu);
void print_info(void);
private:
int age;
int score;
};
CStudent::CStudent(int age,int score)
{
cout<<"Constructor!"<<endl;
this->age = age;
this->score = score;
}
CStudent::~CStudent()
{
cout<<"Desconstructor!"<<endl;
}
CStudent::CStudent(const CStudent &stu)
{
cout<<"Copy constuctor!"<<endl;
this->age = stu.age;
this->score = stu.score;
}
void CStudent::print_info(void)
{
cout<<"age: "<<age<<endl;
cout<<"score: "<<score<<endl;
}
CStudent func(void)
{
CStudent tmp(11,88);
return tmp;
}
int main(int argc, char** argv)
{
CStudent stu1(8,90);
CStudent stu2;
stu2 = func();
stu2.print_info();
return 0;
}
4.2 运行结果
如下图所示。
由下图可知:
(1)func函数的返回值是类的对象,但并没有调用拷贝构造函数。
(2)从stu2打印的信息来看,func函数中创建的tmp对象,的确“赋值”给了stu2。这怎么理解?下面看看汇编代码。
4.3 汇编代码
汇编代码中r8d是指r8寄存器的低32位。
4.3.1 func函数汇编代码
完整的汇编代码如下:
push %rbp
mov %rsp,%rbp
sub $0x20,%rsp
mov %rcx,0x10(%rbp) //rcx存储了对象tmp的地址
mov $0x58,%r8d //r8d的低32位初始化为88
mov $0xb,%edx //edx初始化为11
mov 0x10(%rbp),%rcx //即是tmp对象地址
callq 0x401530 <CStudent::CStudent(int, int)>
nop
mov 0x10(%rbp),%rax
add $0x20,%rsp
pop %rbp
retq
如上图中的注释,func函数里的对象tmp的地址是由调用者main函数传入的,即tmp对象是在main函数的堆栈里存储,而不是在func函数的堆栈里。
4.3.2 构造函数汇编代码
CStudent::CStudent(int, int)函数的完整汇编代码如下:
push %rbp
mov %rsp,%rbp
sub $0x20,%rsp
mov %rcx,0x10(%rbp)//rcx存储了对象tmp的地址
mov %edx,0x18(%rbp) //初始化tmp.score的值为11
mov %r8d,0x20(%rbp) //初始化tmp.age的值为88
lea 0x86ab6(%rip),%rdx # 0x488000
mov 0x8b17f(%rip),%rcx # 0x48c6d0 <.refptr._ZSt4cout>
callq 0x46ee10 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
mov 0x8b183(%rip),%rdx # 0x48c6e0 <.refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_>
mov %rax,%rcx
callq 0x44d500 <_ZNSolsEPFRSoS_E>
mov 0x10(%rbp),%rax
mov 0x18(%rbp),%edx
mov %edx,(%rax)
mov 0x10(%rbp),%rax
mov 0x20(%rbp),%edx
mov %edx,0x4(%rax)
add $0x20,%rsp
pop %rbp
retq
retq
注意第4~5行代码的注释。构造函数里,初始化了tmp对象的数据成员。
4.3.3 main函数汇编代码
main函数的完整汇编代码如下:
push %rbp
push %rbx
sub $0x58,%rsp
lea 0x80(%rsp),%rbp
mov %ecx,-0x10(%rbp)
mov %rdx,-0x8(%rbp)
callq 0x40e950 <__main>
lea -0x50(%rbp),%rax //堆栈偏移0x50的空间,分配给对象stu1.这里rax存储了stu1的地址
mov $0x5a,%r8d //r8的低32位初始化为90
mov $0x8,%edx //edx寄存器初始化为8
mov %rax,%rcx //传递stu1的地址给构造函数
callq 0x401530 <CStudent::CStudent(int, int)>
lea -0x60(%rbp),%rax //堆栈偏移0x60的空间,分配给对象stu2.这里rax存储了stu2的地址
mov $0x0,%r8d
mov $0x0,%edx
mov %rax,%rcx //传递stu2的地址给构造函数
callq 0x401530 <CStudent::CStudent(int, int)>
lea -0x40(%rbp),%rax //堆栈偏移0x40的空间,分配给了一个临时对象,暂时命名为m_tmp.这里rax存储了m_tmp的地址
mov %rax,%rcx //传递m_tmp的地址给func函数
callq 0x401685 <func()> //func函数里的tmp对象直接使用了main函数创建的m_tmp
mov -0x40(%rbp),%rax
mov %rax,-0x60(%rbp) //将m_tmp赋值给stu2
lea -0x40(%rbp),%rax
mov %rax,%rcx
callq 0x40157e <CStudent::~CStudent()> //析构m_tmp
lea -0x60(%rbp),%rax
mov %rax,%rcx
callq 0x401606 <CStudent::print_info()>
mov $0x0,%ebx
lea -0x60(%rbp),%rax
mov %rax,%rcx
callq 0x40157e <CStudent::~CStudent()>
lea -0x50(%rbp),%rax
mov %rax,%rcx
callq 0x40157e <CStudent::~CStudent()>
mov %ebx,%eax
jmp 0x401770 <main(int, char**)+192>
mov %rax,%rbx
lea -0x60(%rbp),%rax
mov %rax,%rcx
callq 0x40157e <CStudent::~CStudent()>
jmp 0x401759 <main(int, char**)+169>
mov %rax,%rbx
lea -0x50(%rbp),%rax
mov %rax,%rcx
callq 0x40157e <CStudent::~CStudent()>
mov %rbx,%rax
mov %rax,%rcx
callq 0x40f670 <_Unwind_Resume>
add $0x58,%rsp
pop %rbx
pop %rbp
retq
如代码中的注释:
(1)main函数在调用func函数前,创建了一个临时对象,这里给它命名为m_tmp。
(2)m_tmp对象的地址传递给func函数,func函数里的tmp对象直接使用了m_tmp的地址。因此,可以认为,tmp就是m_tmp的别名。
(3)func函数返回后,将m_tmp对象的数据赋值给stu2对象。
(4)最后,析构m_tmp。
所以,从始至终,没有调用过拷贝构造函数。