面试题:C++ 什么时候生成默认拷贝构造函数?

旅行   2024-09-29 12:01   广东  

欢迎关注本公众号,专注面试题拆解

分享一套视频课程:《C++实现百万并发服务器》 面试需要项目的可以找我获取
,免费分享。 欢迎V:fb964919126







C++ 什么时候生成默认拷贝构造函数?





默认拷贝构造函数主要的语义是用一个类对象去初始化另外一个类对象。

这里需要说明一点拷贝和复制是一个意思,对应的英文单词都是copy,有些人会习惯叫默认复制构造函数,是一个意思。


一般来说,对象的创建分为两个阶段,分配内存空间,进行初始化操作。


分配内存是创建对象的第一步,它涉及到在程序的不同存储区域(如堆区、栈区或全局数据区)预留足够的内存空间来存放对象的数据成员。这个时候的内存还比较白纸化,没有被污染,它所包含的数据一般是零值或者随机值,没有实际的意义。


初始化是是创建对象的第二步,是为新分配的内存首次赋予值的过程。初始化确保对象处于一个有效且可预测的状态。注意是首次赋值,再次赋值不叫初始化。


要知道什么时候生成默认拷贝构造函数,我们首先需要了解什么时候调用拷贝构造函数?

调用拷贝构造函数的三种常见情况:

(1)用类的一个对象去初始化另一个对象时

class X {...};X x;X xx = x;//明确的以一个object的内容作为另外一个class object的初值


(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用

extern void foo(X x) 


(3)当函数的返回值是类的对象(这里不考虑返回值优化的问题)

X foo_bar(){  X xx;  return xx;}

如果提供了拷贝构造函数上面三种情况都会调用拷贝构造函数。


接下来讨论默认拷贝构造函数


这里会有一个常见的理解误区:如果没有显提供拷贝构造函数,编译器就会自动生成一个默认拷贝构造函数。

在《深度探索C++对象模型》这本书当中,作者明确说明了:


如果一个class未定义copy constructor,编译器就自动为它产生出一个这句话不对,而是应该像ARM所说:Default constructors和copy constructors在必要的时候才有编译器产生出来

书籍:《深度探索C++对象模型》

作者在书中提到了一个概念:位逐次拷贝(bitwis copy semantics)。

C++标准把copy constructor区分为trivial 和nonttivial两种,只有nontrivial的实体才会被合成到程序当中。而决定一个copy constructor是否为trivial的标准就是看这个类是否符合位拷贝语义。


如果不符合位拷贝语义,才会生成默认拷贝构造函数。

如果符合位拷贝语义,将不会生成默认拷贝构造函数,而是执行的默认的逐个成员初始化动作(default memberwise initialization)。


这里有点拗口,仔细品味下。(默认拷贝构造函数的行为表现出来和逐个成员初始化动作一样,但是是不一样的定义


我们来看一个示例:类展现了位拷贝语义

class word{public:  word(const char*){    cnt = 9;    str = new char[10];  }  ~word(){    delete[] str;  }private:  int cnt;  char* str;};
int main(){ word a("hello");  word b = a; return 0;}

这个示例代码,类是没有显示声明拷贝构造函数的,但是编译器也不会为它生成一个默认的拷贝构造函数。因为类的上述声明展现了位拷贝语义,所以执行的是默认的逐个成员初始化动作。


书中这里讲的其实有点晦涩难懂,尤其先讲的符合位拷贝语义,会给人难以理解,如果先讲不符合位拷贝语义的,剩下的就是符合位拷贝语义的,更好理解一点。当人这里,我们还是自己用汇编来验证下书中所说。

汇编代码:

通过汇编代码可以看到

word a("hello")

call了一个初始化函数 又call了word的构造函数

word b = a

只call了一个初始化函数,没有任何构造函数的call


下面我们看不符合位拷贝语义的情况

第一种情况:某个类有内部类对象,内部类包含拷贝构造函数(不管是被class设计者明确声明还是编译器合成)

示例:

#include <iostream>class A{public:   A(){}   A(A&){}};
class word{public: word() { cnt = 99; str = nullptr; } ~word() { delete[] str;  }  A a;private: int cnt; char* str;};
int main(){ word a;  word b = a; return 0;}

word类含有类对象A,类A显示声明了拷贝构造函数,word类没有显示声明拷贝构造函数,编译器必须生成一个默认拷贝构造函数。

我们还是看汇编代码:


可以看到word b =a 的汇编代码调用了构造函数。


第二种情况:某个类继承基类,而基类里面有拷贝构造函数(不管是被class设计者明确声明还是编译器合成)

示例:

#include <iostream>
class A{public: A(){} A(A&){}};class word : public A{public: word() { cnt = 99; str = nullptr; } ~word() { delete[] str; }private: int cnt; char* str;};
int main(){ word a; word b = a;
return 0;}

word类继承自基类A,基类A显示声明了拷贝构造函数,word类没有显示声明拷贝构造函数,编译器必须生成一个默认拷贝构造函数。

我们还是看汇编代码:

可以看到word b =a 的汇编代码调用了构造函数。


第三种情况:类里面有虚函数

示例:

class ZoonAnimal{public:  ZoonAnimal();  virtual ~ZoonAnimal();  virtual void animate();  virtual void draw();private:  //数据};
class Bear : public ZoonAnimal{public: Bear(); void animate(); void draw();  virtual void cance();private: //数据};int main(){ Bear a;  Bear b = a;  ZoonAnimal c = a; return 0;}

含有虚函数的类,会有虚函数指针(Vptr);ZoonAnimal c = a;这句代码如果没有拷贝构造函数,那么vptr会发生切割行为,c的vptr是不应该指向基类虚函数表,而应该指向子类的虚函数表,所以这里必须生成一个默认的拷贝构造函数来完成这个动作,而不是简单的位拷贝进行逐个成员初始化。


第四种情况:类存在虚继承

示例

#include <iostream>class ZoonAnimal{public:  ZoonAnimal();  virtual ~ZoonAnimal();  virtual void animate();  virtual void draw();private:  //数据};
class Bear : public virtual ZoonAnimal{public: Bear(); void animate(); void draw(); virtual void cance();
private: //数据};class ReadBear : public Bear{public: ReadBear();};
int main(){ ReadBear a; Bear b = a;
return 0;}

Bear 虚继承 ZoonAnimal,Bear也有子类,Bear b = a;当基类对象b指向子类对象a的时候也会发生虚指针偏移,所以这里需要生成默认的拷贝构造函数。


总结:

如果没有显示提供拷贝构造函数,编译器不是一定会自动生成一个默认拷贝构造函数。只有不符合拷贝语义的情况才会生成默认拷贝构造函数。


题外话:需要<<深度探索C++对象模型>>这本书电子版的,加v:fb964919126

end



CppPlayer 



关注,回复【电子书】珍藏CPP电子书资料赠送

精彩文章合集

专题推荐

【专辑】计算机网络真题拆解
【专辑】大厂最新真题
【专辑】C/C++面试真题拆解

CppPlayer
一个专注面试题拆解的公众号
 最新文章