前言

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

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

在上一篇博文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. 【bzoj4570 scoi2016】妖怪

    题目描述 邱老师是妖怪爱好者,他有n只妖怪,每只妖怪有攻击力atk和防御力dnf两种属性.邱老师立志成为妖怪大师,于是他从真新镇出发,踏上未知的旅途,见识不同的风景. 环境对妖怪的战斗力有很大影响,在 ...

  2. hdu 5012(bfs)

    题意:给你2个 骰子,让你通过翻转使第一个变成第二个,求最少翻转数 思路:bfs #include<cstdio> #include<iostream> #include< ...

  3. Linux下如何进入中文目录

    给Linux安装图形用户界面之后,会在工作目录中生成图片, 文档, 下载........等中文目录,以前不知道如何进入这些目录,感觉也没有必要,今天在火狐上下载了一个软件,默认在下载这个目录当中,实在 ...

  4. JMQ

    [京东技术]京东的MQ经历了JQ->AMQ->JMQ的发展,其中JQ的基于关系数据库,严格意义上讲称不上消息中间件,JMQ的存储是JFS和HBase,AMQ即ActiveMQ,本文说说JM ...

  5. mybatis逆向工程,转载别人的,很清楚

    转载博客地址:http://www.cnblogs.com/selene/p/4650863.html

  6. 40. Combination Sum II(midum, backtrack, 重要)

    Given a collection of candidate numbers (C) and a target number (T), find all unique combinations in ...

  7. scratch写的图灵机

    大多数人对于scratch不感冒,因为觉得这是孩子玩的.的确,积木的方式不适合专业程序员写代码,然而别小看scratch,怎么说,它也是图灵完备的.而且,过程支持递归,虽然带不了返回值. 虽然计算速度 ...

  8. Python3 XML解析

    什么是XML? XML 指可扩展标记语言(eXtensible Markup Language),标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言. 你可以通过本站学习XML教程 ...

  9. sublime snippet 示例

    <snippet> <content><![CDATA[local ${1:M} = {} function ${1:M}.new(cls, self) self = s ...

  10. 20160212.CCPP体系详解(0022天)

    程序片段(01):01.二维数组.c 内容概要:二维数组 #include <stdio.h> #include <stdlib.h> //01.关于栈内存开辟数组: // 诀 ...