多态:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为
在继承中要构成多态有两个条件:
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "正常排队-全价买票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() //重写/覆盖 父类虚函数
{
cout << "正常排队-半价买票" << endl;
}
};
class Soldier : public Person
{
public:
virtual void BuyTicket() //重写/覆盖 父类虚函数
{
cout << "优先买票-全价买票" << endl;
}
};
// 多态两个条件:
// 1. 子类重写父类的虚函数
// 2. 必修是父类的指针或者引用去调用虚函数
void Func(Person* ptr)
{
//多态--ptr指向父类对象调用父类的虚函数,指向子类对象调用子类虚函数
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(&ps);
Func(&st);
Func(&sd);
return 0;
}
补充:
虚函数:即被virtual修饰的类成员函数称为虚函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;} //虚函数
};
注:这里的虚函数与虚继承使用了同一个关键字,但是要注意这两个语法并没有关联
补充:
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
虚函数重写有两个例外:
2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
注:在普通场景下,虚函数是否重写都是可以的
注:new函数的特殊场景需要用多态(虚函数重写)才能解决问题
总结:
final:修饰虚函数,表示该虚函数不能再被重写
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
总结:
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序
写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
在虚函数的后面写上 =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;
}
注:
注意一下这类情况:
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();
//注意:子类重写父类的虚函数时,只是将虚函数的实现方法重写,
//函数声明不变(包括virtual、函数名、参数、参数顺序、缺省值都跟父类的保持一致)
//相当于:
/*void func(int val = 1) { std::cout << "B->" << val << std::endl; }*/
return 0;
}
//运行结果:B->1
总结:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
private:
int _b = 1;
};
typedef void(*vfptr)();
// 打印虚表
void PrintVFT(vfptr vft[])
{
printf("%p\n", vft);
for (size_t i = 0;vft[i] != nullptr;i++)
{
printf("vft[%d]:%p->", i, vft[i]);
vft[i]();
}
printf("\n");
}
int main()
{
Base bb;
int a = 0;
int* p1 = new int;
const char* p2 = "hello";
auto pf = PrintVFT;
static int b = 1;
printf("栈帧变量:%p\n", &a);
printf("堆变量:%p\n", p1);
printf("常量区变量:%p\n", p2);
printf("函数地址变量:%p\n", pf);
printf("静态变量:%p\n", &b);
printf("虚函数表地址:%p\n", *((int*)&bb));
return 0;
}
注:
- 父子类无论是否完成虚函数重写,都有各自的独立的虚表
- 一个类的所有对象,共享一张虚表
- 多态之所以必须通过基类的指针或者引用调用虚函数原因是指针或引用可以根据该对象的地址找到对应基类的部分,从中找到虚函数表,执行对应的函数;而用赋值传参的方式是万万不行的,因为赋值传参只是将子类中的成员变量切片赋值到父类中去,而参数中的父类还是用的是父类的虚表 ,综上多态必须通过基类的指针或者引用调用虚函数
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 Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
总结:多态的虚函数调用,不是在编译时确定的,是运行起来以后到对象的中去找的。不满足多态的函数调用时编译时确认好的。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _d = 2;
};
typedef void(*vfptr)();
// 打印虚表
void PrintVFT(vfptr vft[])
{
printf("%p\n", vft);
for (size_t i = 0;vft[i] != nullptr;i++)
{
printf("vft[%d]:%p->", i, vft[i]);
vft[i]();
}
printf("\n");
}
int main()
{
Base b;
Derive d;
PrintVFT((vfptr*)*((int*)&b));
PrintVFT((vfptr*)*((int*)&d));
return 0;
}
思路:取出b、d对象的头4bytes,就是虚表的指针,虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
- 先取b的地址,强转成一个int*的指针
- 再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
- 再强转成vfptr*,因为虚表就是一个存vfptr类型(虚函数指针类型)的数组。
- 虚表指针传递给PrintVFT进行打印虚表
- 需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
注:这里vs编译器的监视窗口故意隐藏了这个函数,也可以认为是他的一个小bug
实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。菱形继承、菱形虚拟继承在实际中很少用。
注: