当前位置: 首页 > news >正文

C++----多态

一、多态的概念与分类

多态是指不同对象对同一消息产生不同响应的特性。

C++中多态分为两类:

  1. 静态多态(编译时多态):函数重载、运算符重载、模板
  2. 动态多态(运行时多态):通过虚函数机制实现,本文重点讨论

二、动态多态的实现条件

需同时满足以下三个条件:

  1. 存在继承关系
  2. 必须通过基类的指针或引用调用虚函数
  3. 派生类必须重写(覆盖)基类的虚函数
class Person {
public:
    virtual void BuyTicket() const {
        cout << "全价票" << endl;
    }
};

class Student : public Person {
public:
    virtual void BuyTicket() const { 
        cout << "半价票" << endl; 
    }
};

void Func(const Person& p) {
    p.BuyTicket();  // 多态调用
}

int main() {
    Person p;
    Student s;
    Func(p);  // 输出:全价票
    Func(s);  // 输出:半价票
}

三、虚函数重写规则

1.基本规则

  • 函数名、参数列表、返回值必须相同(协变例外)
  • virtual关键字在派生类中可省略(建议保留)

2.协变

允许返回值类型作为基类指针/引用的派生类型:

class A {};
class B : public A {};
class Base {
public:
    virtual A* Create(){ 
        return new A; 
    }
};

class Derived : public Base {
public:
    virtual B* Create() { 
        return new B; 
    }  // 协变
};

3.析构函数必须为虚函数

在C++中,当将析构函数声明为 virtual 时,就构成了虚函数重写。这是因为在底层,类的析构函数都会被处理成统一的名字 destructor ,这样的处理方式使得派生类和基类的析构函数能够构成重写关系。

1.场景:未将析构函数设为析构函数的风险

#include <iostream>

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

class Student : public Person {
public:
    void BuyTicket() override {
        std::cout << "买票-半价" << std::endl;
    }

    ~Student() {
        std::cout << "~Student()" << std::endl;
        delete[] ptr;
    }

protected:
    int* ptr = new int[10];
};

int main() {
    Person* p = new Person;
    p->BuyTicket();
    delete p;

    p = new Student;
    p->BuyTicket();
    delete p; // p->destructor() + operator delete(p)
    return 0;
}

问题分析:在 main 函数中,当 p 指向 Student 对象并执行 delete p 时,由于 Person 类的析构函数不是虚函数,只会调用 Person 类的析构函数,而不会调用 Student 类的析构函数。这会导致 Student 类中动态分配的 ptr 所指向的内存无法被释放,最终造成内存泄露。我们期望的是,当 delete p 时, p->destructor() 能够根据 p 实际指向的对象类型,进行多态调用,而不是普通调用。

2.正确做法

将基类的析构函数声明为虚函数,使派生类的析构函数自动成为虚函数并重写基类的析构函数:

#include <iostream>

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

class Student : public Person {
public:
    void BuyTicket() override {
        std::cout << "买票-半价" << std::endl;
    }

    ~Student(){
        std::cout << "~Student()" << std::endl;
        delete[] ptr;
    }

protected:
    int* ptr = new int[10];
};

int main() {
    Person* p = new Person;
    p->BuyTicket();
    delete p;

    p = new Student;
    p->BuyTicket();
    delete p;
    return 0;
}

结果分析:在上述代码中, Person 类的析构函数被声明为虚函数。当 delete p 时,如果 p 指向 Student 对象,就会调用 Student 类的析构函数,从而正确释放 ptr 所指向的内存,避免内存泄露问题。当使用基类指针指向派生类对象,并且需要通过该指针释放对象时,一定要将基类的析构函数声明为虚函数,以确保析构函数的多态性,正确调用派生类的析构函数 。

四、C++新特性

1.override关键字:强制检查重写有效性

class Transport {
public:
    virtual void StartEngine() const {
        cout << "引擎启动" << endl;
    }
};

class Tesla : public Transport {
public:
    // 正确重写
    virtual void StartEngine() const override {
        cout << "电动机启动" << endl;
    }

    // 错误:签名不匹配(缺少const)
    // virtual void StartEngine() override {}
};

2.final关键字:禁止继承或重写

禁用虚函数重写

class Car {
public:
    virtual void Drive() final {  // 禁止后续重写
        cout << "基础驾驶模式" << endl;
    }
};

class Benz : public Car {
public:
    // 错误:尝试重写final函数
    // void Drive() override { cout << "Benz驾驶模式" << endl; }
    
    // 正确:新函数(参数不同构成隐藏)
    void Drive(int mode) {
        cout << "Benz-舒适模式" << endl;
    }
};

禁止类被继承

class Vehicle final {  // 最终类,禁止继承
public:
    void Run() { cout << "行驶中..." << endl; }
};

// 错误:尝试继承final类
// class Car : public Vehicle {};

3.设计不可继承类的方法

方法一:利用构造函数或析构函数私有(C++98)

(一)基类构造函数私有

通过将基类的构造函数设为私有,阻止其他类通过继承来调用该构造函数,从而实现不可被继承的目的。但这样会导致无法直接创建基类对象,因此需要在基类中提供一个静态成员函数来创建对象。

class A {
public:
    // 静态成员函数用于创建A类对象
    static A CreateObj() {
        return A();
    }
private:
    // 私有构造函数,防止外部直接构造对象
    A() {} 
};

// 尝试继承A类,编译会报错,因为无法访问A的私有构造函数
class B : public A {}; 

int main() {
    // 通过静态函数创建A类对象
    A::CreateObj(); 
    return 0;
}
(二)基类析构函数私有

将基类析构函数设为私有,同样可以阻止继承。因为派生类对象析构时需要调用基类析构函数,而私有析构函数使得派生类无法进行这一操作。但这种方式下创建基类对象也变得复杂,通常通过指针方式创建对象,并提供一个成员函数来释放对象。

class A {
public:
    // 提供一个成员函数来释放对象,这里函数名设为Destructor
    void Destructor() {
        delete this;
    }
private:
    // 私有析构函数
    ~A() {} 
};

// 尝试继承A类,编译会报错,因为无法访问A的私有析构函数
class B : public A {}; 

int main() {
    A* p = new A;
    // 通过调用Destructor函数来释放对象
    p->Destructor(); 
    // 不能直接创建B类对象,因为B的析构需要调用A的私有析构函数
    // B bb; 

    return 0;
}

方法二:使用final关键字(C++11及以后)

在C++11及后续版本中,可以使用 final 关键字将类标记为最终类,即不可被继承。

// 将A类标记为final,不可被继承
class A final { 
public:
private:
};

// 尝试继承A类,编译会报错,因为A是final类
class B : public A {}; 

int main() {
    return 0;
}

五、重载、覆盖、隐藏对比

特征作用域函数名参数列表返回值其他要求
重载同一类相同不同任意        -
覆盖基类与派生类相同相同协变virtual关键字
隐藏基类与派生类相同不同任意无virtual关键字

六、虚函数与多态原理

1.虚表机制

  • 每个包含虚函数的类都有一个虚函数表
  • 对象内存首部存放指向虚表的指针
  • 虚表在编译阶段生成,存放在只读数据段
#include <iostream>

class Parent {
public:
    virtual void virtualFunction() {
        std::cout << "Parent's virtual function" << std::endl;
    }
};

class Child : public Parent {
public:
    // 重写父类的虚函数
    void virtualFunction() override {
        std::cout << "Child's virtual function" << std::endl;
    }
};

int main() {
    Parent* parentPtr = new Parent();
    parentPtr->virtualFunction();

    parentPtr = new Child();
    parentPtr->virtualFunction();

    delete parentPtr;
    return 0;
}

在上述代码中, Parent  类中的 virtualFunction  是虚函数, Child  类重写了该虚函数。通过 Parent  类指针,根据其指向的对象类型( Parent  或 Child ),调用不同版本的 virtualFunction ,体现了多态性。同时, Child  类在重写虚函数时,会在其虚表中更新 virtualFunction  的地址,从而实现多态调用。

2.多态实现条件解析

(一)为什么必须是父类的指针或引用
多态实现需要通过父类的指针或引用。因为父类指针或引用具有灵活性,既可以指向父类对象,也能指向子类对象。相比之下,子类指针或引用只能局限于指向子类对象,无法满足多态中根据不同对象类型调用不同函数实现的需求。
 
(二)为什么不能是父类对象
对象切片与指针、引用的情况不同。在多态机制中,虚函数的调用依赖于虚表。派生类会先拷贝父类的虚表,然后将重写的虚函数地址覆盖到虚表相应位置。若使用父类对象,对象切片时只会将父类的成员拷贝到子类对象中,但父类的虚表不会拷贝过去。原因在于,如果拷贝虚表,当父类指针指向父类对象时,可能会错误调用子类的虚表,导致程序逻辑混乱。
 
(三)为什么要对虚函数重写
普通函数的继承是实现继承,而多态实现的是接口继承。通过重写虚函数,子类在继承相同接口的基础上,能够提供不同的实现,从而实现多态特性。

3.虚表的存储位置

虚表存储在常量区(实际上,虚表通常存储在只读数据段,不同编译器可能略有差异,但概念上类似常量区的性质,存放常量数据,程序运行期间不可修改)。

七、多继承与虚函数相关原理

1.多继承下派生类虚函数在虚表中的位置

class Base1 {
public:
    virtual void func1() { std::cout << "Base1::func1" << endl; }
    virtual void func2() { std::cout << "Base1::func2" << endl; }
private:
    int b1;
};

class Base2 {
public:
    virtual void func1() { std::cout << "Base2::func1" << endl; }
    virtual void func2() { std::cout << "Base2::func2" << endl; }
private:
    int b2;
};

class Derive : public Base1, public Base2 {
public:
    virtual void func1() { std::cout << "Derive::func1" << endl; }
    virtual void func3() { std::cout << "Derive::func3" << endl; }
private:
    int d1;
};

int main()
{
    Derive d;
    return 0;
}

原理分析:在多继承场景下, Base1  和 Base2  分别拥有自己的虚函数表。对于派生类 Derive  新增的 func3  函数,通过验证发现其函数指针会放在 Base1  的虚表中。同时,由于 Base1  和 Base2  都有 func1  函数且在各自虚表中代表不同的函数实现,所以 func1  函数在 Base1  和 Base2  虚表中的地址是不一样的。 

2.菱形继承下虚函数重写问题

class A {
public:
    int _a;
};

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

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

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

(一)只重写A的虚函数,B和C尝试重写的情况

class A {
public:
    virtual void func1() {
        cout << "A::func1" << endl;
    }
public:
    int _a;
};

class B : virtual public A {
public:
    virtual void func1() {
        cout << "B::func1" << endl;
    }
public:
    int _b;
};

class C : virtual public A {
public:
    virtual void func1() {
        cout << "C::func1" << endl;
    }
public:
    int _c;
};

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

在菱形继承结构中, A  是 B  和 C  的虚基类, B  和 C  共享 A  的数据成员和虚函数。如果在 B  和 C  中同时重写 func1  函数,会导致编译报错。原因在于 B  和 C  共享 A  的虚函数 func1 ,编译器无法确定在 D  类中到底是 B  重写的 func1  生效还是 C  重写的 func1  生效,产生了歧义。

解决办法

让D去重写虚函数:

class A {
public:
    virtual void func1() {
        cout << "A::func1" << endl;
    }
public:
    int _a;
};

class B : virtual public A {
public:
    virtual void func1() {
        cout << "B::func1" << endl;
    }
public:
    int _b;
};

class C : virtual public A {
public:
    virtual void func1() {
        cout << "C::func1" << endl;
    }
public:
    int _c;
};

class D : public B, public C {
public:
    virtual void func1() {
        cout << "D::func1" << endl;
    }
    int _d;
};

原理分析:当在 D  类中重写 func1  函数时,就明确了在 D  对象调用 func1  时的具体实现。虽然对于 D  而言, B  和 C  中重写的 func1  函数被覆盖,但对于单独定义的 B  对象和 C  对象,它们各自重写的 func1  函数仍然有意义,调用时会执行各自类中定义的 func1  函数实现。

(二)重写A的虚函数,同时B和C有单独虚函数的情况

 class A {
public:
    virtual void func1() {
        cout << "A::func1" << endl;
    }
public:
    int _a;
};

class B : virtual public A {
public:
    virtual void func1() {
        cout << "B::func1" << endl;
    }
    virtual void func2() {
        cout << "B::func2" << endl;
    }
public:
    int _b;
};

class C : virtual public A {
public:
    virtual void func1() {
        cout << "C::func1" << endl;
    }
    virtual void func2() {
        cout << "C::func2" << endl;
    }
public:
    int _c;
};

class D : public B, public C {
public:
    virtual void func1() {
        cout << "D::func1" << endl;
    }
public:
    int _d;
};

原理分析:在这种情况下,由于 B  和 C  除了重写 A  的 func1  函数外,还各自拥有单独的虚函数 func2 。为了管理这些不同的虚函数实现, B  和 C  会各自创建一个虚表。这种结构使得虚函数的调用和管理变得更加复杂,需要在运行时根据对象的具体类型以及虚表指针来准确找到对应的虚函数实现。

八、抽象类

1.概念

包含纯虚函数的类被称为抽象类,也叫接口类。抽象类不能实例化出对象,因为它在现实世界中没有直接对应的实体,主要为派生类提供一个通用的接口框架。

2.示例

#include <iostream>

// 定义抽象类Car
class Car {
public:
    // 纯虚函数Drive,使得Car类成为抽象类
    virtual void Drive() = 0;
};

// Benz类继承自Car类
class Benz : public Car {
public:
    // 若Benz类未重写Drive函数,Benz也会成为抽象类
    void Drive() override {
        std::cout << "Benz is driving." << std::endl;
    }
};

// BMW类继承自Car类
class BMW : public Car {
public:
    void Drive() override {
        std::cout << "BMW is driving." << std::endl;
    }
};

// BYD类继承自Car类
class BYD : public Car {
public:
    void Drive() override {
        std::cout << "BYD is driving." << std::endl;
    }
};

// 函数Func接受一个Car类型的指针,并调用Drive函数
void Func(Car* p) {
    if (p) {
        p->Drive();
        // 这里p->Func() 是错误的,Car类中没有Func函数,假设这是误写,应删除
    }
}

int main() {
    Func(new Benz);
    Func(new BMW);
    Func(new BYD);

    // 以下代码会报错,因为抽象类Car不能实例化对象
    // Car car; 

    return 0;
}

3.总结

  • 纯虚函数的作用:在抽象类中定义纯虚函数,间接强制派生类重写该虚函数。派生类若不重写纯虚函数,自身也会成为抽象类,同样不能实例化对象。
  • 抽象类的使用:虽然抽象类不能实例化对象,但可以定义指向抽象类的指针或引用,通过这些指针或引用调用派生类重写后的虚函数,实现多态行为。在上述代码中, Func  函数通过  Car*  指针调用不同派生类的  Drive  函数,体现了这一特性。

九、相关面试题

1. 什么是多态?
答:多态分为静态多态和动态多态,静态多态就是函数重载,动态多态就是继承中虚函数重写,再加上父类指针调用。静态多态和动态多态的本质是更方便和灵活多种形态的调用。
2. 什么是重载、重写 ( 覆盖 ) 、重定义 ( 隐藏 )
答:
  • 重载:在同一作用域内,函数名相同但参数列表(参数类型、个数、顺序)不同的函数称为重载函数。重载发生在编译阶段,根据调用时传入的参数来决定调用哪个重载版本,不关心函数返回值类型。
  • 重写:也叫覆盖,发生在继承体系中。派生类重新定义基类中已有的虚函数,函数名、参数列表、返回值类型(协变返回类型除外)必须与基类中的虚函数完全一致,且使用override关键字标识(C++11及以后)。重写是实现动态多态的关键,运行时根据对象实际类型调用相应的重写版本。
  •  重定义:也叫隐藏,指派生类定义了与基类同名的成员(函数或变量),此时基类的同名成员在派生类作用域内被隐藏。如果同名函数参数列表不同,即使基类函数不是虚函数,也会发生重定义,调用时取决于调用对象的类型。
3. 多态的实现原理?
答:
  • 静态多态:基于函数名修饰规则。编译器在编译时对重载函数的名字进行特殊修饰,使其包含函数参数类型等信息,从而在调用时能够准确识别并调用正确的函数版本。
  • 动态多态:核心是虚函数表。在包含虚函数的类中,编译器会为其创建一个虚函数表,表中存储了虚函数的地址。当对象被创建时,会在对象的内存布局中设置一个指向虚函数表的指针。运行时,通过这个指针找到对应的虚函数表,并根据实际对象类型调用相应的虚函数。
4. inline 函数可以是虚函数吗?
答:可以,不过编译器就忽略 inline 属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有 this 指针,使用类型 :: 成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。 当使用基类指针或引用指向子类对象并通过该指针释放对象时,如果基类析构函数不是虚函数,只会调用基类的析构函数,子类的析构函数不会被调用,导致子类资源无法正确释放,产生内存泄漏。将基类析构函数设为虚函数后,delete对象时会根据对象实际类型调用相应的析构函数,确保资源的正确释放。
8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段( 常量区 ) 的。
10. C++ 菱形继承的问题?虚继承的原理?
答:菱形继承的问题是数据冗余和二义性。通过虚函数表实现动态多态,在运行时根据对象实际类型调用相应的虚函数。(注意不要将虚函数表与虚基表混淆,虚基表用于解决菱形继承中数据冗余和二义性问题,通过虚继承使间接基类在派生类中只有一份拷贝 。)
11. 什么是抽象类?抽象类的作用?
答:
  • 定义:包含纯虚函数的类称为抽象类,也叫接口类。纯虚函数是一种特殊的虚函数,只声明函数原型,不提供函数体,用“= 0”标识。
  • 作用:抽象类不能实例化对象,主要为派生类提供统一的接口规范和抽象的概念模型。通过强制派生类重写抽象类中的纯虚函数,实现不同的具体行为,支持多态性,方便程序的扩展和维护,提高代码的可复用性和可维护性。

相关文章:

  • 单元测试、系统测试、集成测试知识详解
  • 神经网络常见激活函数 8-SELU函数
  • 【MyBatis】预编译SQL与即时SQL
  • 数据结构:串
  • 2025年如何选择合适的微服务工具
  • datasets: PyTorch version 2.5.1+cu124 available 这句话是什么意思
  • DeepSeek 遭 DDoS 攻击背后:DDoS 攻击的 “千层套路” 与安全防御 “金钟罩”_deepseek ddos
  • EMC测试中的环境噪声控制:为什么6dB是关键?
  • HCIA项目实践--动态路由的相关知识
  • 使用python脚本提取html网页上的所有文本信息
  • 【Linux】nmcli命令详解
  • 如何提升插屏广告在游戏APP广告变现表现,增加变现收益
  • 上位机知识篇---SSHSCP密钥与密钥对
  • LVS集群模式
  • linux笔记3----防火墙(ubuntu)
  • IoTDB 集群节点 IP 改变,如何更新集群
  • 更加通用的Hexo多端部署原理及实现,适用于各种系统之间
  • Apollo 9.0 速度动态规划决策算法 – path time heuristic optimizer
  • 关于 IoT DC3 中设备(Device)的理解
  • OpenCL实现深度图生成点云功能
  • 聆听百年唐调正声:唐文治王蘧常吟诵传习的背后
  • 欧洲史上最严重停电事故敲响警钟:能源转型如何保证电网稳定?
  • 习近平同瑞典国王卡尔十六世·古斯塔夫就中瑞建交75周年互致贺电
  • 从上海首个到成片复制,闵行零工市场如何优化劳动就业服务?
  • 比特币价格时隔三个月再度站上10万美元
  • 中华人民共和国和俄罗斯联邦关于进一步加强合作维护国际法权威的联合声明