编译器角度看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 ...
随机推荐
- jq分页异步刷新 ,全局刷新问题
在做分页的时候,可能点击下一页全部刷新 这样写会导致动态刷新,页面全部刷新了 $("#pageList a").click(function () { var $s = $(thi ...
- React 学习笔记(一)
React + es6 一.createClass 与 component 的区别 The API (via 'extends React.Component') is similar to Reac ...
- python中的字符串操作
#!/usr/bin/python # -*- coding: UTF-8 -*- ''' str.capitalize() ''' str = 'this is a string example' ...
- Meet Python: little notes 2
From this blog I will turn to Markdown for original writing. Source: http://www.liaoxuefeng.com/ ♥ l ...
- Linux 网络编程详解九
TCP/IP协议中SIGPIPE信号产生原因 .假设客户端socket套接字close(),会给服务器发送字节段FIN: .服务器接收到FIN,但是没有调用close(),因为socket有缓存区,所 ...
- JAVA CDI 学习(3) - @Produces及@Disposes
上一节学习了注入Bean的生命周期,今天再来看看另一个话题: Bean的生产(@Produces)及销毁(@Disposes),这有点象设计模式中的工厂模式.在正式学习这个之前,先来看一个场景: 基于 ...
- 航空货运:运价类别Rate Class
1.普通货物运价(1)基础运价(代号N -注:Normal的首字母)民航总局统一规定各航段货物基础运价为45公斤以下普通货物运价.(2)重量分界点运价(代号Q -注:Quantity的首字母)国内航 ...
- location.href 实现点击下载功能
如果页面上要实现一个点击下载的功能,传统做法是使用一个 a 标签,然后将该标签的 href 属性地址指向下载文件在服务端的地址(相对地址或者绝对地址),比如这样: 能这样实现是因为,在浏览器地址栏输入 ...
- MATLAB调用C程序、调试和LDPC译码
MATLAB是一个很好用的工具.利用MATLAB脚本进行科学计算也特别方便快捷.但是代码存在较多循环时,MATLAB运行速度极慢.如果不想放弃MATLAB中大量方便使用的库,又希望代码能迅速快捷的运行 ...
- 使用Jekyll在Github上搭建博客
最近在玩github,突然发现很多说明网站或者一些介绍页面全部在一个域名是*****.github.io上. 好奇!!!真的好奇!!!怎么弄的?我也要一个~~~ 于是去网站上查询了一下,找到了http ...