写在前面:文章内容分享为主,如有不当之处,恳请批评指正。
今天在使用C++的工厂模式的时候,突然发下有些生疏,就想着发一篇博客,巩固一下,但突然想到工厂模式中设计的继承以及多态的特性,决定先发一篇有关于C++多态的文章,其他的就丢给明天吧!
首先说一下面向对象的三大特性:继承、封装、多态。其实这三种特性的思想在我们日常生活中也很常见,很多地方都有这三种思想的运用,不一定是在编程的时候。首先说一下封装,我们先看一下封装的介绍:
封装是指将数据和操作数据的方法结合在一起,形成一个独立的单元(即对象),并隐藏对象的内部细节,对外只暴露必要的接口。这种隐藏内部实现细节、保护数据不被随意访问和修改的方式,增强了代码的安全性和可维护性。
这是官方的说法,很好理解,但在日常的学习过程中,我更倾向于去想问什么这么做,然后结合自己的经历来理解。我们每天拿在手里的手机其实就是一种封装的思想,手机封装了内部的结构,你不必知道它内部用了多少晶体管,多少电阻,多少电容,也不必关心他们是串联还是并联,我们只需要按一下开机键,就可以畅通无阻的使用,这实际上就是封装的思想,隐藏了内部的细节,避免我们对电路进行不当的操作。
继承是指一个类可以基于另一个类(称为父类或基类)的特性来创建,这样新类(称为子类或派生类)可以继承父类的属性和方法,并可以在此基础上进行扩展或修改。继承允许代码的重用,并且通过继承可以实现类与类之间的层次结构。
这个我没想出来生活中的应用,但是很好理解,你可以理解为你可以继承你父亲的财产,你可以调用这些财产,你父亲定义这份财产为一个买房的方法,而你用这些钱来买彩票,这是一种继承+多态的行为,哈哈,开个玩笑,当然这么说不好,这也许是程序员的冷幽默。
多态性是指相同的操作在不同对象上可以有不同的表现形式。多态性主要体现在方法重载(同一个类中相同方法名但参数不同)和方法重写(子类可以重写父类的方法)。多态使得代码更加灵活,可以根据不同的对象执行不同的行为。
还是上面说的手机的例子,想想我们的手机,现在是不是一般都只有一个接口?这个接口你可以用来插耳机,插充电器,还可以用来传数据,为什么一个接口可以实现这么多功能,为什么我们看不到实现的原理呢?这就像我们通过虚函数实现的多态,封装在class里,我们也不知道怎么实现,我们只是调用了相应的接口,(后面会讲到),我们不必知道是怎么实现的,只需要使用,剩下的交给手机内部就好了!现在想想以前的手机有那么多接口,圆圆的插耳机的,方方的插充电线的,一定是因为不知道面向对象的多态~!(实际我也不了解哈,也有可能是因为技术的瓶颈,不能太武断,哈哈哈)。
谈完了上面的面向对象的三大特性,下面我们来说说实际的操作。
上面我们已经讲过,多态其实就是一种接口,调用不同的是实现方式。C++中的多态主要通过两种方式来实现,一种是操作符重载,在这里并不详细讲述,(我们在设计一个class的时候,应该本着要设计出和编写标准库的大师一样方便好用的class,当然这是很难很难很难的,但一个好的class,一定绕不开操作符的重载,过后我会发一篇设计Class相关的博客,来详细阐述运算符重载类型的多态)另外一种就是通过虚函数实现的多态。
首先先提问:什么是虚函数?
虚函数是在基类之中声明的一个函数,这个函数我们可以在派生类中对他进行重写。当我们生命一个基类的指针,指向一个派生类实例的时候,我们可以通过基类指针调用派生类中重写的方法,虚函数的这种特性允许程序根据对象的实际类型动态决定需要调用哪个版本的函数。如此便实现了多态。
class Base {
public:
virtual void show() {
cout << "Base class show()" << endl;
}
};
上述中的show函数,即为一个虚函数,并且基类给出了自己的定义,即输出一个字符串“Base class show()”,表示调用的为基类的show方法。
class Derived : public Base {
public:
void show() override {
cout << "Derived class show()" << endl;
}
};
接下来我们声明了一个派生类Derived,pubilc继承Base,并且对show方法进行的重写,输出一个字符串“Derived class show()”,代表调用的为派生类的show方法。
int main() {
Base* base_class_pointer;
Derived derived_class_instance;
base_class_pointer = &derived_class_instance;
// 调用的将是Derived类中的show()函数,而不是Base类中的show()函数
base_class_pointer->show(); // 输出:Derived class show()
return 0;
}
在main函数中,我们实例化了一个派生类,并初始化了一个基类类型的指针,指向这个派生类,然后通过指针调用show方法,输出的结果为Derived class show();也就是说此时调用的是派生类的方法。那么可能有些人心里会有疑惑,为什么如此顺理成章地就成功了呢?为什么我基类类型的指针可以调用派生类的方法呢?反过来为什么不行呢?
下面让我讲述一下指针的一些机制,请看下图!哈哈哈,画工比较抽象。
我们可以这么来看,首先int型占4个字节,int_pointer是一个int型的指针,那么他就会先找到存储在自己内存空间内的地址,即a元素的首地址0x0001,然后往后偏移四个字节(因为int占4字节),如此做后,读取这片内存的数据,然后按照int的读取方式,就可以得到正确的数值。那我们在类中进行读取也是如此,一般情况下,派生类占用的内存都比基类的内存大,那如果我们声明基类的指针指向派生类,也就是下面这种情况。
哈哈,这样的话就比较好理解了吧,你的钥匙只能插在你自己家的大门上,不可以插在邻居家的大门上。这也是为什么我们需要用一个基类类型的指针,指向派生类来调用方法,而反过来是不允许的。
如果你声明一个类的时候,你完全不知道该怎么设计,甚至你不知道你定义的这个方法该怎么实现,这种情况下,你可以使用纯虚函数,即告诉子类,请一定要重写这个方法,因为我根本不知道该怎么做!(就好像老爸对你说:你老爸我没开上劳斯莱斯,你继承了我,然后替我实现这个梦想吧!)声明了纯虚函数的类,一定是抽象基类,即无法被实例化的类,因为类中有没定义好的方法,怎么实例化呢?
class AbstractBase {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};
这是纯虚函数的声明,即在()后面加上=0即可;此时你就拥有了一个抽象基类。
下面让我来讲讲虚表,即C++用于支持多态的一个内部数据结构。虚表内存储了类的所有虚函数的指针,由此使得程序在运行阶段能够根据对象的实际类型调用正确的函数。虚表是与类相关联的,而不是与具体的对象(即类的实例),说到这里你可能已经想到,很像static呀,没错,虚表也是存储在程序的静态区域之中,而不是在对象的具体内存,不同的对象,共享同一类的虚表。
那么对象是怎么和远在静态区的虚表联系起来的呢?答案是虚指针,在每个对象的内存中,都包含一个虚指针,这个指针指向对象的实际类型关联的虚表,虚指针是对象的一部分,但是我们在sizeof查看一个类的大小时,往往会发现,并没有算进去虚指针的内存,这是因为虚指针是由编译器自动管理的,他是一种内部的机制。
当编译器处理一个含有虚函数的类时,他会为该类创建一个虚表。虚表之中,存储了所有虚函数的地址。这个虚表在程序加载到内存是创建,并在运行时静态存储。也就是说,还在编译器的时候,虚表就已经建立了。在后续运行期我们创建对象时,编译器会自动初始化虚指针,指向相应的虚表,来进行虚函数的调用,函数调用这个过程是在运行期实现的,是一个动态的过程,程序动态的决定实际调用哪个版本的函数,这也就是动态绑定。
到此为止,C++通过虚函数实现的多态就到这里了,如果对虚表更具体的实现感兴趣的,可以看看深度探索C++对象模型这本书,在github上就可以找到,在里面更加详细的讲解了这部分的内容。
写在后面:如有不当,恳请批评指正!!!!!