本专栏目的
- 更新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
多态成立的三要素:(结合解决方案)
- 要有继承:多态发生在父子类之间
- 要有虚函数重写:重写了虚函数,才能进行动态绑定
- (解决方案)
- 要有父类指针(引用)指向子类对象 ,传递参数的时候必须为引用或者指针,推荐常引用
虚析构
前置知识:
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
析构函数可以是虚的。通过父类指针释放所有的子类资源
虚析构:
虚析构 :通过父类去释放子类的时候,如果分类没有虚析构 是不会 调用子类的析构函数的,会调用子类的析构函数,想要通过父类去释放子类, 必须在父类定义虚析构。
让我们来看一下,这段代码结果会是什么:
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中,含有抽象方法的类称为抽象类,同样不能生成对象。
- 抽象类是不完整的,它只能用作基类。
抽象类特征
- 抽象类不能实例化
- 抽象类和包含抽象方法(纯虚函数)、非抽象方法和属性
- 从抽象类派生的非抽象类,必须对继承过来的所有抽象方法实现
关键字
abstract
MSVC独有的关键字,申明类为抽象类
cpp
class Animal abstract
{
};
int main()
{
Animal a; //error C3622: "Animal": 声明为"abstract"的类不能被实例化
return 0;
}
final
C++标准关键字,结束的意思
-
禁用虚函数重写
cppclass Animal { protected: virtual void show() final { } }; class Dog final :public Animal { public: void show()override //error C3248: "Animal::show": 声明为"final"的函数无法被"Dog::show"重写 { } };
-
禁止该类被继承
cppclass 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
; - 这个时候就可以通过指针转化成不同定义的函数指针,转化成相应的函数调用。
步骤:
-
因为vptr指针在对象的第一个元素(通过证明vptr指针的存在可以看出),所以对对象t取地址可以拿到对象的地址
cppParent* p = &obj;
-
现在拿到的指针的步长是对象的大小,因为vptr是指针,只有4/8个字节,所以需要把p强转成int*指针,这样对(int*)&t就得到了vptr指针
cppint vptr = *(int*)p; //拿到了vptr指针的指针 int* pvptr = (int*)vptr; //把vptr的值转成指针
-
因为vptr指针是指向的存储指针数组的首地址,所以拿到vptr指针后先把vptr转成int*指针,这样进行取值的话,刚好是每个指针
cppFUN foo = (FUN)*(pvptr+0) // 获取元素
-
接着吧得到的数组里面的元素(指针)转成函数指针,即可直接使用了
🤠 结果:
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()