天天看點

const與#define的優缺點

1.define由預處理程式處理,const由編譯程式處理

2.#define不分記憶體,因為它是預編譯指令,編譯前進行了宏替換。const 不一定?某種說法,Const常量是占有記憶體的被“當機”了的變量

3.const定義常量是有資料類型的,這樣const定義的常量編譯器可以對其進行資料靜态類型安全檢查,而#define宏定義的常量卻隻是進行簡單的字元替換,沒有類型安全檢查,且有時還會産生邊際效應

4.有些調試程式可對const進行調試,但不對#define進行調試。

5.const在編譯期間會計算其值,而define不會

6 當定義局部變量時,const作用域僅限于定義局部變量的函數體内。但用#define時其作用域不僅限于定義局部變量的函數體内,而是從定義點到整個程式的結束點。但也可以用#undef取消其定義進而限定其作用域範圍。隻用const定義常量,并不能起到其強大的作用。const還可修飾函數形式參數、傳回值和類的成員函數等。進而提高函數的健壯性。因為const修飾的東西能受到c/c++的靜态類型安全檢查機制的強制保護,防止意外的修改。

二、const與c++

      看了傳遞方式後我們繼續來談“const隻能用于修飾輸入參數”的情況。

         當輸入參數用“值傳遞”方式時,我們不需要加const修飾,因為用值傳遞時,函數将自動用實際參數的拷貝初始化形式參數,當在函數體内改變形式參數時,改變的也隻是棧上的拷貝而不是實際參數。

          但要注意的是,當輸入參數為ADT/UDT(使用者自定義類型和抽象資料類型)時,應該将“值傳遞”改為“const &傳遞”,目的可以提高效率。

          例如:

             voidfun(A a);//效率底。函數體内産生A類型的臨時對象用于複制參數 a,但是臨時對象的

                          //構造、複制、析構過程都将消耗時間。

             voidfun(A const &a);//提高效率。用“引用傳遞”不需要産生臨時對象,省了臨時對象的

                                 //構造、複制、析構過程消耗的時間。但光用引用有可能改變a,是以加const

          當輸入參數用“指針傳遞”方式時,加const修飾可防止意外修改指針指向的記憶體單元,起到保護作用。

          例如:

             voidfunstrcopy(char *strdest,const char *strsrc)//任何改變strsrc指向的記憶體單元,

                                                             //編譯器都将報錯

             些時保護了指針的記憶體單元,也可以保護指針本身,防止其位址改變。

          例如:

            voidfunstrcopy(char *strdest,const char *const strsrc)

(3)const修飾函數的傳回值

     如給“指針傳遞”的函數傳回值加const,則傳回值不能被直接修改,且該傳回值隻能被指派給加const修飾的同類型指針。

      例如:

         const char *GetChar(void){};

       指派 char *ch = GetChar();//錯誤     const char *ch = GetChar();//正确

(4)const修飾類的成員函數(函數定義體)

     任何不會修改資料成員的函數都應用const修飾,這樣當不小心修改了資料成員或調用了非const成員函數時,編譯器都會報錯。

      const修飾類的成員函數形式為:int GetCount(void)   const;

(5)用傳引用給const取代傳值

預設情況下,C++ 以傳值方式将對象傳入或傳出函數(這是一個從 C 繼承來的特性)。除非你特别指定其它方式,否則函數的參數就會以實際參數(actual argument)的拷貝進行初始化,而函數的調用者會收到函數傳回值的一個拷貝。這個拷貝由對象的拷貝構造函數生成。這就使得傳值(pass-by-value)成為一個代價不菲的操作。例如,考慮下面這個類層級結構:

class Person {

public:

Person(); // parametersomitted for simplicity

virtual ~Person(); // seeItem 7 for why this is virtual

...

private:

std::string name;

std::string address;

};

class Student: publicPerson {

public:

Student(); // parametersagain omitted

~Student();

...

private:

std::string schoolName;

std::stringschoolAddress;

};

  現在,考慮以下代碼,在此我們調用一個函數—— validateStudent,它得到一個 Student 參數(以傳值的方式),并傳回它是否驗證有效的結果:

boolvalidateStudent(Student s); // function taking a Student

// by value

Student plato; // Platostudied under Socrates

bool platoIsOK =validateStudent(plato); // call the function

  當這個函數被調用時會發生什麼呢?

  很明顯,Student 的拷貝構造函數被調用,用 plato 來初始化參數 s。同樣明顯的是,當 validateStudent 傳回時,s 就會被銷毀。是以這個函數的參數傳遞代價是一次 Student 的拷貝構造函數的調用和一次 Student 的析構函數的調用。

  但這還不是全部。一個 Student 對象内部包含兩個 string 對象,是以每次你構造一個 Student 對象的時候,你也必須構造兩個 string 對象。一個 Student 對象還要從一個 Person 對象繼承,是以每次你構造一個 Student 對象的時候,你也必須構造一個 Person 對象。一個 Person 對象内部又包含兩個額外的 string 對象,是以每個 Person 的構造也承擔着另外兩個 string 的構造。最終,以傳值方式傳遞一個 Student 對象的後果就是引起一次 Student 的拷貝構造函數的調用,一次 Person 的拷貝構造函數的調用,以及四次 string 的拷貝構造函數調用。當 Student 對象的拷貝被銷毀時,每一個構造函數的調用都對應一個析構函數的調用,是以以傳值方式傳遞一個 Student 的全部代價是六個構造函數和六個析構函數!

  好了,這是正确的和值得的行為。畢竟,你希望你的全部對象都得到可靠的初始化和銷毀。盡管如此,如果有一種辦法可以繞過所有這些構造和析構過程,應該變得更好,這就是:傳引用給 const(pass by reference-to-const):

boolvalidateStudent(const Student& s);

  這樣做非常有效:沒有任何構造函數和析構函數被調用,因為沒有新的對象被構造。被修改的參數聲明中的 const 是非常重要的。 validateStudent 的最初版本接受一個 Student 值參數,是以調用者知道它們屏蔽了函數對它們傳入的 Student 的任何可能的改變;validateStudent 也隻能改變它的一個拷貝。現在 Student 以引用方式傳遞,同時将它聲明為 const 是必要的,否則調用者必然擔心 validateStudent 改變了它們傳入的 Student。

  以傳引用方式傳遞參數還可以避免切斷問題(slicing problem)。當一個派生類對象作為一個基類對象被傳遞(傳值方式),基類的拷貝構造函數被調用,而那些使得對象的行為像一個派生類對象的特殊特性被“切斷”了。你隻剩下一個純粹的基類對象——這沒什麼可吃驚的,因為是一個基類的構造函數建立了它。這幾乎絕不是你希望的。例如,假設你在一組實作一個圖形視窗系統的類上工作:

class Window {

public:

...

std::string name() const;// return name of window

virtual void display()const; // draw window and contents

};

class WindowWithScrollBars:public Window {

public:

...

virtual void display()const;

};

  所有 Window 對象都有一個名字,你能通過 name 函數得到它,而且所有的視窗都可以顯示,你可一個通過調用 display 函數來做到這一點。display 為 virtual 的事實清楚地告訴你:一個純粹的基類的 Window 對象的顯示方法有可能不同于專門的 WindowWithScrollBars 對象的顯示方法。

  現在,假設你想寫一個函數列印出一個視窗的名字,并随後顯示這個視窗。以下這個函數的寫法是錯誤的:

voidprintNameAndDisplay(Window w) // incorrect! parameter

// may be sliced!

std::cout <<w.name();

w.display();

}

  考慮當你用一個 WindowWithScrollBars 對象調用這個函數時會發生什麼:

WindowWithScrollBarswwsb;

printNameAndDisplay(wwsb);

  參數 w 将被作為一個 Window 對象構造——它是被傳值的,記得嗎?而且使 wwsb 表現得像一個WindowWithScrollBars 對象的特殊資訊都被切斷了。在 printNameAndDisplay 中,全然不顧傳遞給函數的那個對象的類型,w 将始終表現得像一個 Window 類的對象(因為它就是一個 Window 類的對象)。特别是,在 printNameAndDisplay 中調用 display 将總是調用 Window::display,絕不會是 WindowWithScrollBars::display。

  繞過切斷問題的方法就是以傳引用給 const 的方式傳遞 w:

voidprintNameAndDisplay(const Window& w) // fine, parameter won’t

{

// be sliced

std::cout <<w.name();

w.display();

}

  現在 w 将表現得像實際傳入的那種視窗。

  如果你掀開編譯器的蓋頭偷看一下,你會發現用指針實作引用是非常典型的做法,是以以引用傳遞某物實際上通常意味着傳遞一個指針。由此可以得出結論,如果你有一個内建類型的對象(例如,一個 int),以傳值方式傳遞它常常比傳引用方式更高效。那麼,對于内建類型,當你需要在傳值和傳引用給 const 之間做一個選擇時,沒有道理不選擇傳值。同樣的建議也适用于 STL 中的疊代器(iterators)和函數對象(function objects),因為,作為慣例,它們就是為傳值設計的。疊代器(iterators)和函數對象(function objects)的實作有責任保證拷貝的高效并且不受切斷問題的影響。(這是一個“規則如何變化,依賴于你使用 C++ 的哪一個部分”的執行個體。)

  内建類型很小,是以有人就斷定所有的小類型都是傳值的上等候選者,即使它們是使用者定義的。這樣的推論是不可靠的。僅僅因為一個對象小,并不意味着調用它的拷貝構造函數就是廉價的。很多對象——大多數 STL 容器也在其中——容納的和指針一樣,但是拷貝這樣的對象必須同時拷貝它們指向的每一樣東西。那可能是非常昂貴的。

  即使當一個小對象有一個廉價的拷貝構造函數,也會存在性能問題。一些編譯器對内建類型和使用者定義類型并不一視同仁,即使他們有同樣的底層表示。例如,一些編譯器拒絕将僅由一個 double 組成的對象放入一個寄存器中,即使在正常上它們非常願意将一個純粹的 double 放入那裡。如果發生了這種事情,你以傳引用方式傳遞這樣的對象更好一些,因為編譯器理所當然會将一個指針(引用的實作)放入寄存器。

  小的使用者定義類型不一定是傳值的上等候選者的另一個原因是:作為使用者定義類型,它的大小常常變化。一個現在較小的類型在将來版本中可能變得更大,因為它的内部實作可能會變化。甚至當你換了一個不同的 C++ 實作時,事情都可能會變化。例如,就在我這樣寫的時候,一些标準庫的 string 類型的實作的大小就是另外一些實作的七倍。

  通常情況下,你能合理地假設傳值廉價的類型僅有内建類型及 STL 中的疊代器和函數對象類型。對其他任何類型,請遵循本 Item 的建議,并用傳引用給 const 取代傳值。

Things to Remember

·用傳引用給 const 取代傳值。典型情況下它更高效而且可以避免切斷問題。

·這條規則并不适用于内建類型及 STL 中的疊代器和函數對象類型。對于它們,傳值通常更合适。

原文來源:http://dev.firnow.com/course/3_program/c++/cppjs/20091014/178760.html

繼續閱讀