编译器角度看C++复制构造函数
[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 语意呢?
- 当类内含有一个member object 而后者的类声明中有个复制构造函数时(无论这个构造函数是设计者明确地声明还是编译器合成)。
- 当类继承于一个基类而后者有已给复制构造函数时(同样的,无论基类的构造函数是设计者明确声明的还是合成的)。
- 当类声明了一个或多个虚函数时。
- 当类派生自一个继承串链,其中有一个或多个虚基类时。
前两种情况中,编译器必须将“类成员或基类的复制构造函数调用操作”安插到新合成的复制构造函数中去,如果类设计者已经明确声明了一个复制构造函数,则这些调用操作代码将插入到已有的复制构造函数中去(在函数体的最前端插入)。
后两种操作涉及到了虚表指针与虚基类指针的产生于初值设置。我们知道,当一个类含有虚函数时(无论这虚函数是类本身定义还是继承而来),在编译期间会有以下两个程序扩张操作:
- 为类增加一个虚表(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++复制构造函数的更多相关文章
- c++ 复制构造函数和赋值函数
c++ 自动提供了下面这些成员函数 1默认构造函数 2.复制构造函数 3.赋值操作符 4.默认析构函数 5.地址操作符 赋值构造函数copy construtor 用于将一个对象复制到新创建的对象中, ...
- C++ 类 复制构造函数 The Copy Constructor
一.复制构造函数的定义 复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性.复制构造函数创建一个新的对象,作为另一个对象的拷贝.复制构造函数只含有一个形参,而且其形参为本类对象的引用.复制构 ...
- c++,类的对象作为形参时一定会调用复制构造函数吗?
c++,类的对象作为形参时一定会调用复制构造函数吗? 答:如果参数是引用传递,则不会调用任何构造函数:如果是按值传递,则调用复制构造函数,按参数的值构造一个临时对象,这个临时对象仅仅在函数执行是存在, ...
- C++构造函数(复制构造函数)、析构函数
注:若类中没有显示的写如下函数,编译会自动生成:默认复制构造函数.默认赋值构造函数(浅拷贝).默认=运算符重载函数(浅拷贝).析构函数: 1.默认构造函数(默认值)构造函数的作用:初始化对象的数据成员 ...
- C++中复制构造函数
复制构造函数 复制构造函数用于: 根据另一个同类型的对象显示或隐式初始化一个对象 复制一个对象,将它作为实参传给一个函数 从函数返回时复制一个对象 初始化顺序容器中的元素 根据元素初始化式列表初始化数 ...
- 深入理解c++构造函数, 复制构造函数和赋值函数重载(operator=)
注 以下代码编译及运行环境均为 Xcode 6.4, LLVM 6.1 with GNU++11 support, Mac OS X 10.10.2 调用时机 看例子 // // main.cpp / ...
- C++中复制构造函数和赋值操作符
先看一个例子: 定义了一个类:
- C++在单继承、多继承、虚继承时,构造函数、复制构造函数、赋值操作符、析构函数的执行顺序和执行内容
一.本文目的与说明 1. 本文目的:理清在各种继承时,构造函数.复制构造函数.赋值操作符.析构函数的执行顺序和执行内容. 2. 说明:虽然复制构造函数属于构造函数的一种,有共同的地方,但是也具有一定的 ...
- [转]为什么复制构造函数的参数需要加const和引用
[转]为什么复制构造函数的参数需要加const和引用 一.引言 1.0在解答这个问题之前,我们先跑个小程序,看下调用关系. #include <iostream> using namesp ...
随机推荐
- proxy改变this指向
var core_slice = Array.prototype.slice; var proxy = function(context,fn) { var args, proxy; if ( typ ...
- web端通信技术
1.web端通信技术:长连接.长轮询.websocket; 什么是长连接.长轮询? 就是客户端不停的向服务器发送请求以获取最新的数据信息.这里的“不停”其实是有停止的,只是我们人眼无法分辨是否停止,它 ...
- Java核心技术点之动态代理
本篇博文会从代理的概念出发,介绍Java中动态代理技术的使用,并进一步探索它的实现原理.由于个人水平有限,叙述中难免出现不清晰或是不准确的地方,希望大家可以指正,谢谢大家:) 一.概述 1. 什么是代 ...
- Java集合系列:-----------02Collection架构
出处:http://www.cnblogs.com/skywang12345/p/3308513.html 我们知道Collection是和Map架构平级的,我们看一下这个架构是怎样的. 他主要的两个 ...
- SharePoint 2013无代码实现列表视图的时间段动态筛选
本文介绍两种为列表视图设置时间段筛选器的方法.其中,第一个方法用于SharePoint Server,第二个方法同时还能用于SharePoint Foundation. 方法一:日期筛选器Web部件 ...
- Linux shell文本过滤
正则表达式 --概念:一种用来描述文本模式的特殊语法 --由普通字符(例如:字符a到z),以及特殊字符(元字符,如/*?等)组成匹配的字符串 --文本过滤工具在某种模式之下,都支持正则表达式 --基本 ...
- Windows 提高效率的常用快捷键
开发中减少使用鼠标次数,是一个很cool的体验!下面的快捷键在Win7上测试有效 快捷键 说明 Ctrl+Shift+N 创建一个新的文件夹. (Ctrl + N 打开桌面) Ctrl+Shift+ ...
- 分布式中使用Redis实现Session共享(一)
上一篇介绍了如何使用nginx+iis部署一个简单的分布式系统,文章结尾留下了几个问题,其中一个是"如何解决多站点下Session共享".这篇文章将会介绍如何使用Redis,下一篇 ...
- Qt5 新特性
Qt 5 已经临近发布,其最大的特点就是模块化.将原来庞大的模块更细分为不同的部分,同时,一个大版本的升级,当然少不了添加.删除各个功能类.文本简单介绍 Qt5 的特性,其具体内容来自 Qt5 官方 ...
- Google搜索的几个使用技巧——让你的搜索结果更准确
对于软件开发人员来说,不知道的内容在网上搜索是再正常不过的了.今天同事在组内分享了几个谷歌搜索的使用技巧,在此自己总结一下,希望可以帮到更多人. 在此之前先要唠叨几句,什么时候用百度,什么时候用谷歌? ...