目录
- 多态的概念
- 多态的定义及实现
- 协变
- 析构函数的重写
- 通过一段代码理解多态
- C++11 final 和 override
- 重载、覆盖(重写)、隐藏(重定义)的对比
- 多态调用原理
- 单继承中的虚函数表
- 抽象类
- 多继承中的虚函数表
多态的概念
概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
多态的定义及实现
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。 比如Student继承了Person。Person对象买票全价,Student对象买票半价。
只有在继承中多态才存在。
class person
{
public:virtual void buy(){cout << "全价" << endl;}
};
class student:public person
{
public:/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*//*void buy() { cout << "买票-半价" << endl; }*/virtual void buy(){cout << "半价" << endl;}
};
void buy(person& p)
{p.buy();
}
int main()
{person p;student s;buy(p);//全价buy(s);//半价return 0;
}
在继承中要构成多态有两个条件:
- 父子类完成虚函数重写
虚函数的重写(覆盖):子类中有一个跟基类完全相同的虚函数(即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数。
- 父类的指针或引用去调用虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
这里virtual关键字和菱形虚拟继承那里使用没有任何关系,只是关键字一样。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
协变
子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。
协变是多态的一种特殊情况,三同中,返回值不同。
class A {};
class B : public A {};
class Person {
public:virtual A* f() { return new A; }
};
class Student : public Person {
public:virtual B* f() { return new B; }
};
返回值只要是父子类的关系即可,子类返回的是子类对象的指针或引用,父类亦是如此。
析构函数的重写
析构函数建议重写为虚函数,原因如下代码说明:
class Person {
public:~Person(){cout << "~Person()" << endl;}
private:int p1;int p2;string ps1;
};
class Student : public Person {
public:~Student(){cout << "~Student()" << endl;}
private:int s1;int s2;string ss1;
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
结果为:
只执行了父类的析构,但是空间全部释放了,子类没有执行析构。如果子类中存在空间的申请,那么就会造成内存的泄露。
注意,析构函数并不释放空间,堆上的对象由free释放,栈上的由操作系统释放。
但是如果使用虚函数的重写,就会变为多态的调用,父类的指针,指向子类,那么就调用子类的析构。
为什么没有满足三同也可以执行呢?因为为了满足多态的需要,编译的时候析构函数的名称统一处理成destructor
。这也是前期设计时经验不足,后期加上的规则。
所以正确的满足多态的写法是:
class Person {
public:virtual ~Person(){cout << "~Person()" << endl;}
private:int p1;int p2;string ps1;
};
class Student : public Person {
public:virtual ~Student(){cout << "~Student()" << endl;}
private:int s1;int s2;string ss1;
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;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:virtual void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{B* p = new B;p->test();//p->func();//B->0,非多态调用,先在B中查找return 0;
}
执行结果是B->1。
分析:
首先,第一个结论:多态调用是指向谁调用谁,和类型无关,函数调用看类型。
p是B类型即子类类型的指针,指向子类。p->test();
去调用父类的test()
,但是test()
的this
类型是不变的,还是A*
,在test()
函数中,执行的是this->func()
。this
的值是子类的地址,指向子类,但是是父类类型的指针。所有构成多态调用。所有调用子类的func()
。因为this的类型是父类类型指针,所有使用父类的缺省值。
如下代码可以证明:
如果把main函数换为:
int main()
{A* p = new B;p->test();p->func();return 0;
}
执行结果为B->1和B->1。
第二次执行出B->1,便可以证明多态调用时,指针是父类型,函数就调用父类型的缺省值。 ,也可以说多态调用调用父类型的缺省值。 因为不是父类型就不再是多态调用了。
结合两个结论,虽然p是父类型指针,但是指向的是B,所以调用B的函数实现。所以满足多态调用,所以p->func();
输出B->1。
结合上述两个案例,最重要的结论就是,要牢记多态调用的条件,满足了虚函数重写,如果是父类型的指针调用了虚函数,那么就会产生多态调用。且指针指向谁调用谁
C++11 final 和 override
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};
上述代码表示Car中的Drive不能被重写,所以会报错
也可以用在类上面,表示不能被继承
class Car final
{
public:virtual void Drive(){}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};
上述代码表示,Car无法被继承。
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
若没有完成重写,就会报错。
重载、覆盖(重写)、隐藏(重定义)的对比
多态调用原理
探究多态调用原理可以通过下列代码入手,输出是几呢?
class student
{
public:virtual void buy(int i = 200){cout << "半价" << i << endl;}
private:int a;char b;
};
int main()
{cout << sizeof(student) << endl;return 0;
}
输出结果为12
为什么呢?这就是虚函数和不同函数的区别,当类中存在虚函数的时候,编译器在编译阶段形成虚函数表,即使没有实例化对象,虚函数表也是存在的。对象实例化的时候,存在虚表指针指向虚函数表。每个虚函数的地址都会存在虚函数表中。虚函数表中存的是虚函数的地址。地址就是成员函数所在的地址。多态调用和普通调用都是同一个地址,只是多态调用多走了一个虚表。
因为多了一个虚表指针,且要满足对齐规则,并且是32位系统下,所以大小是12。
将代码改为这样:
class person
{
public:virtual void buy(int i = 100){cout << "全价" << i << endl;}
private:int a;
};
class student :public person
{
public:virtual void buy(int i = 200){cout << "半价" << i << endl;}
private:int a;char b;
};
void buy(person& p)
{p.buy();
}
int main()
{student s1;cout << sizeof(student) << endl;return 0;
}
student继承了person,其中有buy()这个虚函数
通过内存观察一下,子类中的vfptr就是虚表指针,指向的一张表就是虚表,虚表中存的是函数地址。可以看到蓝色框到的有student说明是student的虚函数,如果单独调用虚函数而不进行多态调用,可以发现使用的地址都是同一个地址。
每个对象都有自己的虚表指针,且指向自己类的虚表。同一类的对象,虚表使用的是同一个。虚表指针都指向同一个虚表。
通过上述了解,便可以明白多态调用的原理:
类似下图红色框住的:
当满足多态调用时,Student会进行切片,对于Func()来说都是父类,并不知道传进来的是什么,传入后,会在虚表中去找调用函数的地址,子类中虚表是子类虚函数的地址,父类中虚表是父类虚函数的地址。
于是就出现了前面说的,满足多态调用时,指向父类调用父类,指向子类调用子类的现象。
在Func()中调用时,多态的调用会转为一串汇编指令,指令就是将调用函数地址的值写入eax。
整个过程是比较复杂的,而是否符合多态的调用是在运行时确定的,并不是在编译时确定的。
单继承中的虚函数表
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;
}
通过监视窗口看一下上述的代码:
可以发现Derive的窗口有些问题,虚表中没有func3()和func()4,这是编译器的处理的问题,实际上通过内存窗口观察一下。
可以了解的是,d的虚表中func1()还是指向的func1(),可以这样理解,虚表其实是重父类拷贝下来的,然后子类有重写的函数就进行覆盖。覆盖的概念就是在虚表这里,这是形象的说法。
我们可以看到func1()和func2()都在虚表中,但是另外两个疑似也是某个函数的地址。
可以通过下面的代码证明一下。
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;void(**p)() = (void(**)())(*((int*)(&d)));(**p)();(**(p + 1))();(**(p + 2))();(**(p + 3))();return 0;
}
我们知道虚表中存的是函数的地址,那么虚表就可以看作是一个函数指针数组。
而对象的第一个元素,即前四个字节存的是虚表的地址。那么我们就可以通过两次解引用去调用函数,看一下函数输出的内容。
那么如何拿到对象的前四个字节呢,我们知道只有相近类型才能够强制类型转换,Derive怎么转为int呢,显然没办法转。但是C语言存在一个类似BUG的行为,指针可以任意转换。
所以就可以取d的地址,然后强转为int*,然后解引用就可以拿到前四个字节了。但是此时具有的是int属性,即为整形。所以还需要一次强转,转为函数指针的地址。所以整个转化就是void(**p)() = (void(**)())(*((int*)(&d)));
此时p是函数指针的地址,所以进行解引用就好了,一次解引用是函数指针,两次解引用就是函数自己了。然后调用即可。
输出即为
所以就证明了,虚函数表中存在另外两个函数。即得,只要是虚函数,就会写入虚函数表。
直接使用函数指针会比较麻烦,所以可以是使用宏,typedef void(*VFPTR)();
整体代码即为:
typedef void(*VFPTR)();
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;
};
void ptintfunc(VFPTR* vfpptr)
{for (int i = 0; i < 4; i++){printf("%p->", vfpptr[i]);(*vfpptr[i])();//printf("\n");}
}
int main()
{Base b;Derive d;VFPTR* pptr = (VFPTR*)(*(int*)(&d));ptintfunc(pptr);return 0;
}
抽象类
在虚函数的后面写上 = 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();
}
假如基类为一个抽象的对象,没有什么特征可以描述便可以使用抽象类,这样使用抽象类的指针,便可以调用派生类重写的函数。
多继承中的虚函数表
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) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);Base2* ptr = &d;VFPTR* vTableb2 = (VFPTR*)(*(int*)ptr);//VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}
如上代码,Derive继承了Base1和Base2,那么子类会有两个虚表指针,指向两个虚表,重写会在两个虚表中都完成重写。但是类似func3()
函数,在父类中没有的虚函数只会在第一个虚表中。如果在Base2中存在虚函数func3()
,那么只会在第二个虚表中重写,第一个虚表中并不会存在。