StupidBeauty
Read times:3461Posted at:Sun Oct 8 06:42:13 2017 - no title specified

《C++面向对象软件设计及构建》文章翻译:3.4以引用和指针的方式传递对象,3.4 Communicating Objects by Reference and By Pointer

通过使用引用或指针来传递对象,可构造出三种重要的结构:

  • •. 结果参数 在执行了接收者的方法之后,发送者能够看到对于该对象所做的修改,

  • •. 管理 器:使得,某个类的某个对象,能够,创建、分发或以其它方式管理另一个类的对象,以及

  • •. 关联 在对象之间建立连接,使得,这些对象之间,能够在超过单次方法调用时长的时间跨度中进行交互。

在本小节中,会对这些用法进行说明。在后续两个小节中,会说明关联所具有的额外特性。

以引用方式传递对象,和,以指针方式传递对象,其区别主要在于,语法不同,因为,它们的重要目标是一样的:传递原始的、未被复制的对象。在C++中,以与和符(&)来表示通过引用来传递对象,以星号(*)来表示通过指针来传递对象。不过,要记住,这两个符号,在 C++ 中有多种意义,具体意义取决于使用它们时的上下文。因此,要想表达出以引用或指针的方式来传递对象的意义,则, "&" "*"符号 ,必须出现在参数列表 或返回类型中的某个类名字之后。

结果参数

将某个对象,以传递引用或传递指针的方式用作参数,就使得,发送者能够看到由接收者对该对象所做的修改。在某些情况下,该对象仅仅被用作输出,也就是说,接收者并不会使用该对象的初始值。在其它情况下,接收者会将该对象同时用作输入和输出,也就是说,接收者会使用该个参数对象的初始值,并且,在之后,接收者可能会以某种形式改变该个对象。下面的示例中,会展示后一种用法。

一个简单的信息获取示例,展示了,如何以传递引用和传递指针的方式来传递对象。在这个示例中,会对一个简单的文字文件进行搜索,寻找第一行出现特定字符串的行。利用这种模型,可以构建出多种不同的应用,包括:

  • •.在图书馆的目录文件中寻找特定作者的名字

  • •.在书签文件中寻找特定组织的某个网址

  • •.在电话本文件中寻找特定人士的电话号码

还有狠多其它示例,都是按照这个基本的套路来做的。

在简单的信息获取示例中,对File 类进行了扩展,加入了一个方法,可用于在文件对象中寻找特定的搜索字符串。

以下类,代表着一个信息查询请求:

Query

       class Query {

         private:

                                // 封装的实现

         public:

          Query (char* searchText);

          Query();

          void   SetSearch(char* searchText);

          char*  GetSearch();

          void   AskUser();

          void   SetResult(char* resultText);

          char*  GetResult();

          ~Query();

        };

Query类中,包含了两个文字字符串,即,一个搜索(search)字符串,和一个结果(result)字符串。两个字符串,都有与其相关联的一对查询和设值方法,用于设置及获取该个字符串的值。如果对应的字符串尚未定义,那么,两个“取值”("get")方法会返回空指针。AskUser方法,会显示出一个对话框,以让用户提供一个搜索字符串。在这种设计中的玩法是这样的:发送者定义好搜索字符串(通过构造函数SetSearch方法AskUser方法来指定);实际进行搜索的接收者,定义好结果字符串(通过SetResult方法来指定);最后,发送者取回结果字符串(通过GetResult方法来获取)

File类被扩展了,加入了一个方法,用于在File 对象中搜索指定的查询字符串,如果搜索成功了,则会修改这个Query 对象。在File 类中,添加了以下东西:

class File {

private:

public:

...

void SearchFor (Query& q);              // 通过引用 来传递

void SearchFor (Query* q);              // 通过指针 来传递

...

};

"Query&",表示以引用方式传递Query 对象。"Query*",表示以指针方式传递Query 对象。两种形式中,都不会对该个Query 对象做出复制。

Query类,以及扩展过的File类,可按照如下方式使用。这砣代码中,还展示了,通过引用和通过指针方式传递对象的过程中,在语法上的不同之处。

Query query1("object"), query2("oriented");

Query *query3;

Query *query4;

query3 = new Query("programming");

query4 = new Query("C++");

File booklist("booklist");

booklist.SearchFor( query1);            // 通过引用传递

booklist.SearchFor(&query2);            // 通过指针传递

booklist.SearchFor( query3);            // 通过指针传递

booklist.SearchFor(*query4);            // 通过引用传递

char* result1 = query1.GetResult();

char* result2 = query2.GetResult();

char* result3 = query3->GetResult();

char* result4 = query4->GetResult();

在这个示例中,query1query2是对象的名字,而query3query4是指向对象的指针。在使用query1 作为参数对 SearchFor 方法进行的第一次调用中,使用的是传递引用的方式,因为query1 是一个对象。在传递query1 对象的代码中,匹配到了SearchFor 方法的那个以引用方式传递参数的重载版本。在使用query2 作为参数对 SearchFor 方法进行的第二次调用中,使用的是传递指针的方式,因为,此处的参数,"&query2",是一个指针。此处上下文中的"&"操作符,是“取地址”操作符。因此,此处实际传递的是“query2 的地址”(也就是说,一个指向query2 对象的指针),这样,就匹配到了SearchFor 方法的那个以指针方式传递参数的重载版本。在使用query3 作为参数对 SearchFor 方法进行的第三次调用中,使用的也是传递指针的方式,因为,query3,根据它的声明来看,就是一个指向某个Query 对象的指针。在使用query4 作为参数对 SearchFor 方法进行的第四次调用中,使用的是传递引用的方式。尽管query4 是指向某个Query 对象的指针,但是,实际的参数是"*query4"。"*"操作符,是解引用操作符,它将取出被指向的那个对象。因此,实际的参数是一个对象。由于实际参数是一个对象,所以,它就匹配到了SearchFor 方法的那个以引用方式传递参数的重载版本。

以下表格中,总结了,以引用方式传递参数,与以指针方式传递参数,在语法上的区别。表格中,表头内容,从左往右看,表达的意义是:

  • •.接收者如何声明此处的形式参数,此参数被接收者称为"y",表示T类的某个对象

  • •.发送者如何声明此处的实际参数,此参数被发送者称为"x",表示T类的某个对象

  • •.发送者如何使用对象的名字,"x",来调用接收者的方法

  • •.发送者如何使用“点”符号来访问实际参数的方法

  • •.接收者如何使用“点”符号来访问形式参数的方法。

第一行,对应的是,以上代码示例中,使用query1 对象作为参数来第一次调用SearchFor 方法的情况。接下来各行,分别对应着query2、query3和query4。

传递引用与传递指针:
匹配发送者和接收者

接收者的声明

发送者的声明

实际参数

发送者访问参数对象的方法

接收者访问参数对象的方法

T& y

T x

x

x.f()

y.f()

T* y

T x

&x

x.f()

y->f()

T* y

T *x

x

x->f()

y->f()

T& y

T *x

*x

x->f()

y.f()

管理

管理器对象,一般会以传递引用或传递指针的方式来返回对象。在某些情况下,管理器对象,具有职责,需要创建及分发另一个类的对象。扮演这种角色的管理器,也被称作“工厂”对象。在另外的一些情况下,管理器对象,具有职责,要维护一个由被管理对象组成的集合,按照某些属性来将它们组织起来(例如,根据某个字符串属性按照字母序排序),或者,寻找某个具有特定属性值(例如,特定的名字)的被管理对象。扮演这种角色的管理器,也被称作“集合”。管理器对象,能够简化整个系统,因为,它们能够隐藏掉对于被管理对象的构造、复制、索引、存储和分发过程中的设计细节。系统中的其它部分,可以方便地使用这些被管理的对象,而无需深入关心较底层的管理细节。为了正确地扮演管理器的角色,就必须要使用指针及引用,因为,通常情况下,维护一个由被管理对象的副本所组成的集合,是无意义的;要管理的,是那些对象的原件。

以下,给出了两个示例,以展示,在定义管理器的过程中,如何使用指针和引用。第一个示例中,对File 类进行了扩展,使得,一个文件对象,可以返回以下事物:返回一个对应着底层磁盘文件的文件流对象;或者,返回一个文字窗口,可在其中查看该文件的内容。在这个示例中,File对象就扮演着工厂的角色,它产生出某种类型的文件流对象,或者产生出一个文字窗口对象。第二个示例,展示了集 合的概念,其中定义了一个Frame 管理器,它维护着一组Frame 对象。此处的Frame管理器,与大部分系统中“窗口管理器”的概念类似。

File类示例

File类进行了扩展,以展示,通过传递引用和传递指针来返回对象。对File 类的其中一个扩展,即是,一个新的方法,GetStream(),它返回某个文件流对象(fstream 类的某个对象)的引用,通过这个文件流对象,可对该个File 对象所代表的磁盘文件进行操作。对File 类的第二个扩展,即是,对View()方法做出一个修改,使得,它返回指向某个TextWindow 对象的指针。所指向的TextWindow,即是当前显示着该File 对象内容的那个窗口。返回指向这个窗口的指针,就使得,程序能够对这个窗口进行操作(也就是说,移动它的位置,或者改变它的大小)。对于File 类的两个扩展,如下所示。

扩展过的File

  class File {

    private:

    public:

      ...

       fstream&    GetStream(); // 返回这个文件的流

       TextWindow* View();      // 返回这个文件的内容窗口的指针

      ...

   };

File 类中的GetStream 方法,使得,能够完全暴露出流式输入/输出模型的功能,而又无需在自己的接口中将所有的流式输入/输出方法复制一遍。例如,以下代码中,使用扩展后的File 类来向用户选择的某个文件中输出内容:

FileNavigator nav;

File aFile = nav.AskUser();

fstream& fileStream = file.GetStream();

fileStream << "add new data to file" << endl;

GetStream方法,还支持更复杂的用法,例如,它使得,系统中的不同部分能够获取到对于同一个流对象的引用。这样,系统中的不同部分就能够共享对于同一个底层磁盘文件的访问能力了。GetStream 方法提供的另一个好处是,对该File 对象进行使用的代码中,无需知道该个File 的名字,也无需知道该个文件的名字是从何处得来的。

重新定义过的View 方法,使得,显示着该个磁盘文件内容的窗口,能够被程序控制代码所操作。以下代码,展示了这种用法的一个示例:

FileNavigator nav;

File aFile = nav.AskUser();

TextWindow* tw = aFile.View();  // 显示 出文件内容,并返回查看窗口的指针

tw->MoveTo(Location(10,10));    // 移动查看窗口

tw->Resize(Shape(200,500));     // 改变查看窗口 的大小

正如这个代码中展示的那样,View方法,创建了一个TextWindow,并返回了指向这个TextWindow 的指针。在这个窗口中,可以查看该文件中的文字内容。然后,程序会移动这个查看窗口,并且改变它的大小。

Frame管理器示例

我们会设计一个简单的FrameManager 类,以展示,如何通过传递指针的方式来返回对象。FrameManger,它的职责是,维护一个由Frame 对象组成的集合,并且,返回该集合中具有特定名字的Frame 对象。回想一下,Frame 对象的构造过程中,可以向它赋予一个名字。FrameManager 就是利用这个名字来将特定的Frame 对象与FrameManger 维护的集合中的其它对象区分开来。此处,我们并不去考虑那些细微的东西,例如,如果有两个Frame 对象具有相同的名字的话,管理器(Manager)该做出何种行为。

FrameManager 的定义,如下所示。FrameManager 的构造函数中,可以指定,该个管理器在任何时刻允许的最大的Frame个数。默认值,会导致FrameManager 在任何时刻最多能够容纳10个Frame 对象。AddRemove方法都进行了重载,这样,它们的参数,也就是那个Frame 对象,即可以以传递指针的方式来使用,也可以以传递引用的方式来使用。另外,Remove方法还有一个额外的重载版本,可以通过名字来指定Frame 对象。FindByName方法,会返回一个指针,它指向某个Frame 对象,那个对象的名字与该方法的输入参数一致;如果FrameManager 未能找到符合条件的Frame 对象,那么,这个方法会返回空指针。

FrameManager

class FrameManager {

private:

public:

         FrameManager(int maxFrames = 10);

  void   Add(Frame& frame);

  void   Add(Frame* frame);

  Frame& FindByName(char* frameName);

  void   Remove(char* frameName);

  void   Remove(Frame& frame);

  void   Remove(Frame* frame);

        ~FrameManager();

};

以下简短代码中,展示了FrameManager 的用法示例。在这砣代码中,某个程序里,会同时打开多个窗口。每当在其中一个窗口中发生了鼠标事件时,就会调用OnMouseEvent 函数,并且传入对应的Frame 对象的名字,作为第一个参数。在OnMouseEvent函数中,通常需要对该个发生了鼠标事件的Frame 对象进行操作。FrameManager,提供了一种根据名字来寻找对应Frame 对象的简单方法。

Frame *dialogWindow;

Frame* drawingWindow;

// .. 声明指向其它窗口 的指针

FrameManager windowManager(5);

void OnStart()

{ dialogWindow = new Frame("Dialogue",...);

drawingWindow = Frame("Drawing,...);

//... 创建其它窗口

windowManager.Add(dialogueWindow);

windowManager.Add(drawingWindow);

//... 将其它窗口添加到管理器

}

void OnMouseEvent (char* frameName, int x, int y, int buttonState)

{  Frame* frame = windowManager.FindByName(frameName));

if (frame != (Frame*)0         // 检查返回 值是否为空

{     // 处理frame 中的鼠标事件,需采用frame->语法

}

else { // 处理意外情况 ,FrameManager并不知道这个 frame

}

}


上面的代码中,展示了,FrameManager扮演着一个狠有用的角色,它帮助简化了OnMouseEvent 函数中的代码 - 如果所请求的frame 是在该个FrameManager 的管理之下,那么,单次简单的调用即可由该FrameManager返回它。

关联

我们会使用一个新定义的Message 类和已有的Frame 类来创建一个简单的关联。Message类,表示的是,对于可显示的文字字符串的抽象。Message对象知道该向Frame 中写入什么文字内容,以及,文字内容应当放置在Frame 中的什么位置。另外,Message对象还负责以下事务:从 Frame 中擦除它自己的文字内容;以及,在该Message 对象发生变化时,更新Frame 中的文字内容。下表中给出了Message 类的定义。

Message

class Message {

  private:

                //封装的实现

  public:

      Message (char *textString, Location whereAt);

      Message (Location whereAt);

void  DisplayIn (Frame&   whichFrame);

void  MoveTo (Location newLocation);

void  setText(char* newText);

char* getText();

void  Clear();

void  Draw ();

     ~Message ();

};

注意,在DisplayIn方法中,参数对象whichFrame 是通过引用来传递的;&符号,跟在类名"Frame"之后,表示按照引用来传递。

为了更深入地了解关联这个概念,我们会研究Message 类的实现代码中的一些细节。Message 类的私有数据中,包含两个指针,一个指向该Message 对象显示的文字字符串,另一个,指向要在其中显示字符串的那个Frame 对象。在Message中,还包含着一个Location 对象,它表示的是,要将文字字符串显示在Frame 中的什么位置。Message 类的私有数据,声明如下:

class Message {

private:

//封装 的实现

char     *msgText;           // 显示 这个文字字符串

Frame    *msgFrame;          // 显示 在这个Frame 中

Location msgLocation;        // 显示 在Frame中的这个位置(Location)

public:

...

};

省略掉一些语法细节来看,DisplayIn 方法中的主要代码是这样的:

DisplayIn (Frame& whichFrame) {

msgFrame = &whichFrame;

}

在这个方法中,简单地取得whichFrame 对象的地址,并将这个地址记录下来,作为Frame 指针msgFrame 的值。要想观摩这个方法的效果的话,则,研究以下声明和代码:

// 声明

Frame window("Message Test", Location(100,100), Shape(200,200));

Message greeting("Hello World!", Location(20,20));

// 代码

greeting.DisplayIn(window);

这砣代码,在greeting 对象和window 对象之间创建了一个关联,这种关联关系展示于下图中。它们之间形成了关联关系,因为,在greeting 对象中,保留了指向window 对象的一个长效指针。这个指针,会保持有效,直到发生以下任意事情为止:被greeting 对象修改(也就是说,调用了DisplayIn 方法,使得该个Message 指向另一个不同的Frame 对象);或者,greeting对象本身被销毁。

一个简单的关联关系

关联关系,必须仔细处理,以避免产生野指针和内存泄漏。上面这个关于greeting对象window对象的示例,也可以用于展示这两个缺陷。如果window对象被销毁,那么,在greeting对象中就 出现一个野指针了,因为,greeting对象仍然指向已经被销毁的window 对象在之前所占用的那块内存。如果greeting 对象被销毁,而window 对象未被销毁,则发生内存泄漏。在这种情况下,没有办法再访问到window 对象了。这种对象,会继续占用内存,但是,无法再对它们进行访问。在长期运行的程序中,内存泄漏可能导致整个系统崩溃。

Message 对象与Frame 对象之间的关联关系,会在Message 类的Clear 方法中得到使用。这个方法的代码如下:

Clear() {               // 位于Message 类中

Shape msgShape = msgFrame->TextSize(msgText);

msgFrame->Clear(msgLocation, msgShape);

}

在这砣代码中,会使用私有数据成员msgFrame 和msgText 来从Frame 对象中获取到由该个Message 对象所显示的文字的形状。然后,使用Frame 类的Clear 方法来擦除包含着Message 对象的文字的矩形区域。注意,msgTextmsgFramemsgLocation,是Message 对象的私有数据成员。这些数据成员,可被Message 对象的方法所看到,并且,嘦该对象存在,这些数据成员就也存在(当然,这些数据成员的值可能会发生变化)。而,Shape对象msgShape,只是Clear 方法中的一个本地对象;这个对象,只在Clear 方法的执行期间存在。

另外,注意,Message 类和Frame 类中都有一个Clear()方法。在具体的调用过程中,并不会混淆,会知道该调用哪个Clear 方法。因为,在其上调用方法的那个对象,决定了,会执行哪个方法。例如,以下调用,

greeting.Clear();

会调用到Message 类中的Clear 方法,因为,greeting 是Message 类的一个对象。类似地,以下调用,

window.Clear();

会调用到Frame 类中的Clear 方法,因为,window 是Frame 类的一个对象。

下面的世界妳好(Hello World)示例中,展示了某个Message 对象和某个Frame 对象之间的简单关联关系。在这个版本中,所显示的文字,"Hello World",可通过鼠标拖动。下一节中展示的闪烁文字(Blinking Text)示例,会详细说明关联关系这个概念。

使用类Message 实现的世界妳好

Frame window("Message Test", Location(100,100), Shape(200,200));

Message greeting("Hello World!", Location(20,20));

void OnStart()

{ window.Clear();

  greeting.DisplayIn(window);

  greeting.Draw();

}

void OnMouseEvent(char *frameName, int x, int y, int buttonState)

{ if(buttonState & leftIsDown)

     greeting.MoveTo(Location(x,y));

}

void OnPaint() {

 greeting.Draw();

}          

这个版本的世界妳好程序,跟之前不使用Message 类的版本相比,要简单得多。Message类中,包含了一些必要的辅助设施,使得一个Message 对象能够更完整地管理自己的事务。于是,要想在屏幕上移动某个文字的话,只需要告知Message 对象来移动它自身就可以了 - 不需要再在Message 对象之外跟踪这个信息了。

任务

  1. 1. 文字变幻:编写一个程序,它会利用Message 类和Frame 类组成一个简单的关联关系,使得,"Hello"和"World!"交替出现。也就是说,在任何时刻,只有其中一个单词出现。

  2. 2.使用Query类来实现一个小巧的信息提取系统:(1)使用FileNavigator来从用户处获取到要从中搜索内容的文件,(2)从用户处获取到搜索字符串,(3)在某个窗口中显示查询条件、文件名以及查询结果。

  3. 3. 改写 在2.4 小节的练习中创建 两个 缩小窗口 程序中的任意一个,使得 在window 中,包含着两个Message 对象,它们分别显示window 的当前高度和宽度。 在window 中,还要包含另一个Message,显示着妳的全名。

  4. 4. 改写 在2.3 小节的练习中创建的 边界漫游 程序,使得 在window 中,包含着两个Message对象,它们显示着window 的当前位置。 在window 中,还要包含另一个Message,显示着妳的全名。

  5. 5.编写一个文字坠落程序,使得,一个文字字符串,从屏幕的顶部移动到屏幕的底部。到达屏幕底部之后,文字字符串应当重新出现在屏幕的顶部。

http://www.prenhall.com/divisions/esm/app/kafura/secure/chapter1/html/1.6_generalization.htm

未知美人

未知美人

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

HxLauncher: Launch Android applications by voice commands