多态
提前声明:本篇的代码是在vs2013下的x86中运行,指针都是4个字节。如果在其他平台下代码需要进行修改。
首先我们来说说什么多态?
多态就是进行某个行为时,但不同的对象去会有不同的结果。举个例子:买电影票这个行为,普通人去买就是普通票,学生去买就是学生票。这种行为体现的就是多态。
多态的实现条件
前提:在继承体系下,继承权限是必须是public。
1.基类中必须有虚函数(被virtual关键字修饰的成员函数为虚函数),在派生类中要对基类中的虚函数进行重写。
(1)重写的时候,必须是在派生类中重写基类中的某个虚函数。
(2)派生类中的虚函数,必须与基类中的虚函数的原型(原型:返回值类型、参数列表、函数名字)完全一致。
2.对于虚函数的调用:必须使用基类的指针或者引用来调用虚函数。
注意:这两个条件缺一不可,都要存在才能是多态。
如果没有完全满足多态的实现条件
1.如果基类函数不是虚函数,或者函数原型不一致,就会造成重写失败。
2.没有通过基类的指针或者引用调用虚函数,不能实现多态。
多态的体现:
1.代码编译时,不能确定到底调用哪个类的虚函数。
2.只有在代码运行时,根据基类的引用所指向的实际对象来选择调用对应的虚函数。
如何重写:
1.一个函数在基类,一个函数在派生类中。
2.基类中的成员函数必须是虚函数,如果不是虚函数,则就不是多态,而会产生同名隐藏。
3.派生类中的同名成员函数前virtual关键字加不加都可以,此处最好加上。
4.基类虚函数必须与派生类虚函数的原型必须完全相同(返回值,函数名字,参数列表)。
此处有两个例外:
(1)协变:基类中虚函数返回基类的引用(指针),子类的虚函数返回子类的引用(指针)。这种虽然返回值不同,但也可以形成重写。
(2)析构函数:虽然两个类的析构函数名字不同,但是只要基类中的析构函数是虚函数,则也可以形成重写。
此处我们来区分一下函数重载、同名隐藏、重写
override和final关键字
在C++11中关于虚函数这部分,提供了两个关键字。
override关键字:
1.只能放在派生类的虚函数后面。
2.作用是让编译器帮助用户检测派生类中某个虚函数是否重写了基类的那个虚函数。
final关键字:
1.我们在继承中也见到了final关键字,在继承中它修饰的是类,作用是让一个类无法被继承。
2.在多态中,其修饰的是虚函数,作用是让这个虚函数在其派生类中不能被重写。
以下代码是多态的实现:
class person
{
public:
virtual void buy()//基类中的虚函数
{
cout << "成人票" << endl;
}
};
class student :public person
{
public:
virtual void buy() override//对基类中的虚函数进行重写。派生类中的基类的虚函数前面可以不加virtual,但这里最好加上
{
cout << "学生票" << endl;
}
};
void Buyticket(person& p)//用基类的指针或者引用来调用虚函数
{
p.buy();
}
int main()
{
person s;
student stu;
Buyticket(s);
Buyticket(stu);
return 0;
}
注意:如果基类中有虚函数,则子类中必须对该虚函数进行重写,否则该函数没必要设置为虚函数。
抽象类:
概念:包含纯虚函数的类称为抽象类。
我们先来了解一下什么是纯虚函数:
纯虚函数就是虚函数的后面加个=0,这样的虚函数为纯虚函数。
作用:规范了派生类中的虚函数必须重写。
抽象类特性:
1.抽象类不能实例化对象,但可以创建该类的指针(引用)。
2.派生类继承抽象类后如果没有重写派生类的纯虚函数,则派生类也是抽象类,只有派生类中的纯虚函数被重写,派生类才可以实例化对象。
3.作用:规范后序接口,也就是说以后我们可以将自己可能需要用的方法作为纯虚函数放在基类中,如果哪个子类要用再在子类中具体实现即可用
关于抽象类的例子:
class person//抽象类
{
public:
virtual void toWc() = 0;//纯虚函数
};
class man :public person
{
public:
void toWc()override
{
cout << "left" << endl;
}
};
class woman :public person
{
public:
void toWc()override
{
cout << "right" << endl;
}
};
int main()
{
person* s;//抽象类虽然不能实例化对象,可以创建该类的指针。
s = new man;//基类的指针指向派生类man
s->toWc();//调用man中的虚函数
s = new woman;//基类的指针指向派生类woman
s->toWc();//调用woman中的虚函数
return 0;
}
多态的原理:
我们先来看一段代码:
class person
{
public:
virtual void toWc()
{}
virtual void toWc1()
{}
virtual void toWc2()
{}
int b;
};
int main()
{
cout << sizeof(person) << endl;
return 0;
}
由结果可以看出,如果一个类中有虚函数(不管有几个虚函数),则类中会多出四个字节。毋庸置疑,这四个字节是构造对象期间加上的,所以编译器会给该类生成一个默认构造函数来给前四个字节赋值,当然如果该类已经显示定义了自己的构造函数,编译器就会对构造函数多增加一条语句来给对象前四个字节赋值。
注意:此处应该区分 虚表和 虚机表。
基类虚表的构建过程:虚表中所有虚函数的次序是根据类中虚函数的声明次序放到虚表中。
派生类虚表的构建过程:
1.将基类虚表中的内容拷贝一份放到子类的虚表中(基类虚表与派生类的虚表不是同一个)。(补充:同一个类的不同对象在底层共享一张虚表)
2.如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址替换(覆盖)派生类虚表中相同偏移量位置的虚函数。
3.如果在派生类中增加了新的虚函数,按照其在类中的声明次序依次增加到派生类虚表的最后。(一般派生类不会再次增加虚函数,如果不重写则没有必要)。
对于第3点我们可以进行验证:
有两种方法:
一、我们可以结合vs2013上的监视窗口+内存窗口+自己的猜想,来验证派生类对象中新增的虚函数是否按照上述第三点放在派生类的虚表中。
二、我们可以通过自己猜想反推来验证第3点:(也是打印虚表的方法)
我们先来假设派生类对象模型的前四个字节位置存储的是虚函数的入口地址,如果能取到前四个字节空间中的内容,我们就可以调用派生类中的虚函数,如果调用到了派生类中新增的虚函数,则验证成功。
步骤:
1.我们先得到对象的前四个字节(虚表指针),并且从中获得虚表的地址。
2.从虚表指针指向的空间中找到虚函数的入口地址。
代码为:
class Father
{
public:
virtual void TestFunc1()
{
cout << "Father::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "Father::TestFunc2()" << endl;
}
virtual void TestFunc3()
{
cout << "Father::TestFunc3()" << endl;
}
};
class Child : public Father
{
public:
virtual void TestFunc1()
{
cout << "Child::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "Child::TestFunc2()" << endl;
}
virtual void TestFunc3()
{
cout << "Child::TestFunc3()" << endl;
}
virtual void TestFunc4()
{
cout << "Child::TestFunc4()" << endl;
}
};
typedef void(*PT)();//函数的类型为*PT
void PrintTable(Father& F, const string& str)
{
cout << str << endl;
//&F; // 这种取地址。是指向对象本身
//(int*)&F; // 对象前四个字节的地址
//*(int*)&F; // 解引用后,为对象前4个字节中内容--->但是此时是整形数字
//此时我们就需要将数字转化为虚表中首地址,所以就需要知道表格中元素的元素类型,前面说过虚表可以看做是函数指针数组,所以里面元素是函数指针,因此我们要得到函数的类型,才可以将数字强转为函数堆的入口地址,然后进行调用。
PT* pVFT = (PT*)(*(int*)&F);//获取虚函数的入口地址。
while (*pVFT)//因为在vs2013编译器中,虚表中在所有虚函数的入口地址之后有四个字节的0,所以可以以此作为循环终止条件。
{
(*pVFT)();//依次通过入口地址来调用虚函数。函数的名字,就是函数的入口地址,所以可以这么写。
++pVFT;
}
cout << endl;
}
int main()
{
Child c;
PrintTable(c, "Child:");
return 0;
}
最终我们通过结果可以验证,结论正确。
虚函数的调用原理:
我们先来看一段代码:
class Father
{
public:
virtual void TestFunc1()
{
cout << "Father::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "Father::TestFunc2()" << endl;
}
virtual void TestFunc3()
{
cout << "Father::TestFunc3()" << endl;
}
void TestFunc4()
{
cout << "Father::TestFunc4()" << endl;
}
};
class Child : public Father
{
public:
virtual void TestFunc1()
{
cout << "Child::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "Child::TestFunc2()" << endl;
}
virtual void TestFunc3()
{
cout << "Child::TestFunc3()" << endl;
}
};
void Test(Father& F, const string& s)//因为前面说过,调用虚函数必须用基类的指针或者引用。
{
cout << s << endl;
F.TestFunc1();
F.TestFunc2();
F.TestFunc3();
F.TestFunc4();
}
int main()
{
Father F;
Child c;
Test(F, "Father:");
Test(c, "Child:");
return 0;
}
通过代码和反汇编我们可以发现不论是基类或者派生类虚函数调用:
1.都是先从对象前四个字节中找到虚表,找到虚表的地址。
2.其次传递this指针。
3然后从虚表中找要调用的对应虚函数。
4最后调用虚函数即可。
5.不是虚函数,直接调用即可。
带有虚函数的多继承派生类
实现代码:
class B1
{
public:
virtual void TestFunc1()
{
cout << "B1::TestFunc1()" << endl;
}
};
class B2
{
public:
virtual void TestFunc2()
{
cout << "B2::TestFunc2()" << endl;
}
};
class Child : public B1,public B2
{
public:
virtual void TestFunc1()
{
cout << "c::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "c::TestFunc2()" << endl;
}
virtual void TestFunc3()
{
cout << "c::TestFunc3()" << endl;
}
};
typedef void(*PVFT)();
void print1(B1& b)//因为有两个基类,派生类的对象模型有两个虚表。因此虚表打印需要写两个。
{
cout << "Child重写B1基类的虚表" << endl;
PVFT* p = (PVFT*)(*(int*)&b);
while (*p)
{
(*p)();
++p;
}
cout << endl;
}
void print2(B2& b)
{
cout << "Child重写B2基类的虚表" << endl;
PVFT* p = (PVFT*)(*(int*)&b);
while (*p)
{
(*p)();
++p;
}
cout << endl;
}
int main()
{
Child c;
print1(c);
print2(c);
return 0;
}
带有虚函数的多继承派生类对象模型:
多继承派生类虚表的构建过程:
1.与单继承中派生类虚标的构建过程相同。
2.如果派生类新增加的虚函数,按照其在类中的声明次序加到第一个虚表后。