【C++】Virtual function and Polymorphism

ops/2025/3/19 11:16:46/

在这里插入图片描述

《C++程序设计基础教程》——刘厚泉,李政伟,二零一三年九月版,学习笔记


文章目录


更多有趣的代码示例,可参考【Programming】


1、多态性的概念

多态性(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它允许对象通过同一接口调用不同函数的行为。在 C++ 中,多态性主要通过虚函数(virtual functions)和继承(inheritance)来实现。多态性增强了程序的灵活性和可扩展性,使得代码更加通用和易于维护。

提供了同一个接口可以用多种方法调用的机制。简单概括为一个接口,多种方法

eg:函数的重载、运算符重载都是多态现象

多态性分为静态多态性(Static Polymorphism)动态多态性(Dynamic Polymorphism)

  • 静态多态性,也称为编译时多态,主要通过函数重载(Function Overloading)和模板(Templates)来实现。这种多态性在编译时就已经确定,因此称为“静态”。
  • 动态多态性,也称为运行时多态,主要通过虚函数(Virtual Functions)和继承(Inheritance)来实现。这种多态性在运行时根据对象的实际类型来决定调用哪个函数,因此称为“动态”。

在这里插入图片描述

多态性允许我们编写更通用的代码,这些代码可以与不同类型的对象一起工作,而不需要知道这些对象的具体类型。在 C++ 中,这通常通过基类指针或引用来调用派生类的重写函数来实现。

2、虚函数的定义

在C++中,虚函数(virtual function)是一种允许在派生类中重写基类方法的机制,以实现多态性。通过虚函数,程序可以在运行时根据对象的实际类型来调用相应的函数,而不是在编译时决定。动态多态性,动态联编。

2.1、引入虚函数的原因

1、实现动态多态

  • 动态绑定:虚函数通过虚函数表(vtable)实现动态绑定,使得程序可以在运行时决定调用哪个函数。这与静态绑定(在编译时决定调用哪个函数)形成对比。

  • 灵活性:动态多态性提供了更大的灵活性,允许程序根据对象的实际类型来执行不同的操作,而无需在编译时知道对象的类型。

2、支持抽象基类和接口

  • 抽象基类:虚函数允许基类定义抽象接口,即只声明函数而不提供实现。派生类必须实现这些函数,从而确保它们符合基类的接口。

  • 接口设计:通过虚函数,可以设计出清晰、简洁的接口,使得代码更加模块化和可维护。

3、提高代码的可扩展性和可维护性

  • 可扩展性:通过虚函数,可以很容易地添加新的派生类,而无需修改现有的代码。只需实现基类的虚函数即可。
  • 可维护性:虚函数使得代码更加模块化,每个类只负责自己的实现。这有助于降低代码的复杂性,提高可维护性。

4、实现回调和事件处理

  • 回调机制:虚函数可以用于实现回调机制,允许程序在特定事件发生时调用用户定义的函数。
  • 事件处理:在图形用户界面(GUI)编程中,虚函数常用于处理用户事件,如按钮点击、鼠标移动等。

5、遵循开闭原则(Open/Closed Principle)

  • 开闭原则:软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改。虚函数使得类可以通过派生来扩展功能,而无需修改现有的代码。

一般对象的指针之间没有联系,彼此独立,不能混用。但派生类是由基类派生出来,它们之间有继承关系,因此,指向基类和派生类的指针之间也有一定的联系,如果使用不当,将会出现一些问题

eg 12-1 没有使用虚函数的例子

#include <iostream>
using namespace std;class Base
{private:int x,y;public:Base(int x=0, int y=0) // 带有默认值的构造函数{this->x = x;this->y = y;}void view(){cout << "Base:" << x << " " << y << endl; }
};class SubBase: public Base
{private:int z;public:SubBase(int x, int y, int z):Base(x,y){this->z = z;}void view(){cout << "SubBase:" << z << endl; }};int main() 
{Base obj(3,4), *objp;SubBase subobj(1,2,3);objp = &obj;objp->view();objp = &subobj;objp->view();return 0;
}

output

Base:3 4
Base:1 2

在 C++ 中,如果基类的成员函数不是虚函数,那么通过基类指针或引用调用该函数时,将始终调用基类的版本,而不会调用派生类的版本。这种行为称为静态绑定。

上述代码中,Base 类的 view 方法不是虚函数,因此即使 objp 指向一个 SubBase 对象,objp->view() 也会调用 Base 类的 view 方法。

原因是调用成员函数在编译时静态联编了

为解决这个问题,就要引入虚函数的概念

改进如下

#include <iostream>
using namespace std;class Base
{private:int x,y;public:Base(int x=0, int y=0) // 带有默认值的构造函数{this->x = x;this->y = y;}virtual void view(){cout << "Base:" << x << " " << y << endl; }
};class SubBase: public Base
{private:int z;public:SubBase(int x, int y, int z):Base(x,y){this->z = z;}void view(){cout << "SubBase:" << z << endl; }};int main() 
{Base obj(3,4), *objp;SubBase subobj(1,2,3);objp = &obj;objp->view();objp = &subobj;objp->view();return 0;
}

在基类的成员函数 view 前加上 virtual 即可

output

Base:3 4
SubBase:3

补充,派生类对象指针使用时应注意的问题

(1)声明为指向基类对象的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象

(2)允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象。

(3)声明为指向基类对象的指针,当其指向它的公有派生类的对象时,只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员。要想访问其公有派生类中的成员,可将基类指针用显示类型转换方式转化为派生类指针。

2.2、虚函数的定义与使用

(1)虚函数的定义

当基类中的某个成员函数被声明为虚函数后,它就可以在派生类中被重新定义。

在派生类重新定义时,其函数原型,包括返回类型、函数名、参数个数和类型、参数的顺序都必须与基类的原型完全一致。

虚函数定义的一般形式为

virtual <函数类型> <函数名> (形参表)
{函数体
}

(2)虚函数与重载函数的关系

函数重载要求其函数的参数或参数类型必须不同,函数的返回类型也可以不同

虚函数被重新定义时,要求函数名、返回类型、参数个数、参数的类型和参数的顺序必须与基类中的虚函数原型完全相同(否则系统认为时普通的函数重载,虚函数的特性将丢失掉)

  • 绑定时间
    虚函数:动态绑定,在运行时决定调用哪个函数版本。
    重载函数:静态绑定,在编译时决定调用哪个函数版本。

  • 作用范围
    虚函数:涉及基类和派生类之间的多态性。
    重载函数:在同一作用域内,根据参数列表区分不同的函数版本。

  • 实现机制
    虚函数:通过虚函数表(vtable)实现动态绑定。
    重载函数:通过名称修饰(name mangling)和编译时类型检查实现静态绑定。

  • 多态
    虚函数:支持多态性,允许通过基类接口操作派生类对象。
    重载函数:不支持多态性,仅在同一作用域内提供多个同名函数。

  • 函数签名
    虚函数:在基类和派生类中,函数签名(包括返回类型、函数名、参数列表)必须相同(或兼容),才能实现重写。
    重载函数:在同一作用域内,函数名相同但参数列表必须不同。

(3)虚函数与重载函数的结合使用

虚函数可以重载:在基类中,虚函数可以被重载,即基类可以有多个同名虚函数,但参数列表不同

派生类中的重写:派生类可以重写基类中的虚函数,但不能直接重写重载函数中的某一个版本;它必须根据参数列表重写相应的虚函数

(4)多重继承和虚函数

一个虚函数无论被继承多少次,仍保持其虚函数的特性,与继承的次数无关。

#include <iostream>
using namespace std;class Base
{private:int x,y;public:Base(int x=0, int y=0) // 带有默认值的构造函数{this->x = x;this->y = y;}virtual void view(){cout << "Base:" << x << " " << y << endl; }
};class SubBase: public Base
{private:int z;public:SubBase(int x, int y, int z):Base(x,y){this->z = z;}void view(){cout << "SubBase:" << z << endl; }};class SubBase2: public SubBase
{private:int p;public:SubBase2(int x, int y, int z, int p):SubBase(x,y,z){this->p = p;}void view(){cout << "SubBase2:" << p << endl; }};int main() 
{Base obj(1,2), *objp;SubBase subobj(1,2,3);SubBase2 subobj2(1,2,3,4);objp = &obj;objp->view();objp = &subobj;objp->view();objp = &subobj2;objp->view();return 0;
}

output

Base:1 2
SubBase:3
SubBase2:4

2.3、虚函数的限制

(1)虚函数的声明只能出现在类声明的函数原型的声明中,不能出现在函数体实现中,同时基类中只有保护成员或公有成员才能被声明为虚函数

(2)在派生类中重新定义虚函数时,关键字 virtual 可以写也可以不写,但在容易引起混乱时,应该写上关键字

(3)动态联编只能通过成员函数来调用或通过指针、引用来访问虚函数,如果用对象名的形式来访问虚函数,将采用静态联编。

(4)虚函数必须时所在类的成员函数,不能是友元函数或静态成员函数。 但可以在另一个类中被声明为友元函数。

(5)构造函数不能声明为虚函数,析构函数可以声明为虚函数

(6)由于内联函数不能在运行中动态确定其外治,所以它不能声明为虚函数

(7)虚函数通过虚函数表(vtable)实现动态绑定,这需要在运行时查找函数地址,相较于静态绑定(编译时决定调用哪个函数),会有一定的性能开销。

3、抽象类

抽象类是带有纯虚函数的类

3.1、纯虚函数

虚函数是C++面向对象编程中的一个重要概念,主要用于定义接口和抽象类。理解纯虚函数对于掌握C++的多态性和抽象类设计至关重要。

虚函数在基类中声明的虚函数,它在基类中没有实现,要求任何派生类都必须提供自己的实现,除非派生类本身也是抽象类

一个抽象类带有至少一个纯虚函数

虚函数的一般定义形式为:

virtual <函数类型> <函数名> (参数表)=0;

虚函数与普通虚函数的定义的不同在于书写形式上加了 “=0”

#include <iostream>
using namespace std;// 抽象基类
class Shape {
public:virtual void draw() const = 0;  // 纯虚函数virtual ~Shape() {}             // 虚析构函数
};// 派生类
class Circle : public Shape {
public:void draw() const override {cout << "Drawing a Circle" << endl;}
};class Square : public Shape 
{public:void draw() const override {cout << "Drawing a Square" << endl;}
};int main() 
{Shape* shapes[2];shapes[0] = new Circle();shapes[1] = new Square();for (int i = 0; i < 2; ++i) {shapes[i]->draw();  // 调用派生类的 draw 方法}for (int i = 0; i < 2; ++i) {delete shapes[i];}return 0;
}

output

Drawing a Circle
Drawing a Square

注意事项

  • 不能实例化抽象类,当类声明中包含纯虚函数时,则不能创建该类的对象。这里,纯虚函数的类只用作基类。
  • 析构函数应为虚函数
  • 虚函数没有函数体
  • 最后的 =0 并不表示函数的返回值为 0,它只是形式上的作用,告诉编译系统这是纯虚函数
  • 是一个声明语句,最后以分号结束
  • 多重继承中的纯虚函数:在多重继承中,如果多个基类有同名的纯虚函数,派生类需要明确重写该函数。

虚函数虚函数的对比

在这里插入图片描述

3.2、抽象类

抽象类是包含至少一个纯虚函数的类,不能直接实例化。其主要作用是定义接口,强制派生类实现特定方法。

  • 虚函数:在基类中声明但没有实现的虚函数,要求派生类必须实现。
  • 无法实例化抽象类不能创建对象,只能作为基类使用。
  • 设计契约:通过纯虚函数抽象类定义了派生类必须遵循的接口。

比如抽象类为动物,可以派生出狮子、老虎、孔雀

比如设计一个游戏角色系统,不同角色类型实现特定的行为。

eg 12-4 使用抽象类访问队列与栈

注意队列是先进先出

栈是先进后出

注意:抽象类本身不能实例化,但可以定义指向抽象类对象的指针。这是因为指针只是存储对象的地址,并不需要实际创建对象。

#include <iostream>
using namespace std;class list  // 抽象类
{public:list *head, *tail, *next; // 定义头指针,尾指针和指向下一个结点的指针int num;list(){head = tail = next = nullptr; // 初始化}virtual void store(int i) = 0; // 纯虚函数,保存值virtual int retrieve() = 0;  // 纯虚函数,读取值
};class queue: public list  // 派生类
{public:void store(int i);  // 声明int retrieve();  // 声明
};void queue::store(int i) // 保存值
{list * item;item = new queue;if(!item){cout << "Allocation error\n";exit(1);}item->num = i;if(tail)tail->next = item;  // 将当前 tail 的 next 指向新节点tail = item; // 更新 tail 为新节点if(!head)head = tail;   // 如果队列为空,head 也应该指向新节点}int queue::retrieve()  // 读取值
{int i;list *p;if(!head){cout << "queue empty\n";return 0;}i = head->num;  // 先进先出p = head;            head = head->next;delete p;return i;
}class stack: public list  // 派生类
{public:void store(int i);  // 声明int retrieve();  // 声明
};void stack::store(int i)  // 保存值
{list *item;item = new stack;if (!item){cout << "Allocation error\n";exit(1);}item->num =i;if(head)item->next = head;head = item;if(!tail)tail = head;
}int stack::retrieve()  // 读取值
{int i;list *p;if(!head){cout << "stack empty\n";return 0;}i=head->num;p=head;head=head->next;delete p;return i;
}int main() 
{list *p;queue qb;p = &qb;p->store(1);p->store(2);p->store(3);cout << "Queue:";cout << p->retrieve();cout << p->retrieve();cout << p->retrieve();cout << endl;cout << p->retrieve();cout << endl;stack sb;p = &sb;p->store(1);p->store(2);p->store(3);cout << "Stack:";cout << p->retrieve();cout << p->retrieve();cout << p->retrieve();cout << endl;cout << p->retrieve();return 0;
}

output

Queue:123
queue empty
0
Stack:321
stack empty
0

例子中基类 list 是一个抽象类,其中定义了向单向列表中保存值的纯虚函数 store() 和从中读取值的纯虚函数 retrieve()

抽象类派生了两个队列类 queue 和栈类 stack,它们根据各自的需要,重新定义了纯虚函数 store()retrieve()

在这里插入图片描述

注意事项

(1)抽象类只能用作其它类的基类,不能建立抽象类的对象。但可以定义指向抽象类的指针

(2)抽象类不能用作参数类型、函数的返回类型或显示转换的类型。

(3)可以声明抽象类的指针和引用,通过它们,可以指向并访问派生类对象,从而访问派生类的成员

(4)抽象类的派生类中没有给出所有的纯虚函数的函数体,这个派生类仍是一个抽象类。若抽象类的派生类中给出了所有的纯虚函数的函数体,这个派生类不再是一个抽象类,可以声明自己的对象。

4、应用实例

eg 12-5 先建立一个 Point 类,包含 x 和 y,以它为基类,派生出一个 Circle 类,增加数据成员 r,再以 Circle 类为直接基类,派生出一个 Cylinder(圆柱体)类,再增加数据成员 h。要求编写程序,重载运算符 <<>>,使之能用于输出以上类对象。

在 C++ 中,当我们在成员函数声明的末尾添加 const 关键字时,我们是在声明该成员函数为 常量成员函数(也称为只读成员函数)。这意味着该成员函数不会修改其所属对象的任何成员变量(除非这些成员变量被声明为 mutable)。

#include <iostream>
using namespace std;
#define PI 3.14159class Point
{protected:float x,y;public:Point(float xx, float yy) // 构造函数{x = xx;y = yy;}void setPoint(float xx, float yy)  // 设置坐标值{x = xx;y = yy;}float getX() const {return x;}  // 获取 x 坐标, 只读float getY() const {return y;} // 获取 y 坐标, 只读friend ostream &operator<<(ostream &, const Point &); // 重载运算符 <<, 声明为友元函数
};ostream &operator<<(ostream &output, const Point &p)
{output << "["<<p.x<<","<<p.y<<"]" << endl;return output;
}class Circle: public Point
{protected:float r;public:Circle(float x=0, float y=0, float r=0):Point(x,y){this->r = r;}void setRadius(float rr){ r=rr;}float getRadius() const { return r; }float area() const {return (PI *r *r); }friend ostream &operator<<(ostream &, const Circle &); // 重载运算符 <<, 声明为友元函数  
};ostream &operator<<(ostream &output, const Circle &c)
{output << "Center=["<<c.x<<","<<c.y<<"], Radium=" << c.r << ", Area=" << c.area() <<endl;return output;
}class Cylinder: public Circle  // 圆柱体
{protected:float h;public:Cylinder(float x=0, float y=0, float r=0, float h=0):Circle(x,y,r){this->h = h;}void setHeight(float hh) {h = hh;}float getHeight() const {return h;}float area() const {return (2*Circle::area() + 2*PI*r*h);} // 表面积float Volumn() const {return Circle::area()*h;}friend ostream &operator<<(ostream &, const Cylinder &); // 重载运算符 <<, 声明为友元函数  
};ostream &operator<<(ostream &output, const Cylinder &cy)
{output << "Center=["<<cy.x<<","<<cy.y<<"], Radium=" << cy.r << ", Height=" << cy.h << ", Area=" << cy.area() << ", Volumn=" << cy.Volumn() << endl;return output;
}int main() 
{// PointPoint p(3.5, 6.4);cout << "x=" << p.getX() << ", y=" << p.getY() << endl;p.setPoint(8.5, 6.8); cout << "p(new):" << p;// CircleCircle c(3.5,6.4,5.2);cout << "原来的圆的数据:x=" << c.getX() << ", y=" << c.getY() << ", r=" << c.getRadius() << ", area=" << c.area() << endl;c.setRadius(7.5);c.setPoint(5,5);cout << "修改后圆的数据:x=" << c.getX() << ", y=" << c.getY() << ", r=" << c.getRadius() << ", area=" << c.area() << endl;// CylinderCylinder cy(3.5, 6.4, 5.2, 10);cout << "原来的圆柱体的数据:x=" << cy.getX() << ", y=" << cy.getY() << ", r=" << cy.getRadius() << ", h=" << cy.getHeight() << ", area=" << cy.area() << ", volumn=" << cy.Volumn() << endl;cy.setHeight(15);cy.setRadius(7.5);cy.setPoint(5,5);cout << "修改后圆柱体的数据:x=" << cy.getX() << ", y=" << cy.getY() << ", r=" << cy.getRadius() << ", h=" << cy.getHeight() << ", area=" << cy.area() << ", volumn=" << cy.Volumn() << endl;// overload// << as PointPoint &pRef1 = cy;  // pRef 是 Point 类的引用变量,被 C 初始化cout << "Point:" << pRef1; // << as CircleCircle &pRef2 = cy;cout << "Circle:" << pRef2;// << as Cylindercout << cy;return 0;
}

output

x=3.5, y=6.4
p(new):[8.5,6.8]
原来的圆的数据:x=3.5, y=6.4, r=5.2, area=84.9486
修改后圆的数据:x=5, y=5, r=7.5, area=176.714
原来的圆柱体的数据:x=3.5, y=6.4, r=5.2, h=10, area=496.623, volumn=849.486
修改后圆柱体的数据:x=5, y=5, r=7.5, h=15, area=1060.29, volumn=2650.72
Point:[5,5]
Circle:Center=[5,5], Radium=7.5, Area=176.714
Center=[5,5], Radium=7.5, Height=15, Area=1060.29, Volumn=2650.72

上面的例子是有关继承和运算符重载内容的综合应用例子,更适合放在上个章节,哈哈

【C++】Inheritance and Derivation


更多有趣的代码示例,可参考【Programming】


http://www.ppmy.cn/ops/167011.html

相关文章

下载指定版本的transformers

如果你想手动下载 transformers 库的 v4.49.0-Gemma-3 版本&#xff0c;而不是通过 pip install 命令直接安装&#xff0c;可以按照以下步骤操作。以下是详细的步骤说明&#xff1a; 步骤 1&#xff1a;访问 GitHub 仓库 打开浏览器&#xff0c;访问 Hugging Face 的 transform…

使用Flux查询数据

以下指南介绍了 Flux 的常见和复杂查询以及使用案例。 示例 data 变量 以下指南中提供的许多示例都使用 data 变量 ,它表示按度量和字段筛选数据的基本查询。 数据定义为: data = from(bucket: "example-bucket")|> range(start: -1h)|> filter(fn: (r) =…

《苍穹外卖》SpringBoot后端开发项目核心知识点与常见问题整理(DAY1 to DAY3)

目录 一、在本地部署并启动Nginx服务1. 解压Nginx压缩包2. 启动Nginx服务3. 验证Nginx是否启动成功&#xff1a; 二、导入接口文档1. 黑马程序员提供的YApi平台2. YApi Pro平台3. 推荐工具&#xff1a;Apifox 三、Swagger1. 常用注解1.1 Api与ApiModel1.2 ApiModelProperty与Ap…

Hunyuan3D,腾讯推出的3D资产系统

Hunyuan3D 2.0是腾讯推出的大规模3D 资产生成系统&#xff0c;专注于从文本和图像生成高分辦率的3D模型。系统采用两阶段生成流程&#xff1a;首先生成无纹理的几何模型&#xff0c;再合成高分辨率纹理贴图。包含两个核心组件&#xff1a;Hunyuan3D-DiT&#xff08;几何生成模型…

如何通过Python的`requests`库接入DeepSeek智能API

本文将详细介绍如何通过Python的requests库接入DeepSeek智能API&#xff0c;实现数据交互与智能对话功能。文章涵盖环境配置、API调用、参数解析、错误处理等全流程内容&#xff0c;并提供完整代码示例。 一、环境准备与API密钥获取 1. 注册DeepSeek账号 访问DeepSeek官网&am…

Maven | 站在初学者的角度配置

目录 Maven 是什么 概述 常见错误 创建错误代码示例 正确代码示例 Maven 的下载 Maven 依赖源 Maven 环境 环境变量 CMD测试 Maven 文件配置 本地仓库 远程仓库 Maven 工程创建 IDEA配置Maven IDEA Maven插件 Maven 是什么 概述 Maven是一个项目管理和构建自…

LuaJIT 学习(4)—— FFI 语义

文章目录 C Language SupportC Type Conversion RulesConversions from C types to Lua objects例子&#xff1a;访问结构体成员 Conversions from Lua objects to C typesConversions between C types例子&#xff1a;修改结构体成员 Conversions for vararg C function argum…

vlc录制的视频伪时长修复方法

问题描述 遇到个vlc录制的rtsp视频流&#xff0c;duration时长只有12分钟&#xff0c;但src duration有3个多小时&#xff08;实际正确时长&#xff09;&#xff0c;而且用potplayer能播放3个小时的完整视频&#xff0c;但vlc只能播放12分钟。 解决方法 下载ffmpeg&#xf…