(转)从信息隐藏的一个需求看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。
(转)从信息隐藏的一个需求看C++接口与实现的分离的更多相关文章
- Qt核心剖析:信息隐藏
原文 http://devbean.blog.51cto.com/448512/326686 (一) 如果你阅读了 Qt 的源代码,你会看到一堆奇奇怪怪的宏,例如 Q_D,Q_Q.我们的Qt源码之旅就 ...
- 如何封装JS ----》JS设计模式《------ 封装与信息隐藏
1. 封装与 信息隐藏之间的关系 实质是同一个概念的两种表达,信息隐藏式目的,二封装是借以达到目的的技术方法.封装是对象内部的数据表现形式和实现细节,要想访问封装过额对象中的数据,只有使用自己定义的操 ...
- JavaScript设计模式——前奏(封装和信息隐藏)
前面一篇讲了js设计模式的前奏,包括接口的讲解.. 三:封装和信息隐藏: 信息隐藏用来进行解耦,定义一些私有的数据和方法. 封装是用来实现信息隐藏的技术,通过闭包实现私有数据的定义和使用. 接口在这其 ...
- XML的应用 ---- 从一个范例看xml数据、xsd验证、xslt样式
从一个范例看XML的应用 引言 如果你已经看了Asp.Net Ajax的两种基本开发模式 这篇文章,你可能很快会发现这样一个问题:在那篇文章的方式2中,客户端仅仅是发送了页面上一个文本框的内容到服务端 ...
- zz:一个框架看懂优化算法之异同 SGD/AdaGrad/Adam
首先定义:待优化参数: ,目标函数: ,初始学习率 . 而后,开始进行迭代优化.在每个epoch : 计算目标函数关于当前参数的梯度: 根据历史梯度计算一阶动量和二阶动量:, 计算当前时刻的下降 ...
- Spark小课堂Week7 从Spark中一个例子看面向对象设计
Spark小课堂Week7 从Spark中一个例子看面向对象设计 今天我们讨论了个问题,来设计一个Spark中的常用功能. 功能描述:数据源是一切处理的源头,这次要实现下加载数据源的方法load() ...
- Web应用程序的敏感信息-隐藏目录和文件
Web应用程序的敏感信息-隐藏目录和文件 0x1.场景 Web应用程序根文件夹中可能存在大量隐藏信息:源代码版本系统文件夹和文件(.git,.gitignore,.svn),项目配置文件(.npmrc ...
- CTF之LSB信息隐藏术
LSB也就是最低有效位,原理是图片中的像素一般是由三种颜色构成,即三原色(绿红蓝),由这三种颜色可以组成其它各种颜色. 例如在PNG图片的储存中,每个颜色会有8bit,LSB隐写就是修改了像素中的最低 ...
- JS设计模式——3.封装与信息隐藏
封装.信息隐藏与接口的关系 信息隐藏是目的,封装是手段. 接口提供了一份记载着可供公共访问的方法的契约.它定义了两个对象间可以具有的关系.只要接口不变,这个关系的双方都是可以替换的. 一个理想的软件系 ...
随机推荐
- 关于80286——《x86汇编语言:从实模式到保护模式》读书笔记15
一.80286的工作模式 80286首次提出了实模式和保护模式的概念. 实模式:和8086的工作方式相同: 保护模式:提供了存储器管理机制和保护机制,支持多任务. 二.80286的寄存器 (一)通用寄 ...
- sharding-jdbc集成spring+mybatis分表分库
maven: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http ...
- php将“\\”转换成“\”的例子
str_replace('\\\\', '\\', $url);
- React.js 小书 Lesson20 - 更新阶段的组件生命周期
作者:胡子大哈 原文链接:http://huziketang.com/books/react/lesson20 转载请注明出处,保留原文链接和作者信息. 从之前的章节我们了解到,组件的挂载指的是将组件 ...
- 【关于使用SpringJUnit4ClassRunner单元测试报错问题】
今天单元测试如下的代码的时候发现项目老是报错: package com.yhb.jsxn.service; import java.text.SimpleDateFormat; import java ...
- Db - DataAccess
/* Jonney Create 2013-8-12 */ /*using System.Data.OracleClient;*/ /*using System.Data.SQLite;*/ /*us ...
- [转]真正了解CSS3背景下的@font face规则
本文转自:http://www.zhangxinxu.com/wordpress/2017/03/css3-font-face-src-local/ by zhangxinxu from http:/ ...
- golang学习之奇葩的time format
golang格式化时间时,比如如下格式进行格式化输出: fmt.Println(time.Now().Format("2007年03月")) 程序直接输出: 16007年02月 很 ...
- [android] android项目架构
准备步骤: 1.创建工程(设置版本兼容,最低兼容版本) 2.导入常用jar包,(处理字符串和加密用的jar文件) Commons Lang,Commons Codec commons-codec.ja ...
- Java并发编程:CountDownLatch、CyclicBarrier和Semaphore (总结)
下面对上面说的三个辅助类进行一个总结: 1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同: CountDownLatch一般用于某个线程A等待 ...