文章

C++ 特殊成员函数、虚继承、浅拷贝深拷贝

特殊成员函数

架构

alt text

合成:编译器自动生成

默认:特殊成员函数的最基本形式

构造

  • 默认构造函数:可以不用实参(也可以提供默认实参)进行调用的构造函数
  • 能否重载:能
  • 能否声明为虚函数:构造函数不能声明为虚函数
    • 构造函数声明为虚函数会产生虚表,虚表指针,虚构造函数存放在虚表中,需通过虚表指针访问
    • 虚表指针在构造函数内初始化,即指向虚表
    • 调用构造函数前,未初始化虚指针,因此虚构造函数无法访问,对于类来说无法访问构造函数,则不能创建类对象,否则会出现编译错误
  • 合成默认构造函数的合成条件:
    • 当没有自定义构造函数时,且需要触发以下条件之一:
      • 含有类对象数据成员,且该类对象有默认构造函数
      • 类派生自一个含有默认构造函数的基类时
      • 类包含虚函数:本身定义的 / 继承重写的
      • 带有虚继承的类
  • 构造函数调用时机:定义类对象时自动调用
  • 合成默认构造函数的合成时机:定义类对象时才会开始合成
1
2
3
4
5
class MyClass {
public:
    MyClass() { ... }                    
    MyClass(int x = 0, int y = 0) { ... }
};

展开

  • 自定义默认构造函数,显示编写的,而合成默认构造函数是隐式的,它们都不需要参数调用
  • 类成员初始化方式:
    • 注:20以下为VS_Debug模式下测试结果,对于诸如对类成员的默认初始化的方式、未满足合成条件的行为、未定义的值……受到编译器、调试模式、项目设置……影响,因此以下结果只作为参考
    • 定义类对象
    • 按照类定义中的声明顺序,为所有类成员分配内存空间
    • 如果类对象使用默认初始化方式定义的
      • 如果有自定义的默认构造函数
        • 成员初始化列表初始化
          • 如果在列表中,列表显示初始化
          • 否则如果存在类内初始值,初始化它
          • 否则对成员执行默认初始化(如果成员是类类型要递归处理)
        • 调用构造函数体
          • 函数体内赋值:如果有赋值操作,进行赋值,会覆盖初始值
          • 其他操作指令……
      • 如果有合成的默认构造函数
        • 如果存在类内初始值,初始化它
        • 否则对成员执行默认初始化(如果成员是类类型要递归处理)
      • 如果没有默认构造,发生错误
    • 如果类对象使用值初始化方式定义的
      • 如果有自定义的默认构造函数,同上(最后一步改为零初始化
      • 如果有合成的默认构造函数,同上(最后一步改为零初始化
      • 如果没有默认构造,发生错误
    • 如果类对象使用直接/拷贝初始化方式定义的
      • 如果有匹配的自定义非默认构造函数,同上
      • 如果没有匹配的自定义非默认构造函数,发生错误
  • 哪些成员必须通过成员初始化列表进行初始化:
    • 常量成员变量,虽然允许类内初始值,但C++语法要求必须通过成员初始化列表进行初始化
    • 引用类型成员变量
    • 没有默认构造函数类类型成员, 应显示调用有参构造函数
    • 继承体系中基类构造函数
  • 成员变量的初始化顺序:与它们在类定义中的声明顺序相同,而不是它们在初始化列表中出现的顺序
  • 静态变量不能出现在列表中,因为它不属于具体类对象,它必须在类外显示定义和初始化
  • 继承体系中基类构造函数调用方式
    • 显示/隐式调用:派生类 显示的在其初始化列表中调用基类的构造函数 / 隐式调用基类的默认构造函数,如果基类没有默认构造函数,将编译失败
    • 只有调用了基类的构造函数,才能初始化基类部分
  • 继承体系中构造函数调用顺序是递归的
    • 调用顺序:派生类 -> 直接基类 -> 间接基类
    • 执行顺序:间接基类 -> 直接基类 -> 派生类

析构

1
~ MyClass();

展开

  • 析构函数合成条件:当未自定义析构函数时,且基类和成员变量均有可访问的析构函数
  • 能否重载:不能像构造函数一样重载,一个类仅有一个析构函数
  • 能否声明为虚函数:析构函数可以声明为虚函数,特别当基类指针调用时,最好声明为虚函数,否则会导致内存泄漏
  • 析构函数调用时机:作用域结束/调用delete清理内存时
  • 合成的默认析构函数做了什么:会对类内类类型成员按照声明逆序调用各自的析构函数,内置类型成员无需额外操作
  • 析构顺序:当基类指针指向派生类,并且基类为虚析构,执行顺序:派生类 -> 直接基类 -> 间接基类

拷贝构造

1
MyClass(const MyClass& class);

展开

  • 拷贝构造函数形式:
  • 能否重载:能,但有一定限制,const/非const版本,其余参数均有默认实参
  • 能否声明为虚函数:不能
  • 调用时机:使用拷贝初始化方式定义类对象
  • 形参必须是引用:A = B,A的拷贝构造传入B,B拷贝给形参,A的形参的拷贝构造传入B,B拷贝给形参的形参,A的形参的形参的拷贝构造传入B……无限循环
  • 合成的默认拷贝构造做了什么:将B类每个成员通过赋值方式(浅拷贝)传递给A类对应成员
  • 继承体系中基类拷贝构造函数调用方式
    • 显示的在初始化列表中调用基类的拷贝构造函数 / 隐式调用基类的默认拷贝构造函数,参数是派生类的实参
  • 继承体系中拷贝构造函数调用顺序
    • 调用顺序和普通的构造函数相同

拷贝赋值

1
2
3
4
5
6
7
8
MyClass& operator=(const MyClass& b){
  ……
  if(this != &b){//避免自赋值
    delete this->p;
    this->p = new type(*(b.p));
  }
	return *this;//返回左侧运算对象的引用
}

展开

  • 拷贝赋值函数:2元运算符,应该返回自身引用(可以支持连续赋值)
  • 能否重载:能,但有一定限制,const/非const版本,其余参数均有默认实参
  • 能否声明为虚函数:不能
  • 调用时机:A已初始化,使用同类类型对象B赋值时
  • 合成的默认拷贝赋值做了什么:将B类每个成员通过赋值方式(浅拷贝)传递给A类对应成员
  • 避免自赋值:
    • 提高效率
    • 如果类包含指针成员,将数据丢失

注:移动构造,移动赋值: 详见其他章节

指针数据成员

  • 如果一个类有指针数据成员,合成的析构函数不会delete它,因此需要自定义
  • 对于合成的拷贝构造和赋值,将会浅拷贝指针,多个指针指向同一内存,会多次释放同一内存,因此也需要自定义

= default = delete

1
2
3
MyClass() = default;  // 等价于:MyClass();
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;

展开

default:创建合成的默认函数,相比于自定义默认函数,效率要高,适用于所有特殊成员函数(注意:默认版本要保持唯一性,非默认版本可以有重载)

delete:表示此函数不能被调用,适用于任何成员函数

  • 修饰构造;禁止创建此类型的变量
  • 修饰析构;禁止创建此类型的变量,但是可以动态分配内存,但不能delete
  • 修饰拷贝构造/赋值;禁止拷贝
  • 修饰移动构造/赋值;禁止移动
  • 如果类某成员的析构=delete,则类的析构、拷贝构造、构造也=delete
  • 如果类某成员的拷贝构造=delete,则类的拷贝构造也=delete
  • 如果类某成员的拷贝赋值=delete/const成员/引用成员,则类的拷贝赋值也=delete
  • 类const成员没有在列表初始化并且没有显示定义默认构造/引用成员没有在列表初始化,,则类的构造也=delete

private:

  • 也可以达到=delete的效果,但是比较麻烦,
  • 比如拷贝构造为private,构造析构为public,因此可以创建对象但不能拷贝对象,
  • 但是对于友元和成员函数,仍可以拷贝对象,为了阻止此行为,我们仅声明拷贝构造不定义它,
  • 如果外部调用它,将发生编译错误,如果友元/成员调用了它,将发生链接错误
  • 而对于delete的话比较简单,只需要将它们=delete,外部/友元/成员调用它,都将出现编译错误
  • 因此优先使用=delete

虚继承

alt text

1
2
3
4
class X  { public: int i; };
class A : virtual public X{ public:int j; };
class B : virtual public X{ public:double d; };
class C : public A, public B{ public: int k; };

展开

  • 虚继承:多重继承下确保子类对象中,每个父类只含有一个副本,解决菱形继承问题
  • 虚基类:若类A虚继承于类X,则对于A来说,类X是类A的虚基类
  • 虚继承会一直传递到最终派生类

浅拷贝与深拷贝

  • 浅拷贝
    • 定义:对类对象中的成员进行简单的赋值
    • 问题:
      • 存在静态成员
        • 静态成员不会被拷贝,多个类对象共享同一静态成员
      • 存在动态成员
        • 由于默认赋值操作:A_p = B_p, 是将B_p指向的内存地址拷贝给A_p,因此它们共享同一内存,共同指向同一个对象
        • 会出现多次释放同一内存的错误
        • 因此要使用深拷贝,在拷贝构造/赋值
  • 深拷贝
    • 定义:对类对象中的成员非进行简单的赋值
    1
    2
    
    A_p = new type();//分配独立内存
    *A_p = *B_p;//值拷贝
    

    展开

    • 方式:分配一块新的内存,将值拷贝过去
本文由作者按照 CC BY 4.0 进行授权
本页总访问量