1.const關鍵字的性質
簡單來說:const關鍵字修飾的變量具有常屬性。 即它所修飾的變量不能被修改。
2.修飾局部變量
1 const int a = 10; 2 int const b = 20;
這兩種寫法是等價的,都是表示變量的值不能被改變,需要注意的是,用const修飾變量時,一定要給變量初始化,否則之后就不能再進行賦值了,而且編譯器也不允許不賦初值的寫法:
在C++中不賦初值的表達一寫出來,編譯器即報錯,且編譯不通過。


在C中不賦初值的表達寫出來時不報錯,編譯時只有警告,編譯可以通過。而當你真正給它賦值時才會報錯,那么沒有初值也不能被賦值這樣的變量有什么用哪?

1 const chsr* p = "qwerty"; //const用於修飾常量靜態字符串
如果沒有const的修飾,我們可能會在后面有意無意的寫p[4]=’x’這樣的語句,這樣會導致對只讀內存區域的賦值,然后程序會立刻異常終止。有了const,這個錯誤就能在程序被編譯的時候就立即檢查出來,這就是const的好處。讓邏輯錯誤在編譯期被發現。(這個特性在C/C++中相同)
3.修飾指針
常量指針是指針所指向的內容是常量,不可被修改。
1 const int * n = &a; 2 int const * n = &a;
上面兩種寫法也是等價的,性質如下:
1)常量指針不能通過這個指針改變變量的值,但是可以通過其他的引用來改變變量的值的。
1 const int *n = &a; 2 *n = b;
上面的寫法報錯
1 int c = 3; 2 const int *n = &a; 3 a = 10; 4 a = c;
這樣賦值是可以的。
常量指針指向的值不能改變,但是指針本身可以改變,即常量指針可以指向其他的地址。
1 int a = 1; 2 int b = 2; 3 const int *n = &a; 4 n = &b;
2)指針常量是指指針本身是個常量,不能在指向其他的地址,寫法如下:
1 int a = 1; 2 int b = 2; 3 int * const n = &a; 4 *n = b;
5 b = a;
而這么寫是錯誤的
1 int a = 1; 2 int b = 2; 3 int c = 3; 4 int * const n = &a; 5 n = &b;
它們的區別在於const的位置,可以這樣記憶:const在“*”前面時它修飾(*n),而*n是n所指向的變量,所以是常量指針,const在“*”后面時它修飾(n),使指針變為常量,所以是指針常量。
指向常量的常指針
1 const int * const p= &a; 2 int const * const p= &a;
指針指向的位置不能改變並且也不能通過這個指針改變變量的值,但是依然可以通過變量賦值,或其他的普通指針改變變量的值。
(這種用法在C和C++中是相同的。)
4.修飾引用
1 int a = 1; 2 int const &a = b; 3 const int &a = b;
兩種定義形式在本質上是一樣的
5.函數中使用const
(1)修飾函數參數
根據const修飾指針的特性,const修飾函數的參數也是分為三種情況
1 void StrCopy(char *strdes, const char *strsrc);//防止修改指針指向的內容
其中 strsrc是輸入參數,strdes是輸出參數。給 strsrc 加上 const 修飾后,如果函數體內的語句試圖改動 sresrc 的內容,編譯器將指出錯誤。
1 void swap ( int * const p1 , int * const p2 ) //防止修改指針指向的地址
指針p1和指針p2指向的地址都不能修改。
1 void test ( const int * const p1 , const int * const p2 ) //以上兩種的結合
另外當參數為引用時
1 void function(const Class& Var); //引用參數在函數內不可以改變 2 void function(const TYPE& Var); //引用參數在函數內為常量不可變
(這樣的一個const引用傳遞和最普通的函數按值傳遞的效果是一模一樣的,他禁止對引用的對象的一切修改,唯一不同的是按值傳遞會先建立一個類對象的副本, 然后傳遞過去,而它直接傳遞地址,所以這種傳遞比按值傳遞更有效.另外只有引用的const傳遞可以傳遞一個臨時對象,因為臨時對象都是const屬性, 且是不可見的,他短時間存在一個局部域中,所以不能使用指針,只有引用的const傳遞能夠捕捉到這個家伙。)
(2)修飾函數返回值
如果給以“指針傳遞”方式的函數返回值加 const 修飾,那么函數返回值(即指針)的內容不能被修改,該返回值只能被賦給加const 修飾的同類型指針。
1 const int * fun2() //調用時 const int *pValue = fun2(); 2 //我們可以把fun2()看作成一個變量,即指針內容不可變。 3 c.int* const fun3() //調用時 int * const pValue = fun2(); 4 //我們可以把fun2()看作成一個變量,即指針本身不可變。
const int fun1() //這個其實無意義,因為參數返回本身就是賦值。
6.修飾類相關
(1)用const修飾的類成員變量,只能在類的構造函數初始化列表中賦值,不能在類構造函數體內賦值。
1 class A 2 { 3 public: 4 A(int x) : a(x) // 正確 5 { 6 //a = x; // 錯誤 7 } 8 private: 9 const int a; 10 };
(2)const修飾成員函數
用const修飾的類成員函數,在該函數體內不能改變該類對象的任何成員變量, 也不能調用類中任何非const成員函數。一般寫在函數的最后來修飾。
1 class A 2 { 3 public: 4 int& getValue() const 5 { 6 // a = 10; // 錯誤 7 return a; 8 } 9 private: 10 int a; // 非const成員變量 11 };
(3)const修飾類對象/對象指針/對象引用
用const修飾的類對象表示該對象為常量對象,該對象內的任何成員變量都不能被修改。對於對象指針和對象引用也是一樣。
因此不能調用該對象的任何非const成員函數,因為對非const成員函數的調用會有修改成員變量的企圖。
1 class A 2 { 3 public: 4 void funcA() {} 5 void funcB() const {} 6 }; 7 int main 8 { 9 const A a; 10 a.funcB(); // 正確 11 a.funcA(); // X 12 13 const A* b = new A(); 14 b->funcB(); // 正確 15 b->funcA(); // X 16 }
(4)在類內重載成員函數
1 class A 2 { 3 public: 4 void func() {} 5 void func() const {} // 重載 6 };
另外,const數據成員只在某個對象生存期內是常量,而對整個類而言是可變的,因為類可以創建多個對象,不同對象的const數據成員值可以不同。
class A { public: A(int size) : _size(size) // 正確 {} private: const int _size; }; A a(10); //對象a的_size值為10 A b(20); //對象b的_size值為20
那么,怎樣才能建立在整個類中都恆定的常量呢?用枚舉常量。
class A { public: enum{SIZE1 = 10, SIZE2 = 20};//枚舉常量 private: int arr1[SIZE1]; int arr2[SIZE2]; };
枚舉常量不會占用對象的存儲空間(整個枚舉類型只占四個字節),它們在編譯時被全部求值。但缺點是隱含數據類型只能是整數,最大值有限,且不能表示浮點數。
7.修飾全局變量
全局變量的作用域是整個文件,我們應該盡量避免使用全局變量,以為一旦有一個函數改變了全局變量的值,它也會影響到其他引用這個變量的函數,導致除了bug后很難發現,如果一定要用全局變量,我們應該盡量的使用const修飾符進行修飾,這樣方式不必要的以為修改,使用的方法與局部變量是相同的。
8.const常量與宏常量的區別
(1).便於進行類型檢查
const常量有數據類型,而宏常量沒有數據類型。編譯器可以對前者進行類型安全檢查,而對后者只進行字符替換,沒有類型安全檢查,並且在字符替換時可能會產生意料不到的錯誤(邊際效應)。
1 //例子: 2 void f(const int i) { .........} //對傳入的參數進行類型檢查,不匹配進行提示
(2)可以節省空間,避免不必要的內存分配
const定義常量從匯編的角度來看,只是給出了對應的內存地址,而不是象#define一樣給出的是立即數,所以,const定義的常量在程序運行過程中只有一份拷貝,而#define定義的常量在內存中有若干個拷貝。
1 #define PI 3.14159 //常量宏 2 const doulbe Pi=3.14159; //此時並未將Pi放入ROM中 3 ...... 4 double i=Pi; //此時為Pi分配內存,以后不再分配! 5 double I=PI; //編譯期間進行宏替換,分配內存 6 double j=Pi; //沒有內存分配 7 double J=PI; //再進行宏替換,又一次分配內存!
(3)提高了效率
宏定義是一個“編譯時”概念,在預處理階段展開,不能對宏定義進行調試,生命周期結束於編譯時期。const常量是一個“運行時”概念,在程序運行時使用,類似於一個只讀數據。
編譯器通常不為普通const常量分配存儲空間,而是將它們保存在符號表中,這使得它成為一個編譯期間的常量,沒有了存儲與讀內存的操作,使得它的效率也很高
(4)可以保護被它修飾的東西
防止意外的修改,增強程序的健壯性。
1 void f(const int i) { i=10;//error! } //如果在函數體內修改了i,編譯器就會報錯 2
(5)為函數重載提供了一個參考
1 class A 2 { 3 ...... 4 void f(int i) {......} //一個函數 5 void f(int i) const {......} //上一個函數的重載 6 ...... 7 };
(6)定義域不同
1 void f1 () 2 { 3 #define N 12 4 const int n 12; 5 } 6 void f2 () 7 { 8 cout<<N <<endl; //正確,N已經定義過,不受定義域限制 9 cout<<n <<endl; //錯誤,n定義域只在f1函數中。若想在f2中使用需定義為全局的 10 }
(7)做函數參數
宏定義不能作為參數傳遞給函數;const常量可以在函數的參數列表中出現。
9.const_cast
const_cast運算符用來修改類型的const或volatile屬性。
(1)常量指針被轉化成非常量的指針,並且仍然指向原來的對象;
(2)常量引用被轉換成非常量的引用,並且仍然指向原來的對象。
1 void func() 2 { 3 const int a = 10; 4 int* p = const_cast<int*> (&a); 5 *p = 20; 6 std::cout<<*p; // 20 7 std::cout<<a; // 10 8 }
注:C++中使用const 常量而不使用宏常量,即const 常量完全取代宏常量。
10.const與volatile
1 //注意使用C++編譯器編譯(源文件使用. cpp后綴) 2 //或者在C編譯器中編譯期間打開優化選項, 比如: 3 // gcc test. c -O2 4 #include <stdi o. h> 5 int main() 6 { 7 const int num = 10; 8 int *p = (int *) # 9 *p = 20; 10 printf("%d\n", num) ; 11 return 0; 12 }/ 13 /看看程序輸出的結果是什么?
只要編譯器適當的對代碼進行優化, 這里就可能輸出: 10, 而不是我們改變之后的值。因為編譯器在編譯期間, 可能對代碼進行優化。當編譯器看到這里的num被const修飾, 從語義上講這里的num是不期望被改變(不改變)的,那優化的時候就可以把num的值存放到寄存器(以提高訪問的效率) 中。 以后只要使用num的地方都去寄存器中取, 那即使num對應的內存中的值發生變化, 寄存器也是感知不到的。 所以造成輸出10的結果。
1 //當我們對代碼進行如下修改: 2 #include <stdi o. h> 3 int main() 4 { 5 //使用volati le關鍵字對num修飾。 6 volatile const int num = 10; 7 int *p = (int *) # 8 *p = 20; 9 printf("%d\n", num) ; 10 return 0; 11 }
這里我們可以看到, 當我們對*p做了修改之后, num的輸出變成了20。這里 volatile 這個關鍵字起到關鍵的作業。
作用:
編譯時不優化, 執行時不緩存, 每次需從內存中讀出(保證內存的可見性) 。
使用場景:
用於多線程或多CPU編程。
ps:高質量C/C++第5章、第11章。
