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++代码质量将迈上一个新的台阶。

复制代码
C++ 接口与实现分离的两种方法
下面我将展示两种在C++中实现接口与实现分离的方法:Handle Classes(句柄类/桥接模式)和Interface Classes(接口类/策略模式)。

方法一:Interface Classes(接口类)
cpp
#include <iostream>
#include <memory>
#include <vector>

// 接口类 - 定义图形绘制接口
class IDrawable {
public:
    virtual ~IDrawable() = default;
    virtual void draw() const = 0;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

// 具体实现 - 圆形
class Circle : public IDrawable {
private:
    double radius;
    
public:
    explicit Circle(double r) : radius(r) {}
    
    void draw() const override {
        std::cout << "Drawing a circle with radius " << radius 
                  << " and area " << area() << std::endl;
    }
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
    
    std::string name() const override {
        return "Circle";
    }
};

// 具体实现 - 矩形
class Rectangle : public IDrawable {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    void draw() const override {
        std::cout << "Drawing a rectangle " << width << "x" << height 
                  << " with area " << area() << std::endl;
    }
    
    double area() const override {
        return width * height;
    }
    
    std::string name() const override {
        return "Rectangle";
    }
};

// 使用接口的客户端代码
void drawAll(const std::vector<std::shared_ptr<IDrawable>>& shapes) {
    std::cout << "=== Drawing all shapes ===\n";
    for (const auto& shape : shapes) {
        shape->draw();
    }
}

int main() {
    // 创建不同类型的图形对象
    std::vector<std::shared_ptr<IDrawable>> shapes;
    shapes.push_back(std::make_shared<Circle>(5.0));
    shapes.push_back(std::make_shared<Rectangle>(4.0, 6.0));
    
    // 使用接口处理所有图形
    drawAll(shapes);
    
    return 0;
}
方法二:Handle Classes(句柄类)
cpp
#include <iostream>
#include <memory>
#include <vector>

// 前置声明
class DrawableImpl;

// 句柄类 - 对外接口
class Drawable {
private:
    std::shared_ptr<DrawableImpl> pImpl; // 指向实现的指针
    
public:
    // 构造函数 - 通过具体实现类创建
    Drawable(std::shared_ptr<DrawableImpl> impl) : pImpl(impl) {}
    
    // 转发方法到实现类
    void draw() const;
    double area() const;
    std::string name() const;
};

// 实现基类
class DrawableImpl {
public:
    virtual ~DrawableImpl() = default;
    virtual void draw() const = 0;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

// 实现Drawable的方法 - 转发调用
void Drawable::draw() const { pImpl->draw(); }
double Drawable::area() const { return pImpl->area(); }
std::string Drawable::name() const { return pImpl->name(); }

// 具体实现 - 圆形
class CircleImpl : public DrawableImpl {
private:
    double radius;
    
public:
    explicit CircleImpl(double r) : radius(r) {}
    
    void draw() const override {
        std::cout << "Drawing a circle with radius " << radius 
                  << " and area " << area() << std::endl;
    }
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
    
    std::string name() const override {
        return "Circle";
    }
};

// 具体实现 - 矩形
class RectangleImpl : public DrawableImpl {
private:
    double width, height;
    
public:
    RectangleImpl(double w, double h) : width(w), height(h) {}
    
    void draw() const override {
        std::cout << "Drawing a rectangle " << width << "x" << height 
                  << " with area " << area() << std::endl;
    }
    
    double area() const override {
        return width * height;
    }
    
    std::string name() const override {
        return "Rectangle";
    }
};

// 使用句柄的客户端代码
void drawAll(const std::vector<Drawable>& shapes) {
    std::cout << "=== Drawing all shapes ===\n";
    for (const auto& shape : shapes) {
        shape.draw();
    }
}

int main() {
    // 创建不同类型的图形对象
    std::vector<Drawable> shapes;
    shapes.emplace_back(std::make_shared<CircleImpl>(5.0));
    shapes.emplace_back(std::make_shared<RectangleImpl>(4.0, 6.0));
    
    // 使用句柄处理所有图形
    drawAll(shapes);
    
    return 0;
}
两种方法的比较
Interface Classes(接口类):
优点:

直接使用多态,实现简单直观

运行时多态,灵活性强

标准面向对象设计模式

缺点:

需要动态分配(通常使用智能指针)

虚函数调用有轻微性能开销

客户端代码需要知道具体实现类型来创建对象

Handle Classes(句柄类):
优点:

完全隐藏实现细节(Pimpl惯用法)

减少编译依赖,提高编译速度

可以在不重新编译客户端代码的情况下更改实现

值语义,更容易使用

缺点:

需要额外的间接层

实现更复杂,需要维护两个类层次

所有方法调用都需要转发

结合两种方法的完整示例
cpp
#include <iostream>
#include <memory>
#include <vector>
#include <cmath>

// 方法1: Interface Classes
namespace InterfaceApproach {
    // 接口
    class IDrawable {
    public:
        virtual ~IDrawable() = default;
        virtual void draw() const = 0;
        virtual double area() const = 0;
        virtual std::string name() const = 0;
    };

    // 具体实现
    class Circle : public IDrawable {
        double radius;
    public:
        explicit Circle(double r) : radius(r) {}
        void draw() const override {
            std::cout << "Drawing a circle with radius " << radius << std::endl;
        }
        double area() const override { return M_PI * radius * radius; }
        std::string name() const override { return "Circle"; }
    };

    class Rectangle : public IDrawable {
        double width, height;
    public:
        Rectangle(double w, double h) : width(w), height(h) {}
        void draw() const override {
            std::cout << "Drawing a rectangle " << width << "x" << height << std::endl;
        }
        double area() const override { return width * height; }
        std::string name() const override { return "Rectangle"; }
    };
}

// 方法2: Handle Classes
namespace HandleApproach {
    // 实现基类
    class DrawableImpl {
    public:
        virtual ~DrawableImpl() = default;
        virtual void draw() const = 0;
        virtual double area() const = 0;
        virtual std::string name() const = 0;
    };

    // 句柄类
    class Drawable {
        std::shared_ptr<DrawableImpl> pImpl;
    public:
        explicit Drawable(std::shared_ptr<DrawableImpl> impl) : pImpl(impl) {}
        
        void draw() const { pImpl->draw(); }
        double area() const { return pImpl->area(); }
        std::string name() const { return pImpl->name(); }
    };

    // 具体实现
    class CircleImpl : public DrawableImpl {
        double radius;
    public:
        explicit CircleImpl(double r) : radius(r) {}
        void draw() const override {
            std::cout << "Drawing a circle with radius " << radius << std::endl;
        }
        double area() const override { return M_PI * radius * radius; }
        std::string name() const override { return "Circle"; }
    };

    class RectangleImpl : public DrawableImpl {
        double width, height;
    public:
        RectangleImpl(double w, double h) : width(w), height(h) {}
        void draw() const override {
            std::cout << "Drawing a rectangle " << width << "x" << height << std::endl;
        }
        double area() const override { return width * height; }
        std::string name() const override { return "Rectangle"; }
    };
}

int main() {
    std::cout << "=== Interface Classes Approach ===\n";
    {
        std::vector<std::shared_ptr<InterfaceApproach::IDrawable>> shapes;
        shapes.push_back(std::make_shared<InterfaceApproach::Circle>(5.0));
        shapes.push_back(std::make_shared<InterfaceApproach::Rectangle>(4.0, 6.0));
        
        for (const auto& shape : shapes) {
            shape->draw();
            std::cout << "Area: " << shape->area() << ", Name: " << shape->name() << "\n\n";
        }
    }
    
    std::cout << "=== Handle Classes Approach ===\n";
    {
        std::vector<HandleApproach::Drawable> shapes;
        shapes.emplace_back(std::make_shared<HandleApproach::CircleImpl>(5.0));
        shapes.emplace_back(std::make_shared<HandleApproach::RectangleImpl>(4.0, 6.0));
        
        for (const auto& shape : shapes) {
            shape.draw();
            std::cout << "Area: " << shape.area() << ", Name: " << shape.name() << "\n\n";
        }
    }
    
    return 0;
}
总结
两种方法都实现了接口与实现的分离,但适用于不同的场景:

Interface Classes 更适合:

需要运行时多态性的场景

需要频繁扩展新实现的系统

框架和库的设计

Handle Classes 更适合:

需要减少编译依赖的大型项目

需要值语义而不是引用语义的场景

需要完全隐藏实现细节的库

在实际项目中,可以根据具体需求选择合适的方法,或者结合使用两种方法以获得各自的优势

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的一堆头文件才能保证编译通过。因此适当的转换是有用且必要的。

相关推荐
XH华23 分钟前
C语言第十一章内存在数据中的存储
c语言·开发语言
AndrewHZ2 小时前
【python与生活】如何用Python写一个简单的自动整理文件的脚本?
开发语言·python·生活·脚本·文件整理
拉法豆粉2 小时前
在压力测试中如何确定合适的并发用户数?
java·开发语言
枯萎穿心攻击3 小时前
Unity VS UE 性能工具与内存管理
开发语言·游戏·unity·ue5·游戏引擎·虚幻·虚幻引擎
爱上纯净的蓝天3 小时前
迁移面试题
java·网络·c++·pdf·c#
binbinaijishu883 小时前
Python爬虫入门指南:从零开始的网络数据获取之旅
开发语言·爬虫·python·其他
chenglin0163 小时前
Logstash_Input插件
java·开发语言
3壹3 小时前
单链表:数据结构中的高效指针艺术
c语言·开发语言·数据结构
不过普通话一乙不改名4 小时前
第四章:并发编程的基石与高级模式之Select语句与多路复用
开发语言·golang