前言
经过前几章c++基础语法的铺垫后,我们后面可以研究一些稍微深入的知识了。c++的三大特性-封装、继承、多态。这三大特性在我的理解更多的是表明C++中OOP(面向对象编程)思想的一个总结,其中封装性代表的是一个思想,即:应暴露尽可能少的实现细节给外部。其中涉及到的点有访问限定符、提供读写数据接口而非直接提供数据等,具体的细节本文不再展开。而继承和多态二者是相辅相成的,也就是多态依赖于继承,而继承的一大亮点也是可以实现多态。下面我们就继承和多态来引入c++最核心的知识点~
什么是多态
多态,从语义上理解就是多种形态,在c++中它代表的就是调用一个函数能够产生多种形态。分为静态多态和动态多态,其中静态多态就是重载,函数名相同,参数个数或者类型不同,这个很容易理解,不多说,我们主要说一下动态多态。
动态多态
动态多态是基于c++的RTTI机制结合c++中的继承和虚函数所表现出来的一种强大特性,他的释义是:将函数绑定推迟至运行期,根据实际对象的类型来确定调用哪种版本的函数。听起来很抽象是吧,我尝试用我的理解来解释一下:某一天村长(编译器)里要集中盖房子,每家都必须出人,老黄家也必须出人,问到老黄家的时候,回复说:那天不知道谁空,等到了那天再说(运行期),我家谁有空就谁去,然后村长就记着:老黄家不确定派谁干活,等到那天再说(多态)。例子举得不太恰当,意思就是告诉编译器这个函数(虚函数)你先不要编译,等到运行期间的话,再根据调用对象的实际类型来调用我。
如何使用多态
多态 = 继承+虚函数,继承就不多说了,我们直接上代码
class Base
{
public:
virtual void function()
{
std::cout << "base createproduct" << std::endl;
}
};
class Deived:public Base
{
public:
void function()
{
std::cout << "deived createproduct" << std::endl;
}
};
int main()
{
Base * base = nullptr;
base = new Base();
base->function();
delete base;
base = new Deived();
base->function();
delete base;
}
结果如下:
我们来分析一下,我首先定义了一个指针置为nullptr(C++11出的空指针),然后将指针指向了一个base对象的内存空间,调用function,然后将指针指向了一个deived对象的内存空间,再次调用function函数。其中我的调用方式是一模一样的,区别是不是只在于base这个指针实际指向内存地址的不同。而这就是动态多态。
注意点:
1.只有虚函数才有多态性,普通函数没有;
2.只有有继承关系的类才会根据实际对象类型而呈现多态;
3.虚函数的声明需要加上virtrual标志符,定义不需要加,子类的虚函数重写也可以不加;
多态的重要性
一定要学习多态吗?我一直搞不懂多态是干嘛的,能不使用多态就可以写代码吗?答案是肯定的,不用多态你是个码农,但是你肯定写不出好的代码,用了多态,你就踏上了进阶之路,有了多态的基础你才能理解很多设计模式(工厂模式、观察者模式等等)和优秀框架设计,所以多态非常重要。
多态的实现机制
为啥继承+虚函数就可以实现多态了呢?没有为啥,c++就是这样规定的,既然用了人家的语言,就遵守人家的语法。但是我们可以尝试稍微深入看一下c++是如何实现多态的。
“我知道很多道理,却依旧过不好这一生”这句话也适用于我们的学习之路,我们在很多的博客中见过太多介绍多态实现的文章了,说的都很好,但是学习的最好方法就是实践,对于多态的实现机制我们先介绍其理论,再写代码验证之~
首先多态的实现是由于编译器在编译期间为函数分配入口地址,但是有一个例外,那就是虚函数,如果编译器检测到这个函数是虚函数的话,那么就不会为这个函数分配地址,会将这个过程推迟到运行期,换句话说,也还是我们上面那句话,这个虚函数的地址会在运行到调用的时候,才会临时分配地址。
而编译器是如何在运行期间找到对应版本的虚函数地址的呢?bingo,都知道,每个对象的内存在初始化的时候,如果类中有虚函数的话,那么他会维护一个虚函数表指针,这个虚函数表里面存放了虚函数的地址。
咱们写段代码验证一下这个东西
#pragma once
#include <iostream>
class base
{
public:
int privateValue;
public:
void Func()
{
std::cout << "非虚函数base :: func 被调用" << std::endl;
}
virtual void virFunc()
{
std::cout << "虚函数base :: virFunc被调用" << std::endl;
}
};
然后子类deived重写virFunc函数
#pragma once
#include <iostream>
class deived :public base
{
public:
mutable int a;
public:
void virFunc()
{
std::cout << "虚函数deived :: virFunc被调用" << std::endl;
}
};
#include <stdio.h>
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>
#include "deived.h"
using namespace std;
int main()
{
base* ptr = nullptr;
ptr = new base();
delete ptr;
ptr = new deived();
delete ptr;
}
很简单的代码,我们设断点看一下两个对象内存里面到底存放了什么东西
没错,base构造出来的内存中,我们看到在内存的首位就维护了一个虚表指针,确实指向了一个虚函数地址,而在deived中同样也维护了一个虚表指针,而它指向的确实自己重写过的虚函数的地址。
那么我们得出以下结论:
1.如果一个类中存在虚函数,那么它所构造的内存空间中首部就会维护一个虚表指针,指向一个虚函数表,这个虚函数表存放的是虚函数的地址;
2.如果子类重写了父类的虚函数,那么虚表中存放的地址是子类版本的虚函数地址,而非父类;
那么问题又来了,那既然子类重写了父类的虚函数,那么虚表中存放的地址是子类版本的虚函数地址,那Base *ptr = new deived();中ptr是不是无法访问父类版本的虚函数了呢?
答案是可以访问,但是需要强行加上作用域符ptr->base::virFunc();即可访问,我擦,这是为啥,那是因为编译器如果直接遇到这种用法的话,会直接访问虚函数的地址,而不再通过虚表指针了~
小结
1.多态,从语义上理解就是多种形态,在c++中它代表的就是调用一个函数能够产生多种形态。分为静态多态和动态多态,其中静态多态就是重载,函数名相同,参数个数或者类型不同,动态多态就是通过父类指针指向父类或者子类对象内存,相同调用代码能够呈现出不同的表现形式。
2.有了多态的基础才能理解很多设计模式(工厂模式、观察者模式等等)和优秀框架设计,所以多态非常重要。
3.多态的实现机制就是通过虚表指针来实现的,多态性就是通过父类指针指向不同的对象内存地址(父类或者子类),而不同的对象内存中会维护不同的虚表指针,指向不同的虚表,从而能够呈现出多态性。
好了,我是Double beans,一个希望通过简单代码能够说明一件事的c++程序员,学海无涯,我们后面再见。