本文中的代码及解释都是在vs2015下的x86程序中,涉及的指针都是4bytes。
如果要其他平台下,部分代码需要改动。比如x64程序,指针是8bytes
3.抽象类
图形类
Shape是不具体的,有三角形的图形,有圆的图形,不能说:有一个图形的图形
正常情况下:Shape类是不能创建对象的,该类本身就不具体,即抽象的,所以也不应该创建其对象
将类似于Shape的类:抽象类--->将包含纯虚函数的类称为抽象类,抽象类特性:不能创建对象
纯虚函数:在虚函数名之后跟上=0 表明该虚函数为纯虚函数
class Shape
{
public:
// 在虚函数名之后跟上=0 表明该虚函数为纯虚函数
virtual void DrawShape() = 0;
virtual double GetShapePerimeter() = 0;
void Show()
{
cout << "我是可爱的图形" << endl;
}
};
有抽象类好处:
1. 代码实现更加符合逻辑:即有些不具体的类就是不应该让其创建对象
2. 不花费时间去考虑纯虚函数中的代码该怎么写
3. 抽象类实际规范了:后序子类要实现的虚函数的原型--->将接口规划范
注意:抽象类一定要被继承
在子类中,要对抽象类中的所有的纯虚函数进行重写,否则子类也是抽象类
即:子类将抽象类中的纯虚函数全部重写之后则子类就可以创建对象,否则子类也是抽象类
class Rect : public Shape
{
public:
Rect(double length = 1, double width = 1)
: _length(length)
, _width(width)
{}
virtual void DrawShape()
{
cout << "□" << endl;
}
virtual double GetShapePerimeter()
{
return 2 * (_width + _length);
}
protected:
double _length;
double _width;
};
// Triangle类中没有重写Shape类中所有的抽象类,因此
// Triangle也是抽象类,即不能创建对象,该类必须要被继承
class Triangle : public Shape
{
public:
Triangle(int a = 1, int b = 1, int c = 1)
: _a(a)
, _b(b)
, _c(c)
{}
virtual double GetShapePerimeter()
{
return _a + _b + _c;
}
protected:
int _a;
int _b;
int _c;
};
class Triangle90 : public Triangle
{
public:
Triangle90(int a = 3, int b = 4, int c = 5)
: Triangle(a, b, c)
{}
virtual void DrawShape()
{
cout << "直角三角形" << endl;
}
};
4.多态的原理
对象模型
对象模型:带有虚函数的类对象的模型
class B
{
public:
void func()
{
cout << "B::func()" << endl;
}
int _b;
};
虚函数表
sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代virtual,f代表function)。
如果一个类中含有虚函数,发现:
类对象大小多了4个字节
经过探究发现,多的4个字节中放的是一个地址,地址指向的空间中放置的虚函数地址
将对象中多的4个字节称为虚表指针--》虚函数表格(虚表)
发现:编译器一定会给该类生成构造的方法,目的:在生成的构造方法中,将虚表地址填充到对象的前4个字节中
同一个类的多个对象共享同一张虚表(创建一个类的多个不同的对象,发现每个对象前4个字节中的虚表地址是相同的)
基类虚函数表的构成原理:在编译代码阶段,编译器会将该类中虚函数按照其在类中的声明次序依次添加到虚表中
class B
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
int _b;
};
00 00 00 00是结尾标识
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
子类虚表的构成的过程:
将基类虚表中内容原封不动的拷贝到子类的虚表中
如果子类重写了某个基类的虚函数,则编译器会用子类虚函数地址去替换子类虚表中相同偏移量位置的基类虚函数地址
如果子类增加了新的虚函数,则将新增加的虚函数按照其在子类声明的先后次序依次增加到子类虚表的最后
动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
附:买票例子展示多态原理
1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的
对代码的汇编进行分析
void Func(Person* p)
{
...
p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调
用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
}