C++之多态详解!
多态的概念
多态的概念简单来说就是多种形态,在语言里面我们一般完成某个动作就是去调用某个函数,而当不同的对象去完成同一个动作的行为是不一样的即不同的对象使用同一个函数的执行的方法也是不一样的!(和函数重载有点相似,函数重载也有被叫做是静态的多态!——静态就是在编译时期完成的多态!)而我们下面要说的也可以说是动态的多态!
多态的定义以及实现
虚函数与虚函数的重写
==虚函数:即被virtual修饰的类成员函数称为虚函数。==
多态的定义
多态是在==不同继承关系的类对象==,==去调用同一函数==,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通==过基类的指针或者引用==调用虚函数
- ==被调用的函数必须是虚函数==,且派生类必须==对基类的虚函数进行重写==
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 p;
Student s;
func(p);
func(s);
//父类指针或者引用因为赋值兼容转换是可以接收子类的对象的!
//因为会发生切片!不会发生类型转换!
//s传过去后就不是Student类了!发生了切片!是p是Student里面的Person类的一部分!
//p都是Person类!但是指向的对象不同
return 0;
}
此时就发生了多态!因为已经满足了多态的条件!完成了虚函数的重写!也是由父类的引用或者指针调用!
==一般的类成员调用:与调用对象的类型有关!是Person就是调用Person的函数,是Student就是调用Student的函数!==
==多态调用:跟指针或者引用指向的对象有关!指向父类就是调用父类的虚函数,指向子类就是调用子类的虚函数!==
void func(Person p)
{
p.BuyTicket();
}
//如果不给引用调用的都是父类的函数!
void func(Person* p)
{
p.BuyTicket();
}
//指针调用也构成多态!
class Person
{
public:
void BuyTicket()//破坏任意一个的条件都会导致虚函数失效
{
cout << "全价票" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "半价票" << endl;
}
};
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价票" << endl;
}
};
class Student :public Person
{
public:
void BuyTicket()//这个virtual可以删掉!因为基类的三同函数如果是虚函数那么继承的子类的三同函数默认是虚函数!
{
cout << "半价票" << endl;
}
};
析构函数的虚函数
class Person
{
public:
~Person()
{
cout << "~Person delete :" <<_p <<endl;
delete _p;
}
protected:
int* _p = new int[10];
};
class Student :public Person
{
public:
~Student()
{
cout << "~Student delete :" <<_s <<endl;
delete _s;
}
protected:
int* _s = new int[20];
};
int main()
{
Person p;
Student s;
return 0;
}
首先是Student类对象s的==子类析构先调用释放子类那一部分的资源==,然后==自动的去调用父类的析构Student类中父类的那一部分的资源==!
然后是父类对象p调用父类的析构释放父类的资源! 这个程序看起来都没有什么问题,
但是我们一般都是建议在==父类的析构函数前面加上virtual将其变成虚函数!==
为什么?看起来没有增加虚函数我们也是可以正常释资源的!
但是如果我们看下面的场景
int main()
{
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
return 0;
}
class Person
{
public:
virtual ~Person()
{
cout << "~Person delete :" <<_p <<endl;
delete _p;
}
protected:
int* _p = new int[10];
};
class Student :public Person
{
public:
~Student()//子类仍然可以不加virtual!
{
cout << "~Student delete :" <<_s <<endl;
delete _s;
}
protected:
int* _s = new int[20];
};
int main()
{
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
return 0;
}
==我们可以看到这样子delete就可以正常的调用子类的析构函数了!==
所以我们写父类的时候推荐无脑的对析构函数加上virtual!这样子无论什么情况都不会出错!基本上是没有什么坏处的!
重载,覆盖(重写),隐藏(重定义)的对比——面试会考
override与final——C++11新增
这两个关键字是C++11新增的有关虚函数的关键字!
为什么要增加这两个关键字?
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:==++11提供了override和final两个关键字,可以帮助用户检测是否重写。==
final
final既可以在==类后面也可以加在虚函数==后面!——分别有两个作用!
我们该如何实现一个==不能被继承的类?==
- 构造私有——因为子类的成员必须调用==父类的构造==进行初始化!一旦私有就不能被子类使用了!——这是C++98的方式
- 在类定义的时候加上final!—— C++11的方式
==这个后面加上final的类我们叫做最终类==
//方法1
class A
{
private:
A()//构造私有
{}
//析构私有的话是可以new出来一个子类的对象!但是只是无法调用delete!
};
class B :public A
{};
//方法2
class C final
{
public:
C()
{}
};
class D:public C
{};
int main()
{
B bb;
D dd;
}
==final:修饰虚函数,则表示虚函数不能再被重写!==
class Car
{
public:
virtual void Drive() final
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-good" << endl;
}
};
overrider
overrider是用来==检查子类是否完成重写==!
如果没有完成重写!那么就会直接报错!——==是放在子类的函数后面!==
==一般的实际作用就是我们在父类增加了某些东西!不确定是否发生了重写!于是可以加上进行检查!==
class Car
{
public:
virtual void Drive(int i)
{}
};
class Benz :public Car
{
public:
virtual void Drive()override
{
cout << "Benz-good" << endl;
}
};
int main()
{
return 0;
}
抽象类
概念
在==虚函数的后面写上=0==,则这个==函数为纯虚函数==。包含==纯虚函数的类叫做抽象类(也叫接口类)==,抽象类==不能实例化出对象==。==派生类继承后也不能实例化出对象==,只有==重写纯虚函数==,派生 类才能实例化出对象。==纯虚函数规范了派生类必须重写==,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};
class BNW : public Car//继承了抽象了类的派生类
{
};
int main()
{
Car c;
BNW b;
return 0;
}
==只要派生类继承了抽象类也有了纯虚函数!也变成了抽象类!==
==基类是派生类本身也无法实例化对象!==
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};
class BNW : public Car
{
public:
void Drive()//可以加virtual也可以不加!
{
cout << "BNW ——Drive" << endl;
}
};
==只有重写了虚函数才能实例化!==
override与抽象类的区别!——override你不重写就在编译期会直接报错!但是抽象类你只要不实例化放在那边不管就不会报错!
==抽象类就像是一种概括!例如:什么品牌的车都可以是车,所以这个类也只要提供接口机就好了!不用这个类的对象!==
接口继承与实现继承
==普通函数的继承是一种实现继承==,派生类继承了基类函数,可以使用函数,==继承的是函数的实现==。
——就是说接口还是我自己的!
==虚函数的继承是一种接口继承==,派生类继承的是==基类虚函数的接口,目的是为了重写,达成多态,继承的是接口==。所以如果不实现多态,不要把函数定义成虚函数。——接口是来自父类的!
所以这就是为什么子类即使不加virtual也依旧是虚函数!
下面这个例子就可以很好的看出什么叫接口继承!
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B :public A
{
public:
virtual void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B* p = new B;
p->test();
p->func();//此时不符合多态就是调用子类自己的!缺省值也是自己的!
return 0;
}
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test(int val = 100)
{
func();
}
};
class B :public A
{
public:
virtual void func(int val = 0)
{
cout << "B->" << val << endl;
}
virtual void test(int val = 10000)
{
func();
cout << val<<endl;
}
};
int main()
{
B* p = new B;
p->test();//这个不构成多态所以调用的是B的test接口!里面的this指针是B*类型的,func不符合多态调用
A* pp = new B;
pp->test();//构成多态接口继承!虽然接口继承了但是里面的this指针是B*类型的!,func不符合多态调用
return 0;
}
==想知道this指针是什么类型的!那就看实现的那一部分在那个定义域里面!接口继承了父类,如果实现是在子类那么this指针的类型也是子类!==
多态的原理
虚函数表
在介绍多态的原理之前我们先问一个问题
//你觉得Base的大小是多少呢?
class Base
{
public:
void virtual func()
{
cout << "func" << endl;
}
private:
int _b = 1;
char _ch;
};
int main()
{
cout << sizeof(Base);
return 0;
}
==首先 _b是int类型, _ch是char类型,一个是4字节+一个1字节!然后对齐之后应该是8字节的大小!但是这是怎么回事呢?会什么会是12字节?==
这就和多态的实现原理有关!
我们可以看一下这个类在监视窗口下面的样子
==我们可以看到最上面多了一个指针!(这是32位环境下面)==——_vfptr是virtual function 指针的简写!
这个指针指向的是虚表指针!指向的是虚表!——==虚表里面放的是虚函数的地址!有几个虚函数就放几个虚函数的地址!==
实现方式
我们发现了多了虚函数,那么就对象模型最上面就会多一个指向虚表的指针!由此我们可以推断多态的实现方式肯定和这个虚表有很大的关系!
class Base
{
public:
void virtual func()
{
cout << "Base::func1" << endl;
}
void func2()
{
cout << "Base::func2" << endl;
}
void virtual func3()
{
cout << "Base::func3" << endl;
}
private:
int _b = 1;
char _ch;
};
class Drive :public Base
{
public:
virtual void func()
{
cout << "Drive-func()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Drive d;
return 0;
}
接下来我们看派生类在这时候的对象模型!
==首先我们可以看到重写后的虚函数的地址!是完全不一样的!但是没有重写的地址确是一样的!父类与子类对象的虚表地址也是不一样的!==
==虚函数的重写也叫做覆盖!(重写是语法层面上的概念,而覆盖是底层原理上面的概念!)==
==我们还能发现只有虚函数会进表,非虚函数是不会进表的!==
==首先子类首先将父类的虚表拷贝一份过来,然后看那个虚函数完成了重写,就将虚表的那个地址位置覆盖成重写后的地址!==
那么多态到底是如何通过虚函数表来多态呢?
class Base
{
public:
void virtual func()
{
cout << "Base::func" << endl;
}
void virtual func2()
{
cout << "Base::func2" << endl;
}
void func3()
{
cout << "Base::func3" << endl;
}
private:
int _b = 1;
char _ch;
};
class Drive :public Base
{
public:
virtual void func()
{
cout << "Drive-func()" << endl;
}
void func3()
{
cout << "Drive::func3" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Drive d;
Base* ptr;
//普通调用——只看类型——普通调用又叫编译时/静态 绑定/决议
ptr = &b;
ptr->func3();
ptr = &d;
ptr->func3();
//多态调用——看对象——多态调用又叫运行时/动态 绑定/决议
ptr = &b;
ptr->func();
ptr = &d;
ptr->func();
return 0;
}
我们看一下以上代码的反汇编
==父类对象和子类对象中父类的那一部分在对象模型上面是没有任何差别的!都有虚表指针,也有自己的虚表==
==但是子类与父类的虚表里面的虚函数地址是有差别的!——如果是父类里面的虚表都是父类的虚函数,子类里面的虚表会将重写后的虚函数重写覆盖==
==所以多态的实现方式就是通过指向的对象的虚表指针!找到虚表然后通过虚表里面存的函数地址来找到要调用的虚函数!然后来调用!==
==这样虽然指向的对象他们的对象模型看上去没有差!但是他们的虚函数表里面的地址是不一样的!可以通过这个来找到要用的虚函数!==
这样子就做到了指向父类调用父类,指向子类调用子类!
虚函数表位置
我们已经说了怎么多有关虚函数表的内容,那么虚函数表在哪里呢?
我们可以用下面的程序来测试一下
int main()
{
int i = 0;
cout << "栈区:" << &i << endl;
int* ptr = new int;
cout << "堆区:" << ptr << endl;
const char* str = "hello world";
cout << "代码段/常量区:" << (void*)str << endl;
static int b = 10;
cout << "数据段/静态区:" << &b << endl;
Base B;
cout << "虚表:" << (void*)(*((int*)&B)) << endl;
//因为虚表指针是放在最顶端,所以我们可以将Base类型的地址强转为int* 然后解引用得到
//前面四个字节的地址然后解引用,获得这个虚表指针!因为解引用出来后是int类型的整数
//再次强转为一个指针类型!
}
补充
所有的父类都是公用一个虚表的!
所有的子类也是公用一个虚表的!
父类和子类虚表是不同的!
int main()
{
Base B;
Base B1;
Drive D;
cout << "虚表:" << (void*)(*((int*)&B)) << endl;
cout << "虚表:" << (void*)(*((int*)&B1)) << endl;
cout << "虚表:" << (void*)(*((int*)&D)) << endl
}
动态绑定和静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
多继承和单继承关系的虚函数表
单继承
class Base
{
public:
virtual void fun1()
{
cout << "Base::func1" << endl;
}
virtual void fun2()
{
cout << "Base::func2" << endl;
}
private:
int a;
};
class Drive :public Base
{
public:
virtual void fun1()
{
cout << "Deriv::func1" << endl;
}
virtual void fun3()
{
cout << "Drive::func3" << endl;
}
void fun4()
{
cout << "Drive::func4" << endl;
}
private:
int b;
};
int main()
{
Base b;
Drive d;
return 0;
}
这是在vs下面监视窗口b与d虚表里面的内容!如我们所料,fun1重写后地址改变,fun2没有重写地址不改变!但是有个问题?==那么func3呢?按理来说也是一个虚函数,它应该进子类的虚表不是吗?==
这个时候其实内存窗口就有点不准确了!应该看内存窗口
==记住以空地址结尾是vs下面的linux下面的虚表不是以nullptr结尾!==
==我们也可以自己把虚表打印出来!==
void PrintVFTable(VFPtr vft[])
{
for (int i = 0; vft[i]; i++)
{
printf("[%d]:%p\n",i,vft[i]);
//vtf[i]();//我们甚至可以直接调用这个函数!
}
}
int main()
{
Base b;
PrintVFTable((VFPtr*)*(int*)&b);
//*((int*)&b)是为了取得虚表地址!因为int*解引用是int类型的所以再次强转为VFTptr
//这是32位环境下面!所以用int如果是64位的那么就要使用longlong!
//其实我们也可以直接改成
//PrintVFTable((VFPtr*)*(void**)&b);
//void**是指针!在32位下解引用就是前4位的void*指针!,在64位下面解引用就是前8位!
cout << endl;
Drive d;
PrintVFTable((VFPtr*)*(int*)&d);
return 0;
}
==我们可以发现监视窗口和我们打印的是一致的!==
多继承
我们一直讨论的是单继承下面的虚表继承模型!那么多继承呢?
class Base1
{
public:
virtual void fun1()
{
cout << "Base1::func1" << endl;
}
virtual void fun2()
{
cout << "Base1::func2" << endl;
}
private:
int b1;
};
class Base2
{
public:
virtual void fun1()
{
cout << "Base2::func1" << endl;
}
virtual void fun2()
{
cout << "Base2::func2" << endl;
}
private:
int b2;
};
class Drive :public Base1 ,public Base2
{
public:
virtual void fun1()
{
cout << "Deriv::func1" << endl;
}
virtual void fun3()
{
cout << "Drive::func3" << endl;
}
private:
int d1;
};
typedef void(*VFPtr)();
void PrintVFTable(VFPtr vft[])
{
for (int i = 0; vft[i]; i++)
{
printf("[%d]:%p->",i,vft[i]);
vft[i]();
}
cout << endl;
}
int main()
{
Base1 b1;
Base2 b2;
PrintVFTable((VFPtr*)*(void**)&b1);
PrintVFTable((VFPtr*)*(void**)&b2);
return 0;
}
我们一直讨论的是单继承下面的虚表继承模型!那么多继承呢?
==多继承下面父类是没有任何改变的!还是原先的样子但是主要是子类!==
多继承的模型
int main()
{
Drive d;
PrintVFTable((VFPtr*)*(void**)&d);
Base2* ptr2 = &d;
PrintVFTable((VFPtr*)*(void**)ptr2);
return 0;
}
==我们发现fun3其实是放在第一个虚表里面!==
==多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中==
菱形继承与菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的 模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看 了,一般我们也不需要研究清楚,因为实际中很少用。所以我们这里只是简单的解释一下!
对于菱形继承
class A
{
public:
virtual void func1()
{
}
public:
int _a;
};
class B : public A
{
public:
virtual void func1()
{
}
public:
int _b;
};
class C : public A
{
public:
virtual void func1()
{
}
public:
int _c;
};
class D :public B, public C
{
public:
virtual void func1()
{
}
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;
}
==其实菱形继承的结构一般没有太大的变化和多继承就是一样的!==
==也就是分别都多了一个虚表!==
菱形虚拟继承
class A
{
public:
virtual void func1()
{
}
public:
int _a;
};
class B :virtual public A
{
public:
virtual void func1()
{
}
public:
int _b;
};
class C :virtual public A
{
public:
virtual void func1()
{
}
public:
int _c;
};
class D :public B, public C
{
public:
virtual void func1()
{
}
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;
}