C++对象模型的那些事儿之四:拷贝构造函数
前言
对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数。这类函数包括一下几个:
- 构造函数
- 拷贝构造函数
- 析构函数
- 赋值运算符
在上一篇博文C++对象模型的那些事儿之三:默认构造函数中讲到,编译器在需要的时候会合成一个空构造函数。本篇博文中就重点来介绍一下第二主角:拷贝构造函数。
引子
正如Linus Torvalds说的一句话:“Talk is cheap,Show me the code”。在程序员的世界里,讲再多都不如直接给看代码。就比如一道算法题,别人跟你讲半天思路,你懂了,但真正要你码出代码来实现时,你可能花的时间比理解思路还要多,更别提后面的调试时间了。所以,一如本系列文章的风格,从代码的角度来观“对象模型”,再合适也不过呢。
拷贝构造函数,就是以一个对象作为另一个类对象初值的构造函数。在下面三种情况下会调用拷贝构造函数:
class Animal{};
//--------------------第一种情况-------------------------//
//对一个对象对另一个对象进行显示的初始化
Animal animal_one;
Animal animal_two = animal_one;
//--------------------第二种情况-------------------------//
//一个对象作为函数参数,以值传递的方式传进函数
void getName(Animal a){}
getName(animal_one);
//--------------------第三种情况-------------------------//
//一个对象作为函数返回值,以值传递的方式从函数返回
Animal setName(){
Animal animal;
//....
return animal;
}
说了这么多,拷贝构造函数到底该怎么写呢?请继续阅读下面的代码。
//单参数拷贝构造函数
Animal::Animal(const Animal& _animal){
//....
}
//多参数拷贝构造函数,其第二参数即后继参数以一个默认值供应
Animal::Animal(const Animal& _animal, int =0){
//.....
}
有了如上的理解之后,还是如默认构造函数那样,接下来就来讨论trivial和non-trivial构造函数以及什么时候编译器会产生non-trivial构造函数。
位逐次拷贝
位逐次拷贝是由“Bitwise Copy Semantics”翻译而来,就是按bit位来拷贝对象。如下面的代码:
class Animal{
//没有提供显示的拷贝构造函数
int age;
char* name;
};
Animal animal_one;
Animal animal_two = animal_one;
这种情况下会采用位逐次拷贝,只是简简单单按位把animal_one的内存中存的值赋给animal_two,这类拷贝也称为浅拷贝。正如大家熟知,这类拷贝是不安全的。

上图就是位逐次拷贝后的对象示意图,现在animal_two的name指针指向了animal_one::name指向的字符串,如果animal_one被析构,animal_two::name就成空悬指针,当animal_two析构的时候,就会释放一个已经释放的内存,会造成不可预知的错误。
如果我们把char* name换成string name,再执行拷贝构造函数后,其对象示意如下:

因为string函数有显式的拷贝构造函数,所以在执行拷贝构造函数的时候是为animal_two::name重新分配一块内存,然后对其赋值,自然就不会存在两个指针指向同一块内存的情况了。
对于上述两种情况,我们可以将拷贝构造函数划分为trivial和non-trivial:
- trivial:直接进行位逐次拷贝
- non-trivial:不进行位逐次拷贝
那么,编译器在什么时候不会展现出位逐次拷贝的能力,即会合成一个non-trivial拷贝构造函数呢?下面就分四种情况来讨论。
带有拷贝构造函数的成员类对象
如果一个类中有带有拷贝构造函数的类成员,或是编译器会为其合成一个拷贝构造函数,那么这个类就不会展现出位逐次拷贝的能力。
class Animal{
public:
Animal(){}
Animal(const Animal& animal){
cout<<"Animal's Copy Constructor"<<endl;
}
};
class Dog{
public:
Dog(){}
Animal animal;
};
int main(){
Dog dog1;
Dog dog2 = dog1;
}
如上述的代码,执行之后会输出:Animal’s Copy Constructor,编译器为dog2合成的拷贝构造函数不是简单的进行位逐次拷贝,而是调用了Animal的拷贝构造函数,重新构造一个dog2::animal。
带有拷贝构造函数的基类
如果一个类继承自一个带有拷贝构造函数的基类的话,那么编译器在为其合成拷贝构造函数的时候会调用基类的拷贝构造函数。简单的以以下代码来测试一下:
class Animal{
public:
Animal(){}
Animal(const Animal& animal){
cout<<"Animal's Copy Constructor"<<endl;
}
};
class Dog : Animal{
public:
Dog(){}
};
int main(){
Dog dog1;
Dog dog2 = dog1;
}
同样的,上述代码会输出Animal’s Copy Constructor,显示调用了基类的拷贝构造函数。
带有虚函数的类对象
如果一个类带有虚函数,想想在上一篇讲默认构造函数的时候,编译期间会执行下面两个操作
- 增加一个虚函数表,内含每一个有作用的虚函数的地址
- 一个指向虚表的指针,安插在每一个类对象内
涉及到虚函数的类在合成拷贝构造函数的时候,有点复杂,我们先看看如下测试代码:
class Animal{
public:
virtual void eat(){}
};
class Dog : public Animal{
public:
virtual void eat(){}
};
int main(){
Dog dog1;
Dog dog2 = dog1;
cout<<"dog1::vptr"<<(long long *)*(long long*)&dog1<<endl;
cout<<"dog1::vptr"<<(long long *)*(long long*)&dog2<<endl;
}
在上述测试代码中,我提取出dog1和dog2对象中的虚表地址,观察输出如下:
dog1::vptr:0x400c60
dog2::vptr:0x400c60
由于虚表是在编译的时候创建的,所以,将dog2的虚表指针指向dog1的虚表这样是安全的,这里使用位逐次拷贝是没有问题的。
Tips:对于带有虚函数的类,用同类型的对象初始化时,采用位逐次拷贝完全够用,不会合成拷贝构造函数。
这里可以对比,将两个指针同时指向同一个字符常量的情况,这样是安全的。
但是,如果执行如下代码:
Dog dog;
Animal animal = dog;
cout<<"dog::vptr:"<<(long long *)*(long long*)&dog<<endl;
cout<<"animal::vptr:"<<(long long *)*(long long*)&animal<<endl;
此时,将一个父类用子类初始化,这时候输出如下:
dog::vptr:0x400c30
animal::vptr:0x400c48
可见,这时候就不能采用位逐次拷贝了,父类的拷贝构造函数需要重新设定自己的虚指针指向Animal类的虚表,而不是直接将dog::vptr直接赋给animal::vptr。
带有虚基类的子类对象
同样,对于带有虚基类的子类,情况也比较复杂,我们先来看看如下继承关系:

在上图中,Canidae由Animal类虚拟派生出来,Dog由Canidae类派生出来,在Canidae和Dog类中都有一个虚基类的指针,指向每个类中的虚基类。因此,在执行以下操作时,位逐次拷贝也会失效,编译器必须合成一个拷贝构造函数,来重新设定指向虚基类的指针。
Dog dog;
Canidae canidae=dog;
cout<<(long long *)*(long long*)&canidae<<endl;
cout<<(long long *)*(long long*)&dog<<endl;
以上测试代码输出:
0x400c20
0x400be0
总结
本篇博客讨论了编译器会合成一个拷贝构造函数的四种情况,现总结如下:
- 带有拷贝构造函数的成员类对象
- 带有拷贝构造函数的基类对象
- 带有虚函数的类对象
- 带有虚基类的子类对象
其中,需要注意的是:对于带有虚函数的类对象和带有虚基类的子类对象这两种情况中,如果是以同类型的对象作为初始对象的话,是不会合成拷贝构造函数的,仅仅使用位逐次拷贝就能完成。
About Me
由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。
最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me。
另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode
欢迎持续关注!Thx!
C++对象模型的那些事儿之四:拷贝构造函数的更多相关文章
- C++对象模型的那些事儿之五:NRV优化和初始化列表
前言 在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的 ...
- 【C++对象模型】构造函数语意学之二 拷贝构造函数
关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在[需要的时候]才去合成默认的拷贝构造函数. 在什么时候才是[需要的时候]呢? 也就是类不展现[bitwise copy semantic ...
- C++对象模型的那些事儿之三:默认构造函数
前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...
- C++对象模型(二):The Semantics of Copy Constructors(拷贝构造函数之编译背后的行为)
本文是 Inside The C++ Object Model's Chapter 2 的部分读书笔记. 有三种情况,需要拷贝构造函数: 1)object直接为另外一个object的初始值 2)ob ...
- C++拷贝构造函数总结
C++拷贝构造函数总结 目录: 拷贝构造函数的基础知识 拷贝构造函数的使用 拷贝构造函数的行为 1.拷贝构造函数的基础知识 拷贝构造函数(copy constructor)是构造函数,是拷贝已经存在的 ...
- C++对象模型的那些事儿之一:对象模型(上)
前言 很早以前就听人推荐了<深入理解C++对象模型>这本书,从年初买来到现在也只是偶尔翻了翻,总觉得晦涩难懂,放在实验室上吃灰吃了好久.近期由于找工作对C++的知识做了一个全面系统的学习, ...
- C++拷贝构造函数心得
C++Primer作者提到拷贝构造函数调用的三种时机: 1. 当用一个类对象去初始化另外一个类对象(类似于 AClass aInstance = bInstance),这里不是调用赋值构造函数(也叫赋 ...
- C++ 拷贝构造函数和赋值运算符
本文主要介绍了拷贝构造函数和赋值运算符的区别,以及在什么时候调用拷贝构造函数.什么情况下调用赋值运算符.最后,简单的分析了下深拷贝和浅拷贝的问题. 拷贝构造函数和赋值运算符 在默认情况下(用户没有定义 ...
- C++ 一个例子彻底搞清楚拷贝构造函数和赋值运算符重载的区别
class TestChild { public: TestChild() { x=; y=; printf("TestChild: Constructor be called!\n&quo ...
随机推荐
- hdu 4283 区间dp
You Are the One Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)T ...
- hihocoder 1249(2015ACM/ICPC北京)
题意: 给你一块正方形的土地,里面有矩形的草地,要求把土地分成两份,满足以下两个条件 1.两边的绿洲,左边>=右边,差值尽可能的小 2.在满足1的情况下分给左边的土地尽快能的多 而且绿洲不会出现 ...
- [bzoj4625][BeiJing2016]水晶
来自FallDream的博客,未经允许,请勿转载,谢谢. 不用惊慌,今天的题都不是小强出的.——融入了无数心血的作品,现在却不得不亲手毁掉,难以体会他的心情啊 .——那也是没有办法的事情,能量共振不消 ...
- [3.19FJ四校联考]
来自FallDream的博客.未经允许,请勿转载,谢谢. ---------------------------------------------------- A.积分,不会 以后补 B.给定一 ...
- 2393Cirno的完美算数教室 容斥
2393: Cirno的完美算数教室 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 652 Solved: 389[Submit][Status][ ...
- A Problem-Solving FlowChart || 如何解决编程问题
This is from book Cracking the coding interview, Gayle Laakmann Mcdowell. The flowchart can be used ...
- java读取mysql表的注释及字段注释
/** * 读取mysql某数据库下表的注释信息 * * @author xxx */ public class MySQLTableComment { public static Connectio ...
- 正确在遍历中删除List元素
最近在写代码的时候遇到了遍历时删除List元素的问题,在此写一篇博客记录一下. 一般而言,遍历List元素有以下三种方式: 使用普通for循环遍历 使用增强型for循环遍历 使用iterator遍历 ...
- Intellij IDEA自动编译问题
对IDEA的界面很有爱,但是感到他的项目启动速度太慢了.所以查了资料做了优化. 1:开启自动测试 File->setting->compiler 勾选上上面的, 2修改run/de ...
- My Stuck in C++
My Stuck in C++ Zhong-Liang Xiang Oct. 1st, 2017 这个专题记录了对于我而言, c++迷一样的东西.