本文学习大佬的文章,所摘录和整理的一些知识,同时记录一自己的理解
《C++面向对象程序设计》✍千处细节、万字总结(建议收藏)_c++面向对象程序设计千处细节-CSDN博客
前言
本文主要是对于多态的理解方面,即C++面向对象三要素之一的多态(Polymorphism):允许不同对象对相同的消息做出不同的响应。通过多态可以提高代码的灵活性和可扩展性,实现基于抽象接口的编程。在C++中,多态通常通过虚函数来实现。
多态
多态性概述
所谓多态性就是不同对象收到相同的消息时,产生不同的动作。这样,就可以用同样的接口访问不同功能的函数,从而实现“一个接口,多种方法”。
从实现的角度来讲,多态可以划分为两类:编译时的多态和运行时的多态。在C++中,多态的实现和连编这一概念有关。所谓连编就是把函数名与函数体的程序代码连接在一起的过程。静态连编就是在编译阶段完成的连编。编译时的多态是通过静态连编来实现的。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。运行时的多态是用动态连编实现的。动态连编时运行阶段完成的,即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。
编译时多态(静态多态):函数重载
正如下面的例子中,我们定义了两个名为 print
的函数,一个接受整数参数,另一个接受浮点数参数。根据函数调用时传入的参数类型,编译器会在编译时决定使用哪个函数进行调用。这种根据参数类型确定函数重载版本的机制就是编译时的多态。
#include <iostream>
using namespace std;
void print(int num) {
cout << "Printing an integer: " << num << endl;
}
void print(float num) {
cout << "Printing a float: " << num << endl;
}
int main() {
int x = 10;
float y = 3.14;
print(x); // 输出:Printing an integer: 10
print(y); // 输出:Printing a float: 3.14
return 0;
}
运行时多态(动态多态):虚函数
在下面例子中,Animal 类作为基类,它的 makeSound() 函数被定义为虚函数。
Dog 类和 Cat 类分别继承了 Animal 类,并且重写了 makeSound() 函数。
在主函数中,我们创建了指向基类对象的指针 animal1 和 animal2,并将它们分别赋值为 Dog 对象和 Cat 对象。然后调用这些指针的 makeSound() 函数时,会根据实际对象类型来决定执行哪个版本的函数。
animal1 指针指向 Dog 对象,因此调用 animal1->makeSound() 时会执行 Dog 类中的 makeSound() 函数,输出 "Dog barks"。
同样地,animal2 指针指向 Cat 对象,调用 animal2->makeSound() 会执行 Cat 类中的 makeSound() 函数,输出 "Cat meows"。
这就是运用虚函数所实现的动态多态
#include <iostream>
using namespace std;
class Animal {
public:
virtual void makeSound() {
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
cout << "Dog barks" << endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
cout << "Cat meows" << endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // 输出:Dog barks
animal2->makeSound(); // 输出:Cat meows
delete animal1;
delete animal2;
return 0;
}
在C++中,编译时多态性主要是通过函数重载和运算符重载实现的;运行时多态性主要是通过虚函数来实现的。
虚函数
虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。
定义虚函数的方法如下:
virtual 返回类型 函数名(形参表) {
函数体
}
在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
就如上面的代码,讲述多态时候的经典代码animals类
#include <iostream>
using namespace std;
class Animal {
public:
virtual void makeSound() {
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
cout << "Dog barks" << endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
cout << "Cat meows" << endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // 输出:Dog barks
animal2->makeSound(); // 输出:Cat meows
delete animal1;
delete animal2;
return 0;
}
C++规定,如果在派生类中,没有用virtual显式地给出虚函数声明,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:该函数与基类的虚函数是否有相同的名称、参数个数以及对应的参数类型、返回类型或者满足赋值兼容的指针、引用型的返回类型。
下面对虚函数的定义做几点说明:
1、由于虚函数使用的基础是赋值兼容规则,而赋值兼容规则成立的前提条件是派生类从其基类公有派生。因此,通过定义虚函数来使用多态性机制时,派生类必须从它的基类公有派生。
2、必须首先在基类中定义虚函数;
3、在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。
4、虽然使用对象名和点运算符的方式也可以调用虚函数,如mom.like()可以调用虚函数Mother::like()。但是,这种调用是在编译时进行的静态连编,它没有充分利用虚函数的特性,只有通过基类指针访问虚函数时才能获得运行时的多态性
5、一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
6、虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
7、内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做非内联的。
8、构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数。
虚析构函数
虚析构函数在处理多态性对象时非常重要。当基类指针或引用指向派生类对象,并且通过该指针或引用删除对象时,如果基类的析构函数不是虚拟的,那么只会调用基类的析构函数而不会调用派生类的析构函数,这可能导致资源泄漏或未完全释放。
虚析构函数声明一般为:
virtual ~类名(){
·····
}
class Base {
public:
virtual ~Base() { // 虚析构函数
// 执行基类清理操作
cout << "调用基类Base的析构函数..." << endl;
}
};
class Derived : public Base {
public:
~Derived() override { // 派生类自己的析构函数
// 执行派生类清理操作
cout << "调用派生类Derived的析构函数..." << endl;
}
};
int main() {
Base* ptr = new Derived(); // 创建派生类对象并使用基类指针指向
delete ptr; // 删除对象
return 0;
}
如果没有virtual,即基类没有声明虚析构函数,那么调用基类的析构函数而不会调用派生类的析构函数,这可能导致资源泄漏或未完全释放。
如果希望程序执行动态连编方式,在用delete运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。
override关键字
override
是 C++11 引入的关键字,用于显式地标识派生类中的函数覆盖基类中的虚函数。它在语法上没有强制要求使用,但是建议使用 override
关键字,因为它可以提高代码的可读性和可维护性。
在派生类中重写基类的虚函数时,如果使用了 override
关键字,则编译器会检查该函数是否真正覆盖了基类中的虚函数。如果没有正确地重写或者基类中不存在对应的虚函数,编译器将报错,帮助发现潜在的错误。
class Base {
public:
virtual void foo() const {
// 基类中的虚函数实现
}
};
class Derived : public Base {
public:
void foo() const override {
// 派生类中对基类虚函数进行覆盖
}
};
在上述示例中,在派生类 Derived
中使用 override
关键字确保了我们意图重写基类中的 foo()
虚函数。这样做不仅让代码更加清晰明了,并且如果我们错误地拼写了函数名、参数列表或者类型不匹配等问题时,编译器会发出警告或错误提示。
总结来说,尽管 override
并非必须写出来,但强烈建议在派生类中重写基类的虚函数时使用 override
关键字,以增加代码的清晰性,并帮助捕捉潜在的错误。
纯虚函数
纯虚函数是在基类中声明的没有实际定义的虚函数。它通过在函数声明末尾加上 = 0
来标识,告诉编译器该函数在基类中没有具体的实现,而是由派生类来实现。
纯虚函数有以下用途:
定义接口:纯虚函数可以被用于定义抽象基类,即只提供接口而不提供具体实现。派生类必须重写这个纯虚函数才能被实例化。
实现多态性:通过基类指针或引用调用纯虚函数时,运行时会根据指针或引用所指向的对象类型调用相应派生类的实现,从而实现多态性。
class Shape {
public:
virtual double calculateArea() const = 0; // 纯虚函数
void displayArea() const {
double area = calculateArea();
std::cout << "Area: " << area << std::endl;
}
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() const override {
return width * height;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double calculateArea() const override {
return 3.14 * radius * radius;
}
};
int main() {
Shape* shape1 = new Rectangle(5, 3);
shape1->displayArea(); // 调用Rectangle的calculateArea()
Shape* shape2 = new Circle(2);
shape2->displayArea(); // 调用Circle的calculateArea()
delete shape1;
delete shape2;
return 0;
}
在上述示例中,Shape
是一个抽象基类,其中的 calculateArea()
函数被声明为纯虚函数。派生类 Rectangle
和 Circle
必须重写这个函数。
通过使用基类指针调用 displayArea()
函数,在运行时会根据指针所指向的具体对象类型来决定调用哪个派生类实现的 calculateArea()
函数,从而计算出正确的面积。
纯虚函数使得基类能够定义通用接口,并强制派生类提供自己的实现,实现了接口规范和多态性。
如果一个类至少有一个纯虚函数,那么就称该类为抽象类,对于抽象类的使用有以下几点规定:
1、由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象。
2、不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类。
3、抽象类不能用作函数的参数类型、函数的返回类型或是显式转换的类型。
4、可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
5、如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
总结
本文主要介绍了多态相关知识,从多态性是什么,到认识虚函数,虚析构函数和纯虚函数等相关多态知识。