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

c++ 常用接口设计

《Effective C++》全文读书笔记_51CTO博客_effective c++笔记

Effective C++笔记(1-4章,条款1-25)_帮我把effice c++ 的笔记-CSDN博客

设计模式精要:提升代码复用与可维护性-CSDN博客

核心设计原则回顾 (作为基础)
SOLID 原则:

S - 单一职责原则: 一个类应该只有一个引起它变化的原因。

O - 开闭原则: 对扩展开放,对修改关闭。

L - 里氏替换原则: 子类必须能够完全替代其父类。

I - 接口隔离原则: 客户端不应该被迫依赖于它不使用的接口。

D - 依赖倒置原则: 依赖于抽象(接口),而不是具体实现。

RAII: 资源获取即初始化。这是C++管理资源的生命线,将资源生命周期与对象生命周期绑定。

高内聚,低耦合: 模块内部元素紧密相关,模块之间依赖尽可能少。

常用设计案例与实践技巧
案例 1: PIMPL (Pointer to IMPLementation) - 编译防火墙与接口稳定性
问题: 头文件中的私有成员会导致实现细节暴露。当修改私有成员时,所有包含该头文件的代码都需要重新编译,这在大型项目中非常耗时。

解决方案: 使用一个不透明的指针,将类的实现细节完全隐藏在一个单独的类中,头文件中只包含接口和一个指向实现的指针。

代码示例:

cpp
// Widget.h - 稳定接口,不暴露任何实现细节
#include <memory>

class Widget {
public:
    Widget(); // 构造函数
    ~Widget(); // 析构函数必须声明,因为Impl是不完整类型

    // 公开接口
    void doSomething();
    int getValue() const;

    // 禁止拷贝(示例,也可实现拷贝语义)
    Widget(const Widget&) = delete;
    Widget& operator=(const Widget&) = delete;

private:
    // 前置声明实现类
    struct Impl;
    // 使用唯一指针管理实现对象
    std::unique_ptr<Impl> pImpl;
};
cpp
// Widget.cpp - 实现细节在这里
#include "Widget.h"
#include <vector>
#include <string>

// 定义实现类
struct Widget::Impl {
    // 这里可以包含任何复杂的、经常变动的实现细节
    std::vector<int> complexData;
    std::string name;
    void privateHelperFunction() { /* ... */ }
};

// 构造函数需要构造Impl对象
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}

// 析构函数必须在Impl定义后看到其完整类型,因此放在.cpp中
// 但使用默认析构函数即可,unique_ptr会自动删除Impl对象
Widget::~Widget() = default;

// 接口实现,通过pImpl访问具体数据
void Widget::doSomething() {
    pImpl->privateHelperFunction();
    pImpl->complexData.push_back(42);
}

int Widget::getValue() const {
    return pImpl->complexData.empty() ? 0 : pImpl->complexData.back();
}
优点:

二进制兼容性: 修改 Impl 的结构不会改变 Widget 类的大小和布局,头文件不变,客户端无需重新编译。

信息隐藏: 头文件极其简洁,只暴露公共接口,完美实现了信息隐藏。

编译速度: 减少头文件依赖,显著提升编译速度。

案例 2: 工厂模式与依赖倒置 - 创建灵活对象
问题: 客户端代码直接 new 一个具体类,导致紧密耦合。如果想替换一种实现(例如,SqlDatabase 换为 MockDatabase),需要修改所有客户端代码。

解决方案: 定义一个抽象接口(纯虚类),然后通过一个工厂函数(或工厂类)来返回具体实现的对象。客户端只依赖于抽象接口。

代码示例:

cpp
// IDatabase.h - 抽象接口
#include <string>

class IDatabase {
public:
    virtual ~IDatabase() = default; // 基类析构函数必须为virtual
    virtual bool connect(const std::string& connectionString) = 0;
    virtual bool query(const std::string& sql) = 0;
    // ... 其他数据库操作
};
cpp
// DatabaseFactory.h
#include "IDatabase.h"
#include <memory>

// 工厂函数返回抽象接口的智能指针
std::unique_ptr<IDatabase> createDatabase(const std::string& dbType);
// 可以扩展为注册模式的工厂,更灵活
cpp
// DatabaseFactory.cpp
#include "DatabaseFactory.h"
#include "SqlDatabase.h" // 具体实现A
#include "MockDatabase.h" // 具体实现B

std::unique_ptr<IDatabase> createDatabase(const std::string& dbType) {
    if (dbType == "SQL") {
        return std::make_unique<SqlDatabase>();
    } else if (dbType == "MOCK") {
        return std::make_unique<MockDatabase>();
    }
    throw std::runtime_error("Unknown database type: " + dbType);
}
cpp
// Client.cpp - 客户端代码
#include "IDatabase.h"
#include "DatabaseFactory.h"

void clientCode() {
    // 客户端只依赖于IDatabase抽象接口和工厂
    auto db = createDatabase("MOCK"); // 轻松切换类型,只需修改配置字符串

    db->connect("...");
    db->query("SELECT ...");
    // db 离开作用域后自动释放资源
}
优点:

解耦: 客户端与具体实现类完全解耦。

可扩展: 添加新的数据库类型(如 OracleDatabase)无需修改客户端和工厂逻辑(尤其是在使用注册模式时)。

可测试: 可以轻松注入 MockDatabase 进行单元测试。

案例 3: RAII 与资源管理 - 构建异常安全的代码
问题: 手动管理资源(如内存、文件句柄、锁)容易导致泄漏,尤其是在异常发生时。

解决方案: 将资源封装在对象中,在构造函数中获取资源,在析构函数中释放资源。利用栈对象生命周期自动管理资源。

代码示例(自定义文件句柄管理):

cpp
// FileHandle.h
#include <cstdio>

class FileHandle {
public:
    // 显式构造函数,接管已有的FILE*或通过文件名打开
    explicit FileHandle(const char* filename, const char* mode = "r");
    explicit FileHandle(FILE* f) : file(f) {} // 接管所有权

    // 禁止拷贝
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // 支持移动语义
    FileHandle(FileHandle&& other) noexcept : file(other.file) {
        other.file = nullptr;
    }
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            close();
            file = other.file;
            other.file = nullptr;
        }
        return *this;
    }

    ~FileHandle() { close(); }

    // 显式释放资源,并可检查是否有效
    void close();
    bool isOpen() const { return file != nullptr; }

    // 提供访问原始资源的接口(必要时)
    FILE* get() const { return file; }

    // 常用的文件操作可以封装为成员函数,更安全
    size_t read(void* buffer, size_t size, size_t count);
    size_t write(const void* buffer, size_t size, size_t count);
    // ...

private:
    FILE* file = nullptr;
};
cpp
// FileHandle.cpp
#include "FileHandle.h"
#include <stdexcept>

FileHandle::FileHandle(const char* filename, const char* mode) {
    file = std::fopen(filename, mode);
    if (!file) {
        throw std::runtime_error("Failed to open file");
    }
}

void FileHandle::close() {
    if (file) {
        std::fclose(file);
        file = nullptr;
    }
}
// ... 其他成员函数实现
使用方式:

cpp
void processFile() {
    try {
        FileHandle fh("data.txt", "w"); // 资源在构造时获取
        fh.write(data, sizeof(Data), 1, fh.get());

        // 即使这里抛出异常,fh的析构函数也会被调用,文件会被安全关闭
        someRiskyOperation();

    } catch (const std::exception& e) {
        // 处理异常,无需担心文件泄露
    }
    // 离开作用域,文件自动关闭
}
优点:

异常安全: 保证资源在任何执行路径下都能被正确释放。

无需手动管理: 避免了忘记调用 close/delete 的问题。

清晰的所有权语义: 明确表示了资源的所有权归属。

总结表格
设计模式    解决的核心问题                               关键实现手段                                   带来的好处
PIMPL        编译依赖、接口稳定、信息隐藏    不透明指针 std::unique_ptr<Impl>    减少编译时间,二进制兼容,完美信息隐藏
工厂模式    对象创建与使用的耦合    抽象接口 + 工厂函数返回智能指针    解耦,提高灵活性,便于测试和扩展
RAII    资源泄漏,尤其是异常安全    将资源生命周期绑定到对象生命周期    自动资源管理,强异常安全保证
策略模式    算法在运行时需要灵活切换    将算法抽象为接口,通过组合注入     符合开闭原则,算法可独立变化
这些案例是构建现代、高效、稳定C++程序的基石。熟练掌握它们,并理解其背后的设计哲学,你的C++代码质量将迈上一个新的台阶。

https://zhuanlan.zhihu.com/p/338227526

对于很多出入门C++ 的程序员来说,大部门新手都是在用别人封装好的库函数,却没有尝试过自己封装一个自己的库提供给别人用。在公司里也见过一些新同事对于库的封装手足无措,不知道怎么将层级抽象化。这里提供一下我自己的见解。

我们知道,C++的三大特性:继承,多态,封装。在抽象一个功能库的时候,就是运用到了这三大核心思路。先说说在C++头文件接口设计中秉承的思路:

隔离用户操作与底层逻辑
这个其实就是要对你的底层代码逻辑做好抽象,尽量不要暴露你的代码逻辑,比如在opencv里面,对图像的操作大部分是通过cv::Mat这个矩阵类来实现的,这个类提供了很多操作图像的接口,使得用户可以不用直接接触像素操作,非常方便。举个简单的例子:

class Complex{
public:
    Complex& operator+(const Complex& com );

    Complex& operator-(const Complex& com );

    Complex& operator*(const Complex& com );

    Complex& operator/(const Complex& com );

private:
    double real_;
    double imaginary_;
};
通过这样简单的封装,用户可以直接使用+-*/四种运算符进行复数的运算,而数据成员则是被private隐藏了,用户看不见。这不仅是形式上的需要,更是为了我们程序员的身心健康着想。试想,一旦我们在接口中暴露了数据成员,那么一定有用户做出一些超出你设计意图之外的操作,为了防止这些骚操作不把程序crash掉,你要增加很多的异常处理。更有可能的是有些异常是你预想不到的。

那么这样是否就完美了呢?显然不是。如果把上述代码作为一个接口文件发布出去,用户依然能清清楚楚看到你的private成员,于是你就“暴露”了你的实现。我们要把接口的用户当成十恶不赦的蠢货,就要把成员再次隐藏起来。这时候就可以用到两种处理方式

1)PImp手法

所谓PImp是非常常见的隐藏真实数据成员的技巧,核心思路就是用另一个类包装了所要隐藏的真实成员,在接口类中保存这个类的指针。看代码:

//header complex.h
class ComplexImpl;
class Complex{
public:
    Complex& operator+(const Complex& com );

    Complex& operator-(const Complex& com );

    Complex& operator*(const Complex& com );

    Complex& operator/(const Complex& com );

private:
    ComplexImpl* pimpl_;
};
在接口文件中声明一个ComplexImpl*,然后在另一个头文件compleximpl.h中定义这个类

//header compleximpl.h
class ComplexImpl{
public:
    ComplexImpl& operator+(const ComplexImpl& com );

    ComplexImpl& operator-(const ComplexImpl& com );

    ComplexImpl& operator*(const ComplexImpl& com );

    ComplexImpl& operator/(const ComplexImpl& com );

private:
    double real_;
    double imaginary_;
};
可以发现,这个ComplexImpl的接口基本没有什么变化(其实只是因为这个类功能太简单,在复杂的类里面,是需要很多private的内部函数去抽象出更多实现细节),然后在complex.cpp中,只要

#include "complex.h"
#include "compleximpl.h"
包含了ComplexImpl的实现,那么所有对于Complex的实现都可以通过ComplexImpl这个中介去操作。详细做法百度还有一大堆,就不细说了。

2)抽象基类

虽然使用了pimp手法,我们隐藏掉了复数的两个成员,但是头文件依然暴露出了新的一个ComplexImpl*指针,那有没有办法连这个指针也不要呢?

这时候就是抽象基类发挥作用的时候了。看代码:

class Complex{
public:
    static std::unique_ptr<Complex> Create();

    virtual Complex& operator+(const Complex& com ) = 0;

    virtual Complex& operator-(const Complex& com ) = 0;

    virtual Complex& operator*(const Complex& com ) = 0;

    virtual Complex& operator/(const Complex& com ) = 0;
};
将要暴露出去的接口都设置为纯虚函数,通过 工厂方法Create来获取Complex指针,Create返回的是继承实现了集体功能的内部类;

//Complex类功能的内部实现类
class ComplexImpl : public Complex{
public:
    virtual Complex& operator+(const Complex& com ) override;

    virtual Complex& operator-(const Complex& com ) override;

    virtual Complex& operator*(const Complex& com ) override;

    virtual Complex& operator/(const Complex& com ) override;

private:
    double real_;
    double imaginary_;
}


至于Create函数也很简单:

std::unique_ptr<Complex> Complex::Create()
{
    return std::make_unique<ComplexImpl>();
}
这样,我们完完全全将Complex类的实现细节全部封装隐藏起来了,用户一点都不知道里面的数据结构是什么;

当然,对于Complex这样的类来说,用户是有获取他的实部虚部这样的需求的,也很简单,再加上两个Get方法就可以达到目的。

2.减少编译依赖,简化参数结构

减少编译依赖,一言蔽之,就是不要再头文件里include太多其他头文件,尽可能使用指针或引用来代替。

有些接口需要用户设置的参数,尽量傻瓜化,不必寻求这些参数结构也可以在内部实现中通用。

就比如说,一个渲染字体的接口,如果内部使用到了opencv的一些方法,用户层应该怎么设置参数呢?

struct FontConfig{
    int line_with;
    int font_style;
    int scale;  //比重因子
    int r;
    int g;
    int b;
    double weight; //权重
}

void Render(const FontConfig& config)  //内部实现
{
    cv::Scaler color(config.r, config.g, config.b);
    cv::putText(...color);
    // ... 
}
类似这种代码,其内部实现需要的结构是 cv::Scaler 这个结构,但是我们不能在接口文件中出现,一旦出现了,那也就毫无封装可言,你必须在接口里包含opencv的一堆头文件才能保证编译通过。因此适当的转换是有用且必要的。

http://www.xdnf.cn/news/18644.html

相关文章:

  • CSS 进阶用法
  • Linux camera 驱动流程介绍(rgb: ov02k10)(chatgpt version)
  • Java 20 新特性及具体应用
  • 关于并查集
  • Text Blocks:告别字符串拼接地狱
  • 量子链(Qtum)分布式治理协议
  • 单词搜索+回溯法
  • Linux内核ELF文件签名验证机制的设计与实现(C/C++代码实现)
  • 源滚滚React消息通知框架v1.0.2使用教程
  • 《支付回调状态异常的溯源与架构级修复》
  • 【RAGFlow代码详解-3】核心服务
  • Linux驱动之DMA(三)
  • ubuntu中网卡的 IP 及网关配置设置为永久生效
  • Maxwell学习笔记
  • 8月精选!Windows 11 25H2 【版本号:26200.5733】
  • 从技术精英到“芯”途末路:一位工程师的沉沦与救赎
  • IC验证 APB 项目(二)——框架结构(总)
  • 项目编译 --- 基于cmake ninja编译 rtos项目
  • COSMIC智能化编写工具:革命性提升软件文档生成效率
  • 20.13 ChatGLM3 QLoRA微调实战:3步实现高效低资源训练
  • Shell Case 条件语句详解
  • 数据挖掘 4.8 评估泛化能力
  • k8s原理及操作
  • Go语言环境安装
  • Spring面试题及详细答案 125道(16-25) -- 核心概念与基础2
  • Jwt令牌设置介绍
  • c++基础知识入门
  • Https之(三)TLS双向认证
  • 打响“A+H”双重上市突围战,云天励飞实力如何?
  • 云原生俱乐部-RH294知识点归纳(3)