C/C++语言基础--C++神奇的多态

本专栏目的

  • 更新C/C++的基础语法,包括C++的一些新特性

前言

  • 通过前面几节课,我们学习了抽象、封装、继承相关的概念,接下来我们将讲解多态,多态他非常神奇,正式有了他,类才能出现多样性特征;
  • C语言后面也会继续更新知识点,如内联汇编;
  • 欢迎收藏 + 关注,本人将会持续更新。

文章目录

问题思考?

如果子类定义了与父类中定义相同函数会发生什么?如下面代码所示:

c++ 复制代码
#include <iostream>

using namespace std;

class Parent
{
public:
	void show() {
		cout << "I am father" << endl;
	}
};

class Son : public Parent
{
public:
	void show() {
		cout << "I am son" << endl;
	}
};

void print(Parent& p) {
	p.show();
}

int main()
{
	Parent pa;
	Son so;

	print(pa);
	print(so);  // 子赋值给父亲,可以当作父亲用

	return 0;
}

输出:

I am father
I am father

但是,如何在传入不同对象的时候输出相应的数据呢? 这个就是我们接下来要学的多态。

面向对象新需求

上面的这一种场景,需要C++需要做的事情是:

  • print函数中,传递什么对象调用什么对象的show函数,传递父类的,就调用父类的,传递子类的,就调用子类的。

解决方案:虚函数

  • 在父类中,在能让子类重写的函数前面必须 加上virtual关键字
  • 在子类中,在重写的父类的虚函数后面 加上override关键字,表示是虚函数重写(非必须,但是加上可以防止重写的虚函数写错)

虚函数重写概念

派生类(父类)中有一个跟基类(子类)完全相同的函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同)

多态的意义探究

面向对象三大概念:

封装:提取事物的属性与方法

继承:代码复用------可以用父类的代码

多态:在代码复用基础上,实现不同功能

案例:打印矩形和圆形坐标和面积

矩形:x,y,length,width

圆形:x,y,radius

在这个案例中,我们可以划分:

  • 封装:将矩形和圆形共有属性 抽象出来,这里是x,y坐标,同时将共有方法 抽象出来,这里是打印坐标和面积,将这些封装成一个基类A
  • 继承:分别定义矩形、圆形类,继承基类A,同时定义属于自己的属性或者方法,这里是矩形中定义属性length,width,圆形定义radius;
  • 多态:基类中定义了方法(打印坐标和面积),在圆形和矩形中分别重写这两个方法;
  • 测试:利用子类可以赋值给父类的特征,实现传入什么类就输出什么类对应的API。

代码实现如下:

c++ 复制代码
#include <iostream>

using namespace std;

class Geometry
{
public:
	Geometry(int x, int y) : m_x(x), m_y(y) {}

	virtual void print_coordinates() {}

	virtual void print_area() {}

	int m_x;			// 测试:整形
	int m_y;
};

class Rectangle : public Geometry
{
public:
	Rectangle(int x, int y, int width, int length)
		: Geometry(x, y),
		m_width(width),
		m_length(length) {

	}

	// 重写
	void print_coordinates() override
	{
		std::cout << "x: " << m_x << " y: " << m_y << std::endl;
	}

	void print_area() override
	{
		std::cout << "Rectangle area: " << m_width * m_length << std::endl;
	}

	int m_width;
	int m_length;
};

class Round : public Geometry
{
public:
	Round(int x, int y, int riduas)
		: Geometry(x, y),
		m_riduas(riduas)
	{

	}

	// 重写
	void print_coordinates() override
	{
		std::cout << "x: " << m_x << " y: " << m_y << std::endl;
	}

	void print_area() override
	{
		std::cout << "Round area: " <<  3.14 * m_riduas * m_riduas << std::endl;
	}

	int m_riduas;
};

void test(Geometry& various)
{
	various.print_coordinates();
	various.print_area();
}

int main()
{
	Rectangle rect(1, 2, 3, 4);
	Round round(5, 6, 1);

	test(rect);
	test(round);

	return 0;
}

输出:

txt 复制代码
x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14

多态成立的三要素:(结合解决方案)

  1. 要有继承:多态发生在父子类之间
  2. 要有虚函数重写:重写了虚函数,才能进行动态绑定
    1. (解决方案)
  3. 要有父类指针(引用)指向子类对象 ,传递参数的时候必须为引用或者指针,推荐常引用

虚析构

前置知识:

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数

析构函数可以是虚的。通过父类指针释放所有的子类资源

虚析构:

虚析构 :通过父类去释放子类的时候,如果分类没有虚析构不会 调用子类的析构函数的,会调用子类的析构函数,想要通过父类去释放子类, 必须在父类定义虚析构。


让我们来看一下,这段代码结果会是什么:

cpp 复制代码
#include <iostream>

using namespace std;

class Base
{
public:
	Base()
	{
		cout << __FUNCSIG__ << endl;
	}
	~Base()
	{
		cout << __FUNCSIG__ << endl;
	}
};

class Derive : public Base
{
private:
	char* _str;
public:
	Derive()
	{
		_str = new char[10] { "wy" };
		cout << __FUNCSIG__ << endl;
	}
	~Derive()
	{
		delete _str;
		cout << __FUNCSIG__ << endl;
	}
};


int main()
{
	Base* base = new Derive;       
	delete base;			
	return 0;
}

结果:

txt 复制代码
__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Base::~Base(void)

但是这个时候,子类的内存没有释放(_str),这样就造成了内存泄露问题😵😵😵😵😵😵


解决方法:

🔥虚析构:

  • 在父类析构函数中,加上关键字vartual
c++ 复制代码
class Base
{
public:
	Base()
	{
		cout << __FUNCSIG__ << endl;
	}
	virtual ~Base()   // 加上virtual
	{
		cout << __FUNCSIG__ << endl;
	}
};

结果:

txt 复制代码
__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Derive::~Derive(void)
__cdecl Base::~Base(void)

这样子类通过父类去释放,这样就能够自动识别是父类还是子类了,从而避免内存泄露🌝🌝🌝

函数的重载、重写、重定义

函数重载

  • 必须在同一个作用域相同
  • 子类无法重载父类的函数,父类同名函数将被名称覆盖
  • 重载是在编译期间根据参数类型和个数决定函数调用
c++ 复制代码
int add(int a, int b) {  // 函数1
    return a + b;
}

int add(int a, int b, int c) {   // 函数2
    return a + b + c;
}

add(2, 3);  // 调用函数1
add(2, 3, 4);   // 调用函数2

函数重定义

  • 发生于父类和子类之间,如果子类写了个和父类函数原型一样的函数,并且父类中的函数没有声明为虚函数,则子类会直接覆盖掉父类的函数
  • 但是要注意,通过父类指针或引用执行子类对象时,会调用父类的函数

子类继承父类函数,且子类直接调用父类函数:

c++ 复制代码
#include <iostream>

using namespace std;

class Parent
{
public:
	void show() {
		cout << "I am father" << endl;
	}
};

class Son : public Parent
{
public:

};

int main()
{
	Parent pa;
	Son so;

	pa.show();
	so.show();


	return 0;
}

结果:

txt 复制代码
I am father
I am father

但是如果这样:

c++ 复制代码
// 在子类中
class Son : public Parent
{
public:
	void show() {   // 重新写show函数
		cout << "I am son" << endl;
	}
};

结果:

txt 复制代码
I am father
I am son

📖 📖📖📖 ​ ​ 这样通过子类调用show,就调用的是子类定义的show的。

虚函数重写

  • 必须发生于父类和子类之间
  • 并且父类与子类中的函数必须有完全相同的原型
  • 必须使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
  • 多态是在运行期间根据具体对象的类型决定函数调用
c++ 复制代码
// 如这个打印面积的案例
#include <iostream>

using namespace std;

class Geometry
{
public:
	Geometry(int x, int y) : m_x(x), m_y(y) {}

	virtual void print_coordinates() {}   // virtual

	virtual void print_area() {}

	int m_x;			// 测试:整形
	int m_y;
};

class Rectangle : public Geometry
{
public:
	Rectangle(int x, int y, int width, int length)
		: Geometry(x, y),
		m_width(width),
		m_length(length) {

	}

	// 重写
	void print_coordinates() override
	{
		std::cout << "x: " << m_x << " y: " << m_y << std::endl;
	}

	void print_area() override
	{
		std::cout << "Rectangle area: " << m_width * m_length << std::endl;
	}

	int m_width;
	int m_length;
};

class Round : public Geometry
{
public:
	Round(int x, int y, int riduas)
		: Geometry(x, y),
		m_riduas(riduas)
	{

	}

	// 重写
	void print_coordinates() override
	{
		std::cout << "x: " << m_x << " y: " << m_y << std::endl;
	}

	void print_area() override
	{
		std::cout << "Round area: " <<  3.14 * m_riduas * m_riduas << std::endl;
	}

	int m_riduas;
};

void test(Geometry& various)
{
	various.print_coordinates();
	various.print_area();
}

int main()
{
	Rectangle rect(1, 2, 3, 4);
	Round round(5, 6, 1);

	test(rect);
	test(round);

	return 0;
}

结果:

txt 复制代码
统一调用:test(rect),test(round)的时候输出的:
x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14

纯虚函数和抽象类

纯虚函数

纯虚函数也可以叫抽象函数 ,一般来说它只有函数名、参数和返回值类型,不需要函数体,这意味着它没有函数的实现,需要让派生类去实现

C++中的纯虚函数,一般在函数签名后使用=0作为此类函数的标志。Java、C#等语言中,则直接使用abstract作为关键字修饰这个函数签名,表示这是抽象函数(纯虚函数)。

简单理解 :如果类里面声明了纯虚函数,那么这个类就叫做抽象类,且抽象类无法定义对象

cpp 复制代码
class Animal
{
public:
    virtual void cry() = 0;     //virtual 为虚函数标志,后面赋值 = 0,代表为这个为纯虚函数,则这个类为抽象类
}

抽象类与接口

接口 :在C++里面,就是通过抽象类来实现接口的(不要在接口里面存放任何变量,一般只放虚函数

抽象类:是对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。

  • 通常在编程语句中用 abstract 修饰的类是抽象类。在C++中,含有纯虚拟函数的类称为抽象类 ,它不能生成对象;在java中,含有抽象方法的类称为抽象类,同样不能生成对象。
  • 抽象类是不完整的,它只能用作基类。
抽象类特征
  1. 抽象类不能实例化
  2. 抽象类和包含抽象方法(纯虚函数)、非抽象方法和属性
  3. 从抽象类派生的非抽象类,必须对继承过来的所有抽象方法实现

关键字

abstract

MSVC独有的关键字,申明类为抽象类

cpp 复制代码
class  Animal abstract
{
};


int main()
{
	Animal a;	//error C3622: "Animal": 声明为"abstract"的类不能被实例化
	return 0;
}
final

C++标准关键字,结束的意思

  • 禁用虚函数重写

    cpp 复制代码
    class  Animal 
    {
    protected:
    	virtual void show() final
    	{
    
    	}
    };
    
    class Dog final :public Animal
    {
    public:
    	void show()override	//error C3248: "Animal::show": 声明为"final"的函数无法被"Dog::show"重写
    	{
    
    	}
    };
  • 禁止该类被继承

    cpp 复制代码
    class  Animal  final
    {
    };
    
    class Dog final :public Animal //error C3246: "Dog": 无法从 "Animal" 继承,因为它已声明为 "final"
    {
    };

多态探究

参考博客: 详解

🌺 提示:多态概念很重要,但是概念同时也很容易忘记,可以先较为深入学习一下,记一下笔记,收藏一点资料,等要用到的时候再看,可以快速回忆。

多态的理论基础

静态联编和动态联编:联编是指一个程序模块、代码之间互相关联的过程。

  • 静态联编(关联),是程序的匹配、连接在编译阶段实现,也称为早期匹配。
    重载函数使用静态联编。

  • 动态联编(关联),是指程序联编推迟到运行时进行,所以又称为动态联编(迟绑定),将函数体和函数调用关联起来,就叫绑定
    switch 语句和 if 语句是动态联编的例子。

    那么C++中的动态联编是如何实现的呢?

    如果我们声明了类中的成员函数为虚函数,那么C++编译器会为类生成一个虚函数表,通过这个表即可实现动态联编

获得虚函数表

c++ 复制代码
#include<iostream>
//1、得到虚函数
//2、验证不同兄弟类虚函数都是一样的
class A
{
public:
	virtual void a()
	{
		std::cout << __FUNCSIG__ << std::endl;
	}
	virtual void b()
	{
		std::cout << __FUNCSIG__ << std::endl;
	}
	virtual void c()
	{
		std::cout << __FUNCSIG__ << std::endl;
	}
private:
	int x;
	int y;
};

typedef void(*func)();  //使用函数指针,强制转换成函数

int main()
{
	A a, b;
	uint64_t* p = (uint64_t*)&a;
	uint64_t* arr = (uint64_t*)*p;
	func fa = (func)arr[0];
	func fb = (func)arr[1];
	func fc = (func)arr[2];
	fa();
	fb();
	fc();
	uint64_t* pp = (uint64_t*)&b;
	uint64_t* arr2 = (uint64_t*)*pp;
	std::cout << arr << " " << arr2 << std::endl;
	return 0;
}
  • 继承虚函数
c++ 复制代码
#include<iostream>
/*
* 1、父类虚函数和子类虚函数
* 2、兄弟虚函数
* 3、继承虚函数
*/

//继承虚函数表和重写
class A
{
public:
	virtual void a()
	{
		std::cout << __FUNCSIG__ << std::endl;
	}
	virtual void b()
	{
		std::cout << __FUNCSIG__ << std::endl;
	}
	virtual void c()
	{
		std::cout << __FUNCSIG__ << std::endl;
	}
private:
	int x;
	int y;
};

class B : public A
{
public:
	void b() override
	{ 
		std::cout << __FUNCSIG__ << std::endl; 
	} 
};

typedef void(*func)();  //使用函数指针,强制转换成函数

int main()
{
	A a;
	B b;
	uint64_t* pa = (uint64_t*)&a;
	uint64_t* arra = (uint64_t*)*pa;
	uint64_t* pb = (uint64_t*)&b;
	uint64_t* arrb = (uint64_t*)*pb;

	func faa = (func)arra[0];
	func fab = (func)arra[1];
	func fac = (func)arra[2];

	func fba = (func)arrb[0];
	func fbb = (func)arrb[1];
	func fbc = (func)arrb[2];

	faa();
	fab(); 
	fac();
	fba(); 
	fbb(); 
	fbc();

	return 0;
}

ss

多态的本质(原理)

虚函数表是顺序存放虚函数地址的,虚表是顺序表(数组),依次存放着类里面的虚函数。

虚函数表是由编译器自动生成与维护的,相同类的不同对象的虚函数表是一样的

既然虚函数表,是一个顺序表,那么它的首地址存放在哪里呢?

  • 当我们在类中定义了virtual函数时,C++编译器会偷偷的给对象添加一个vptr指针,vptr指针就是存的虚函数表的首地址

虚函数简单介绍:

  • 虚函数表存放了类的虚函数(就是一个函数指针数组)
  • 虚函数的指针分布初始化:创建子类对象的时候,会先构造父类,构造父类的时候,父类的虚函数指针,指向自己的虚函数(父类构造完后会构造子类,这个时候父类的虚函数指针,会指向类的虚函数标)
  • 在构造函数里面禁止使用虚函数(因为分布初始化还没有完成,可能得到不正确的结果)

虚函数图像

  • 三层

如何证明vptr指针存在

我们可以通过求出类的大小判断是否有vptr的存在

cpp 复制代码
class Dog
{
	void show() {}
};

class Cat
{
	virtual void show() {}
};

int main()
{
	cout << "Dog size:" << sizeof(Dog) << " Cat size:" << sizeof(Cat) << endl;

	return 0;
}
output: Dog size:1 Cat size:8

通过调试确实能看到vptr指针的存在,而且存放在对象的第一个元素

如何找到vptr指针呢

既然vptr指针存在,那么能不能拿到vptr指针,手动来调用函数呢?

答案是可以的,利用它存在对象的第一个元素特征,但是操作起起来很麻烦,以下过程也是我收集资料学习到的。

思路:核心(存放在第一个对象元素)

  • 首先定义一个子类,拿取子类地址;
  • 接着将子类地址转化成long long*类型,再次解引用,这样就告诉编译器,这个指向子类指针,没有子类约束,并且这个类型是long long类型了 ,这个时候就拿到了对象第一个元素,很绕,但是没办法;
  • 再接着,将这个类型重新转化为指针long long
  • 这个时候就可以通过指针转化成不同定义的函数指针,转化成相应的函数调用。

步骤:

  1. 因为vptr指针在对象的第一个元素(通过证明vptr指针的存在可以看出),所以对对象t取地址可以拿到对象的地址

    cpp 复制代码
    Parent* p = &obj;
  2. 现在拿到的指针的步长是对象的大小,因为vptr是指针,只有4/8个字节,所以需要把p强转成int*指针,这样对(int*)&t就得到了vptr指针

    cpp 复制代码
    int vptr = *(int*)p;	//拿到了vptr指针的指针
    int* pvptr = (int*)vptr; //把vptr的值转成指针
  3. 因为vptr指针是指向的存储指针数组的首地址,所以拿到vptr指针后先把vptr转成int*指针,这样进行取值的话,刚好是每个指针

    cpp 复制代码
    FUN foo = (FUN)*(pvptr+0)  // 获取元素
  4. 接着吧得到的数组里面的元素(指针)转成函数指针,即可直接使用了

🤠 结果:

cpp 复制代码
#include <iostream>

using namespace std;

using FUN = void(*)();   // (*) 代表是一个指针,指向一个void类型函数

class Parent
{
public:
	virtual void func1()
	{
		cout << "Parent::func1()" << endl;
	}

	virtual void func2()
	{
		cout << "Parent::func2()" << endl;
	}
};

class Child : public Parent
{
public:
	void func1() override
	{
		cout << "Child::func1()" << endl;
	}

	void func2() override
	{
		cout << "Child::func2()" << endl;
	}
};

int main()
{
	Child obj;

	Parent* p = &obj;

	long long vptr = *(long long*)p;
	long long* pvptr = (long long*)vptr;
	auto foo = (FUN) * (pvptr + 1);
	foo();
	
	return 0;
}

🍼 输出:

txt 复制代码
Child::func2()
相关推荐
小白要加油哈7 分钟前
Lua--1.基础知识
开发语言·junit·lua
萧萧玉树12 分钟前
分布式在线评测系统
前端·c++·后端·负载均衡
bbppooi19 分钟前
堆的实现(完全注释版本)
c语言·数据结构·算法·排序算法
网络安全Ash20 分钟前
企业网络安全之OPENVPN
开发语言·网络·php
xcLeigh22 分钟前
C# Winform贪吃蛇小游戏源码
开发语言·c#
易辰君26 分钟前
【Python爬虫实战】深入解析 Scrapy:从阻塞与非阻塞到高效爬取的实战指南
开发语言·python
FFDUST26 分钟前
C++ 优先算法 —— 无重复字符的最长子串(滑动窗口)
c语言·c++·算法·leetcode
荒-漠27 分钟前
php CURL请求502
开发语言·php
shiming887927 分钟前
C/C++链接数据库(MySQL)超级详细指南
c语言·数据库·c++
前端白袍28 分钟前
C语言:C语言实现对MySQL数据库表增删改查功能
c语言·数据库·mysql