Skip to content

OOP 朋辈辅学 Lec3 继承与多态

1 继承

1. 1 何为继承

继承(Inheritance)允许一个类(派生类,derived class)从另一个类(基类,base class)继承属性和行为,从而实现代码复用层次化建模

1. 2 语法

在 C++ 中,通过 : 表示继承关系。

示例
#include <iostream>
#include <string>

using namespace std;

class Person {
public:
    Person(string name): name(name) {}

    void speak() {
        cout << "I'm " << name << endl;
    }

    string name;
};

class Student : public Person {
public:
    Student(string name, int grade): Person(name), grade(grade) {}

    void speak() {
        cout << "I'm " << name << " and my grade is " << grade << endl;
    }

    int grade;
};

派生类会继承基类成员,但访问权限会受到继承方式影响。

  • public 继承:基类的 public 成员在派生类中仍是 publicprotected 成员仍是 protected
  • protected 继承:基类的 publicprotected 成员在派生类中都会变成 protected
  • private 继承:基类的 publicprotected 成员在派生类中都会变成 private

补充说明

  • 基类中的 private 成员依然存在于派生类对象中,但不能被派生类直接访问
  • 若不显式写继承方式,则 class 默认是 private 继承,struct 默认是 public 继承

1. 3 构造与析构顺序

在继承体系中,对象的创建和销毁遵循固定顺序:

  • 构造顺序:先基类,后派生类
  • 析构顺序:先派生类,后基类

示例

#include <iostream>

using namespace std;

class Base {
public:
    Base() { cout << "Base Constructor" << endl; }
    virtual ~Base() { cout << "Base Deconstructor" << endl; }
};

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

int main()
{
    Derived d;
    return 0;
}
运行结果
Base Constructor
Derived Constructor
Derived Deconstructor
Base Deconstructor

1. 4 多重继承

C++ 允许一个派生类同时继承多个基类,这被称为多重继承

典型问题:菱形继承

class A {
public:
    int x;
};

class B : public A {};
class C : public A {};
class D : public B, public C {};

int main()
{
    D d;
    // int value = d.x;  // 二义性:到底走 B 还是 C 继承来的 A?
    return 0;
}

D 中会包含两份 A 子对象,因此访问 d.x 会出现二义性。

解决方式:虚继承

class A {
public:
    int x;
};

class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

int main()
{
    D d;
    int value = d.x;   // 不再二义
    return 0;
}
  • virtual public A 表示虚继承
  • 虚继承会保证最终派生类中只保留一份共享的基类子对象
  • 它正是为了解决菱形继承问题而设计的

1. 5 组合优于继承

组合(Composition)表示一种 has-a 关系,即一个对象拥有另一个对象作为成员。

继承 vs 组合

class A {
    // ...
};

// 继承:B is-a A
class B : public A {
    // ...
};

// 组合:C has-a A
class C {
public:
    A a;
};
为什么常说组合优于继承
  • 灵活性不足:继承关系在编译时确定,运行时难以调整
  • 脆弱基类问题:基类稍有改动,依赖它的子类都可能受影响
  • 类爆炸:若试图用继承叠加多种特性,类数量容易迅速膨胀

实践中,表达明确的 is-a 关系时再考虑继承,若只是为了复用代码时,往往优先考虑组合。

2 多态

2. 1 何为多态

多态(Polymorphism)字面意思是“多种形态”,在编程里表示:同一个接口作用在不同对象上时,可以表现出不同的行为

C++ 中的多态
  • 编译时多态
    • 函数重载
    • 运算符重载
    • 模板
  • 运行时多态
    • 虚函数

2. 2 运算符重载

运算符重载允许我们为自定义类型重新定义标准运算符的行为,使对象的使用方式更接近内置类型。

基本约束

  1. 不能创建新的运算符
  2. 不能改变运算符优先级和结合性
  3. 不能改变操作数个数
  4. 内置类型原有行为不能被修改

2. 2. 1 成员函数方式重载

如果把运算符写成类的成员函数,那么左操作数必须是该类对象。

示例:重载 +

class Vector2D {
public:
    double x, y;

    Vector2D(double x = 0, double y = 0): x(x), y(y) {}

    Vector2D operator+(const Vector2D& other) const
    {
        return Vector2D(this->x + other.x, this->y + other.y);
    }
};
  • a + b 本质上会被翻译为 a.operator+(b)
  • 左操作数 a 由隐式的 this 指针传入
  • 这里返回的是一个新的 Vector2D 对象

2. 2. 2 友元

友元函数(friend function)定义在类外,但可以访问类的 privateprotected 成员。

示例
#include <iostream>

using namespace std;

class Box {
private:
    double width;

public:
    Box(double w): width(w) {}

    friend void printWidth(Box b);
};

void printWidth(Box b)
{
    cout << "Width of box: " << b.width << endl;
}

int main()
{
    Box box(10.5);
    printWidth(box);
    return 0;
}

友元的特点

  • 友元函数需要在类内部声明,但它不是成员函数
  • 它没有 this 指针,若要访问对象成员,必须通过参数拿到对象
  • 调用时和普通函数一样,不需要通过对象或类名调用

注意

  • 单向:A 是 B 的友元,不代表 B 是 A 的友元
  • 不可传递:A 是 B 的友元,B 是 C 的友元,不能推出 A 是 C 的友元
  • 不可继承:基类的友元不会自动成为派生类的友元
  • 友元会削弱封装性,因此通常只在少数必要场景下使用

友元类

  • 除了把单个函数声明为友元,也可以把整个类声明为另一个类的友元
  • 这样该友元类中的所有成员函数都可以访问原类的私有成员

2. 2. 3 全局函数(友元)重载

当左操作数不是类对象,或者需要访问私有成员时,常把运算符重载写成全局友元函数。最常见的场景就是重载输入输出运算符。

示例:重载 <<

#include <iostream>

using namespace std;

class Complex {
private:
    double real, imag;

public:
    Complex(double r, double i): real(r), imag(i) {}

    friend ostream& operator<<(ostream& os, const Complex& c)
    {
        os << c.real << " + " << c.imag << "i";
        return os;
    }
};
为什么返回 ostream&
  • C++ 的输入输出本质上是流对象
  • 返回流的引用后,才能继续链式调用,例如 cout << a << b << endl

注意事项

  • 不是所有运算符都能重载,常见不能重载的有:.::?:sizeof
  • 需要区分运算符应该返回新对象引用还是
  • ++ 分为前置和后置:
    • operator++() 表示前置 ++x
    • operator++(int) 表示后置 x++
  • 后置版本里的 int 只是占位符,用来和前置版本区分

2. 3 虚函数

2. 3. 1 动态绑定

示例:没有虚函数时

#include <iostream>

using namespace std;

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

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

int main()
{
    Base* b = new Derived();
    b->show();
    delete b;
    return 0;
}
运行结果

输出 Base

原因
  • b编译时类型Base*
  • 编译器在编译阶段就把 b->show() 绑定成了 Base::show()
  • 这种按编译时类型决定调用目标的方式叫做静态绑定

如果希望指向基类的指针或引用在运行时根据对象真实类型调用正确函数,就要使用虚函数

示例:使用虚函数后

#include <iostream>

using namespace std;

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

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

int main()
{
    Base* b = new Derived();
    b->show();
    delete b;
    return 0;
}
运行结果

输出 Derived

解析
  • virtualshow() 具备运行时多态能力
  • b 虽然是 Base*,但它实际指向的是 Derived 对象
  • 因此运行时会根据对象真实类型调用 Derived::show()

如果一个类可能被继承,并且对象会通过基类指针delete,那么基类析构函数必须写成 virtual,否则:

  • delete basePtr 时只会调用基类析构函数
  • 派生类析构函数不会执行
  • 派生类自己管理的资源可能泄漏
注意
  • 只有通过指针或引用调用虚函数时,才会触发动态绑定
  • 直接用对象调用成员函数,不会产生动态绑定
  • 默认参数采用静态绑定
  • 构造函数不能是虚函数

2. 3. 2 实现机制

C++ 通过虚函数表(vtable)实现虚函数:

  • 虚函数表 vtable:编译器为每个含虚函数的类生成的一张表,里面记录该类虚函数的地址
  • 虚函数指针 vptr:每个对象内部隐藏保存的一个指针,指向所属类的虚函数表

运行时调用流程

  1. 程序先通过基类指针或引用定位到对象
  2. 通过对象里的 vptr 找到对应的 vtable
  3. vtable 中取出目标虚函数地址并执行

2. 4 关键字

  • override:用于派生类重写虚函数时,要求编译器检查函数签名是否真的匹配
  • final:用于阻止继续继承,或阻止某个虚函数继续被重写
示例
class Base {
public:
    virtual void show() {}
};

class Derived final : public Base {
public:
    void show() override {}
};

2. 5 抽象类

抽象类(Abstract Class)是专门用于被继承的类,不能被实例化。它的核心作用是定义统一接口,并强制派生类实现某些行为。

纯虚函数

只要一个类中包含至少一个纯虚函数,这个类就会成为抽象类。

virtual void func() = 0;

抽象类的特点

  • 不能创建抽象类对象
  • 但可以创建抽象类的指针或引用,用于实现动态绑定
  • 抽象类中的非纯虚函数仍然会被派生类继承

3 类型转换

为了提高代码可读性与安全性,C++ 引入了四种专门的类型转换运算符,而不是沿用 C 风格的强制转换 (type)value

3. 1 static_cast

static_cast(静态转换)最常用,主要用于那些编译器认可的、可检查的转换

常见使用场景

  1. 基本数据类型转换,如 int -> double
  2. 添加 const 限定
  3. 继承体系中的向上转型(派生类指针 / 引用转基类指针 / 引用)
关于向下转型
  • static_cast 也允许某些继承体系中的向下转型写法
  • 但它不会做运行时检查
  • 若对象真实类型不匹配,就会产生不安全行为

示例

double d = 3.14;
int i = static_cast<int>(d);

Base* b = static_cast<Base*>(new Derived());   // 向上转型,安全

3. 2 dynamic_cast

dynamic_cast 主要用于类层次结构中的安全向下转型,它会在运行时检查对象真实类型。

使用前提

  • 基类必须至少有一个虚函数,也就是说该基类必须是多态类型

示例

Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b);

if (d) {
    // 转换成功,可以安全使用 d
}
转换失败时
  • 若目标是指针类型,失败时返回 nullptr
  • 若目标是引用类型,失败时会抛出异常(通常是 std::bad_cast

3. 3 const_cast

const_cast(常量转换)用于添加或移除 const / volatile 属性,它是唯一可以移除 const 属性的转换。

使用场景

  • 常见于需要把 const 指针 / 引用传给只接受非常量参数的旧接口
  • 但若原对象本身就是常量,强行去掉 const 后再修改,行为是未定义的

3. 4 reinterpret_cast

reinterpret_cast(重解释转换)是最危险的一种转换,本质上只是把一段底层比特模式按另一种类型解释。

示例

int* p = new int(65);
char* ch = reinterpret_cast<char*>(p);
特点
  • 常见于不同指针类型之间转换,或指针与足够大的整数类型之间转换
  • 它几乎不提供语义保证,类型安全极弱
  • 除了底层系统编程等特殊场景,一般应尽量避免使用