C++ 接口和实现分离初步简介

简介: C++ 接口和实现分离初步简介

C++虽然不太常提到设计模式,但是对外接口和实现细节的分离仍然是必须的。

今天在提交代码的时候遇到一个问题,给出的.h文件中定义了一个类,虽然类中只有一些对外暴露的接口的成员函数,但是类中包含了一些private的成员变量。虽然不影响使用,但是从规范上讲是不合理的。因此需要将接口和实现的细节进行分离。也就是常说的信息隐藏。下面通过一个常用的头文件格式进行说明。考虑我们对外Release的一个头文件,a.h


class A
{
public:
  X getX();
  Y getY();
  Z getZ();
private:
  X god;
  Y damn;
  Z it;
};


我们定义了一个类class A类它包含了成员函数 getX()getY()getZ() 分别返回类型为 XYZ 的对象。成员变量 goddamnit 分别是类型为 XYZ 的对象


private成员变量


这种方式的头文件形式如下:


#include "X.h"
#include "Y.h"
#include "Z.h"
class A
{
public:
  X getX();
  Y getY();
  Z getZ();
private:
  X god;
  Y damn;
  Z it;
};


我们在代码开头处进行了头文件引用。包含了 "X.h""Y.h""Z.h" 这三个头文件的引用。这表示类 A 使用了 XYZ 类的定义,

其中我们如果直接使用private的方式进行信息隐藏,面临多个问题:

  • 别人能看到我们private成员变量的信息;
  • 必须同时给出我们依赖的X.hY.hZ.h
  • 依赖的头文件和类本身的任何改动都将引发重新编译,即使这个改动本质上是不影响外部调用的。

这种方式本质上是一种紧耦合,只是简单的面向对象的封装,隐藏实现细节。


使用依赖类的声明而非定义


这种方式的头文件形式如下:


class X;
class Y;
class Z;
class A
{
public:
  X getX();
  Y getY();
  Z getZ();
private:
  X god;
  Y damn;
  Z it;
};


可以看到,我们不用再包含X.hY.hZ.h,当他们发生变化时,A的调用者不必重新编译,阻止了级联依赖的发生,但是别人仍然能看到我们的私有成员信息,这也不是我们预想的。


pImpl模式


使用Impl的代理模式,即A本身只是一个负责对外提供接口的类,真正的实现使用一个AImpl类来代理,接口的实现通过调用Impl类的对应函数来实现,从而实现真正意义上的接口和实现分离


// AImpl.h
struct AImpl
{
public:
 X getX();
 Y getY();
 Z getZ();
private:
 X x;
 Y y;
 Z z;
};
// A.h
class X;
class Y;
class Z;
struct AImpl;
class A
{
public:
 // 可能的实现: X getX() { return pImpl->getX(); }
  X getX()
 Y getY()
 Z getZ();
private:
 std::tr1::unique_ptr<AImpl> pImpl;
};


来看看这种实现的好处。首先,任何实现的细节都封装在AImpl类中,所以对于调用端来说是完全不可见的,包括可能用到的成员。其次,只要A的接口没有变化,调用端都不需要重新编译。

但是这种实现也有一个问题,就是多了一个类需要维护,并且每次对A的调用都将是对AImpl的间接调用,效率肯定有所降低。

这种实现方式有一些问题需要注意:

  • Impl的声明最好设置为struct,原因我也不清楚,因为我用class声明的AImpl(不包含private成员),在Linux上能过,在windows过不去,一直报LINK ERROR的错误。我怀疑windows上看不到类的定义时,直接引用类成员函数会有问题。
  • 一般使用unique_ptr来包装Impl类,但是使用unique_ptr的时候,接口类的析构函数不能直接定义在类的声明中。因为在类的声明中直接定义析构函数(或者使用=default)的时候,看不到Impl类的实现,也就是看不到Impl类的析构函数,而接口类的析构函数,必须要看unique_ptr成员函数Impl类的析构函数,否则会报can't delete an incomplete type错误。
  • 这个错误其实是一类错误,主要是类的声明不知道类的大小,无论是构造还是析构,都不知道需要为类的对象分配或者回收的内存大小,因此是incomplete type
  • 同时这中前向声明的方式,通常也用于解决循环引用的问题,但是forward declaration方式,被声明的类只能被用于指针,因为作为类的成员变量,必须知道其大小,而声明的Impl类没看到定义,不知道大小,但是指针的大小是固定的。

Interface类


一个能够同时满足两个需求的方法是使用接口类,也就是不包含私有数据的抽象类。调用端首先获得一个AConcrete对象的指针,然后通过接口指针A*来进行操作。


// A.h
class A
{
public:
  virtual ~A();
  virtual X getX() = 0;
  virtual Y getY() = 0;
  virtual Z getZ() = 0;
  ..
};
class AConcrete: public A
{ ... };


这种方法也比较常用,好处类似使用Impl模式,代价是可能会多一个VPTR,指向虚表。

目录
相关文章
|
4月前
|
C++
C++中类的接口与实现分离的技术性探讨
C++中类的接口与实现分离的技术性探讨
76 1
|
16天前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
23天前
|
存储 算法 编译器
[C++] STL简介
[C++] STL简介
17 1
|
3月前
|
存储 算法 C++
C++一分钟之-标准模板库(STL)简介
【6月更文挑战第21天】C++ STL是高效通用的算法和数据结构集,简化编程任务。核心包括容器(如vector、list)、迭代器、算法(如sort、find)和适配器。常见问题涉及内存泄漏、迭代器失效、效率和算法误用。通过示例展示了如何排序、遍历和查找元素。掌握STL能提升效率,学习过程需注意常见陷阱。
44 4
|
2月前
|
存储 编译器 Linux
【C++】string类的使用②(容量接口Capacity )
这篇博客探讨了C++ STL中string的容量接口和元素访问方法。`size()`和`length()`函数等价,返回字符串的长度;`capacity()`提供已分配的字节数,可能大于长度;`max_size()`给出理论最大长度;`reserve()`预分配空间,不改变内容;`resize()`改变字符串长度,可指定填充字符。这些接口用于优化内存管理和适应字符串操作需求。
|
2月前
|
C++ 容器
【C++】string类的使用①(迭代器接口begin,end,rbegin和rend)
迭代器接口是获取容器元素指针的成员函数。`begin()`返回首元素的正向迭代器,`end()`返回末元素之后的位置。`rbegin()`和`rend()`提供反向迭代器,分别指向尾元素和首元素之前。C++11增加了const版本以供只读访问。示例代码展示了如何使用这些迭代器遍历字符串。
|
2月前
|
存储 算法 数据处理
【C++】STL简介
**STL是C++标准库的关键部分,源于Alexander Stepanov的泛型编程研究。它提供了数据结构(如vector、list)和算法,是高效、通用的软件框架。STL始于惠普,后由SGI发展,现已成为C++1998标准的一部分并不断进化。它包括容器、迭代器、算法、仿函数、配接器和分配器六大组件,带来高效性、通用性和可扩展性,但也存在性能开销和学习难度。学习STL涉及理解底层数据结构、用法、实现和实践。推荐[cplusplus.com](https://cplusplus.com)作为学习资源。**
|
3月前
|
存储 算法 编译器
程序与技术分享:C++模板元编程简介
程序与技术分享:C++模板元编程简介
29 0
|
3月前
|
数据采集 自然语言处理 数据挖掘
一文搞懂:【VC++技术杂谈005】如何与程控仪器通过GPIB接口进行通信
一文搞懂:【VC++技术杂谈005】如何与程控仪器通过GPIB接口进行通信
34 0
|
3月前
|
算法 编译器 Linux
【C++】:模板初阶和STL简介
【C++】:模板初阶和STL简介
19 0