本专栏目的
- 更新C/C++的基础语法,包括C++的一些新特性
前言
- 通过前面几节课,我们学习了抽象、封装、继承相关的概念,接下来我们将讲解多态,多态他非常神奇,正式有了他,类才能出现多样性特征;
- C语言后面也会继续更新知识点,如内联汇编;
- 欢迎收藏 + 关注,本人将会持续更新。
文章目录
- 问题思考?
- 面向对象新需求
- 多态的意义探究
- 面向对象三大概念:
- 多态成立的三要素:(结合解决方案)
- 虚析构
- 函数的重载、重写、重定义
- 函数重载
- 函数重定义
- 虚函数重写
- 纯虚函数和抽象类
- 纯虚函数
- 抽象类与接口
- 抽象类特征
- 关键字
- abstract
- final
- 多态探究
- 多态的理论基础
- 获得虚函数表
- 多态的本质(原理)
- 虚函数简单介绍:
- 虚函数图像
- 如何证明vptr指针存在
- 如何找到vptr指针呢
问题思考?
如果子类定义了与父类中定义相同函数会发生什么?如下面代码所示:
#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。
代码实现如下:
#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;
}
输出:
x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14
多态成立的三要素:(结合解决方案)
- 要有继承:多态发生在父子类之间
- 要有虚函数重写:重写了虚函数,才能进行动态绑定
- (解决方案)
- 要有父类指针(引用)指向子类对象,传递参数的时候必须为引用或者指针,推荐常引用
虚析构
前置知识:
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
析构函数可以是虚的。通过父类指针释放所有的子类资源
虚析构:
虚析构:通过父类去释放子类的时候,如果分类没有虚析构是不会调用子类的析构函数的,会调用子类的析构函数,想要通过父类去释放子类, 必须在父类定义虚析构。
让我们来看一下,这段代码结果会是什么:
#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;
}
结果:
__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Base::~Base(void)
但是这个时候,子类的内存没有释放(_str),这样就造成了内存泄露问题😵😵😵😵😵😵
解决方法:
🔥虚析构:
- 在父类析构函数中,加上关键字
vartual
class Base
{
public:Base(){cout << __FUNCSIG__ << endl;}virtual ~Base() // 加上virtual{cout << __FUNCSIG__ << endl;}
};
结果:
__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Derive::~Derive(void)
__cdecl Base::~Base(void)
这样子类通过父类去释放,这样就能够自动识别是父类还是子类了,从而避免内存泄露🌝🌝🌝
函数的重载、重写、重定义
函数重载
- 必须在同一个作用域相同
- 子类无法重载父类的函数,父类同名函数将被名称覆盖
- 重载是在编译期间根据参数类型和个数决定函数调用
int add(int a, int b) { // 函数1return a + b;
}int add(int a, int b, int c) { // 函数2return a + b + c;
}add(2, 3); // 调用函数1
add(2, 3, 4); // 调用函数2
函数重定义
- 发生于父类和子类之间,如果子类写了个和父类函数原型一样的函数,并且父类中的函数没有声明为虚函数,则子类会直接覆盖掉父类的函数
- 但是要注意,通过父类指针或引用执行子类对象时,会调用父类的函数
子类继承父类函数,且子类直接调用父类函数:
#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;
}
结果:
I am father
I am father
但是如果这样:
// 在子类中
class Son : public Parent
{
public:void show() { // 重新写show函数cout << "I am son" << endl;}
};
结果:
I am father
I am son
📖 📖📖📖 这样通过子类调用show,就调用的是子类定义的show的。
虚函数重写
- 必须发生于父类和子类之间
- 并且父类与子类中的函数必须有完全相同的原型
- 必须使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
- 多态是在运行期间根据具体对象的类型决定函数调用
// 如这个打印面积的案例
#include <iostream>using namespace std;class Geometry
{
public:Geometry(int x, int y) : m_x(x), m_y(y) {}virtual void print_coordinates() {} // virtualvirtual 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;
}
结果:
统一调用:test(rect),test(round)的时候输出的:
x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14
纯虚函数和抽象类
纯虚函数
纯虚函数也可以叫抽象函数,一般来说它只有函数名、参数和返回值类型,不需要函数体,这意味着它没有函数的实现,需要让派生类去实现。
C++中的纯虚函数,一般在函数签名后使用=0作为此类函数的标志。Java、C#等语言中,则直接使用abstract作为关键字修饰这个函数签名,表示这是抽象函数(纯虚函数)。
简单理解:如果类里面声明了纯虚函数,那么这个类就叫做抽象类,且抽象类无法定义对象
class Animal
{
public:virtual void cry() = 0; //virtual 为虚函数标志,后面赋值 = 0,代表为这个为纯虚函数,则这个类为抽象类
}
抽象类与接口
接口:在C++里面,就是通过抽象类来实现接口的(不要在接口里面存放任何变量,一般只放虚函数)
抽象类:是对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。
- 通常在编程语句中用 abstract 修饰的类是抽象类。在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象;在java中,含有抽象方法的类称为抽象类,同样不能生成对象。
- 抽象类是不完整的,它只能用作基类。
抽象类特征
- 抽象类不能实例化
- 抽象类和包含抽象方法(纯虚函数)、非抽象方法和属性
- 从抽象类派生的非抽象类,必须对继承过来的所有抽象方法实现
关键字
abstract
MSVC独有的关键字,申明类为抽象类
class Animal abstract
{
};int main()
{Animal a; //error C3622: “Animal”: 声明为“abstract”的类不能被实例化return 0;
}
final
C++标准关键字,结束的意思
-
禁用虚函数重写
class Animal { protected:virtual void show() final{} };class Dog final :public Animal { public:void show()override //error C3248: “Animal::show”: 声明为“final”的函数无法被“Dog::show”重写{} };
-
禁止该类被继承
class Animal final { };class Dog final :public Animal //error C3246: "Dog": 无法从 "Animal" 继承,因为它已声明为 "final" { };
多态探究
参考博客: 详解
🌺 提示:多态概念很重要,但是概念同时也很容易忘记,可以先较为深入学习一下,记一下笔记,收藏一点资料,等要用到的时候再看,可以快速回忆。
多态的理论基础
静态联编和动态联编:联编是指一个程序模块、代码之间互相关联的过程。
-
静态联编(关联),是程序的匹配、连接在编译阶段实现,也称为早期匹配。
重载函数使用静态联编。
-
动态联编(关联),是指程序联编推迟到运行时进行,所以又称为动态联编(迟绑定),将函数体和函数调用关联起来,就叫绑定
switch 语句和 if 语句是动态联编的例子。
那么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;
}
- 继承虚函数
#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的存在
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
; - 这个时候就可以通过指针转化成不同定义的函数指针,转化成相应的函数调用。
步骤:
-
因为vptr指针在对象的第一个元素(通过证明vptr指针的存在可以看出),所以对对象t取地址可以拿到对象的地址
Parent* p = &obj;
-
现在拿到的指针的步长是对象的大小,因为vptr是指针,只有4/8个字节,所以需要把p强转成int*指针,这样对(int*)&t就得到了vptr指针
int vptr = *(int*)p; //拿到了vptr指针的指针 int* pvptr = (int*)vptr; //把vptr的值转成指针
-
因为vptr指针是指向的存储指针数组的首地址,所以拿到vptr指针后先把vptr转成int*指针,这样进行取值的话,刚好是每个指针
FUN foo = (FUN)*(pvptr+0) // 获取元素
-
接着吧得到的数组里面的元素(指针)转成函数指针,即可直接使用了
🤠 结果:
#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;
}
🍼 输出:
Child::func2()