前言

对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数。这类函数包括一下几个:

  • 构造函数
  • 拷贝构造函数
  • 析构函数
  • 赋值运算符

在上一篇博文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++对象模型的那些事儿之四:拷贝构造函数的更多相关文章

  1. C++对象模型的那些事儿之五:NRV优化和初始化列表

    前言 在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的 ...

  2. 【C++对象模型】构造函数语意学之二 拷贝构造函数

    关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在[需要的时候]才去合成默认的拷贝构造函数. 在什么时候才是[需要的时候]呢? 也就是类不展现[bitwise copy semantic ...

  3. C++对象模型的那些事儿之三:默认构造函数

    前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...

  4. C++对象模型(二):The Semantics of Copy Constructors(拷贝构造函数之编译背后的行为)

    本文是 Inside The C++ Object Model's Chapter 2  的部分读书笔记. 有三种情况,需要拷贝构造函数: 1)object直接为另外一个object的初始值 2)ob ...

  5. C++拷贝构造函数总结

    C++拷贝构造函数总结 目录: 拷贝构造函数的基础知识 拷贝构造函数的使用 拷贝构造函数的行为 1.拷贝构造函数的基础知识 拷贝构造函数(copy constructor)是构造函数,是拷贝已经存在的 ...

  6. C++对象模型的那些事儿之一:对象模型(上)

    前言 很早以前就听人推荐了<深入理解C++对象模型>这本书,从年初买来到现在也只是偶尔翻了翻,总觉得晦涩难懂,放在实验室上吃灰吃了好久.近期由于找工作对C++的知识做了一个全面系统的学习, ...

  7. C++拷贝构造函数心得

    C++Primer作者提到拷贝构造函数调用的三种时机: 1. 当用一个类对象去初始化另外一个类对象(类似于 AClass aInstance = bInstance),这里不是调用赋值构造函数(也叫赋 ...

  8. C++ 拷贝构造函数和赋值运算符

    本文主要介绍了拷贝构造函数和赋值运算符的区别,以及在什么时候调用拷贝构造函数.什么情况下调用赋值运算符.最后,简单的分析了下深拷贝和浅拷贝的问题. 拷贝构造函数和赋值运算符 在默认情况下(用户没有定义 ...

  9. C++ 一个例子彻底搞清楚拷贝构造函数和赋值运算符重载的区别

    class TestChild { public: TestChild() { x=; y=; printf("TestChild: Constructor be called!\n&quo ...

随机推荐

  1. [Codeforces]860E Arkady and a Nobody-men

    屯一个虚树的板子,顺便总结一下这样的题型. Description 给定一棵n个节点的有根树,在输入数据通过给出每个节点的父亲来表示这棵树.若某个节点的父亲为0,那么该节点即为根.现在对于每个点,询问 ...

  2. bzoj1293[SCOI2009]生日礼物 尺取法

    1293: [SCOI2009]生日礼物 Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 2838  Solved: 1547[Submit][Stat ...

  3. C++值传递与引用传递

    值传递:形参是对实参的拷贝,改变形参的值不会改变外部实参的值,从被调用的角度来说,值传递时单向传递(实参->形参),参数的值只能传入,不能传出. 当函数内部需要修改参数,并且不希望这个改变影响调 ...

  4. js 在iframe子页面获取父页面元素,或在父页面 获取iframe子页面的元素的几种方式

    用JS或jquery访问页面内的iframe,兼容IE/FF 注意:框架内的页面是不能跨域的! 假设有两个页面,在相同域下. index.html 文件内含有一个iframe: XML/HTML代码 ...

  5. Map,HashMap,TreeMap

    一.HashMap,TreeMap差别 1.两种常规Map性能 HashMap:适用于在Map中插入.删除和定位元素. Treemap:适用于按自然顺序或自定义顺序遍历键(key). 2.总结 Has ...

  6. CentOS7.2安装jdk7u80

    1.cd /usr/local 2.tar zxvf jdk-7u80-linux-x64.tar.gz 3.vi /etc/profile 4.输入i 加入内容如下: export JAVA_HOM ...

  7. jsp根据参数默认选中radio

    <% int vol = (Integer)request.getAttribute("cardtype") ; %> <input type="rad ...

  8. c++ 文件操作详解

    C++ 通过以下几个类支持文件的输入输出: ofstream: 写操作(输出)的文件类 (由ostream引申而来) ifstream: 读操作(输入)的文件类(由istream引申而来) fstre ...

  9. Git 中 SSH key 生成步骤

    由于本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以必须要让github仓库认证你SSH key,在此之前,必须要生成SSH key. 第1步:创建SSH Key.在windows下 ...

  10. vue+node.js+webpack开发微信公众号功能填坑——组件按需引入

    初次开发微信公众号,整体框架是经理搭建,小喽喽只是实现部分功能,整体页面效果 整个页面使用两个组件:布局 FlexBox,搜索框 Search,demo文档 http://vue.ydui.org/d ...