
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态 举个例子:比如说买票,普通人是全价买,学生是半价,退伍军人是优先。
多态很重要的前提就是先继承。 并且要去用基类的指针或者是引用去调用虚函数 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }//成员函数前面加一个virtual就成为虚函数
};
class Student:public Person
{
public:
//这里是虚函数的 重写/覆盖
virtual void BuyTicket() { cout << "买票-半价" << endl; }//条件是三同:返回值和函数名还有参数相同
};
int main()
{
Person s1;
Student s2;
Person* p = &s1;
p->BuyTicket();
p = &s2;
p->BuyTicket();
return 0;
}
这里也叫做多态调用。 之前的调用都是普通调用,一直都和对象的类型有关。 多态调用是跟指向的对象有关。 如果改成普通调用就是类型是谁就去调用谁的成员函数,多态调用就是指向的对象是谁就去调用谁的虚函数。
子类虚函数可以不加virtual:
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student:public Person
{
public:
//这里是虚函数的 重写/覆盖
void BuyTicket() { cout << "买票-半价" << endl; }//只要三同,子类不加virtual也是虚函数
}
int main()
{
Person s1;
Student s2;
Person* p = &s1;
p->BuyTicket();
p = &s2;
p->BuyTicket();
return 0;
}
不过这里建议都加上virtual。 协变: 三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用。
#include<iostream>
using namespace std;
class Person {
public:
virtual Person* BuyTicket() { cout << "买票-全价" << endl; return this; }
};
class Student:public Person
{
public:
virtual Student* BuyTicket() { cout << "买票-半价" << endl; return this; }
};
int main()
{
Person s1;
Student s2;
Person* p = &s1;
p->BuyTicket();
p = &s2;
p->BuyTicket();
return 0;
}
正常运行。 析构函数的重写
#include<iostream>
using namespace std;
class A
{
public:
~A()
{
cout << "delete s1" << endl;
delete[] s1;
}
protected:
int* s1 = new int[20];
};
class B :public A
{
public:
~B()
{
cout << "delete s2" << endl;
delete[] s2;
}
protected:
int* s2 = new int[20];
};
int main()
{
A a;
B b;
return 0;
}
目前看来确实没什么问题,都是正常调用,来看看如下的情况:

这里导致了内存泄漏,因为析构函数不是虚函数,只能完成普通调用,所以最好在析构面前加一个virtual。

这下子就可以了。 其实子类不加virtual这里更合适,更方便。 所以在实现父类的时候,最好无脑的给析构函数加virtual。
final: 如何实现一个不被继承的类? C++11提供了一个关键字,类定义的时候加final:

如果放在父类的某个虚函数后面就是不让这个虚函数被重写。

但是这个情况很少见。 override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。


在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。


#include <iostream>
using namespace std;
class A//抽象类
{
public:
virtual void add() = 0 {};//纯虚函数
};
class B:public A
{
public:
void add(){}
};
int main()
{
B s;//但是A仍然不能实例化
return 0;
}
这就是说给某个函数必须进行重写。 抽象类一般用于,比如说车,他是一个概念,但是他有自行车,电动车,跑车等等,然后还被分为好多的品牌,所以车必须要分类出来。 接口继承和实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。 下面程序输出什么?
#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;
}
首先,创建的是子类对象,子类对象去调用虚函数test(),然后里面是调用func(),这里要注意,是一个多态调用,因为test成员函数是属于A类的,调用func函数是通过this指针去调用(就算是test函数被子类继承了,内部的this指针也不会被更换,还是A类的this指针),并且func函数也进行了重写,在main函数中调用的也是子类对象,所以走向的是B类中的func函数。 这里最让我们疑惑的就是为什么是1不是0,这里就涉及到了只继承接口,所以val的缺省值还是1。 但是子类的缺省参数并不是一点用处都没有,当普通调用的时候这个缺省参数就可以使用了。 再看一个程序:选哪个?
#include <iostream>
using namespace std;
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3 数据模型大概是这样的:

所以选C。 这里注意一下:其实继承的对象在内存里是从下面开始放,因为下面是低地址,上面是是高地址,我们经常能看到一个数组,用数组名+n就能到对应的位置,这就是为什么从低地址放的原因,加就代表要到高地址。
先来研究一下这个类的大小:32位环境下
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
这里明明只有一个成员变量,之前说过成员函数并不在类中,可是为什么结果是8呢?

这里多出来了一个_vfptr,这个叫做虚表/虚函数表,里面储存的是虚函数的地址。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
Base a;
return 0;
}

多态的原理一定跟虚表有着千丝万缕的联系。 再来看看完成重写有什么区别;
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
这里虚表也变了,之前重写也可以叫做覆盖,这里就是覆盖的部分。 其实重写只是语法上的,继承了父类的接口,重写了实现部分。覆盖就是覆盖了父类继承过来重写的虚函数的地址。 那么我们这样调用试一下:



多态调用更长。 这里差别就在于,根本不在乎是指向哪里,因为有虚表的存在,如果指向父类就去父类的虚表中找,如果指向子类就去子类的虚表中找。 在汇编当中eax里面存的就是虚表指针数组。
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。 2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
那么虚表是放在哪一个位置呢?

打印出来的地址和常量区非常接近,所以是在常量区。
#include <iostream>
using namespace std;
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当中其实并不能看到虚表当中所有的虚函数,这时VS编译器的一个优化,也可以看作是一个BUG。 这个时候我们可以用内存窗口去看。

这里也将func3和func4的函数地址给显示出来,顺便说一下,在VS编译器下,虚表是以空指针结尾的。 但是这样看有些麻烦,我们想个办法给他打印出来。
#include <iostream>
using namespace std;
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(*p)();
void PrintVFTbale(p vft[])//打印虚表
{
for (int i = 0; vft[i]; i++)
{
printf("[%d]:%p->", i, vft[i]);//打印虚表当中每个数组的内容,也就是每个虚函数的地址
vft[i]();//调用对应的函数
}
}
int main()
{
Base b;
Derive d;
PrintVFTbale((p*)(*(int*)&b));//将虚表的地址传过去
PrintVFTbale((p*)(*(int*)&d));
return 0;
}
这里还可以改进,因为有时候是64位和32位,到时候64位就是取头8个字节了。

其实只需要将里面的变成二级指针就行了(任何类型的二级指针都可以),因为二级指针是储存一级指针的,解引用之后再去看解引用多大时,剩下的就是一级指针,一级指针就可以根据平台位数变化了,到时候就对应了64位和32位的平台大小了。
#include <iostream>
using namespace std;
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;
};
typedef void(*VFPTR) ();
typedef void(*p)();
void PrintVFTbale(p vft[])//打印虚表
{
for (int i = 0; vft[i] != nullptr; i++)
{
printf("[%d]:%p->", i, vft[i]);
vft[i]();
}
}
int main()
{
Base1 b1;
Base2 b2;
PrintVFTbale((p*)(*(void**)&b1));
PrintVFTbale((p*)(*(void**)&b2));
Derive d;
PrintVFTbale((p*)(*(void**)&d));//d有两个虚表
PrintVFTbale((p*)(*(void**)((char*)/*不强转就不是+1跳过一个字节了*/ &d + sizeof(Base1))));//想要打印第二个虚表就要跳过第一个虚表的地址
return 0;
}
我们发现,d中的func3虚函数放进了第一个虚表里面。
#include<iostream>
using namespace std;
class A
{
public:
virtual void func1(){}
int _a;
};
class B : public A
//class B : virtual public A
{
public:
virtual void func1() {}
int _b;
};
class C : public A
//class C : virtual public A
{
public:
virtual void func1() {}
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}如果是菱形继承,这样重写虚函数是没有问题的,因为B和C都有一个A。 但如果是菱形虚拟继承就不可以了,因为A类变成了一个,B和C进行重写A类的函数的时候编译器无法分辨应该按照B还是C进行重写。

所以这种情况只能在d中进行重写。 在d中重写之后,d就有三张虚表,分别是A,B,C。
#include<iostream>
using namespace std;
class A {
public:
A(const char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A
{
public:
B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2),C(s1, s3),A(s1)
{
cout << s4 << endl;
}
};
int main() {
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
这里要注意: 1.因为是菱形虚拟继承,A只有一份,定义D是直走D当中的初始化列表的A类初始化,不会走B和C类的A类初始化,因为A是B和C共享的,B和C去初始化都不合适,所以A只被D初始化了一次。 但是B和C类中的A类初始化必须有,因为万一要创建B和C类就需要有A类的初始化了。 2.初始换是按照成员声明顺序初始化(如果是继承就看继承顺序),而不是看初始化列表的顺序进行初始化。 那么如果只是菱形继承呢?
#include<iostream>
using namespace std;
class A {
public:
A(const char* s) { cout << s << endl; }
~A() {}
};
class B :public A
{
public:
B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :public A
{
public:
C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2),C(s1, s3)//这里就不用初始化A了,因为有两份A,在B和C中,B和C就会去调用A的初始化
{
cout << s4 << endl;
}
};
int main() {
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
1.什么是多态? 静态的多态和动态的多态。就是重载和虚函数的重写。 2.内联函数能不能是虚函数呢? 语法上来说是可以的,但是其实是并不行的,因为inline关键字只是提出一个建议,到底需不需要去变成内联函数是要看实际情况的,如果将虚函数变成内联函数是没有办法放进虚表里面的,编译器就忽略inline属性,这个函数就不再是inline。 也就是说,如果是多态调用就没有内联属性,但是普通调用可以继续保持内联属性。 3.为什么父类的对象无法完成多态调用呢?
#include <iostream>
using namespace std;
class A
{
public:
virtual void add() { cout << "A:add" << endl; }
int _a;
};
class B :public A
{
public:
virtual void add() { cout << "B:add" << endl; }
int _b;
};
void app(A a)//这里传过来的如果是B类的对象,顶多是切片过来的A类一部分,不可能是拷贝过来所有的内容
{
a.add();//这里就是静态绑定,普通调用
}
int main()
{
A a;
B b;
app(a);
app(b);//如果拷贝到A a中,里面的内容就乱套了,所以只能用引用或者是指针才能指向子类对象完成多态的调用
return 0;
}4.静态成员可以是虚函数吗? 不可以,因为静态成员函数没有this指针(this指针指向的对象中存有虚函数表指针,没有this指针就找不到对象,也就找不到虚函数表指针了),使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 5.构造函数能不能是虚函数? 虚函数要放进虚表,在没有调用构造函数之前,虚表还有没进行初始化,构造函数之后才进行初始化。 这也说明,如果构造函数都是虚函数,那么我们就无法找到构造函数的地址了。 6.对象访问普通函数快还是虚函数更快? 首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 7.虚函数表是在什么阶段生成的,存在哪的? 虚函数表是在编译阶段就生成的初始化列表中初始化虚表指针,一般情况下存在代码段(常量区)的。