自学内容网 自学内容网

C++中多态性在实际项目中的应用场景有那些?以及override关键字的用法。

在讲解多态在实际项目中的应用场景时,我们先来了解一下override这个关键字。

override

在 C++ 中,override 关键字用于指示一个虚函数是重写(override)基类中的同名虚函数的实现。override 关键字是 C++11 引入的,旨在提高代码的可读性和安全性。

使用override的好处

  • 提高代码可读性:使用 override 可以明确表明某个函数是用于重写基类的虚函数,这样其他开发者就能快速理解这个方法的目的。

  • 编译检查:如果您在基类中声明的虚函数签名发生改变,或者您的重写函数与基类的签名不匹配,编译器将会报错。这种编译期检查可以帮助您避免常见的错误。

示例

#include <iostream>  

class Base {  
public:  
    // 声明一个虚函数  
    virtual void show() {  
        std::cout << "Base class show function called." << std::endl;  
    }  
    
    virtual ~Base() = default; // 虚析构函数  
};  

class Derived : public Base {  
public:  
    // 使用 override 关键字重写基类的虚函数  
    void show() override {   
        std::cout << "Derived class show function called." << std::endl;  
    }  
};  

int main() {  
    Base* b = new Derived(); // 基类指针指向派生类对象  
    b->show(); // 调用重写的函数  
    delete b; // 清理资源  
    return 0;  
}

输出

Derived class show function called.

注意事项

**1.如果不使用 override:**即便我们在 Derived 类中定义了一个与 Base 中的 show() 函数同名但没有使用 override,编译器仍然可以编译通过。如果基类 show() 函数重命名、参数类型改变或返回类型改变,Derived 类的 show() 将不再覆盖基类的 show(),这个潜在的错误在编译时是不会被发现的。

class IncorrectDerived : public Base {  
public:  
    // 这是一个新的函数,并不是重写基类的 show  
    void show(int x) {   
        std::cout << "Incorrect Derived class show function called." << std::endl;  
    }  
};

**2.仅适用于虚函数:**只有被声明为虚函数的成员函数才能使用 override 关键字。

**3.使用 final 关键字结合:**如果您不想让某个虚函数被进一步重写,可以使用 final 关键字与 override 结合:

class FinalDerived : public Base {  
public:  
    void show() override final {   
        std::cout << "Final derived class show function called." << std::endl;  
    }  
};

final关键字防止继承,防止重写,只能在虚函数的声明和类的定义中使用,并且不能从被标记为final的类继承

应用场景

接口与抽象类

在C++中定义一个抽象类(包含至少一个纯虚函数)作为多个类的基类,通过多态性允许这些类实现公共接口。例如,图形库中可能定义一个Shape抽象类,所有形状(如Circle、Rectangle等)都继承自这个类并实现其draw()方法。

class Shape {  
public:  
    virtual void draw() = 0; // 纯虚函数  
};  

class Circle : public Shape {  
public:  
    void draw() override { /* 绘制圆形代码 */ }  
};  

class Rectangle : public Shape {  
public:  
    void draw() override { /* 绘制矩形代码 */ }  
};  

// 使用多态性  
void renderShape(Shape* shape) {  
    shape->draw(); // 调用具体实现  
}

动态绑定

在运行时根据对象的实际类型选择调用相应的方法。比如,在实现图形界面时,可以使用基类指针或引用来操控具体的图形对象,而不需要在编译时确定具体哪个类对象。

#include <iostream>  

// 基类  
class Animal {  
public:  
    // 虚函数,允许动态绑定  
    virtual void speak() {  
        std::cout << "Animal speaks!" << std::endl;  
    }  
};  

// 派生类 Dog  
class Dog : public Animal {  
public:  
    void speak() override { // 重写基类的虚函数  
        std::cout << "Woof!" << std::endl;  
    }  
};  

// 派生类 Cat  
class Cat : public Animal {  
public:  
    void speak() override { // 重写基类的虚函数  
        std::cout << "Meow!" << std::endl;  
    }  
};  

int main() {  
    Animal* animal1 = new Dog(); // 基类指针指向派生类对象  
    Animal* animal2 = new Cat(); // 基类指针指向另一个派生类对象  

    // 动态绑定:在运行时决定调用哪个版本的speak()  
    animal1->speak(); // 输出: Woof!  
    animal2->speak(); // 输出: Meow!  

    // 清理动态分配的内存  
    delete animal1;  
    delete animal2;  

    return 0;  
}

代码解析

1.基类和派生类的定义:

  • 定义了一个基类 Animal,其中包含一个虚函数 speak()。虚函数的声明通过 virtual 关键字实现,表示此函数可以被派生类重写。
  • Dog 和 Cat 是 Animal 的两个派生类,它们各自实现了 speak() 函数。

2.动态绑定的实现在主函数中:

  • 使用 Animal* 类型的指针分别指向 Dog 和 Cat 类的对象。此时,虽然使用的是基类指针,但由于 speak() 被声明为虚函数,程序在运行时根据对象的实际类型来决定调用哪个版本的 speak() 方法。
  • 调用 animal1->speak() 和 animal2->speak() 时,程序会输出 Dog 和 Cat 各自的实现。

3.内存管理:

  • 在动态分配对象后,代码还通过 delete 删除指针,防止内存泄漏。

关键点

  • 虚函数:在基类中声明为虚函数,以便允许在派生类中重新定义。
  • 基类指针/引用:通过基类指针或引用来调用派生类的函数,这是实现动态绑定的方式。
  • 运行时决策:动态绑定在程序运行时决定调用哪一个具体实现,而不是在编译时。

回调机制

多态可以用于实现回调机制,通过基类指针或引用传递不同类型的对象,让同一段代码能够调用不同对象的行为。这在事件驱动编程(如 GUI 编程)非常常见。

class EventListener {  
public:  
    virtual void onEvent() = 0;  
};  

class Button : public EventListener {  
public:  
    void onEvent() override { /* 按钮事件处理 */ }  
};  

class Menu : public EventListener {  
public:  
    void onEvent() override { /* 菜单事件处理 */ }  
};  

// 注册事件处理  
void registerListener(EventListener* listener) {  
    // 根据需要触发事件  
}

策略模式

多态性常用于设计模式中,比如策略模式。在这种模式下,不同的策略(算法)可以实现同一接口,因此可以在运行时使用不同的策略。

class Strategy {  
public:  
    virtual void execute() = 0;  
};  

class ConcreteStrategyA : public Strategy {  
public:  
    void execute() override { /* A 策略实现 */ }  
};  

class ConcreteStrategyB : public Strategy {  
public:  
    void execute() override { /* B 策略实现 */ }  
};  

// 根据上下文选择策略  
void context(Strategy* strategy) {  
    strategy->execute();  
}

插件系统

在构建可扩展的应用程序时,可以使用多态性来实现插件机制。定义一个基类接口,通过多态性加载不同的插件模块,使得核心代码不需要知道具体的插件实现。

#include <iostream>  
#include <vector>  
#include <memory>  

// 插件接口  
class Plugin {  
public:  
    // 插件应实现的虚拟方法  
    virtual void execute() = 0;  
    virtual ~Plugin() = default;  // 确保基类可以被安全析构  
};  

// 插件管理器类  
class PluginManager {  
private:  
    std::vector<std::unique_ptr<Plugin>> plugins;  // 存储插件的动态数组  

public:  
    // 注册插件  
    void registerPlugin(std::unique_ptr<Plugin> plugin) {  
        plugins.push_back(std::move(plugin)); // 使用智能指针管理内存  
    }  

    // 执行所有注册的插件  
    void executePlugins() {  
        for (const auto& plugin : plugins) {  
            plugin->execute();  // 调用每个插件的 execute 方法  
        }  
    }  
};  

// 具体插件实现1  
class HelloWorldPlugin : public Plugin {  
public:  
    void execute() override {  
        std::cout << "Hello, World!" << std::endl;  // 插件逻辑  
    }  
};  

// 具体插件实现2  
class GoodbyePlugin : public Plugin {  
public:  
    void execute() override {  
        std::cout << "Goodbye!" << std::endl;  // 插件逻辑  
    }  
};  

int main() {  
    PluginManager manager;  // 创建插件管理器  

    // 创建并注册插件  
    manager.registerPlugin(std::make_unique<HelloWorldPlugin>());  
    manager.registerPlugin(std::make_unique<GoodbyePlugin>());  

    // 执行所有注册的插件  
    manager.executePlugins();  

    return 0;  
}

//输出    Hello, World!  
// Goodbye!

代码解析

1.插件接口 Plugin:

  • 这是一个抽象基类,定义了一个纯虚方法 execute(),每个插件都必须实现这个方法。

2.插件管理器 PluginManager:

  • 这个类管理所有插件,使用 std::vector 存储插件的智能指针 (std::unique_ptr)。
  • registerPlugin 方法用于注册新的插件。
  • executePlugins 方法遍历注册的插件并执行它们的 execute() 方法。

3.具体插件实现:

  • HelloWorldPlugin 和 GoodbyePlugin 类实现了 Plugin 接口,分别在 execute() 方法中输出不同的消息。

4.main 函数:

  • 创建 PluginManager 实例,注册具体插件,然后调用 executePlugins() 执行所有插件,输出结果。

游戏开发

在游戏开发中,多态性广泛用于不同类型的游戏对象(如玩家、敌人、道具等)。通过基类GameObject,实现各种对象的行为,例如移动、绘制等,这样游戏引擎可以轻松管理不同类型的对象。

class GameObject {  
public:  
    virtual void update() = 0; // 更新游戏对象  
};  

class Player : public GameObject {  
public:  
    void update() override { /* 更新玩家状态 */ }  
};  

class Enemy : public GameObject {  
public:  
    void update() override { /* 更新敌人状态 */ }  
};

测试与模拟

在单元测试中,多态性允许创建模拟对象(mock objects),使得测试更加灵活。通过继承接口,可以在不依赖于具体实现的情况下,进行功能测试。
以下是一个使用 C++ 和 Google Test 库来进行单元测试与模拟的示例。

1.被测试的类(被测试的组件)
假设我们有一个简单的计算器类,依赖于一个外部服务(例如,一个 Logger 类)来记录日志。

// Calculator.h  
#ifndef CALCULATOR_H  
#define CALCULATOR_H  

class Logger {  
public:  
    virtual void log(const std::string& message) = 0;  
    virtual ~Logger() = default;  
};  

class Calculator {  
public:  
    Calculator(Logger* logger) : logger_(logger) {}  

    int add(int a, int b) {  
        int result = a + b;  
        logger_->log("Add operation: " + std::to_string(result));  
        return result;  
    }  

private:  
    Logger* logger_;  // 外部依赖  
};  

#endif // CALCULATOR_H

2. 使用 Google Mock 创建 Logger 的模拟类
接下来,创建一个 Logger 的模拟类,以便我们在测试中可以代替真实的日志记录。

// MockLogger.h  
#ifndef MOCKLOGGER_H  
#define MOCKLOGGER_H  

#include <gmock/gmock.h>  
#include <string>  

class MockLogger : public Logger {  
public:  
    MOCK_METHOD(void, log, (const std::string& message), (override));  
};  

#endif // MOCKLOGGER_H

3. 编写单元测试
最后,编写单元测试来测试 Calculator 类,确保它在调用 add 方法时正确调用 Logger 的 log 方法。

// CalculatorTest.cpp  
#include "Calculator.h"  
#include "MockLogger.h"  
#include <gtest/gtest.h>  
#include <gmock/gmock.h>  

using ::testing::AtLeast;     // Google Mock 的测试断言  
using ::testing::StrEq;  

TEST(CalculatorTest, AddCallsLogger) {  
    MockLogger mock_logger;  // 创建 MockLogger 实例  
    Calculator calculator(&mock_logger);  

    // 设置期望,assert log 方法应该被调用,并且记录的消息是 "Add operation: 5"  
    EXPECT_CALL(mock_logger, log(StrEq("Add operation: 5")))  
        .Times(1);  // 期望调用一次  

    // 调用 add 方法  
    calculator.add(2, 3);  // 2 + 3 = 5  

    // Google Test 会自动检查预期的调用是否发生  
}  

int main(int argc, char** argv) {  
    ::testing::InitGoogleTest(&argc, argv);  
    return RUN_ALL_TESTS();  // 运行所有测试  
}

代码解析

1.被测试的 Calculator 类:

  • Calculator 类有一个依赖于 Logger 的构造函数。其主要功能是执行加法并记录结果。

2.Logger 接口:

  • 定义了一个日志记录的接口,实际上会在测试中通过模拟类实现。

3.模拟类 MockLogger:

  • 继承自 Logger 接口,并使用 Google Mock 提供的 MOCK_METHOD 宏定义模拟方法。

4.测试 Calculator 的单元测试:

  • 在 CalculatorTest 中,创建 MockLogger 对象并将其传递给 Calculator。使用 EXPECT_CALL 设置测试期望,即调用 log 方法时传递的字符串内容应当符合预期。
  • 当 add 被调用时,Google Test 将验证 log 方法是否如预期那样被调用。

5.主函数:

  • 初始化 Google Test 并运行所有的测试。

运行结果

[==========] Running 1 test from 1 test suite.  
[----------] Global test environment set-up.  
[----------] 1 test from CalculatorTest  
[ RUN      ] CalculatorTest.AddCallsLogger  
[       OK ] CalculatorTest.AddCallsLogger (0 ms)  
[----------] 1 test from CalculatorTest (0 ms total)  
[----------] Global test environment tear-down  
[==========] 1 test from 1 test suite ran. (0 ms total)  
[  PASSED  ] 1 test.

农业自动化系统

在农业管理软件中,可以定义一个Plant基类,包含不同植物的共同行为(如生长、浇水等)。不同植物(如Tomato、Cucumber)可以各自实现特定的生长规律,从而提高代码的重用性和扩展性。

网络编程

在网络应用中,常用多态性设计不同的协议处理类。例如,定义一个Protocol基类,TCP、UDP等协议类从该类派生,确保向上层应用程序提供统一的接口,而具体实现则在各自的子类中处理。


原文地址:https://blog.csdn.net/2302_80908313/article/details/144434097

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!