当前位置: 首页 > news >正文

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

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

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。

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

http://www.lryc.cn/news/334373.html

相关文章:

  • C#操作MySQL从入门到精通(8)——对查询数据进行高级过滤
  • Centos 7 安装通过yum安装google浏览器
  • 题目:学习使用按位与 。
  • 逐步分解,一文教会你如何用 jenkins+docker 实现主从模式
  • WebSocket 对于手游的意义
  • 安卓APP的技术质量:如何提高
  • 二分查找 -- 力扣(LeetCode)第704题
  • Windows下如何确定虚函数在虚函数表中的位置
  • C++设计模式:观察者模式(三)
  • CentOS运行Py脚本报错illegal instruction故障处理
  • 软件设计师——1.备考提纲
  • [开源] 基于GRU的时间序列预测模型python代码
  • SQL SERVER 备份
  • 提示词专场:从调整提示改善与LLMs的沟通,到利用LLMs优化提示效果
  • 测开面经(pytest测试案例,接口断言,多并发断言)
  • Golang 开发实战day09 - package Scope
  • 24考研-东南大学916经验贴
  • 【AI面试】YOLO 如何通过 k-means 得到 anchor boxes的?Yolo、SSD 和 faster rcnn 的正负样本定义
  • MySQL高级篇(B-Tree、Btree)
  • Zookeeper脑裂解决方案
  • 常用日常脚本
  • Longan Pi 3H 开发板体验
  • SpringCloud Alibaba Sentinel 创建流控规则
  • Mysql底层原理五:如何设计、用好索引
  • python学习杂记
  • C# Socket发送、接收结构体
  • ics-05-攻防世界
  • Web API(三)之事件流事件委托其他事件
  • SSL证书的作用是什么?
  • 皮具5G智能制造工厂数字孪生可视化平台,推进企业数字化转型