欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費電子書(shū)等14項超值服

開(kāi)通VIP
【讀書(shū)筆記】[Effective C++第3版][第31條] 要努力減少文件間的編譯依賴(lài) ...

第31條:     要努力減少文件間的編譯依賴(lài)

為了更新某個(gè)類(lèi)的某個(gè)功能實(shí)現,你可能需要在浩瀚 C++ 的代碼中做出一個(gè)細小的修改,要提醒你的是,修改的地方不是類(lèi)接口,而是實(shí)現本身,并且僅僅是私有成員。完成修改之后,你需要對程序進(jìn)行重新構建,這時(shí)你肯定會(huì )認為這一過(guò)程將十分短暫,畢竟你只對一個(gè)類(lèi)做出了修改。當你按下“構建”按鈕,或輸入 make 命令(或者其他什么等價(jià)的操作)之后,你驚呆了,然后你就會(huì )陷入困惑中,因為你發(fā)現一切代碼都重新編譯并重新鏈接了!所發(fā)生的事情難道不會(huì )讓你感到不快嗎?

問(wèn)題的癥結在于: C++ 并不擅長(cháng)區分接口和實(shí)現。一個(gè)類(lèi)的定義不僅指定了類(lèi)接口的內容,而且指明了相當數量的實(shí)現細節。請看下面的示例:

class Person {

public:

  Person(const std::string& name, const Date& birthday,

         const Address& addr);

  std::string name() const;

  std::string birthDate() const;

  std::string address() const;

  ...

 

private:

      std::string theName;        // 具體實(shí)現

      Date theBirthDate;          // 具體實(shí)現

      Address theAddress;         // 具體實(shí)現

};

這里,如果無(wú)法訪(fǎng)問(wèn) Person 具體實(shí)現所使用的類(lèi)(也就是 string 、 Date Address )定義,那么 Person 類(lèi)將不能夠得到編譯。通常這些定義通過(guò) #include 指令來(lái)提供,因此在定義 Person 類(lèi)的文件中,你應該能夠找到這樣的內容:

#include <string>

#include "date.h"

#include "address.h"

不幸的是,這樣做使得定義 Person 的文件對這些頭文件產(chǎn)生了依賴(lài)。如果任一個(gè)頭文件的內容被修改了,或者這些頭文件所依賴(lài)的另外某個(gè)頭文件被修改,那么包含 Person 類(lèi)的文件就必須重新編譯,有多少個(gè)文件包含 Person ,就要進(jìn)行多少次編譯操作。這種瀑布式的編譯依賴(lài)將招致無(wú)法估量的災難式的后果。

你可能會(huì )考慮:為什么 C++ 堅持要將類(lèi)具體實(shí)現的細節放在類(lèi)定義中呢?假如說(shuō),如果我們換一種方式定義 Person ,單獨編寫(xiě)類(lèi)的具體實(shí)現,結果又會(huì )怎樣呢?

namespace std {

     class string;               // 前置聲明 ( 這個(gè)是非法的,參見(jiàn)下文 )

}

 

class Date;                     // 前置聲明

class Address;                  // 前置聲明

 

class Person {

public:

      Person(const std::string& name, const Date& birthday,

                 const Address& addr);

      std::string name() const;

      std::string birthDate() const;

      std::string address() const;

    ...

};

如果這樣可行,那么對于 Person 的客戶(hù)端程序員來(lái)說(shuō),僅在類(lèi)接口有改動(dòng)時(shí),才需要進(jìn)行重新編譯。

這種想法存在著(zhù)兩個(gè)問(wèn)題。首先, string 不是一個(gè)類(lèi),它是一個(gè) typedef typedef basic_string<char> string )。于是,針對 string 的前置聲明就是非法的。實(shí)際上恰當的前置聲明要復雜的多,因為它涉及到其他的模板。然而這不是主要問(wèn)題,因為你本來(lái)就不應該嘗試手工聲明標準庫的內容。僅僅使用恰當的 #include 指令就可以了。標準頭文件一般都不會(huì )成為編譯中的瓶頸,尤其是在你的編譯環(huán)境允許你利用事先編譯好的頭文件時(shí)更為突出。如果分析標準頭文件對你來(lái)說(shuō)的確是件麻煩事,那么你可能就需要改變你的接口設計,避免去使用那些會(huì )帶來(lái)多余 #include 指令的標準類(lèi)成員。

對所有的類(lèi)做前置聲明會(huì )遇到的第二個(gè)(同時(shí)也是更顯著(zhù)的)難題是:在編譯過(guò)程中,編譯器需要知道對象的大小。請觀(guān)察下面的代碼:

int main()

{

  int x;                          // 定義一個(gè) int

 

  Person p( params );             // 定義一個(gè) Person

   ...

}

當編譯器看到了 x 的定義時(shí),它們就知道該為其分配足夠的內存空間(通常位于棧中)以保存一個(gè) int 值。這里沒(méi)有問(wèn)題。每一種編譯器都知道 int 的大小。當編譯器看到 p 的定義時(shí),他們知道該為其分配足夠的空間以容納一個(gè) Person ,但是他們又如何得知 Person 對象的大小呢?得到這一信息的唯一途徑就是通過(guò)類(lèi)定義,但是如果允許類(lèi)定義省略具體實(shí)現的細節,那么編譯器又如何得知需要分配多大空間呢?

同樣的問(wèn)題不會(huì )在 Smalltalk Java 中出現,因為在這些語(yǔ)言中,每當定義一個(gè)對象時(shí),編譯器僅僅分配指向該對象指針大小的空間。也就是說(shuō),在這些語(yǔ)言中,上面的代碼將做如下的處理:

int main()

{

  int x;                         // 定義一個(gè) int

 

  Person *p;                     // 定義一個(gè) Person

  ...

}

當然,這段代碼在 C++ 中是合法的,于是你可以自己通過(guò)“將對象實(shí)現隱藏在指針之后”來(lái)玩轉前置聲明。對于 Person 而言,實(shí)現方法之一就是將其分別放在兩個(gè)類(lèi)中,一個(gè)只提供接口,另一個(gè)存放接口對應的具體實(shí)現。暫且將具體實(shí)現類(lèi)命名為 PersonImpl , Person 類(lèi)的定義應該是這樣的:

#include <string>                // 標準庫成員,不允許對其進(jìn)行前置聲明

 

#include <memory>               // 為使用 tr1::shared_ptr; 稍后介紹

 

class PersonImpl;               // Person 實(shí)現類(lèi)的前置聲明

 

class Date;                     // Person 接口中使用的類(lèi)的前置聲明

class Address;

 

class Person {

public:

 Person(const std::string& name, const Date& birthday,

        const Address& addr);

 std::string name() const;

 std::string birthDate() const;

 std::string address() const;

 ...

 

private:                        // 指向實(shí)現的指針

  std::tr1::shared_ptr<PersonImpl> pImpl;

};                // 關(guān)于 std::tr1::shared_ptr 的更多信息,參見(jiàn) 13

在這里,主要的類(lèi)( Person )僅僅包括一個(gè)數據成員——一個(gè)指向其實(shí)現類(lèi)( PersonImpl )的指針(這里是一個(gè) tr1::shared_ptr ,參見(jiàn)第 13 條),其他什么也沒(méi)有。我們通常將這樣的設計稱(chēng)為 pimpl idiom (指向實(shí)現的指針)。在這樣的類(lèi)中,指針名通常為 pImpl ,就像上面代碼中一樣。

通過(guò)這樣的設計, Person 的客戶(hù)端程序員將會(huì )與日期、地址和人這些信息隔離開(kāi)。你可以隨時(shí)修改這些類(lèi)的具體實(shí)現,但是 Person 的客戶(hù)端程序員不需要重新編譯。另外,由于客戶(hù)端程序員無(wú)法得知 Person 的具體實(shí)現細節,他們就不容易編寫(xiě)出依賴(lài)于這些細節的代碼。這樣做真正起到了分離接口和實(shí)現的目的。

這項分離工作的關(guān)鍵所在,就是用聲明的依賴(lài)來(lái)取代定義的依賴(lài)。這就是最小化編譯依賴(lài)的核心所在:只要可行,就要將頭文件設計成自給自足的,如果不可行,那么就依賴(lài)于其他文件中的聲明語(yǔ)句,而不是定義。其他一切事情都應遵從這一基本策略。于是有:

l  只要使用對象的引用或指針可行時(shí),就不要使用對象。 只要簡(jiǎn)單地通過(guò)類(lèi)型聲明,你就可以定義出類(lèi)型的引用和指針。反觀(guān)定義類(lèi)型對象的情形,你就必須要進(jìn)行類(lèi)型定義了。

l  只要可行,就用類(lèi)聲明依賴(lài)的方式取代類(lèi)定義依賴(lài)。 請注意你在使用一個(gè)類(lèi)時(shí),如果你需要聲明一個(gè)函數,那么在任何情況下定義出這個(gè)類(lèi)都不是必須的。即使這個(gè)函數以傳值方式傳遞或返回這個(gè)類(lèi)的對象:

class Date;                     // 類(lèi)聲明

 

Date today();                   // 這樣是可行的

void clearAppointments(Date d);// 但并沒(méi)有必要對 Date 類(lèi)做出定義

當然,傳值方式在通常情況下都不會(huì )是優(yōu)秀的方案,但是如果你發(fā)現某些情景下不得不使用傳值方式時(shí),就會(huì )引入不必要的編譯依賴(lài),你依然難擇其咎。

在不定 Date 的具體實(shí)現的情況下,就可以聲明 today clearAppointments , C++ 的這 一能力恐怕會(huì )讓你感到吃驚,但是實(shí)際上這一行為又沒(méi)有想象中那么古怪。如果代碼中任意一處調用了這些函數,那 么在這次調用前的某處必須要對 Date 進(jìn)行 定義。此時(shí)你又有了新的疑問(wèn):為什么我們要聲明沒(méi)有人調用的函數呢 , 這不是多此一舉嗎?這一疑問(wèn)的答案很簡(jiǎn)單:這種函數并不是沒(méi)有人調用,而是不是所有人都會(huì )去調用。假設你的庫中包含許多函數聲明,這并不意味著(zhù)每一位客戶(hù)端程序員都會(huì )使用到所有的函數。上文的做法中,提供類(lèi)定義的職責將從頭文件中的函數聲明轉向客戶(hù)端文件中包含的函數調用,通過(guò)這一過(guò)程,你就排除了手工造成的客戶(hù)端類(lèi)定義依賴(lài),這些依賴(lài)實(shí)際上是多余的。

l  為聲明和定義分別提供頭文件。 為了進(jìn)一步貫徹上文中的思想,頭文件必須要一分為二:一個(gè)存放聲明,另一個(gè)存放定義。當然這些文件必須保持相互協(xié)調。如果某處的一個(gè)聲明被修改了,那么相應的定義處就必須做出相應的修改。于是,庫的客戶(hù)端程序員就應該始終使用 #include 指令 來(lái)包含一個(gè)聲明頭文件,而不是自己進(jìn)行前置聲明,類(lèi)創(chuàng )建者應提供兩個(gè)頭文件。比如說(shuō) ,在 Date 客戶(hù)端程序員需要聲明 today clearAppointments 時(shí),就應該無(wú)需向上文中那樣, Date 進(jìn) 行前置聲明。更好的方案是用 #include 指令來(lái)引入恰當的聲明頭文件:

#include "datefwd.h"        // 包含 Date 類(lèi)聲明 ( 而不是定義 ) 的頭文件

 

Date today();              // 同上

void clearAppointments(Date d);

頭文件“ datefwd.h ”中僅包含聲明,這一名字來(lái)源于 C++ 標準庫中的 <iosfwd> (參見(jiàn)第 54 條)。 <iosfwd> 包含著(zhù) IO 流組件的聲明,這些 IO 流組件相應的定義分別存放在不同的幾個(gè)頭文件中,包括: <sstream> 、 <streambuf> 、 <fstream> 以及 <iostream> 。

從另一個(gè)角度來(lái)講,使用 <iosfwd> 作示例也是頗有裨益的,因為它告訴我們本節中的建議不僅對非模板的類(lèi)有效,而且對模板同樣適用。盡管在第 30 條中分析過(guò),在許多構建環(huán)境中,模板定義通常保存在頭文件中,一些構建環(huán)境中還是允許將模板定義放置在非頭文件的代碼文件里,因此提供為模板提供僅包含聲明的頭文件并不是沒(méi)有意義的。 <iosfwd> 就是這樣一個(gè)頭文件。

C++ 提供了 export 關(guān)鍵字,它用于分離模板聲明和模板定義。但是遺憾的是,編譯器對 export 的支持是十分有限的,實(shí)際操作中 export 更似雞肋。因此在高效 C++ 編程中, export 究竟扮演什么角色,討論這個(gè)問(wèn)題還為時(shí)尚早。

諸如 Person 此類(lèi)使用 pimpl idiom 的類(lèi)通常稱(chēng)為句柄類(lèi)。為了避免你對這樣的類(lèi)如何完成這些工作產(chǎn)生疑問(wèn),一個(gè)途徑就是將類(lèi)中所有的函數調用放在相關(guān)的具體實(shí)現類(lèi)之前,并且讓這些具體實(shí)現類(lèi)去做真實(shí)的工作。請看下面的示例,其中演示了 Person 的成員函數應該如何實(shí)現:

#include "Person.h"        // 我們將編寫(xiě) Person 類(lèi)的具體實(shí)現,

                           // 因此此處必須包含類(lèi)定義。

 

#include "PersonImpl.h"    // 同時(shí),此處必須包含 PersonImpl 的類(lèi)定義,

                           // 否則我們將不能調用它的成員函數;請注意,

                           // PersonImpl 擁有與 Person 完全一致的成員

                           // 函數 - 也就是說(shuō),它們的接口是一致的。

 

Person::Person(const std::string& name, const Date& birthday,

               const Address& addr)

: pImpl(new PersonImpl(name, birthday, addr))

{}

 

std::string Person::name() const

{

  return pImpl->name();

}

請注 意下面兩個(gè)問(wèn)題: Person 的構造函數是如何調用 PersonImpl 的構造函 數的(通過(guò)使 new - 參見(jiàn)第 16 條),以及 Person::name 是如何調用 PersonImpl :: name 的。這兩點(diǎn)很重要。將 Person 定制為一個(gè)句柄類(lèi)并不會(huì )改變它所做的事情,這樣做僅僅改變它做事情的方式。

除了句柄類(lèi)的方法,我們還可以采用一種稱(chēng)為“接口類(lèi)”的方法來(lái)講 Person 定制為特種的抽象基類(lèi)。這種類(lèi)的目的就是為派生類(lèi)指定一個(gè)接口(參見(jiàn)第 34 條)。于是,通常情況下它沒(méi)有數據成員,沒(méi)有構造函數,但是擁有一個(gè)虛析構函數(參見(jiàn)第 7 條),以及一組指定接口用的純虛函數。

接口類(lèi)與 Java .NET 中的接口一脈相承,但是 C++ 并沒(méi)有像 Java .NET 中那樣對接口做出非常嚴格的限定。比如說(shuō),無(wú)論是 Java 還是 .NET 都不允許接口中出現數據成員或者函數實(shí)現,但是 C++ 對這些都沒(méi)有做出限定。 C++ 所擁有的更強的機動(dòng)靈活性是非常有用的。就像第 36 條中所解釋的那樣,由于非虛函數的具體實(shí)現對于同一層次中所有的類(lèi)都應該保持一致,因此不妨將這些函數實(shí)現放置在聲明它們的接口類(lèi)中,這樣做是有意義的,

Person 的接口類(lèi)可以是這樣的:

class Person {

public:

  virtual ~Person();

 

  virtual std::string name() const = 0;

  virtual std::string birthDate() const = 0;

  virtual std::string address() const = 0;

  ...

};

這個(gè)類(lèi)的客戶(hù)端程序員必須要基于 Person 的指針和引用來(lái)編寫(xiě)程序,因為實(shí)例化一個(gè)包含純虛函數的類(lèi)是不可能的。(然而,實(shí)例化一個(gè)繼承自 Person 的類(lèi)卻是可行的—參見(jiàn)下文。)就像句柄類(lèi)的客戶(hù)端程序員一樣,接口類(lèi)客戶(hù)端程序員除非遇到接口類(lèi)的接口有改動(dòng)的情況,其他任何情況都不需要對代碼進(jìn)行重新編譯。

接口類(lèi)的客戶(hù)端程序員必須有一個(gè)創(chuàng )建新對象的手段。通常情況下,它們可以通過(guò)調用真正被實(shí)例化的派生類(lèi)中的一個(gè)函數來(lái)實(shí)現,這個(gè)函數扮演的角色就是派生類(lèi)的構造函數。這樣的函數通常被稱(chēng)作工廠(chǎng)函數(參見(jiàn)第 13 條)或者虛構造函數。這種函數返回一個(gè)指向動(dòng)態(tài)分配對象的指針(最好是智能指針—參見(jiàn)第 18 條),這些動(dòng)態(tài)分配的對象支持接口類(lèi)的接口。這樣的函數通常位于接口類(lèi)中,并且聲明為 static 的:

class Person {

public:

 ...

 

  static std::tr1::shared_ptr<Person>// 返回一個(gè) tr1::shared_ptr ,

   create(const std::string& name,   // 它指向一個(gè) Person 對象,這個(gè)

          const Date& birthday,       // Person 對象由給定的參數初始化,

          const Address& addr);       // 為什么返回智能指針參見(jiàn)第 18

 ...

};

客戶(hù)端程序員這樣使用:

std::string name;

Date dateOfBirth;

Address address;

...

 

// 創(chuàng )建一個(gè)支持 Person 接口的對象

std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));

 

...

 

std::cout << pp->name()         // 通過(guò) Person 的接口使用這一對象

          << " was born on "

           << pp->birthDate()

          << " and now lives at "

          << pp->address();

...                              // 當程序執行到 pp 的作用域之外時(shí),

                                // 這一對象將被自動(dòng)刪除—參見(jiàn)第 13

當然,與此同時(shí),必須要對支持接口類(lèi)的接口的具體類(lèi)進(jìn)行定義,并且必須有真實(shí)的構造函數得到調用。比如說(shuō),接口類(lèi) Person 必須有一個(gè)具體的派生類(lèi) RealPerson ,它應當為其繼承而來(lái)的虛函數提供具體實(shí)現:

class RealPerson: public Person {

public:

  RealPerson(const std::string& name, const Date& birthday,

             const Address& addr)

  : theName(name), theBirthDate(birthday), theAddress(addr)

  {}

 

  virtual ~RealPerson() {}

 

  std::string name() const;      // 這里省略了這些函數的具體實(shí)現,

  std::string birthDate() const;// 但是很容易想象它們是什么樣子。

  std::string address() const;

 

private:

  std::string theName;

  Date theBirthDate;

  Address theAddress;

};

RealPerson ,編寫(xiě) Person::create 就如 探囊取物一般:

std::tr1::shared_ptr<Person> Person::create(const std::string& name,

                                            const Date& birthday,

                                            const Address& addr)

{

  return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday,addr));

}

Person::create 還有可以以一個(gè)更加貼近現實(shí)的方法來(lái)實(shí)現,它應能夠創(chuàng )建不同種類(lèi)的派生類(lèi)對象,創(chuàng )建的過(guò)程基于某些相關(guān)信息,例如:新加入的函數的參數值、從一個(gè)文件或數據庫中得到讀到的數值,環(huán)境變量,等等。

RealPerson 向我們展示了實(shí)現接口類(lèi)的兩種通用的實(shí)現機制之一:它的接口規范繼 承自接口 類(lèi)( Person ,然后實(shí)現接口中的函數。第二種實(shí)現接口類(lèi)的方法牽扯到多重繼承,那是第 40 條中探索的主題。

句柄類(lèi)和接口類(lèi)將接口從實(shí)現中分離開(kāi)來(lái),因此降低了文件間的編譯依賴(lài)。如果你是一個(gè)喜歡吹毛求疵的人,那么你一定又在想法挖苦本屆的思想了:“做了這么多變魔術(shù)般古怪的事情,我又能得到什么呢?”這個(gè)問(wèn)題的答案就是計算機科學(xué)中極為普遍的一個(gè)議題:你的程序在運行時(shí)更慢了一步,另外,每個(gè)對象所占的空間更大了一點(diǎn)。

使用句柄類(lèi)的情況下,成員函數必須通過(guò)實(shí)現指針來(lái)取得對象的數據。這樣無(wú)形中增加了每次訪(fǎng)問(wèn)時(shí)迂回的層數。同時(shí),實(shí)現指針所指向的對象所占的空間更大了一些,你必須要考慮這一問(wèn)題。最后,你必須要對實(shí)現指針進(jìn)行初始化(在句柄類(lèi)的構造函數中),以便于將其指向一個(gè)動(dòng)態(tài)分配的實(shí)現對象,于是你就必須自己承擔動(dòng)態(tài)內存分配(以及相關(guān)的釋放)內在的開(kāi)銷(xiāo)以及遭遇 bad_alloc (內存越界)異常的可能性。

由于對于接口類(lèi)來(lái)說(shuō)每次函數調用都是虛擬的,因此你在每調用一次函數的過(guò)程中你就會(huì )為其付出一次間接跳轉 的代價(jià)(參見(jiàn)第 7 條)。同時(shí),派生自接口類(lèi)的對象必須包含一個(gè)虛 函數表指針(依然參見(jiàn)第 7 條)。這一指針也可能會(huì )使保存一個(gè)對象所需要的空間加大,這取決于接口類(lèi)是否是該對象中虛函數的唯一來(lái)源。

最后,無(wú)論是句柄類(lèi)還是接口類(lèi),都不適合于過(guò)多使用內聯(lián)。句柄和接口類(lèi)都是特別設計用來(lái)隱藏諸如函數體等具體實(shí)現內容的。

然而,僅僅由于句柄類(lèi)和接口類(lèi)會(huì )帶來(lái)一些額外的開(kāi)銷(xiāo)而遠離它們,這樣的做法存在致命的錯誤。虛函數也一樣,你并不希望忽略這些問(wèn)題,是嗎?(如果你真希望忽略些問(wèn)題,那么你可能看錯書(shū)了。)你應該把使用這些技術(shù)看作一個(gè)革命性的手段。在開(kāi)發(fā)過(guò)層中,使用句柄類(lèi)和接口類(lèi),來(lái)減少在具體實(shí)現有改動(dòng)時(shí)為客戶(hù)端程序員帶來(lái)的影響。在程序的速度和 / 或大小的變動(dòng)太大,足以體現出類(lèi)之間所增加的耦合度時(shí),還是可以適時(shí)使用具體的類(lèi)來(lái)取代句柄類(lèi)和接口類(lèi)。

銘記在心

l  最小化編譯依賴(lài)的基本理念就是使用聲明依賴(lài)代替定義依賴(lài)?;谶@一理念有兩種實(shí)現方式,它們是:句柄類(lèi)和接口類(lèi)。

l  庫頭文件必須以完整、并且僅存在聲明的形式出現。無(wú)論是否涉及模板。

 

本站僅提供存儲服務(wù),所有內容均由用戶(hù)發(fā)布,如發(fā)現有害或侵權內容,請點(diǎn)擊舉報。
打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
我對COM的理解
Huihoo Power! - 對比C 和Java
如何對webbrowser和IE編程(六)
靜態(tài)變量和靜態(tài)函數
翻譯《real-time 3d terrain engines using C++ and DX9 》5
國慶MFC學(xué)習 (上)認識Windows的編程機制
更多類(lèi)似文章 >>
生活服務(wù)
分享 收藏 導長(cháng)圖 關(guān)注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服

欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久