继承是 C++ 面向对象编程的三大特性之一(封装、继承、多态)。如果说类与对象解决了"数据和操作封装在一起"的问题,那继承解决的问题就是代码复用——在已有类的基础上创建新类,复用基类的代码,同时添加新的功能。本篇博客基于课堂笔记和课件,系统讲解 C++ 继承的全部核心知识。


一、前言

1.1 为什么需要继承?

写代码时经常遇到这种情况:你有一个 Person 类,里面有姓名、年龄等属性和一些基本操作。现在你要写一个 Student 类和一个 Teacher 类——它们都有姓名、年龄,但 Student 还有学号,Teacher 还有工号。

没有继承时,你只能把共有的代码再抄一遍:

// 没有继承——重复代码
class Person {
    string _name;
    int _age;
};

class Student {
    string _name;   // 又写一遍
    int _age;       // 又写一遍
    int _stuNum;    // 学号
};

class Teacher {
    string _name;   // 再写一遍
    int _age;       // 再写一遍
    int _jobId;     // 工号
};

这明显不合理——代码大量重复,维护困难。如果你要改 Person 的属性(比如加一个性别字段),所有类都要改。

继承可以解决这个问题:

class Student : public Person {
    int _stuNum;    // 只需要关注 Student 特有的成员
};

class Teacher : public Person {
    int _jobId;     // 只需要关注 Teacher 特有的成员
};

Student 和 Teacher 自动拥有 Person 的所有成员,不需要再写一遍。

1.2 面向对象三大特性

特性 作用 说人话
封装 数据和操作绑定,控制访问权限 类把数据和函数打包,告诉你哪些能用、哪些不能用
继承 在已有类的基础上创建新类,复用代码 子类自动拥有父类的成员,不需要重新造轮子
多态 同一操作在不同对象上表现出不同行为 同一个函数,传学生指针和传普通人指针,结果不一样

继承是连接"封装"和"多态"的桥梁——没有继承就没有多态。

1.3 本篇博客主要内容

  1. 继承的概念与语法——怎么定义一个类继承另一个类
  2. 访问权限——public/protected/private 继承的区别
  3. 对象切片(赋值兼容)——子类对象能不能当父类用
  4. 作用域与隐藏——同名成员会怎样
  5. 派生类的默认成员函数——构造/拷贝构造/赋值/析构的顺序和写法
  6. 静态成员和友元在继承中的表现
  7. 多继承和菱形继承——C++ 继承最复杂的地方
  8. 虚拟继承——如何解决菱形继承的数据冗余和二义性
  9. 组合 vs 继承——两种代码复用的设计取舍
  10. 常见错误和面试问题

二、继承的概念与定义

2.1 基本语法

class Person {
public:
    void Print() {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }

protected:
    string _name = "peter";
    int _age = 18;
};

// Student 继承 Person
class Student : public Person {
public:
    void func() {
        cout << _name << endl;              // ✅ 可以访问父类的 protected 成员
        // cout << _age << endl;            // ❌ 不能直接访问父类的 private 成员
        Print();                            // ✅ 可以间接调用父类的 public 函数
    }

protected:
    int _stuid;    // 学号
};

这里 Student 继承Person,我们说:

  • Person基类(base class),也叫父类
  • Student派生类(derived class),也叫子类
  • StudentPerson 继承了 _name_agePrint() 等成员

💡 课堂比喻:爸爸(Person)有名字、年龄、买东西的技能。儿子(Student)自动继承这些——不需要重新"学"。但爸爸的钱包(private 成员)儿子不能直接碰。

2.2 三种继承方式

继承方式 基类 public 成员 基类 protected 成员 基类 private 成员
public 继承 仍是 public 仍是 protected 不可见
protected 继承 变为 protected 仍是 protected 不可见
private 继承 变为 private 变为 private 不可见

规则:派生类中成员的访问权限 = min(基类中该成员的访问限定符, 继承方式)。

class Person {
public:
    string _name;

protected:
    int _age;

private:
    string _privateInfo;  // 私房钱
};

class Student1 : public Person {
    // _name 仍是 public
    // _age 仍是 protected
    // _privateInfo 不可见(不能访问)
};

class Student2 : protected Person {
    // _name 变成 protected
    // _age 仍是 protected
    // _privateInfo 不可见
};

class Student3 : private Person {
    // _name 变成 private
    // _age 变成 private
    // _privateInfo 不可见
};

❗ 最常用的是 public 继承,它表达了"is-a"的关系——Student 是一个 Person。protected 和 private 继承用得很少。

2.3 class 和 struct 的继承默认方式

// class 默认继承方式是 private
class Student : Person { ... };       // 等价于 class Student : private Person

// struct 默认继承方式是 public
struct Student : Person { ... };      // 等价于 struct Student : public Person

三、基类和派生类之间的赋值转换——对象切片

这是继承中最基础也最重要的概念之一。派生类对象可以当作基类对象来使用。

class Person {
public:
    string _name = "peter";
    int _age = 18;
};

class Student : public Person {
public:
    int _stuNum = 1;
};

3.1 三种赋值兼容方式

Student st;

// 1. 派生类对象赋值给基类对象——切片(slicing)
Person p = st;          // ✅ 只拷贝 Person 部分,_stuNum 被"切掉"

// 2. 派生类对象初始化基类引用
Person& ref = st;       // ✅ ref 引用的是 st 中的 Person 部分

// 3. 派生类对象的地址赋给基类指针
Person* ptr = &st;      // ✅ ptr 指向 st 中的 Person 部分

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
课堂笔记:派生类到基类的切片转换关系。子类对象赋值给父类对象时,子类特有的成员被"切掉"

3.2 反过来不行

Person p;
// Student st = p;     // ❌ 基类对象不能赋值给派生类
// Student& ref = p;   // ❌ 基类引用不能绑定到派生类
// Student* ptr = &p;  // ❌ 基类指针不能直接赋给派生类

为什么不行? 基类对象中没有派生类特有的成员。如果允许父类隐式转成子类,访问 st._stuNum 时就会访问到不存在的数据——这是未定义行为。

3.3 切片后的操作

Student st;
Person p = st;
Person& ref = st;
Person* ptr = &st;

// 通过基类引用/指针操作,只能访问基类部分
ref._name += 'x';       // ✅ 修改的是 st 中的 _name
ptr->_name += 'y';      // ✅ 也是修改 st 中的 _name

// ref._stuNum;          // ❌ 基类引用看不到派生类特有的成员

四、继承中的作用域

4.1 基类和派生类是独立的作用域

class Person {
protected:
    string _name = "peter";
    int _num = 0;           // Person 的 _num
};

class Student : public Person {
public:
    void func() {
        // Student 中也有 _num
        cout << _num << endl;           // 默认访问子类的 _num
        cout << Person::_num << endl;   // 显式访问父类继承来的 _num
    }

protected:
    int _num = 1;           // Student 自己的 _num
};

输出:

1      // 子类的 _num
0      // 父类的 _num

这里两个 _num 是不同的变量——一个在 Person 的作用域中,一个在 Student 的作用域中。Student 中的 _num 隐藏(hides)了 Person 中的 _num

4.2 同名成员函数隐藏

同名成员函数也会被隐藏——而且与参数列表无关(这和函数重载不同)。

class A {
public:
    void fun() {
        cout << "func()" << endl;
    }
};

class B : public A {
public:
    void fun(int i) {          // 隐藏了 A 的 fun()
        cout << "func(int i)->" << i << endl;
    }
};

int main() {
    B bb;
    // bb.fun();               // ❌ 编译错误!B 的 fun(int) 隐藏了 A 的 fun()
    bb.fun(1);                 // ✅ 调用 B 自己的 fun(int)
    bb.A::fun();               // ✅ 显式指定作用域,调用 A 的 fun()

    return 0;
}

🚨 重点强调:派生类中定义了一个和基类同名的函数(即使参数不同),基类的所有同名函数都会被隐藏。这和函数重载(在同一作用域中参数不同构成重载)完全是两码事。


五、派生类的默认成员函数

这一节是面试高频考点。派生类有自己的一套构造、拷贝构造、赋值、析构规则。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
课堂笔记:派生类默认成员函数——构造/拷贝/赋值/析构的调用顺序

5.1 构造函数

class Person {
public:
    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person& p) {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;
        return *this;
    }

    ~Person() {
        cout << "~Person()" << endl;
    }

protected:
    string _name;
};

class Student : public Person {
public:
    // 派生类构造函数——必须调用基类构造函数初始化基类部分
    Student(int num, const char* str, const char* name)
        : Person(name)           // ✅ 显式调用基类构造函数
        , _num(num)
        , _str(str)
    {
        cout << "Student()" << endl;
    }

    // ...

protected:
    int _num;
    string _str;
};

构造顺序

Person()       ← 先构造基类部分
Student()      ← 再构造派生类部分

如果你不显式调用基类构造函数:

Student(int num, const char* str, const char* name)
    // 没有 Person(...) —— 编译器会尝试调用 Person 的默认构造
    // 但 Person 没有默认构造——编译错误!
    , _num(num)
    , _str(str)
{}

💡 规则:派生类的构造函数中,基类的构造函数是必须被调用的。如果不显式调用,编译器会调用基类的默认构造函数(如果存在的话)。基类没有默认构造时,你必须显式调用。

5.2 拷贝构造函数

class Student : public Person {
public:
    // s2(s1)——派生类拷贝构造
    Student(const Student& s)
        : Person(s)           // ✅ 调用基类的拷贝构造(s 切片成 Person)
        , _num(s._num)
        , _str(s._str)
    {}
};

这里 Person(s) 是把 Student 对象 s 传给 Person(const Person& p)——因为 Student 可以切片成 Person,所以这是合法的。

5.3 赋值运算符重载

class Student : public Person {
public:
    Student& operator=(const Student& s) {
        if (this != &s) {
            Person::operator=(s);    // ✅ 必须显式调用基类的 operator=
            _num = s._num;
            _str = s._str;
        }
        return *this;
    }
};

💡 Person::operator=(s) 一定要加 Person::,否则 operator=(s) 会递归调用自己——死循环。

5.4 析构函数

class Student : public Person {
public:
    ~Student() {
        cout << _name << endl;
        cout << "~Student()" << endl;
        // ⚠️ 这里不需要手动调用 Person::~Person()
        // 子类析构函数结束后,会自动调用父类析构函数
    }
};

析构顺序和构造相反:

~Student()     ← 先析构派生类部分
~Person()      ← 再析构基类部分(自动调用)

为什么必须是先子后父?

假设派生类中有一个指针成员指向了基类成员管理的数据。如果先析构基类,基类的资源被释放了,派生类析构时再去访问就崩了。

main() {
    Student s1(1, "xxxx", "张三");
    Student s2(s1);        // 拷贝构造
    // 离开作用域时:
    // 1. ~Student()  —— 析构子类部分
    // 2. ~Person()   —— 析构父类部分(自动)
}

5.5 构造和析构顺序总结

构造顺序:基类构造 → 派生类构造
析构顺序:派生类析构 → 基类析构(自动)

💡 课堂上的一个形象理解:建造房子时,先打地基(父类),再盖上层(子类)。拆除时,先拆上层(子类),再拆地基(父类)——你不会把地基先拆了然后再拆上面的墙。


六、继承与静态成员

6.1 基类的静态成员被所有派生类共享

class Person {
public:
    Person() { ++_count; }
    Person(const Person& p) { ++_count; }

protected:
    string _name;

public:
    static int _count;       // 统计人的个数
};

int Person::_count = 0;      // 类外定义静态成员

class Student : public Person {
protected:
    int _stuNum;
};

class Graduate : public Student {
protected:
    string _seminarCourse;
};

int main() {
    Person p;
    Student s1;
    Student s2;
    Student s3(s1);
    Graduate g1;

    cout << Person::_count << endl;   // 5
    cout << Student::_count << endl;  // 5 —— 同一份!
    cout << &Person::_count << endl;  // 某个地址
    cout << &Student::_count << endl; // 完全相同的地址

    return 0;
}

💡 关键:静态成员在整个继承体系中只有一份。Person 的 _count 和 Student 的 _count 指的是同一个变量。不管是 Person::_count 还是 Student::_count,访问的都是同一个地址上的同一个值。


七、继承与友元

7.1 友元关系不能被继承

class Person {
    friend void Display(const Person& p, const Student& s);
protected:
    string _name;
};

class Student : public Person {
    // friend void Display(const Person& p, const Student& s);
    // ⚠️ 即使取消注释,Display 也只能访问 Student 继承自 Person 的 protected 成员
    // 不能访问 Student 自己的 protected 成员(比如 _stuNum)
    // 除非在 Student 中再声明一次 friend
protected:
    int _stuNum;
};

// 即使 Person 声明了 friend,Display 也不能访问 Student::_stuNum
void Display(const Person& p, const Student& s) {
    cout << p._name << endl;     // ✅ Person 的 friend,可以访问 Person 的 _name
    // cout << s._stuNum << endl; // ❌ Student 没有声明 Display 为友元
}

规则:

  • 基类的友元不能访问派生类的私有/保护成员
  • 派生类的友元不能访问基类的私有/保护成员
  • 基类友元访问派生类成员时,只能访问派生类中从基类继承来的成员

八、多继承

8.1 什么是多继承

C++ 允许一个派生类有多个基类——这叫多继承(Multiple Inheritance)。

class Student {
protected:
    int _num;
};

class Teacher {
protected:
    int _id;
};

// Assistant 同时继承 Student 和 Teacher
class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

多继承的语法:class 派生类 : 继承方式 基类1, 继承方式 基类2, ...

8.2 多继承中的同名字段

如果多个基类中有同名的成员,访问时会产生二义性

class Student {
public:
    string _name;
};

class Teacher {
public:
    string _name;
};

class Assistant : public Student, public Teacher {
};

int main() {
    Assistant a;
    // a._name = "peter";      // ❌ 二义性!不知道用 Student 的还是 Teacher 的
    a.Student::_name = "peter";  // ✅ 显式指定
    a.Teacher::_name = "mary";   // ✅ 显式指定

    return 0;
}

九、菱形继承

9.1 菱形继承的结构

多继承会引出一个经典问题——菱形继承(也叫钻石继承)。

       Person
      /      \
  Student    Teacher
      \      /
      Assistant
class Person {
public:
    string _name;
};

class Student : public Person {
protected:
    int _num;
};

class Teacher : public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

9.2 菱形继承的问题

int main() {
    Assistant a;
    // a._name = "peter";        // ❌ 二义性!哪一份 _name?
    a.Student::_name = "xxx";    // ✅ 指定 Student 路线继承的 _name
    a.Teacher::_name = "yyy";    // ✅ 指定 Teacher 路线继承的 _name

    return 0;
}

菱形继承有两个严重问题:

问题 1:数据冗余

Assistant 对象中有两份 _name——一份从 Student 继承(Student 又从 Person 继承),一份从 Teacher 继承(Teacher 也从 Person 继承)。

// 内存布局(示意):
// Assistant 对象:
//   [Student部分]
//     [Person部分]
//       _name           ← 第一份
//     _num
//   [Teacher部分]
//     [Person部分]
//       _name           ← 第二份(冗余!)
//     _id
//   _majorCourse

问题 2:二义性

访问 a._name 时不知道访问的是 Student 路线继承来的还是 Teacher 路线继承来的,必须显式指定 a.Student::_namea.Teacher::_name。但即使显式指定,数据冗余问题依然存在——两个 _name 是不同的变量,分别赋值互不影响。


十、虚拟继承

10.1 virtual 关键字

虚拟继承(Virtual Inheritance)用来解决菱形继承的数据冗余和二义性问题:

class Person {
public:
    string _name;
};

// 加上 virtual
class Student : virtual public Person {
protected:
    int _num;
};

// 也加上 virtual
class Teacher : virtual public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

虚拟继承后:

int main() {
    Assistant a;
    a._name = "peter";        // ✅ 不再有二义性!只有一份 _name
    cout << a._name << endl;  // "peter"

    // 当然,显式指定也可以
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    cout << a._name << endl;  // "yyy"——因为 Student 和 Teacher 访问的是同一份

    return 0;
}

虚拟继承之后,Assistant 对象中的 _name 只有一份,既没有数据冗余,也没有二义性。

10.2 虚拟继承的底层原理

虚拟继承是如何做到"只有一份"的?答案是虚基表(vbtable)

class A {
public:
    int _a;
};

class B : virtual public A {
public:
    int _b;
};

class C : virtual public A {
public:
    int _c;
};

class D : public C, public B {
public:
    int _d;
};

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
课堂笔记:虚基表和虚基表指针的示意图——D 对象中 B 和 C 部分各有一个 vbptr 指向虚基表,通过偏移量找到公共的 A 部分

对象布局(简化版):

D 对象内存布局(虚拟继承):
+------------------+
| B 部分:           |
|   vbptr           |  ← 指向 B 的虚基表(记录 B 到 A 的偏移量)
|   _b              |
|   _b1             |
|   _b2             |
+------------------+
| C 部分:           |
|   vbptr           |  ← 指向 C 的虚基表(记录 C 到 A 的偏移量)
|   _c              |
+------------------+
| D 部分:           |
|   _d              |
+------------------+
| A 部分:           |
|   _a              |  ← 共享的 A,只有一份!
+------------------+

虚基表中存放的是偏移量——告诉你从当前对象的某个位置到共享基类(A)的偏移距离。

D d;
d._a = 0;      // ✅ 没有二义性——只有一份 A::_a
d._b = 3;
d._c = 4;
d._d = 5;

// 切片时,指针会做偏移调整
B* pb = &d;    // pb 指向 D 中的 B 部分
C* pc = &d;    // pc 指向 D 中的 C 部分

// 即使 pb 和 pc 指向 D 中不同位置,
// 通过它们访问 _a 时,会通过 vbptr + 偏移量找到同一个 _a
pb->_a++;      // _a 变为 1
pc->_a++;      // _a 变为 2(和 pb 操作的是同一个 _a)

💡 理解关键:虚拟继承是通过间接寻址(加上一层偏移量计算)来实现的。代价是访问虚基类成员的效率比普通继承稍低(多一次间接寻址),换来了"数据不冗余"和"无二义性"。


十一、组合 vs 继承

11.1 两种复用方式

继承是代码复用的方式之一,但不是唯一的方式。另一种常见的方式是组合(Composition)。

// 继承——is-a 关系
class A { /* ... */ };
class B : public A { /* ... */ };   // B "是一个" A

// 组合——has-a 关系
class A { /* ... */ };
class B {
    A _aa;          // B "有一个" A
    int _b;
};

11.2 组合示例

class Person {
public:
    Person(int num, const char* str, const char* name)
        : _p(name)
        , _num(num)
        , _str(str)
    {}
private:
    string _name;    // Person 自己管理 name
    int _age;
};

// 组合方式——B 中包含 A 对象
class BB {
public:
    BB(int num, const char* str, const char* name)
        : _p(name)      // 初始化组合的对象
        , _num(num)
        , _str(str)
    {}
private:
    Person _p;      // 组合:BB "有一个" Person
    int _num;
    string _str;
};

11.3 继承 vs 组合:什么时候用什么?

对比维度 继承(is-a) 组合(has-a)
关系 Student Person Car Engine
耦合度 ——派生类依赖基类的实现 ——通过接口使用组件
代码复用 自动继承基类成员 通过包含对象使用其功能
可维护性 基类改变可能影响所有派生类 组件替换不影响整体
适用场景 层次关系明确,子类确实是一种父类 功能模块组合

设计原则:优先使用组合,而不是继承。

但继承在某些场景下是不可替代的:

  • 需要多态(虚函数)——组合不能实现多态
  • 清晰的层次关系——「人 → 学生 → 研究生」这种层次用继承最自然
  • 需要访问基类的 protected 成员——组合不能访问私有或保护成员

十二、继承和多态的关系

继承是多态(Polymorphism)的前提。简单说:没有继承,就没有多态。

多态的核心是虚函数(virtual function):

class Person {
public:
    virtual void BuyTicket() {
        cout << "Person->买票-全价" << endl;
    }
};

class Student : public Person {
public:
    virtual void BuyTicket() {        // 重写(override)基类的虚函数
        cout << "Student->买票-半价" << endl;
    }
};

// 多态:指向谁,调用谁的虚函数
void Func(Person* p) {
    p->BuyTicket();    // 取决于 p 实际指向什么对象
}

int main() {
    Person ps;
    Student st;

    Func(&ps);    // 输出:Person->买票-全价
    Func(&st);    // 输出:Student->买票-半价

    return 0;
}

多态的两个条件:

  1. 父子类完成虚函数重写(三同:函数名、参数、返回值)
  2. 父类的指针或引用调用虚函数

“指向谁,调用谁”——这就是多态。基类指针可以指向不同的派生类对象,调用同一个虚函数却表现出不同的行为。

多态的内容非常丰富,本篇只做简单引入。后续会有专门的博客详细讲解虚函数、抽象类、多态的底层原理(虚函数表等)。


十三、常见易错点总结

# 易错点 说明
1 派生类不能继承基类的友元关系 友元是单向授予,不能传递和继承
2 派生类不能继承基类的静态成员 静态成员是共享的——只有一个副本
3 派生类中访问同名成员默认访问自己的 想访问基类的需要加 基类::
4 派生类函数隐藏基类同名函数 即使参数不同也是隐藏,不是重载
5 派生类构造函数必须调用基类构造函数 不显式调用则调用基类的默认构造
6 派生类拷贝构造必须调用基类拷贝构造 Person(s) 利用切片
7 派生类 operator= 必须显式调用基类 operator= 否则基类部分不会被赋值
8 析构顺序和构造相反:先子后父 编译器自动保证,不要手动调用父类析构
9 切片(slicing)只发生在派生类→基类 基类不能隐式转换为派生类
10 菱形继承会产生数据冗余和二义性 用虚拟继承解决
11 虚拟继承使用虚基表(vbtable) 通过偏移量找到共享的基类部分
12 class 类默认 private 继承,struct 默认 public 注意区别
13 protected 成员不能被外部访问,但能被派生类访问 和 private 的区别就在这里
14 不要总用继承,优先考虑组合 耦合度更低,更灵活

十四、面试 / 学习常见问题

1. 什么是继承?为什么需要继承?

继承是面向对象三大特性之一,允许在已有类(基类)的基础上创建新类(派生类),派生类自动拥有基类的成员。继承的核心目的是代码复用——避免重复编写相同的代码,同时便于维护。

2. C++ 有哪几种继承方式?各有什么区别?

三种:public 继承、protected 继承、private 继承。继承方式决定了基类成员在派生类中的访问权限变化。最常用的是 public 继承。基类的 private 成员在任何继承方式下都不可见。

3. 什么是对象切片?

派生类对象赋值给基类对象时,派生类特有的成员被"切掉",只保留基类部分。派生类对象可以赋值给基类对象、初始化基类引用、赋给基类指针。反过来不行。

4. 派生类的作用域和基类的关系是什么?

派生类和基类有各自独立的作用域。派生类中定义了和基类同名的成员时,基类的成员被隐藏(也叫重定义)。注意是隐藏不是重载——函数隐藏与参数列表无关。

5. 派生类的构造函数有什么注意事项?

必须调用基类的构造函数来初始化从基类继承来的成员。如果不显式调用,编译器会调用基类的默认构造函数。如果基类没有默认构造,就必须显式调用。构造顺序:先基类后派生类。

6. 派生类的拷贝构造怎么写?

Student(const Student& s) : Person(s), _num(s._num), _str(s._str) {}。必须显式调用基类的拷贝构造,Person(s) 利用切片将 s 的基类部分传给基类拷贝构造。

7. 派生类的赋值重载有什么注意事项?

必须显式调用基类的 operator=Person::operator=(s)。如果漏掉,基类部分的数据不会被赋值。还要处理自赋值判断。

8. 派生类的析构顺序是什么?

先调用派生类析构函数,再自动调用基类析构函数(编译器保证)。析构顺序和构造顺序相反。不要手动调用基类析构函数。

9. 静态成员在继承中有什么特性?

基类中定义的静态成员,在整个继承体系中只有一份。通过基类或任何派生类访问的都是同一个变量。静态成员属于类,不属于对象,所有派生类共享。

10. 友元关系可以继承吗?

不能。基类的友元函数不能访问派生类的私有/保护成员。派生类的友元函数也不能访问基类的私有/保护成员。友元是单向授予的,不能传递。

11. 什么是菱形继承?有什么问题?

菱形继承是指一个派生类通过两个中间基类间接继承同一个基类(如 Assistant → Student/Teacher → Person)。问题一:数据冗余——派生类对象中有多份顶层基类的成员。问题二:二义性——访问顶层基类成员时不知道该走哪条路径。

12. 虚拟继承是如何解决菱形继承问题的?

通过 virtual 关键字:class Student : virtual public Person。虚拟继承后,派生类对象中只保留一份顶层基类的成员。底层通过**虚基表(vbtable)**存储偏移量,通过 vbptr(虚基表指针)找到共享基类的位置。代价是访问效率略低(多一次间接寻址)。

13. 组合和继承有什么区别?各有什么优缺点?

继承是 is-a 关系(Student is a Person),组合是 has-a 关系(Car has an Engine)。继承耦合度高,基类改变影响所有派生类;组合耦合度低,组件可替换。设计上优先考虑组合,但多态必须使用继承。

14. 继承和多态有什么关系?

继承是多态的前提。多态通过基类指针或引用调用虚函数,根据实际指向的对象类型表现出不同的行为。条件是:虚函数重写 + 基类指针/引用调用。

15. 派生类中如果定义了和基类同名的函数会发生什么?

基类的所有同名函数都会被隐藏(无论参数是否相同)。这和函数重载不同——重载发生在同一作用域中。想调用基类的隐藏函数需要加 基类::


十五、最后总结

继承的核心思想

继承解决的核心问题是代码复用——站在已有代码的肩膀上构建新类。

继承的本质:复用 + 扩展
  ┌────────────────┐
  │  已有的基类     │  ← 复用已有的代码
  │  (Person)       │
  └────────┬───────┘
           │ 继承
  ┌────────▼───────┐
  │  新增的派生类   │  ← 在复用基础上添加新功能
  │  (Student)      │
  └────────────────┘

本篇的关键知识点

  1. 继承语法class 派生类 : public 基类,最常用的是 public 继承
  2. 切片:派生类可以当作基类使用(赋值、引用、指针),反之不行
  3. 隐藏规则:派生类同名成员会隐藏基类的同名成员
  4. 构造/析构顺序:先父后子构造,先子后父析构
  5. 菱形继承:多继承带来的数据冗余和二义性问题,用 virtual 继承解决
  6. 虚拟继承:通过虚基表(vbtable)和偏移量实现共享基类
  7. 组合优先:能用组合就别用继承,但多态必须用继承

后续学习衔接

  • 本篇讲了继承的语法和原理
  • 下一站:多态——虚函数、虚函数表、抽象类、运行时类型识别
  • 再往后:C++11 新特性异常处理IO 流

继承是理解 C++ 面向对象的"第二阶段"——第一阶段是类与对象(封装),第二阶段就是继承,第三阶段是多态。三个阶段层层递进,缺一不可。

Person → Student 不只是代码上的继承,也是 C++ 面向对象设计思想的延续。理解继承,你就理解了"在已有基础上扩展"的编程思维——这种思维不仅适用于 C++,也适用于一切面向对象语言。


笔记整理自课堂学习(Lesson01–继承)和老师课件代码。代码示例基于课堂 Person/Student/Teacher/Assistant 系列。如有错误或疏漏,欢迎指正。

更多推荐