文章目录
- 简易抽象理解多态
- 多态的具体实现
- 虚函数的定义
- 虚函数的重写
- 重定义(隐藏)、重载 、重写(覆盖)区别
- C++11 override 和 final 关键字
- 抽象类的定义
- 接口继承和实现继承
- 多态的原理:虚函数表
- 单继承和多继承关系的虚函数表
- 动态绑定与静态绑定
简易抽象理解多态
不同的对象做同一件事产生了不同的结果。
多态的具体实现
多态实现的条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
例:
#include <iostream>
class Base {
public:
virtual void func() {
std::cout << "Base::func()" << std::endl;
}
};
class Derived : public Base {
public:
//注意此处func前虽可以不加virtaul,但为了规范最好加上
void func() override {
std::cout << "Derived::func()" << std::endl;
}
};
int main()
{
Base* basePtr_1 = new Derived();
Base* basePtr_2 = new Base();
basePtr_1->func(); // "Derived::func()"
basePtr_2->func(); // "Base::func()"
}
虚函数的定义
即被virtual修饰的类成员函数称为虚函数。
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
存在两种特殊的虚函数重写
-
协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。 -
析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
重定义(隐藏)、重载 、重写(覆盖)区别
重定义(又名隐藏) 条件:
1.两函数分别在基类和派生类中。
2.函数名相同,但不构成重写。
重载 条件 :
0.函数名称必须相同。
1.在同一作用域。
2. 函数参数类型、个数或顺序至少有一个不同。
3. 函数返回类型可以相同也可以不同。
4. 函数重载不能仅仅依靠参数的名称或者参数的默认值来区分。
5. 函数重载不能仅仅依靠函数的访问权限或者静态性质来区分。
重写(又名覆盖)条件:
1.两函数分别在基类和派生类。
2. 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同)(由于虚函数表的原因,虚函数表是个函数指针数组)
C++11 override 和 final 关键字
override 和 final 关键字主要用于规范重写。
- final:修饰虚函数,表示该虚函数不能再被重写
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
具体使用方法:在虚函数参数列表后添加关键字即可
抽象类的定义
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。(接口包括 函数的返回类型 函数名 形参 )
接口继承是指派生类只继承了基类的接口(也就是纯虚函数),而没有继承基类的实现。这种方式使得派生类必须实现基类中的所有纯虚函数,从而使得派生类和基类的实现是分离的,实现了接口和实现的分离。这种继承方式常常用于实现抽象类和接口,强制要求派生类实现接口中的所有函数。
多态的原理:虚函数表
在C++中,虚函数是一种特殊的成员函数,它可以被子类重写,实现多态性。为了实现虚函数的动态绑定,C++编译器会为每个包含虚函数的类生成一个虚函数表(vtable),也称为虚表。
虚函数表是一个指针数组,其中每个指针指向一个虚函数。每个包含虚函数的类都有一个虚函数表,其中包含该类的所有虚函数的地址。当一个对象被创建时,它会包含一个指向其类的虚函数表的指针。当调用一个虚函数时,编译器会使用该对象的虚函数表来查找正确的函数地址。
关于虚函数表/虚表指针 的相关细节:
1.虚函数表是存储在全局数据区 。
2.虚表指针是指向虚函数表的指针。
3.virtual关键字给的是类中的虚函数,而不是虚表指针。虚表指针是由编译器自动生成的
4.虚表是在编译时由编译器生成的,通常存储在只读数据段中,因此在程序运行时是不允许修改虚表的。
5.满足多态条件以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的(通过 虚表指针-> 虚表 -> 虚函数)。不满足多态的函数调用时编译时确认好的。
单继承和多继承关系的虚函数表
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
以下是一个静态绑定的示例代码:
#include <iostream>
class Base {
public:
void print() {
std::cout << "Base class." << std::endl;
}
};
class Derived : public Base {
public:
void print() {
std::cout << "Derived class." << std::endl;
}
};
int main() {
Base base;
Derived derived;
Base* ptr1 = &base;
ptr1->print(); // 静态绑定,调用Base类的print函数
Base* ptr2 = &derived;
ptr2->print(); // 静态绑定,调用Base类的print函数
return 0;
}
以下是一个动态绑定的示例代码:
#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base class." << std::endl;
}
};
class Derived : public Base {
public:
void print() {
std::cout << "Derived class." << std::endl;
}
};
int main() {
Base base;
Derived derived;
Base* ptr1 = &base;
ptr1->print(); // 动态绑定,调用Base类的print函数
Base* ptr2 = &derived;
ptr2->print(); // 动态绑定,调用Derived类的print函数
return 0;
}