漫谈

本文使用 VS 2010/2015 作为开发环境。

小提示:在 Visual Studio 中,为了使得直接运行(不调试)控制台程序时,能够在控制台窗口的最后显示“请按任意键继续”,需要在“解决方案资源管理器”中打开项目的属性页面,然后在 配置属性->链接器 -> 系统 -> 子系统 中选择“控制台 (/SUBSYSTEM:CONSOLE)”。

从结构体开始

以下是一个比较简单的 C++ 程序,实际上,这段代码可以使用 C(而不是C++)编译器完成编译。

运行结果如下

然后,我们开始使用结构体。下面的代码看看结构就可以了,具体的实现方法就不用管了。

运行结果如下

C++

接下来,如果使用 C++ 的编写方法,上面的代码可以写成这样

运行结果不变,此处不再赘述。
顺带一提,本文中将Book结构体中声明的两个变量Book::IdBook::Name称为字段[field]

面向对象

C++最重要的一点就是引入了类[class]的概念以及class关键字。为了和C保持兼容性,C++ 中classstruct仅存在默认可访问性的差异。其实,你正在使用的究竟是结构体还是类,其实是取决于你的使用方式。对于高级语言(如 VB/C#/JAVA)来说,类分配在堆[heap]上,默认是按引用传递(或者说一般用的是对应的指针类型),而结构体分配在栈(堆栈)[stack]上,默认是按值传递。而在C++中,我们可以自由控制结构体/类的分配位置

同样,我们也可以自由控制类和结构体的传递方式。此处就不再赘述了。因此,下文中的“结构体”和“类”指代的是同一种数据结构——这与高级语言是不同的。

把函数放到类的里面,成为方法[method]

运行结果不变。

由此可以看出,类的引入使得程序的模块化变得更为便利。

设置成员的可访问性

假使我们不希望书的信息可以在 main 中随意修改,像这样

而仅允许用户通过控制台来输入/输出书籍信息,那么我们应当限制 Book 类中成员[member]可访问性[accessibility]

这样,在 Book 类之外的地方就不能访问IdName了。实际上,这一点也可以从VS的自动完成中看到

从自动完成列表中可以看到,Id 和 Name 均为私有成员,对外是不可访问的。
从自动完成列表中可以看到,Id 和 Name 均为私有成员,对外是不可访问的。

如果我们使用以下代码

则会导致编译器报错

也就是说,通过可访问性控制,作为一个功能模块和接口的设计者,我们可以明确地指出,一个成员究竟是这个模块的内部组成部分,还是对外暴露的接口。例如,此处Book的定义很明显就是为了让大家通过InputBookPrintBook来使用功能的。至于IdName,作为接口的设计者,我们不希望其可以由Book类外面的代码直接访问到,因此使用private说明其访问性只对类的内部成员可见。

structclass

之前有提到过,classstruct仅存在默认可访问性的区别,那么,它们之间究竟有什么不同呢?我们可以通过两段代码比较一下

等价于

注意:可见性[visibility]和可访问性[accessibility]是两个不同的概念。尽管我们在代码中无法访问到IdName,但任何人只要有Book类的定义[definition],就是上面 class Book {...};这一部分,都可以看到这个类里面有两个字段。尽管类中的函数可以将函数头(声明)和函数体(定义)分开,但字段是必须留在类的定义中的。实际上,编译器需要这段代码来确定Book类的内存布局。例如, sizeof(Book) 的计算结果是 sizeof(Book::Id) + sizeof(Book::Name) = 4 + 64 = 68 :
zephyr-1-book-size

可见性与二进制兼容性

可见性与可访问性的分裂直接影响了 C++ 库的版本二进制兼容性。因为C++中,所有的成员都是通过内存地址,以及内存地址加上偏移量来确定的。因此,一旦一个类的声明发生了变化,即使发生变化的是私有成员——外界无法访问——但仍会造成整个类的内存布局变化。如果你正在设计一个类库,那么你将要面对由此带来的不同版本之间的兼容性问题。人们为了解决这一问题,提出了诸如 COM、PImpl 之类的设计模式,这里就不再赘述了。一般来说,高级语言不存在这个问题。VB6.0 及其以前版本中,所有的类均继承自IDisposeIDispatch,使用 COM 实现[这也就是 VB6 中类模块中不能公开结构体(使用 Type语句定义)的原因];在 .NET/JAVA 中,成员调用是通过成员名称对应而不是内存地址对应的,因此也不存在二进制兼容性的问题。

内聚性与耦合性

这里使用Book类作为示例也存在不妥之处,因为作为一个功能模块,Book仅仅用于保存图书信息;然而,很明显,InputBookPrintBook的实现中使用到了printfscanf等函数,这意味着Book对这些函数产生了依赖(耦合)。试想,如果我们准备使用可视化界面而不是控制台来输入/输出信息,那么InputBookPrintBook就不可避免地需要做出修改。这显然是不合理的。因此,在设计模块的时候,我们需要尽量提高模块的内聚性,减少模块之间的耦合性。也就是说,尽量让一个模块只做一件事情。

那么,整个代码可以写成这个样子:

如果 main 函数愿意的话,可以使用 STL 中的iostream来进行输入/输出

可以注意到,由于Book并未直接依赖stdio,因此我们可以不修改41行以前的所有代码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.