原创作者:郑同学的笔记
原创地址:https://zhengjunxue.blog.csdn.net/article/details/131858812
qq技术交流群:921273910
我们查看《C++ Primer 第5版》第15.3章节 虚函数中的介绍(p537页)
OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语,其含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。
Note当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)
多态性(polymorphism)当用于面向对象编程的范畴时,多态性的含义是指程序能通过引用或指针的动态类型获取类型特定行为的能力。
动态类型(dynamic type)对象在运行时的类型。引用所引对象或者指针所指对象的动态类型可能与该引用或指针的静态类型不同。基类的指针或引用可以指向一个派生类对象。在这样的情况中,静态类型是基类的引用(或指针),而动态类型是派生类的引用(或指针)。
静态类型(static type)对象被定义的类型或表达式产生的类型。静态类型在编译时是已知的。
我们看网上有很多资料介绍动态时,都会提到多态分为静态多态(比如函数重载等)和动态多态,而当我们看了上面书中的定义和介绍后会明白,网上的说法是有问题的。
在c++领域:
动态多态性是在运行时确定方法或函数的调用,根据实际对象的类型进行动态绑定。这种多态性通过虚函数和基类指针或引用来实现。
简单来说,
多态: 就是多种形态,不同的对象去完成同样的事情会产生不同的结果。
举个例子:就拿购票系统来说,不同的人对于购票这个行为产生的结果就是不同的,学生购票时购买的是半价票,普通人购票的时候购买的是全价票。
继承中想要构成多态,必须满足以下两个条件:
① 必须是子类的虚函数重写成父类函数(重写:三同 + 虚函数)
② 必须是父类的指针或者引用去调用虚函数。
- 三同指的是:同函数名、同参数、同返回值。
- 虚函数:即被 virtual 修饰的类成员函数。
#include
using namespace std;
class Person {
public:
Person(const char* name)
: _name(name) {}
// 虚函数
virtual void BuyTicket() {
cout << _name << ": " << "Person-> 买票 全价 100¥" << endl;
}
protected:
string _name;
};
class Student : public Person {
public:
Student(const char* name)
: Person(name) {}
// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
virtual void BuyTicket() {
cout << _name << ": " << "Student-> 买票 半价 50¥" << endl;
}
};
class Soldier : public Person {
public:
Soldier(const char* name)
: Person(name) {}
// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
virtual void BuyTicket() {
cout << _name << ": " << "Soldier-> 优先买预留票 全价 100¥" << endl;
}
};
/* 接收身份 */
void Pay(Person* ptr) {
ptr->BuyTicket(); // 到底是谁在买票,取决于传来的是谁
delete ptr;
}
int main()
{
Person* p1 = new Person("小明爸爸");
Student* stu = new Student("小明");
Soldier* so = new Soldier("小明爷爷");
Pay(p1);
Pay(stu);
Pay(so);
return 0;
}
输出
/* 接收身份 */
void Pay(Person& ptr) {
ptr.BuyTicket(); // 到底是谁在买票,取决于传来的是谁
}
int main()
{
Person p1("小明爸爸");
Student stu("小明");
Soldier so("小明爷爷");
Pay(p1);
Pay(stu);
Pay(so);
return 0;
}
我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)
覆盖也被有的文章叫做”重写“。用 virtual 虚函数,并且做到函数名、参数和返回值相同,就能够达到 “重写” 的效果:
重写是为了将一个已有的事物进行某些改变以适应新的要求。
重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。 即:“外壳不变,核心重写。”
刚才说了,三同+虚函数,就能达到重写的效果(也就是多态)。但是,还有两个意外,也能达成多态的效果。
C++中的协变(Covariance)指的是派生类可以返回基类中相同函数签名的返回类型的子类型。
在C++中,当一个虚函数在基类中使用了virtual关键字声明为虚函数时,派生类可以对该虚函数进行重写,并且在派生类中返回类型可以是基类返回类型的子类型。这种返回类型的子类型关系称为协变。
协变的类型必须是父子关系。
观察下面的代码,并没有达到 “三同” 的标准,它的返回值是不同的,但依旧构成多态:
class A {};
class B : public A {};
class Person {
public:
virtual A* f() {
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* f() {
cout << "virtual B* Student:::f()" << endl;
return nullptr;
};
};
int main(void)
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
输出
当class A、class B是父子关系时,就不能协变:
现在来讲第二个例外。
#include
using namespace std;
class A {};
class B : public A {};
class Person {
public:
virtual A* f() {
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person {
public:
B* f() {
cout << "virtual B* Student:::f()" << endl;
return nullptr;
};
};
int main(void)
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
输出
#include
using namespace std;
class Person {
public:
~Person() { //不加virtual
// virtual ~Person() { //加virtual
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
~Student() {
cout << "~Student()" << endl;
}
};
int main(void)
{
Person p;
Student s;
return 0;
}
#include
using namespace std;
class Person {
public:
~Person() { //不加virtual
//virtual ~Person() { //加virtual
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
~Student() {
cout << "~Student()" << endl;
}
};
int main(void)
{
cout << "=================不加virtual======================\n";
Person *ptr = new Person();
delete ptr;
cout << "=======================================\n";
Student *ptr2 = new Student();
delete ptr2;
cout << "=======================================\n";
Person *ptr3 = new Student();
delete ptr3;
return 0;
}
刚才我们看到了,如果这里不加 virtual,~Student 是没有调用析构的。
这其实是非常致命的,是不经意间会发生的内存泄露。
在C++中,final是一个关键字,用于修饰类、函数或虚函数,具有不同的作用。
class Base final {
// ...
};
class Derived : public Base { // 错误,Derived不能继承自final类Base
// ...
};
在上述示例中,Base类被声明为final,因此Derived类不能继承自Base类。
class Base {
public:
virtual void func() final {
// ...
}
};
class Derived : public Base {
public:
void func() override { // 错误,无法重写被声明为final的函数
// ...
}
};
在上述示例中,Base类中的func()函数被声明为final,因此Derived类无法对其进行重写。
class Base {
public:
virtual void func() final {
// ...
}
};
class Derived : public Base {
public:
void func() override { // 错误,无法重写被声明为final的虚函数
// ...
}
};
在上述示例中,Base类中的虚函数func()被声明为final,因此Derived类无法对其进行重写。
通过使用final关键字,可以显式地阻止类、函数或虚函数被继承、重写或覆盖,从而提高程序的安全性和可靠性。
override是C++11引入的关键字,用于显式地标记派生类中对基类虚函数的重写。它的主要作用是增加代码的可读性和可维护性,并提供编译器的静态检查,避免错误的重写行为。
在C++中,当派生类要重写基类的虚函数时,可以使用override关键字进行标记。通过使用override关键字,可以确保派生类的函数签名与基类的虚函数完全匹配,否则编译器会发出错误。这有助于及时发现错误的重写行为。
以下是使用override关键字的示例:
class Base {
public:
virtual void func() const {
// ...
}
};
class Derived : public Base {
public:
void func() const override {
// ...
}
};
在上述示例中,Base类中的虚函数func()被定义为virtual void func() const,而在Derived类中,重写的函数也被定义为void func() const,并使用override关键字进行标记。如果Derived类的函数签名与基类的虚函数不匹配,或者没有正确使用override关键字,编译器将会报错。
我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)
纯虚函数是通过在函数声明后面加上= 0来声明的,表示该函数没有实现,派生类必须重写它。
virtual void pureVirtualFunction() = 0;
在上述示例中,纯虚函数pureVirtualFunction()。
/* 抽象类 */
class Car {
public:
// 实现没有价值,因为压根没有对象会调用它
virtual void Drive() = 0 { // 纯虚函数
cout << "Drive()" << endl;
}
};
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0;
};
在上述示例中,AbstractClass是一个抽象类,它具有一个纯虚函数pureVirtualFunction()。派生类必须重写这个函数。
抽象类可以包含纯虚函数(没有实现)和带有实现的函数
虽然父类是抽象类不能定义对象,但是可以定义指针。
定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,但是可以 new 子类对象:
#include
using namespace std;
/* 抽象类 */
class Car {
public:
virtual void Drive() = 0;
};
class Benz : public Car {
public:
virtual void Drive() {
cout << "Benz-舒适" << endl;
}
};
int main(void)
{
Car* pBenz1 = new Benz;
pBenz1->Drive();
Benz* pBenz2 = new Benz;
pBenz2->Drive();
return 0;
}
抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。
接口继承指的是一个类从一个或多个接口中继承方法声明,但并不继承这些方法的具体实现。接口只包含纯虚函数(在C++中使用纯虚函数定义接口)或者抽象方法(在其他语言中)。通过接口继承,一个类可以实现多个接口,从而表达出它具备了多个行为或功能。
实现继承指的是子类从父类中继承方法声明和实现。实现继承建立了类的层次结构,允许子类继承并重用父类的代码。子类可以通过继承父类的属性和方法,并且可以根据需要添加新的属性和方法,甚至可以重写父类的方法来改变其行为。