C++多态性原理详解(静态多态、动态多态、虚函数、虚函数表)

简介: C++多态性原理详解(静态多态、动态多态、虚函数、虚函数表)

C++多态性原理详解(静态多态、动态多态、虚函数、虚函数表)

先给出定义:多态是同一个行为具有多个不同表现形式或形态的能力。

1 联编

联编也称绑定,是指在一个源程序经过编译链接成为可执行文件的过程中,将可执行代码“缝合”在一起的步骤。其中在程序运行前就完成的称为静态联编(前期联编);在程序运行时完成的称为动态联编(后期联编)。

静态联编支持的多态性称为编译时多态(静态多态),通过函数重载或函数模板实现;动态联编支持的多态性称为运行时多态(动态多态),通过虚函数表实现。

2 静态多态性

2.1 函数重载

首先考察代码工程中的一种情形:

void Swap1(int* a, int* b);
void Swap2(float* a, float* b);
void Swap3(char* a, char* b);
void Swap4(double* a, double* b);

当用到几个实现功能相同,但细节不同的函数时,C语言中会如上例所示,用不同的函数名进行区分,但这样不仅影响美观,也不便于调用和代码管理。

于是在C++中引入了函数重载:重载允许在同一作用域中声明几个类似的同名函数,这些同名函数的形参列表(参数个数,类型,顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。

上例在C++中可以改写为:

void Swap(int* a, int* b);
void Swap(float* a, float* b);
void Swap(char* a, char* b);
void Swap(double* a, double* b);

在C++中不仅函数可以重载,运算符也可以重载。可以将运算符理解为一种函数名,例如a+=1可以视作:a.+=(1),其中a是对象,+=是其属性。既然运算符可以这样审视,那么自然可以进行重载。操作符重载示例如下,其中operator为声明操作符的关键字。

void operator +=(int number);

2.2 函数模板

考察2.1节中的函数重载。若重载函数仅有接口数据类型的不同,函数体、函数名都完全一样,那么可以使用函数模板进一步简化代码。

首先给出函数模板的格式:

template <typename 类型参数1 , typename 类型参数2 , ...> 返回值类型  函数名(形参列表){
函数体
}

例如2.1节中的swap重载函数可用一个函数模板代替为:

template <typename T>void swap(T* a, T* b)
{
  T temp = *a;
  *a = *b;
  *b = temp;
}

其中template、typename均为声明函数模板用的关键字。用上面一个函数模板即可对任意合法的输入数据类型进行处理。

需要注意的是,目前编译里不支持模板函数的声明和实现分离,因此一般将模板函数体也写在头文件中。

3 动态多态性

动态多态允许用一个或多个派生类对象的属性配置父类对象。在多态性的支持下,父类对象的某个接口会随着派生类对象的不同而执行不同的操作。

3.1 实现原理

先考虑下面的代码工程。

class Father
{
public:
  virtual void func1(){
    std::cout << "FUNC1:Father" << std::endl;
  }
  virtual void func2(){
    std::cout << "FUNC2:Father" << std::endl;
  }
  void func3(){
    std::cout << "FUNC3:Father" << std::endl;
  }
};
class Son :public Father
{
public:
  virtual void func1(){
    std::cout << "FUNC1:Son" << std::endl;
  }
  void func3(){
    std::cout << "FUNC3:Son" << std::endl;
  }
};
void testFunc(Father* ptr)
{
  ptr->func1();
  ptr->func3();
}
int main() {
  Son* testSon = new Son; 
  Father* testFather = new Father;
  std::cout << "==============子类测试=============\n";
  testFunc(testSon);
  std::cout << "\n==============父类测试=============\n";
  testFunc(testFather);
}

其执行结果为:

==============子类测试=============
FUNC1:Son
FUNC3:Father
==============父类测试=============
FUNC1:Father
FUNC3:Father

首先阐明要通过子类属性配置父类的原因。如图1所示的例子,移动硬盘、U盘、SD卡都是从父类存储设备派生出来的子类,在实际应用中,只需设计一个存储外设接口,通过判断具体使用的是哪一种外设来确定该接口将执行的驱动程序。这种面向接口的设计思路可以增强复用性和模块化,否则一种外设就需要对应一种接口,过于冗杂。


image.png

下面通过分析代码结果,来说明多态的实现原理。


函数testFunc()希望展现出多态性,子类实例testSon在执行func1()时体现子类特性,但在执行func3()时却仍保留了父类特性——即不显示出多态,这是因为func1()被关键字virtual所修饰。关键字virtual申请使用后期联编,被其修饰的函数称为虚函数,而在后期联编下才支持动态多态,因此只有func1()显示出多态性。


至此,先小结构成动态多态的条件:(a) 调用函数的对象必须是指针或者引用;(b) 被调用的函数必须是虚函数。


后期联编通过虚函数表V-Table实现动态多态,虚函数表是一个实例的虚函数地址表。若某个实例存在虚函数,则该实例的内存中会自动分配虚函数表,指明该实例实际应该调用的函数。实例中通过虚函数指针,指向虚函数表所在的内存位置。


image.png

image.png

image.png

image.png

图2展示了各种类型的多态情况,实例化时将更新虚函数表——完成继承和覆盖,运行程序过程中,编译器将查找虚表,从而链接到该实例实际应该执行的函数。


示例代码为图2(b)的类型,图3所示是示例代码的变量监视区,可见父类和派生类虚函数表不同,派生类若覆盖虚函数则会对虚表进行更新(func1),父类中不被覆盖的虚函数(func2)仍被子类继承下来。


image.png

3.2 纯虚函数

通过令虚函数为0可以表示纯虚函数,纯虚函数只定义了函数接口,包含纯虚函数的类称为抽象类。


抽象类定义了一个类可能发出的动作的原型,但既没有实现,也没有任何状态信息。引入抽象类的原因在于,很多情况下基类本身实例化不合情理。例如动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身实例化没有意义,这时就可将动物类定义成抽象类。

class Animal
{
public:
  virtual void eat() = 0;
  virtual void run() = 0;
  virtual ~Animal() = default;
};

由于抽象类只提供原型而无法被实例化,因此派生类必须提供接口的具体实现,否则亦无法被实例化。

目录
相关文章
|
1月前
|
存储 人工智能 编译器
c++--多态
上一篇文章已经介绍了c++的继承,那么这篇文章将会介绍多态。看完多态的概念,你一定会感觉脑子雾蒙蒙的,那么我们先以举一个例子,来给这朦胧大致勾勒出一个画面,在此之前,先介绍一个名词虚函数,(要注意与虚拟继承区分)重定义: 重定义(隐藏)只要求函数名相同(但要符合重载的要求,其实两者实际上就是重载);重定义下:在这种情况下,如果通过父类指针或引用调用函数,会调用父类的函数而不是子类。重定义(或称为隐藏)发生的原因是因为函数名相同但参数列表不同,导致编译器无法确定调用哪一个版本的函数。
45 0
|
5月前
|
编译器 C++
c++中的多态
c++中的多态
|
4月前
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
502 0
|
5月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
257 0
|
6月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
113 4
|
6月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
129 3
|
5月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
1月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
45 0
|
1月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
112 0
|
3月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
114 12