🌈 一、多态的概念
- 多态就是多种形态,不同的对象去完成同样的行为时,会产生出不同的结果。
- 例:同样是买票,可能就分为 1.4 m 以下免费,1.4 m ~ 1.6 m 半票,1.6 m 以上全票,购票的对象不同,产生的结果也会不同。
🌈 二、多态的定义及实现
⭐ 1. 多态的构成条件
- 多态指的是不同继承关系的类对象,去调用同一个函数,产生了不同的结果,想要构成多态需要满足以下 3 个条件:
- 必须是在继承体系中。
- 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写。
- 必须通过父类的指针或引用调用虚函数。
⭐ 2. 虚函数的概念
- 被
virtual
关键字修饰的类成员函数被称为虚函数。 - 虽然虚函数和虚继承都用了
virtual
关键字,但它们之间没有任何关系。 - 虚函数的 virtual 是为了实现多态,而虚继承的 virtual 是为了解决菱形继承的数据冗余和二义性问题。
- 例:引用和取地址用的符号都是 &,但引用和取地址可没什么关系。
- 只有类的非静态成员函数可以加 virtual,普通函数或静态成员函数不能加。
虚函数定义样例
class person
{
public:
// 被 virtual 修饰的类成员函数
virtual void fun()
{
// ......
}
};
⭐ 3. 虚函数的重写
- 虚函数的重写也叫做虚函数的覆盖,如果子类中有一个和父类完全相同的虚函数 (返回值类型、函数名、参数类型均相同),此时就称子类的虚函数重写了父类的虚函数。
举个例子
- 让 student 和 solder 这两个子类重写父类 person 的虚函数。
// 父类
class person
{
public:
// 父类的虚函数
virtual void buy_ticket()
{
cout << "person 买票 - 全价" << endl;
}
};
// student 类是继承自 person 类的子类
class student : public person
{
public:
// 子类的虚函数重写父类的虚函数
virtual void buy_ticket()
{
cout << "student 买票 - 半价" << endl;
}
};
// solder 类是继承自 person 类的子类
class solder : public person
{
public:
// 子类的虚函数重写父类的虚函数
virtual void buy_ticket()
{
cout << "solder 买票 - 优先" << endl;
}
};
- 此时就可以通过父类 person 的指针 / 引用来调用虚函数 buy_ticket。
- 多态中,父类的指针 / 引用指向继承体系中的哪个对象,就调用哪个对象中的虚函数。
- 此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。
// 通过父类的 引用 调用虚函数
void Func(person& p)
{
p.buy_ticket();
}
// 通过父类的 指针 调用虚函数
void Func(person* p)
{
p->buy_ticket();
}
int main()
{
person p; // 普通人
student st; // 学生
solder sd; // 军人
cout << "---------- 通过父类的 引用 调用虚函数 ----------" << endl;
Func(p); // 买票 - 全价
Func(st); // 买票 - 半价
Func(sd); // 买票 - 优先
cout << "---------- 通过父类的 指针 调用虚函数 ----------" << endl;
Func(&p); // 买票 - 全价
Func(&st); // 买票 - 半价
Func(&sd); // 买票 - 优先
return 0;
}
⭐ 4. 虚函数重写的例外
- 子类要重写父类的虚函数,必须要保证子类虚函数的返回值类型、函数名、参数类型这 3 个都与父类的虚函数相同。
- 但也有两个例外 (太折磨了),这两个例外分别是协变和重写析构函数。
🌙 4.1 协变
- 父类的虚函数的返回值是父类对象 (该父类可以是任何继承体系中的父类) 的指针 / 引用。
- 子类的虚函数的返回值是子类对象 (该子类可以是任何继承体系中的子类) 的指针 / 引用。
举个例子
- 下列代码中,父类 person 中虚函数 fun 的返回值类型是父类 A 对象的指针,子类 student 中虚函数 fun 的返回值类型是子类 B 对象的指针。
- 此时也会认为子类 student 的虚函数重写了父类 person 的虚函数。
/* 由父类 A 和子类 B 构成的一套继承体现 */
// 父类
class A
{};
// 子类
class B : public A
{};
/* 由父类 person 和子类 student 构成的另一套继承体系 */
// 父类
class person
{
public:
// 返回另一个继承体系中的父类 A 的指针
virtual A* fun()
{
cout << "A* person::f()" << endl;
return new A;
}
};
// 子类
class student : public person
{
public:
// 返回另一个继承体系中的子类 B 的指针
virtual B* fun()
{
cout << "B* student::f()" << endl;
return new B;
}
};
- 此时再通过父类 person 的指针 / 引用调用虚函数 fun,依然能够完成多态。
- 让父类的指针 / 引用指向父类对象时,调用父类的虚函数,让父类的指针 / 引用指向子类对象时,调用子类的虚函数。
// 通过父类的 引用 调用虚函数
void func(person& p)
{
p.fun();
}
// 通过父类的 指针 调用虚函数
void func(person* p)
{
p->fun();
}
int main()
{
person p;
student stu;
cout << "---------- 通过父类的 引用 调用虚函数 ----------" << endl;
func(p); // 让父类的 引用 指向 父类 对象
func(stu); // 让父类的 引用 指向 子类 对象
cout << "---------- 通过父类的 指针 调用虚函数 ----------" << endl;
func(&p); // 让父类的 指针 指向 父类 对象
func(&stu); // 让父类的 指针 指向 子类 对象
return 0;
}
🌙 4.2 重写析构函数
- 如果父类的析构函数是虚函数,此时只要定义了子类的析构函数,无论是否加上 virtual 关键字,都会与父类的析构函数构成重写。
- 虽然父子类的析构函数名表面上看可能不一样,但编译后父子类的析构函数名都会被统一处理成
destructor()
。 - 当使用父类指针 / 引用指向父子类对象时,如果子类没有重写父类的析构函数,不以多态的形式调用析构函数,在使用 delete 释放子类对象时,就会调用到父类的析构函数。
举个例子
- 分别 new 出一个 person 父类对象以及一个 student 子类对象,这两个对象都用 person 父类指针指向。
- 再使用 delete 分别调用父类对象和子类对象中的析构函数。
// 父类
class person
{
public:
virtual ~person()
{
cout << "~person()" << endl;
}
};
// 子类
class student : public person
{
public:
virtual ~student()
{
cout << "~student()" << endl;
}
};
int main()
{
// 分别 new 一个父类对象和子类对象,并均用父类指针指向它们
person* p1 = new person;
person* p2 = new student;
// 使用 delete 调用析构函数并释放对象空间
delete p1; // 指向父类对象时,调用父类的析构函数
delete p2; // 指向子类对象时,调用子类的析构函数
return 0;
}
⭐ 5. C++11 中的 override 和 final
🌙 5.1 final 禁止父类的虚函数被子类重写
final
关键字用于修饰父类的虚函数,子类如果重写了父类中带有final
关键字的虚函数,则会编译报错。
// 父类
class person
{
public:
// 被 final 修饰,该虚函数不能再被重写
virtual void buy_ticket() final
{
cout << "person 买票-全价" << endl;
}
};
// 子类
class student : public person
{
public:
// 子类重写了父类中被 final 修饰的虚函数,报错
virtual void buy_ticket()
{
cout << "student 买票-半价" << endl;
}
};
int main()
{
person p;
student s;
person* pp = &p;
person* ps = &s;
pp->buy_ticket();
ps->buy_ticket();
}
🌙 5.2 override 强制子类重写父类的虚函数
override
关键字用于修饰子类的虚函数,子类中被 override 修饰的虚函数如果没有重写父类的虚函数,则会编译报错。
// 父类
class person
{
public:
virtual void buy_ticket()
{
cout << "person 买票 - 全价" << endl;
}
};
// 子类
class student : public person
{
public:
// 子类中被 override 修饰的虚函数如果不重写父类的虚函数,则会编译报错
virtual void buy_ticket(int i) override
{
cout << "student 买票 - 半价" << endl;
}
};
int main()
{
person p;
student s;
person* pp = &p;
person* ps = &s;
pp->buy_ticket();
ps->buy_ticket();
}
⭐ 6. 重载 VS 重写 VS 重定义
- 重载:两个函数在同一个作用域。两个函数的函数名相同,但参数不同。
- 重写 (覆盖):两个函数分别位于父类和子类的作用域中。两个函数的函数名、参数类型、返回值类型都必须相同。两个函数都必须是虚函数。
- 重定义 (隐藏):两个函数分别位于父类和子类的作用域中。两个函数的函数名相同。父类和子类中的两个同名函数如果不构成重写,就是重定义。
🌈 三、抽象类
⭐ 1. 抽象类的概念
- 在虚函数的后面写上 = 0,这个虚函数就成了纯虚函数,而包含纯虚函数的类叫做抽象类。
🌙 1.1 抽象类不能实例化出对象
// 抽象类(包含了纯虚函数的类被称作抽象类)
class car
{
public:
// 纯虚函数
virtual void fun() = 0;
};
int main()
{
car c; // 抽象类不能实例化出对象
return 0;
}
🌙 1.2 子类重写纯虚函数后才能实例化出对象
- 子类在继承抽象父类后,除非子类重写纯虚函数,否则子类不能实例化出对象。
// 抽象父类
class person
{
public:
// 纯虚函数
virtual void fun() = 0;
};
// 字类
class student : public person
{
public:
// 重写纯虚函数
virtual void fun()
{
cout << "student 买票 - 半价" << endl;
}
};
// 子类
class solder : public person
{
public:
// 重写纯虚函数
virtual void fun()
{
cout << "solder 买票 - 优先" << endl;
}
};
int main()
{
// 这两个子类都重写了抽象父类的纯虚函数,可以实例化出对象
student stu;
solder sol;
// 通过父类指针指向不同的对象
person* pstu = &stu;
person* psol = /
// 通过父类指针调用不同对象中的函数
pstu->fun(); // 调用 student 对象中的 fun 函数
psol->fun(); // 调用 solder 对象中的 fun 函数
return 0;
}
🌙 1.3 抽象类存在的意义
- 子类如果不重写从抽象父类继承下来的纯虚函数,就不能实例化出对象,抽象类的意义就在于强制子类重写抽象类的纯虚函数。
⭐ 2. 接口继承和实现继承
- 实现继承:普通函数的继承是一种实现继承,子类继承了父类函数的实现,可以使用该函数。
- 接口继承:虚函数的继承是一种接口继承,子类继承的是父类函数的接口,目的是为了实现重写,从而达成多态。
🌈 四、多态的原理
⭐ 1. 虚函数表
🌙 1.1 虚函数表的概念
- 每个包含虚函数的类中,都至少有一个虚函数表指针 _vfptr (简称虚表指针),这个指针指向一个虚函数表 (简称虚表)。
- 多态核心上是依靠虚函数表来实现的。
举个例子
- 在 32 位机上,定义一个 A 类对象 a,再使用 sizeof 算出 a 的大小。
class B
{
public:
// 虚函数
virtual void fun()
{
cout << "fun()" << endl;
}
private:
int _b = 1;
};
int main()
{
B b;
cout << sizeof(b) << endl;
return 0;
}
- 通过得出的结果为 8 可知,B 类成员中,除了占了 4 个字节的 _b 成员外,还藏着一个 4 字节的虚表指针。
🌙 1.2 虚函数表中存放着虚函数指针
- 虚函数表 (虚表) 中存储的是虚函数的地址,由于一个类中可能存在着多个虚函数,因此虚函数表中可能存在多个虚函数指针。
举个例子
- 下列代码中,父类 A 中有 3 个成员函数,其中 fun1 和 fun2 是虚函数,fun3 是普通函数。子类 B 中仅对父类 A 的虚函数 fun1 进行了重写。
// 父类
class A
{
public:
// 虚函数
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
// 虚函数
virtual void Ffun2()
{
cout << "A::fun2()" << endl;
}
// 普通成员函数
void fun3()
{
cout << "A::fun3()" << endl;
}
private:
int _b = 1;
};
// 子类
class B : public A
{
public:
// 重写虚函数 fun1
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
private:
int _d = 2;
};
int main()
{
A a; // 父类对象
B b; // 子类对象
return 0;
}
- 通过观察得知,父类对象 a 和子类对象 b 中,除了各自的成员变量之外,还各自拥有一个虚函数表指针 _vfptr。
- 虚函数表指针指向虚函数表,由于父类的 fun1 和 fun2 都是虚函数,因此父类对象 a 的虚表中存储的就是虚函数 fun1 和 fun2 的地址。
- 而子类虽然继承了父类的虚函数 fun1 和 fun2,但子类对 fun1 进行了重写,子类会用新的虚函数地址覆盖父类虚函数的地址。而子类没有重写 fun2,因此父子类的虚函数表中的 fun2 的地址是一致的。
⭐ 2. 多态原理
-
多态是如何实现传父类对象调用父类的内容,传子类对象调用子类的内容。
-
在下列代码中,多态是如何实现当父类 person 指针指向的是父类对象 Mike 时,调用的是父类的 buy_ticket;当父类 person 指针指向的是子类对象 Johnson 时,调用的是子类的 buy_ticket 呢?
class person
{
public:
virtual void buy_ticket()
{
cout << "person 买票 - 全价" << endl;
}
};
class student : public person
{
public:
virtual void buy_ticket()
{
cout << "student 买票 - 半价" << endl;
}
};
void Func(person* p)
{
p->buy_ticket();
}
int main()
{
person Mike;
student Johnson;
Func(&Mike); // person 买票 - 全价
Func(&Johnson); // student 买票 - 半价
return 0;
}
- 已知,Mike 对象和 Johnson 对象中各自包含一个虚函数表指针,这两个虚函数表指针分别指向自己的虚函数表。
- 当子类 student 没有重写父类 person 的虚函数时,这两张虚函数表中的内容会一致,同时指向父类的虚函数。
- 而当子类 student 重写了父类 person 的虚函数时,子类的虚函数表中的属于父类虚函数的地址内容就会被子类重写的虚函数地址覆盖。
- 父类指针 p 指向 Mike 对象时,p->buy_ticket 在 Mike 的虚函数表中找到的虚函数就是 person::buy_ticket
- 父类指针 p 指向 Johnson 对象时,p->buy_ticket 在 Johnson 的虚函数表中找到的虚函数就是 student::buy_ticket
⭐ 3. 静态绑定和动态绑定
- 静态绑定:又被称为前期绑定 (早绑定),在程序的编译期,就确定了程序的行为 (如:函数重载),也称为静态多态。
- 动态绑定:又被称为后期邦迪 (晚绑定),在程序的运行期,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
举个例子
- 对于如下代码的父子类,分别采用静态绑定以及动态绑定的方式调用。
// 父类
class person
{
public:
// 父类的虚函数
virtual void buy_ticket()
{
cout << "person 买票 - 全价" << endl;
}
};
// 子类
class student : public person
{
public:
// 子类重写父类的虚函数
virtual void buy_ticket()
{
cout << "student 买票 - 半价" << endl;
}
};
- 使用静态的方式调用 buy_ticket 函数,不构成多态,如何调用函数在编译时就能够确定。
int main()
{
// 不构成多态 (使用父类的指针/引用指向父/子类对象时才构成多态)
student Johnson;
person p = Johnson;
p.buy_ticket();
return 0;
}
- 将调用函数的代码翻译成汇编语句就只有两条汇编指令,也就是直接调用函数。
- 如果按照动态的方式调用 buy_ticket 函数,就会构成多态,如何调用函数在运行时才会确定。
int main()
{
// 采用父类引用的方式指向子类对象,构成多态
student Johnson;
person& p = Johnson;
p.buy_ticket();
return 0;
}
- 将调用函数的代码返回成汇编语句时,又 8 条汇编指令。
- 因为在程序运行时,需要先找到指定对象的虚函数表,再在虚函数表中找出要调用的虚函数,最后才进行函数的调用。
🌈 五、单继承和多继承关系的虚函数表
⭐ 1. 单继承中的虚函数表
- 以下列单继承关系代码为例,观察父类对象和子类对象的虚函数表模型。
// 父类
class A
{
public:
virtual void func1() { cout << "A::func1()" << endl; }
virtual void func2() { cout << "A::func2()" << endl; }
private:
int _a;
};
// 子类
class B : public A
{
public:
virtual void func1() { cout << "B::func1()" << endl; }
virtual void func3() { cout << "B::func3()" << endl; }
virtual void func4() { cout << "B::func4()" << endl; }
private:
int _b;
};
单继承关系中,子类的虚函数表的生成过程
- 子类继承父类的虚函数表到子类的虚函数表中。
- 对子类中重写了父类的虚函数的地址进行覆盖 (如 func1)。
- 虚函数表中新增子类当中新的虚函数地址 (如 func3 和 func4)。
⭐ 2. 多继承中的虚函数表
- 以以下多继承关系代码为例,观察父类对象和子类对象的虚函数表模型。
// 父类 1
class A1
{
public:
virtual void func1() { cout << "A1::func1()" << endl; }
virtual void func2() { cout << "A1::func2()" << endl; }
private:
int _a1;
};
// 父类 2
class A2
{
public:
virtual void func1() { cout << "A2::func1()" << endl; }
virtual void func2() { cout << "A2::func2()" << endl; }
private:
int _a2;
};
// 多继承子类
class B : public A1, public A2
{
public:
// 重写父类的 func1 和 func3 函数
virtual void func1() { cout << "B::func1()" << endl; }
virtual void func3() { cout << "B::func3()" << endl; }
private:
int _b1;
};
- 其中,两个父类的虚函数表模型如下:
- 子类的虚函数表模型如下:
多继承关系中,子类的虚函数表的生成过程
- 分别继承各个父类的虚函数表的内容到子类的各个虚函数表中。
- 对子类重写了的虚函数的地址进行父类 (如 func1)。
- 在子类的第一个继承父类部分的虚函数表中新增子类中新的虚函数的地址 (如 func3)。