多态(虚函数核心作用原理)--C++学习(0)

当通过基类指针或引用的方法指向派生类的对象的时候,执行同名的函数,会先去查看虚函数指针的表单,查看是否有对应的函数:(1)有则执行(2)无则执行基类的函数(3)基类中没有这个函数则编译不成功

多态

动态绑定

实现:基类中创建一个虚函数,在不同的派生类中重写这个虚函数

使用:使用基类的指针指向生成的派生类对象,执行的函数为派生类的函数(不同对象相同的接口执行不同的功能)

非多态

静态绑定

实现:基类中创建一个非虚函数,在不同的派生类中重写这个虚函数

使用:使用基类的指针指向生成的派生类对象,执行的函数为基类的函数

只要是基类指针或引用的方式,指向派生类对象,都能够触发多态

基类指针指向派生类对象时,不能够执行派生类独有的函数(除非使用强制类型转换,将基类指针转换为派生类指针类型)

使用基类指针指向派生类对象时,记得需要在基类的析构函数添加virtual关键字,防止delete时无法执行派生类的析构函数

C++ 虚函数:核心作用、原理及场景解析

虚函数(virtual 函数)是 C++ 实现运行时多态(动态多态) 的核心机制,其本质是通过「运行时动态绑定」替代「编译期静态绑定」,让基类的指针/引用能根据指向的实际对象类型 (而非指针/引用的静态类型)调用对应函数。结合Person/Student/Teacher 代码场景,以下从「核心作用、底层原理、使用场景、避坑点」四个维度详细拆解虚函数的作用:

一、虚函数的核心定义

虚函数是在基类中用 virtual 关键字声明的成员函数,派生类可「重写(override)」该函数;当通过基类指针/引用 调用该函数时,编译器不会在编译期确定调用哪个版本,而是在运行时根据指针/引用指向的「实际对象类型」(基类/派生类)选择对应的函数实现。

C++ 复制代码
// 基类声明虚函数
class Person {
public:
    virtual void showInfo() { // 虚函数核心:virtual 关键字
        cout << "Person 基类" << endl;
    }
};

// 派生类重写虚函数
class Student : public Person {
public:
    void showInfo() override { // override 显式声明重写(C++11+,推荐)
        cout << "Student 派生类" << endl;
    }
};

二、虚函数的核心作用(按优先级排序)

作用1:实现运行时多态(最核心)

这是虚函数的根本价值------让「同一接口(基类函数)」适配「不同派生类的行为」,且行为选择延迟到运行时。

对比:无虚函数(静态绑定)vs 有虚函数(动态绑定)
无虚函数(静态绑定) 有虚函数(动态绑定)
编译期根据「指针类型」确定函数版本 运行期根据「实际对象类型」确定函数版本
基类指针指向派生类,仍调用基类函数 基类指针指向派生类,调用派生类函数
无多态能力,代码耦合度高 支持多态,代码灵活、可扩展
示例(你的代码场景):
C++ 复制代码
int main() {
    Person* p1 = new Student(); // 基类指针指向派生类对象
    Person* p2 = new Person();  // 基类指针指向基类对象

    // 无虚函数:两次都调用 Person::showInfo()(静态绑定)
    // 有虚函数:p1调用 Student::showInfo(),p2调用 Person::showInfo()(动态绑定)
    p1->showInfo(); 
    p2->showInfo();

    delete p1;
    delete p2;
    return 0;
}

输出(有虚函数)

Plain 复制代码
Student 派生类
Person 基类
作用2:保证派生类析构函数被完整调用(避坑关键)

若基类析构函数未声明为虚函数,通过基类指针 delete 派生类对象时,只会调用基类析构函数,派生类析构函数被「截断」,导致派生类资源泄漏(如堆内存、文件句柄等)。

示例(你此前问过的 delete 派生类问题):
C++ 复制代码
// 错误:基类析构非虚
class Person {
public:
    ~Person() { cout << "Person 析构" << endl; } // 非虚析构
};

class Student : public Person {
public:
    ~Student() { cout << "Student 析构" << endl; }
};

int main() {
    Person* p = new Student();
    delete p; // 仅调用 Person::析构,Student::析构丢失 → 内存泄漏
    return 0;
}

修复:基类析构设为虚函数

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

// delete p 时:先调用 Student::析构 → 再调用 Person::析构(完整销毁)

输出

Plain 复制代码
Student 析构
Person 析构
作用3:解耦代码,符合「开闭原则」

虚函数让「调用方代码」仅依赖基类接口,新增派生类时无需修改调用方代码,只需重写虚函数即可扩展功能(对扩展开放,对修改关闭)。

示例:新增 Teacher 类,调用方无需改动
C++ 复制代码
// 调用方代码(仅依赖基类 Person)
void printPersonInfo(Person* p) {
    p->showInfo(); // 虚函数自动适配新派生类
}

// 新增 Teacher 类(仅重写虚函数)
class Teacher : public Person {
public:
    void showInfo() override {
        cout << "Teacher 派生类" << endl;
    }
};

int main() {
    printPersonInfo(new Student());  // 输出 Student 派生类
    printPersonInfo(new Teacher()); // 输出 Teacher 派生类(调用方未改)
    return 0;
}
作用4:避免「对象切片」的功能退化(间接作用)

若用基类对象接收派生类对象(如 Person p = Student();),会发生「对象切片」------仅拷贝派生类中的基类部分,丢失派生类成员和行为。而虚函数+基类指针/引用可避免切片,完整保留派生类的行为:

C++ 复制代码
// 对象切片(无虚函数,功能退化)
Person p = Student();
p.showInfo(); // 调用 Person::showInfo()(丢失 Student 行为)

// 虚函数+指针(无切片,保留派生类行为)
Person* p = new Student();
p->showInfo(); // 调用 Student::showInfo()

三、虚函数的底层原理:虚函数表(vtable)+ 虚指针(vptr)

虚函数的动态绑定依赖编译器生成的「虚函数表(vtable)」和「虚指针(vptr)」,这是理解虚函数作用的关键:

1. 虚函数表(vtable)
  • 每个包含虚函数的类(基类/派生类)会被编译器生成一张独立的「虚函数表」,本质是存储虚函数地址的数组;

  • 基类的 vtable 存储基类虚函数的地址;

  • 派生类的 vtable 会继承基类 vtable,并将重写的虚函数地址替换为自己的实现地址。

2. 虚指针(vptr)
  • 每个含虚函数的对象会包含一个隐藏的「虚指针(vptr)」(占8字节,64位系统),指向所属类的 vtable;

  • 对象构造时,编译器会初始化 vptr:基类对象的 vptr 指向基类 vtable,派生类对象的 vptr 指向派生类 vtable。

3. 动态绑定的执行流程
C++ 复制代码
Person* p = new Student();
p->showInfo(); // 动态绑定流程:
  1. p 指向的对象的 vptr → 找到 Student 类的 vtable;

  2. 在 vtable 中找到 showInfo() 的地址 → 调用 Student::showInfo()

  3. p 指向 Person 对象,则通过 vptr 找到 Person vtable,调用 Person::showInfo()

四、虚函数的扩展用法:纯虚函数与抽象类

若基类仅想定义「接口」,不想提供具体实现,可将虚函数声明为「纯虚函数」(virtual 函数名() = 0;),包含纯虚函数的类称为「抽象类」:

  • 抽象类不能实例化(如 Person p; 报错),只能作为基类被继承;

  • 派生类必须重写纯虚函数,否则派生类也成为抽象类。

示例(接口设计):
C++ 复制代码
class Person {
public:
    virtual void showInfo() = 0; // 纯虚函数:仅定义接口,无实现
    virtual ~Person() = default; // 虚析构仍需声明
};

// Student 必须重写 showInfo(),否则无法实例化
class Student : public Person {
public:
    void showInfo() override {
        cout << "Student 实现接口" << endl;
    }
};

作用 :强制派生类实现统一接口,适合框架/库的接口设计(如 QT 的 QWidget 纯虚函数 paintEvent())。

五、虚函数的避坑点(关键规则)

1. 重写虚函数的严格规则

派生类重写的虚函数必须满足:

  • 函数名、参数列表、返回值(协变除外)完全一致

  • 基类函数加 virtual,派生类可加可不加(推荐加 override,编译期检查重写是否正确);

  • 访问权限不影响重写(如基类 public 虚函数,派生类可 private,但调用时需符合权限)。

2. 构造/析构中调用虚函数不触发多态
  • 构造函数执行时:vptr 逐步初始化(先基类 vptr,后派生类 vptr),此时调用虚函数,仅调用当前类的版本(如基类构造中调用 showInfo(),仅调用基类版本);

  • 析构函数执行时:vptr 逐步回退(先派生类 vptr,后基类 vptr),此时调用虚函数,仅调用当前类的版本。

3. 虚函数的性能开销
  • 内存开销:每个对象增加一个 vptr(8字节,64位系统);

  • 性能开销:运行时通过 vptr 查找 vtable(纳秒级,几乎可忽略,除非极致性能场景);

  • 优化:非多态场景(如仅用栈对象)无需声明虚函数,避免不必要的开销。

六、示例

(1)基类没有虚函数
C++ 复制代码
// 基类:Person(定义统一接口)
class Person {
public:
    string name;
    Person(string n) : name(n) {}

    // 虚函数:核心!告诉编译器"这个函数要运行时绑定"
    void showInfo() {
        cout << "Person:" << "name:" << name << "(Base::Person)" << endl;
    }

    // 虚析构函数:避免派生类析构不执行(关键!)
    virtual ~Person() {}
};

// 派生类1:Student(重写虚函数)
class Student : public Person {
public:
    int score;
    Student(string n, int s) : Person(n), score(s) {}

    // 重写基类虚函数(override 关键字可选,建议加,编译期检查重写是否正确)
    void showInfo(){
        cout << "Student" << "name:" << name << ",score:" << score << "(Son::Student)" << endl;
    }
};

// 派生类2:Teacher(重写虚函数)
class Teacher : public Person {
public:
    string course;
    Teacher(string n, string c) : Person(n), course(c) {}

//    void showInfo() override {
//        cout << "姓名:" << name << ",课程:" << course << "(派生类Teacher)" << endl;
//    }
};

 //调用方:只需接收基类指针,无需关心具体是Student/Teacher
void printPersonInfo(Person* p) {
    p->showInfo(); // 运行时:根据p指向的实际对象,执行对应showInfo()
}

int main() {

    Person* p1 = new Student("zhangsan", 95);  // 基类指针指向Student对象
    Person* p2 = new Teacher("lisi", "C++");// 基类指针指向Teacher对象

    Person p3("father");
    Person p4 = p3;
    p4.name = "father_fake";

    Student p5("zhangsan_fake", 100);
    Teacher p6("lisi_fake", "C#");

    printPersonInfo(p1); // 输出:姓名:张三,分数:95(派生类Student)
    printPersonInfo(p2); // 输出:姓名:李四,课程:C++(派生类Teacher)
    printPersonInfo(&p3);
    printPersonInfo(&p4);
    printPersonInfo(&p5);
    printPersonInfo(&p6);

    p1->showInfo();
    p2->showInfo();
    p3.showInfo();
    p4.showInfo();
    p5.showInfo();
    p6.showInfo();

    Person *p7 = &p5;
    p7->showInfo();



    delete p1; // 调用Student析构 + Person虚析构
    delete p2; // 调用Teacher析构 + Person虚析构
    return 0;
}

结果:

(2)基类有虚函数

示例:

c++ 复制代码
// 基类:Person(定义统一接口)
class Person {
public:
    string name;
    Person(string n) : name(n) {}

    // 虚函数:核心!告诉编译器"这个函数要运行时绑定"
    virtual void showInfo() {
        cout << "Person:" << "name:" << name << "(Base::Person)" << endl;
    }

    // 虚析构函数:避免派生类析构不执行(关键!)
    virtual ~Person() {}
};

// 派生类1:Student(重写虚函数)
class Student : public Person {
public:
    int score;
    Student(string n, int s) : Person(n), score(s) {}

    // 重写基类虚函数(override 关键字可选,建议加,编译期检查重写是否正确)
    void showInfo(){
        cout << "Student" << "name:" << name << ",score:" << score << "(Son::Student)" << endl;
    }
};

// 派生类2:Teacher(重写虚函数)
class Teacher : public Person {
public:
    string course;
    Teacher(string n, string c) : Person(n), course(c) {}

//    void showInfo() override {
//        cout << "姓名:" << name << ",课程:" << course << "(派生类Teacher)" << endl;
//    }
};

 //调用方:只需接收基类指针,无需关心具体是Student/Teacher
void printPersonInfo(Person* p) {
    p->showInfo(); // 运行时:根据p指向的实际对象,执行对应showInfo()
}

int main() {

    Person* p1 = new Student("zhangsan", 95);  // 基类指针指向Student对象
    Person* p2 = new Teacher("lisi", "C++");// 基类指针指向Teacher对象

    Person p3("father");
    Person p4 = p3;
    p4.name = "father_fake";

    Student p5("zhangsan_fake", 100);
    Teacher p6("lisi_fake", "C#");

    printPersonInfo(p1); // 输出:姓名:张三,分数:95(派生类Student)
    printPersonInfo(p2); // 输出:姓名:李四,课程:C++(派生类Teacher)
    printPersonInfo(&p3);
    printPersonInfo(&p4);
    printPersonInfo(&p5);
    printPersonInfo(&p6);

    p1->showInfo();
    p2->showInfo();
    p3.showInfo();
    p4.showInfo();
    p5.showInfo();
    p6.showInfo();

    Person *p7 = &p5;
    p7->showInfo();



    delete p1; // 调用Student析构 + Person虚析构
    delete p2; // 调用Teacher析构 + Person虚析构
    return 0;
}

结果:

六、总结:虚函数的核心价值

维度 虚函数的核心作用
功能 实现运行时多态,让基类指针适配不同派生类行为
资源管理 保证派生类析构函数完整执行,避免内存泄漏
代码设计 解耦代码,符合开闭原则,支持接口化设计
底层 通过 vtable/vptr 实现动态绑定
结合你的代码场景(Person* p = new Student();),虚函数是让基类指针「识别」派生类对象的唯一方式------没有虚函数,基类指针永远只能调用基类函数,失去多态能力;有了虚函数,才能真正实现「一个接口,多种形态」的面向对象设计。
相关推荐
Sunsets_Red2 小时前
2025 FZYZ夏令营游记
java·c语言·c++·python·算法·c#
自由生长20242 小时前
从流式系统中思考-C++生态和Java生态的区别
java·c++
车载测试工程师3 小时前
CAPL学习-AVB交互层-媒体函数2-其他类函数待分类
学习·tcp/ip·媒体·capl·canoe
深蓝海拓3 小时前
PySide6从0开始学习的笔记(十) 样式表(QSS)
笔记·python·qt·学习·pyqt
饕餮怪程序猿3 小时前
订单分批算法设计与实现:基于商品相似性的智能分拣优化(C++)
开发语言·c++·算法
deng-c-f3 小时前
Linux C/C++ 学习日记(59):手写死锁监测的组件
学习
深蓝海拓3 小时前
PySide6从0开始学习的笔记(十三) IDE的选择
笔记·python·qt·学习·pyqt
崇山峻岭之间3 小时前
Matlab学习记录05
开发语言·学习·matlab
nnsix3 小时前
Unity 新InputSystem 学习笔记
笔记·学习