全栈工程师修炼日记

全栈工程师修炼日记

一.什么是拷贝构造函数?

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;
}

        运行结果如下图所示。       

类的函数成员(三):拷贝构造函数-LMLPHP

         由上图可知:

(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;
}

        运行结果如下图所示。

类的函数成员(三):拷贝构造函数-LMLPHP

        由上图可知:

(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;
}

        运行结果如下图所示。

类的函数成员(三):拷贝构造函数-LMLPHP

        由上图可知。调用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。这怎么理解?下面看看汇编代码。

类的函数成员(三):拷贝构造函数-LMLPHP

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。

        所以,从始至终,没有调用过拷贝构造函数。

04-02 18:58