Skip to content

11 继承和派生

类型兼容性

  • 赋值运算的类型兼容性:
    • 你可以直接将一个派生类的对象赋值给一个基类的对象。
      Derived d;
      Base b = d;   // 允许,切片赋值
      b = d;
      
    • 这种赋值会发生“截断”,即基类对象 b 只能接收派生类对象 d 中继承自基类的那部分成员,派生类自己新增的成员会被丢弃。
  • 指针/引用的兼容(向上转型,Upcasting):
    • 你可以让一个基类指针,指向一个派生类对象。
      Derived d;
      Base* pb = &d;      // ✅ 派生类指针 → 基类指针
      Base& rb = d;       // ✅ 派生类引用 → 基类引用
      
    • 派生类对象的内存布局是"基类子对象 + 派生类特有成员"。所以指向派生类对象的指针,其起始地址恰好就是基类子对象的起始地址。
  • 函数参数的兼容:
    • 你可以将一个派生类对象作为参数,传递给一个需要基类参数的函数。
      void foo(Base* p);
      void bar(Base& r);
      
      foo(&d);            // ✅ 传入 Derived*
      bar(d);             // ✅ 传入 Derived
      

必须是公有继承:类型兼容规则仅适用于公有继承(public。如果是私有继承(private)或保护继承(protected),派生类对象不能在外部被当作基类对象使用。

兼容是单向的:派生类兼容基类,基类不兼容派生类。

对象的类型转换

Cast(类型转换) 就是强制改变对象的类型解释方式,C++ 是强类型语言,编译器对类型检查很严格。当两种类型"不兼容"但你又确定转换是安全/必要时,就需要显式 cast 来绕过编译器的类型检查。

分两大类:向上转型、向下转型

1. 向上转型(Upcast)

派生类 → 基类 - public 继承下可以隐式转换,不用强制 cast - 就是前面说的类型兼容性

Derived d;
Base *p = &d;      // 隐式向上转型,不用cast
Base &r = d;

2. 向下转型(Downcast)

基类 → 派生类 - 不安全,不能隐式转,必须用 cast 强制转换 - 把基类指针/引用,转回原本的派生类类型

Cast 用途 安全性
static_cast 相关类型之间的转换(如数值、继承链中的向上/向下转型) 编译期检查,无运行时开销
dynamic_cast 继承链中的安全向下转型 运行时检查,失败返回 nullptr(指针)或抛异常(引用)
const_cast 添加/移除 const/volatile 危险,可能引发未定义行为
reinterpret_cast 底层位模式的重新解释(如指针 ↔ 整数) 最危险,几乎无检查

1. static_cast — 最常用

double d = 3.14;
int i = static_cast<int>(d);    // 截断小数,i = 3

// 继承链中的转换
Base* pb = new Derived();
Derived* pd = static_cast<Derived*>(pb);  // ✅ 向下转型,程序员担保安全

2. dynamic_cast — 安全向下转型

Base* pb = new Derived();
Derived* pd = dynamic_cast<Derived*>(pb);  // ✅ 运行时检查,确实能转
if (pd) {
    pd->derivedOnlyMethod();  // 安全使用
}

Base* pb2 = new Base();
Derived* pd2 = dynamic_cast<Derived*>(pb2); // ✅ 返回 nullptr,转换失败

前提:基类必须有虚函数(需要有虚表来支持运行时类型识别 RTTI)。

注意:dynamic_cast 可以转指针,也可以转引用,不能转普通值对象。dynamic_cast<Derived>(b)非法。

3. const_cast — 改 const 属性

const int x = 10;
int* p = const_cast<int*>(&x);
*p = 20;  // ⚠️ 未定义行为!x 原本是 const,修改它后果自负

// 合法用途:调用旧 API 时去掉 const
void legacy_func(char* s);
const char* str = "hello";
legacy_func(const_cast<char*>(str));  // 如果 legacy_func 不修改内容,是安全的

4. reinterpret_cast — 最暴力

int x = 65;
char* p = reinterpret_cast<char*>(&x);  // 把 int* 当成 char* 来解释

// 指针 ↔ 整数(用于底层操作)
uintptr_t addr = reinterpret_cast<uintptr_t>(p);

注意 C 与 C++区别: - C语言 void * 可以隐式转型任何类型指针 - C++的 void * 需要显式转型其他类型指针

C 风格 Cast(不推荐)

int i = (int)d;           // C 风格
int i = int(d);           // 函数风格

问题:一个语法能做所有事情(static + const + reinterpret),编译器不会告诉你用了哪种,隐藏了危险。

const_cast的注意事项

const int i = 100;    // ①
const int *p = &i;    // ②
int *q = const_cast<int*>(p);  // ③
int j = i;            // ④
*q = 10;              // ⑤

cout << i << endl     // 输出1
     << j << endl     // 输出2
     << *p << endl    // 输出3
     << *q << endl;   // 输出4

const int i = 100;        // 1
const int &p = i;         // 2
int &q = const_cast<int&>(p); // 3
int j = i;                // 4

q = 10;                   // 5

cout << i << endl         // 输出1
     << j << endl         // 输出2
     << p << endl         // 输出3
     << q << endl;        // 输出4
两个例子的输出结果都是:
100
100
10
10

const int i = 100;定义了一个编译期 const 常量,编译器会把所有用到 i 的地方直接替换成 100,通过地址可以改变这块内存上的值,但是 i 本身已经被替换成了 100 ,是无法改变的。

多重继承

不允许重复继承同一个类BaseA

class 派生类名继承访问控制1 基类名1继承访问控制2 基类名2...
{
成员声明
}

虚基类

继承基类时,在继承访问控制前添加保留字“virtual”。 那么这个基类就是一个虚拟基类。普通基类与虚基类之间的唯一区别只有在派生类重复继承了某一基类时才表现出来。

  • 若派生类有一个虚基类作为祖先类,则在派生类构造函数中需要列出对虚基类构造函数的调用(否则,调用虚基类的默认构造函数),且对虚基类构造函数的调用总是先于普通基类的构造函数。

  • 创建后代类对象时,当该后代类列出的虚基类构造函数被调用,Virtual关键字保证了虚基类的唯一副本只被初始化一次。

  • 创建派生类对象时构造函数的调用次序:

    • 最先调用虚基类的构造函数;
    • 其次调用普通基类的构造函数,多个基类则按派生类声明时列出的次序、从左到右调用,而不是初始化列表中的次序;
    • 再次调用对象成员的构造函数,按类声明中对象成员出现的次序调用,而不是初始化列表中的次序
    • 最后执行派生类的构造函数

在这节课的课后题中,还出现了虚函数的概念,虽然后面的课程中会讲,但是为了更好理解,这里先简单介绍一下。

虚函数

虚函数(Virtual Function)是 C++ 中实现多态性的核心机制,允许子类重写父类的方法,并通过基类指针或引用调用子类的实现。用 virtual 关键字声明的成员函数,可以在子类中被重写(override),实现运行时多态。

基本语法

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

class Dog : public Animal {
public:
    void speak() override {  // 重写父类虚函数
        cout << "Wang Wang" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {  // 重写父类虚函数
        cout << "Miao Miao" << endl;
    }
};
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();

animal1->speak();  // 输出: "Wang Wang"
animal2->speak();  // 输出: "Miao Miao"

delete animal1;
delete animal2;

1. 虚函数表(vtable)

每个包含虚函数的类都有一个虚函数表,存储类中所有虚函数的地址。

Animal 的 vtable:
  [0] -> Animal::speak()

Dog 的 vtable:
  [0] -> Dog::speak()

Cat 的 vtable:
  [0] -> Cat::speak()

2. 虚函数指针(vptr)

每个对象都有一个隐藏的虚函数指针,指向其类的 vtable:

animal1 (Dog对象)
  [vptr] ---> Dog::vtable ---> Dog::speak()

animal2 (Cat对象)
  [vptr] ---> Cat::vtable ---> Cat::speak()

3. 动态绑定

调用虚函数时,通过对象的 vptr 找到对应的 vtable,在运行时决定调用哪个函数

虚函数规则

特性 说明
virtual 声明虚函数,在子类中可被重写
override (C++11)显式声明重写,编译器检查是否真正重写
final (C++11)禁止被子类重写
默认参数 使用父类默认参数,不支持动态绑定
构造函数 构造函数不能是虚函数
析构函数 基类析构函数通常设为虚函数