在C++中,虚函数和抽象基类是实现多态性的重要机制。多态性允许通过基类的指针或引用来调用派生类的函数,从而实现代码的灵活性和可扩展性。
一、虚函数
虚函数通过动态绑定(Dynamic Binding)在运行时决定调用哪个函数版本,而不是在编译时确定。这使得程序可以更加灵活,能够根据对象的实际类型调用相应的函数。
1、常规用法
虚函数就是在函数声明时使用关键字virtual
修饰的成员函数。在派生类中可以选择重写这个虚函数。如果基类指针或引用指向派生类对象,调用该函数时将根据对象的实际类型来选择合适的函数版本。
#include <iostream>
using namespace std;
class Base {
public:
//虚函数定义
virtual void virtualFunction() {
cout << "Base virtualFunction" << endl;
}
// 普通成员函数
void normalFunction() {
cout << "Base normalFunction" << endl;
}
};
class Derived : public Base {
public:
// 重写基类的虚函数,动态绑定
void virtualFunction() override {
cout << "Derived virtualFunction" << endl;
}
// 重写基类的普通函数,是静态绑定
void normalFunction() {
cout << "Derived normalFunction" << endl;
}
};
int main() {
// 基类指针指向派生类对象
Base* basePtr = new Derived();
basePtr->virtualFunction(); // 输出:Derived virtualFunction
basePtr->normalFunction(); //输出:Base normalFunction
return 0;
}
在 main()
函数中通过基类指针 basePtr
调用 virtualFunction()
函数时,发生了动态绑定。虽然 basePtr
的静态类型是 Base*
,但它实际上指向一个 Derived
对象,因此在运行时会调用 Derived
类的 virtualFunction()
。
具体过程:
-
编译时:编译器看到
basePtr->virtualFunction()
调用时,识别到virtualFunction
是一个虚函数。因此,编译器会在生成的代码中插入对虚函数表(vtable)的查找。 -
运行时:程序运行时,根据
basePtr
实际指向的对象类型(即Derived
对象),从虚函数表中找到Derived
类的virtualFunction
函数的地址,并调用它。
2、虚函数特点
-
使用场景:如果函数在不同派生类中行为不同,使用虚函数;否则无需使用虚函数。
-
使用规则:
- 派生类重写虚函数时,函数名、参数类型、顺序、个数以及返回类型必须与基类中的虚函数一致。否则会被认为是重载,而非覆盖。
- 虚函数必须是类的成员函数,不能是友元函数或静态函数。(与对象不关联)
-
构造函数与析构函数:
- 构造函数不能是虚函数:虚函数机制依赖于对象已存在的虚函数表来动态决定调用哪个函数。然而,当构造函数被调用时,对象尚未完全构建完成。
- 析构函数应声明为虚函数。这确保通过基类指针删除派生类对象时能正确调用派生类的析构函数,避免资源泄漏。
二、 纯虚函数与抽象基类
纯虚函数是虚函数的特殊形式,其在基类中不提供实现,只定义接口,强制派生类必须实现该函数,否则派生类本身也会成为抽象类,无法实例化。
定义与用法: 纯虚函数通过在虚函数声明中使用= 0
的方式表示。
class Base {
public:
virtual void show() = 0; // 纯虚函数
};
class Derived : public Base {
public:
void show() override { // 实现纯虚函数
std::cout << "Derived show() called" << std::endl;
}
};
int main() {
Base* bp = new Derived();
bp->show(); // 输出:Derived show() called
}
- 纯虚函数使得该类成为抽象基类,不能直接实例化。
- 派生类必须实现基类的所有纯虚函数才能实例化。如果派生类未完全实现这些纯虚函数,它也将是一个抽象类,无法实例化。
抽象基类
抽象基类(Abstract Base Class, ABC)是包含一个或多个纯虚函数的类。它不能直接实例化,通常用于定义接口,规定派生类必须实现的功能。抽象基类是一种设计模式,建立一个清晰的架构框架,规定哪些方法必须被实现,而不用关心具体的实现细节。
定义与用法:
#include <iostream>
using namespace std;
class AbstractBase {
public:
virtual void operation1() = 0; // 纯虚函数,抽象基类
virtual void operation2() = 0;
};
class DerivedAbstract : public AbstractBase {
public:
// 只重写了一个纯虚函数,因此本类仍然是抽象类
virtual void operation1() override {
cout << "DerivedAbstract operation1" << endl;
}
};
class ConcreteDerived : public DerivedAbstract {
public:
// 重写所有继承的纯虚函数,本类不是抽象类
virtual void operation1() override {
cout << "ConcreteDerived operation1" << endl;
}
virtual void operation2() override {
cout << "ConcreteDerived operation2" << endl;
}
};
int main() {
// AbstractBase base; // 报错:不能实例化抽象类
// DerivedAbstract derivedAbstract; // 报错:不能实例化抽象类
ConcreteDerived concreteDerived; // 可以实例化,因为所有纯虚函数已实现
AbstractBase* basePtr = &concreteDerived;
basePtr->operation1(); // 输出:DerivedAbstract operation1
basePtr->operation2(); // 输出:ConcreteDerived operation2
return 0;
}
特点:
-
规范化:抽象基类提供了一个清晰的接口规范。它强制派生类遵循特定的编程接口,这对维护大型软件项目特别有帮助。
-
不可实例化:由于至少包含一个纯虚函数(pure virtual function),抽象基类不能被直接实例化。这种设计防止了抽象基类本身的对象创建,确保只有具体子类的对象才能存在,子类需要提供纯虚函数的具体实现。
总结
-
清晰的接口设计:在设计抽象类时,明确区分哪些功能是基本操作(通过纯虚函数定义),哪些是可选扩展(通过虚函数实现)。保持抽象类的简洁、专注,使得继承结构更加清晰和易于管理。
-
适当使用虚函数:在需要多态性来处理不同行为的对象时,才应该使用虚函数。过度使用可能导致代码复杂且难以优化。