本文涉及的指针都是 4bytes 。如果要其他平台下,部分代码需要改动。比如:如果是 x64 程序,则需要考虑指针是 8bytes 问题 等等。
什么是多态?
举个例子:比如 买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人买票时是优先买票。同样的行为被不同的对象执行,效果不一样。再比如,对于不同的动物我们封装成类,有一个接口是 叫() , 不同的动物调用这个接口,就会有不同的效果。多态是基于继承的概念:
在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了Person 。 Person 对象买票全价, Student 对象买票半价。
介绍多态之前,先学习几个概念:虚函数 重写
1.虚函数
被 virtual 修饰的类成员函数称为虚函数。虽然都用virtual,但是虚函数和虚拟继承没有过多联系。
虚拟继承语法:
虚函数语法:
class Person { virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class sudent :public Person { virtual void BUyTicket() { cout << "买票-半价" << endl; } };
virtual不能修饰全局函数。虚函数也只是基于类函数的定义。
使用方法就是在返回值类型前面加上一个virtual
2.虚函数的重写(覆盖)
虚函数的重写 ( 覆盖 ) : 派生类中有一个跟基类完全相同的虚函数 ( 即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同 ) ,称子类的虚函数重写了基类的虚函数
“不同的对象完成同一件事情,
得到的结果不同,这便是多态”
3.调用多态
多态的两个条件,缺一不可:
1. 必须通过基类的指针或者引用调用虚函数2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写可以观察Func发现,他的参数是一个父类的引用,所以可以传派生类st给func(会发生切片)
也可以传父类p给func。都调用了,但是发生多态,也就是编译器通过这个引用本来的大小
在使用两个BuuTicket之前,记得先用public修饰。
对于现在的我们来说,可以认为多态是一种智能的、有魔法的技能,两个条件都要成立才能使用这个技能。比如,如果不用基类的引用或指针,就放不出这个技能。
如果将参数Person& p 改成 Person p , 不管是基类还是派生类都会被当作基类,调用基类的BuyTicket() , 原因是你传进去的就是一个基类的引用,在多态的魔法消失后,只会把这里当普通基类处理。
或者是用指针也能完成,也能使用出具有魔法的多态。
如果指定内域的话,就不算多态,而是只会调用指定了内域中的内容。
特殊规定:只去掉派生类的virtual,依然是多态。
派生类此处的virtual的意思是重写该函数的实现,不同于父类不写virtual,父类不写virtual压根就不是虚函数,一定不满足条件。而派生类不写virtual,相当于就是把从父类继承的虚函数的声明拿下来,重写实现一份。
请注意,重写的是基类虚函数的实现,也就是虚函数的定义,而不是声明。
但是并不建议这种派生类不写virtual的写法,因为有可能有多个平等等级的派生类。
4.关于多态和隐藏
不满足多态的条件就是隐藏。
都写virtual的时候,可以理解为多态,也可以理解为一种特殊形态的隐藏。
都不写virtual的时候,连多态的最基本的条件都不满足,所以只能算隐藏。
例,请问以下代码能否使用多态?
虽然满足使用多态的条件:由基类的引用或是指针调该函数
但是这样也是不行的,必须在参数处就接受Person*或者Person&
5. 多态的特殊情况
协变:
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
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;}
};
当然,Person返回Person Student返回Student也可以。
重点:析构函数的重写(经常出现在面试题中)
上文中我们提到,继承中的析构函数都会被改名字为destructor 这也是为什么不需要我们在子类的析构函数中手动释放父类的原因之一。
如果基类的析构函数也为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor 。
基类的析构函数都建议设计成虚函数。
否则,如果出现下列情况:
父类的指针或者引用 来new 子类的对象 然后释放的时候是delete 父类的指针 。、
‘复习:delete是对free和析构函数的封装’
那么此时会调用父类的析构函数,子类当中如果有开空间的话,父类的析构函数就不能清理子类开出的空间,就会造成内存泄漏。
并且这种错误不会被报错,需要我们注意。
但如果我们是将基类的析构函数设计成了虚函数,不管子类的析构函数加不加virtual,都会形成多态,解决这个问题。
可以认为,子类在重写时不是必须加virtual也是为了这个设计的。
也自圆其说的解释了为什么要全部改名为destructor
都把函数名处理成destructor其实是为了填自己的坑,都处理成destructor才能满足虚函数重写的条件。一旦满足虚函数重写的条件,就能通过 “父类的指针或者引用” 这一条件,去使用多态,通过基类的指针或引用,该调用基类的析构就基类的析构,该调用派生类的析构就派生类的析构。
小结:
只有多态的时候才使用虚函数。不要乱使用虚函数,虚函数的使用也是会加大空间的占有的。后文会提到这一点。
6. 关键字:final 和 override 用于修饰函数:
final还能用于修饰类,final修饰的类不能被继承。
或者
还有没有办法能使一个类不能被继承?
将基类函数的构造函数写成private,也能间接使该基类不能被继承。
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
小结:
7 . 抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象 。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class car {
public:
virtual void drive() = 0;//既然是一个不希望被实例化的对象,那么其中的函数自然一般就不会被
//调用,所以一般都不写函数定义
};
无法实例化:
继承了抽象类的类,也包含纯虚函数,也不能实例化出对象。
8. 练习
以下代码的输出结果:
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;
}
A和B中的func是构成了重写虚函数的关系
再看调用,p先在子类内部找test(),找不到;然后就去父类内部找,找到了,调用test()
难点一:此时调用test()的是在父类里面,因此此时的this是A* , 而this调用了func()
A*作为父类指针调用了一个重写的虚函数,多态的条件齐了。
多态是:
所以调用的是子类的func。
难点二:但是因为重写是重写虚函数定义而非声明,缺省参数还是用的基类的声明中的缺省参数,所以答案是B->1
重写 虚函数的 实现 ; 派生类的重写是基类的声明和重新写的定义。
题目二:写出sizeof(Person)的结果。
按照补齐的规则,应该是8个字节,但实际运行显式是12个字节
因为虚函数需要放进虚函数表,会存一个函数的指针,一共12个字节。
可以观察到,除了_x和_c 还有一个虚函数表指针(v代表virtual,f代表function)
占四个字节,共12个。