[C++对象模型]复制构造函数的建构操作

关于复制构造函数的简单介绍,可以看我以前写过的一篇文章C++复制控制之复制构造函数该文章中介绍了复制构造函数的定义、调用时机、也对编译器合成的复制构造函数行为做了简单说明。本文因需要会涉及到上文的一些知识点,但还是推荐先阅读上文。

本文主要从编译器角度对复制构造函数进行分析,纠正以前对复制构造函数的一些错误认识。

浅拷贝(deep copy)与深拷贝(shallow copy)

我们首先来看复制构造函数涉及的两个概念:浅拷贝与深拷贝。假设有两个对象:A与B,它们是同类型的,下面分析B=A时浅拷贝与深拷贝行为。

浅拷贝:

浅拷贝简单地把B复制为A的引用或指针,可以认为B复制了A的地址,复制的结果是B与A拥有相同的地址,它们将指向相同的内存区域的相同的数据。在这种情况下,如果对象A被销毁,那么对对象B的某些操作将是非法的。

深拷贝:

深拷贝时使用一个对象的内容来创建同一个类的另一个实例,B复制了A的所有成员,并在内存中不同于A的区域为B分配了存储空间,也即是说B拥有自己的资源。在这种方式下,如果A被销毁时,B依旧有效,因为A与B并没有共享存储空间,重载复制操作符时要采用这种深拷贝方式。

当你明确知道你中程序中使用的是浅拷贝并且明白它带来的后果时你才去使用浅拷贝。而当你有大量的指针要处理时,对指针做浅拷贝是一个糟糕的做法。如果我们类的数据成员都是内置类型而没有指针,那么简单的浅拷贝是可以接受的,反之如果类中有需要深层复制的内容,则我们的复制构造函数必须以深拷贝的方式进行对象的复制。

Memberwise copy 与 Bitwise copy

Memberwise copy:

逐个成员:我们把merberwise copy当成deep copy来理解就行了,这种复制会根据每个成员的类型来进行复制,对于指针类型会复制指针所指的值(重新分配存储区域)。

Bitwise copy:

Bitwise copy 字面上的意思是逐位拷贝。举个例子,对于两个同类型的对象A与B,对象A在内存中占据存储区为0x0-0x9,执行B=A时,使用Bitwise copy拷贝语义,那么将会拷贝0x0到0x9的数据到B的内存地址,也就是说Bitwise是字节到字节的拷贝。这样子理解起来,实际上Bitwise copy = shallow copy。

类的Bitwise copy 语意

《Effective C++》中说到:

如果你自己没声明,编译器就会为它声明一个copy构造函数、一个copy assignment操作符和一个析构函数。

实际上在《深度探索C++对象模型》中对编译器的行为并不是这样描述的。对于默认构造函数与复制构造函数,都需要类满足一定的条件时编译器才会帮你合成。那么需要满足些什么条件呢?这条件就是:类不展现bitwise copy 语意的时候。

类展现Bitwise copy语意

当我们的类中只含有内置类型或复合类型时,类展现了Bitwise copy 语意。这种情况下并不需要合成一个默认复制构造函数,也即编译器不会帮我们合成复制构造函数。如:


//以下声明展现了bitwise copy 语意
class Word
{
public:
Word(char*temp){ str = temp; };
//...
int cnt;
char *str;
};

这时候如果我们有两个Word对象的赋值操作如下:


int main()
{
char * temp = "hello";
Word A(temp);
Word B(A);
cout

运行程序,你会神奇地发现程序居然通得过编译,而且B也得到了正确的赋值,就好像类中有了一个复制构造函数一样。不是说编译器在Bitwise copy语意下不会进行复制构造函数的合成吗?

说实话这问题我也很疑惑,查看了许多资料,反复看了《深度探索C++对象模型》后,我最终这样认为:展现了Bitwise copy语意的类编译器不会为它写一个函数实体进行成员的复制。展现Bitwise copy语意的类,类的数据成员按照Memberwise Initialization(注意不同于Memberwise copy)进行初始化,具体是这样的:当类对象以同类型的另一个对象进行初始化时,把每一个内建的或派生的date member(例如一个指针或一数目组)的值,从一个对象拷贝到另一个对象,不过它并不会拷贝其中的member class object,而是以递归的方式施行以上的拷贝。实施这些步骤并不在函数实体内。

类不展现Bitwise copy语意

当类不展现出Bitwise copy语意且类设计者没有为类定义一个复制构造函数,这时编译器就会为合成一个复制构造函数实体。那么在什么情况下一个类才会不展现出Bitwise copy 语意呢?

  1. 当类内含有一个member object 而后者的类声明中有个复制构造函数时(无论这个构造函数是设计者明确地声明还是编译器合成)。
  2. 当类继承于一个基类而后者有已给复制构造函数时(同样的,无论基类的构造函数是设计者明确声明的还是合成的)。
  3. 当类声明了一个或多个虚函数时。
  4. 当类派生自一个继承串链,其中有一个或多个虚基类时。

前两种情况中,编译器必须将“类成员或基类的复制构造函数调用操作”安插到新合成的复制构造函数中去,如果类设计者已经明确声明了一个复制构造函数,则这些调用操作代码将插入到已有的复制构造函数中去(在函数体的最前端插入)。

后两种操作涉及到了虚表指针与虚基类指针的产生于初值设置。我们知道,当一个类含有虚函数时(无论这虚函数是类本身定义还是继承而来),在编译期间会有以下两个程序扩张操作:

  • 为类增加一个虚表(virtual function table),虚表内含有每一个有作用的虚函数的地址。
  • 为每一个类对象增加一个虚表指针(vptr),虚表指针指向了该类的虚表。

显然,如果编译器对每个新定义的类对象不能正确地设置好初值,将导致严重的后果。所以编译器需要合成出一个复制构造函数来适当地初始化类对象的vptr。万一类设计者明确定义了自己的复制构造函数,则编译器会把设置vptr的操作插入到已有的复制构造函数中。而vptr的复制又有两种情况:

  • 同类型对象间的vptr复制

同类类型的对象各自的vptr总是指向了同一个位置:该类的虚表指针。这时两个对象的vptr的复制都可以直接考”bitwise copy“来完成(除了可能会有的其他指针成员)。所以同类型对象间的vptr复制总是安全的。

-把子类对象vptr复制给父类对象

不用担心把子类对象复制给父类对象时,vptr也会采用bitwise copy来复制,这点编译器给我们做了保证:编译器合成的默认构造函数(或者说在明确声明的复制构造函数中安插的代码)会明确设定父类的vptr指向父类的虚函数表,而不是采用傻瓜式直接复制子类对象vptr。

而对于第4点涉及到虚基类的情况,可以看C++合成默认构造函数的真相中有关虚基类的描述。虚基类的存在需要特殊处理,一个类对象如果以另一个对象作为初值,而后者派生于虚基类,那么这种情况下bitwise copy语意也会失效,编译器会对派生自虚基类的类合成一个默认构造函数,在其中安插一些操作。对于虚继承,编译器有承偌:派生类对象中的虚基类位置在执行期就要准备妥当,维护”位置的完整性“是编译器的责任,而显然的,Bitwise copy 语意会破坏这个位置(这种傻瓜式的复制好像只适用内置类型的复制以及同类型对象间vptr的复制),所以编译器必须在它自己合成出来的复制构造函数中做出仲裁。同样的,如果类设计者明确声明了复制构造函数,则这些冲裁代码将安插在这个复制构造函数中。

总结

在类不满足"Bitwise copy"语意时编译器会采取行动,如果类设计者没有明确定义复制构造函数,则编译器将行动实施于合成构造函数中,否则将这些行动实施于已有的复制构造函数中。值得注意的是,编译器除了对vptr与虚基类的处理能保证安全之外,对于内置类型或复合类型如指针的复制都是采用浅拷贝,所以,当我们的类中含有指针的时候,我们需要自己写一个复制构造函数来对对象的指针进行深拷贝,而vptr与虚基类的问题,就交给编译器吧!

编译器角度看C++复制构造函数的更多相关文章

  1. c++ 复制构造函数和赋值函数

    c++ 自动提供了下面这些成员函数 1默认构造函数 2.复制构造函数 3.赋值操作符 4.默认析构函数 5.地址操作符 赋值构造函数copy construtor 用于将一个对象复制到新创建的对象中, ...

  2. C++ 类 复制构造函数 The Copy Constructor

    一.复制构造函数的定义 复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性.复制构造函数创建一个新的对象,作为另一个对象的拷贝.复制构造函数只含有一个形参,而且其形参为本类对象的引用.复制构 ...

  3. c++,类的对象作为形参时一定会调用复制构造函数吗?

    c++,类的对象作为形参时一定会调用复制构造函数吗? 答:如果参数是引用传递,则不会调用任何构造函数:如果是按值传递,则调用复制构造函数,按参数的值构造一个临时对象,这个临时对象仅仅在函数执行是存在, ...

  4. C++构造函数(复制构造函数)、析构函数

    注:若类中没有显示的写如下函数,编译会自动生成:默认复制构造函数.默认赋值构造函数(浅拷贝).默认=运算符重载函数(浅拷贝).析构函数: 1.默认构造函数(默认值)构造函数的作用:初始化对象的数据成员 ...

  5. C++中复制构造函数

    复制构造函数 复制构造函数用于: 根据另一个同类型的对象显示或隐式初始化一个对象 复制一个对象,将它作为实参传给一个函数 从函数返回时复制一个对象 初始化顺序容器中的元素 根据元素初始化式列表初始化数 ...

  6. 深入理解c++构造函数, 复制构造函数和赋值函数重载(operator=)

    注 以下代码编译及运行环境均为 Xcode 6.4, LLVM 6.1 with GNU++11 support, Mac OS X 10.10.2 调用时机 看例子 // // main.cpp / ...

  7. C++中复制构造函数和赋值操作符

    先看一个例子: 定义了一个类:

  8. C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容

    一.本文目的与说明 1. 本文目的:理清在各种继承时,构造函数.复制构造函数.赋值操作符.析构函数的执行顺序和执行内容. 2. 说明:虽然复制构造函数属于构造函数的一种,有共同的地方,但是也具有一定的 ...

  9. [转]为什么复制构造函数的参数需要加const和引用

    [转]为什么复制构造函数的参数需要加const和引用 一.引言 1.0在解答这个问题之前,我们先跑个小程序,看下调用关系. #include <iostream> using namesp ...

随机推荐

  1. Apache 的搭建及vim的简单命令

    一. vim 简单命令 pwd     当前路径 ls    当前路径所有目录 cd  目录地址   跳转到指定目录 /xxx  查找xxx x 删除当前字符 n 执行上一次查找 二.为什么使用apa ...

  2. JLOI2016 方

    bzoj4558 真是一道非常excited的题目啊-JLOI有毒 题目大意:给一个(N+1)*(M+1)的网格图,格点坐标为(0~N,0~M),现在挖去了K个点,求剩下多少个正方形(需要注意的是正方 ...

  3. FMDB处理动态插入语句

    昨天做一个需求,参数的数量不确定,所以无法使用这个API: - (BOOL)executeUpdate:(NSString*)sql, ... 但是用 - (BOOL)executeUpdate:(N ...

  4. PAT 1023. 组个最小数 (20)

    给定数字0-9各若干个.你可以以任意顺序排列这些数字,但必须全部使用.目标是使得最后得到的数尽可能小(注意0不能做首位).例如:给定两个0,两个1,三个5,一个8,我们得到的最小的数就是1001555 ...

  5. 快速判断素数 --Rabin-Miller算法

    以前我在判断素数上一直只会 sqrt(n) 复杂度的方法和所谓的试除法(预处理出sqrt(n)以内的素数,再用它们来除). (当然筛选法对于判断一个数是否是素数复杂度太高) 现在我发现其实还有一种方法 ...

  6. C#.NET 大型通用信息化系统集成快速开发平台 4.0 版本 - 多系统开发接口 - 苹果客户端开发接口

    最近工作上需要,给苹果客户端开发接口,实现集中统一的用户管理,下面是接口调用参考. 1: 获取OpenId? http://127.0.0.1/GetOpenId.ashx?username=Admi ...

  7. Linux 守护进程三

    .打开telnet工具,登录服务器,登录校验成功以后, linux服务器会在终端和服务器之间,建立一个会话期session .在这个会话期中,默认启动一个shell程序 .在会话期中有n个进程组 sh ...

  8. 如何在mac本上安装android sdk

    众所周知的原因,google的很多网站在国内无法访问,苦逼了一堆天朝程序员,下是在mac本上折腾android 开发环境的过程: 一.先下载android sdk for mac 给二个靠谱的网址: ...

  9. HFSS学习

    关于边界条件和端口激励的设置,是HFSS应用和学习的重点和难点:“边界条件决定场”,正确地理解和使用边界条件是正确使用HFSS仿真分析电磁问题的前提:HFSS中定义了多种边界条件,大家在学习过程中必须 ...

  10. Cadence Allegro元件封装制作流程

    (本文为转载,原文出处不详) 引言 一个元件封装的制作过程如下图所示.简单来说,首先用户需要制作自己的焊盘库Pads,包括普通焊盘形状Shape Symbol和花焊盘形状Flash Symbol:然后 ...