面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。
封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。
继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
接下来我们就来了解学习多态!
多态是面向对象技术(OOP)的核心思想,我们把具有继承关系的多个类型称为多态类型,通俗来讲:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子:就拿刚刚结束的五一假期买票热为例,买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。同样一个行为在不同的对象上就有不同的显现。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票->全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票->半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
//同一个函数对不同对象有不同效果
Func(p);
Func(s);
return 0;
}
比如Student继承了Person。Person对象买票全价,Student对象买票半价。我们运行看看:
乍一看还挺复杂,接下来我们就来了解多态的构成。
继承的情况下才有虚函数,才有多态!!!
多态构成的条件:
看起来很是简单,当时其实有很多的坑!!!一不小心就会掉进去。
上面我们说了多态的条件:父子虚函数要求三同。但是却有这样一个特殊情况:协变! 协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同:
这样的情况称为协变。
#include<iostream>
using namespace std;
class A {};
class B : public A {};
//这里明显返回类型不同但是结构仍然正常
class Person
{
public:
virtual A* BuyTicket() { cout << "买票->全价" << endl; return nullptr; }
};
class Student : public Person
{
public:
virtual B* BuyTicket() { cout << "买票->半价" << endl; return nullptr; }
};
很明显派生类与基类的返回值不同(注意一定是:基类返回“基类”,派生类返回“派生类”): 但是结果确实正常的,依然构成多态,这样的情况就称为协变!!!
析构函数在编译阶段都会转换成:destructor()
,所以表面析构函数名字不同,但是实质上是一致的。这样就会构成多态。
来看正常情况下的析构:
#include<iostream>
using namespace std;
class Person
{
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person p;
Student s;
return 0;
}
这样会正常的调用析构函数(子类析构会自动调用父类析构->先子后父):
再来看:
int main()
{
//Person p;
//Student s;
//基类可以指向基类 也可以指向派生类的基类部分
Person* p1 = new Person ;
//通过切片来指向对应内容
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
如果是这样呢?
这样调用的析构不对啊!Student对象没有调用自身的析构函数,而是调用Person的,为什么会出现这样的现象呢???
这样就可能会引起一个十分严重的问题:内存泄漏
#include<iostream>
using namespace std;
class Person
{
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
Student() { int* a = new int[100000000]; }
~Student() { cout << "~Student()" << endl; }
};
int main()
{
for(int i = 0; i< 100000 ; i++)
{
Person* p2 = new Student;
delete p2;
}
return 0;
}
如果我们在Student中申请一个空间,而析构的时候却不能调用其析构函数俩把申请的空间free这样就导致了内存泄漏!!!
这就十分危险了!!! 而我们希望的是指向谁就调用谁的析构:指向基类调用基类析构,指向派生类调用派生类析构。 那我们怎么做到呢 ----> 当然就是多态了!!! 那我们来看看现在满不满足多态的条件:
在编译的时候,析构函数都会变成destructor
,这样满足三同!构成重写
那么我们就只需要将析构函数变为虚函数就可以了:
class Person
{
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
来运行看看:
老铁 OK了!!!应该释放的空间全都释放了!!!
所以建议析构函数设置为虚函数,避免出现上述的情况。
virtual
(这个语法点非常奇怪!建议写上virtual
)来看一道面试题: 以下程序输出结果是什么()
#include<iostream>
using namespace std;
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
答案是:B
为什么呢?
A* this
成员函数的默认参数),满足多态条件。B->
1
所以就可以判断是B选项。 当然实际中不能这么写代码奥!!!会有生命危险(Doge)
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
class car final { };
virtual void func() final { }
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
我们来区分一下这三个类似的概念:
重定义包含重写!!!
首先我们来看一下具有多态属性的类的大小:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char _ch = 'x';
};
int main(int argc, char* argv[])
{
cout << sizeof(Base) << endl;
return 0;
}
Base的大小在x86环境下是12字节。这十二字节是怎么组成的呢?
首先类里面有一个虚函数表指针_vfptr
:
只要有虚函数就会有虚表指针,这个是实现多态的关键!!! 我们来探索一下: 通过VS的调试,我们可以发现:
那么如何实现传基类调用基类的虚函数,传派生类调用派生类的虚函数? 当然是使用切片了!
1. 首先每个实例化的类(如果有虚函数)会有一个虚函数表。 2. 传基类调用基类的虚函数,就正常在基类虚表中寻找其对应函数 3. 传派生类,因为多态函数时基类的指针,那么就会切片出来一个基类(虚函数表是派生类的),那么就会在派生类虚表调用对应虚函数。
这样就实现了执行谁就调用谁!!! 运行过程中去虚表中找对应的虚函数调用。具体的汇编语言实现还是比较直白的。
注意同类型的虚表是一样的!!!
这里需要分辨一下两个概念:虚表与虚基表
注意:虚函数不是存在虚表中的 , 虚表中存的是虚函数的指针。那虚函数存在哪里呢? 来验证一下:
class Person
{
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Person p;
Student s;
Person* p3 = &p;
Student* p4 = &s;
printf("Person虚表地址:%p\n", *(int*)p3);
printf("Student虚表地址:%p\n", *(int*)p4);
return 0;
}
运行可以看到:
虚表地址与常量区最接近,那可以推断出虚表储存在常量区!!!
我们来看:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
梳理一下结构:
然后我们来探索一下:
int main()
{
Base b;
Derive d;
return 0;
}
通过监视窗口可以查看一下虚表的内容:
这是VS调试的一点BUG,导致监视中派生类的虚表不能显示。在内存窗口里存在4个函数指针,接下来我们来验证一下他们是不是对应的虚函数。
虚函数表本质是一个函数指针数组! 那么如何定义一个函数指针和函数指针数组呢?
//这样定义
//返回值是void 所以写void
void(*p)( //函数里面的参数 );
void(*p[10])( //函数里面的参数 )
当然可以使用typedef
来简化(这个typedef也很特别)
typedef void(*VFPTR)();
VFPTR p1;
VFPTR p2[10];
那么如果我们想要打印出虚表,我们可以设置一个函数:
//因为是函数指针数组,所以传参是函数指针的指针(int arr[10] 传入 int*)。
void PrintVFT(VFPTR* vft )
{
for(size_t i = 0 ; i < 4 ; i++)
{
printf("%p\n" , vft[i]);
}
}
这样就可以打印了,那么现在就需要解决如何获取虚表的首地址。虚表首地址是类的头4个字节(x86环境),我们如何取出来了呢? 直接把类强转为int类型不就4个字节了吗!?但是没有联系的类型是不能强转的。那怎么办呢??? C/C++中指针可以直接互相强转(BUG级别的操作!!!),整型与指针也可以互相转换。
VFPTR* p = (VFPTR*) *( (int*)&d );//这样就变成4个字节了!
&d
是取类的指针(int*)&d
将类指针强转为int*
指针!*( (int*)&d )
将 int *
解引用为int
(VFPTR*) *( (int*)&d )
将int
转换为VFPTR*
,取到虚表首地址!!!那么我们来验证一下:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
typedef void(*VFPTR)();
void PrintVFT(VFPTR* vft)
{
for (size_t i = 0; i < 4; i++)
{
printf("%p ->", vft[i]);
(*(vft[i]))();
}
}
int main()
{
Base b;
Derive d;
VFPTR* p = (VFPTR*)*((int*)&d);//这样就变成4个字节了!
PrintVFT(p);
return 0;
}
来看:
这样就成功获取到了虚标的内容,验证了虚表的内容中存在4个虚函数!!!
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类不能实例化出对象。派生类继承后也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
//抽象类
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
int main()
{
Car c;
return 0;
}
这样一个抽象类是不可以实例化的,进行实例化就会报错:
如果派生类进行了重新那么就可以正常使用:
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
//各种调用自身的虚函数
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
int main()
{
Test();
return 0;
}
抽象类与override关键字的区别:
多继承我们讲过,是一种很危险的继承,很容易导致菱形继承,引起数据冗余和二义性。那么我们再来看看多态在多继承中是然如何实现的 。
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
分析一下继承关系:
来看看Derive类的大小是多大:
我们分析一下:Base1类应该有一个虚表指针和一个int类型数据,所以应该为8字节。Base2同理8字节。
那么Derive由于多继承的缘故会包含两个基类,所以应该为16 + 4 = 20字节
:
运行一下,看来我们的分析没有问题!也就是有两张虚表,func1重写会改变两个虚表(因为两个基类都有func1函数),func3是放在Base1的虚表中的,通过虚表验证:
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
//通过切片获取
Base2 b2 = d;
VFPTR* vTableb2 = (VFPTR*)(*(int*)&b2);
PrintVTable(vTableb2);
return 0;
}
运行看看:
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。这里简单叙述一下: 先看菱形继承:
class A
{
public:
virtual void func1() { cout << "A::func1" << endl; }
int _a;
};
class B : public A
{
public:
virtual void func2() { cout << "B::func2" << endl; }
int _b;
};
class C : public A
{
public:
virtual void func3() { cout << "C::func3" << endl; }
int _c;
};
class D : public B, public C
{
public:
virtual void func4() { cout << "D::func4" << endl; }
int _d;
};
int main()
{
D d;
cout<< sizeof(d) << endl;
return 0;
}
先来看一下这个类有多大:
28 字节,这个是怎么得到的,来分析一下:
8 + 4 = 12
通过内存来验证一下:
可以看到只有两个虚表指针。所以菱形继承和多继承类似!
再来看菱形虚拟继承:
这个36字节是怎么得到的???
4 (虚表指针) + 4(虚基表指针) + 4(int数据) = 12
12 (B类) + 12(C类) + 4(A类的int)+ 4(D类的int) = 32
来看内存:
很明显,在A类中还有一个虚表指针!!!真滴复杂!
所以应该是:
12 (B类) + 12(C类) + 8(A类的int)+ 4(D类的int) = 36
那为什么A会有一个虚表指针,而不是D类有!?
注意:虚基表中储存两个值:第一个是距离虚表位置的偏移量,第二个是距离基类位置的偏移量