工程实践:C++接口设计指北

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

最近在工作中,需要将代码封装成库,供其他方调用。在其中涉及到如何设计接口类,第一次接触,将总结和经验记录下来。

导读

为什么本文叫做《工程实践:C++的接口设计》,是因为,我们大部分人入门的时候,都是调用别人封装好的库函数,却没有尝试过自己封装库给别人用。但是在正常工作中,也就是工程化中,我们会经常封装库给其他人应用,这里面会涉及到怎么封装一个函数,提供一个优秀的接口。
接口:类暴露出来的部分,是类所提供的功能。

接口设计准则

我们在工程化的过程中,设计接口一般遵守以下几个准则:

  1. 单一功能原则
    一个class就其整体应该只提供单一的服务。如果一个class提供多样的服务,那么就应该把它拆分,反之,如果一个在概念上单一的功能却由几个class负责,这几个class应该合并。
  2. 开放/封闭原则
    一个设计并实现好的class,应该对扩充的动作开放,而对修改的动作封闭。也就是说,这个class应该是允许扩充的,但不允许修改。如果需要功能上的扩充,一般来说应该通过添加新类实现,而不是修改原类的代码。添加新类不单可以通过直接继承,也可以通过组合。
  3. 最小惊讶原理
    在重载函数,或者子类实现父类虚函数时,应该基本维持函数原来所期望的功能。

接口设计注意事项

  1. 过度封装
    很多人喜欢这样封装,把接口局限在仅仅解决一个特定的问题上面,失去了代码的灵活性。而且,也容易出现面条代码,让人不知所云。一个接口被写的仅仅用于解决当前问题,当试图增加其扩展性时,发现为时已晚。
    为了防止过度封装,在设计接口的时候,我们应该考虑以下几个问题:

1.我们需要解决什么问题
2.问题的核心是什么
3.应该怎样设计,可以方便客户程序员扩展

  1. 起名要见名知意
    一个好的的接口方法名,是接口设计中成功的一半。

  2. 不要让使用者进行过多工作
    如果使用者使用我们的接口时,进行了过多的准备,那么对我们来说就是失败的。
    所以,设计的时候要记得尽可能简化客户程序员逻辑,使接口设计能够看起来简洁、漂亮,而不至于被接口的复杂性所吓倒。

  3. 简洁
    这个比较好理解,举个例子,你一定见过一个函数使用,需要传进去五六个参数,但是对我们有用的往往只有那么一俩个。
    所以,简洁设计是接口设计的一个重要原则。可以在接口的内部实现中,使用复杂冗余的参数,而在暴露给客户程序员的接口中,一定要尽可能简洁。

  4. 清晰的文档表述
    这也是我在工作中,最头疼的问题,头疼的不是我不会写,而是在使用其他人提供的库时,没有使用文档,使用起来是痛苦的,所以为了不让这种痛苦发生在其他人身上,我现在从维护一份良好的文档开始。

接口设计想达到的效果

隔离用户操作与底层逻辑

接口的俩种方法

一般来说,有两种方法设计接口类。
第一种是PIMP方法,即Pointer to Implementation,在接口类成员中包含一个指向实现类的指针,这样可以最大限度的做到接口和实现分离的原则。
第二种方法叫Object-Interface方法,它的思想是采用C++的动态功能,实现类继承接口类,功能接口函数定义成虚函数。

先说结论,我们处于自身习惯的原因,选择了Object-Interface方法。

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这个中介去操作。详细做法百度还有一大堆,就不细说了。

Object-Interface 抽象基类法

一般来说,如果一个接口类对应有若干个实现类,可以采用这种方法。

上面我们讲了plmp方法,我们隐藏掉俩个数据成员,但同时也多出了一个新的数据成员,也就是接口指针,那么有没有方法,连这个指针也不要呢?

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

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方法就可以达到目的。

Object_interface 抽象基类示例代码

  1. 首先,声明一个接口
// circle.h
// 圆的接口类
class Circle {
public:
   virtual ~Circle() {};

   // 接口方法:面积
   virtual double area() = 0;
};
  1. 通过继承的方式实现这个接口
// circle_impl.h
#include "circle.h"
 
// 圆的具体实现类
class CircleImpl : public Circle {
 
private:
	double radius;
public:
	CircleImpl(double radius);
	double area() override;
};
// circle_impl.cpp
#include <cmath>
#include "circle_impl.h"
 
inline double pi() {
	return std::atan(1) * 4;
};
 
CircleImpl::CircleImpl(double _radius) : radius(_radius) {
};
 
double CircleImpl::area() {
	return pi() * radius * radius;
};
  1. 最后,通过管理类创建接口派生类的实例,或者销毁接口派生类的实例:
// circle_manager.h
#include "circle.h"
 
// 圆的创建工厂类
class CircleManager {
public:
    static Circle* create(double radius);     // 创建circle实例
    static void destroy(Circle* circlePtr);   // 销毁circle实例
};
// circle_manager.cpp
#include "circle_manager.h"
#include "circle_impl.h"
 
Circle* CircleManager::create(double radius) {
    Circle* circlePtr = new CircleImpl(radius);
 
    return circlePtr;
};
 
void CircleManager::destroy(Circle* circlePtr) {
    delete circlePtr;
}; 

现在我们接口已经实现完毕了,我们可以把它封装成库,给其他人使用了,这里封装库我们就不多言了。

最后,来看一下使用效果:

// main.cpp
#include <iostream>
#include "circle_manager.h"
#include "circle.h"
 
int main() 
{
    Circle* circlePtr = CircleManager::create(3);
    cout << circlePtr->area() <<endl;
    CircleManager::destroy(circlePtr);
    
    system("pause");
 
    return 0;
}

以上代码只提供给外部circle的接口,circle的实现完全被隐藏了起来,外部将无从知晓,外部使用者只能通过circle管理类生成circle的派生类的实例。外部使用者得到circle派生类的实例后,除了能调用接口暴露的方法area()外,其它什么也做不了,这样就完全达到了使用接口的最终目标。

参考资料

C++中的接口设计准则
C++ 头文件接口设计浅谈
一款优秀的 SDK 接口设计十大原则
C++:如何正确的使用接口类

总结

本篇文章抛砖引玉,自己也是刚刚接触,写完收工,干饭去!

C++ 接口的实现,及作用通俗理解方式 想象一下,如果某个C++的项目中已经声明好了很多很有用的接口,那么很多程序就可以使用接口即插即用,就比如计算机上的很多接口(这里单单举一个USB的接口作为例子),当我们需要使用鼠标的时候,可以通过将鼠标连接到USB接口即可,如果需要外接的音响,也可以通过USB接口来连接,这同一个接口却实现了不同的功能,因为鼠标和音响的实现的功能不同——就好比派生类对基类的方法(成员函数)的定义不同,因此实现的功能不同。C++接口通常是使用抽象类来实现的,而什么是抽象类呢?抽象类一般的是包含一个或多个纯虚函数的类。 阅读详情

相关推荐

C++接口”与“实现”分离的两种方法

接口需求 在软件开发这个行业中,一个较大的软件项目,一般由几个小组共同开发完成,为了将小组之间的影响降低到最低,定义好接口势在必行,如若要求短时间开发完成,定义好接口更是如此。或者说你的客户要求为其提供实现某个功能的接口,然后再在这些接口的基础上进行二次开发,如何定义才能定义好的接口呢? 第一,接口名字和实际的功能相符合;第二、接口要对数据进行封装,不允许客户直接操作接口之下的数据,尤其是使用n...

TAOKONG1017的博客 1万+

C++零基础教程(抽象类接口

本篇文章来讲解抽象类接口的概念,抽象类接口都需要依靠我们之前讲解的虚函数来实现,那么我们就来看看如何使用虚函数来实现抽象类接口吧。本篇文章就讲解到这里,下篇文章我们继续讲解。

m0_49476241的博客 2251

C++ 工程实践(5):避免使用虚函数作为库的接口

摘要:作为 C++ 动态库的作者,应当避免使用虚函数作为库的接口。这么做会给保持二进制兼容性带来很大麻烦,不得不增加很多不必要的 interfaces,最终重蹈 COM 的覆辙。 本文主要讨论 Linux x86 平台,会继续举 Windows/COM 作为反面教材。 本文是上一篇《C++ 工程实践(4):二进制兼容性》的延续,在写这篇文章的时候,我原本以外大家都对“以虚函数作为接口”的害处达成共识,我就写得比较简略,看来情况不是这样,我还得展开谈一谈。

陈硕的Blog 2万+

C++——接口

C++——接口 基本概念 在 C++ 中,虽然没有像其他编程语言(比如 Java 中的接口Interface)一样直接定义接口的关键字,但可 以通过抽象类和纯虚函数的方式来实现接口的概念。 接口通常用于定义类应该实现的方法,但不提供具体实现。这样的实现方式允许多个类共享相同的接口,同时让每个类根据需要去实现这些接口。 一个类作为接口可以通过以下步骤来实现: 定义抽象类:创建一个包含纯虚函数的抽象类,这些函数构成了接口的一部分。这些函数在抽象类 中只有声明而没有具体的实现。 派生类实现接口:派生类继承抽

qq_47944751的博客 1843

C++接口

c++接口类到底是干什么用的?什么叫接口? 打一个比方如抽象一个类"人":class people。人有一些必须要做的事,比如:吃饭、呼吸。 但是每个人吃饭又不一样,比如:用筷子吃(中国人)、用刀叉(欧美人)、用手抓(印度人)。 那么你如果不把“人”(people)这个类做为一个接口抽象类)。吃就至少要写3个“吃”的重载方法。但一个中国人可能永远也用不到用刀叉吃饭、用手抓饭这样的方法。这就造...

elisabeth1994的博客 9722

工程实践之路:C++接口设计中的工厂模型

工程实践之路:C++接口设计中的工厂模型设计模式之工厂模式为什么使用工厂模式1.工厂设计模式是为了将对象的创建与使用进行分离2.其他好处简单工厂模式工厂方法模式抽象工厂模式参考资料 上一篇文章写了《工程实践C++接口设计》 主要写了用C++写第三库,如何设计接口的问题,其中用到了工厂模式,在此之前,一直不太明白《设计模式》在实际开发中的应用。 现在正好被我逮到了,以此为例,深入学习一下! 设计模式之工厂模式 工厂顾名思义就是创建产品,根据产品是具体产品还是具体工厂可分为简单工厂模式和工厂方法模式,根

CodeBowl的博客 390

企业级C++动态库构建:从接口设计到CI/CD的工程实践

C++开发中,动态链接库是实现代码复用和模块化部署的核心技术。其原理在于将代码编译为独立的二进制模块,在程序运行时动态加载,从而减少内存占用并支持独立更新。这项技术的核心价值在于提升大型项目的可维护性、促进团队协作并保障二进制兼容性,广泛应用于操作系统组件、中间件和大型商业软件的架构中。要构建一个高质量的企业级动态库,关键在于设计稳定的应用程序二进制接口,并解决跨平台的符号导出问题。通过采用C风格接口封装C++实现、利用CMake进行现代化项目配置,并集成自动化CI/CD流水线,可以系统性地应对这些挑战,

weixin_29243279的博客 342

C++面向接口设计:从依赖倒置到插件化架构的工程实践

在软件工程中,设计模式是构建可维护、可扩展系统的关键。依赖倒置原则作为SOLID原则的核心,强调高层模块不应依赖低层模块,而应共同依赖抽象。这一原则通过接口类实现,将行为契约与具体实现分离,从而达成松耦合。在C++中,接口类通过纯虚函数定义,体现了“能做什么”的契约关系,而非“是什么”的继承关系。其技术价值在于提升代码的可测试性、可扩展性和模块化程度,尤其在插件化系统、数据访问层抽象和跨平台开发等场景中作用显著。本文结合依赖注入和虚函数机制,深入探讨了如何通过面向接口设计构建健壮的C++应用架构。

weixin_33881140的博客 331

一文读懂msvc的cpp_modules:原理、动机与工程实践

C++ Modules是解决传统头文件机制缺陷的新方案。MSVC中的模块通过.ifc文件存储编译接口信息,显著提升编译速度并避免宏污染。关键优势包括:import std;替代标准库头文件可消除重复解析和宏泄漏;模块化组织业务逻辑能实现接口与实现解耦。建议在新项目中优先采用模块,特别是标准库场景,但对跨平台公共库仍需谨慎。模块机制标志着C++向现代化工程实践的转变,虽然目前工具链成熟度仍有提升空间,但其结构化设计为解决长期存在的编译效率问题提供了根本方案。

charlie114514191的博客 1114

C++ 工程实践(8):值语义

陈硕 (giantchen_AT_gmail)http://blog.csdn.net/Solstice  http://weibo.com/giantchen陈硕关于 C++ 工程实践的系列文章: http://blog.csdn.net/Solstice/category/8

陈硕的Blog 1万+

C++接口工程实践:有哪些实现方法?

简介:程序开发的时候经常会使用到接口。众所周知,C++语言层面并没有接口的概念,但并不意味着C++不能实现接口的功能。相反,正是由于C++语言没有提供标准的接口,导致实际实现接口的方法多种多样。那么C++有哪些实现接口的方法呢,不同的方法又适用于哪些场景呢?本文分享在C++接口工程实践上的一些探索心得。一 接口的分类接口按照功能划分可以分为调用接口与回调接口:调用接口一段代码、一个模块、一个程序库、一个服务等(后面都称为系统),对外提供什么功能,以接口的形式暴露出来,用户只需要关心接口怎么调用,不用关心具

阿里云开发者 4076

边用llama.cpp边学C++:Windows 11下真实工程驱动的入门路径

C++是一门强工程导向的语言,其核心价值在于对内存、硬件和系统资源的直接掌控。理解std::vector的RAII语义、std::thread的生命周期管理、CUDA与CPU协同的构建逻辑,远比孤立记忆语法更重要。llama.cpp作为轻量级但工业级的大模型推理引擎,天然融合了现代C++关键特性(如constexpr、模板元编程、跨平台条件编译)与底层实践(mmap内存映射、AVX令对齐、OpenMP并发优化),成为连接语法知识与真实系统行为的理想桥梁。尤其在Windows 11 + CUDA环境下,从编

weixin_33700350的博客 398

C++ 工程实践(2):不要重载全局 ::operator new()

对于现实生活中的 C++ 项目,重载 ::operator new() 几乎没有用武之地,因为很难处理好与程序所用的 C++ library 的关系。如果确实需要,我们可以从 malloc 层面入手,彻底而全面地替换内存分配器。

陈硕的Blog 2万+

Whisper.cpp深度解析:C/C++离线语音识别引擎原理与工程实践

语音识别作为人机交互核心能力,其边缘部署长期受限于Python生态的启动开销、内存不可控和依赖复杂等问题。Whisper.cpp通过纯C/C++重实现推理流水线,结合ggml张量库与混合量化技术,实现了低延迟、确定性内存占用和跨平台离线运行。它不依赖Python或GPU,支持ARM/RISC-V等嵌入式架构,适用于智能硬件、车载语音、会议记录等对实时性与可靠性要求严苛的场景。本文围绕C/C++实现、离线运行、嵌入式友好三大关键词,系统剖析其设计哲学、编译调优、流式处理及量产级稳定性保障机制。

weixin_33871366的博客 332

llama.cpp源码深度解析:C++轻量推理框架原理与工程实践

LLM推理框架是大模型落地的核心基础设施,其底层实现直接决定性能、内存与跨平台能力。llama.cpp作为纯C/C++编写的零依赖推理引擎,以极致精简的设计实现了CPU端高效量化推理,核心涵盖GGUF模型加载、KV Cache内存布局、Q4_K_M/Q5_K_S等位级量化机制、RoPE位置编码计算及多线程状态同步等关键技术环节。它不依赖PyTorch或CUDA,却需开发者深入理解张量生命周期、内存对齐、AVX/NEON向量化及平台特异性约束(如macOS mlock限制、Windows CUDA流同步)。本

weixin_33704591的博客 347

C++工程实践经验

1 C++ 工程实践经验谈 陈硕 (giantchen@gmail.com) 最后更新 2012-4-20 版权声明 本作品采用“Creative Commons 署名 -非商业性使用 -禁止演绎 3.0 Unported 许可 协议 (cc by-nc-nd)”进行许可。http://creativecommons.org/licenses/by-nc-nd/3.0/ 内容

kamo54的专栏 1万+

如何高效使用Godot-CPP:3个专业技巧实现C++高性能游戏开发

想要为你的Godot游戏项目注入C++级别的性能优势吗?今天我们将深入探讨**Godot-CPP**——这款专为Godot引擎设计的C++绑定库,它能帮助开发者轻松实现**C++高性能游戏开发**,让游戏性能不再是瓶颈。 ## 问题引入:为什么需要Godot-CPP? 许多Godot开发者在使用GDScript或C#开发游戏时,可能会遇到性能瓶颈。特别是在处理复杂物理计算、AI算法、图形渲染或

gitblog_00577的博客 419

C++函数封装进阶:从接口设计到RAII与异常安全的工程实践

函数是程序设计的基础单元,其核心价值在于通过封装实现信息隐藏与接口隔离,从而降低代码耦合度、提升可维护性。在C++中,函数封装不仅是代码复用的工具,更是构建健壮、高效程序的关键技术。从参数传递的性能与安全权衡(如const引用、移动语义),到资源管理遵循的RAII范式,再到错误处理策略(异常、错误码、std::optional)的选择,这些原理共同支撑起工业级代码的可靠性。在实际应用场景中,无论是算法实现、配置解析还是模块设计,良好的函数封装能显著提升代码的清晰度与团队协作效率。本文聚焦C++函数封装的高级

weixin_30983353的博客 311

llama-cpp-python架构解析:从C++原生绑定到高性能LLM推理的工程实践

在本地大语言模型部署领域,开发者常面临性能瓶颈、硬件兼容性差和部署复杂度高的三重挑战。llama-cpp-python通过为llama.cpp提供Python原生绑定,实现了CPU推理速度提升3-5倍、GPU利用率优化40%以上的突破性进展,为生产级本地LLM应用提供了完整的技术栈解决方案。 ## 核心架构设计与跨语言接口原理 ### Python-C++混合架构的技术实现 llama-cp

gitblog_00874的博客 439
上一篇: 逆向程序分析:C++的一个空类,为什么在内存中占1字节
下一篇: 工程实践之路:C++接口设计中的工厂模型
CodeBowl
博客等级 码龄5年 7658粉丝 67原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值