1. 虚函数的定义
允许派生类重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类或派生类的同名函数
1.1 动态绑定(动态联编)
函数的运行版本由实参决定,直到运行的时候才知道调用了哪个版本的虚函数。
动态绑定只有通过指针或引用调用虚函数时才会发生
Quote base = derived;
base.net_price(20);
//在这里只调用Quote的net_price
switch、if也属于动态联编
2. 虚函数的构造
2.1 派生类中虚函数的构造
派生类中虚函数的参数列表,函数名必须相同,返回类型在大多情况下是相同的(当返回类型为基类时,派生虚函数返回类型为自己的派生类)
2.2 override/final说明符
使用override关键字来修饰派生类中的虚函数,表示该函数并没有覆盖已存在的虚函数,如果用户强制覆盖会报错。
struct B{
virtual void f1() const;
virtual void f2();
};
struct D:B{
void f1() const override;
void f2(int) override;//编译器会报错,因为B没有f2(int)这样的函数
}
如果用final关键字修饰虚函数,那么它的派生类如果要覆盖该函数会出错
struct B{
virtual void f2() final;
};
struct D:B{
void f2();//出错,因为f2已经声明为final
}
2.2.1 重载/重写/覆盖
- 重载(overload):同一个访问区内被声明的几个具有不同参数列的同名函数,在传入数据时,根据实参选择相应的形参的函数,不关心返回值类型
class A{
public:
void test(int i);
void test(double i);
void test(int i, double j);
void test(double i, int j);
};
- 重写(override):派生类中需要重写的函数,它的返回值,参数列表,函数名都必须与被重写的基类相同,只有函数体不同。并且基类中的被重写函数必须加上virtual。派生类在调用该函数时,就只会调用自己重写的函数,不会调用基类的函数(其实也相当于覆盖了)
class A{
public:
virtual void fun3(int i){
cout << "A::fun3() : " << i << endl;
}
};
class B : public A{
public:
virtual void fun3(double i){
cout << "B::fun3() : " << i << endl;
}
};
- 覆盖:派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
class A{
public:
void fun1(int i, int j){
cout << "A::fun1() : " << i << " " << j << endl;
}
};
class B : public A{
public:
//隐藏
void fun1(double i){
cout << "B::fun1() : " << i << endl;
}
};
2.3 回避虚函数机制
有时候我们在派生类的虚函数的函数体中想要调用基类的虚函数,那么这时候就一定要加上作用符限定,否则就会调用派生类的虚函数,造成无限循环
3. 纯虚函数
当设计者不希望创建一个基类对象,因为基类里面的函数是没有意义的,那么可以将基类的该函数定义为纯虚函数,那么我们将这些含有纯虚函数的基类称为抽象基类。
3.1 抽象基类
3.1.1 为什么有抽象基类
因为纯虚函数不能被调用,所以包含纯虚函数的类是无法实例化的,那么这时候就出现了一个抽象类,它作为多个子类的共同基类,就相当于给多个子类提供一个公共的接口,我们可以通过定义这个公共接口的指针或引用,指向派生类的某个对象,这样就可以通过它来访问派生类对象中的虚函数。
3.1.2 抽象基类的几个要点
- 抽象基类负责定义接口,后续派生类可以覆盖接口,实现该接口。
- 抽象基类无法实例化。
- 如果基类定义多个纯虚函数,子类没有一一将纯虚函数实现,那么子类依旧也会被认为是抽象类。
4. 虚函数表
4.1 虚函数表是如何实现的
先思考一个问题,编译器是在什么时候实现不同对象能调用同名函数绑定关系的?
在创建对象的时候,编译器偷偷给对象加了一个vptr指针。只要我们类中定义了虚函数,那么在实例化对象的时候,就会给对象添加一个vptr指针,类中创建的所有虚函数的地址都会放在一个虚函数表中,vptr指针就指向这个表的首地址。
4.2 在构造函数中定义虚函数会出现什么情况?
看以下代码,思考一下此时虚函数的调用
class Parent{
public:
Parent(int a=0){
this->a = a;
print();}
virtual void print(){cout<<"Parent"<<endl;}
private:
int a;
};
class Son:public Parent{
Son(int a=0,int b=0):Parent(a){
this->b = b;
print();}
virtual void print(){cout<<"Son"<<endl;}
};
void main(int argc, char const *argv[]){
Son s;
return 0;
}
两个类中构造函数中,都只会调用自己类中的print()函数。
为什么呢?因为Son对象在实例化时,先调用基类构造函数,存在虚函数,将vptr指向基类的虚函数表,调用派生类构造函数,存在虚函数,将vptr指向派生类的虚函数表。所以都只会调用自己类中的虚函数。
如果子类重写了父类的某一虚函数,那么父类的该虚函数就被隐藏,无论以后怎么调用,调用同名虚函数调用的都是子类虚函数
为什么析构函数经常定义为虚析构函数
虚析构函数:只有当一个类被定义为基类的时候,才会把析构函数写成虚析构函数。
为什么一个类为基类,析构函数就需要写成虚析构?
假设现在有一个基类指针,指向派生类。此时释放基类指针,如果基类没有虚析构函数,此时只会看指针的数据类型,而不会看指针指向的数据类型,所以此时会发生内存泄露。
4.3 虚继承
4.3.1 为什么会使用虚继承
当类D继承于类B与类C,而类B与类C又继承于公共基类类A,为了避免基类多重拷贝,我们就让类B与类C虚继承于类A
4.3.2 底层原理
每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)
vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间
一个非常容易错的地方!
class parent
{
public:
virtual void output();
};
void parent::output()
{
printf("parent!");
}
class son : public parent
{
public:
virtual void output();
};
void son::output()
{
printf("son!");
}
son s;
memset(&s , 0 , sizeof(s));
parent& p = s;
p.output();
猜一猜上面会输出什么呢?
会编译出错!!!
为什么呢?
memset会将s所指向的某一块内存中的每个字节的内容全部设置为ch指定的ASCII值
虚函数链表地址也清空了, 所以p.output调用失败。 output函数的地址编程0