目录
[2.4 override和final关键字](#2.4 override和final关键字)
(图像由AI生成)
0.前言
多态是C++语言的三大特性之一,另两个特性是封装和继承。多态性使得对象可以根据运行时的实际类型来表现出不同的行为,从而实现灵活和可扩展的设计。在软件开发过程中,多态能够提高代码的复用性和可维护性,减少重复代码,并提供更加抽象和通用的接口。本文将详细探讨C++中的多态,包括其定义、构成、虚函数、抽象类以及实现原理,帮助读者全面理解这一重要概念。
1.多态的定义及构成
1.1什么是多态?
多态(Polymorphism)是指同一接口在不同场景下可以表现出不同的行为。在面向对象编程中,多态允许程序在不同的上下文中调用相同的接口,从而处理不同的数据类型或对象。具体来说,多态可以分为两类:编译时多态(静态多态)和运行时多态(动态多态)。在C++中,编译时多态通过函数重载和模板实现,而运行时多态则通过虚函数和继承实现。本文主要讨论运行时多态。
1.2构成多态的条件
在C++中,要实现多态,需要满足以下条件:
- 继承:必须有一个基类和至少一个派生类。
- 虚函数 :基类中必须有至少一个函数被声明为虚函数(使用
virtual
关键字)。 - 基类指针或引用:通过基类指针或引用来调用虚函数。
示例代码
cpp
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
// 声明虚函数
virtual void makeSound() const {
cout << "Animal makes a sound" << endl;
}
};
// 派生类:Dog
class Dog : public Animal {
public:
// 重写虚函数
void makeSound() const override {
cout << "Dog barks" << endl;
}
};
// 派生类:Cat
class Cat : public Animal {
public:
// 重写虚函数
void makeSound() const 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;
}
在上面的代码中,Animal
类是基类,Dog
和Cat
类是派生类。基类中的makeSound
函数被声明为虚函数,派生类对该函数进行了重写。在main
函数中,通过基类指针调用虚函数,根据实际对象的类型,调用了不同的函数版本,实现了多态。这就是运行时多态的典型实现方式。
2.虚函数
2.1什么是虚函数?
虚函数是使用 virtual
关键字声明的函数,用于实现运行时多态。在基类中声明虚函数时,派生类可以重写该函数。当通过基类指针或引用调用虚函数时,会根据实际对象的类型调用对应的函数版本,而不是基类的版本。虚函数允许派生类提供自己特有的实现,使得代码更加灵活和可扩展。
2.2虚函数的重写
虚函数的重写是指在派生类中重新定义基类中的虚函数。重写虚函数时,派生类的函数签名必须与基类中的虚函数相同。通过这种方式,派生类可以提供其特有的行为。
示例代码
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void display() const {
cout << "Display from Base class" << endl;
}
};
class Derived : public Base {
public:
void display() const override {
cout << "Display from Derived class" << endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->display(); // 输出:Display from Derived class
delete basePtr;
return 0;
}
2.3虚函数重写的特例
2.3.1协变
协变是指在派生类中重写虚函数时,允许返回类型是基类返回类型的派生类。这种特性使得派生类可以返回更具体的对象,而不违反函数重写的规则。
示例代码
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual Base* clone() const {
return new Base(*this);
}
virtual void display() const {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
Derived* clone() const override { // 协变返回类型
return new Derived(*this);
}
void display() const override {
cout << "Derived" << endl;
}
};
int main() {
Base* basePtr = new Derived();
Base* newBasePtr = basePtr->clone(); // 返回Derived类的对象
newBasePtr->display(); // 输出:Derived
delete basePtr;
delete newBasePtr;
return 0;
}
在这个示例中,Derived
类重写了clone
函数,并返回类型是Derived*
,而不是基类的Base*
,这就是协变的应用。
2.3.2析构函数的重写
在使用继承和多态时,基类的析构函数应该声明为虚函数,以确保派生类对象被正确销毁。这是因为如果析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,导致资源泄漏。
示例代码
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() { // 基类析构函数声明为虚函数
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
public:
~Derived() override {
cout << "Derived destructor" << endl;
}
};
int main() {
Base* basePtr = new Derived();
delete basePtr; // 正确调用Derived和Base的析构函数
return 0;
}
在这个示例中,基类Base
的析构函数被声明为虚函数,因此通过基类指针删除派生类对象时,会正确调用派生类的析构函数,避免资源泄漏。
2.4 override和final关键字
override
关键字用于显式声明派生类中的虚函数是重写基类中的虚函数。它有助于编译器进行检查,以确保派生类中的函数确实是在重写基类中的虚函数,而不是定义一个新的函数。这不仅提高了代码的可读性,还减少了因拼写错误或参数不匹配导致的隐藏错误。
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void display() const {
cout << "Display from Base class" << endl;
}
};
class Derived : public Base {
public:
void display() const override { // 使用override关键字
cout << "Display from Derived class" << endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->display(); // 输出:Display from Derived class
delete basePtr;
return 0;
}
在这个示例中,Derived
类中的display
函数使用了override
关键字,明确指出这是对基类中display
函数的重写。
final
关键字 用于防止虚函数在派生类中再次被重写。它可以用于虚函数的声明中,表示该函数不能在进一步派生的类中被重写。另外,final
关键字还可以用于类声明,表示该类不能被继承。
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void display() const {
cout << "Display from Base class" << endl;
}
};
class Derived final : public Base { // 使用final关键字禁止进一步继承
public:
void display() const override {
cout << "Display from Derived class" << endl;
}
};
/*
class FurtherDerived : public Derived { // 错误!Derived类被final修饰,不能被继承
};
*/
class AnotherBase {
public:
virtual void show() const final { // 使用final关键字禁止重写
cout << "Show from AnotherBase class" << endl;
}
};
class AnotherDerived : public AnotherBase {
/*
void show() const override { // 错误!show函数被final修饰,不能被重写
cout << "Show from AnotherDerived class" << endl;
}
*/
};
int main() {
Base* basePtr = new Derived();
basePtr->display(); // 输出:Display from Derived class
delete basePtr;
return 0;
}
在这个示例中,Derived
类被声明为final
,表示不能再派生新的类。同时,AnotherBase
类中的show
函数被声明为final
,表示不能在派生类中被重写。
2.5重载、重写(覆盖)和重定义(隐藏)的对比
2.5.1重载(Overloading)
重载是指在同一个作用域中,函数名相同但参数列表不同的多个函数。重载函数可以有不同的参数类型、数量或顺序,但不能仅靠返回类型区分。
cpp
#include <iostream>
using namespace std;
class Example {
public:
void func(int x) {
cout << "Function with int parameter: " << x << endl;
}
void func(double x) {
cout << "Function with double parameter: " << x << endl;
}
void func(int x, double y) {
cout << "Function with int and double parameters: " << x << ", " << y << endl;
}
};
int main() {
Example ex;
ex.func(5); // 输出:Function with int parameter: 5
ex.func(5.5); // 输出:Function with double parameter: 5.5
ex.func(5, 5.5); // 输出:Function with int and double parameters: 5, 5.5
return 0;
}
在这个示例中,func
函数被重载了三次,分别接受不同的参数列表。
2.5.2重写(覆盖)(Overriding)
重写是指在派生类中重新定义基类中的虚函数。重写函数的签名必须与基类中的虚函数一致。通过重写,派生类可以提供特定的实现。
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void display() const {
cout << "Display from Base class" << endl;
}
};
class Derived : public Base {
public:
void display() const override { // 重写基类的虚函数
cout << "Display from Derived class" << endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->display(); // 输出:Display from Derived class
delete basePtr;
return 0;
}
在这个示例中,Derived
类重写了Base
类中的虚函数display
。
2.5.3重定义(隐藏)(Hiding)
重定义(隐藏)是指在派生类中定义了与基类同名但参数列表不同的函数。这种情况下,基类的同名函数在派生类中会被隐藏,但它们并不是重写,因此不会发生多态。
cpp
#include <iostream>
using namespace std;
class Base {
public:
void show() const {
cout << "Show from Base class" << endl;
}
};
class Derived : public Base {
public:
void show(int x) const { // 重定义基类的show函数
cout << "Show from Derived class with parameter: " << x << endl;
}
};
int main() {
Derived derived;
derived.show(5); // 输出:Show from Derived class with parameter: 5
// derived.show(); // 错误!没有与之匹配的show函数
Base* basePtr = &derived;
basePtr->show(); // 输出:Show from Base class
return 0;
}
在这个示例中,Derived
类中的show
函数隐藏了Base
类中的同名函数。通过派生类对象只能调用重定义后的函数,通过基类指针调用时则调用基类的函数。
3.抽象类
3.1概念
抽象类是不能实例化的类,通常作为其他类的基类使用。它包含至少一个纯虚函数。纯虚函数是没有具体实现的函数,只提供接口规范。纯虚函数使用=0
语法声明,表示派生类必须重写该函数。抽象类用于定义一个统一的接口,让派生类去实现具体的行为,从而实现多态。
cpp
#include <iostream>
using namespace std;
class AbstractClass {
public:
virtual void pureVirtualFunction() const = 0; // 纯虚函数
void concreteFunction() const {
cout << "Concrete function in AbstractClass" << endl;
}
};
class ConcreteClass : public AbstractClass {
public:
void pureVirtualFunction() const override {
cout << "Implementation of pureVirtualFunction in ConcreteClass" << endl;
}
};
int main() {
// AbstractClass obj; // 错误!不能实例化抽象类
ConcreteClass obj;
obj.pureVirtualFunction(); // 输出:Implementation of pureVirtualFunction in ConcreteClass
obj.concreteFunction(); // 输出:Concrete function in AbstractClass
return 0;
}
在这个示例中,AbstractClass
是一个抽象类,包含一个纯虚函数pureVirtualFunction
。ConcreteClass
是AbstractClass
的派生类,提供了pureVirtualFunction
的实现。
3.2接口继承和实现继承
3.2.1接口继承
接口继承是指派生类继承抽象类的函数声明,而不继承其具体实现。派生类必须提供所有纯虚函数的具体实现。接口继承使得派生类可以有不同的实现,但遵循相同的接口规范。
cpp
#include <iostream>
using namespace std;
class Interface {
public:
virtual void doSomething() const = 0; // 纯虚函数,接口声明
};
class ImplementationA : public Interface {
public:
void doSomething() const override {
cout << "Implementation A doing something" << endl;
}
};
class ImplementationB : public Interface {
public:
void doSomething() const override {
cout << "Implementation B doing something" << endl;
}
};
int main() {
Interface* a = new ImplementationA();
Interface* b = new ImplementationB();
a->doSomething(); // 输出:Implementation A doing something
b->doSomething(); // 输出:Implementation B doing something
delete a;
delete b;
return 0;
}
在这个示例中,Interface
类是一个抽象类,定义了一个纯虚函数doSomething
。ImplementationA
和ImplementationB
类分别提供了该函数的具体实现,实现了接口继承。
3.2.2实现继承
实现继承是指派生类不仅继承基类的接口,还继承其具体实现。基类中的非纯虚函数可以在派生类中直接使用,也可以在派生类中被重写。实现继承使得派生类可以复用基类的代码,减少重复实现。
cpp
#include <iostream>
using namespace std;
class BaseClass {
public:
virtual void virtualFunction() const {
cout << "BaseClass implementation of virtualFunction" << endl;
}
void anotherFunction() const {
cout << "BaseClass implementation of anotherFunction" << endl;
}
};
class DerivedClass : public BaseClass {
public:
void virtualFunction() const override {
cout << "DerivedClass override of virtualFunction" << endl;
}
};
int main() {
DerivedClass obj;
obj.virtualFunction(); // 输出:DerivedClass override of virtualFunction
obj.anotherFunction(); // 输出:BaseClass implementation of anotherFunction
return 0;
}
在这个示例中,BaseClass
包含一个虚函数virtualFunction
和一个普通成员函数anotherFunction
。DerivedClass
重写了virtualFunction
,但直接继承并使用了anotherFunction
的实现。这就是实现继承的应用。
4.多态原理详析
4.1虚函数表
虚函数表(Virtual Table,简称vtable)是实现C++多态的核心机制。当一个类包含虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个指针数组,每个元素指向该类的一个虚函数。每个对象在创建时都会包含一个指向虚函数表的指针(vptr),通过这个指针,程序在运行时能够找到并调用对象实际类型的虚函数。
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() { cout << "Base func1" << endl; }
virtual void func2() { cout << "Base func2" << endl; }
};
class Derived : public Base {
public:
void func1() override { cout << "Derived func1" << endl; }
void func2() override { cout << "Derived func2" << endl; }
};
int main() {
Base* basePtr = new Derived();
basePtr->func1(); // 输出:Derived func1
basePtr->func2(); // 输出:Derived func2
delete basePtr;
return 0;
}
在这个示例中,Base
类和Derived
类都有两个虚函数func1
和func2
。Base
类的虚函数表会指向其虚函数实现,而Derived
类的虚函数表会指向其重写的虚函数实现。通过基类指针调用虚函数时,会通过vptr找到实际对象的虚函数表,从而调用正确的函数版本。
4.2多态的实现原理
多态的实现原理依赖于虚函数表和虚函数表指针。以下是多态实现的几个关键步骤:
- 创建对象:当创建一个包含虚函数的类的对象时,编译器会为该对象分配内存,并初始化其vptr,使其指向该类的虚函数表。
- 调用虚函数:通过基类指针或引用调用虚函数时,程序会通过vptr找到实际对象的虚函数表,然后根据函数的偏移量找到正确的函数地址并进行调用。
- 运行时决策:由于vptr在运行时指向实际对象的虚函数表,程序可以在运行时根据对象的实际类型调用相应的函数,实现运行时多态。
cpp
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() { cout << "Animal speaks" << endl; }
};
class Dog : public Animal {
public:
void speak() override { cout << "Dog barks" << endl; }
};
class Cat : public Animal {
public:
void speak() override { cout << "Cat meows" << endl; }
};
void makeAnimalSpeak(Animal& animal) {
animal.speak();
}
int main() {
Dog dog;
Cat cat;
makeAnimalSpeak(dog); // 输出:Dog barks
makeAnimalSpeak(cat); // 输出:Cat meows
return 0;
}
在这个示例中,makeAnimalSpeak
函数接受一个Animal
类的引用参数,通过该引用调用虚函数speak
。实际调用的是Dog
和Cat
类重写的speak
函数,从而实现多态。
4.3静态绑定和动态绑定
-
静态绑定(Static Binding):静态绑定在编译时进行决策,函数调用在编译时被解析。普通成员函数和非虚函数使用静态绑定。静态绑定的优点是速度快,因为在编译时已经确定了调用地址。
-
动态绑定(Dynamic Binding):动态绑定在运行时进行决策,函数调用在运行时通过虚函数表解析。虚函数使用动态绑定。动态绑定的优点是灵活,可以在运行时根据对象的实际类型调用相应的函数版本。
静态绑定示例代码
cpp
#include <iostream>
using namespace std;
class Base {
public:
void staticFunc() { cout << "Base staticFunc" << endl; }
};
class Derived : public Base {
public:
void staticFunc() { cout << "Derived staticFunc" << endl; }
};
int main() {
Base base;
Derived derived;
Base* basePtr = &derived;
base.staticFunc(); // 输出:Base staticFunc
derived.staticFunc(); // 输出:Derived staticFunc
basePtr->staticFunc(); // 输出:Base staticFunc
return 0;
}
在这个示例中,staticFunc
是普通成员函数,不是虚函数,因此使用静态绑定。即使通过基类指针调用staticFunc
,也调用的是基类版本。
动态绑定示例代码
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void dynamicFunc() { cout << "Base dynamicFunc" << endl; }
};
class Derived : public Base {
public:
void dynamicFunc() override { cout << "Derived dynamicFunc" << endl; }
};
int main() {
Base base;
Derived derived;
Base* basePtr = &derived;
base.dynamicFunc(); // 输出:Base dynamicFunc
derived.dynamicFunc(); // 输出:Derived dynamicFunc
basePtr->dynamicFunc(); // 输出:Derived dynamicFunc
return 0;
}
在这个示例中,dynamicFunc
是虚函数,因此使用动态绑定。通过基类指针调用dynamicFunc
时,会根据实际对象的类型调用Derived
类的版本。
5.结语
通过深入探讨C++中的多态特性及其实现原理,我们可以理解虚函数、虚函数表以及静态绑定和动态绑定的机制。多态作为C++的三大特性之一,不仅提升了代码的灵活性和可扩展性,还提高了程序设计的抽象能力。掌握多态的概念和应用,对于编写高质量的面向对象程序至关重要。希望本文能够帮助读者更好地理解和运用C++中的多态特性。