【C++】多态

前言:

OOP特性,封装、继承、多态,然多态是继承之下的一个重要的特性。

多态的介绍

  • 多态是面向对象编程中的关键概念,它允许同一个接口或父类引用指向多种实际类型,并根据实际类型来调用相应的方法。
  • 多态的实现主要依赖于继承、虚函数以及动态绑定。
  • 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

多态

一、多态构成的条件

  • 基类声明虚函数。
  • 派生类重写基类的虚函数。
  • 将基类指针或引用指向派生类对象,通过基类指针或引用访问虚函数.
  • 虚函数的声明方式是在成员函数的返回值类型前添加 virtual 关键字。

简单的多态实现

cpp 复制代码
class base
{
	virtual void func()
	{
		cout << "void func()" << endl;
	}
};

class derive:public base
{
	virtual void func()
	{
		cout << "class derive:public base" << endl;
	}
};

二、虚函数

  • 虚函数是通过在类的成员函数声明前加上virtual关键字来定义的。
cpp 复制代码
class BaseClass {
public:
    virtual ReturnType FunctionName(Parameters) 
    {
        // 函数体
    }
};

纯虚拟函数

  • 可以通过在类的虚函数前面加上关键字 = 0 来声明一个纯虚拟函数。
  • 这样声明的函数没有实现,要求派生类必须提供这个函数的定义。
cpp 复制代码
class Base {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚拟函数声明
};

class Derived : public Base {
public:
    void pureVirtualFunction() override { // 派生类提供纯虚拟函数的定义
        // 函数实现...
    }
};

虚函数重写的规则

  • 函数签名一致性 :派生类重写基类虚函数时,必须保持函数签名的一致性,包括函数名、参数列表和返回类型(协变返回类型除外)。
  • 访问控制:重写的虚函数不能具有比基类函数更严格的访问控制。
  • override关键字C++11引入了override关键字,用于明确指出派生类中的函数是对基类虚函数的重写。如果没有正确重写基类虚函数,编译器将报错。
  • final关键字final关键字可以用于修饰虚函数,表示该虚函数在其派生类中不能被进一步重写,即锁定重写。
协变
  • **协变:**基类与派生类虚函数返回值类型不同
cpp 复制代码
class Person 
{
	public:
    virtual A* f() {return new A;}
};
class Student : public Person 
{
	public:
	virtual B* f() {return new B;}
};

三、重写虚构函数

  • 如果基类的析构函数不是虚函数,并且基类指针指向派生类对象时被删除。
  • 编译器将只调用基类的析构函数,忽略派生类中可能存在的额外资源。
  • 这会导致这些资源没有被正确释放,从而引起内存泄漏或其他问题。
cpp 复制代码
class Person 
{
	public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person 
{
	public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
  Person* p1 = new Person;
 Person* p2 = new Student;
 delete p1;
 delete p2;

}

**注意:**针对上面的代码运行我们创建了Student对象,但是没有调用~Student()

  • 如果基类的析构函数不是虚函数,并且基类指针指向派生类对象时被删除,编译器将只调用基类的析构函数,忽略派生类中可能存在的额外资源。
  • 这会导致这些资源没有被正确释放,从而引起内存泄漏或其他问题。
  • 为了确保多态环境下调用析构函数的正确性,基类的析构函数应该被声明为虚函数。

四、重载、覆盖(重写)、隐藏(重定义)的对比

三个概念对比 重载 1.两个函数在同一个作用域 2.两个函数同名和同参 重写 1.两个函数分别在基类和派生类 2.函数名返回值和参数均相同协变除外 隐藏 1.函数名相同 2.两个函数分别在基类和派生类

多态的底层

cpp 复制代码
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
  • 这段代码的大小在x86 大小是 8 ,在行x64下是16

  • 除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。

  • 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表

下段代码,运行结果:

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
			cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
  • 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。

  • 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法

抛出一个问题:为什么不是子类的指针和引用和父类的对象

  1. 子类的指针或引用通常用于指向子类对象,而父类对象不包含子类特有的信息
  2. 父类类的指针和引用进行切片后,切出共有的信息,但是不进行虚表的拷贝,只进行切片,循着虚表即可访问虚函数
  3. 如果是父类的对象,要实现多态就要就进行子类虚表的拷贝,如果实现了虚表的拷贝,在调用父类成员是,就会访问子类函数

这里值得反复理解

  • Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

  • 派生类的虚表生成:
    1. 先将基类中的虚表内容拷贝一份到派生类虚表中
    2. 如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

再次抛出一个问题:虚函数存在哪的?虚表存在哪的?

  1. 虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。
  2. 对象中存的不是虚表,存的是虚表指针。

深入底层

怎么确定下面的结论真实的:

  1. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  2. 对象中存的不是虚表,存的是虚表指针
cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }

	virtual void Func1() 
	{
		cout << "Person::Func1()" << endl;
	}

	virtual void Func2() 
	{
		cout << "Person::Func2()" << endl;
	}

protected:
	int _a = 0;
};

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

private:
	virtual void Func3()
	{
		cout << "Student::Func3()" << endl;
	}
protected:
	int _b = 1;
};

void test4()
{
	Person ps;
	Student st;

	int a = 0;
	printf("栈:%p\n", &a);

	static int b = 0;
	printf("静态区:%p\n", &b);

	int* p = new int;
	printf("堆:%p\n", p);

	const char* str = "hello world";
	printf("常量区:%p\n", str);

	printf("虚表1:%p\n", *((int*)&ps));
	printf("虚表2:%p\n", *((int*)&st));
}

生成结果:

  • 再此可以推断虚表是储存在常量区的。

加上这段代码

cpp 复制代码
typedef void(*FUNC_PTR) ();

void PrintVFT(FUNC_PTR* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);

		FUNC_PTR f = table[i];
		f();
	}
	printf("\n");
}
  • Person类
  • Student类

多继承

cpp 复制代码
class Base1 {
public:
 virtual void func1() {cout << "Base1::func1" << endl;}
 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 int b1;
};
class Base2 {
public:
 virtual void func1() {cout << "Base2::func1" << endl;}
 virtual void func2() {cout << "Base2::func2" << endl;}
private:
 int b2;
};
class Derive : public Base1, public Base2 {
public:
 virtual void func1() {cout << "Derive::func1" << endl;}
 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 int d1;
};
  • 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
相关推荐
爱编程— 的小李3 分钟前
结构体(c语言)
c语言·开发语言
星光樱梦15 分钟前
24. 正则表达式
c++
fathing16 分钟前
c# 调用c++ 的dll 出现找不到函数入口点
开发语言·c++·c#
前端青山37 分钟前
webpack指南
开发语言·前端·javascript·webpack·前端框架
nukix1 小时前
Mac Java 使用 tesseract 进行 ORC 识别
java·开发语言·macos·orc
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】内存可见性问题 & volatile
java·开发语言·java-ee
hope_wisdom1 小时前
C++网络编程之SSL/TLS加密通信
网络·c++·ssl·tls·加密通信
erxij1 小时前
【游戏引擎之路】登神长阶(十四)——OpenGL教程:士别三日,当刮目相看
c++·经验分享·游戏·3d·游戏引擎
Lizhihao_1 小时前
JAVA-队列
java·开发语言
林开落L2 小时前
前缀和算法习题篇(上)
c++·算法·leetcode