StupidBeauty
Read times:414Posted at:Sat Mar 10 05:28:55 2018 - no title specified

《C++面向对象软件设计及构建》文章翻译:4.4 组织 好代码 ,4.4 Organizing the Code

那些用于定义、实现及使用各个类的代码,是以若干个文件的方式组织起来的。对于非常小的程序,可以将所有的代码放置在一个单独的文件中。不过,狠显然的是,这种做法,除了可以用于最简单的系统之外,想要用在任何其它情况下都是不实际的。下面所讲到的组织方式,在所有的情况下都适用,也是实际项目中所通用的做法。

将接口定义和具体实现相分离

类的定义,是放置在头文件中的。而代码文件中,包含的是,具体实现或使用那些类的代码。按照惯例,头文件的文件名带有".h"后缀。对于代码文件,其惯用的后缀取决于编译器或系统;常用的代码文件后缀是".C"".cc"".cpp"。而文件名中的基础部分,显然是根据该文件的内容来命名的。具体使用哪种代码文件名后缀并不重要 - 真正重要的是,统一地使用相同的后缀名。

每个头文件中,包含着一个类的定义,或者高度相关的多个类的定义。例如,头文件"Frame.h"中,仅包含着Frame 类。在一个窗口系统中,还可能会有单独的头文件,用于包含按钮、滑动条、画笔和其它元素的类定义,这些元素在图形用户界面中是狠常见的。在狠多情形下,会有一些类的定义是高度相关的,于是就被组织到同一个头文件中。在两种情况下,会实际采用这种组织方式。第一种情况是,在某个窗口类的定义中,可能需要用到其它类,而其它类被定义到同一个头文件中。第二种情况是,某一组类的设计者认为,实际开发过程中,用户程序员会需要用到这一整组类的定义;将它们放置到同一个头文件中,就能够让用户程序员更方便地使用它们,因为,用户程序员只需要查看一个头文件即可找到所有相关的类定义。例如,在某个图形系统中,可能会提供关于矩形、圆形、颜色和标尺的类。使用一个单个的头文件,例如"Graphics.h",即可容纳所有这些定义。在进行头文件组织的过程中,要注意的狠重要的一点就是,只能将高度相关的类定义放置在同一个文件中。

用于实现一个类的代码,是放置在其独自的代码文件中;例如,用于实现Frame 类的代码,可以被放置在文件"Frame.cc"中。即使是多个类的定义都可以放置在同一个头文件中,但是,它们的具体实现一般都是放置在各自的代码文件中的。

使用之前要先定义

由于C++是一门静态类型的语言,因此,在对某个类进行任何形式的使用之前,C++编译都必须看到那个类的定义。在以下三种重要的情形下,都必须满足这条规则:

  • •.编译某个类的实现代码:在对代码文件中每个方法进行翻译的过程中,编译器必须进行检查,以确认,该个方法的特征属性与头文件中为该个方法所给出的特征属性相符。以下情况下,编译器会检测出错误:方法名不匹配;或者,某个方法的参数的个数和/或类型不匹配。在这种情况下,编译器必须能够看到该个类的头文件,才可能成功地对该个类的实现代码进行编译。

  • •.编译某个类的代码,这个类的接口中引用到了另一个类:在之前的多个示例中,已经展示过了,某个类(被用作参数的类)的某个对象,被作为参数,传递到在另一个不同的类(被调用的类) 上所调用的方法中。在这种情况下,当C++编译器被要求编译此处被调用的类时,就必须检查,被用作参数的类,是否实际存在。在这种情况下,编译器在看到被调用的类的定义之前,必须先看到被作为参数的类的定义。

  • •.编译某个类的代码,这个类的实现代码中引用了另一个类:一种狠常见的用法是,某个类(消息发送者)调用了另一个类(消息接收)的方法。在这种情况下,C++编译必须进行检查,以确认,在消息发送者中进行的方法调用,其特征,与消息接收者对象的类中定义的某个方法的特征相符。如果未能找到相符的方法,那么,编译器就会检测到错误,并报告出一条错误消息。在这种情况下,编译器在编译消息发送者类的实现代码之前,必须能够看到消息接收者类的头文件。

以下展示这三种情况下的示例。

预处理器

为了确保,向编译器提供的代码,满足“先定义后使用”的顺序关系,会首先使用一个简单的预处理器来对代码进行扫描。预处理器(通常名为"cpp",意思是C/C++预处理程序(C/C++ Preprocessor Program)会在它所扫描的文件中寻找简单的命令或指令。指令,以首列的井号(#)开头,该行的其它内容即为该指令的具体信息。严格来说,这些指令,并不是C++语言的一部分,它们只能被预处理器所理解。预处理器可能会扫描狠多文件,但是,它一定只会产生一个输出文件。对于每一行不是指令的输入内容,它或者被原样复制到输出文件中,或者直接被忽略掉而不复制到输出文件中。预处理器也可能会在输出某些行之前对其内容进行修改,但是,它如何做到这一点,以及为何要做这一点,并不是我们此刻要在意的事情。

预处理器所支持的最基本的指令之一,就是,使得妳能够将新的文件添加到正在被扫描的文件集合中去。这种指令,其语法是 #include filename 在包含了多个文件的情况下, 预处理器 会利用一个栈式数据结构来辅助进行文件扫描。 举个例子,假设, 预处理器被要求扫描文件 A ,这个文件的情况是:

  • •.文件A中含有一些文字内容,A1,接着,包含(includes)了文件B 和C,接着含有一些文字内容,A2

  • •.文件B中含有一些文字内容,B1,接着,包含(includes)了文件D,接着含有一些文字内容,B2

  • •.文件C中含有一些文字内容,C1,接着,包含(includes)了文件D

  • •.文件D中包含(includes)了文件E,接着,含有一些文字内容,D1

  • •.文件E中仅含有文字内容E1

在这种情况下,预处理器在它的唯一一个输出文件中,会按照顺序输出以下文字内容:

A1, B1, E1, D1, B2, C1, E1, D1, A2

注意,此处的处理顺序是栈式风格的。在扫描完了文字内容A1之后,对文件A 的扫描就暂停(“被压入栈中”)了,此时开始扫描被包含的(included)文件B 和C。在扫描完了这些被包含的(included)文件之后,才继续(“从栈中弹出”)扫描文件A。另外,要注意,文件D 和E 在输 出流中出现了两次,因为,文件D 被包含(included)了两次,一次是由文件B 包含的,另一次是由文件C 包含的。

包含(Including)头文件

预处理器的#include指令,是用来确保,所提供给编译器的源代码文字内容,是符合“先定义后使用”顺序的。接下来的这个示例,展示了,此处所说的这个指令,应当如何在头文件和代码文件中使用。

Location类,展示了,第一种必须包含头文件的情况。下表中再次展示了Location 类的接口和实现。注意,代码文件中是以一个包含(include)指令开头的。


Location 类中对 #include 指令的使用

接口
(位于文件Location.h)

实现
(位于文件Location.cpp)

class Location {

  private:

     int currentX, currentY;

 public:

    Location(int x, int y);

     Location();  

    int Xcoord();  

    int Ycoord();  

};

#include "Location.h"

Location::Location( int x, int y )

   { currentX = x; currentY = y; }

Location::Location ()

   { currentX = -1; currentY = -1; }

int Location::Xcoord()

   { return currentX; }

int Location::Ycoord()

   { return currentY; }

Message类,展示了,第二种需要使用包含(include)指令的情况的例子。如下所示,在Message类的接口中,引用了LocationFrame两个类。这是因为,有一个Location对象和一个Frame指针都是这个类的私有数据的一部分。同时,在Message类的方法中,也会使用一个Location对象或一个Frame引用作为其参数。


Message 类中对于 #include 指令的使用

接口
(位于文件Message.h)

实现
(位于文件Message.cpp)

#include "Frame.h"

#include "Location.h"

class Message {

  private:

      char*    messageText;

      Frame*   frame;

      Location location;

  public:

      Message (char *textString,

               Location whereAt);

      Message (Location whereAt);

void  DisplayIn (Frame& aFrame);

void  MoveTo (Location newLocation);

void  setText(char* newText);

char* getText();

void  Clear();

void  Draw ();

     ~Message ();

};

#include "Message.h"

Message::Message(char *textString,...)

   { ... }

Message::Message(Location whereAt)

   { ... }

void Message::DisplayIn(Frame& aFrame)

   { ... }

void Message::MoveTo(Location newLocation)

   { ... }

// Message 类的实现代码的其余部分

注意,代码文件(Message.cpp)包含includes)了头文件(Message.h),同时,Message 类的头文件又包含(includes)了Location 和Frame 类的头文件。在代码文件中,并不需要包含(include)Location 和Frame 类的头文件,因为,在Message 类的头文件中已经包含(included)了那两个文件了。

对于第三种需要用到包含(include)指令的情况,则在下表中以FileChooser 类来展示。注意,在FileChooser 类的接口中,仅仅使用了内置的"char*"类型,因此,不需要包含(include)任何其它的文件。FileChooser 的接口并不依赖任何其它类的接口。然而,FileChooser 的实现却用到了两个其它的类,即为Directory 类和Selector 类。在 FileChooser 的AskUser 方法中会用到这两个类的对象。在编译FileChooser 类的代码的过程中,编译器需要确认,Directory 类中确实定义了名为First()和Next()的方法,并且,Selector 类中确实定义了名为Add 和AskUser 的方法。为了满足编译器的要求,在实现文件Choose.cpp 中,包含(includes)了Directry.h 和Selector.h 两个文件。有一点狠重要的注意事项,那就是,在头文件中并不需要包含(include)这两个文件中的任何一个,因为,它的接口并不依赖Directory 和Selector 类,仅仅是它的实现依赖于这两个类。


FileChooser 类中对 #include 指令的使用

接口
(位于文件Choose.h)

实现
(位于文件Choose.cpp)

class FileChooser {

private:

  char* thePath;

  char* theFilter;

public:

 FileChooser(char* path, char* filter);

 FileChooser(char* path);      

 FileChooser();                    

 File AskUser();                      

 ~FileChooser();

};

#include "Choose.h "

#include " Directry.h "

#include " Selector.h "

// 其它代码被省略

File FileChooser::AskUser()

{ Directory directory(thePath,

                      theFilter);

  Selector selector(thePath);

  char* nextName;

  char* fileChosen;

  nextName = directory.First();

  while (nextName) {

    selector.Add(nextName);

    nextName = directory.Next();

  }

  fileChosen = selector.AskUser();

  return File(fileChosen);

}


避免重复定义

必须仔细组织代码,以避免将同一个类的定义多次呈现到编译器面前,即使这些定义是完全相同的也不行。如果编译器遇到了同一个类的两次(即使是完全相同的)定义,它就会报告一个错误,其大意会是,表明,这个类有“多次定义”。这种情况,在实际开发中狠容易出现:在预处理器中将某个头文件两次包含(includes)到输出文件中去即可引起这种问题。下面的示例,展示了,这种错误多么容易出现。利用额外的一些预处理器指令,可阻止这种错误的发生。

下面会使用文字闪(BlinkingText)程序来展示,向编译器提供同一个类的多次定义的问题。下表展示了这些源文件中的关键部分。注意,在这些文件中,对于#include指令的使用是正确的:例如,BlinkingText.cpp文件中必须包含(includeFrame.hMessage.h两个头文件,因为,在代码中声明了这两个类的对象。同时,Location.h这个头文件,也必须被Frame.h 和Message.h 两个文件所包含(included),因为,在这些文件中,会有一个或多个方法,是以某个Location 对象作为参数的。

文件

内容

BlinkingText.cpp

#include "Frame.h"

#include "Message.h"

Frame window(...);

Message greeting(...);

...

Frame.h

#include "Location.h"

class Frame

{...

 void MoveTo(Location loc);

 ...

}

Message.h

#include "Location.h"

class Message

{...

 void MoveTo(Location loc);

 ...

}

当预处理器扫描BlinkingText.cpp 这个文件时,多次定义的问题就出现了;在这个例子中,预处理之后,向编译器所提交的输出内容中,将会含有以下的类的定义代码:

Location, Frame, Location, Message

Location类,出现了两次,因为,它被Frame.h 和Message.h 两个文件所包含(included)了。

使用预处理器变量,可以阻止产生重复的类定义。这些变量,能够帮助预处理器得知,应当在何时将某个被包含的文件写入到预处理器输出内容中去,以及,在何时,被包含的文件已经被写入到输出文件中去因而不应当再次写入了。一个预处理器变量,可由任意的字符串表示,而在此处,针对我们的具体场景来说,只需要区分出一个预处理器变量是未定义的(预处理器此刻还不知道它)还是已定义的(预处理器此刻知道它)即可。对于已定义的预处理器变量,还可能会在其上附着有具体的值,但是,目前,如何做到这一点,以及为何要做这一点,并不重要。预处理器变量,与C++代码中的那些变量毫无关系;它们只是在预处理过程中使用的名字而已。

预处理器变量,由预处理器指令 #define variablename 来定义。任何一个尚未明确定义过的变量,都是未定义的(undefined)。究竟一个变量当前是定义了的,还是未定义的,可使用另一个预处理器指令来测试。这个指令是, #ifndef variablename 。如果对应的变量名尚未定义,则其值为真,否则其值即为假。与之配套的,还有一个指令, #endif ,它表示 #ifndef 指令内容的结束。预处理器,会按照以下规则来对这条指令做出反应:

  • •.如果此条 #ifndef 语句的值为真(true),则,继续将正在扫描的文件中后续的行的内容放置到输出文件中去

  • •.如果此条#ifndef语句的值为假(false),则,跳过这个文件中接下来扫描到的那些行的内容,直到遇到一个 #endif 为止

这三个预处理指令 ( #define #ifndef #endif ) ,构成了一个标准模式,用于避免 类的定义重复出现 ,下表中的Location.h 文件即展示了这一点。注意 ,此处使用的预处理器变量,其名字还是比较怪异的 ," _LOCATION_H" 在标准的实践中,此处所使用的预处理器变量, 会是在文件名基础上进行某种变化而构成的。使用 这种标准实践,就可以狠容易地唯一定义好每个预处理器变量,因为,具有特定名字 的文件,都是只有一个的。


带有预处理器指令的 Location.h 文件

#ifndef _LOCATION_H

#define _LOCATION_H

class Location

{

   ... // 类定义

};

#endif

这种做法,将导致,预处理器按照以下方式来做出行为:

  • •. 当第一次包含这个文件时,此处 的预处理器变量 ( _LOCATION_H )尚未定义 。于是 ,这个文件中,除了其它的预处理器命令之外的那些行的内容,都会被处理,并且输出到输出文件中去。 这样,Location 类的定义就被写入到输出文件中去了。 在这个处理过程中, 会遇到的一个预处 理器命令即是,#define命令 它会定义那个预处理器变量。 在这个例子中,#endif指令 没有任何额外效果,因而不会被复制到输出文件中去。

  • •. 当这个文件第二次 ( 以及后续的每一次 ) 被包含时,对应 的预处理器变量 ( _LOCATION_H ) 就已经是定义了的。于是 预处理器就会忽略 ( 不将内容复制到输出文件中去 ) 该文件中全部 行的内容,直到遇到一个#endif指令为止。 这样,Location 类的定义就不会在预处理器的输出内容中重复出现了。

只需稍加练习,日后,妳就会在所有的头文件中自发地包含这些预处理器指令了。

任务

  1. 1.对于妳目前使用的编译器,搞清楚,如何运行它的预处理器以及将预处理器产生的输出内容保存下来。通常会有一些对应的选项、设置或者标志位,使得妳能够完成这件事。针对某个示例程序,或者妳之前为其它任务而写的程序来运行预处理器。注意,都包含了哪些头文件,是以什么顺序包含的。请解释一下妳在这个输出内容中看到的东西。

  2. 2.临时删除Location.h 文件中的#ifndef#define#endif指令,尝试编译一个会多次包含这个头文件的程序。结果遇到了什么错误信息?重新加入这三个指令,再重新编译这个程序,以确认,妳是否正确地重新加入了它们。

  3. 3.假设,预处理器被调用,以扫描文件A,该文件是这样的情况:

    1. 1.文件A包含了文件B,然后具有某些文字内容A1,然后包含了文件C,然后具有某些文字内容A2

    2. 2.文件B包含了文件D,然后具有某些文字内容B1

    3. 3.文件C具有某些文字内容C1,然后包含了文件E,然后具有某些文字内容C2

    4. 4.文件D仅具有某些文字内容D1

    5. 5.文件E仅具有某些文字内容E1

    在预处理器的输出文件中,各段文字内容之间的顺序是什么样的?

杨蕊

星雨

Your opinions
Your name:Email:Website url:Opinion content:
- no title specified

为OsoLinux用户提供的RPM包仓库

 
??Like this article? Give us some tips.??