当前位置: 首页 > news >正文

C++学习:六个月从基础到就业——面向对象编程:虚函数与抽象类

C++学习:六个月从基础到就业——面向对象编程:虚函数与抽象类

本文是我C++学习之旅系列的第十四篇技术文章,主要探讨C++中的虚函数与抽象类,这是实现多态性的核心机制。查看完整系列目录了解更多内容。

引言

多态性是面向对象编程的三大支柱之一(另外两个是封装和继承),它允许我们使用统一的接口处理不同类型的对象。在C++中,多态性主要通过虚函数机制实现。虚函数允许派生类重写基类的方法,从而在运行时根据对象的实际类型调用相应的函数,而不是根据指针或引用的静态类型。抽象类则更进一步,通过纯虚函数定义接口,强制派生类提供实现,但自身不能被实例化。

本文将深入探讨C++中虚函数与抽象类的概念、实现机制、使用场景和最佳实践。通过理解这些概念,您将能够设计更加灵活、可扩展的面向对象系统。

虚函数基础

什么是虚函数?

虚函数是一种可以在派生类中被重写(覆盖)的成员函数。当通过基类的指针或引用调用虚函数时,实际调用的是对象的动态类型所对应的函数版本,而不是指针或引用的静态类型所对应的版本。

在C++中,使用virtual关键字声明虚函数:

class Base {
public:virtual void display() const {std::cout << "Base class display" << std::endl;}
};class Derived : public Base {
public:void display() const override {std::cout << "Derived class display" << std::endl;}
};int main() {Base* ptr = new Derived();ptr->display();  // 输出 "Derived class display"delete ptr;return 0;
}

在上面的例子中,尽管ptrBase*类型,但调用display()方法时实际执行的是Derived类中的版本,这就是动态绑定(也称为晚绑定或运行时多态)的表现。

override关键字

从C++11开始,我们可以使用override关键字明确表示一个函数是对基类虚函数的重写:

class Base {
public:virtual void method1() const {}virtual void method2() {}
};class Derived : public Base {
public:void method1() const override {}  // 正确// void method2() const override {}  // 错误:签名不匹配
};

使用override关键字有两个主要好处:

  1. 提高代码可读性,明确指出函数是重写基类的虚函数
  2. 编译器会检查是否真的重写了基类的虚函数,避免因函数签名不匹配导致的错误

final关键字

C++11还引入了final关键字,可以用于防止进一步重写虚函数或继承类:

class Base {
public:virtual void method() {}
};class Derived : public Base {
public:void method() override final {}  // 将此方法标记为final
};class Further : public Derived {
public:// void method() override {}  // 错误:不能重写final方法
};// 防止类被继承
class FinalClass final {// 类定义...
};// class DerivedFromFinal : public FinalClass {}; // 错误:不能继承final类

虚函数表机制

虚函数是如何实现的?编译器通常使用"虚函数表"(vtable)机制:

  1. 每个包含虚函数的类都有一个虚函数表,存储该类所有虚函数的地址
  2. 每个对象包含一个指向虚函数表的指针(通常称为vptr)
  3. 当调用虚函数时,程序通过对象的vptr找到对应的虚函数表,再从表中查找函数地址并调用

下图演示了虚函数表的基本结构:

类Base:            类Derived:          对象布局:
+-------------+    +-------------+    Base对象:      Derived对象:
| vtable 指针 |--> | Base::func1 |    +----------+  +----------+
+-------------+    | Base::func2 |    | vptr      |  | vptr      |-->+-------------+
| virtual func1 |  +-------------+    +----------+  +----------+    | Derived::func1 |
| virtual func2 |                     | 成员变量... |  | 成员变量... |    | Base::func2    |
+-------------+                                                     +-------------+

正是这种机制使得程序能在运行时根据对象的实际类型调用正确的函数版本。

虚析构函数

当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,则只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致资源泄漏:

class Base {
public:~Base() {  // 非虚析构函数std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
private:int* data;public:Derived() {data = new int[100];std::cout << "Derived constructor" << std::endl;}~Derived() {delete[] data;std::cout << "Derived destructor" << std::endl;}
};int main() {Base* ptr = new Derived();delete ptr;  // 只会调用Base的析构函数,导致内存泄漏return 0;
}

正确的做法是将基类析构函数声明为虚函数:

class Base {
public:virtual ~Base() {std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
private:int* data;public:Derived() {data = new int[100];std::cout << "Derived constructor" << std::endl;}~Derived() override {delete[] data;std::cout << "Derived destructor" << std::endl;}
};int main() {Base* ptr = new Derived();delete ptr;  // 先调用Derived的析构函数,再调用Base的析构函数return 0;
}

最佳实践: 任何可能作为基类的类都应该有虚析构函数,即使它不需要做任何清理工作。

纯虚函数与抽象类

纯虚函数

纯虚函数是一种特殊的虚函数,它在基类中没有实现,而是要求派生类必须提供实现。纯虚函数的声明形式为:

virtual ReturnType functionName(parameters) = 0;

示例:

class Shape {
public:virtual double area() const = 0;  // 纯虚函数virtual double perimeter() const = 0;  // 纯虚函数virtual ~Shape() {}  // 虚析构函数
};

抽象类

包含至少一个纯虚函数的类被称为抽象类。抽象类不能被直接实例化,只能作为基类使用:

class Shape {
public:virtual double area() const = 0;virtual double perimeter() const = 0;virtual ~Shape() {}
};class Circle : public Shape {
private:double radius;public:Circle(double r) : radius(r) {}double area() const override {return 3.14159 * radius * radius;}double perimeter() const override {return 2 * 3.14159 * radius;}
};class Rectangle : public Shape {
private:double width, height;public:Rectangle(double w, double h) : width(w), height(h) {}double area() const override {return width * height;}double perimeter() const override {return 2 * (width + height);}
};int main() {// Shape shape;  // 错误:不能创建抽象类的实例Shape* circle = new Circle(5.0);Shape* rectangle = new Rectangle(4.0, 6.0);std::cout << "Circle area: " << circle->area() << std::endl;std::cout << "Rectangle area: " << rectangle->area() << std::endl;std::cout << "Circle perimeter: " << circle->perimeter() << std::endl;std::cout << "Rectangle perimeter: " << rectangle->perimeter() << std::endl;delete circle;delete rectangle;return 0;
}

抽象类主要用于定义接口,强制派生类实现特定的功能。

带实现的纯虚函数

虽然纯虚函数通常没有实现,但C++允许为纯虚函数提供一个默认实现:

class AbstractBase {
public:virtual void normalVirtual() {std::cout << "Normal virtual function" << std::endl;}virtual void pureVirtualWithImpl() = 0;  // 纯虚函数声明virtual ~AbstractBase() {}
};// 纯虚函数的实现必须在类外部定义
void AbstractBase::pureVirtualWithImpl() {std::cout << "Pure virtual function with implementation" << std::endl;
}class Concrete : public AbstractBase {
public:// 必须重写纯虚函数,即使基类提供了实现void pureVirtualWithImpl() override {// 可以选择调用基类的实现AbstractBase::pureVirtualWithImpl();std::cout << "Additional implementation in derived class" << std::endl;}
};int main() {Concrete obj;obj.normalVirtual();obj.pureVirtualWithImpl();return 0;
}

尽管纯虚函数可以有实现,但包含纯虚函数的类仍然是抽象类,不能被实例化。派生类必须重写所有纯虚函数才能成为非抽象类。

抽象类的构造函数

抽象类不能被实例化,但它可以有构造函数。这些构造函数只能由派生类的构造函数调用:

class AbstractBase {
protected:int data;public:AbstractBase(int val) : data(val) {std::cout << "AbstractBase constructor" << std::endl;}virtual void display() const = 0;  // 纯虚函数virtual ~AbstractBase() {std::cout << "AbstractBase destructor" << std::endl;}
};class Concrete : public AbstractBase {
public:Concrete(int val) : AbstractBase(val) {std::cout << "Concrete constructor" << std::endl;}void display() const override {std::cout << "Data: " << data << std::endl;}~Concrete() override {std::cout << "Concrete destructor" << std::endl;}
};int main() {Concrete obj(42);obj.display();return 0;
}

抽象类的构造函数通常用于初始化基类的成员变量,为派生类提供共同的初始化逻辑。

虚函数的高级用法

虚函数表的继承和覆盖

当派生类重写基类的虚函数时,虚函数表中对应的函数指针会被更新为派生类的函数地址:

class Base {
public:virtual void func1() { std::cout << "Base::func1" << std::endl; }virtual void func2() { std::cout << "Base::func2" << std::endl; }virtual void func3() { std::cout << "Base::func3" << std::endl; }
};class Derived : public Base {
public:// 重写func1void func1() override { std::cout << "Derived::func1" << std::endl; }// 保留func2// 重写func3void func3() override { std::cout << "Derived::func3" << std::endl; }
};int main() {Base* ptr = new Derived();ptr->func1();  // 输出 "Derived::func1"ptr->func2();  // 输出 "Base::func2"ptr->func3();  // 输出 "Derived::func3"delete ptr;return 0;
}

虚函数表的示意图如下:

Base vtable:         Derived vtable:
+--------------+     +--------------+
| &Base::func1 |     | &Derived::func1 |
| &Base::func2 |     | &Base::func2    |
| &Base::func3 |     | &Derived::func3 |
+--------------+     +--------------+

这种机制确保了通过基类指针或引用调用的虚函数总是调用对象的真实类型对应的函数版本。

虚拟继承

当多个派生类继承自同一个基类,然后这些派生类又被另一个类继承(菱形继承),可能导致基类的多个实例。虚拟继承可以解决这个问题:

class Animal {
protected:std::string name;public:Animal(const std::string& n) : name(n) {std::cout << "Animal constructor" << std::endl;}virtual void speak() const {std::cout << name << " makes a sound" << std::endl;}virtual ~Animal() {std::cout << "Animal destructor" << std::endl;}
};// 虚拟继承Animal
class Mammal : virtual public Animal {
public:Mammal(const std::string& n) : Animal(n) {std::cout << "Mammal constructor" << std::endl;}void speak() const override {std::cout << name << " makes a mammal sound" << std::endl;}~Mammal() override {std::cout << "Mammal destructor" << std::endl;}
};// 虚拟继承Animal
class Bird : virtual public Animal {
public:Bird(const std::string& n) : Animal(n) {std::cout << "Bird constructor" << std::endl;}void speak() const override {std::cout << name << " chirps" << std::endl;}~Bird() override {std::cout << "Bird destructor" << std::endl;}
};// Bat继承自Mammal和Bird
class Bat : public Mammal, public Bird {
public:// 必须显式调用虚基类的构造函数Bat(const std::string& n) : Animal(n), Mammal(n), Bird(n) {std::cout << "Bat constructor" << std::endl;}void speak() const override {std::cout << name << " squeaks" << std::endl;}~Bat() override {std::cout << "Bat destructor" << std::endl;}
};

在没有虚拟继承的情况下,Bat类会包含Animal类的两个实例(一个来自Mammal,另一个来自Bird),导致数据冗余和歧义。使用虚拟继承,Bat类只包含Animal类的一个共享实例。

协变返回类型

C++允许重写的虚函数返回派生类的指针或引用,而不必严格匹配基类函数的返回类型,这称为协变返回类型:

class Base {
public:virtual Base* clone() const {return new Base(*this);}virtual ~Base() {}
};class Derived : public Base {
public:// 注意返回类型是Derived*,而不是Base*Derived* clone() const override {return new Derived(*this);}
};int main() {Base* basePtr = new Derived();Base* clonedPtr = basePtr->clone();  // 实际返回Derived*,但赋值给Base*delete basePtr;delete clonedPtr;return 0;
}

协变返回类型允许派生类的虚函数返回更具体的类型,而不必使用类型转换。

抽象接口类

接口是一种只包含纯虚函数的特殊抽象类,用于定义类必须提供的功能,而不指定实现细节:

// 可绘制接口
class Drawable {
public:virtual void draw() const = 0;virtual ~Drawable() = default;
};// 可序列化接口
class Serializable {
public:virtual std::string serialize() const = 0;virtual void deserialize(const std::string& data) = 0;virtual ~Serializable() = default;
};// 实现多个接口
class Shape : public Drawable, public Serializable {
protected:int x, y;  // 位置坐标public:Shape(int xPos, int yPos) : x(xPos), y(yPos) {}virtual ~Shape() = default;// 所有派生的具体形状都需要实现这些接口方法
};class Circle : public Shape {
private:double radius;public:Circle(int x, int y, double r) : Shape(x, y), radius(r) {}// 实现Drawable接口void draw() const override {std::cout << "Drawing Circle at (" << x << ", " << y << ") with radius " << radius << std::endl;}// 实现Serializable接口std::string serialize() const override {return "Circle," + std::to_string(x) + "," + std::to_string(y) + "," + std::to_string(radius);}void deserialize(const std::string& data) override {// 解析CSV格式数据std::istringstream stream(data);std::string type;std::getline(stream, type, ',');if (type != "Circle") throw std::runtime_error("Invalid type");std::string xStr, yStr, rStr;std::getline(stream, xStr, ',');std::getline(stream, yStr, ',');std::getline(stream, rStr);x = std::stoi(xStr);y = std::stoi(yStr);radius = std::stod(rStr);}
};

接口类使我们能够实现"基于接口而非实现"的设计原则,增强代码的灵活性和可扩展性。

纯虚函数与策略模式

纯虚函数常用于实现策略模式,允许在运行时选择不同的算法:

// 排序策略接口
class SortStrategy {
public:virtual void sort(std::vector<int>& data) = 0;virtual ~SortStrategy() = default;
};// 冒泡排序实现
class BubbleSort : public SortStrategy {
public:void sort(std::vector<int>& data) override {std::cout << "Bubble sorting..." << std::endl;// 冒泡排序实现...for (size_t i = 0; i < data.size(); i++) {for (size_t j = 0; j < data.size() - 1 - i; j++) {if (data[j] > data[j + 1]) {std::swap(data[j], data[j + 1]);}}}}
};// 快速排序实现
class QuickSort : public SortStrategy {
public:void sort(std::vector<int>& data) override {std::cout << "Quick sorting..." << std::endl;quicksort(data, 0, data.size() - 1);}private:void quicksort(std::vector<int>& data, int left, int right) {// 快速排序实现...if (left < right) {int pivot = partition(data, left, right);quicksort(data, left, pivot - 1);quicksort(data, pivot + 1, right);}}int partition(std::vector<int>& data, int left, int right) {int pivot = data[right];int i = left - 1;for (int j = left; j < right; j++) {if (data[j] <= pivot) {i++;std::swap(data[i], data[j]);}}std::swap(data[i + 1], data[right]);return i + 1;}
};// 排序上下文
class SortContext {
private:SortStrategy* strategy;public:explicit SortContext(SortStrategy* strategy) : strategy(strategy) {}~SortContext() {delete strategy;}// 设置排序策略void setStrategy(SortStrategy* newStrategy) {delete strategy;strategy = newStrategy;}// 执行排序void executeSort(std::vector<int>& data) {strategy->sort(data);}
};int main() {std::vector<int> data = {5, 3, 8, 1, 2, 9, 4, 7, 6};// 使用冒泡排序SortContext context(new BubbleSort());context.executeSort(data);// 打印排序结果for (int val : data) {std::cout << val << " ";}std::cout << std::endl;// 使用相同的上下文切换到快速排序data = {5, 3, 8, 1, 2, 9, 4, 7, 6};  // 重置数据context.setStrategy(new QuickSort());context.executeSort(data);// 打印排序结果for (int val : data) {std::cout << val << " ";}std::cout << std::endl;return 0;
}

策略模式允许我们在不修改客户端代码的情况下更换算法,提高了代码的可维护性和灵活性。

虚函数调用的成本与优化

虚函数调用的开销

虚函数调用比非虚函数调用有一定的性能开销,主要原因有:

  1. 间接调用:需要通过虚函数表查找函数地址,增加了一次内存访问
  2. 阻碍内联:虚函数通常不会被内联(尽管某些情况下编译器可能会内联)
  3. 缓存不友好:虚函数表查找可能导致缓存未命中

对于性能关键的部分,应该考虑这些开销。

CRTP(奇异递归模板模式)

CRTP(Curiously Recurring Template Pattern)是一种在编译时实现多态性的技术,避免了运行时虚函数调用的开销:

template<typename Derived>
class Base {
public:void interface() {// 在编译时解析为派生类的实现static_cast<Derived*>(this)->implementation();}// 默认实现(可选)void implementation() {std::cout << "Base implementation" << std::endl;}
};class Derived1 : public Base<Derived1> {
public:void implementation() {std::cout << "Derived1 implementation" << std::endl;}
};class Derived2 : public Base<Derived2> {// 使用基类的默认实现
};int main() {Derived1 d1;Derived2 d2;d1.interface();  // 输出 "Derived1 implementation"d2.interface();  // 输出 "Base implementation"return 0;
}

CRTP通过模板和静态绑定在编译时解析函数调用,避免了运行时多态的开销,但代价是类型安全性降低和编译依赖增加。

虚函数优化

一些情况下编译器可能会优化虚函数调用:

  1. 去虚化(Devirtualization):当编译器可以确定对象的确切类型时
  2. 内联虚函数:在特定条件下,编译器可能会内联虚函数调用
  3. 分支预测:现代CPU的分支预测可以减轻虚函数调用的延迟

例如,当直接通过对象(而非指针或引用)调用虚函数时,编译器可能会进行优化:

Derived d;
d.virtualFunction();  // 可能会被优化为直接调用

然而,不应该过度依赖这些优化,应该根据实际需求选择合适的设计。

实际应用案例

图形用户界面框架

以下是一个简化的GUI框架示例,展示了虚函数和抽象类的实际应用:

// 窗口组件的抽象基类
class Widget {
protected:int x, y;int width, height;bool visible;public:Widget(int xPos, int yPos, int w, int h): x(xPos), y(yPos), width(w), height(h), visible(true) {}// 公共接口void show() { visible = true; update(); }void hide() { visible = false; update(); }void moveTo(int newX, int newY) {x = newX;y = newY;update();}void resize(int newWidth, int newHeight) {width = newWidth;height = newHeight;update();}// 纯虚函数,必须由子类实现virtual void draw() const = 0;virtual bool handleClick(int mouseX, int mouseY) = 0;// 带默认实现的虚函数virtual void update() {if (visible) {draw();}}// 访问器int getX() const { return x; }int getY() const { return y; }int getWidth() const { return width; }int getHeight() const { return height; }bool isVisible() const { return visible; }virtual ~Widget() = default;
};// 具体组件:按钮
class Button : public Widget {
private:std::string label;std::function<void()> onClick;public:Button(int x, int y, int w, int h, const std::string& text): Widget(x, y, w, h), label(text), onClick(nullptr) {}void setLabel(const std::string& text) {label = text;update();}void setOnClick(const std::function<void()>& callback) {onClick = callback;}void draw() const override {std::cout << "Drawing Button at (" << x << ", " << y<< ") with size " << width << "x" << height<< " and label \"" << label << "\"" << std::endl;}bool handleClick(int mouseX, int mouseY) override {if (mouseX >= x && mouseX < x + width &&mouseY >= y && mouseY < y + height) {std::cout << "Button \"" << label << "\" clicked" << std::endl;if (onClick) {onClick();}return true;}return false;}
};// 具体组件:文本框
class TextField : public Widget {
private:std::string text;bool focused;public:TextField(int x, int y, int w, int h): Widget(x, y, w, h), text(""), focused(false) {}void setText(const std::string& newText) {text = newText;update();}std::string getText() const {return text;}void setFocus(bool isFocused) {focused = isFocused;update();}void draw() const override {std::cout << "Drawing TextField at (" << x << ", " << y<< ") with size " << width << "x" << height<< " and text \"" << text << "\""<< (focused ? " (focused)" : "") << std::endl;}bool handleClick(int mouseX, int mouseY) override {if (mouseX >= x && mouseX < x + width &&mouseY >= y && mouseY < y + height) {setFocus(true);return true;} else {setFocus(false);return false;}}
};// 容器组件
class Panel : public Widget {
private:std::vector<std::unique_ptr<Widget>> children;public:Panel(int x, int y, int w, int h): Widget(x, y, w, h) {}void addWidget(Widget* widget) {children.emplace_back(widget);}void draw() const override {std::cout << "Drawing Panel at (" << x << ", " << y<< ") with size " << width << "x" << height << std::endl;// 绘制所有子组件for (const auto& child : children) {child->draw();}}bool handleClick(int mouseX, int mouseY) override {if (mouseX >= x && mouseX < x + width &&mouseY >= y && mouseY < y + height) {// 从最后一个(最上层)子组件开始检查点击for (auto it = children.rbegin(); it != children.rend(); ++it) {if ((*it)->handleClick(mouseX, mouseY)) {return true;  // 点击已处理}}return true;  // 点击在面板内但没有子组件处理}return false;  // 点击在面板外}
};// 窗口(最顶层容器)
class Window : public Panel {
private:std::string title;public:Window(int x, int y, int w, int h, const std::string& windowTitle): Panel(x, y, w, h), title(windowTitle) {}void draw() const override {std::cout << "Drawing Window \"" << title << "\" at ("<< x << ", " << y << ") with size "<< width << "x" << height << std::endl;// 调用基类的draw方法绘制子组件Panel::draw();}
};int main() {// 创建窗口Window window(0, 0, 800, 600, "Demo Window");// 添加一个面板Panel* panel = new Panel(50, 50, 700, 500);// 添加一个按钮Button* button = new Button(100, 100, 200, 50, "Click Me");button->setOnClick([]() {std::cout << "Button clicked callback" << std::endl;});// 添加一个文本框TextField* textField = new TextField(100, 200, 300, 30);textField->setText("Hello, World!");// 构建组件层次panel->addWidget(button);panel->addWidget(textField);window.addWidget(panel);// 绘制窗口(这将递归绘制所有组件)window.draw();// 模拟点击std::cout << "\nSimulating click at (150, 125):" << std::endl;window.handleClick(150, 125);  // 点击按钮std::cout << "\nSimulating click at (150, 215):" << std::endl;window.handleClick(150, 215);  // 点击文本框return 0;
}

这个GUI框架示例展示了:

  • 使用抽象基类Widget定义统一接口
  • 通过虚函数实现多态行为
  • 使用组合模式处理复杂的组件层次结构
  • 事件处理的实现(如处理鼠标点击)

插件系统

以下是一个简单的插件系统示例,展示了抽象类作为接口的用途:

// 插件接口
class Plugin {
public:virtual std::string getName() const = 0;virtual std::string getVersion() const = 0;virtual void initialize() = 0;virtual void shutdown() = 0;virtual bool execute(const std::string& command, std::string& result) = 0;virtual ~Plugin() = default;
};// 日志插件
class LoggerPlugin : public Plugin {
public:std::string getName() const override {return "Logger";}std::string getVersion() const override {return "1.0.0";}void initialize() override {std::cout << "Logger plugin initialized" << std::endl;}void shutdown() override {std::cout << "Logger plugin shutdown" << std::endl;}bool execute(const std::string& command, std::string& result) override {if (command == "log") {result = "Log message recorded";return true;}return false;}
};// 数据库插件
class DatabasePlugin : public Plugin {
private:bool connected;public:DatabasePlugin() : connected(false) {}std::string getName() const override {return "Database";}std::string getVersion() const override {return "2.1.5";}void initialize() override {std::cout << "Database plugin connecting..." << std::endl;connected = true;std::cout << "Database plugin initialized" << std::endl;}void shutdown() override {if (connected) {std::cout << "Database plugin disconnecting..." << std::endl;connected = false;}std::cout << "Database plugin shutdown" << std::endl;}bool execute(const std::string& command, std::string& result) override {if (!connected) {result = "Database not connected";return false;}if (command == "query") {result = "Query executed successfully";return true;} else if (command == "update") {result = "Update executed successfully";return true;}return false;}
};// 插件管理器
class PluginManager {
private:std::map<std::string, std::unique_ptr<Plugin>> plugins;public:// 注册插件void registerPlugin(Plugin* plugin) {std::string name = plugin->getName();if (plugins.find(name) != plugins.end()) {std::cerr << "Plugin '" << name << "' already registered" << std::endl;delete plugin;return;}std::cout << "Registering plugin: " << name << " (version " << plugin->getVersion() << ")" << std::endl;plugins[name] = std::unique_ptr<Plugin>(plugin);}// 初始化所有插件void initializeAll() {for (auto& pair : plugins) {std::cout << "Initializing plugin: " << pair.first << std::endl;pair.second->initialize();}}// 关闭所有插件void shutdownAll() {for (auto& pair : plugins) {std::cout << "Shutting down plugin: " << pair.first << std::endl;pair.second->shutdown();}}// 执行插件命令bool executeCommand(const std::string& pluginName, const std::string& command, std::string& result) {auto it = plugins.find(pluginName);if (it == plugins.end()) {result = "Plugin not found: " + pluginName;return false;}return it->second->execute(command, result);}// 获取插件列表std::vector<std::string> getPluginNames() const {std::vector<std::string> names;for (const auto& pair : plugins) {names.push_back(pair.first);}return names;}
};int main() {PluginManager manager;// 注册插件manager.registerPlugin(new LoggerPlugin());manager.registerPlugin(new DatabasePlugin());// 初始化插件manager.initializeAll();// 获取插件列表std::cout << "\nAvailable plugins:" << std::endl;for (const auto& name : manager.getPluginNames()) {std::cout << "- " << name << std::endl;}// 执行插件命令std::string result;std::cout << "\nExecuting commands:" << std::endl;if (manager.executeCommand("Logger", "log", result)) {std::cout << "Logger command succeeded: " << result << std::endl;} else {std::cout << "Logger command failed: " << result << std::endl;}if (manager.executeCommand("Database", "query", result)) {std::cout << "Database command succeeded: " << result << std::endl;} else {std::cout << "Database command failed: " << result << std::endl;}if (manager.executeCommand("Unknown", "test", result)) {std::cout << "Unknown command succeeded: " << result << std::endl;} else {std::cout << "Unknown command failed: " << result << std::endl;}// 关闭插件std::cout << "\nShutting down:" << std::endl;manager.shutdownAll();return 0;
}

这个插件系统示例展示了:

  • 使用抽象类Plugin定义统一接口
  • 不同插件通过实现接口提供功能
  • 插件管理器处理插件的注册、初始化和命令执行

最佳实践

何时使用虚函数

  1. 当需要运行时多态性时:基类指针或引用需要调用派生类的方法
  2. 当设计接口时:定义一组必须由派生类实现的操作
  3. 当基类无法提供有意义的默认实现时:使用纯虚函数强制派生类提供实现

何时使用抽象类

  1. 定义接口:抽象类适合定义必须由派生类实现的操作集合
  2. 提供部分实现:抽象类可以包含一些实现,减少派生类中的重复代码
  3. 强制实现特定方法:通过纯虚函数强制派生类提供特定功能的实现

设计技巧

  1. 遵循接口隔离原则:接口应该小而专注,不应强制实现不需要的方法
  2. 考虑是否需要虚析构函数:任何带有虚函数的类都应该有虚析构函数
  3. 使用override明确标记重写的函数:提高可读性并防止错误
  4. 避免过深的继承层次:过深的继承层次会增加复杂性和脆弱性
  5. 注意性能考虑:虚函数调用有一定开销,在性能关键的代码中考虑替代方案
  6. 适当使用final:当不希望进一步重写或继承时使用final关键字

常见陷阱

  1. 忽略虚析构函数:导致资源泄漏
  2. 意外隐藏虚函数:派生类中的函数签名与基类不匹配
  3. 构造函数中调用虚函数:在构造过程中,对象还不是完全的派生类类型
  4. 复杂的多重继承:导致代码难以理解和维护

总结

虚函数和抽象类是C++中实现多态性的核心机制,它们允许我们定义统一的接口,同时支持不同的实现。虚函数使派生类能够重写基类的方法,而抽象类通过纯虚函数定义了必须由派生类实现的接口。

本文探讨了虚函数的工作原理(虚函数表机制)、纯虚函数与抽象类的用途、虚函数的高级用法(如协变返回类型和虚拟继承)、虚函数调用的成本与优化,以及在实际项目中的应用(如GUI框架和插件系统)。

正确理解和使用虚函数与抽象类,可以帮助我们设计出更灵活、可扩展的面向对象系统,同时避免常见的陷阱和性能问题。随着对这些概念的掌握,您将能够更好地利用C++的强大特性,创建更加优雅和高效的代码。

在下一篇文章中,我们将探讨C++的接口设计,这是基于抽象类和虚函数的更高层次抽象,用于定义对象之间的交互方式。


这是我C++学习之旅系列的第十四篇技术文章。查看完整系列目录了解更多内容。

相关文章:

C++学习:六个月从基础到就业——面向对象编程:虚函数与抽象类

C学习&#xff1a;六个月从基础到就业——面向对象编程&#xff1a;虚函数与抽象类 本文是我C学习之旅系列的第十四篇技术文章&#xff0c;主要探讨C中的虚函数与抽象类&#xff0c;这是实现多态性的核心机制。查看完整系列目录了解更多内容。 引言 多态性是面向对象编程的三大…...

git分支操作

一、git branch&#xff1a;分支管理 1. 查看分支 git branch # 查看本地分支&#xff08;* 表示当前分支&#xff09; git branch -a # 查看所有分支&#xff08;本地远程&#xff09; git branch -vv # 查看分支跟踪关系 2. 创建/删除分支…...

IDEA 中 Scala 项目远程连接虚拟机 Spark 环境

IDEA 中 Scala 项目远程连接虚拟机 Spark 环境 1. 环境准备 确保虚拟机 Spark 环境正常运行 虚拟机中已安装并启动 Spark记录虚拟机的 IP 地址和 Spark 端口&#xff08;默认 7077&#xff09;确保虚拟机防火墙允许相关端口访问 本地 IDEA 环境配置 安装 Scala 插件安装 Spar…...

2. 判断列表元素的单一性

【问题描述】编写程序&#xff0c;判断一个列表中的各个元素如果相同(例如[2,2,2,2,2]),则输出True&#xff0c;不相同(例如[1,2,3,2,3])则输出False。 【输入形式】ainput() 【输出形式】用print()函数 【样例输入】 [2,2,2,2,2] 【样例输出】 True 【样例输入】 [1,2,…...

King3399(ubuntu文件系统)GDB/GDBServer调试配置

0 引言 最近在用king3399进行驱动开发&#xff0c;即使是一些简单的外设也不免反复修改与烧录&#xff0c;若仅仅通过printk这种方法对程序进行打印调试&#xff0c;其过程也是相当复杂&#xff0c;因此想通过GDB/GDBServer的方式进行调试&#xff0c;本文主要是记录配置过程的…...

机器学习简介

目录 机器学习简介机器学习的大致分类监督学习 (Supervised learning)RegressionClassification / Predict categories 无监督学习 (Unsupervised learning)Clustering algorithmAnomaly DetectionDimensionality Reduction对比总结 强化学习 (Reinforcement learning)强化学习…...

k8s调度器:如何控制Pod的分布

引言&#xff1a;从“随机分配”到“智能调度” 想象你的Kubernetes集群是一个繁忙的物流中心&#xff0c;节点&#xff08;Node&#xff09;是仓库&#xff0c;Pod是货物。 默认调度器 就像一名普通分拣员&#xff0c;简单地将货物塞进最近的仓库&#xff0c;可能导致某些仓…...

机器学习在催化剂设计中的应用理论加实操

背景介绍​​ 数据智能驱动&#xff0c;催化理性设计新纪元​​ 催化材料设计是能源转化、化工合成及环境治理等领域的核心挑战。传统催化研究主要依赖密度泛函理论(DFT)计算与实验试错法&#xff0c;通过量子力学模拟揭示活性位点电子结构&#xff0c;结合高通量实验筛选候选…...

Spring Cloud Alibaba微服务-微服务介绍和搭建

1. 课程介绍 单体服务中有订单&#xff0c;用户&#xff0c;库存&#xff0c; 两个缺陷&#xff1a; a. 是以应用的维度进行负载均衡&#xff0c;资源占用大 b. 当其中一个模块宕机&#xff0c;整个应用就不能用了&#xff1b; nacos&#xff1b;ribbon&#xff0c;loadBa…...

量子通信应用:量子安全物联网(三)协议融合

第一部分:引言与概述 1.1 量子安全物联网的背景与必要性 随着物联网(IoT)设备的爆炸式增长(预计2030年全球连接设备超750亿台),传统安全机制(如RSA、ECC加密)正面临量子计算的颠覆性威胁。量子计算机的Shor算法可在多项式时间内破解非对称加密体系,而Grover算法则对…...

JUC学习(1) 线程和进程

2.线程和进程 线程&#xff0c;进程进程&#xff1a;一个程序。 一个进程往往可以包含多个线程&#xff0c;至少包含一个&#xff01; Java默认有2个线程 mainGC 对于Java而言&#xff0c;三种开启线程的方式 ThreadRunnableCallable Java真的可以开启线程吗 不可以&am…...

Java基础系列-LinkedList源码解析

文章目录 简介LinkedList 插入和删除元素的时间复杂度&#xff1f;LinkedList 为什么不能实现 RandomAccess 接口&#xff1f; LinkedList 源码分析Node 定义初始化获取元素插入元素删除元素遍历链表 简介 LinkedList 是一个基于双向链表实现的集合类&#xff0c;经常被拿来和…...

pycharm无法识别到本地python的conda环境解决方法

问题一 现象描述&#xff1a; 本地已经安装了conda&#xff0c;但在pycharm中选择conda环境却识别不到&#xff0c; 解决方法&#xff1a;手动输入conda path&#xff0c;点击R eload environments基本就能修复&#xff0c;比如我的路径如下 /Users/test/conda/miniconda3/b…...

【机器人创新创业应需明确产品定位与方向指南】

机器人领域的创新创业, 需要对公司和产品的定位和生态进行深入思考, 明确其定位与发展目标, 明确产品在是为G、为B还是为C进行服务。 本文引用地址&#xff1a;https://www.eepw.com.cn/article/202504/469401.htm 超前的、探索性的创新技术一般是面向G端, 而不是面向B端或者C…...

《似锦》:画饼之—你画给我我画给你

甄珩&#xff0c;看似刚正不阿&#xff0c;正得发邪&#xff0c;一板一眼的严肃角色 可是每次余七和甄珩在一起&#xff0c;就是一部行走的喜剧&#xff0c;众网友称他们为“甄儿八锦” 《似锦》剧集精彩片段&#xff1a;甄珩余七爆笑修罗场&#xff08;四&#xff09; 谁懂这…...

鸿蒙系统开发中路由使用详解

鸿蒙系统提供了两种主要的路由机制&#xff1a;传统的Router模块和组件化的Navigation容器。下面我将详细介绍这两种路由方式的使用方法、区别以及实际应用示例。 一、Router模块基础使用 Router是鸿蒙早期提供的页面路由模块&#xff0c;通过URL实现页面跳转和数据传递。 1…...

拖拉拽效果加点击事件

<!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><title>自由拖拽点击元素</title><style>body {margin: 0;height: 100vh;display: flex;justify-content: center;align-items: center;backgr…...

Ubuntu利用docker搭建Java相关环境记录(二)

Ubuntu利用docker搭建Java相关环境记录&#xff08;二&#xff09; 接上篇&#xff1a;Ubuntu利用docker搭建Java相关环境记录&#xff08;一&#xff09; 启动Docker 1. 查看Docker容器 已启动的容器 docker ps所有容器 docker ps -a本人很懒并不想一直敲命令操作&#…...

2025华中杯B题——AI实现

以下内容全文由以下网站AI实现&#xff0c;内容和代码仅供参考 如需实现自己的需求和目标&#xff0c;请使用网站自行调试。 参考写作 1. 共享单车数量与分布估算 问题分析 本题要求根据校园共享单车在各停车点的不同时段统计数据&#xff0c;估算校园内共享单车总量&#…...

【软考-系统架构设计师】OSI体系解析

一、OSI体系的核心定义 OSI&#xff08;Open System Interconnection&#xff09;模型是国际标准化组织&#xff08;ISO&#xff09;于1984年提出的网络通信分层框架&#xff0c;旨在解决异构网络系统间的兼容性问题。它将复杂的网络通信过程划分为七层&#xff0c;每层独立完…...

用手机也能打《无畏契约》?登录ToDesk即可开玩

《无畏契约》火到出圈&#xff01;但手机玩家只能干瞪眼&#xff1f; 作为拳头游戏继《英雄联盟》后的又一爆款&#xff0c;《无畏契约》凭借快节奏的战术对抗和全球化的地图设计&#xff08;比如东京“霓虹町”、百慕大“微风岛屿”&#xff09;&#xff0c;迅速成为电竞圈的顶…...

jmeter提取返回值到文件

前言 如何将请求的返回值&#xff0c;保存到本地文件&#xff0c;有具体以下3种方式。 保存到响应文件BeanShell 取样器BeanShell 后置处理程序 一、监听器–保存响应到文件 1、提取全部返回值&#xff0c;&#xff08;.json&#xff09;格式 2、保存到响应文件 添加----…...

iPaaS集成平台在电商行业的五大核心应用场景

在电商行业“多平台运营、多系统并行”的竞争格局下&#xff0c;订单激增、数据割裂、跨系统协作低效等问题成为企业增长的隐形阻碍。谷云科技作为国内领先的iPaaS&#xff08;集成平台即服务&#xff09;技术厂商&#xff0c;通过低代码、高扩展的集成能力&#xff0c;帮助电商…...

猪行为视频数据集

猪行为数据集包含 23 天(超过 6 周)的日间猪行为视频,这些视频由近乎架空的摄像机拍摄。视频已配准颜色和深度信息。数据以每秒 6 帧的速度捕获,并以 1800 帧(5 分钟)为一批次进行存储。大多数帧显示 8 头猪。 这里可以看到颜色和深度图像的示例: 喂食器位于图片底部中…...

在conda环境下使用pip安装库无法import

安装seleniumwire包&#xff0c;conda环境没有&#xff0c;pip之后安装不到当前conda环境 网上的方法都试过了&#xff0c;包括强制安装等 python -m pip install --upgrade --force-reinstall selenium-wire 最后定位应该是没有安装到当前conda的环境下&#xff0c;使用list…...

[net 6] udp_chat_server基于udp的简单聊天室(多线程的服务器与业务相分离)

目录 1. 网络聊天室的意义 2. 网络聊天室了解 2.1. 网络聊天室模块分析 2.2. 目标 3. 基本框架 3.1. 文件基本框架 3.2. 设计回调函数解耦 4. Route.hpp 模块(消息转发) 4.1. 头文件包含 4.2. 基本类框架 4.3. Route::Forward() 转发 4.3.1. 函数头设计 4.3.2. 维护…...

驱动-自旋锁

前面原子操作进行了讲解&#xff0c; 并使用原子整形操作对并发与竞争实验进行了改进&#xff0c;但是原子操作只能对整形变量或者位进行保护&#xff0c; 而对于结构体或者其他类型的共享资源&#xff0c; 原子操作就力不从心了&#xff0c; 这时候就轮到自旋锁的出场了。 两个…...

TDengine 存储引擎剖析:数据文件与索引设计(二)

TDengine 索引设计 索引设计关键特性 TDengine 的索引设计采用了多种技术和策略&#xff0c;以满足时序数据高效存储和快速查询的需求&#xff0c;具有以下关键特性&#xff1a; 多级时间戳压缩索引&#xff1a;TDengine 使用了时间戳压缩索引技术&#xff0c;能够有效减少索…...

基于Python的医疗质量管理指标智能提取系统【2025代码版】

系统概述 本系统旨在帮助医疗质量管理部从医院信息系统(HIS)中智能提取《2025年国家医疗质量安全改进目标》中的关键指标数据。系统采用Python编程语言,结合现代数据处理库,实现高效、准确的数据提取与分析功能。 import json import logging import logging.handlers impo…...

中介者模式(Mediator Pattern)

中介者模式(Mediator Pattern)是一种行为型设计模式。它通过引入一个中介者对象,来封装一系列对象之间的交互,使这些对象之间不再直接相互引用和通信,而是通过中介者进行间接通信,从而降低对象之间的耦合度,提高系统的可维护性和可扩展性。 一、基础 1. 意图 核心目的…...

Hbuilder 上的水印相机实现方案 (vue3 + vite + hbuilder)

效果 思路 通过 live-pusher 这个视频推流的组件来获取摄像头拿到视频的一帧图片之后&#xff0c;跳转到正常的 vue 页面&#xff0c;通过 canvas 来处理图片水印 源码 live-pusher 这个组件必须是 nvue 的 至于什么是 nvue&#xff0c;看这个官方文档吧 https://uniapp.dcl…...

聊聊Spring AI Alibaba的PdfTablesParser

序 本文主要研究一下Spring AI Alibaba的PdfTablesParser PdfTablesParser community/document-parsers/spring-ai-alibaba-starter-document-parser-pdf-tables/src/main/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParser.java public class PdfTablesParser…...

二分查找-LeetCode

题目 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target&#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 示例 1: 输入: nums [-1,0,3,5,9,12], target 9 输出: 4 解释: …...

StarRocks Community Monthly Newsletter (Mar)

版本动态 3.4.1 版本更新 核心功能升级 数据安全与权限管控 支持「安全视图」功能&#xff0c;严格管控视图查询权限 MySQL协议连接支持SSL认证&#xff0c;保障数据传输安全 存算分离架构增强 支持自动创建Snapshot&#xff08;集群恢复更便捷&#xff09; Storage Volu…...

STM32+dht11+rc522+jq8400的简单使用

1.dht11的使用 硬件&#xff1a;3v3&#xff0c;gnd&#xff0c;data数据线接一个gpio&#xff0c;三根线即可 软件&#xff1a; ①dht11.c #include "dht11.h" #include "delay.h" #include "stdbool.h"static STRUCT_DHT11_TYPEDEF dht11;…...

mpstat指令介绍

文章目录 1. 功能介绍2. 语法介绍3. 应用场景4. 实际举例 1. 功能介绍 mpstat 英文全称( Multi-Processor Statistics)&#xff0c;多处理器统计信息的含义。 下面大致说一下功能作用&#xff1a; 多核性能监控 可实时监控每个 CPU 核心的利用率、中断频率、上下文切换等指标&…...

网络层IP协议知识大梳理

全是通俗易懂的讲解&#xff0c;如果你本节之前的知识都掌握清楚&#xff0c;那就速速来看我的IP协议笔记吧~ 自己写自己的八股&#xff01;让未来的自己看懂&#xff01; &#xff08;全文手敲&#xff0c;受益良多&#xff09; 网路基础3 网路层 TCP并没有把数据发到网路…...

Linux-codec

codec原理图 codec接口 ①音频输入接口&#xff0c;连接mic ②音频输出接口&#xff0c;连接speaker ③sai/i2s接口&#xff0c;连接soc&#xff0c;soc和codec互发音频数据 ④i2c接口&#xff0c;连接soc&#xff0c;soc配置codecsai音频接口 MCLK&#xff1a;主时钟&#x…...

HTTP协议与web服务器

HTTP协议与web服务器 目录 一、浏览器与服务器通信过程 1.1 域名解析与连接建立 1.2 数据交互 1.3 连接管理 二、HTTP请求报头 2.1 请求行 2.2 请求报头 2.3 空行 2.4 请求体 三、HTTP应答报头 3.1 http应答报文头部信息 1. 状态行 2. 服务器名称 3. 数据长度 4…...

ECharts散点图-散点图7,附视频讲解与代码下载

引言&#xff1a; ECharts散点图是一种常见的数据可视化图表类型&#xff0c;它通过在二维坐标系或其它坐标系中绘制散乱的点来展示数据之间的关系。本文将详细介绍如何使用ECharts库实现一个散点图&#xff0c;包括图表效果预览、视频讲解及代码下载&#xff0c;让你轻松掌握…...

蓝桥杯之二分法(二)

存在某条件使得一边均满足&#xff0c;一边均不满足&#xff1a; 如果问题满足某种条件&#xff0c;使得在某个点之前的所有值都满足条件&#xff0c;而之后的所有值都不满足条件&#xff08;或反之&#xff09;&#xff0c;那么可以使用二分法来找到这个边界。 1.问题的解具有…...

当 AI 有了 “万能插头” 和 “通用语言”:MCP 与 A2A 如何重构智能体生态

目录 一、MCP&#xff1a;让 AI 拥有 “万能工具插头” 1.1 从 “手工对接” 到 “即插即用” 1.2 架构解密&#xff1a;AI 如何 “指挥” 工具干活 1.3 安全优势&#xff1a;数据不出门&#xff0c;操作可追溯 二、A2A&#xff1a;让智能体学会 “跨语言协作” 2.1 从 “…...

从零开始 保姆级教程 Ubuntu20.04系统安装MySQL8、服务器配置MySQL主从复制、本地navicat远程连接服务器数据库

从零开始&#xff1a;Ubuntu 20.04 系统安装 MySQL 8、服务器配置 MySQL 主从复制、本地 Navicat 远程连接服务器数据库 初始化服务器1. 更新本地软件包列表2. 安装 MySQL 服务器3. 查看 MySQL 安装版本4. 登录 MySQL 管理终端5. 设置 root 用户密码&#xff08;推荐使用 nativ…...

PHP序列化/反序列化漏洞原理

PHP反序列化原理详解 引言 PHP反序列化是PHP中一个重要的概念&#xff0c;它允许将序列化后的数据重新转换为原始的数据结构。在PHP中&#xff0c;可以使用serialize()函数将数据序列化为字符串&#xff0c;然后使用unserialize()函数将序列化后的字符串反序列化为原来的数据结…...

并查集(力扣2316)

这种涉及不同连通分量的&#xff0c;看上去就可以用并查集。并查集的模板请参见上一篇内容。并查集&#xff08;力扣1971&#xff09;-CSDN博客 现在我们要求的是无法互相到达的点对。根据观察易得&#xff0c;我们只需要求出每个并查集的元素数量&#xff0c;然后遍历每个点&…...

【web服务_负载均衡Nginx】一、Nginx 基础与核心概念解析

一、Nginx 概述&#xff1a;从起源到行业地位​ Nginx&#xff08;发音为 “engine x”&#xff09;是一款高性能的开源 Web 服务器、反向代理服务器&#xff0c;同时具备负载均衡、内容缓存、TCP/UDP 代理及邮件代理等功能。它由俄罗斯工程师伊戈尔・赛索耶夫&#xff08;Igo…...

【Python入门】文件读取全攻略:5种常用格式(csv/excel/word/ppt/pdf)一键搞定 | 附完整代码示例

大家好&#xff0c;我是唐叔&#xff01;今天给大家带来一篇Python文件读取的终极指南。无论是数据分析、办公自动化还是爬虫开发&#xff0c;文件读取都是Python程序员必须掌握的核心技能。本文将详细介绍Python处理5大常用文件格式的方法&#xff0c;包含完整可运行的代码示例…...

考研系列-计算机网络冲刺考点汇总(下)

写在前面 本文将总结王道408考研课程的计算机网络冲刺考点的第四章到第六章内容&#xff08;网络层、传输层、应用层&#xff09;。 第四章、网络层 1.SDN SDN的基本概念 注意对应关系&#xff1a;数据平面-转发&#xff1b;控制平面-路由选择 2.路由选择算法 (1)RIP协议-基于…...

GitLab-CI集成FTP自动发布

简介 在某些场景下&#xff0c;代码是以 FTP 的方式部署到服务器上&#xff0c;那么我们可以使用 GitLab-CI 来实现自动发布。 配置参考 .sftp-deploy: &sftp-deploy |-files$(git log -10 --prettyformat: --name-only | grep -v ^$ | sort -u)include_patterns$(echo …...

Ubuntu 安装cuda踩坑记录

Ubuntu 安装cuda踩坑记录&#xff1a; 运行run文件时出错&#xff1a; sh cuda_12.4.0_550.54.14_linux.run 报错&#xff1a; ./cuda-installer: error while loading shared libraries: libxml2.so.2: cannot open shared object file: No such file or directory 解决&am…...