有三种情况,会以一个对象的内容作为另一个类对象的初值

  • 最明显的一种情况是对一个对象做明确的初始化操作,比如:
class X{ ... };
X x;
X xx = x; // 明确的以一个对象的内容作为另一个类对象的初值
  • 另一种情况是当对象被当作参数交给某个函数时,比如:
extern void foo(X x);void bar(){X xx;foo(xx); // 以xx作为foo()第一个参数的初值(不明显的初始化操作)
}
  • 当函数返回一个类对象是,比如:
X foo_bar(){X xx;return xx;
}

假设类设计者明确定义了一个拷贝构造函数(有一个参数的类型是类类型[class type]),比如:

// 用户定义的拷贝构造函数的实例
// 可以是多参数形式,其第二参数以及后继参数有默认值
X::X(const X& x);
Y::Y(const Y& y, int = 0);

那么在大部分情况下,当一个类对象以另一个同类实体作为初值时,就会调用上面的构造函数。这可能会导致一个临时类对象的产生或者重新代码的蜕变

Default Memberwise Initialization

如果类没有提供一个显式拷贝构造函数会是怎样?

  • 当类对象以相同类的另一个对象作为初值时,其内部是以所谓的默认成员初始化(Default Memberwise Initialization)手法完成的
  • 也就是把每一个内建的或者派生的数据成员的值,从某个对象拷贝一份到另一个对象身上。
  • 不过它不会拷贝其中的成员类对象(member class object),而是以递归的方式施行memberwise initalization。比如:
class String{public://没有显示拷贝构造函数
private:char *str;int len;
};

一个String对象的默认成员初始化发生在这种情况下:

String noun("book");
String verb = noun;

其完成方式就好像个别设定每一个成员一样:

// 语义相等
verb.str = noun.str;
verb.len = noun.len;

如果一个String对象被声明为另一个类的成员,像这样:

class Word{public://没有显式拷贝构造
private:int _occurs;String _word; // String对象是word类的一个成员
};

那么Word对象的默认成员初始化会拷贝其内建的成员_occurs,然后再于String成员对象_word上递归实施memberwise initalization

这个操作实际上怎么完成呢?

  • 从概念上讲,对于一个类X,这个操作是被一个拷贝构造函数实现的
  • 一个良好的编译器可以为大部分类对象产生逐位拷贝(bitwise copies),因为它们有bitwise copy semantics

也就是说,”如果一个类没有定义拷贝构造函数,编译器就自动产生一个“这句话不对。默认构造函数和拷贝构造函数都是在必要的时候才由编译器产生出来的

这个必要指的是当类不展现bitwise copy semantics时。

一个类对象可以从两种方式复制得到:

  • 被初始化,通过拷贝构造函数完成
  • 被指定,通过拷贝赋值运算符完成

如果类没有声明一个拷贝构造函数,就会有隐式声明或者隐式定义一个。

  • C++标准把拷贝构造函数分为trivial和nontrivial两种。
  • 只有nontrival的实体才会被合成于程序中
  • 决定一个拷贝构造函数是否为trivial的标准在于类是否展示出所谓的bitwise copy semantics`

bitwise copy semantics(位逐次拷贝)

Word noun("book");
Word verb = noun;

verb是根据noun来初始化的。

  • 如果类Word显式定义了一个拷贝构造函数,verb的初始化操作就会调用它
  • 如果类Word没有显式定义一个拷贝构造函数,那么是否有一个编译器合成的实体被调用呢?这就需要看该类是否展现bitwise copy semantics。 看个例子:
// 以下声明展现了bitwise copy semantics
class Word{public:Word(const char *);~Word(){delete [] str};
private:int   cnt;char  *str;
};

这种情况下不需要合成出一个默认拷贝函数,因为上面声明展现了default copy semantics。 而如果Word声明如下:

// 以下声明没有展现了bitwise copy semantics
class Word{public:Word(const string &);~Word();
private:int   cnt;String str;
};

其中String声明了一个显式拷贝函数

class String{public:String(const char *);String(const String &);~String();
};

这时,编译器必须合成一个拷贝构造函数以调用成员类对象String的拷贝构造函数

// 一个被合成出来的拷贝构造函数
inline Word::Word(const Word &wd){str.String::String(wd.str);cnt = wd.cnt;
}

注意:这里的拷贝构造函数中,比如整数、指针、数组等nonclass members也会被赋值

没有bitwise copy semantics

当类不再保持bitwise copy semantics时,而且没有声明默认拷贝函数时,这个类会被视为nontrival。如果没有声明拷贝函数,编译器为了正确处理以一个类对象作为另一个类对象的初值,必须合成一个拷贝对象。

什么时候一个类不展示出bitwise copy semantics呢?有四种情况:

  • 含有一个成员对象而后者的声明有一个拷贝构造函数时(不管这个拷贝构造函数是被显式声明还是被编译器合成的)
  • 继承自一个基类而后者存在有一个拷贝构造函数时(不管这个拷贝构造函数是被显式声明还是被编译器合成的)
  • 声明了一个或者多个虚函数
  • 派生自一个继承串链,其中有一个或者多个虚基类

前两种情况中,编译器必须将成员对象或者基类拷贝构造函数调用操作插入到被合成的拷贝构造函数中

后两种请看下面讨论:

重新设定虚函数表的指针

当有一个类声明了一个或者多个虚函数时,编译期间就会做如下扩张工作:

  • 增加一个虚函数表vtbl,内含每一个有作用的虚函数的地址
  • 将一个指向虚函数表的指针vptr,安插在每一个类对象

显然,如果编译器对于每一个新产生的类对象的vptr不能成功正确的设定好初值,将导致可怕的后果。因此,当编译器导入一个vptr到类之后,该类就不再展现bitwise semantics了。现在,编译器需要合成出一个拷贝构造函数,以求将vptr适当的初始化。举个例子:

class ZooAnimal{public:ZooAnimal();virtual ~ZooAnimal();virtual void animate();virtual void draw();private://...
};class Bear : public ZooAnimal{public:Bear();void animate(); // 虽然没有明写virtual,但是是一个virtualvoid draw();    //  虽然没有明写virtual,但是是一个virtualvirtual void dance();private:// ...
};

ZooAnimal类对象以另一个ZooAnimal类对象作为初值,或者Bear类对象以另一个Bear类对象作为初值,都可以直接靠"bitwise copy semantics"完成,举个例子:

Bear yogi;
Bear winnie = yopi;

yogi会被默认构造函数初始化,在这个默认构造函数中,yogi的vptr被设定指向Bear类的虚函数表(靠编译器安插的码完成)。因此,把yogi的vptr值拷贝给winnie的vptr是安全的。

当一个基类对象以其派生类对象内容做初始化操作时,其vptr复制操作也必须保证安全。比如:

ZooAnimal franny = yogi; //这会发生切割行为

franny的vptr不可以被设定为指向Bear类的虚函数表,但是如果yogi的vptr被直接"bitwise copy",就会导致此结果。后果是下面程序片段就会被“炸毁”

void draw(const ZooAnimal& zoey) { zoey.draw(); }
void foo(){// franny的vptr指向ZooAnimal的虚函数表而不是Bear的虚函数表ZooAnimal franny = yogi;draw(yogi); // 调用Bear::draw;draw(franny); // 调用ZooAnimal::draw;
}

通过franny调用虚函数draw,调用的是ZooAnimal实体而不是Bear实体(虽然franny是以Bear类yogi作为初值),因为franny是一个ZooAnimal对象。实际上,yogi的Bear部分已经在franny初始化时被切割掉了。如果franny被声明为一个引用(或者指针,其值为yogi的地址),那么经由franny所调用的draw()才会是Bear的函数实体

也就是说,合成出来的ZooAnimal拷贝构造函数会明确设定对象的vptr指向ZooAnimal类的虚函数表,而不是直接从右手边的类对象中将其vptr现值拷贝出来。

处理虚基类子对象

虚基类的存在需要特别处理。一个类对象如果以另一个对象作为初值,而后者有虚基类子对象(virtual base class subobject),那么也会使bitwise copy semantics失效

每一个编译器对于虚拟继承的支持承诺,都表示必须让派生类对象中的虚基类子对象位置在执行期就准备妥当。维护位置的完整性是编译器的责任。bitwise copy semantics肯能会破坏这个位置,所以编译器必须在(编译器)合成出来拷贝构造函数做出仲裁。举个例子:

class Raccon : public virtual ZooAnimal{public:Raccon(){}Raccon(int val){}private:
};

编译器所产生的代码(用以调用ZooAnimal的默认构造函数、将Raccon的vptr初始化,并定位出Raccon的ZooAnimal subobject)被安插在两个Raccon构造函数之内,成为其先头部队

那么所有成员初始化呢?

  • 首先,一个虚基类的存在会使得bitwise copy semantics失效
  • 其次,问题并不发生于一个类对象以另一个同类对象作为初值(这时可以bitwise copy semantics)之时,而是发生与一个类对象以其派生类对象作为初值(bitwise copy semantics失效)时。比如:
class RedRanda : public Raccon{public:RedRanda(){};RedRanda(int val){};private:
}

如果同类对象作为初值,那么bitwise copy就绰绰有余了:

// 简单的bitwise copy就足够了
Raccon rocky;
Raccon little_critter = rocky;

如果以子对象作为父对象的初值,编译器必须判断"后继当程序员视图存取其ZooAnimal子对象时是否能够正确的执行":

// 简单的bitwise copy不够,编译器必须能够明确little_critter的虚基类pointer/offset初始化
RedRanda little_red;
Raccon little_critter = little_red;

在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个拷贝构造函数,安插一些码以设定虚基类pointer/offset的初值,对每一个成员指向必要的初始化操作,以及执行它们的内存相关操作

再看一种情况:

// 简单的bitwise copy可能够用,也可能不够用
Raccon *ptr;
Raccon lillte_critter = *ptr;

上面编译器无法知道是否bitwise copy semantics还保持着,因为它无法知道Raccon指针是否指向一个真正的Raccon对象,还是指向一个派生类对象。

这里有一个有趣的问题:当一个初始化操作存在并保持着bitwise copy semantics的状态时,如果编译器能够保证对象有正确而相等的初始化操作,是否它应该压抑拷贝构造函数的调用,以使其所产生的程序代码优化?

  • 如果是合成的拷贝构造函数,程序副作用为〇,会优化
  • 如果这个拷贝构造是由类设计者提供的呢?

C/C++编程:拷贝构造函数的构建操作相关推荐

  1. 继承关系中的拷贝构造函数和赋值操作重载函数分析

    文章目录 1 继承关系中的拷贝构造函数和赋值操作重载函数分析 1 继承关系中的拷贝构造函数和赋值操作重载函数分析 在继承关系中,如果子类未实现拷贝构造函数,那么在子类进行拷贝构造操作时,会直接调用父类 ...

  2. 类的6个默认成员函数:构造函数、析构函数、拷贝构造函数、重载运算符、三/五法则

    文章目录 6个默认成员函数 构造函数 概念 默认构造函数的类型 默认实参 概念 默认实参的使用 默认实参声明 全局变量作为默认实参 某些类不能依赖于编译器合成的默认构造函数 第一个原因 第二个原因 第 ...

  3. 构造函数、拷贝构造函数、赋值函数和析构函数

    文章目录 一.构造函数 1.认识构造函数 2.初始化列表 二.拷贝构造函数 1.类对象的拷贝 2.浅拷贝和深拷贝 三.赋值函数 四.析构函数 1.认识析构函数 2.销毁,清理? 3.析构函数来阻止该类 ...

  4. QObject 的拷贝构造和赋值操作

    QOject 中没有提供一个拷贝构造函数和赋值操作符给外界使用,其实拷贝构造和赋值的操作都是已经声明了的,但是它们被使用了Q_DISABLE_COPY () 宏放在了private区域.因此所有继承自 ...

  5. c++复习(2)拷贝构造函数与运算符重载

    目录 前言 拷贝构造函数 函数定义 调用 缺省(默认)的拷贝构造函数 -- 浅拷贝 涉及指针或者内存操作 用char * 用char[] 用string 自己写的拷贝构造函数 类中数据含有指针 类中含 ...

  6. C/C++编程:默认构造函数的建构操作

    C++标准提出:默认构造函数会在需要的时候被编译器生成.那什么时候被需要?被谁需要?做什么呢? class Foo{public:int val;Foo *next; };void foo_bar() ...

  7. C++编程思想:指针,引用,拷贝构造函数,赋值运算符

    文章目录 对指针的引用和指针的指针到底是怎么一回事? 函数返回引用到底返回了什么? 拷贝构造函数和默认拷贝构造函数 自定义拷贝构造函数 使用默认拷贝构造函数 =赋值运算符重载 对指针的引用和指针的指针 ...

  8. 类和对象编程(四):拷贝构造函数

    C++ 拷贝构造函数 拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象.拷贝构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象. 复制 ...

  9. C++编程思想 第1卷 第11章 引用和拷贝构造函数 拷贝构造函数 拷贝构造函数

    编译器对如何从现有的对象产生新的对象进行了假定. 当通过按值传递的方式传递一个对象时,就创立了一个新对象,函数体内的 对象是由函数体外的原来存在的对象传递的 编译器假定我们想使用位拷贝来创建对象 每当 ...

最新文章

  1. 诊断IIS中的ASP0115错误
  2. Flink在美团的实践与应用--大数据技术栈15
  3. Azure上的VM代理及可扩展程序
  4. Leetcode(20210419-20210425 第二周 每日一题)
  5. USACO1.3.4 Prime Cryptarithm 牛式 解题报告(模拟)
  6. Spark API 详解(转)
  7. SAP Cloud for Customer Lead OData服务的ETAG字段
  8. 财务部门:你需要多长时间才能够回答老板的这些问题?
  9. CF#574E. OpenStreetMap 题解
  10. 又被分治题卡住好几个小时!用最笨的方法搞懂分治法边界,告别死循环!
  11. Fun with Opterons, SATA, and INNODB
  12. Lenovo DS存储Linux下ISCSI 多路径映射配置
  13. golang打包流程
  14. 静态背景下运动目标检测 matlab_动态拉伸、静态拉伸你做对了么?
  15. 算法题6 b站扭蛋机
  16. 3Dmax使用者快速上手Maya心得之建模
  17. Topic 15. 临床预测模型之决策曲线 (DCA)
  18. 推进全息智慧情报研判,助力构建现代交通安全防控体系
  19. 图片鼠标移入图片改变颜色、显示另外一张图片(2种方式)
  20. ST7789V初始化代码

热门文章

  1. PointNet代码详细解释(Pytorch版本)
  2. 隔空操作鼠标——基于人工智能的鼠标控制器
  3. 西电大网络工程与计算机专业,西安电子科技大学:除了计算机和通信工程,这些专业高考也很热门...
  4. 熵简技术谈 | 熵简科技在资管数据中台的探索与实践
  5. 三菱FX1N PLC 485与三菱变频器modbus通讯可直接拿来实用了,三菱FX PLC与三菱变频器通讯
  6. 百练+二叉树操作+直接找到父节点,然后交换左右儿子,递归
  7. 中文文本分析(3)--文本相似度
  8. 优思学院:空降兵,是组织的一场考验
  9. python opencv 实现透视变换——将侧视图进行正投影
  10. python编写格斗游戏_基于C++语言编程格斗游戏毕业设计正文