一、虚函数、虚函数表、虚表指针、覆盖
1、虚函数
在成员函数前面加 virtual 后,该函数就成为虚函数,此时该类就会像虚继承一样多了一个虚表指针(虚函数表指针、虚指针)
2、虚函数表
虚表指针指向的是一张属于该类一张表格的首地址,该表格中记录了该类中所有虚函数的首地址
((void(*)(void))(**(int**)b))();
如果类中没有其他成员变量(类的首地址即虚表指针的地址),可以直接通过虚函数表以及虚表指针来访问虚函数表中第一个虚函数void func(void)
3、覆盖(重写)(构成多态的基础)
当使用virtual关键字修饰父类的成员函数时,此时父类中就会多一个虚表指针以及一张虚函数表,子类继承父类时会把父类的虚表指针以及虚函数表一起继承过来,然后编译器会去比较父子类之间同名的虚成员函数的格式,如果格式完全相同的虚函数,就会把子类中虚函数表中原来父类虚函数的地址改为子类中同名函数的地址,此时就称为覆盖
此时使用父类指针或引用指向子类对象时,调用虚函数则会根据被覆盖后的虚函数表中所指向子类的同名且格式相同的成员函数,不再调用父类的同名函数
4、构成覆盖的条件
①子类以public继承父类
②父类中被覆盖的函数必须是virtual修饰的虚函数
③子类中必须有与父类虚函数同名的成员函数,且该函数的返回值、参数列表、常属性都必须相同
④返回值类型相同,或者子类同名成员函数的返回值类型可以向父类虚函数的返回值类型做隐式转换,且有继承关系
重载、覆盖、隐藏、重写的区别?
隐藏:
①父子类中,如果同名且格式不相同(只有返回值不同会报错:由于派生类重写基类的虚函数,但是返回类型不一致导致的),无论是否有virtual修饰都构成隐藏
②父子类中,如果同名且格式相同,如果没有virtual修饰则构成隐藏
③隐藏可以隐藏同名成员变量、成员函数,但是覆盖只能覆盖满足条件的成员函数
④在父子类中,同名成员函数要么构成隐藏、要么构成覆盖
⑤除去父子类外,其他的不同作用域下同名标识符之间也构成隐藏关系
二、多态
1、什么是多态
指的是一条指令可以有多种形态,当调用一个指令时,它能够根据参数、环境的不同作出不同的操作,这种情况称为多态
C++中根据确定指令具体操作的时间划分多态:
编译时多态、运行时多态
2、编译时多态
当调用函数重载版本时,编译器会根据参数的类型、个数等确定调用的是哪个版本的重载函数,这就是所谓的编译时多态,还有例如泛型编程中的模板技术等
3、运行时多态
当子类覆盖了父类的同名函数,然后使用父类指针或引用去访问虚函数,该父类指针或引用既可能指向父类对象、也可能指向子类对象,所以调用的是父类还是子类的同名函数在编译期间无法确定,需要在运行期间才能确定,称为运行时多态(必须发生覆盖)
构成运行时多态的条件:
①父子类以public继承
②子类中有对父类成员函数构成覆盖
③运行时才能确定父类指针或引用指向的是父类还是子类的对象
三、虚构造和虚析构
1、虚构造
C++中不允许构造函数为虚函数
假设构造函数可以定义为虚函数,那么此时子类的构造函数就会自动覆盖父类的构造函数,当创建子类对象时,会执行子类的构造函数,但是按照执行顺序会执行父类的构造函数,而此时父类的构造函数已经被覆盖成子类的构造函数,又重新回来形成死循环,所以编译器禁止把构造函数定义为虚函数
其次构造函数的使命就是去把类中的成员创建,包括虚函数表、虚表指针,因此如果把构造函数定义为虚函数,但是构造函数都没有执行成功就没有所谓的虚函数表、虚表指针,因此想要把构造函数放入虚函数表就是悖论
2、虚析构
析构函数可以定义为虚函数
当使用类多态,通过父类指针或引用去释放子类对象时,如果析构函数不是虚函数,那么只会执行父类的析构函数,但是由于在创建子类对象时,一定执行了子类的构造函数,如果在子类构造函数中申请了资源,此时对象销毁时没有调用子类的析构函数,就会导致内存泄露
只有把父类的析构函数定义为虚函数,通过父类指针或引用去释放子类对象时,会先调用子类的析构函数(覆盖),当子类的析构函数执行结束后(子类已经完成释放),按照要求对象的释放顺序,会自动执行父类的析构函数,此时子类已经被释放完,所以调用父类的析构函数不会再继续因为覆盖而去执行子类的析构函数,因此不会造成内存泄露
3、总结
当使用多态时且子类的析构函数中有需要释放的资源,此时父类中就必须设置为虚析构
四、纯虚函数、抽象类、纯抽象类
1、纯虚函数的格式
virtual 返回值 函数名(参数列表) = 0;
①纯虚函数可以不去实现,一般人也没必要去实现
②父类中如果有纯虚函数,那么继承该父类的子类必须对其进行覆盖,否则无法创建对象
③有纯虚函数的类不能创建对象
④纯虚函数就是为了强制子类去覆盖,为了强制子类实现某些功能
⑤有纯虚函数的类都称为抽象类
⑥析构函数可以设置为纯虚函数,但是必须在类外定义
2、纯抽象类
所有的成员函数都是纯虚函数的类,称为纯虚函数,这种类一般用于设置功能接口,所以也称为接口类
五、工厂模式
工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
#include <iostream>
using namespace std;
enum Phone
{
C_Huawei,C_Xiaomi,C_Apple,C_Oppo
};
class Factory
{
public:
virtual void makephone(void) = 0;
};
class Huawei : public Factory
{
public:
void makephone(void)
{
cout << "华为手机" << endl;
}
};
class Xiaomi : public Factory
{
public:
void makephone(void)
{
cout << "小米手机" << endl;
}
};
class Apple : public Factory
{
public:
void makephone(void)
{
cout << "苹果手机" << endl;
}
};
class Oppo : public Factory
{
public:
void makephone(void)
{
cout << "欧泊手机" << endl;
}
};
Factory* create_phone(Phone phone)
{
switch(phone)
{
case C_Huawei: return new Huawei;
case C_Xiaomi: return new Xiaomi;
case C_Apple: return new Apple;
case C_Oppo: return new Oppo;
}
}
int main(int argc,const char* argv[])
{
Factory* phone = create_phone(C_Xiaomi);
phone->makephone();
}