多态的概念
多态其实就是多种形态,当不同对象做相同的事情时,得到的不同的状态。多态其实可以分为两个,一个静态式多态,一个动态式多态。函数重载就是静态式多态的一种,在编译器期间就确定了程序的行为。而动态多态是在程序运行期间,根据自己拿到的类型确定程序的具体行为。简单来说就是一个提前知道要干嘛,一个边走边看。多态的前提是继承。
多态的定义及实现
class Person
{
public:
//用关键字virtual修饰的成员函数叫做虚函数
virtual void BuyTicket() {cout << "Person--买票-全价" << endl;}
};
class Student : public Person
{
public:
virtual void BuyTicket() {cout << "Student--买票-半价" << endl;}
};
这个代码中有三个类,我们可以看到这两个函数名字相同,那么它是否是构成了隐藏。这里是不构成隐藏的,函数名前加了个virtual 表示这是一个虚函数,这两个函数构成重写(也叫做覆盖)。
重写的条件(重写是重写实现)
1.要有关键字virtual来修饰
2.满足三同(函数名字相同,返回类型相会,参数类型相同)
但是这两个类并不构成多态的条件
构成多态的条件
1.必须是虚函数
2.父类的指针或引用来调用虚函数
//这里用的父类引用
void func(Person& p)
{
p.BuyTicket();
}
void Test()
{
Person p;
Student s;
func(p);
func(s);
}
结果:
如果用的不是父类的引用
void func(Person p)
{
p.BuyTicket();
}
结果:
如果是指针调用
void func(Person* p)
{
p->BuyTicket();
}
结果:
若父类和子类都不加virtual输出的结果:
普通调用和多态调用
这里其实要讲两种调用,分别是普通调用和多态调用
普通调用:根据调用的类型有关
多态调用:根据指针或者引用指向的对象有关
多态的两个例外
例外一
父类函数前加virtual, 子类不加,也构成多态。
class Person
{
public:
virtual void BuyTicket() {cout << "Person--买票-全价" << endl;}
};
class Student : public Person
{
public:
void BuyTicket() {cout << "Student--买票-半价" << endl;}
};
void func(Person& p)
{
p.BuyTicket();
}
void Test()
{
Person p;
Student s;
func(p);
func(s);
}
结果:
例外二
协变:三同中的返回值可以不同,但是要求返回值必须是父子关系的指针或引用
class Person
{
public:
//void BuyTicket()
virtual Person* BuyTicket()
{
cout << "Person--买票-全价" << endl;
return this;
}
};
class Student : public Person
{
public:
virtual Student* BuyTicket()
{
cout << "Student--买票-半价" << endl;
return this;
}
};
结果:
知识补充
补充一
析构函数建不建议设为虚函数,有如下类:
class A
{
public:
~A()
{
cout << "~A()" << endl;
delete[] _pa;
}
protected:
int* _pa = new int[5];
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
delete[] _pb;
}
protected:
int* _pb = new int[10];
};
我们正常使用场景:
void Test()
{
A a;
B b;
}
结果:
有如下使用场景:
void Test()
{
A* ptr1 = new A;
A* ptr2 = new B;
delete ptr1;
delete ptr2;
}
结果:
此时可以看到结果,调用了两次A的析构函数,B的析构函数并没有调用,这种代码会造成内存泄漏
原因:
这里是普通调用,普通调用跟对象的类型有关,这里的对象类型都是A*,那么就只会调用A类
解决办法:
多态调用,多态调用根据指针或引用指向的对象有关,虽然都是A*,但是一个指向父类,一个指向的是子类,那么根据多态,它两就分别能去调用自己的析构函数
如何改为多态调用?
在析构函数前加上virtual,在上篇继承中已经说明析构函数会被特殊处理成destructor函数名,那么子类和父类的析构函数满足三同。
修改代码之后的结果:
补充二
关键字final和override
final关键字有两个作用
一是用来让一个类不能被继承,当我们不想让我们的一个类被继承就可以用这个关键字
class A final
{};
class B : public A
{};
结果:
二是用来让虚函数不能被重写
class A
{
public:
virtual void TestCode() final
{}
};
class B : public A
{
public:
virtual void TestCode()
{}
};
结果:
override关键字的作用是检查该子类虚函数是否被重写
class A
{
public:
virtual void TestCode(int)
{}
};
class B : public A
{
public:
virtual void TestCode() override
{}
};
结果:
抽象类
含有纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化对象,子类继承之后也不能实例化,除非重写虚函数,子类才能实例化对象。纯虚函数只需在虚函数后加上 =0
class A
{
public:
virtual void test_code()=0
{}
};
class B : public A
{
public:
//此时的A,B类都不能实例化
//如果将下面取消注释后那么B可以实例化对象,A依然不可以
//virtual void test_code()
//{}
};
接口继承和实现继承
普通函数的继承就是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,子类继承父类函数的接口,目的是为了重写,达成多态,继承的是接口,如果不是多态,就不要把函数定义成虚函数
多态的原理
计算下面这个类的大小(在·32位平台下)
class A
{
public:
virtual void func()
{};
protected:
int _a;
char _c;
};
结果:
在没有了解过虚表指针的时候可能大家算的应该都是8,但是这里包含了个指针,因此大小是12
如上图这个指针__vfptr。一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。
class A
{
public:
virtual void func1() {cout << "A->func1()" << endl;}
virtual void func2() {cout << "A->func2()" << endl;}
void func3() {cout << "A->func3()" << endl;}
protected:
int _a;
};
class B
{
public:
virtual void func1() {cout << "B->func1()" << endl;}
protected:
int _b;
};
int main()
{
A a;
B b;
}
上述代码通过调试,我们发现子类b对象中也有一个虚表指针,我们观察上图中,我们的func1完成了重写,所以b的虚表中存的就是b::func1,所以虚函数的重写也叫做覆盖,覆盖就是指虚表中的虚函数的覆盖。其中func3也被继承下来但不是虚函数,因此不会放进虚表。
知识补充
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
总结子类虚表的生成
先将父类的虚表内容拷贝一份到子类虚表中
如果子类中重写了父类的某个虚函数,那么子类会将自己的虚函数重写到虚表中的对应位置
子类中自己的虚函数会依此按声明顺序增加到虚表中
单继承和多继承关系的虚函数表
单继承中的虚函数表
class A
{
public:
virtual void func1() {cout << "A->func1()" << endl;}
virtual void func2() {cout << "A->func2()" << endl;}
void func3() {cout << "A->func3()" << endl;}
protected:
int _a;
};
class B
{
public:
virtual void func1() {cout << "B->func1()" << endl;}
virtual void func3() {cout << "B->func3()" << endl;}
void func4() {cout << "B->func4()" << endl;}
protected:
int _b;
};
按理来说,依据上述代码在图中b指向的虚表应该包含func3,但是并没有,这里是编译器做了相关的优化,我们将b类对象的虚表打印出来看看
typedef void(*VFPtr)();
void PrintVFT(VFPtr vft[])
{
for (int i = 0; vft[i] != nullptr; ++i)
{
printf("[%d] : %p->", i, vft[i]);
vft[i]();
}
cout << endl;
}
通过打印的方式观察到:
多继承中的虚函数表
class A
{
public:
virtual void func1() {cout << "A->func1()" << endl;}
virtual void func2() {cout << "A->func2()" << endl;}
protected:
int _a;
};
class B
{
public:
virtual void func1() {cout << "A->func1()" << endl;}
virtual void func2() {cout << "A->func2()" << endl;}
protected:
int _b;
};
class C : public A, public B
{
public:
virtual void func1() {cout << "A->func1()" << endl;}
virtual void func3() {cout << "A->func2()" << endl;}
protected:
int _c;
};
我们可以看到c中继承了两个虚表分别都对func1,func2进行了重写,但是C中的func3是存在A中,还是存在B中,我们可以打印出每个对象的虚表。结果如下
int main()
{
A a;
PrintVFT((VFPtr*)(*(void**)(&a)));
B b;
PrintVFT((VFPtr*)(*(void**)(&b)));
C c;
PrintVFT((VFPtr*)(*(void**)(&c)));
PrintVFT((VFPtr*)(*(void**)((char*)&c + sizeof(A))));
//下面这个代码是发生了切片是指针指向C中的B
//B* ptr = &c;
//PrintVFT((VFPtr*)(*(void**)ptr));
}
我们可以观察到我们的func3是存放在第一个继承类部分的虚表中。
总结:多继承子类未重写的虚函数放在第一个继承父类部分的虚函数表中