目录

c-常用接口设计

目录

c++ 常用接口设计

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

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

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

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

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

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

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

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

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

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

代码示例:

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

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 pImpl;
};
cpp
// Widget.cpp - 实现细节在这里
#include “Widget.h”
#include
#include

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

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

// 析构函数必须在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

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

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

std::unique_ptr createDatabase(const std::string& dbType) {
    if (dbType == “SQL”) {
        return std::make_unique();
    } else if (dbType == “MOCK”) {
        return std::make_unique();
    }
    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

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

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    减少编译时间,二进制兼容,完美信息隐藏
工厂模式    对象创建与使用的耦合    抽象接口 + 工厂函数返回智能指针    解耦,提高灵活性,便于测试和扩展
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 更适合:

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

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

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

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

对于很多出入门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 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::Create()
{
    return std::make_unique();
}
这样,我们完完全全将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的一堆头文件才能保证编译通过。因此适当的转换是有用且必要的。