C++:深入浅出讲解=>多态

大家好,这里是彩妙呀~

C++中OOP(面向对象语言)三大件前两站:

c++:继承 - 深入浅出,超详解https://blog.csdn.net/weixin_66776566/article/details/157870359?spm=1001.2014.3001.5501C++:类与对象https://blog.csdn.net/weixin_66776566/article/details/156678915?spm=1001.2014.3001.5501深幽:C++:类与对象https://blog.csdn.net/weixin_66776566/article/details/156775780?spm=1001.2014.3001.5501

本篇就要开始学习OOP最后一个特性:多态。

目录

什么是多态?

多态的定义与实现

[C++ 多态的两大分类:静态与动态](#C++ 多态的两大分类:静态与动态)

编译时多态(静态多态)

运行时多态(动态多态)本博客重点(涉及底层原理)

存在继承关系:必须有基类和派生类,派生类继承自基类

[基类中声明虚函数:在基类中使用 virtual 关键字声明虚函数](#基类中声明虚函数:在基类中使用 virtual 关键字声明虚函数)

什么是虚函数

虚函数的覆盖/重写

通过基类指针或引用调用虚函数(多态的底层原理)

静态类型与动态类型

静态类型

动态类型

为什么要区分这两种类型?

对象切片

虚函数表(多态底层逻辑)的工作原理

---一道多态的经典例题---

代码执行流程

关键知识点

[C++11新特性:override 与 final 关键字](#C++11新特性:override 与 final 关键字)

[override --- 检查重写的关键字](#override --- 检查重写的关键字)

[final --- 不允许继承与重写的关键字](#final --- 不允许继承与重写的关键字)

虚函数重写的⼀些其他问题

协变(了解即可,不常用)

析构函数的重写

[重写 --- 重载 --- 隐藏 的对比(一图流)](#重写 --- 重载 --- 隐藏 的对比(一图流))

纯虚函数和抽象类

纯虚函数

抽象类

注意事项


什么是多态?

多态,顾名思义就是一个物品的"多种状态" ===>同一个操作,作用在不同对象上,会产生不同的结果:

举个生活中的例子,在购买车票的场景中,不同身份的人买票规则不同:

  • 普通人全价买票
  • 学生凭借学生证能享受半价优惠
  • 军人则享有优先购票的特权。

这里 "买票" 就是一个统一的接口,但对于不同身份(类型)的对象,有着不同的实现方式。

对应到编程中:假设有一个 "图形" 基类Shape ,它有一个绘制图形的函数draw

Circle (圆形)和Rectangle (矩形)是继承自Shape 的子类,它们都重写了draw函数,以实现各自独特的绘制逻辑 。

当使用Shape类型的指针或引用,分别指向CircleRectangle 对象,并调用draw函数时,就会呈现出不同的行为 ------ 绘制出圆形和矩形,这便是多态在 C++ 中的体现。

多态的定义与实现

代码层面,多态就是:在继承关系下,用不同的对象去调用同一个函数,却产生了不同的行为

比如我们之前说的买票例子:

  • Person对象买票是全价
  • Student对象买票就是半价

都是调用buyTicket,结果不一样,这就是多态。

那么,有哪些多态呢?

C++ 多态的两大分类:静态与动态

在 C++ 中,多态主要分为编译时多态(静态多态)和运行时多态(动态多态),它们各自有着独特的实现方式与应用场景 。

编译时多态(静态多态)

指在编译阶段编译器就能确定函数调用的具体版本 。这种多态性主要通过函数重载和模板来实现。

函数重载是指在同一个作用域内,可以定义多个同名函数,但这些函数的参数列表(参数类型、参数个数或参数顺序)必须不同 。

编译器会根据调用函数时传入的实参类型和个数,在编译阶段确定调用哪个函数版本 。

不了解函数重载可以看下面的文章:

初学C++:函数大转变:缺省参数与函数重载https://blog.csdn.net/weixin_66776566/article/details/154156542?spm=1001.2014.3001.5501模板是 C++ 中实现泛型编程的重要工具,也是实现编译时多态的一种方式 。

模板可以让我们编写通用的代码,这些代码能够处理不同的数据类型,而不需要为每种类型都编写一份独立的代码 。

同样的,不了解模版可以看下面的文章:

c++:详解:模版https://blog.csdn.net/weixin_66776566/article/details/156945543?spm=1001.2014.3001.5501

用一段代码就明白了:

cpp 复制代码
#include <iostream>
using namespace std;
// 函数重载示例
void print(int num) {
    std::cout << "打印整数: " << num << std::endl;
}
void print(double num) {
    std::cout << "打印双精度浮点数: " << num << std::endl;
}
void print(const char* str) {
    std::cout << "打印字符串: " << str << std::endl;
}

// 函数模板示例
template <typename T>
T max(T a, T b) {
    return (a > b)? a : b;
}

int main() {
    print(10);       // 调用print(int num)
    print(3.14);     // 调用print(double num)
    print("Hello");  // 调用print(const char* str)
   
    int a = 5, b = 10;
    double c = 3.14, d = 2.71;
    std::cout << "较大的整数是: " << max(a, b) << std::endl;  // 编译器生成max(int, int)实例
    std::cout << "较大的浮点数是: " << max(c, d) << std::endl;  // 编译器生成max(double, double)实例
    return 0;
}

通过代码,我们可以看到:

同样的函数,根据传参的不同(或是像模版那样,同一逻辑,传参与返回值不同),选择不同的方法。

这也说明了多态的概念:用不同的对象去调用同一个函数,却产生了不同的行为

运行时多态(动态多态)本博客重点(涉及底层原理)

运行时多态,也称为动态多态,是指在程序运行阶段才确定函数调用的具体版本 。

这种多态性主要通过虚函数继承来实现,它允许我们使用基类的指针或引用调用派生类的函数,从而实现 "一个接口,多种实现" 的效果 。

而要实现动态多态,我们的代码要满足三个条件:

  • 存在继承关系:必须有基类和派生类,派生类继承自基类 。
  • 基类中声明虚函数 :在基类中使用virtual关键字声明虚函数 。
  • 通过基类指针或引用调用虚函数:使用基类的指针或引用指向派生类对象,并调用虚函数 。

存在继承关系:必须有基类和派生类,派生类继承自基类

在这里,多态的本质就变为了:通过一个通用的接口(基类)来操作多种具体的类型(派生类)。

cpp 复制代码
class Animal {             // 基类
public:
    virtual void speak() { cout << "Animal speaks" << endl; }
};

class Dog : public Animal { // 派生类1
public:
    void speak() override { cout << "Dog barks" << endl; }
};

class Cat : public Animal { // 派生类2
public:
    void speak() override { cout << "Cat meows" << endl; }
};

由于继承关系是" is - a "的关系,所以这里可以看做:派生类是基类的一种特化。没有继承这一个关系,就没有特化这一说法,也就没有多态这个说法(最多也就像STL中容器适配器那样)。

而多态是依赖于类型之间的可替换性(也叫里氏替换原则),而学过继承后,我们知道:在继承关系之间存在切片(基类和派生类间的转换),而切片刚刚好提供可替换性这个前提。

基类中声明虚函数:在基类中使用 virtual 关键字声明虚函数

这个小结牵扯到的知识点很多,也很重要,所以会详细讲一下

什么是虚函数

概念:允许子类重写该函数以适应其自身需求的函数。说白了,虚函数就是为了实现多态而设计出来的(不太准确,但是刚学多态只需要知道就行)。

而虚函数很简单:在类的成员函数前面加上 virtual 关键字,这个函数就变成了虚函数。但要注意:只有成员函数才能被转换为虚函数,全局函数以及静态函数都不行。

cpp 复制代码
class Animal {             // 基类
public:
    //前面加上关键字,这个成员函数就变成了虚函数
    virtual void speak() { cout << "Animal speaks" << endl; }
};

class Dog : public Animal { // 派生类1
public:
    //重写父类的虚函数,实现多态
    void speak() override { cout << "Dog barks" << endl; }
};

class Cat : public Animal { // 派生类2
public:
    void speak() override { cout << "Cat meows" << endl; }
};

虚函数底层原理要牵扯到另一个知识点:虚函数表(继承中虚继承也用了这一点),但虚函数表晦涩难懂,所以后面单开一篇细讲。

总的来说:使用虚函数是为了告诉编译器:这个函数可能会被派生类重写,所以要动态决定调用的模块。

虚函数的覆盖/重写

重写(也叫覆盖)指的是:在派生类中定义一个和基类完全一样的虚函数------返回值类型、函数名、参数列表都完全相同。这样,派生类的这个函数就重写了基类的虚函数。

cpp 复制代码
#include <iostream>
using namespace std;

class Person {
public:
    virtual void buyTicket() {   // 虚函数
        cout << "普通人:全价购票" << endl;
    }
};

class Student : public Person {
public:
    void buyTicket() override {   // 重写,override关键字(C++11)可加可不加,加上更清晰
        cout << "学生:半价购票" << endl;
    }
};

class Soldier : public Person {
public:
    virtual void buyTicket() {    // 即使这里不写virtual,也构成重写(但不推荐)
        cout << "军人:优先购票" << endl;
    }
};

int main() {
    // 基类指针指向派生类对象
    Person* p1 = new Student();
    Person* p2 = new Soldier();
    Person p3;  // 基类对象

    p1->buyTicket();   // 输出:学生:半价购票
    p2->buyTicket();   // 输出:军人:优先购票
    p3.buyTicket();    // 输出:普通人:全价购票(对象调用,不构成多态)

    delete p1;
    delete p2;
    return 0;
}

注意:当父类的成员函数是虚函数,而子类刚刚好重写这个虚函数,子类的成员函数可以不写virtual关键字也能构成重载(父类的虚函数被继承后在子类中依旧是虚函数)。但是这么写不规范,属于防御性编程但考试爱考

通过基类指针或引用调用虚函数(多态的底层原理)

静态类型与动态类型

在 C++ 中,多态的核心是**"通过统一的接口操作不同的具体类型"** 。这个统一的接口就是基类指针引用

静态类型

变量在代码中被声明时的类型,由编译器在编译阶段确定,且不会改变

静态绑定: 函数调用在编译时就被确定下来,即编译器根据变量的静态类型决定调用哪个函数(相当于对静态函数的处理)。

特点

  • 编译器根据静态类型(例如int double等)进行语法检查(如成员访问权限、函数调用合法性)。

  • 对于非虚函数,编译器会根据静态类型直接确定调用哪个版本的函数(静态绑定)。

  • 静态类型决定了变量在内存中的布局解释方式(例如通过指针能访问哪些偏移量)。

cpp 复制代码
Person* p;          // 静态类型为 Person*(指向Person的指针)
Student* s;         // 静态类型为 Student*

简单的说:静态类型就是你写代码时给变量定的类型,比如 Person* p。编译器编译的时候看到这个,就认为 p 是一个指向 Person 的指针,所有检查都按这个来。这个类型是写死的,不会再变

所以说:这个阶段主要是确定语法之类的正确性以及防止类型的非法操作等,主要是保证转汇编不会出错

记住一点:静态类型只管编译阶段,运行时就不归他管了~(但他的影响会持续到程序运行时,但他不会干扰运行)

动态类型

指针或引用****实际指向对象的类型,由程序运行时决定,可能随程序执行而变化(但通常指针指向的对象创建后类型不变)。

动态绑定 :函数调用在运行时才被解析,即根据指针或引用动态类型决定调用哪个函数版本。这是通过**虚函数表(vtable)**实现的(调用虚函数,重写虚函数的那套逻辑)。

特点

  • 只有指针或引用才具有动态类型(普通变量没有动态类型,因为它的类型就是声明类型 --- 也就是静态类型)。

  • 动态类型决定了通过基类指针调用虚函数时的实际函数版本动态绑定)。

  • 动态类型需要运行时信息(如虚函数表)来支持多态。

cpp 复制代码
Person* p = new Student();  // 静态类型: Person*, 动态类型: Student*
p = new Teacher();          // 动态类型变为 Teacher* (仍为同一静态类型)

简单的说:动态类型就是你所声明指针或引用真正指向的对象的类型。比如你写 p = new Student(),虽然 p 的静态类型还是 Person*,但它实际指向的是一个 Student 对象。这个 Student 就是动态类型,只有在程序跑起来的时候才知道(当然,后面这个指针指向新的对象 --比如 p = new Teacher() -- ,那么Teacher也是动态类型)。

记住一点:设计动态类型的目的就是完成多态,而实现多态起到决定性作用的其实是虚函数表。

为什么要区分这两种类型?
  • 静态类型保证代码在编译时的类型安全,防止非法操作。

  • 动态类型实现运行时多态,使程序能根据对象实际类型表现出不同行为,提高灵活性和可扩展性。

  • 前者保证编译顺利,后者则完成多态的转变。

  • 但是要注意:一个对象的类型的属性是分为静态类型多态类型的。

cpp 复制代码
class Person { 
virtual void speak() { 
cout << "Person"; 
} 
};
class Student : public Person { void speak() override { 
cout << "Student"; 
} 
};

int main() {
    Person* p = new Student();
    p->speak();  // 输出 "Student" ------ 根据动态类型调用
}

在上面的代码中:

  • 编译时 :编译器看到 p 的静态类型是 Person*,但 speak 是虚函数,于是生成通过虚函数表间接调用的代码。

  • 运行时 :根据 p 指向的实际对象(动态类型 Student)从虚函数表中取出 Student::speak 的地址并调用。

  • 而对于代码中的 p ,他的静态类型是Person,而动态类型是Student。

了解了这两个类型的区别后,我们就知道:动态类型是实现多态的关键。

而我们不使用动态类型而是直接使用对象,则会直接静态绑定

cpp 复制代码
Dog dog;
Animal a = dog;  // 这里发生了对象切片
a.speak();

此时 a静态类型 是 Animal,动态类型也是 Animal(因为 dog 被赋值给 a 时,只复制了基类部分,派生类部分被切掉了)。

所以 a 永远是一个 Animal 对象,调用 speak 必然是 Animal::speak,从而不会产生多态。

对于静态绑定与动态绑定,后续会在详解虚函数表中说明~

对象切片

在彩妙的C++继承的博客中讲到,当你尝试用基类对象直接接收派生类对象时,会发生切片

cpp 复制代码
Student Stu;
Person a = Stu;   // 切片发生

这就相当于:

  • 我们创建了一个新的 Person对象 a 。
  • 把与Student对象重叠(相同)的成员对象进行赋值操作(就是把Student中Person这个类中一样的对象复制过去)
  • 之后,a就是一个纯粹的Person,而不具有Student的特性,所以不构成多态(你算你的,我算我的,各论各的)

所以,在发生切片的情况下,是不构成多态的,所以也pass了

虚函数表(多态底层逻辑)的工作原理

这一点非常晦涩难懂却非常重要,单另一篇来讲:

---一道多态的经典例题---

看看下面代码:

cpp 复制代码
class A
{
public:
    virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
    virtual void test(){ func();}
};
class B : public A
{
public:
    void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
    B*p = new B;
    p->test();
    return 0;
}

选项:A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

上面的题属于多态最经典的代码题(曾经被多家公司使用过,用来筛选高级程序员)。

代码执行流程
  • main 中创建 B 对象,并用 B* 指针 p 指向它。

  • 调用 p->test()。test 是虚函数,但 B 没有重写它,因此通过虚函数表找到并执行 A::test。

  • 在 A::test 内部,执行 func()。这里的 func() 等价于 this->func(),this 的类型是 A*(静态类型),但实际指向的是 B 对象(动态类型)。

  • 由于func是虚函数,动态绑定到****B::func

  • 调用 B::func 时,没有传递实参,因此使用默认参数。但默认参数取自调用点的静态类型,即 A* 类型,所以使用的是 A::func 的默认参数 val = 1。

  • 最终输出 B->1

关键知识点
  1. 虚函数动态绑定:通过基类指针或引用调用虚函数时,实际执行的是对象的动态类型(实际类型)对应的函数版本。

  2. 默认参数静态绑定:默认参数值在编译时根据调用点的静态类型(指针或引用的声明类型)确定,不会动态绑定。

C++11新特性:override 与 final 关键字

C++11 引入了override和final关键字,用于增强虚函数重写的安全性和代码的可读性 。

override --- 检查重写的关键字

在派生类中,当重写基类的虚函数时,可以在函数声明后加上override关键字 。这样,编译器会检查该函数是否确实重写了基类的虚函数,如果函数签名与基类中的虚函数不匹配,或者基类中没有对应的虚函数,编译器会报错 :

cpp 复制代码
class Base {
public:
    virtual void Func() {
        std::cout << "Base::Func()" << std::endl;
    }
};
class Derived : public Base {
public:
    void Func() override {
        std::cout << "Derived::Func()" << std::endl;
    }
    // 以下声明会导致编译错误,因为基类中没有void Func(int)函数
    // void Func(int) override { } 
};

本质上这个关键字是一个检查函数否重写的关键字,建议加上,以确保函数的健壮性。

final --- 不允许继承与重写的关键字

final关键字有两种用法 。

一是修饰虚函数,表示该虚函数在当前类中是最终实现,不能在派生类中被重写 :

cpp 复制代码
class Base {
public:
    virtual void Func() final {
        std::cout << "Base::Func()" << std::endl;
    }
};
class Derived : public Base {
public:
    // 以下声明会导致编译错误,因为Base::Func()是final函数
    // void Func() override { } 
};

二是修饰类,表示该类不能被继承 :

cpp 复制代码
class FinalClass final {
    // 类的成员
};
// 以下声明会导致编译错误,因为FinalClass不能被继承
// class SubClass : public FinalClass { } 

虚函数重写的⼀些其他问题

协变(了解即可,不常用)

通常情况下,派生类重写基类的虚函数时,返回值类型必须与基类完全相同

但 C++ 允许一个例外:如果基类虚函数返回的是某个类的指针或引用,那么派生类重写的函数可以返回****该类的派生类的指针或引用。这种允许返回类型"一起变化"的规则就叫协变

听起来非常难懂,但没关系:

  • 日常业务逻辑代码 (CRUD、接口封装):很少用(甚至来说几乎不用),甚至可能见不到。因为这类代码通常不涉及复杂的类层次和克隆需求,虚函数本身都用得克制。

  • 基础库、框架、工具集出现频率中等。比如标准库虽没直接使用(C++17前),但很多第三方库(如LLVM、Qt的部分内部实现)会用到,用来简化多态对象的复制和创建。

  • 设计模式密集的项目 (如游戏开发、图形引擎):相对常见。像原型模式、抽象工厂模式里,协变能减少类型转换,让代码更干净。

本质上:协变最常见在克隆模式工厂方法建造者模式这类需要返回自身类型(或相关类型)的地方。但我们也可以不使用它------用基类指针 + 强转替代。但愿意深度学习C++的可以试着学一下(彩妙后面有时间也会出一期来讲讲协变与逆变

析构函数的重写

在谈继承时,我们说到:存在继承关系的类中,其析构函数构成重写(override - 也叫覆盖)。

由于构成重写关系,导致我们调用析构函数时,**只能调用****父类(基类)**的析构函数(因为析构函数没有构成多态,这里父类的指针Person *p 只进行静态绑定),从而导致内存泄漏(子类的对象无法释放):

cpp 复制代码
class Person {
public:
    ~Person() { cout << "~Person()" << endl; }   // 非虚析构
};

class Student : public Person {
    int* data;
public:
    Student() { data = new int[100]; }
    ~Student() { 
        delete[] data; 
        cout << "~Student()" << endl; 
    }
};

int main() {
    Person* p = new Student();
    delete p;   // 只会调用 ~Person(),不会调用 ~Student()
    return 0;
}

而为了解决这个问题,我们就要把基类的析构函数声明为虚函数

cpp 复制代码
class Person {
public:
    virtual ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person {
    int* data;
public:
    Student() { data = new int[100]; }
    ~Student() override {   // override 可选,但推荐加上
        delete[] data;
        cout << "~Student()" << endl;
    }
};

int main() {
    Person* p = new Student();
    delete p;   // 先调用 ~Student(),再调用 ~Person()
    return 0;
}

析构函数重写的特殊性:

  • 名字不同 :每个类的析构函数名字都不同(~Person 与 ~Student),但它们仍然是虚函数重写关系C++ 标准规定:如果基类的析构函数是虚函数,那么派生类的析构函数自动成为虚函数并重写基类的析构函数(无论是否加 virtualoverride 关键字)。

  • 调用顺序 :通过基类指针删除派生类对象时,会先调用****派生类的析构函数,然后沿着继承链依次调用基类的析构函数(就是析构函数的调用顺序)。

  • 应对方法只要一个类被设计为基类(即可能会被继承),就应该将它的析构函数声明为虚函数。否则,通过基类指针删除派生类对象将导致未定义行为(通常表现为资源泄漏)。

重写 --- 重载 --- 隐藏 的对比(一图流)

纯虚函数和抽象类

纯虚函数

纯虚函数(pure virtual function)是在基类中声明的虚函数,它没有具体的实现,要求派生类必须提供自己的实现。

声明语法是在函数原型后面加上 = 0

cpp 复制代码
virtual 返回类型 函数名(参数列表) = 0;

class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};
  • 定义接口:纯虚函数只需要声明,不需要定义(因为要被派生类重写)。
  • 实现多态:通过基类指针或引用调用纯虚函数时,会根据实际对象类型动态绑定到派生类的实现。

抽象类

包含至少一个****纯虚函数的类称为抽象类(abstract class)。抽象类用于描述抽象概念,不能直接实例化对象。

特点:

不能创建对象:抽象类不能定义该类的实例,但可以定义指向该类的指针或引用,用于多态。

cpp 复制代码
Shape s;          // 错误,抽象类不能实例化
Shape* pShape;       // 正确,可以定义指针

可以包含普通成员:抽象类可以拥有普通成员函数、数据成员、构造函数、析构函数等,这些可以被派生类继承和使用。

派生类的责任 :如果一个派生类没有实现基类中的所有纯虚函数,那么该派生类仍然是抽象类,不能实例化。只有实现了所有纯虚函数派生类才能成为具体类(concrete class)。

构造函数和析构函数:抽象类可以有构造函数和析构函数,用于初始化基类部分或进行资源管理。析构函数通常声明为虚函数,以确保正确释放派生类资源。

cpp 复制代码
#include <iostream>
using namespace std;

// 抽象类 Shape
class Shape {
public:
    virtual void draw() const = 0;  // 纯虚函数,定义绘制接口
    virtual double area() const = 0; // 纯虚函数,定义面积接口
    virtual ~Shape() {}              // 虚析构函数,确保派生类资源正确释放
};

// 具体类 Circle
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    void draw() const override {
        cout << "Drawing a circle" << endl;
    }
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

// 具体类 Rectangle
class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    void draw() const override {
        cout << "Drawing a rectangle" << endl;
    }
    double area() const override {
        return width * height;
    }
};

int main() {
    // Shape s;            // 错误,抽象类不能实例化
    Shape* shapes[2];
    shapes[0] = new Circle(5.0);
    shapes[1] = new Rectangle(4.0, 6.0);

    for (int i = 0; i < 2; ++i) {
        shapes[i]->draw();
        cout << "Area: " << shapes[i]->area() << endl;
    }

    for (int i = 0; i < 2; ++i) {
        delete shapes[i];
    }
    return 0;
}

简单的说,纯虚函数与抽象类类似于模版(相当于提供蓝图,而非C++里面的模版),而继承于抽象类实现的具体类就是模版的实例化(按照蓝图构建的物品):

  • 抽象类就像一张设计蓝图,规定了必须实现哪些功能(纯虚函数),但本身不完成具体构造,因此不能直接制造对象(不能实例化)。

  • 具体类(派生类)按照蓝图的要求,实现所有纯虚函数,相当于按图纸建造出的实际物品,可以创建对象并使用。

  • 底层原理:利用OOP(面向对象语言),进行对父类的封装(纯虚函数的接口设计,且符合对拓展开放,对修改封闭的原则),子类对父类的继承(利用通用接口,实现代码复用),最后在进行多态来衍生多种不同的子类(利用基类指针或引用操作一系列派生类对象,实现动态绑定)。

注意事项

  1. 抽象类不能用于参数类型、函数返回类型或显式类型转换(如关键字: dynamic_cast 可以用于指针或引用)。

  2. 如果派生类只实现了部分纯虚函数,它仍然是抽象类,无法创建对象。

  3. 纯虚函数可以有实现,但即使有实现,类仍是抽象的,派生类必须重写(或至少声明)该函数。

  4. 析构函数应声明为虚函数,以避免通过基类指针删除派生类对象时产生未定义行为。


好啦,本篇博客到此就告一段落啦,后续彩妙还会在新的博客再次对本博客做一些零碎知识点的补充,喜欢彩妙的文章点点赞,关注彩妙,获取更多优质好文吧~

相关推荐
qq_24218863322 小时前
使用 PyInstaller 打包 Python 脚本为 EXE(教程)
开发语言·python
苦学编程的谢2 小时前
好运buff机 ------ 测试报告
java·开发语言·功能测试
黎雁·泠崖2 小时前
Java常用类核心精讲 · 七篇精华总结
java·开发语言
Zevalin爱灰灰2 小时前
针对汽车工业软件安全性的C语言编码规范——MISRA C
c语言·开发语言·汽车·嵌入式
JienDa2 小时前
HaiO安装与快速开始
开发语言·php
lightqjx2 小时前
【C++】C++11 - Lambda表达式+包装器
开发语言·c++·c++11·lambda·包装器
BHXDML2 小时前
操作系统实验:(七)动态分区分配方式的模拟
开发语言·数据库·操作系统
载数而行5202 小时前
算法系列1之最小生成树
c语言·数据结构·c++·算法·贪心算法
额,不知道写啥。2 小时前
HAO的DP
c++·算法·深度优先·动态规划