多态代码如何写
总结:
- 父类用指针或引用指向子类(不用不行)
- 子类进行虚函数重写
class Person
{
public:
virtual void f()
{
cout << "person" << endl;
}
};
class Stu : public Person
{
public:
virtual void f()//子类的virtual可以不写,但是推荐写上
{
cout << "student" << endl;
}
};
int main()
{
Stu s;
Person& p = s;
p.f();
}
协变和析构函数的重写
总结:能看和讲明白的可以直接跳过。不明白的建议写代码验证。
- 知道有一种特定情况下重写虚函数返回值可以不同即可,叫协变。没什么用,我们自己不要写协变。
- 析构函数写成虚函数并且必须重写,原因是对于指针类型,它们调用delete本质也是一种多态,父类delete时要调用父类的析构,子类delete时要调用子类的析构。不重写的话,子类有部分成员由于没有调用自己的析构,会造成内存泄漏。
关于协变的讲解:
重点是返回值的类型必须是继承关系且必须是指针或引用
正常来讲虚函数和重写的虚函数的返回值应该是一样的,但是协变是不同的。基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
关于析构函数的重写,不明白的用以下代码验证(知道这个就够了):
不重写情况:
class Person
{
public:
virtual void f()
{
cout << "person" << endl;
}
~Person()
{
cout << "~person" << endl;
}
};
class Stu : public Person
{
public:
virtual void f()
{
cout << "student" << endl;
}
~Stu()
{
cout << "~Stu" << endl;
}
};
int main()
{
Person* p = new Person();
Person* q = new Stu();
delete p;
delete q;
}
此时p调用了person的析构,q也调用了person的析构。
对于q而言,stu的父类部分被析构完了,子类部分没有析构,造成内存泄漏。不行!!!
class Person
{
public:
virtual void f()
{
cout << "person" << endl;
}
virtual ~Person()
{
cout << "~person" << endl;
}
};
class Stu : public Person
{
public:
virtual void f()
{
cout << "student" << endl;
}
virtual ~Stu()
{
cout << "~Stu" << endl;
}
};
int main()
{
Person* p = new Person();
Person* q = new Stu();
delete p;
delete q;
}
此时p调用了person的析构,q调用了stu的析构。
此时由于q调用了stu的析构函数,student对象被完整释放了,没有内存泄漏。
final和override(C++11)
总结:
- final的作用是不允许父类这个虚函数被重写。写在虚函数最后面
- override的作用是检查子类是否完成了父类的基函数重写,也是写在虚函数最后面,如果没有重写直接报错**(用意是以免出现你想重写虚函数但是由于参数顺序写错了或者其他粗心的原因导致重写变成隐藏)**
重载,隐藏(重定义),覆盖(重写)
知道图上的信息即可跳过,没必要扣那么严格。
接口类(抽象类)
总结:如果不懂得话自己写代码验证
- 有纯虚函数的类叫接口类
- 接口类不能实例出对象
- 继承接口的子类必须实现接口函数,不实现子类也无法实例出对象
多态原理
这里不可以跳过,直接全文阅读。下面都是一些八股结论,背住即可。
1.多态是在对象种加了一个指针,指向一个数组,这个数组里面放的是函数指针。这个指针在C++里面叫虚表指针。这个数组在C++里面叫虚表(虚函数表,用来放虚函数的地址)。注意:有虚函数才有虚表,没有虚函数没有虚表。
验证结论:
代码:
class Person
{
public:
virtual void f()
{
cout << "person" << endl;
}
int a;
};
class Stu : public Person
{
public:
virtual void f()
{
cout << "student" << endl;
}
int b;
};
int main()
{
Person p;
Stu s;
}
画图解释:
2.为什么重写也叫做覆盖?
这个没办法验证,但是可以解释原因。子类继承父类,会把父类的虚表拷贝一份。(你可以试一下子类什么也没有,父类三个虚函数,看子类的虚函数表是否和父类是一样的)。C++语言层面的重写,其实就是相当于把虚表里面的函数指针改掉。所以叫重写。
3.总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
4.虚表是存在代码段的,你可以尝试验证一下。只要打印出所有区域的地址,比较一下即可。(不放代码了,复习的时候这个实验没必要做,很简单)
5.多态的调用条件之一为什么一定是指针或者引用?不能是对象。
原因是:子类给父类切片的时候,不会把虚表指针切过去,只会进行普通的赋值。因此如果用对象的话,这个对象连虚表指针都没有,更别谈调用函数了。(这是语言的设计,记住对象切片的时候不会切虚表指针即可。)
6.多态调用过程和普通函数调用过程的对比
多态调用过程:先找虚表,再找虚表里面的函数指针,然后调用
普通函数调用过程:直接调用。
直接区别就在于两个不同方式的调用汇编代码不同。
7.动态绑定和静态绑定
动态绑定就是说多态是动态的,在运行阶段的时候才去找对应的函数。
静态绑定,如函数重载,就是编译(把代码变成汇编)的过程已经决定好调用什么函数了。
8.多继承的多态原理
和单继承没有任何差别,继承谁的,虚表就拷贝谁的,如果重写了就直接覆盖对应的函数指针即可。需要注意有一个点:多继承子类自己的虚函数会放在第一个父类的虚表中。
多态题目
- 什么是多态?
从两个角度答:静态多态和动态多态。分别分析一下。 - 什么是重载覆盖隐藏?
把上面那张图复述一遍即可。 - 多态的实现原理?
把上面多态原理的第一点重点讲一下即可。 - inline函数可以是虚函数吗?
可以。如果inline函数是虚函数了,它的inline性质就没有了,就是普通函数了。 - 静态成员函数可以是虚函数吗?
不可以。因为静态成员函数没有this指针,对象没有办法直接调用它。必须通过类域来调用。对象调用不了,就找不到对象里的虚表指针,因此不可以。(简单来说就是静态成员函数没有this指针,找不到虚表指针,因此不行) - 构造函数可以是虚函数吗?
不可以。因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的。调用虚函数的时候要先有对象,可是调用构造函数的时候连对象都没有。先有对象再有虚函数指针,因此构造函数不可能是虚函数。 - 析构函数可以是虚函数吗?
必须可以。父类指针可能new的是父类对象,可能new的是子类对象。父类对象要调用父类的析构,子类要调用子类的析构。上面那两张图画出来就完美解答了。 - 访问普通函数快还是虚函数更快?
如果构成多态,就要去虚表里面找指针。这时候普通函数快。如果没有构成多态,两者速度一样。都是直接调用。(不是说是虚函数就一定要去虚表里面找指针再调用,构成多态才会这样)
由于p不是指针或者引用,不构成多态,还是普通调用,即使f1是虚函数。
- 虚表是什么阶段生成的,存在哪里?
虚表是编译阶段生成的,一般情况下存在代码段。(虚表指针才是构造函数初始化之后生成的) - C++菱形继承和虚继承的原理?
参考继承,重点讲虚基表和偏移量
11.什么是抽象类?抽象类的最大作用是什么?
抽象类就是接口。抽象类最大的作用就是强迫子类重写虚函数(接口),不重写就没有办法实例化对象。