欢迎光临
我们一直在努力

【C++】多态

在这里插入图片描述

多态概述

多态(Polymorphism)是面向对象程序设计(OOP)的三大核心特性之一(另两个是封装和继承)。
它的字面意思是“多种形态”,在 C++ 中,多态允许同一段代码调用,根据对象的实际类型执行不同的操作,从而使程序更具灵活性和可扩展性。

C++ 中的多态主要分为两大类:

  • 编译时多态(静态多态):在编译阶段就确定函数调用地址,主要通过函数重载、运算符重载和函数/类模板实现。
  • 运行时多态(动态多态):在程序执行期间(运行时)才确定调用哪个函数,依赖于继承和虚函数。

编译时多态(静态多态)

静态多态之所以称为“静态”,是因为所有决策都在编译期完成,不涉及运行时类型识别。它的优势是调用迅速(无间接寻址开销),并且可以完全内联优化。

函数重载

概念
在同一作用域内,可以定义多个同名函数,但要求它们的参数列表不同(参数的类型、个数或顺序不同)。编译器根据调用时传入的实参,在编译期选择最匹配的重载版本。

重要规则

  • 仅返回值类型不同不能构成重载。
  • 参数类型不同(包括 const / volatile 修饰)可以重载,但顶层 const(修饰参数本身)不影响签名;底层 const(修饰指针指向的对象)会影响重载。
  • 重载解析会进行隐式类型转换、默认参数匹配等操作。

示例:数学运算函数的多种重载

#include <iostream>
#include <string>

class Calculator {
public:
// 重载:处理整数
int add(int a, int b) {
std::cout << "int version" << std::endl;
return a + b;
}

// 重载:处理浮点数
double add(double a, double b) {
std::cout << "double version" << std::endl;
return a + b;
}

// 重载:处理字符串拼接
std::string add(const std::string& a, const std::string& b) {
std::cout << "string version" << std::endl;
return a + b;
}
};

int main() {
Calculator calc;
calc.add(1, 2); // int version
calc.add(1.5, 2.7); // double version
calc.add("Hello, ", "World!"); // string version
}

底层实现
C++ 编译器通过**名字修饰(name mangling)**技术,将函数名与参数类型信息编码成唯一的内部符号。例如 add(int, int) 可能被修饰为 _Z3addii,add(double, double) 修饰为 _Z3adddd,这样链接器就能区分不同的重载版本。

运算符重载

概念
运算符重载允许为自定义类型(如类、结构体)定义运算符的行为,使其像内置类型一样使用运算符(+、-、*、/、<<、[] 等)。

实现方式

  • 成员函数形式:Complex operator+(const Complex& rhs) const;
  • 非成员函数形式(通常声明为友元):friend Complex operator+(const Complex& lhs, const Complex& rhs);

限制

  • 不能创建新的运算符,只能重载 C++ 已有的运算符。
  • 不能改变运算符的优先级、结合性和操作数个数。
  • 某些运算符不能重载(如 ::、.、.*、?:)。
  • 重载后不能改变运算符用于内置类型时的原有含义。

示例:重载下标运算符 [] 实现安全的数组访问

#include <iostream>
#include <stdexcept>

class SafeArray {
private:
int data[10];
public:
SafeArray() {
for (int i = 0; i < 10; ++i) data[i] = i;
}

// 重载 [](读/写)
int& operator[](int index) {
if (index < 0 || index >= 10)
throw std::out_of_range("Index out of range");
return data[index];
}

// 重载 [](只读,用于 const 对象)
const int& operator[](int index) const {
if (index < 0 || index >= 10)
throw std::out_of_range("Index out of range");
return data[index];
}
};

int main() {
SafeArray arr;
std::cout << arr[5] << std::endl; // 5
arr[5] = 100; // 修改
std::cout << arr[5] << std::endl; // 100
// arr[20] = 0; // 抛出异常
}

运算符重载的本质
运算符重载实际上是函数调用的语法糖。c1 + c2 会被编译器解释为 c1.operator+(c2) 或 operator+(c1, c2)。因此它也是静态多态的一种形式。

函数模板(泛型多态)

函数模板是一种泛型编程技术,通过参数化类型,使同一段代码能处理不同类型的数据。它也是一种编译时多态,编译器会根据实参类型生成具体的函数实例。

示例:通用的 max 函数

template<typename T>
T myMax(T a, T b) {
return (a > b) ? a : b;
}

int main() {
std::cout << myMax(3, 5) << std::endl; // T 推断为 int
std::cout << myMax(3.14, 2.72) << std::endl; // T 推断为 double
std::cout << myMax('a', 'z') << std::endl; // T 推断为 char
}

模板与重载的比较

  • 模板可实现类型无关的算法,一次编写,多次实例化。
  • 重载则针对不同参数类型分别编写,更适用于差异较大的处理逻辑。

运行时多态(动态多态)

运行时多态是面向对象编程的精髓。它允许你使用基类的指针或引用来操作派生类对象,并在运行时根据对象的实际类型调用对应的派生类函数。这一特性依赖于虚函数和继承。

继承与虚函数

核心要素

  • 继承:派生类继承基类的成员(非私有)。
  • 虚函数:在基类中使用 virtual 关键字声明的成员函数,允许在派生类中重写(override)。
  • 基类指针/引用指向派生类对象。
  • 通过该指针/引用调用虚函数。
  • 示例:动物叫声的多态

    #include <iostream>
    #include <vector>

    class Animal {
    public:
    virtual void speak() const { // 虚函数
    std::cout << "Animal makes a sound." << std::endl;
    }
    virtual ~Animal() {} // 虚析构函数,稍后解释
    };

    class Dog : public Animal {
    public:
    void speak() const override { // 重写虚函数(override 可选但推荐)
    std::cout << "Dog barks: Woof!" << std::endl;
    }
    };

    class Cat : public Animal {
    public:
    void speak() const override {
    std::cout << "Cat meows: Meow!" << std::endl;
    }
    };

    void makeSound(const Animal& animal) {
    animal.speak(); // 运行时多态:实际调用 Dog::speak 或 Cat::speak
    }

    int main() {
    Dog dog;
    Cat cat;

    makeSound(dog); // Dog barks: Woof!
    makeSound(cat); // Cat meows: Meow!

    std::vector<Animal*> zoo;
    zoo.push_back(new Dog);
    zoo.push_back(new Cat);

    for (Animal* a : zoo) {
    a->speak(); // 输出: Woof! Meow!
    delete a;
    }
    }

    重写虚函数的条件

    • 派生类中的函数与基类虚函数完全相同(函数名、参数列表、返回类型,协变返回类型除外)。
    • 基类函数必须是虚函数(不写 virtual 则隐藏而非重写)。
    • 使用 override 关键字可让编译器检查是否符合重写条件(C++11 起)。

    协变返回类型
    如果基类虚函数返回某个类(或指针/引用),而派生类重写函数返回该类的派生类(或指针/引用),则允许返回类型不同,称为协变。

    class Base { public: virtual Base* clone() { return new Base(*this); } };
    class Derived : public Base {
    public:
    virtual Derived* clone() override { return new Derived(*this); }
    };

    纯虚函数与抽象类

    纯虚函数:在基类中用 = 0 声明的虚函数,如 virtual void draw() = 0;。
    抽象类:含有纯虚函数的类,无法实例化,只能作为接口或基类使用。
    作用:定义规范,强制派生类实现该函数。

    示例:图形接口

    class Shape {
    public:
    virtual double area() const = 0; // 纯虚函数
    virtual ~Shape() {}
    };

    class Circle : public Shape {
    double r;
    public:
    Circle(double radius) : r(radius) {}
    double area() const override {
    return 3.14159 * r * r;
    }
    };

    class Rectangle : public Shape {
    double w, h;
    public:
    Rectangle(double w, double h) : w(w), h(h) {}
    double area() const override {
    return w * h;
    }
    };

    void printArea(const Shape& s) {
    std::cout << "Area: " << s.area() << std::endl;
    }

    虚析构函数

    当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,派生类的资源将无法释放,导致内存泄漏。因此,只要一个类有可能作为基类被继承,就应该将析构函数声明为虚函数。

    反例(未定义虚析构函数):

    class Base { ~Base() {} };
    class Derived : public Base {
    int* p;
    public:
    Derived() { p = new int[100]; }
    ~Derived() { delete[] p; }
    };

    Base* b = new Derived;
    delete b; // 仅调用 Base::~Base,Derived 的资源未被释放!

    正确做法:给基类添加 virtual ~Base() = default;。

    底层原理:虚函数表(vtable)与虚指针(vptr)

    运行时多态的实现依赖于 C++ 编译器为每个包含虚函数的类建立的虚函数表以及每个对象内部的虚指针。

    虚函数表(vtable)

    • 是一个存储函数指针的数组(通常放在只读数据段)。
    • 每个包含虚函数的类都有一个自己的虚函数表,表中按声明顺序存放该类所有虚函数的地址。
    • 派生类如果重写了某个虚函数,则虚函数表中对应条目会被替换为派生类的函数地址;如果没有重写,则保留基类的地址。
    • 通常虚函数表末尾带有一个标记(如 RTTI 相关信息)。

    虚指针(vptr)

    • 每个对象实例在内存开头(通常是前8/4个字节)会有一个隐藏的指针,指向所属类的虚函数表。
    • 当通过基类指针调用虚函数时,程序会:
    • 通过对象的 vptr 找到虚函数表。
    • 从虚函数表中取出对应槽位的函数指针。
    • 间接调用该函数。

    单继承下的对象内存布局示例(32位系统):

    class Base {
    public:
    virtual void f();
    virtual void g();
    int a;
    };

    class Derived : public Base {
    public:
    virtual void f() override; // 重写 f
    virtual void h(); // 新增虚函数
    int b;
    };

    内存布局(以 Visual C++ 风格为例,其他编译器类似但可能不同):

    Derived 对象:
    +——–+
    | vptr | –> 指向 Derived vtable
    +——–+
    | a | (Base::a)
    +——–+
    | b | (Derived::b)
    +——–+

    Derived vtable:
    +———————–+
    | &Derived::f | (覆盖 Base::f)
    +———————–+
    | &Base::g | (未重写,指向基类函数)
    +———————–+
    | &Derived::h | (新增虚函数,扩展表)
    +———————–+
    | … (RTTI) |
    +———————–+

    构造函数与析构函数中的虚函数
    在构造和析构期间,对象的 vptr 会随着构造/析构的进行而改变,指向当前正在构造/析构的类的虚函数表。此时通过对象调用的虚函数不会表现多态,而是调用当前构造函数/析构函数所属类中的版本。这是需要特别注意的陷阱。

    class Base {
    public:
    Base() { f(); } // 构造时调用 Base::f,不会调用派生类重写版本
    virtual void f() { std::cout << "Base::f\\n"; }
    };

    class Derived : public Base {
    public:
    Derived() { f(); } // 此时 vptr 已指向 Derived 表,调用 Derived::f
    void f() override { std::cout << "Derived::f\\n"; }
    };

    int main() {
    Derived d;
    // 输出:Base::f Derived::f
    }

    多继承下的虚函数表

    当类从多个基类继承,且这些基类都有虚函数时,情况变得复杂。派生类对象会包含多个虚指针,分别指向对应的基类子对象的虚函数表(或者指向一个整合的表,具体取决于编译器实现)。同时,可能还会产生调整 this 指针的操作,以确保不同基类视角下能正确访问成员。这部分原理较为深奥,但我们可以记住一个简单结论:慎用多继承,尤其在涉及虚函数时,推荐优先使用单一继承加接口类。

    动态多态的性能与适用场景

    优点

    • 代码可扩展性强,符合开闭原则(对扩展开放,对修改关闭)。
    • 非常适合框架设计、插件系统、以及需要通用接口的场景。

    代价

    • 一次虚函数调用相当于:两次内存读取(vptr + 函数指针)+ 一次间接调用,相比普通函数调用有微小但可测量的开销。
    • vptr 占用对象额外内存(4/8 字节)。
    • 虚函数通常不能内联,因为调用地址在编译时未知。
    • 虚函数表占据静态存储区。

    适用建议

    • 如果性能敏感且调用频繁,考虑静态多态(CRTP 模式)或模板。
    • 如果层次清晰、扩展性更重要,动态多态是合适的选择。

    C++11 特性:override 与 final

    C++11 引入了两个上下文关键字,极大地改善了虚函数重写的代码安全性和可读性。

    override 说明符

    作用:显式标记派生类中的虚函数是对基类虚函数的重写。
    好处:

  • 增加代码可读性,明确告知读者这是一个重写函数。
  • 让编译器进行签名检查:若基类中没有相同签名的虚函数,或签名不匹配(如参数类型、const 限定、协变不符),编译器报错,避免因拼写错误导致意外隐藏而非重写。
  • 错误示例(不加 override 可能隐藏错误):

    class Base {
    public:
    virtual void show(int x) {}
    };

    class Derived : public Base {
    public:
    void show(double x) {} // 不同参数类型,并未重写,而是隐藏基类的 show
    };

    int main() {
    Derived d;
    Base* p = &d;
    p->show(5); // 调用 Base::show(int),不是多态!
    }

    使用 override 暴露问题:

    class Derived : public Base {
    public:
    void show(double x) override {} // 编译错误:没有基类虚函数 void show(double)
    };

    final 说明符

    作用:

    • 用于虚函数:阻止派生类继续重写该虚函数。
    • 用于类:阻止该类被继承。

    示例:禁止重写

    class Base {
    public:
    virtual void keyFunction() final; // 派生类不可重写 keyFunction
    };

    class Derived : public Base {
    void keyFunction() override; // 错误:无法重写 final 函数
    };

    示例:禁止继承

    class Sealed final { /* … */ };
    class InheritFromSealed : public Sealed { }; // 错误:Sealed 是 final,不可继承

    final 和 override 可以同时使用(void func() override final;),表示这是一个重写函数,并且禁止后续重写。

    多态的设计思想与应用

    多态并不仅仅是一种语法特性,更是一种程序设计方法论。它体现了以下核心设计原则:

  • 针对接口编程,而非针对实现编程
    多态使得我们可以依赖抽象的基类(接口),而不是具体的派生类。客户代码只需操作抽象基类,增加新类型时无需修改原有代码。

  • 开闭原则(Open-Closed Principle)
    对扩展开放,对修改关闭。通过多态,新增功能只需添加新的派生类,而不用改动已存在的稳定代码。

  • 依赖倒置原则
    高层模块不依赖低层模块的具体实现,二者都依赖抽象。多态是实现依赖注入、控制反转的基础。

  • 经典应用场景

    • 图形渲染系统:Shape 抽象基类,派生 Circle、Rectangle、Triangle,渲染器统一调用 draw()。
    • 策略模式:定义一系列算法,每个算法封装在派生类中,通过多态使算法可相互替换。
    • 工厂方法模式:通过基类指针创建具体产品对象,实现创建逻辑与使用的解耦。
    • 访问者模式:借助双重分发(多态)处理不同元素类型的操作。

    多态与继承的组合
    在实际工程中,应优先使用组合而非继承。当确实需要“is-a”关系和接口统一时,才使用继承和多态。过度使用继承会导致脆弱的基类问题(Fragile Base Class Problem)。

    赞(0)
    未经允许不得转载:171主机测评 » 【C++】多态
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址