首页 > 编程语言 >C++:多态中的虚/纯虚函数,抽象类以及虚函数表

C++:多态中的虚/纯虚函数,抽象类以及虚函数表

时间:2024-11-06 17:50:57浏览次数:3  
标签:函数 派生类 多态 C++ 基类 重写 指针

我们在平时,旅游或者是坐高铁或火车的时候。对学生票,军人票,普通票这些概念多少都有些许耳闻。而我们上篇文章也介绍过了继承与多继承。如果这些票我们都分别的去写一个类,当然很冗余,这里我们便可以去使用继承,我们假设我们的票价是由一个票价函数控制的,如果子类与父类中有着同名的票价函数,我们之前也介绍过他会隐藏,那我们要如何去实现使用不同的子类达到不同的效果呢--答案就是多态。

一,多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点介绍运行时多态,编译时多态(静态多态)和运行时多态(动态多态)编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。 

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。 其实就是我们上面的买票行为,不同的人买票对应的价格也不同。

 

二,构成多态的前提与两个重要条件

构成多态的前提是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。 而实现多态则必须具备以下两个重要条件:

  1. 必须指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数。

需要注意的是,引用的指针必须为父类指针。而且被调用的虚函数必须要在父类中也为虚函数,这样才能被子类重写覆盖:
 

class parent
{
public:
	parent(int a = 1)
		:_a(a)
	{}
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

class child
{
public:
	child(int b = 2)
		:_b(b)
	{}                  //这是一个虚函数,但父类中对应完全相同函数没有virtual前缀,
	virtual void print()//所以没有构成重写,也就不会形成多态
	{
		cout << _b << endl;
	}

private:
	int _b;
};

而我们如果想要实现多态,一个是在父类的完全相同函数(返回值,函数名,参数完全相同) 前加上virtual前缀,另一点则需要用父类指针去调用子类对象的对应虚函数,此时才能形成多态:

int main()
{
	child c;
	parent& p1 = c;
	p1.print();//构成多态
	parent* p2 = &c;
	p2->print();//构成多态
	parent p3 = c;
	p3.print();//不构成多态
	return 0;
}

从运行结果我们可以清晰的看到必须重写和使用父类指针两个条件同时满足才能实现多态。 

三,虚函数与虚函数的重写与覆盖 

3.1虚函数的定义方式

class parent
{
public:
	virtual void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

 类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修
饰。(比如类中的静态成员函数, 所有子类公用与父类相同的静态成员,也正是因为静态成员函数无法变为虚函数,因此静态成员函数无法形成多态)。

3.2虚函数的重写/覆盖 

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

但是我们平时可能会看到如下的情况:

class parent
{
public:
	parent(int a = 1)
		:_a(a)
	{}
	virtual void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

class child : public parent
{
public:
	child(int a = 1,int b = 2)
		:_b(b)
		,parent(a)
	{}
	void print()
	{
		cout << _b << endl;
	}

private:
	int _b;
};

此时子类的完全相同函数虽然没有加上virtual前缀,但实际上也构成了重写,不过这种写法并不规范。也正是这种原因,它经常会被作为面试/笔试的考题出现。 

3.3虚函数中的协变 

 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解一下即可。


private:
	int _b;
};

class A {};
class B : public A {};
class Person {
public:
	virtual A* BuyTicket()
	{                              //
		cout << "买票-全价" << endl;//
		return nullptr;            //这就是一种协变,返回值不同,
	}                              //但返回的对象互为父子类,所以
};                                 //也会构成重写。
class Student : public Person {    //
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

注意,返回的不一定必须是当前类的父子类指针/引用,也可以是其他父子类指针/引用。只要返回的对像构成父子类关系以及同为指针/引用即可。

3.4override关键字与final关键字 

 override关键字可以帮助我们检查虚函数是否构成覆写,而final关键字则可以使虚函数无法被覆写:

// error C3668: “Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类方法
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
// error C3248: “Car::Drive”: 声明为“final”的函数无法被“Benz::Drive”重写
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};

3.5总结:重载/重写/隐藏的对比

 

四,纯虚函数与抽象类 

在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。 

class parent//抽象类
{
public:
	parent(int a = 1)
		:_a(a)
	{}
	virtual void print() = 0;//纯虚函数
private:
	int _a;
};

class child : public parent
{
public:
	child(int a = 1,int b = 2)
		:_b(b)
		,parent(a)
	{}
	void print()
	{
		cout << _b << endl;
	}

private:
	int _b;
};

五,多态的原理 

5.1虚函数表

当我们创建了一个类时,它所占用的实际大小是虚表与成员变量所占空间之和:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};

比如上面这个类,它的一个实例化对像实际大小为12Byte。其中八个字节存放两个成员变量,另四个字节用来存放虚表(__vfptr)放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

 

5.1.1虚函数表的相关概念与知识 

  1. 基类对象的虚函数表中存放基类所有虚函数的地址。
  2. 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基
  3. 类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
  4. 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
  5. 派生类的虚函数表中包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。
  6. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)
  7. 虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
  8. 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,vs下是存在代码段(常量区) 

5.2多态实现原理 

我们拿上面的parent与child类来说明:

class parent
{
public:
	parent(int a = 1)
		:_a(a)
	{}
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

class child
{
public:
	child(int b = 2)
		:_b(b)
	{}                  //这是一个虚函数,但父类中对应完全相同函数没有virtual前缀,
	virtual void print()//所以没有构成重写,也就不会形成多态
	{
		cout << _b << endl;
	}

private:
	int _b;
};

 

通过上图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

所以我们在使用父类指针去调用子类对象中构成重写的虚函数时,实际上它并不是到父类中去调用父类完全相同虚函数再对其重写,而是通过虚表在运行时确定要调用的虚函数,所以最终调用的虚函数是由调用指针指向的对像决定的而不是由指针的类型决定。

5.2.1动态绑定与静态绑定 

对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数
的地址
,也就做动态绑定。 

5.3虚函数表的一些其他注意点 

如果我们有以下一段代码:

 

class parent
{
public:
	parent(int a = 1)
		:_a(a)
	{}
	virtual void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

class child
{
public:
	child(int b = 2)
		:_b(b)
	{}                  
	virtual void print()
	{
		cout << _b << endl;
	}
	virtual void print1()
	{}

private:
	int _b;
};

print1虚函数是否会存在与虚函数表中?答案是存在的,我们在vs下会看到以下情景:

 

虽然print1没有构成重写,但它依然存放与虚函数表中。但有时我们会在vs下遇到看不到print1函数在虚表中的场景,这时我们可以使用内存窗口查看,便可以看到以下情况:

即可证明print1函数存放于虚表中。

 

 

 

 

 

标签:函数,派生类,多态,C++,基类,重写,指针
From: https://blog.csdn.net/xiuwoaiailixiya/article/details/143439434

相关文章

  • QT creator 中c和c++混编问题
    今天在编译包含.c和.cpp的QT项目,在整合各种代码的时候,碰到一些问题,为了方便后查,初步总结如下:1.新版QT中一般使用g++编译cpp文件和c文件,可以在项目中同时使用c文件和cpp文件。后缀cpp文件g++自动识别为c++代码自动进行c++编译,后缀c文件自动识别为c代码进行c编译。这个时候必须注意......
  • 南沙C++信奥赛陈老师解一本通题 1225:金银岛
    ​ 【题目描述】某天KID利用飞行器飞到了一个金银岛上,上面有许多珍贵的金属,KID虽然更喜欢各种宝石的艺术品,可是也不拒绝这样珍贵的金属。但是他只带着一个口袋,口袋至多只能装重量为w的物品。岛上金属有ss个种类,每种金属重量不同,分别为n1,n2,...,nsn1,n2,...,ns,同时每个种类......
  • 鸿蒙开发进阶(HarmonyOS )FileUri开发指南(C/C++)
     鸿蒙NEXT开发实战往期必看文章:一分钟了解”纯血版!鸿蒙HarmonyOSNext应用开发!“非常详细的”鸿蒙HarmonyOSNext应用开发学习路线!(从零基础入门到精通)HarmonyOSNEXT应用开发案例实践总结合(持续更新......)HarmonyOSNEXT应用开发性能优化实践总结(持续更新......)场景介......
  • C++工厂模式全解析:从简单工厂到抽象工厂的进阶之路
    在软件设计中,工厂模式(FactoryPattern)是一类创建型设计模式,用于将对象的创建过程和使用过程解耦。这种设计模式在面向对象编程中非常常见,特别是在构建复杂系统时,工厂模式可以使代码更加灵活、模块化、易于扩展。工厂模式的主要类型包括:简单工厂模式(SimpleFactory)工厂方法模......
  • C++中的各种锁p8
    在多线程开发中,经常会遇到数据同步,很多情况下用锁都是一个很好的选择。C++中常用的锁主要有下面几种:互斥锁(std::mutex)这是最基本的一种锁。它用于保护共享资源,在任意时刻,最多只有一个线程可以获取该锁,从而访问被保护的资源。当一个线程获取了互斥锁后,其他试图获取该锁的线程会......
  • Python 继承、多态、封装、抽象
    面向对象编程(OOP)是Python中的一种重要编程范式,它通过类和对象来组织代码。OOP的四个核心概念是继承(Inheritance)、多态(Polymorphism)、封装(Encapsulation)和数据抽象(DataAbstraction)。下面将详细介绍这四个概念。继承(Inheritance)继承是面向对象编程(OOP)的一个基本概念,它允......
  • 【C++】踏上C++学习之旅(五):auto、范围for以及nullptr的精彩时刻(C++11)
    文章目录前言1.auto关键字(C++11)1.1为什么要有auto关键字1.2auto关键字的使用方式1.3auto的使用细则1.4auto不能推导的场景2.基于范围的for循环(C++11)2.1范围for的语法2.2范围for的使用条件3.指针空值nullptr(C++11)3.1为什么会有nullptr这个关键字?前言本......
  • C语言之输出函数printf以及puts
    printf和puts都是c语言的库函数,都可以输出的函数但他们也存在着一定的区别printf函数:1.功能强大:printf是一个格式化输出的函数,它可以输出各种类型的数据,并且能够按照指定的格式进行输出,例如会以10进制整数输出10。可以同时输入多组数据,灵活的控制输出的格式,如控制整数的......
  • Java函数式编程基础之【Lambda表达式】疑难问题析解
    一、Lambda表达式概述Lambda表达式是Java8引入的一个重要特性,它是函数式编程的基础。Lambda表达式本质上是一种匿名函数(AnonymousFunction),是代码块。Lambda表达式允许将函数作为方法的参数或者将代码块作为数据进行传递。匿名内部类和Lambda表达式匿名内部类和Lambda......
  • 【C/C++】野指针概念以及避免方式
    C语言中的野指针详解野指针(WildPointer)是指向未定义或非法内存位置的指针。本博客讲解野指针的概念、产生原因、危害以及如何避免野指针的问题。1.什么是野指针野指针指的是未初始化或已经失效的指针变量。这些指针指向的内存位置不再有效,可能被系统回收或被其他变量使......