const, static, inline, #define的用法以及關係

在我們寫代碼的過程中,添加合適恰當的相應修飾符去告訴編譯器該怎麼做,限制編譯器在我們背後偷偷的做一些出乎我們意料的行爲,這樣方能提高程序的魯棒性。作爲程序員,我們是主宰,應該由我們來明確告訴編譯器,我們希望它做什麼,不希望它做什麼,而不是等發生錯誤的時候才花大量的時間去debug。合理應用各種修飾符,就像我們爲程序開鑿了一條河牀,讓它按照我們的意願流淌,何樂而不爲?


語言有很多修飾符,我們在這裏只分析const, static,enum, inline,#define的用法以及他們的關係。

const的用法可以概括爲下面幾個方向:

1. 修飾指針,迭代器。

char str[] = "Hello World";
const char *pStr =  str;  //常量指針,指針指向的對象是一個常量,無法通過指針修改對象的值,
                          //但是可以改變指針的指向
*pStr = 'h';  //error
pStr++;   //right

char* const pStr = str;  //指針常量, 常量的內容爲一個指針,我們無法改變指針本身的值,
                          //但是可以改變指針所指向對象的值
*pStr = 'h';  //right
pStr++;       //error

const char* const pStr = str;  //指針本身已經指針所指向的值均不能改變, 
                               //可以理解爲完全的常量

//迭代器
vector<int>::const_iterator  IteVecInt;  //常量迭代器, 同義與常量指針
const vector<int>::iterator  IteVecInt; //迭代器常量, 同義與指針常量

2. const 修飾local 變量

 const int allocNum = 1000;
 int  array[allocNum];

3. const 修飾函數參數以及返回值

這應該是用得最多的用法。修飾函數參數,可以告訴編譯器該參數在函數作用域內不會改變, 請您放心做優化, 編譯器還可以根據這個發現一些書寫錯誤, 如下:

 void  functionA(const int &a) {
     int b;
     b = 10;

     if(a = b) { //應該爲 a == b, 書寫錯誤
     ...
     }
    ...
}    

只要我們認爲這些參數在函數內不會發生改變, 特別是指針, 迭代器,引用或者數組, 我們應該斟酌着加上合適的const修飾符, 可以避免上述類似錯誤,因爲編譯器會發出錯誤警告。 否則我們只能進入徹夜的調試大會。

const修飾函數的返回值, 我們的初衷是不希望函數的返回值被賦值, 返回值爲只讀, 不可寫。

 const int&  functionB(const int &a) {
     int b;
     b = 10;

     if(a = b) { //應該爲 a == b, 書寫錯誤
     ...
     }
    ...
  return a;
}   

int num = 10;
int numBack = functionB(num);  //right
functionB(num) = 5;  //error;

4. const 修飾類成員函數

在C++中,const 修飾類成員函數非常有用,通過const修飾成員函數,一方面提高函數接口可能性,讀者閱讀代碼可以很清楚的知道在這個函數內部不會改變成員變量,易於明白作者的意圖;另一方面可以操作相關的const對象;還有一點,我們經常會忽略的是,兩個成員函數如果只是常量性不同,可以被重載。

class book {
public:
....
const char& operator[](std::size_t position) const
 { return bookText[position];}

char&  operator[](std::size_t position)
 { return bookText[position]; }
...
private:
    std::string bookText;
}

上述代碼有兩個需要注意的地方, 一是const修飾的成員函數返回值應該採用const, const修飾的成員函數我們要做到不僅在函數內部成員變量不被改變,與此同時也不能在返回值留下被修改的漏洞。
二是const屬性不同的成員函數重載,如下:

book bookA("C++ primer");
std::cout<<bookA[0];  //調用char&  operator[](std::size_t position)
bookA[0] = 'B';   // "B++ primer"   is right

book bookB("C++ primer");
std::cout<<bookB[0];  //調用const char&  operator[] (std::size_t position) const
bookA[0] = 'B';   // error, 返回值爲const,不能修改

static的用法:

static在c語言中的用法有兩方面:

1. 修飾函數以及全局變量, 使被修飾的對象的可見性僅限於被定義的文件中

// calNum.cpp
static int num = 10;
static int calNum(int );  
//可見性均只限於calNum.cpp, 在其他文件中不可見

上述static的用法在C代碼中比較常見,但是在C++代碼中,我們可以用更加有效的方法替代, 那就是無名namesapce, 既然namespace沒有名字, 你在其他文件當然是無法訪問的,也就是說無名namespace的作用域只能在定義文件內。代碼如下:

namespace {
    int number = 10;

    int calNum(int num) { return num+1;}
};

int main()
{
    number = 11;  

    int newNum = calNum(number);

    return 0;
}

2.修飾local變量

static修飾local變量,使得local變量的生命週期上升到跟全局變量一樣,但是不會改變local變量的作用域。 static修飾的local變量會被存儲在全局變量/靜態變量區內, 而不是在棧中。

int calTimes(void )
{
    static int times = 0;
    return  ++times;
}

int num1 = calTImes();  // 1
int num2 = calTimes();  // 2

3. 修飾類成員變量以及成員函數

在c++中,static新增的用法是修飾成員變量以及成員函數。 static修飾的成員變量是隸屬於整個類別所有成員所共享, 不是僅僅屬於某一個類對象。static修飾的成員函數不能操作非static成員變量。這是因爲普通成員函數會有個默認的形參爲this指針, this指針所指向對象爲具體的某個類對象, static修飾的成員函數不存在this指針。

class textBook {
public:
    static int getNum() {return num;}  //right
    static int getNum1() {return num1;}  //Error
private:
    int num1;
    static int num;
};

inline的用法

引用《effective c++》的一句話: inline函數看起來像函數,動作像函數,卻不是函數,比#define表現要好,調用inline函數不會蒙受調用普通函數的額外開銷。

下面我們分別對上面的話進行解析。
inline函數在書寫的時候完全是按照函數的格式, 所以看起來像函數; 動作像函數,主要是指調用inline函數時,會像普通函數一樣進行形參檢查,而宏不會對參數進行檢查,所以有時候一不小心就會出錯(即使你的本意不是這樣)。
inline函數的本意是在每一個調用inline函數的地方都會以inline函數的本體替換之,所以不會帶來額外的普通函數調用開銷。普通函數的調用開銷一般有保存返回地址,保存實參,保存某些寄存器值,而被調用函數不一定跟調用函數在同一個內存頁,因此可能還會涉及到內存頁面置換帶來的額外開銷。

上述所說的inline函數那麼多優點,是不是我們可以無所顧忌的使用inline函數呢?不是的。過度熱衷inline 函數會導致代碼膨脹,進而造成更多的內存換頁行爲,還會降低cache的命中率,直接造成運行效率降低。反之,如果函數的本體很小,那麼inline直接替換以後的函數本體可能比未替換的更小。運行效率不僅沒有下降,反而有提高。

一般來說,函數本體只有幾行,且沒有循環遞歸等較爲複雜邏輯運算的均可以定義爲inline函數。此外,並不是我們用inline去聲明一個函數或者將一個函數定義在class內,它就一定爲inline函數。請記住,inline只是我們對編譯器的一個請求,最終是不是得看編譯器的決定。

inline函數一般被定義於頭文件。因爲大部分的編譯器都是在編譯的時候對inline函數進行函數本體替換,這個時候編譯器需要在每一個調用inline函數的地方都看到inline的函數本體。

virtual函數被定義爲inline函數的願望一般不能實現。因爲inline函數在編譯的時候就要進行函數替換,需要知道調用的具體函數是哪一個,此過程發生在鏈接與運行之前。而virtual函數意味着,不知道調用的是哪一個函數,直到函數運行到此。

在工程實現的初期,爲了便於測試代碼debug,我們先不要定義任何inline函數,否則無法設定斷點之類的debug手段。在測試成功,進行優化時,我們可以再考慮將哪些函數進行inline。還有一點需要注意的是,因爲inline函數是在每一個調用處以本體替換,因此只要inline函數本體發生變化,每一個調用它的文件都要重新編譯鏈接,可能會消耗更多的時間。如果是非inline函數只要一個文件重新編譯,其它文件鏈接即可。這是在inline函數時要考慮的。


define的用法:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章