C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计

C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 《C++从入门到入土》 ✨ 数据即知识,压缩即智能


文章目录

  • [C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计](#C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计)
    • 前言
    • 一、派生类对象由哪些部分组成?
      • [1.1 基类子对象和派生类成员](#1.1 基类子对象和派生类成员)
      • [1.2 对象初始化不是简单地执行构造函数体](#1.2 对象初始化不是简单地执行构造函数体)
    • 二、派生类构造函数如何初始化基类?
      • [2.1 基类有默认构造函数](#2.1 基类有默认构造函数)
      • [2.2 基类没有默认构造函数](#2.2 基类没有默认构造函数)
    • 三、派生类的拷贝构造
      • [3.1 编译器默认生成时会处理基类部分](#3.1 编译器默认生成时会处理基类部分)
      • [3.2 手动编写时要显式拷贝基类](#3.2 手动编写时要显式拷贝基类)
    • 四、派生类的赋值运算符
      • [4.1 赋值也要处理两部分](#4.1 赋值也要处理两部分)
      • [4.2 可以优先使用 = default](#4.2 可以优先使用 = default)
    • 五、构造和析构顺序
      • [5.1 构造顺序](#5.1 构造顺序)
      • [5.2 析构顺序](#5.2 析构顺序)
    • [六、基类析构函数什么时候要加 virtual?](#六、基类析构函数什么时候要加 virtual?)
      • [6.1 通过基类指针删除派生对象](#6.1 通过基类指针删除派生对象)
      • [6.2 哪些基类需要虚析构?](#6.2 哪些基类需要虚析构?)
    • 七、如何禁止一个类被继承?
      • [7.1 使用 final](#7.1 使用 final)
      • [7.2 私有构造函数的旧思路](#7.2 私有构造函数的旧思路)
    • 八、友元关系不会沿继承链传播
      • [8.1 基类友元只能访问基类](#8.1 基类友元只能访问基类)
      • [8.2 友元也不具有传递性](#8.2 友元也不具有传递性)
    • 九、静态成员在继承体系中只有一份
      • [9.1 普通成员每个对象各有一份](#9.1 普通成员每个对象各有一份)
      • [9.2 static 成员属于类](#9.2 static 成员属于类)
    • 十、多继承是什么?
      • [10.1 单继承](#10.1 单继承)
      • [10.2 多继承](#10.2 多继承)
      • [10.3 多继承中的同名成员二义性](#10.3 多继承中的同名成员二义性)
    • 十一、多继承中的指针调整
      • [11.1 不同基类指针可能具有不同地址值](#11.1 不同基类指针可能具有不同地址值)
      • [11.2 不要依赖具体布局](#11.2 不要依赖具体布局)
    • 十二、菱形继承为什么麻烦?
      • [12.1 菱形结构](#12.1 菱形结构)
      • [12.2 数据冗余](#12.2 数据冗余)
      • [12.3 访问二义性](#12.3 访问二义性)
    • 十三、虚继承如何解决菱形问题?
      • [13.1 声明虚继承](#13.1 声明虚继承)
      • [13.2 virtual 写在什么位置?](#13.2 virtual 写在什么位置?)
    • 十四、虚基类由谁负责构造?
      • [14.1 普通继承中的基类构造](#14.1 普通继承中的基类构造)
      • [14.2 虚继承中的最派生类规则](#14.2 虚继承中的最派生类规则)
      • [14.3 单独构造中间类时仍由中间类负责](#14.3 单独构造中间类时仍由中间类负责)
    • 十五、标准库中的虚继承例子
    • 十六、继承和组合如何选择?
      • [16.1 继承表达 is-a](#16.1 继承表达 is-a)
      • [16.2 组合表达 has-a](#16.2 组合表达 has-a)
      • [16.3 白盒复用和黑盒复用](#16.3 白盒复用和黑盒复用)
      • [16.4 优先组合,但不是拒绝继承](#16.4 优先组合,但不是拒绝继承)
      • [16.5 一个简单判断流程](#16.5 一个简单判断流程)
    • 十七、常见错误整理
      • [17.1 在构造函数体中"调用"基类构造](#17.1 在构造函数体中“调用”基类构造)
      • [17.2 手动写拷贝构造却忘记基类部分](#17.2 手动写拷贝构造却忘记基类部分)
      • [17.3 派生类赋值时只复制自己的成员](#17.3 派生类赋值时只复制自己的成员)
      • [17.4 多态基类没有虚析构函数](#17.4 多态基类没有虚析构函数)
      • [17.5 把友元理解成可继承权限](#17.5 把友元理解成可继承权限)
      • [17.6 认为每个对象都有一份 static 成员](#17.6 认为每个对象都有一份 static 成员)
      • [17.7 用类域解决菱形继承后认为问题全部消失](#17.7 用类域解决菱形继承后认为问题全部消失)
      • [17.8 依赖多继承对象的具体内存排列](#17.8 依赖多继承对象的具体内存排列)
      • [17.9 为了复用实现随意使用 public 继承](#17.9 为了复用实现随意使用 public 继承)
    • 总结

前言

掌握继承的基本语法后,接下来会遇到一些更容易出错的问题:

  • 派生类构造时,基类部分由谁初始化?
  • 拷贝构造和赋值运算符是否需要处理基类?
  • 为什么构造顺序是从基类到派生类,析构顺序却正好相反?
  • 基类析构函数什么时候必须是虚函数?
  • 友元关系和静态成员会不会被继承?
  • 多继承为什么会产生二义性?
  • 菱形继承为什么会造成数据冗余?
  • 虚继承如何让多个路径共享同一个基类子对象?
  • 继承和组合到底应该如何选择?

这些内容看起来比较分散,实际上都围绕一个核心问题:

一个派生类对象中,不仅有派生类自己的成员,还包含基类子对象。

接下来我们就围绕着这点展开


一、派生类对象由哪些部分组成?

1.1 基类子对象和派生类成员

假设有下面两个类:

cpp 复制代码
#include <string>
using namespace std;

class Person
{
protected:
    string _name;
    int _age = 0;
};

class Student : public Person
{
protected:
    int _studentId = 0;
};

一个 Student 对象可以抽象成:

text 复制代码
Student 对象
├── Person 基类子对象
│   ├── _name
│   └── _age
└── Student 自己的成员
    └── _studentId

这里的 Person 不是独立于 Student 的另一个对象,而是 Student 对象内部的一个基类子对象。

因此,构造 Student 时,需要先让 Person 部分进入有效状态,再初始化学生自己的成员。

1.2 对象初始化不是简单地执行构造函数体

构造对象时,真正的顺序大致是:

text 复制代码
虚基类
→ 直接非虚基类
→ 成员变量
→ 派生类构造函数体

其中:

  • 基类按照继承列表中的声明顺序初始化
  • 成员变量按照它们在类中的声明顺序初始化
  • 和初始化列表中的书写先后无关

因此不要故意把初始化列表写成与声明顺序不同的样子,虽然程序可能仍能编译,但阅读起来很容易产生误解。


二、派生类构造函数如何初始化基类?

2.1 基类有默认构造函数

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
    Person()
    {
        cout << "Person()" << endl;
    }

protected:
    string _name;
};

class Student : public Person
{
public:
    Student()
    {
        cout << "Student()" << endl;
    }

private:
    int _studentId = 0;
};

创建对象:

cpp 复制代码
Student student;

输出顺序是:

text 复制代码
Person()
Student()

编译器会先构造基类子对象,再执行派生类构造函数。

2.2 基类没有默认构造函数

假设基类只能通过姓名构造:

cpp 复制代码
class Person
{
public:
    explicit Person(const string& name)
        : _name(name)
    {
        cout << "Person(const string&)" << endl;
    }

protected:
    string _name;
};

派生类必须在初始化列表中明确调用基类构造函数:

cpp 复制代码
class Student : public Person
{
public:
    Student(const string& name, int studentId)
        : Person(name)
        , _studentId(studentId)
    {
        cout << "Student(const string&, int)" << endl;
    }

private:
    int _studentId;
};

不能写成:

cpp 复制代码
Student(const string& name, int studentId)
{
    Person(name);
}

因为构造函数体开始执行时,基类子对象早就应该完成初始化了。

函数体中的:

cpp 复制代码
Person(name);

只会创建一个临时的 Person 对象,并不能重新初始化当前对象中的基类部分。


三、派生类的拷贝构造

3.1 编译器默认生成时会处理基类部分

cpp 复制代码
class Person
{
public:
    Person(const Person& other)
        : _name(other._name)
    {
    }

protected:
    string _name;
};

class Student : public Person
{
private:
    int _studentId = 0;
};

如果没有手动编写 Student 的拷贝构造,编译器生成的版本会依次拷贝:

text 复制代码
Person 基类子对象
Student 自己的成员

3.2 手动编写时要显式拷贝基类

cpp 复制代码
class Student : public Person
{
public:
    Student(const string& name, int studentId)
        : Person(name)
        , _studentId(studentId)
    {
    }

    Student(const Student& other)
        : Person(other)
        , _studentId(other._studentId)
    {
    }

private:
    int _studentId;
};

其中:

cpp 复制代码
Person(other)

负责调用基类的拷贝构造,复制基类部分。

如果忘记写这一项,编译器会尝试默认构造基类,而不是自动猜测你想调用基类拷贝构造。

如果基类没有默认构造函数,就会直接编译失败。


四、派生类的赋值运算符

4.1 赋值也要处理两部分

派生类赋值时,需要复制:

text 复制代码
基类部分
派生类自己的部分

可以这样实现:

cpp 复制代码
class Student : public Person
{
public:
    Student(const string& name, int studentId)
        : Person(name)
        , _studentId(studentId)
    {
    }

    Student& operator=(const Student& other)
    {
        if (this != &other)
        {
            Person::operator=(other);
            _studentId = other._studentId;
        }

        return *this;
    }

private:
    int _studentId;
};

这里需要明确写:

cpp 复制代码
Person::operator=(other);

原因是派生类自己的 operator= 会隐藏基类中的同名函数。

如果只写:

cpp 复制代码
operator=(other);

很可能再次调用当前派生类的赋值函数,形成错误递归。

4.2 可以优先使用 = default

如果成员本身都支持正确的拷贝和赋值,通常不需要手动写这些函数。

cpp 复制代码
class Student : public Person
{
public:
    Student(const Student&) = default;
    Student& operator=(const Student&) = default;

private:
    int _studentId = 0;
};

编译器生成的版本会自动处理基类子对象和成员变量。

只有在类需要特殊资源管理、日志或额外约束时,才有必要手动实现。


五、构造和析构顺序

5.1 构造顺序

假设:

cpp 复制代码
class Base
{
public:
    Base()
    {
        cout << "Base()" << endl;
    }
};

class Member
{
public:
    Member()
    {
        cout << "Member()" << endl;
    }
};

class Derived : public Base
{
public:
    Derived()
    {
        cout << "Derived()" << endl;
    }

private:
    Member _member;
};

创建:

cpp 复制代码
Derived object;

输出:

text 复制代码
Base()
Member()
Derived()

构造顺序为:

text 复制代码
基类
→ 成员对象
→ 派生类构造函数体

5.2 析构顺序

对象销毁时顺序反过来:

text 复制代码
派生类析构函数体
→ 成员对象
→ 基类

如果加入析构输出,结果大致是:

text 复制代码
~Derived()
~Member()
~Base()

为什么析构要反过来?

因为派生类可能还依赖基类提供的状态。只有先清理最外层的派生类,再逐层清理它依赖的内部部分,才能保持对象生命周期的完整性。

可以把它理解成搭积木:

text 复制代码
搭建时:先底座,再上层
拆除时:先上层,再底座

六、基类析构函数什么时候要加 virtual?

6.1 通过基类指针删除派生对象

看下面的代码:

cpp 复制代码
class Base
{
public:
    ~Base()
    {
        cout << "~Base()" << endl;
    }
};

class Derived : public Base
{
public:
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
};

如果执行:

cpp 复制代码
Base* ptr = new Derived;
delete ptr;

基类析构函数不是虚函数时,通过基类指针删除派生对象会产生未定义行为。

正确设计通常是:

cpp 复制代码
class Base
{
public:
    virtual ~Base() = default;
};

此时:

cpp 复制代码
Base* ptr = new Derived;
delete ptr;

会先调用:

text 复制代码
Derived::~Derived()

再调用:

text 复制代码
Base::~Base()

6.2 哪些基类需要虚析构?

如果一个类满足下面的情况:

text 复制代码
它具有虚函数
它会被当作多态基类使用
外部可能通过 Base* 删除派生对象

那么它通常应该具有公有虚析构函数:

cpp 复制代码
virtual ~Base() = default;

如果明确禁止通过基类指针删除对象,也可以把析构函数设计成保护的非虚函数,但这种设计需要更明确的约束。


七、如何禁止一个类被继承?

7.1 使用 final

C++11 提供了 final

cpp 复制代码
class Utility final
{
public:
    void run()
    {
    }
};

下面的代码会编译失败:

cpp 复制代码
class Derived : public Utility
{
};

final 明确表示:

当前类不允许继续派生。

它比通过特殊构造函数权限绕开继承更加直观。

7.2 私有构造函数的旧思路

在旧式设计中,也可以把基类构造函数设为私有:

cpp 复制代码
class Base
{
private:
    Base() = default;
};

派生类无法调用基类构造函数,因此通常无法正常构造。

不过这种写法表达意图不够直接,也会影响基类本身的创建方式。现代 C++ 中,禁止继承优先使用 final


八、友元关系不会沿继承链传播

8.1 基类友元只能访问基类

cpp 复制代码
class Student;

class Person
{
    friend void display(const Person&, const Student&);

protected:
    string _name;
};

class Student : public Person
{
protected:
    int _studentId = 0;
};

函数:

cpp 复制代码
void display(const Person& person, const Student& student)
{
    cout << person._name << endl;

    // cout << student._studentId << endl; // 错误
}

虽然 displayPerson 的友元,但它不是 Student 的友元。

友元关系不会自动继承。

如果确实需要访问学生的非公有成员,需要在 Student 中再次声明:

cpp 复制代码
class Student : public Person
{
    friend void display(const Person&, const Student&);

protected:
    int _studentId = 0;
};

8.2 友元也不具有传递性

即使:

text 复制代码
A 是 B 的友元
B 是 C 的友元

也不能推出:

text 复制代码
A 是 C 的友元

友元是一种由类明确授予的访问权限,不应当沿关系链自动扩散。


九、静态成员在继承体系中只有一份

9.1 普通成员每个对象各有一份

cpp 复制代码
class Person
{
public:
    string _name;
};

class Student : public Person
{
};

创建:

cpp 复制代码
Person person;
Student student;

person._namestudent._name 属于不同对象,地址也不同。

9.2 static 成员属于类

cpp 复制代码
class Person
{
public:
    static int _count;
};

int Person::_count = 0;

class Student : public Person
{
};

无论通过哪个类名访问:

cpp 复制代码
Person::_count = 10;

cout << Person::_count << endl;
cout << Student::_count << endl;

访问的都是同一个静态成员。

也可以通过对象访问:

cpp 复制代码
Person person;
Student student;

cout << &person._count << endl;
cout << &student._count << endl;

两个地址相同。

静态成员不属于某个具体对象,而属于整个类及其继承体系所引用的那一个声明。


十、多继承是什么?

10.1 单继承

一个派生类只有一个直接基类:

cpp 复制代码
class Student : public Person
{
};

关系为:

text 复制代码
Person
   ↓
Student

10.2 多继承

一个派生类同时拥有两个或更多直接基类:

cpp 复制代码
class Scanner
{
public:
    void scan()
    {
    }
};

class Printer
{
public:
    void print()
    {
    }
};

class AllInOneMachine : public Scanner, public Printer
{
};

此时:

text 复制代码
Scanner      Printer
    \          /
     \        /
 AllInOneMachine

对象可以同时使用两个基类提供的接口:

cpp 复制代码
AllInOneMachine machine;

machine.scan();
machine.print();

10.3 多继承中的同名成员二义性

如果两个基类都有同名函数:

cpp 复制代码
class Scanner
{
public:
    void start()
    {
        cout << "Scanner::start()" << endl;
    }
};

class Printer
{
public:
    void start()
    {
        cout << "Printer::start()" << endl;
    }
};

class AllInOneMachine : public Scanner, public Printer
{
};

调用:

cpp 复制代码
AllInOneMachine machine;

// machine.start();  // 错误:不知道调用哪个 start

需要明确指定路径:

cpp 复制代码
machine.Scanner::start();
machine.Printer::start();

多继承会让名字查找、构造过程和对象转换变得更加复杂,因此不宜为了简单复用几个函数就随意使用。


十一、多继承中的指针调整

11.1 不同基类指针可能具有不同地址值

cpp 复制代码
class Base1
{
public:
    int _value1 = 1;
};

class Base2
{
public:
    int _value2 = 2;
};

class Derived : public Base1, public Base2
{
public:
    int _value3 = 3;
};

执行:

cpp 复制代码
Derived object;

Base1* ptr1 = &object;
Base2* ptr2 = &object;
Derived* ptr3 = &object;

编译器会让每个基类指针指向它对应的基类子对象。

因此,从 Derived* 转换为某个基类指针时,编译器可能需要调整地址。

可以抽象理解为:

text 复制代码
Derived 对象
├── Base1 子对象 ← Base1*
├── Base2 子对象 ← Base2*
└── Derived 成员

11.2 不要依赖具体布局

某些编译器中,第一个非虚基类子对象可能位于对象起始位置,第二个基类会产生偏移。

但具体对象布局和偏移属于实现相关细节,不能把某个编译器观察到的地址关系当成通用语言规则。

应当依赖 C++ 的类型转换,让编译器完成正确的指针调整,而不是手动计算地址。


十二、菱形继承为什么麻烦?

12.1 菱形结构

考虑下面的类层次:

cpp 复制代码
class Person
{
public:
    string _name;
};

class Student : public Person
{
protected:
    int _studentId = 0;
};

class Teacher : public Person
{
protected:
    int _teacherId = 0;
};

class Assistant : public Student, public Teacher
{
};

结构为:

text 复制代码
        Person
       /      \
  Student    Teacher
       \      /
       Assistant

形状像一个菱形,因此称为菱形继承。

12.2 数据冗余

Student 中有一份 Person 子对象。

Teacher 中也有一份 Person 子对象。

因此 Assistant 中会出现两份普通的 Person 子对象:

text 复制代码
Assistant
├── Student
│   └── Person
│       └── _name
└── Teacher
    └── Person
        └── _name

一个助教对象中出现两个姓名,通常不是我们真正想表达的模型。

12.3 访问二义性

cpp 复制代码
Assistant assistant;

// assistant._name = "小王";  // 错误:访问路径不明确

编译器不知道要访问:

cpp 复制代码
assistant.Student::_name

还是:

cpp 复制代码
assistant.Teacher::_name

可以通过指定类域解决访问二义性:

cpp 复制代码
assistant.Student::_name = "学生路径";
assistant.Teacher::_name = "教师路径";

但两份数据仍然存在,所以数据冗余没有消失。


十三、虚继承如何解决菱形问题?

13.1 声明虚继承

让中间层虚继承 Person

cpp 复制代码
class Student : virtual public Person
{
protected:
    int _studentId = 0;
};

class Teacher : virtual public Person
{
protected:
    int _teacherId = 0;
};

class Assistant : public Student, public Teacher
{
};

此时,最派生的 Assistant 对象中只保留一份共享的 Person 虚基类子对象:

text 复制代码
        Person
       ↗      ↖
  Student    Teacher
       ↘      ↙
       Assistant

对象可以抽象成:

text 复制代码
Assistant
├── Student 部分
├── Teacher 部分
├── Assistant 自己的成员
└── 共享的 Person 虚基类子对象

于是:

cpp 复制代码
Assistant assistant;
assistant._name = "小王";

不再有两份普通 Person 子对象造成的二义性。

13.2 virtual 写在什么位置?

下面两种写法等价:

cpp 复制代码
class Student : virtual public Person
{
};
cpp 复制代码
class Student : public virtual Person
{
};

一般使用第一种或团队统一的风格即可。


十四、虚基类由谁负责构造?

这是虚继承中最容易忽略的规则。

14.1 普通继承中的基类构造

普通继承中,直接派生类负责构造自己的直接基类:

cpp 复制代码
class Student : public Person
{
public:
    Student(const string& name)
        : Person(name)
    {
    }
};

14.2 虚继承中的最派生类规则

虚基类由最派生类负责初始化。

cpp 复制代码
class Person
{
public:
    explicit Person(const string& name)
        : _name(name)
    {
    }

protected:
    string _name;
};

class Student : virtual public Person
{
public:
    Student(const string& name, int studentId)
        : Person(name)
        , _studentId(studentId)
    {
    }

private:
    int _studentId;
};

class Teacher : virtual public Person
{
public:
    Teacher(const string& name, int teacherId)
        : Person(name)
        , _teacherId(teacherId)
    {
    }

private:
    int _teacherId;
};

最终派生类:

cpp 复制代码
class Assistant : public Student, public Teacher
{
public:
    Assistant(const string& name)
        : Person(name)
        , Student("Student 路径", 1001)
        , Teacher("Teacher 路径", 2001)
    {
    }
};

创建:

cpp 复制代码
Assistant assistant("最终姓名");

共享的虚基类 Person 使用的是:

cpp 复制代码
Person(name)

也就是 Assistant 初始化列表中的参数。

StudentTeacher 初始化列表中对 Person 的构造,在创建最派生的 Assistant 对象时不会负责最终虚基类的初始化。

14.3 单独构造中间类时仍由中间类负责

如果直接创建:

cpp 复制代码
Student student("学生姓名", 1001);

此时 Student 本身就是最派生类,所以它负责初始化虚基类 Person

"最派生类"不是固定指某个类名,而是指这一次实际创建对象时最外层的完整类型。


十五、标准库中的虚继承例子

C++ 输入输出流体系是虚继承的经典例子。

可以简化理解为:

text 复制代码
             basic_ios
             /       \
    basic_istream   basic_ostream
             \       /
             basic_iostream

输入流和输出流都需要共享基础的流状态。

如果采用普通菱形继承,basic_iostream 中会出现两份基础流状态。使用虚继承后,最派生流对象只保留一份共享的 basic_ios 虚基类子对象。

这说明虚继承并非完全不能使用。

但它适合真正存在共享基类语义的场景,而不是为了展示语法而主动设计复杂菱形结构。


十六、继承和组合如何选择?

16.1 继承表达 is-a

公有继承通常表达:

text 复制代码
Derived is a Base

例如:

text 复制代码
Student is a Person
Circle is a Shape
Dog is an Animal

派生类应当能够在需要基类的地方自然替代基类。

16.2 组合表达 has-a

组合表示一个类拥有另一个类对象:

cpp 复制代码
class Tire
{
private:
    string _brand = "Michelin";
};

class Car
{
private:
    Tire _frontLeft;
    Tire _frontRight;
    Tire _rearLeft;
    Tire _rearRight;
};

关系为:

text 复制代码
Car has Tires
汽车拥有轮胎

汽车不是一种轮胎,因此不应该写:

cpp 复制代码
class Car : public Tire
{
};

16.3 白盒复用和黑盒复用

继承有时被称为白盒复用。

派生类能够看到基类的公有和保护成员,容易依赖基类内部实现。基类一旦发生改变,派生类可能受到较大影响。

组合通常被称为黑盒复用。

组合类主要通过另一个对象的公有接口使用它,不需要了解其内部细节,耦合度通常更低。

16.4 优先组合,但不是拒绝继承

"优先组合而不是继承"不是说继承不能使用。

更准确的理解是:

text 复制代码
只有确实存在稳定的 is-a 关系时,才使用 public 继承。
只是为了复用实现时,优先考虑组合。

例如,栈内部使用 vector

cpp 复制代码
template<class T>
class Stack
{
public:
    void push(const T& value)
    {
        _data.push_back(value);
    }

    void pop()
    {
        _data.pop_back();
    }

    T& top()
    {
        return _data.back();
    }

private:
    vector<T> _data;
};

这里:

text 复制代码
Stack has a vector

比:

text 复制代码
Stack is a vector

更符合实际语义。

16.5 一个简单判断流程

设计类关系时,可以依次问:

text 复制代码
1. 派生类能否自然替代基类?
   能:考虑 public 继承

2. 当前类只是需要另一个类提供能力?
   是:考虑组合

3. 只是为了复用几段实现代码?
   是:优先组合或普通成员函数复用

4. 是否需要运行时多态?
   是:通常需要基类接口和 public 继承

十七、常见错误整理

17.1 在构造函数体中"调用"基类构造

错误:

cpp 复制代码
Student(const string& name)
{
    Person(name);
}

正确:

cpp 复制代码
Student(const string& name)
    : Person(name)
{
}

17.2 手动写拷贝构造却忘记基类部分

错误:

cpp 复制代码
Student(const Student& other)
    : _studentId(other._studentId)
{
}

正确:

cpp 复制代码
Student(const Student& other)
    : Person(other)
    , _studentId(other._studentId)
{
}

17.3 派生类赋值时只复制自己的成员

错误:

cpp 复制代码
Student& operator=(const Student& other)
{
    _studentId = other._studentId;
    return *this;
}

正确:

cpp 复制代码
Student& operator=(const Student& other)
{
    if (this != &other)
    {
        Person::operator=(other);
        _studentId = other._studentId;
    }

    return *this;
}

17.4 多态基类没有虚析构函数

如果会通过基类指针删除派生对象,应当使用:

cpp 复制代码
virtual ~Base() = default;

17.5 把友元理解成可继承权限

基类的友元不是派生类的友元。

每个类都需要单独授予友元权限。

17.6 认为每个对象都有一份 static 成员

静态成员属于类,不属于具体对象。继承体系中引用的是同一个基类静态成员声明。

17.7 用类域解决菱形继承后认为问题全部消失

指定:

cpp 复制代码
assistant.Student::_name

只能解决访问路径二义性。

普通菱形继承中的两份 Person 数据仍然存在。

17.8 依赖多继承对象的具体内存排列

不同编译器和 ABI 可能采用不同布局。

正确做法是通过语言提供的指针转换访问基类子对象,而不是手工计算偏移。

17.9 为了复用实现随意使用 public 继承

public 继承会建立公开的类型替代关系。

如果只需要另一个类提供的功能,组合通常更合适。


总结

继承进阶部分的规则虽然多,但都可以从"派生类对象包含基类子对象"这一点出发理解。

需要重点掌握:

  1. 构造派生类时,必须先初始化基类子对象
  2. 手动实现拷贝构造和赋值时,要同时处理基类部分
  3. 构造顺序是基类到派生类,析构顺序相反
  4. 通过基类指针删除派生对象时,基类通常需要虚析构函数
  5. final 可以明确禁止一个类继续被继承
  6. 友元关系不会继承,静态成员在继承体系中共享
  7. 多继承可能产生名字二义性和指针调整
  8. 普通菱形继承会产生基类数据冗余
  9. 虚继承让多个路径共享一个虚基类子对象
  10. 虚基类由最派生类负责初始化
  11. public 继承表达 is-a,组合表达 has-a
  12. 仅为了复用实现时,通常优先考虑组合

继承本身不是越多越好。

一个清晰、稳定的类层次可以减少重复并支持多态;一个勉强拼出来的继承体系,则会让访问权限、对象生命周期和依赖关系越来越复杂。

设计继承关系时,先判断类型语义,再考虑代码复用,通常会更加稳妥。

相关推荐
Yeauty2 小时前
用 Whisper 转录前,你不用再离开 Rust
开发语言·rust·ffmpeg·音视频·视频
电子云与长程纠缠2 小时前
UE中使用TGuardValue与TInlineComponentArray数据结构
开发语言·数据结构·学习·ue5·游戏引擎
foo1st2 小时前
MySQL 8.0(Windows)升级笔记
数据库·笔记·mysql
呜喵王阿尔萨斯2 小时前
C/C++ const -- 多义混乱
c语言·开发语言·c++
__log2 小时前
幂等性设计:从“重复提交“到“稳如磐石“的系统防护
java·开发语言·spring boot
spider_xcxc2 小时前
Helm 部署 K8s 集群完整笔记
java·开发语言·kubernetes
海清河晏1112 小时前
Qt实战:从零构建美化登录界面
开发语言·c++·qt
一只小灿灿3 小时前
C++ 各类特殊符号、运算符
开发语言·c++
Fu_Lin_3 小时前
《Qt嵌入式从零基础到精通》前言与阅读指南
开发语言·qt