首页 > 编程语言 >C++多态

C++多态

时间:2024-01-30 11:32:14浏览次数:17  
标签:函数 void 基类 多态 C++ 派生类 重写

多态的概念

多态(Polymorphism)是面向对象编程中的一个重要概念,它允许同一类型的对象在不同的上下文中表现出不同的行为。多态性有两种主要形式:编译时多态(静态多态性)和运行时多态(动态多态性)。

C++多态_多态

C++多态_多态_02

编译时多态可以看成是函数重载和运算符重载,之前的文章已经涉及过,不再赘述;

所以,下面所提到的多态,都指的是运行时多态。

多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。

Person对象买票全价,Student对象买票半价。

由此,在继承中构成多态需要两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

对于第一点,是实现多态的重要前提,即通过相同的函数名,在不同类中有着不同的响应,下面以代码实例演示:

#include <iostream>
class Base{
public:
    virtual void someFunction(){
        std::cout << "Base::someFunction()" << std::endl;
    }
};

class Derived : public Base{
public:
    void someFunction() override{
        std::cout << "Derived::someFunction()" << std::endl;
    }
};
void Func1(){
    Base b1;
    Derived d1;
    b1.someFunction();
    d1.someFunction();
    Base* ptr1 = new Derived();
    ptr1->someFunction();
    Base* ptr2 = new Base();
    ptr2->someFunction();
    delete ptr1;
    delete ptr2;
}

int main(void){
    Func1();
    return 0;
}

结果如下:

C++多态_虚函数_03

对于第18、19行,虽然调用了不同类的成员函数,但是并不符合多态的要求,而接下来的代码演示了多态的具体实现,通俗来说,就是不管你基类指针指向什么对象,但凡要调用虚函数时,必须使用基类的指针或引用,否则就不是多态。

虚函数

什么是虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

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

这里的BuyTicket函数就是一个虚函数。

虚函数的重写(覆盖)

虚函数的重写(覆盖):

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

注意,这里重写的要求极为苛刻,函数名、返回值类型、参数列表均要求相同,才符合虚函数重写的要求。

前文已经演示了虚函数的重写代码,此处不再赘述;但注意,基类声明了虚函数,派生类重写时无需再次声明,尽管依然构成重写,但是不规范,不建议使用。

因为基类的虚函数继承下来,在派生类中依然保持着虚函数属性,所以无需再次声明。

虚函数重写时的两个例外:


  1. 协变(基类与派生类虚函数返回值类型不同)

从派生类指针到基类指针的转换称为协变。

协变的概念扩展到函数的返回类型,即如果一个函数的返回类型是某个类的指针或引用,那么它可以在子类中被重写为该子类的指针或引用。

#include <iostream>
class Base{
public:
    virtual Base* func1(){
        return this;
    }
};
class Derived : public Base{
public:
    Derived* func1() override {
        return this;
    }
};

如上,派生类重写时发生了协变,改变了基类虚函数的返回值;但请注意,协变不是一个强制要求,你当然可以按照基类虚函数的返回值进行重写,不进行协变操作。


  1. 析构函数的重写(基类和派生类的析构函数名不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的 析构函数构成重写,虽然基类与派生类析构函数名字不同。

虽然函数名不相同,看起来违背了重写的规 则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

代码演示如下:

#include <iostream>
class Person{
public:
    Person(){
        std::cout << "Person()" << std::endl;
    }
    virtual ~Person(){
        std::cout << "~Person()" << std::endl;
    }
};

class Student : public Person{
public:
    Student(){
        std::cout << "Student()" << std::endl;
    }
    ~Student() override {
        std::cout << "~Student()" << std::endl;
    }
};

void Func1(){
    Person* ptr1 = new Person();
    Person* ptr2 = new Student();

    delete ptr1;
    delete ptr2; 
}

int main(void){
    Func1();
    
    return 0;
}

结果如下:

C++多态_派生类_04

可以看见,调用ptr2的析构函数时发生了多态,其使用的是派生类的析构函数而非子类,可见派生类的析构函数对基类的析构函数进行了重写。

关键字override、final

override

用于检查派生类的虚函数是否对基类虚函数进行了重写,如果没有发生,报出编译错误。

下面简单演示一下用法:

class Base{
public:
    virtual void show(){
        std::cout << "Hello World!" << std::endl;
    }
};
class Derived : public Base{
public:
    void show(){
        std::cout << "Hello CPlusPlus World!" << std::endl;
    }
};

final

用于修饰虚函数,表示该虚函数不会再被重写:

class Base{
public:
    virtual void show() final{
        std::cout << "Hello World!" << std::endl;
    }
};

class Derived : public Base{
public:
    // 编译出错,基类的虚函数已经被限制重写
    void show(){
        std::cout << "Hello CPlusPlus World!" << std::endl;
    }
};

这里基类的虚函数重写已经被限制,所以当派生类对该函数进行重写时,编译出错。

小结

重载、重写(覆盖)、重定义(隐藏)概念辨析

C++多态_多态_05

抽象类

抽象类的概念及定义

抽象类(也称为接口类),即包含纯虚函数的类;

纯虚函数,在虚函数后写上=0,即构成了纯虚函数。

抽象类无法实例化出对象,且如果继承后的派生类未对纯虚函数进行重写,派生类依然无法实例化出对象,所以抽象类更像是一个接口的性质,故也称之为接口类。

class Person {
public:
	virtual void show() = 0;
};

class Student :public Person {
public:
    void show() {
        std::cout << "Hello C++ World!" << std::endl;
    }
};

void Func1() {
    // Person p1;
    Student s1;
}

注意,第14行想要去实例化一个抽象类,编译出错;

当我们在派生类中重写了纯虚函数,派生类就可以去实例化一个s1对象了。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现;

虚函数的 继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口;

所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

虚函数表

这里我们先以一个简单的基类为例,看看它的组成结构:

#include <iostream>
class Base {
public:
	virtual void show() {
		std::cout << "Hello World!" << std::endl;
	}
private:
	int _base = 0;
};
void Func1() {
	Base b1;
}
int main(void) {
	Func1();
	return 0;
}

我们调用监视窗口:

C++多态_虚函数_06

可以看到,除了成员变量_base外,还有一个指针类型_vfptr,而这个我们称之为虚函数表指针(virtual function pointer);

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,那么派生类中这个表放了些什么呢?

为了验证,我改写了实验用例:

#include <iostream>
class Base {
public:
	virtual void Func1() {
		std::cout << "Base::Func1()" << std::endl;
	}
	virtual void Func2() {
		std::cout << "Base::Func2()" << std::endl;
	}
private:
	int _base = 0;
};
class Derived :public Base {
public:
	void Func1() {
		std::cout << "Derived::Func1()" << std::endl;
	}
	virtual void Func3() {
		std::cout << "Derived::Func3()" << std::endl;
	}
	void Func4() {
		std::cout << "Derived::Func4()" << std::endl;
	}
private:
	int _derived = 0;
};
void test1() {
	Base b1;
	Derived d1;
}

int main(void) {
	test1();
	return 0;
}

监视窗口结果:

C++多态_多态_07

代码中,基类的两个虚函数,在派生类中我只是对Func1进行了重写,观察它们的值,发现Func1在派生类和基类中的地址是不一样的,而没有实现重写的Func2地址与基类相同;

所以重写在原理层面就是将基类的虚函数覆盖了,当调用派生类对象的函数时,会直接调用覆盖后的函数,从而实现多态;而重写是语法层面的叫法,和原理没有什么关系;

派生类的虚函数和继承下来的基类的虚函数一样,放在同一张虚函数表里,如果是多继承语法,则放在第一个基类的虚表里(按照声明的顺序排序)。

单继承和多继承的虚函数表

单继承中的虚函数表

这里我们采用之前的代码样例:

#include <iostream>
class Base {
public:
	virtual void Func1() {
		std::cout << "Base::Func1()" << std::endl;
	}
	virtual void Func2() {
		std::cout << "Base::Func2()" << std::endl;
	}
private:
	int _base = 0;
};
class Derived :public Base {
public:
	void Func1() {
		std::cout << "Derived::Func1()" << std::endl;
	}
	virtual void Func3() {
		std::cout << "Derived::Func3()" << std::endl;
	}
	void Func4() {
		std::cout << "Derived::Func4()" << std::endl;
	}
private:
	int _derived = 0;
};
void test1() {
	Base b1;
	Derived d1;
}

int main(void) {
	test1();
	return 0;
}

为了验证上面的说法,还要写一个验证程序打印出对象虚表的结构,因为对于监视窗口,我们无法看到完整的虚表结构,所以要通过一些手段:

using VFPTR = void(*)();

void Print_VTable(VFPTR vTable[]) {
	std::cout << "虚表地址->" << vTable << std::endl;
	for (int i = 0; vTable[i] != nullptr; ++i) {
		std::cout << "第" << i << "个虚函数地址:" << vTable[i];
		VFPTR f = vTable[i];
		f();
	}
	std::cout << std::endl;
}

int main(void) {
	Base b;
	Derived d;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	Print_VTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	Print_VTable(vTabled);
	return 0;
}

这里的代码设计较为复杂,我将详细解释一下这样设计的原理:

首先是函数指针的声明,采用了C++11的新特性,使用了using,类似于typedef,所以这行代码也可以写成:

typedef void(*VFPTR)();

第一个括号表示是一个函数指针类型,其中VEPTR表示它的别名,第二个括号表示参数列表,void表示返回类型;

紧接着是打印函数的实现,其中参数列表表示一个由类似VFPTR函数组成的函数指针数组,随后遍历函数数组,打印其地址并对其进行调用,会执行相应的函数操作;

接下来的问题就是解决如何找到虚表的地址:

正如我们之前代码演示的那样,可以发现虚表一般都放在对象的前4个字节(因为指针是4个字节),而虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr,所以我们打印函数中参数是一个函数指针数组,它表示的其实就是一个虚表;

有了以上的思路,我们先是要获得对象的前4个字节:(int*)&b;

先对对象取地址,随后强转成(int*)类型,这里有人可能会疑惑:为什么一定要转成(int*),其他的指针不也是4个字节吗,这是因为我们还要对该内容进行解引用,以获取前4个字节,所以要转成(int*)类型;

((int)&b),而一旦解引用,表示所指向的内容就是一个虚表指针,指向虚函数表,接下来为了适应函数的参数列表,我们要对它进行再次的类型强转,(VFPTR*)((int*)&b),这样就满足了函数的参数类型要求;

下面就是实现的完整代码:

VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);

VFPTR* vTabled = (VFPTR*)(*(int*)&d);
 PrintVTable(vTabled);

通过这一套完整的打印验证程序,最终就能验证说法的真伪;

#include <iostream>
class Base {
public:
	virtual void Func1() {
		std::cout << "Base::Func1()" << std::endl;
	}
	virtual void Func2() {
		std::cout << "Base::Func2()" << std::endl;
	}
private:
	int _base = 0;
};
class Derived :public Base {
public:
	void Func1() {
		std::cout << "Derived::Func1()" << std::endl;
	}
	virtual void Func3() {
		std::cout << "Derived::Func3()" << std::endl;
	}
	void Func4() {
		std::cout << "Derived::Func4()" << std::endl;
	}
private:
	int _derived = 0;
};

using VFPTR = void(*)();

void Print_VTable(VFPTR vTable[]) {
	std::cout << "虚表地址->" << vTable << std::endl;
	for (int i = 0; vTable[i] != nullptr; ++i) {
		std::cout << "第" << i << "个虚函数地址:" << vTable[i];
		VFPTR f = vTable[i];
		f();
	}
	std::cout << std::endl;
}

int main(void) {
	Base b;
	Derived d;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);  
	Print_VTable(vTableb);

	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	Print_VTable(vTabled);

	return 0;
}

结果如下(如果出现打印失败的情况,只需要清理一下解决方案即可)(因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题):

C++多态_虚函数_08

可以看到监视窗口看不到的完整虚函数表,可以发现其内部是否重写的具体情况;

多继承中的虚函数表

多继承的打印方法和单继承类似,原理都是通过对象的头4个字节找到对应的虚表地址,随后进行打印;

但是多继承不同于单继承,多继承对象不止有一个虚表,而且在之前的监视窗口中可以看到两个虚表地址相差的位置取决于继承的对象:

C++多态_虚函数_09

可以看到Base1和Base2相差4个字节,而这正是sizeof Base1的大小,所以我们要改变打印的具体实现:

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	std::cout << " 虚表地址>" << vTable << std::endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		std::cout << "第" << i << "个虚函数地址:" << vTable[i];
		VFPTR f = vTable[i];
		f();
	}
	std::cout << std::endl;
}


int main()
{
	Derived d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

打印函数不变,传入对象进行改变;

为什么第二十二行要强转成(char*)类型?这是因为字节相加,如果是int类型,会以4个字节相加减,导致未知错误,所以要强转成char类型;

结果如下:

C++多态_多态_10


标签:函数,void,基类,多态,C++,派生类,重写
From: https://blog.51cto.com/u_16271511/9481410

相关文章

  • C++实现直接插入排序、冒泡排序、快速排序、选择排序(含调试程序)
    #include<iostream>#include<fstream>#include<string>#include<vector>#include<algorithm>usingnamespace::std;classSolution{public: //直接插入排序 voidinsertsort(vector<int>&num){ for(inti=1;i<num......
  • C++ Qt开发:运用QJSON模块解析数据
    Qt是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍如何运用QJson组件的实现对JSON文本的灵活解析功能。JSON(JavaScriptObjectNotation)是一种轻量级......
  • 有关UE5在VisualStudio升级后产生C++无法编译的问题及处理方案
    哈喽大家好,我是咕噜美乐蒂,很高兴又见面啦!最近,许多使用UE5的游戏开发者遇到了一个问题:在VisualStudio升级后,他们的C++代码无法编译。这个问题可能是由于UE5工程和VS之间的版本不兼容导致的。本文将深入探讨这个问题的原因以及如何解决它。一、问题的产生原因UE5是一款基于C++的游戏......
  • 面相对象之多态和鸭子类型
    面相对象之多态和鸭子类型多态指的是一类事物有多种形态一、多态动态绑定(多态性)多态动态绑定在继承的背景下使用时,有时也称为多态性多态性是指在不考虑实例类型的情况下使用实例在面向对象方法中一般是这样表述多态性:向不同的对象发送同一条消息不同的对象在接收......
  • QT Creator12.0.1运行普通C/C++程序时候没有控制台输出
    问题:QTCreator12.0.1运行普通C/C++程序时候没有控制台输出菜单栏选择:[编辑]->[设置],按下图依次设置。启用终端输出,还有去掉内部终端输出的选项运行后控制台窗口正常弹出......
  • Qt/C++音视频开发64-共享解码线程/重复利用解码/极低CPU占用/画面同步/进度同步
    一、前言共享解码线程主要是为了降低CPU占用,重复利用解码,毕竟在一个监控系统中,很可能打开了同一个地址,需要在多个不同的窗口中播放,形成多屏渲染的效果,做到真正的完全的画面同步,在主解码线程中切换了播放进度,所有关联的同一个解码线程的播放窗体也会立即同步画面,使得感官上看起来......
  • C++类模板
    1.类模板作用:建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表语法:template<typenameT>类解释:template-声明创造模板typename-表面其后面的符号是一种数据类型,可以用class代替T-通用的数据类型,名称可以替换,通常为大写字母二.类模板和函数模......
  • C/C++中的宏
    目录"##"预处理运算符"##"预处理运算符在C++中,##是一个预处理运算符,被称为“连接符”或“连接运算符”。当它在宏定义中出现时,它将其左右两边的标记(token)连接成一个标记。这在创建宏时特别有用,尤其是当你需要合并两个标记来形成一个新的、有效的标记时。以下是一些使用##的......
  • C++多线程 第一章 你好,C++并发世界
    第一章你好,C++并发世界C++并发并发(concurrency):主要包括任务切换与硬件并发两类.并发(concurrency)实际上与多线程(multithreading)存在差异.并发的种类任务切换(taskswitching):计算机在某一时刻只可以真正执行一个任务,但它可以每秒切换任务许多次.通过做一......
  • Windows Server 2012 R2 安装 Visual C++ Redistributable (VC_redist.x64) 失败 0x80
    PHP8需要 VisualC++RedistributableforVisualStudio2019,但怎么都装不上,有个0x80240017-未指定的错误。 看日志 Windows8.1-KB2999226-x64.msu好像有补丁安装失败了,网上找到一篇解决办法:https://blog.51cto.com/u_12701820/3032471能成功安装VC,但是PHP8无法......