• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            Shuffy

            不斷的學習,不斷的思考,才能不斷的進步.Let's do better together!
            posts - 102, comments - 43, trackbacks - 0, articles - 19
            本文原址:http://m.shnenglu.com/tiandejian/archive/2007/04/11/ECPP_03.html

             

            第3項: 盡可能使用 const

            const令人贊嘆之處就是:你可以通過它來指定一個語義上的約束(一個特定的不能夠更改的對象)這一約束由編譯器來保證。通過一個const,你可以告訴編譯器和其他程序員,你的程序中有一個數值需要保持恒定不變。不管何時,當你需要這樣一個數時,你都應該這樣做,這樣你便可以讓編譯器來協助你確保這一約束不被破壞。

            const 關鍵字的用途十分廣泛。在類的外部,你可以定義全局的或者名字空間域的常量,也可以通過添加 static 關鍵字來定義文件、函數、或者程序塊域的對象。在類的內部,你可以使用它來定義靜態的或者非靜態的數據成員。對于指針,你可以制定一個指針是否是 const 的,其所指的數據是否是 const 的,或者兩者都是 const ,或者兩者都不是。

            char greeting[] = "Hello";

            char *p = greeting;                    // const 指針,非 const 數據

            const char *p = greeting;              // const 指針, const 數據

            char * const p = greeting;             // const 指針,非 const 數據

            const char * const p = greeting;       // const 指針, const 數據

            這樣的語法看上去反復無常,實際上并不是這樣。如果 const 關鍵字出現在星號的左邊,那么指針所指向的就是一個常量;如果 const 出現在星號的右邊,那么指針本身就是一個常量;如果 const 同時出現在星號的兩邊,那么兩者就都是常量。

            當所指向的為常量時,一些程序員喜歡把 const 放在類型之前;其他一些人則喜歡放在類型后邊,但要在星號的前邊。這兩種做法沒有什么本質的區別,所以下邊給出的兩個函數聲明的參數表實際上是相同的:

            void f1(const Widget *pw); // f1 傳入一個指向 Widget 對象常量的指針

            void f2(Widget const *pw); // f2 也一樣

            由于這兩種形式在實際代碼中都會遇到,所以你都要適應。

            STL 迭代器是依照指針模型創建的, 所以說一個 iterator 更加像一個指向 T* 的指針。把一個 iterator 聲明為 const 的更像是聲明一個 const 的指針(也就是聲明一個指向 T* const 的指針): iterator 不允許指向不同類型的內容,但是其所指向的內容可以被修改。如果你希望一個迭代器指向某些不能被修改的內容(也就是指向 const T* 的指針),此時你需要一個 const_iterator

            std::vector<int> vec;

            ...

            const std::vector<int>::iterator iter = vec.begin();

                                              // iter 就像一個 T* const

            *iter = 10;                       // 正確,可以改變 iter 所指向的內容

            ++iter;                           // 出錯! Iter 是一個 const

             

            std::vector<int>::const_iterator cIter = vec.begin();

                                              // cIter 就像一個 const T*

            *cIter = 10;                      // 出錯! *cIter 是一個 const

            ++cIter;                          // 正確,可以改變 cIter

            const 在函數聲明方面還有一些強大的用途。在一個函數聲明的內部, const 可以應用在返回值、單個參數,對于成員函數,可以將其本身聲明為 const 的。

            讓函數返回一個常量通常可以減少意外發生的可能,而且不用放棄考慮安全和效率問題。好比有理數乘法函數( operator* )的聲明,更多信息請參見第 24 項。

            class Rational { ... };

            const Rational operator*(const Rational& lhs, const Rational& rhs);

            很多程序員在初次到這樣的代碼時都不會正眼看一下。為什么 operator* 要返回一個 c onst 對象呢?這是因為如果不是這樣,客戶端將會遇到一些不愉快的狀況,比如:

            Rational a, b, c;

            ...

            (a * b) = c;                  // 調用 operator= 能返回一個 a*b

            我不知道為什么一些程序員會企圖為兩個數 的乘積賦值,但是我確實知道好多程序員的初衷并非如此。他們也許僅僅在錄入的時候出了個小差錯(他們的本意也許是一個布爾型的表達式):

            if (a * b = c) ...            // 本來是想進行一次比較!

            顯而易見,如果 a b 是內建數據類型,那么這樣的代碼就是非法的。避免與內建數據類型不必要的沖突,這是一個優秀的用戶自定義類型的設計標準之一(另請參見第 18 項),而允許為兩數乘積賦值這讓人看上去就很不必要。聲明 operator* 函數時如果讓其返回一個 const 型數據則可以避免這一沖突,這便是要這樣做的原因所在。

            const 的參數沒有什么特別新鮮的——它們與局部 const 對象的行為基本一致,你在必要的時候要盡可能使用它們。除非你需要更改某個參數或者局部對象,其余的所有情況最好都聲明為 const 。這僅僅需要你多打六個字母,但是它可以使你從惱人的錯誤(比如我們剛才見到的“我本想打‘ == ’但是卻打了‘ = ’”)中解放出來。

            const 成員函數

            對成員函數使用 const 的目的是指明這些成員函數可以被 const 對象調用。這一類成員函數是很重要的,首先,它們使得類的接口更加易于理解。很有必要了解哪些函數可以修改而哪些不可以。其次,這些函數可以與 const 對象協同工作。這對于高效編碼是十分重要的一方面,這是由于(將在第 20 項中展開解釋)提高 C++ 程序性能的一條最基本的途徑就是:傳遞對象的 const 引用。使用這一技 術需要一個前提:這就是首先要有 const 成員函數存在,并且它們用于處理之前生成的 const 對象。

            如果若干成員函數之間的區別僅僅為“是否是 const 的”,那么它們也可以被重載。很多人都忽略了這一點,但是這是 C++ 重要特征之一。請觀察下面的代碼,這是一個文字塊的類:

            class TextBlock {

            public:

             ...

             const char& operator[](std::size_t position) const

                                            // operator[] 用于返回相應位置的字符

             { return text[position]; }     // 返回一個 const 對象

             

             char& operator[](std::size_t position)

                                            // operator[] 用于返回相應位置的字符

             { return text[position]; }     // 返回一個非 const 對象

            private:

               std::string text;

            };

            TextBlock operator[] 可以這樣使用:

            TextBlock tb("Hello");

            std::cout << tb[0];        // 調用非 const TextBlock::operator[]

             

            const TextBlock ctb("World");

            std::cout << ctb[0];       // 調用 const TextBlock::operator[]

            順便說一下,在真實的程序中, const 對象在大多數情況下都以“通過指針傳遞”或“引用一個 const ”的形式出現。 上面的 ctb 的例子純粹是人為的,而下面的例子在真實狀況中常會出現:

            void print(const TextBlock& ctb)       // 在這個函數中 ctb const

            {

             std::cout << ctb[0];        // 調用 const TextBlock::operator[]

             ...

            }

            通過對 operator[] 的重載以及為每個版本提供不同類型的返回值,你便可以以不同的方式處理 const 的或者非 const TextBlock

            std::cout << tb[0];           // 正確:讀入一個非 const TextBlock

            tb [0] = 'x';                  // 正確:改寫一個非 const TextBlock

            std::cout << ctb[0];          // 正確:讀入一個 const TextBlock

            ctb [0] = 'x';                 // 錯誤 ! 不能改寫 const TextBlock

            請注意,這一錯誤只與所調用的 operator[] 返回值的類型有關,如果僅僅調用 operator[] 本身則不會出現任何問題。錯誤出現在:企圖為一個 const char& 賦值,而 const char& 則是 operator[] const 版本的返回值類型。

            同時還要注意的是,非 const operator[] 的返回值類型是一個 char 的引用,而不是 char 本身。如果 operator[] 真的簡單的返回一個 char ,那么下面的語句將不能正確編譯:

            tb[0] = 'x';

            這是因為,企圖修改一個返回內建數據類型的函數的返回值根本都是非法的。即使假設這樣做合法,而 C++ 通過傳值返回對象的,所修改的僅僅是由 tb.text[0] 復制出的一份副本,而不是 tb.text[0] 本身,你不會得到預期的效果。

            讓我們暫停一小會兒,來考慮一下這里邊的哲學問題。把一個成員函數聲明為 const 的有什么涵義呢?這里有兩個流行的說法:按位恒定(也可叫做物理恒定)和邏輯恒定。

            按位 恒定陣營堅信:當且僅當一個成員函數對于所有對象的數據成員( static 數據成員除外)都不做出改動時,才需要將這一成員函數聲明為 const 的,換句話說,將成員函數聲明為 const 的條件是:成員函數不對對象內部做任何的改動。按位恒定的好處之一就是,它使得錯誤檢查便得更輕松:編譯器僅需要查找對數據成員的賦值。實際上,按位恒定就是 C++ 對于恒定的定義,如果一個 const 的成員函數調用了某個對象,那么即使該對象擁有非靜態數據成員,其所有數據成員也都是不可修改的。

            不幸的是,大多數不完全是 const 的成員函數也可以通過按位恒定的檢驗。在特定的情況下,如果一個成員函數頻繁的修改一個指針所指的位置,那么我們說它就不是一個 const 的成員函數。但是只要這個指針存在于一個對象中,這個函數就是按位恒定的,這時候編譯器不會報錯。這樣會導致編成的行為不符合常規習慣。比如說,我們手頭有一個類似于 TextBlock 的類,其中保存著 char* 類型的數據而不是 string ,因為這段代碼有可能要與一些 C 語言的 API 交互,但是 C 語言中沒有 string 對象一說。

            class CTextBlock {

            public:

             ...

             char& operator[](std::size_t position) const

             // operator[] 不恰當的 (但是符合按位恒定規則)定義方法

             { return pText[position]; }

            private:

             char *pText;

            };

            盡管 operator[] 返回一個對對象內部數據的引用,這個類仍(不恰當地)將其聲明為 const 成員函數(第 28 項將深入討論這個問題)。先忽略這個問題,請注意 operator[] 的實現中并沒有以任何形式修改 pText 。于是編譯器便會欣然接受這樣的做法,畢竟,所有的編譯器所檢查的是“代碼是否符合按位恒定規則”。但是請觀察,在編譯器的縱容下,還會有什么樣的事情發生:

            const CTextBlock cctb("Hello");// 聲明對象常量

             

            char *pc = &cctb[0];            // 調用 const operator[]

                                            // 從而得到一個指向 cctb 中數據的指針

             

            *pc = 'J';                      // cctb 現在的值為 "Jello"

            當你創建了一個包含具體值的對象常量后,你僅僅通過對其調用 const 的成員函數,就可以改變它的值!這顯然是有問題的。

             

            輯恒定應運而生。堅持這一宗旨的人們爭論道,一個 const 的成員函數可能對其調用的對象內部做出改動,但是僅僅以客戶端無法察覺的方式進行。比如說,你的 CTextBlock 類可能需要保存文字塊的長度,以便在需要的時候調用:

            class CTextBlock {

            public:

             ...

             std::size_t length() const;

             

            private:

             char *pText;

             std::size_t textLength;      // 最后一次計算出的文字塊長度

             bool lengthIsValid;          // 當前長度是否可用

            };

             

            std::size_t CTextBlock::length() const

            {

             if (!lengthIsValid) {

            textLength = std::strlen(pText);    // 錯誤!不能在 const 成員函數中

            lengthIsValid = true;          // textLength lengthIsValid 賦值

             }

             return textLength;

            }

            以上 length 的實現絕不是按位恒定的。這是因為 textLength lengthIsValid 都可以改動。盡管看上去它應該對于 CTextBlock 對象常量可用,但是編譯器不答應。編譯器始終堅持遵守按位恒定。那么該怎么辦呢?

            解決方法很簡單:利用 C++ const 相關的靈活性,使用可變的( mutable )數據成員。 mutable 可以使非靜態數據成員不受按位恒定規則的約束:

            class CTextBlock {

            public:

             ...

             std::size_t length() const;

             

            private:

             char *pText;

             mutable std::size_t textLength;// 這些數據成員在任何情況下均可修改

             mutable bool lengthIsValid;     // const 成員函數中也可以

            };

             

            std::size_t CTextBlock::length() const

            {

             if (!lengthIsValid) {

                textLength = std::strlen(pText);    // 現在可以修改了

                lengthIsValid = true;               // 同上

             }

             return textLength;

            }

            避免 const 與非 const 成員函數之間的重復

            mutable 對于“我不了解按位恒定”的情況不失為一個良好的解決方案,但是它對于所有的 const 難題并不能做到一勞永逸。舉例說, TextBlock (以及 CTextBlock )中的 operator[] 不僅僅返回一個對恰當字符的引用,同時還要進行邊界檢查、記錄訪問信息,甚至還要進行數據完整性檢測。如果將所有這些統統放在 const 或非 const 函數(我們現在會得到過于冗長的隱式內聯函數,不過不要驚慌,在第 30 項中這個問題會得到解決)中,看看我們會得到什么樣的龐然大物:

            class TextBlock {

            public:

             ...

             const char& operator[](std::size_t position) const

             {

                ...                                 // 邊界檢查

                ...                                 // 記錄數據訪問信息

                ...                                 // 確認數據完整性

                return text[position];

             }

             char& operator[](std::size_t position)

             {

                ...                                 // 邊界檢查

                ...                                 // 記錄數據訪問信息

                ...                                 // 確認數據完整性

                return text[position];

             }

             

            private:

               std::string text;

            };

            噢!天哪,這讓人頭疼:重復代碼,以及隨之而來的編譯時間增長、維護成本增加、代碼膨脹、等等……當然,像邊界檢查這一類代碼是可以移走的,它們可以單獨放在一個成員函數(當然是私有的)中,然后讓這兩個版本的 operator[] 來調用它,但是你的代碼仍然有重復的函數調用,以及重復的 return 語句。

            對于 operator[] 你真正需要的是:一次實現,兩次使用。也就是說,你需要一個版本的 operator[] 來調用另一個。這樣便可以通過轉型來消去函數的恒定性。

            通常情況下轉型是一個壞主意,后邊我將專門用一項來告訴你為什么不要使用轉型(第 21 項),但是代碼重復也不會讓人感到有多輕松。在這種情況下, const 版的 operator[] 與非 const 版的 operator[] 所做的事情完全相同,不同的僅僅是它的返回值是 const 的。通過轉型來消去返回值的恒定性是安全的,這是因為任何人調用這一非 const operator[] 首先必須擁有一個非 const 的對象,否則它就不能調用非 const 函數。所以盡管需要一次轉型,在 const operator[] 中調用非 const 版本,可以安全地避免代碼重復。下面是實例代碼,讀完后邊的文字解說你會更明了。

            class TextBlock {

            public:

             ...

             const char& operator[](std::size_t position) const     // 同上

             {

                ...

                ...

                ...

                return text[position];

             }

             char& operator[](std::size_t position) // 現在僅調用 const op[]

             {

                return

                  const_cast<char&>(       // 通過對 op[] 的返回值進行轉型,消去 const

                    static_cast<const TextBlock&>(*this)// *this 的類型添加 const

                      [position];                      // 調用 const 版本的 op[]

                  );

             }

            ...

            };

            就像你所看到的,上面的代碼進行了兩次轉型,而不是一次。我們要讓非 const operator[] 去調用 const 版本的,但是如果在非 const operator[] 的內部,我們只調用 operator[] 而不標明 const ,那么函數將對自己進行遞歸調用。那將是成千上萬次的毫無意義的操作。為了避免無窮遞歸的出現,我們必須要指明我們要調用的是 const 版本的 operator[] ,但是手頭并沒有直接的辦法。我們可以用 *this TextBlock& 轉型到 const TextBlock& 來取代。是的,我們使用了一次轉型添加了一個 const !這樣我們就進行了兩次轉型:一次為 *this 添加了 const (于是對于 operator[] 的調用將會正確地選擇 const 版本),第二次轉型消去了 const operator[] 返回值中的 const 。

            添加 const 的那次轉型是為了保證轉換工作的安全性(從一個非 const 對象轉換為一個 const 的),這項工作的關鍵字是 static_cast 。消去 const 的工作只可以通過 const_cast 來完成,所以在這里我們實際上并沒有其他的選擇。(從技術上講,我們有。 C 言風格的轉型在這里也能工作,但是,就像我在第 27 項中所講的,這一類轉型在很多情況下都不是好的選擇。如果你對于 static_cast const_cast 還不熟悉,第 27 項中有詳細的介紹。)

            在眾多的示例中,我們最終選擇了一個運算符來進行演示,因此上面的語法顯得有些古怪。這些代碼可能不會贏得任何選美比賽,但是通過以 const 版本的形式實現非 const 版本的 operator[] ,可以避免代碼重復,這正是我們所期望的。為達到這一目標而寫下看似笨拙的代碼,這樣做是否值得全看你的選擇,但是,以 const 版本的形式來實現非 const 的成員函數——了解這一技術肯定是值得的。

            更值得你了解的是按反方向完成上面的工作——通過讓 const 版本的函數調用非 const 版本來避免代碼重復——一定不要這樣做。請記住,一個 const 成員函數保證其對象永遠不會更改其邏輯狀態,但是一個非 const 的成員函數并沒有這一類的保證。如果你在一個 const 函數中調用了一個非 const 函數,曾保證不會被改動的對象就有被修改的風險。這就是為什么說讓一個 const 函數調用一個非 const 函數是錯誤的:對象有可能被修改。實際上,為了使代碼能夠得到編譯,你還需要使用一個 const_cast 來消去 *this const 屬性,顯然這是不必要的麻煩。上一段中相反的調用次序才是安全的:非 const 成員函數可以對一個對象做任何想做的事情,因此調用一個 const 成員函數不會帶來任何風險。這就是為什么 static_cast 在沒有與 const 相關的危險的情況下可以正常工作的原因。

            就像本項最開始所說的, const 是一個令人贊嘆的東西。對于指針和迭代器,以及指針、迭代器和引用所涉及的對象,函數的參數和返回值,局部變量,成員函數來說, const 都是一個強大的伙伴。只要可能就可以使用它。你會對你所做的事情感到高興的。

            需要記住的

            將一些東西聲明為 const 的可以幫助編譯器及時發現用法上的錯誤。 const 針對對象作用于所有的作用域,針對函數參數和返回值、成員函數作用于整體。

            編譯器嚴格遵守按位恒定規則,但是你應該在需要時應用邏輯恒定。

            const 和非 const 成員函數的實現在本質上相同時,可以通過使用一個非 const 版本來調用 const 版本來避免代碼重復。

            精产国品久久一二三产区区别| 国产亚洲婷婷香蕉久久精品| 国产麻豆精品久久一二三| 久久国产精品一区| 久久精品国产一区二区三区| 久久国产一区二区| 99久久精品国产一区二区蜜芽 | 国产精品久久久久影视不卡| 亚洲精品乱码久久久久久自慰 | 亚洲国产精品无码久久一线| 国产亚洲精品久久久久秋霞| 一本久久a久久精品vr综合| 国产免费久久精品99re丫y| 亚洲国产高清精品线久久| 亚洲国产成人久久一区WWW| 久久本道综合久久伊人| 久久夜色撩人精品国产小说| 久久久久亚洲爆乳少妇无| 亚洲综合久久久| 久久久久久久久久久久中文字幕| 亚洲中文字幕无码一久久区| 久久久久人妻精品一区| 久久香蕉国产线看观看乱码| 久久精品国产亚洲一区二区三区| 性做久久久久久久久| 色欲av伊人久久大香线蕉影院 | 国产成人久久精品区一区二区| 国产产无码乱码精品久久鸭| 国产成人精品久久亚洲| 日日狠狠久久偷偷色综合免费| 久久久噜噜噜久久中文字幕色伊伊| 久久男人Av资源网站无码软件| 国产激情久久久久影院| 国产精品成人久久久| 99久久久精品| 久久久久亚洲AV无码观看| 国产亚洲婷婷香蕉久久精品| 亚洲一区精品伊人久久伊人| 国产精品岛国久久久久| 久久人人爽人人爽人人片AV东京热| 国产精品久久午夜夜伦鲁鲁|