一文吃透 C++ 类的继承与多态机制,从语法到内存布局(含虚函数详解)

前言:本文是基于GCC编译器进行讲解的,不同编译器如MSVC在部分地方实现方式上可能有所不同

继承

C++继承是面向对象编程的核心特性之一,允许一个类(派生类)基于另一个类(基类)来构建,实现代码复用和层次化设计。通过继承,派生类自动获得基类的成员变量和成员函数,并可添加新功能或重写基类方法。

一、单继承

基础结构

#include <iostream>
#include <fstream>
using namespace std;
class Base {
public:
    Base() {
        cout << "调用Base构造函数" << endl;
    }
    int a = 1;
    int b = 2;

    ~Base() {
        cout << "调用Base析构函数" << endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        cout << "调用Derived构造函数" << endl;
    }
    int c = 3;
    ~Derived() {
        cout << "调用Dervice析构函数" << endl;
    }
};
int main(){
    Derived d;
    return 0;
}

输出结果

在创建对象时先调用父类的构造函数再调用自身的构造函数,而销毁时则是先调用自身的析构函数再调用父类析构函数。

操作顺序原因
构造父类 → 子类子类依赖父类,父类要先准备好
析构子类 → 父类子类先结束,避免访问已销毁的父类资源

成员调用

1.基础成员访问规则
class Base {
public:
    int public_var = 10;
protected:
    int protected_var = 20;
private:
    int private_var = 30;
};

class Derived : public Base {
public:
    void access_check() {
        public_var = 100;      // ✅ 可访问(public继承保留public权限)
        protected_var = 200;   // ✅ 可访问(protected成员对派生类可见)
        // private_var = 300;  // ❌ 编译错误:基类private成员不可见
    }
};

int main() {
    Derived d;
    d.public_var = 1000;      // ✅ 外部可访问
    // d.protected_var = 2000; // ❌ 编译错误:protected成员外部不可见
}
2.隐藏问题
class Base {
public:
    void func(int x) { cout << "Base::func" << endl; }
};

class Derived : public Base {
public:
    // 隐藏基类同名函数(参数不同也会隐藏)
    void func() { cout << "Derived::func" << endl; }
};

int main() {
    Derived d;
    d.func();        // ✅ 调用Derived::func()
    // d.func(100);  // ❌ 编译错误:基类func(int)被隐藏
    d.Base::func(100); // ✅ 显式指定基类版本
}

继承本质

内存结构

Derived 对象:
|- Base 子对象
   |- a 
   |- b 
|- Derived 新增成员
   |- c 

继承本质为将父类内容拷贝一份到自身类当中,子类对象包含父类对象的所有成员变量(不管是 public / protected / private)。 很多人认为子类对象不继承父类的私有成员,其实这是不对的

#include <iostream>
using namespace std;

class Base {
private:
    int secret = 42;

public:
    int getSecret() {
        return secret;
    }
};

class Derived : public Base {
public:
    void showSecret() {
        // cout << secret << endl; //  编译错误,不能直接访问 private
        cout << "Access via Base's public method: " << getSecret() << endl; 
    }
};

int main() {
    Derived d;
    d.showSecret();  // 输出: 42
    return 0;
}

输出结果


注意事项

注: 在继承当中子类对象创建时会隐式调用父类的无参构造,因此如果你在父类中写了带参的构造函数,要手动实现一个无参构造(当手动写了带参构造,编辑器就不会自动生成默认无参构造函数了),或者显示调用自己写的带参构造,如下:

class Base {
public:
    Base(int x) {
        cout << "Base构造函数, x = " << x << endl;
    }
};

class Derived : public Base {
public:
    Derived(int y) : Base(y) {  // 通过初始化列表显式调用父类带参构造
        cout << "Derived构造函数, y = " << y << endl;
    }
};

二、多继承

基础结构

class Base1 {
public:
    int a;
};

class Base2 {
public:
    int b;
};

class Derived : public Base1, public Base2 {
public:
    int c;
};

内存结构

Derived 对象:
|- Base1 子对象
   |- a 
|- Base2 子对象
   |- b 
|- Derived 新增成员
   |- c 

多继承和上文提到的单继承基本一致,但有些问题需要注意,见下文

同名成员二义性问题

在 C++ 中使用多继承(multiple inheritance)时,如果多个基类中包含同名的成员(变量或函数),派生类就会出现二义性(ambiguity),即编译器无法确定你指的是哪个基类的成员,这就是同名成员的二义性问题。

#include <iostream>
using namespace std;

class A {
public:
    int x = 10;
    void print() { cout << "A::print()" << endl; }
};

class B {
public:
    int x = 20;
    void print() { cout << "B::print()" << endl; }
};

class C : public A, public B {
};

上述代码中A与B有同名成员,当直接在C中访问xprint()的时候会编译报错,因为编译器不知道该调用哪个类的成员

int main() {
    C obj;
    // cout << obj.x;         // ❌ 错误:x 不明确
    // obj.print();           // ❌ 错误:print() 不明确

    // 正确写法:
    cout << obj.A::x << endl;    // ✅ 10
    cout << obj.B::x << endl;    // ✅ 20

    obj.A::print();              // ✅ 调用 A 的 print
    obj.B::print();              // ✅ 调用 B 的 print
}

菱形继承

菱形继承问题的本质在于:多个派生类从同一个基类继承时,最终派生类会重复继承该基类,导致基类成员在内存中存在多份,从而引发二义性、数据冗余以及资源管理混乱的问题

class A {
public:
    int data;
};

class B : public A {
public:
    void fb() { cout << data; }
};

class C : public A {
public:
    void fc() { cout << data; }
};

class D : public B, public C {}; // 菱形继承
int main(){
    D d;
    // d.data = 10;        // 编译错误:ambiguous access
    d.B::data = 10;       // 修改 B 路径的 A::data
    d.C::data = 20;       // 修改 C 路径的 A::data
    cout << d.B::data;    // 输出 10
    cout << d.C::data;    // 输出 20
    return 0;
}

所谓“菱形”,指的是继承结构像个菱形:

    A
   / \
  B   C
   \ /
    D

  • 类 B 和 C 都继承自 A,类 D
  • 同时继承自 B 和 C

于是 D 间接继承了两次 A

内存布局

D 对象:
|- B 部分
   |- A 子对象
      |- data
|- C 部分
   |- A 子对象
      |- data

该问题可通过虚继承解决

三、虚继承

虚继承(virtual inheritance)是 C++ 中为了解决菱形继承(钻石继承)问题而引入的一种机制

在讲虚继承之前先讲讲虚基表,因为虚继承实际上是基于虚基表进行的

虚基表(Virtual Base Table)

虚基表是C++实现虚继承机制的关键数据结构,每个​​包含虚基类的类​​会生成自己的虚基表用于存储虚基类在派生类对象中的位置信息。

虚基表(vbtable)主要用于解决以下问题:

  1. ​​共享虚基类实例​​:确保在菱形继承中虚基类只有一份拷贝
  2. 动态定位​​:在运行时确定虚基类子对象的位置
  3. ​​指针调整​​:正确进行派生类指针到虚基类指针的转换

先简单提提一下概念性内容,下面我将基于前文讲讲虚继承是怎么利用虚基表解决菱形继承问题的

解决菱形继承

虚继承代码结构:

class A {
public:
    int data;
};

class B : virtual public A {
public:
    void fb() { cout << data; }
};

class C : virtual public A {
public:
    void fc() { cout << data; }
};

class D : public B, public C {};

int main(){
    D d;
    d.data = 10;          // ✅ 不再歧义
    cout << d.data;       // ✅ 输出 10
    return 0;
}

内存结构变化

普通继承:
D 对象:
|- B 部分
   |- A 子对象
      |- data
|- C 部分
   |- A 子对象
      |- data


虚继承后
D对象:
|- B子对象
    |- vbptr(指向vbtable)→ A子对象偏移量
|- C子对象
    |- vbptr(指向vbtable)→ A子对象偏移量
|- 共享的 A子对象(唯一的一份)
  1. 编译器为 B 和 C 增加了一个vbptr(虚基表指针)
  2. vbptr 指向一个 vbtable
  3. vbtable 告诉编译器:A 的子对象在 D 对象内的偏移量;

所以,无论通过 B 还是 C,最终都会访问同一个 A,从而解决了菱形继承会重复继承祖父类的情况

总结

普通继承(不加 virtual)虚继承(加 virtual)
A子对象存在两份A子对象只存在一份
成员访问有歧义成员访问统一无歧义
需要显式指定路径可直接访问
内存占用更大占用更少

多态

多态(Polymorphism)是面向对象编程的三大特性之一,字面意思是“多种形态”。在 C++ 中,多态允许我们通过相同的接口调用不同的行为。这让程序结构更灵活、更具扩展性。

C++ 支持两类多态:

  • 编译时多态(静态多态):如函数重载、模板

  • 运行时多态(动态多态):依赖继承 + 虚函数

本节我们重点介绍运行时多态。

虚函数/虚函数表

介绍多态前要介绍一个很重要的概念——虚函数

虚函数

定义
在 C++ 中,如果一个类的成员函数前加上 virtual 关键字,那么这个函数就是虚函数

class Base {
public:
    virtual void show() {
        std::cout << "Base::show()" << std::endl;
    }
};

虚函数的意义在于支持运行时多态(也叫动态绑定、动态分派),即通过基类指针或引用调用派生类的重写方法

此外还有纯虚函数、抽象类 这里不过多赘述,以一张表的形式总结

类型定义是否支持实例化
虚函数(virtual)普通虚函数,有实现
纯虚函数(= 0)没有实现,子类必须重写
抽象类至少包含一个纯虚函数的类

虚函数表

定义
为了实现运行时的动态分派,编译器会为每个含有虚函数的类生成一张虚函数表(vtable)

  • 每一个含有虚函数的类都有一个vtable
  • 每一个对象中会有一个虚指针(vptr),它指向所属类的 vtable
  • vtable 中存储的是该类中虚函数的地址;
  • 调用虚函数时,实际是通过 vptr 找到 vtable,再找到正确的函数地址去调用。

假设有以下代码结构

class Base {
public:
    virtual void f1() { std::cout << "Base::f1()\n"; }
    virtual void f2() { std::cout << "Base::f2()\n"; }
};

class Derived : public Base {
public:
    void f1() override { std::cout << "Derived::f1()\n"; }
};

对于Base

vtable_Base:
+-----------+
| &Base::f1 | 
| &Base::f2 |
+-----------+

对于 Derived

vtable_Derived:
+--------------+
| &Derived::f1 |
| &Base:: f2   | 没有重写函数,继承来自父类的函数
+--------------+

虚函数表初始化时先存储自己的函数,多态的时候通过虚指针(vptr)动态调整指向

注:虚函数表只会存储带virtual的函数,因此带virtual的函数才会实现运行时多态

小结

虚函数表(vtable)每个含有虚函数的类(包括直接声明或继承得到虚函数)在编译时会拥有一张由编译器生成的虚函数表,用于支持多态。
虚函数指针(vptr)每个类中含有虚函数的对象实例,在其内部都会包含一个vptr(虚函数指针),它指向该类的虚函数表。vptr 的存在是为每个对象提供运行时多态能力。

运行时多态

要实现运行时多态,C++ 要求满足以下三个条件:

  1. 基类中定义虚函数(使用 virtual 关键字)
  2. 派生类对该虚函数进行重写(override)

示例代码:

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {  // 虚函数
        cout << "Animal speaks" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Dog barks" << endl;
    }
};

void makeSound(Animal& a) {
    a.speak();  // 多态调用
}

int main() {
    Dog d;
    makeSound(d);  // 输出:Dog barks
}

运行结果

结构分析

对象d (构造中):
+-------------------+
| vptr              | ────>  暂时指向 Animal 的虚函数表
+-------------------+
                          │
                     重新调整指向
                          │
                          ↓
                  Dog的vtable:
                  +-------------------+
                  | type_info         |
                  +-------------------+
                  | Dog::speak()      | → 输出"Dog barks" (覆盖Animal的实现)
                  +-------------------+

对象d的虚函数指针(vptr)从原来指向Animal的虚函数表到指向Dog的虚函数表,因此调用的是Dog中的函数

如果不使用虚函数会怎样?

示例代码

#include <iostream>
using namespace std;

class Animal {
public:
     void speak() { 
        cout << "Animal speaks" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "Dog barks" << endl;
    }
};

void makeSound(Animal& a) {
    a.speak();  // 调用
}

int main() {
    Dog d;
    makeSound(d);  // 输出:Animal speaks
}

结果

在这个例子中,speak() 方法​​没有​​被声明为 virtual,因此不会触发多态行为

  • Animal::speak()Dog::speak() 都是普通成员函数
  • 调用在编译时静态绑定

因此虽然传递的是 Dog 对象,但使用 Animal& 会发生对象切片,导致派生类特有的部分被"切掉"的情况。

虚析构问题

在 C++ 中,如果你打算通过“基类指针”来释放“派生类对象”,基类的析构函数必须是虚的,否则会导致派生类资源不会被释放,造成内存泄漏或程序行为异常。

场景复现

#include <iostream>
using namespace std;

class Base {
public:
    ~Base() { cout << "Base::~Base()\n"; }
};

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

int main() {
    Base* p = new Derived();  // 用基类指针指向派生类对象
    delete p;                 // 只会调用 Base::~Base()
}


正确做法

#include <iostream>
using namespace std;

class Base {
public:
    //进行虚析构
    virtual ~Base() { cout << "Base::~Base()\n"; }
};

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

int main() {
    Base* p = new Derived();  
    delete p;                 
}

输出

Derived::~Derived()
Base::~Base()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值