(转)从信息隐藏的一个需求看C++接口与实现的分离

原文地址https://blog.csdn.net/tonywearme/article/details/6926649

让我们从stackoverflow上一个同学的问题来开始。问题的原型是这样的(原问题见:class member privacy and headers in C++):
Portaljacker:“有一个类A, 有一些共有成员函数和私有数据,如下所示。”

class A
{
public:
X getX();
Y getY();
Z getZ();
..

private:
X god;
Y damn;
Z it;
};
“可是我不想让使用这个类的使用者看到我的私有数据,应该怎么做?可能是因为担心别人嘲笑我给变量起的名字太难听!哎,可是这关他们什么事呢!我试过把这三个成员变量放进另一个头文件中,就像下面那样,可是编译器报错- - 我该怎么办?!”

// A.h
class A
{
public:
X getX();
Y getY();
Z getZ();
};

// A.cpp
class A
{
private:
X god;
Y damn;
Z it;
};
刚看到这个问题的时候,觉得它很幼稚。首先,C++一个类的定义必须包含该类所有的成员函数和变量,而不像一个名字空间里的不同函数那样可以自由分布在不同的源文件中。其次,这些私有成员即使对调用者可见,又怎么样?反正它们是私有的,用户怎么也不可能直接访问它们。

然而,我错了。

Portaljacker的这个需要实际上是很合情合理的。试想,调用者一般是这样使用类A的。

// main.cpp
#include "A.h"
int main()
{
A a;
X x = a.getX();
Y y = a.getY();
Z z = a.getZ();
 ..
return 0;
}
通常情况下调用者必须要包含A的定义所在的头文件才能顺利通过编译,也就是说建立了一个编译依赖关系:main.cpp -> A.h。这样,任何A.h文件中的变化都将导致main.cpp重新编译,即使改变的只是类A中的私有变量(比如名称改变)。这非常糟糕,因为一个类的私有数据属于它的实现细节(implementation details),理想情况下应该隐藏起来,它的变化对于调用者不可见。哦,不知道你是否曾经遇到过这样一个工程,里面有成百上千的源文件。你只是改变了一个小小的头文件,结果发现项目中的大多数文件都重新编译,几分钟都没有编译完。

其实Portaljacker提出了一个很好的问题。问题的关键在于如何把实现的细节隐藏起来。这样,调用者既不会看到任何类内部的实现,也不会因为实现的任何改变而被迫重新编译。

在讨论问题的解决方法之前,有必要回过头来看看为什么Portaljacker同学的方法行不通。他是把同一个类的共有成员和私有成员风的定义分别放到了两个同名类的定义中(见上)。

我听到了,你说肯定不行。没错,为什么呢?”因为类的定义不能分割开。。“ 好吧,可是为什么呢?”C++就是这样的,常识!“ 资深一些的程序员甚至会翻到C++标准的某一页说,”喏,这就是标准“。我们中的很多人(包括我),学习一门语言的时候都是书上(或者老师)说什么就是什么,只要掌握了正确使用就行,很少有人会去想一下这规则背后的原因是什么。

回到正题。C++之所以不允许分割类定义的一大原因就是编译期需要确定对象的大小。考虑上面的main函数,在类定义分割开的情况下,这段代码将无法编译。因为编译器在编译”A a"的时候需要知道对象a有多大,而这个信息是通过查看A的定义得来的。而此时类的私有成员并不在其中,编译器将无法确定a的大小。注意,Java中并不存在这样的问题,因为Java所有的对象默认都是引用,类似于C++中的指针,编译期并不需要知道对象的大小。

接口与实现的分离

好了,现在让我们回到需要解决的问题上:

不希望使用者可以看到类内部的实现(比如有多少个私有数据,它们是什么类型,名字是什么等等)。
除了接口,任何类的改变不应引起调用者的重新编译。
解决这些问题的方法就是恰当地将实现隐藏起来。为了完整性,我们来看看几个常见的接口与实现分离的技术,它们对于信息隐藏的支持力度是不一样的,也不是都能解决以上所有的问题。

一、使用私有成员

类的接口作为共有,所有的实现细节作为私有。这也是C++面向对象思想的精髓。通过将所有实现封装成私有,这样当类发生改变时,调用者不需要改变任何代码,除非类的公共接口发生了变化。然而,这样的分离只是最初步的,因为它可能会导致调用者重新编译,即使共有接口没有发生变化。

#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;
};
二、依赖对象的声明(declaration)而非定义(definition)

在前一种方法中,类A与X,Y,Z之间是紧耦合的关系。如果类A使用指针而非对象的话,类A并不需要包含X,Y,Z的定义,简单的向前声明(forward declaration)就可以。

// A.h
class X;
class Y;
class Z;

class A
{
public:
 X getX();
Y getY();
Z getZ();
..
private:
X* god;
Y* damn;
Z* it;
};
这样,当X,Y或者Z发生变化的时候,A的调用者(main.cpp)不需要重新编译,这样可以有效阻止级联依赖的发生。在前一种方法中,若X改变,包含A.h的所有源文件都需要重新编译。注意,在声明一个函数的时候,即使函数的参数或者返回值中有传值拷贝,也不需要对应类的定义(上例中,不需要包含X,Y,Z的头文件)。只有当函数实现的时候才需要。

三、Pimpl模式

一个更好的方法是把一个类所有的实现细节都“代理”给另一个类来完成,而自己只负责提供接口。接口的实现则是通过调用Impl类的对应函数来实现。Scott Meyers称这是“真正意义上接口与实现的分离”。

// AImpl.h
class AImpl
{
public:
X getX();
Y getY();
Z getZ();
..
private:
X x;
Y y;
Z z;
};

// A.h
class X;
class Y;
class Z;
class AImpl;

class A
{
public:
// 可能的实现: X getX() { return pImpl->getX(); }
  X getX()
 Y getY()
Z getZ();
..
private:
std::tr1::shared_ptr<AImpl> pImpl;
};
我们来看一下,这种方法能否满足我们的两个要求。首先,因为任何实现细节都是封装在AImpl类中,所以对于调用端来说是不可见的。其次,只要A的接口没有变化,调用端都不需要重新编译。很好!当然,天下没有免费的午餐,这种方法也是需要付出代价的。代价就是多了一个AImpl类需要维护,并且每次调用A的接口都将导致对于AImpl相应接口的间接调用。所以,遇到这样的问题,想一想,效率和数据的封装,哪个对于你的代码更重要。
四、Interface类

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

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

class AConcrete: public A
{ ... };
小结:
尽量依赖对象的声明而不是定义,这样的松耦合可以有效降低编译时的依赖。
能够完全隐藏类的实现,并减少编译依赖的两种方法:Pimpl、Interface。

原文地址:https://www.cnblogs.com/wangshaowei/p/9835603.html

时间: 2024-08-30 15:01:03

(转)从信息隐藏的一个需求看C++接口与实现的分离的相关文章

一个需求变更的故事

昨天我们的物流部门提了一个需求,希望我能为他们做一张出库明细报表,以便他们统计和核对数据.嗯嗯,这个很简单的说,复制一个类似的模板,替换下数据源,按日期分组,20分钟搞定! 这里简单插一下,介绍下我们系统中的报表的实现.报表是采用的第三方控件FastReport,通过设计报表模板->定义报表(选择模板.分期规则.会计主体.报送对象)->生成报表(即时.按分期规则自动). 物流部的同事用即时报表功能看过后提了个新的需求或者是BUG,没有按仓库分组呀!亲! 汗呀,疏忽了,赶紧加上,5分钟搞定!然后

码农的产品思维培养第2节----一个需求的奋斗史(人人都是产品经理)

今天我们继续坚持每日一节的产品思维培养,我喜欢在纸上画,喜欢做笔记.不是为了自己后面回去看,而是为了当时更好理解.不知道大家是否认同这点. 今天看到苏杰的一句话,其实和我之前讲过的是一致的,看来英雄所见略同,还是给大家分享一下"和学习任何领域的知识一样,建议大家在了解了知识框架之后,坚持"需求驱动学习"". 第二章,讲述的是一个需求的奋斗史.其实就是描述如何从用户那里得到需求,得到需求后如何处理的一个过程.今天,我们这一节讲如何从用户那里拿到需求. 用户研究,或者说

二值信息隐藏(分块和游程编码实现)

使用分块进行信息隐藏,因为在对角线上的分块上进行的隐藏,所以 可以明显看到在对角线上有一条线, 200*200的二值图像 512*512的二值图像 (二)使用游程编码,书上的代码逻辑上有错误,还有一些函数错误,根本不能运行出结果 自己修改了得到以下结果 200*200的隐藏160位,可以看到微小的变化 512*512的隐藏160位,基本看不到变化 二值对角线分块隐藏代码 clc; clear; msgfid = fopen('hidden.txt','r'); % 隐藏信息 [msg, coun

教室实时信息查询系统 项目需求规格说明书

  教室实时信息查询系统 项目需求规格说明书 参与人:尹伟 吴文斌 东华理工大学 2015年10月31日 目    录 1.            文档说明... - 1 - 1.1编写目的... - 1 - 1.2背景... - 1 - 1.3适用范围... - 1 - 1.4预期读者与阅读建议... - 2 - 1.5参考文献... - 2 - 1.6术语与缩写解释... - 2 - 1.6.1术语... - 2 - 1.6.2缩写... - 3 - 2.       系统说明... - 4

关于一个需求引发的事务操作和锁-记录解决过程和思路

参考资料: http://openwares.net/java/spring_mybatis_transaction.htmlspring,mybatis事务管理配置与@Transactional注解使用 http://www.cnblogs.com/mingxuan/archive/2011/10/11/2207560.html锁行还是锁表的实践验证 http://blog.csdn.net/hushanfeng110/article/details/50174787使用mybatis 实现乐

MongoDB,另一个角度看数据

传智-玄痛(传智播客北京校区C/C++学院技术指导老师) MongoDB的起源 几年前 10gen 公司做了 SaaS 方面的研发,因为公司一个 MongoDB 产品存储接口的易用性,用户评价非常好,公司开始全力开发 MongoDB.也因此10gen 公司改名为 MongoDB. MongoDB的应用  MongoDB 适用于网站数据.游戏数据.缓存.高伸缩性等场景.目前,百度.阿里.快的打车.京东.赶集网.360.CERN等众多公司纷纷部署MongoDB. MongoDB的特点 在 Web2.

图片嵌入-大容量的信息隐藏算法

分享一下最近看到的一个关于图片嵌入隐藏的算法: “大容量的信息隐藏算法",这是一种基于空间域的自适应多平面位的信息隐藏算法.该算法计算复杂度低.信息隐藏量大.且有实验表明在不影响图像视觉效果的前提下,其信息隐藏量比LSB算法大,并具有更高的安全性.该算法的主要思想是对每个像素点进行判断,根据HVS的特性,在最高非0有效位后的指定位(y)开始嵌入隐藏信息,嵌入到另一个指定位(z)为止. 下面直接贴上MATLAB代码和实验结果: (1) main_ImgEmbed.m clc; clear all;

Web应用程序的敏感信息-隐藏目录和文件

Web应用程序的敏感信息-隐藏目录和文件 0x1.场景 Web应用程序根文件夹中可能存在大量隐藏信息:源代码版本系统文件夹和文件(.git,.gitignore,.svn),项目配置文件(.npmrc,package.json,.htaccess),自定义配置文件使用config.json,config.yml,config.xml等常见扩展以及许多其他扩展. 资源可以分为几个常见类别: 源代码版本控制系统 IDE(集成开发环境)配置文件 项目和/或技术特定的配置和设置文件 1.1.GIT Gi

廖雪峰js教程笔记6 generator一个坑 看完python在回来填坑

generator(生成器)是ES6标准引入的新的数据类型.一个generator看上去像一个函数,但可以返回多次. ES6定义generator标准的哥们借鉴了Python的generator的概念和语法,如果你对Python的generator很熟悉,那么ES6的generator就是小菜一碟了.如果你对Python还不熟,赶快恶补Python教程!. 我们先复习函数的概念.一个函数是一段完整的代码,调用一个函数就是传入参数,然后返回结果: function foo(x) { return