你真的理解了const和volatile關鍵字麼?(我看不一定)
C語言const和volatile關鍵字
這部分內容比較簡單,我這裡直接先做總結,然後通過寫三個測試程式碼,體會其中的關鍵點
一、總結
1、const使得變數具有隻讀屬性(但是不一定就是不能更改)
2、const不能定義真正意義上的常量(因為有的用const定義的變數,仍然可以更改)
3、const將具有全域性生命期的變數儲存於只讀儲存區(這個是對現代編譯器是這樣的,但是對ANSI編譯器,仍然可以更改)
4、volatile強制編譯器減少優化,必須每次從記憶體中取值
5、const修飾的變數不是一個真的常量,它只是告訴編譯器該變數不能出現在賦值符號的左邊
6、在現在C編譯器中,修改const全域性變數將導致程式崩潰
7、c語言中字串字面量儲存於只讀儲存區中,在程式中需要使用const char*指標(這句話的意思就是用const char*修飾的字串字面量時(包括區域性和全域性的字串字面量),字串字面量是儲存在全域性只讀儲存區的,不能更改,更改會導致程式崩潰或者段錯誤)
8、const修飾函式引數表示在函式體內不希望改變引數的值(注意:這裡是不希望,那到底能不能更改,這得分情況)
9、const修飾函式返回值表示返回值不可更改,多用於返回指標情況
二、下面通過幾個測試程式碼體會上面結論(平臺:Ubuntu10 gcc 編譯器)
第1個例子是const修飾變數情況
#include <stdio.h> const int g_cc = 4; int main() { const const int cc = 0x01; int* p = (int*)&cc; printf("cc = %d *p = 0x%x\n",cc,*p); //cc = 2; //編譯通過,執行錯誤,因為cc被定義成const區域性變數,不能出現在賦值符號左邊,執行時導致程式段錯誤 *p = 3; //編譯和執行都通過,因為cc是區域性變數,所以不管是ANSI還是現代GCC編譯器都可以更改,同時也說明了用const修飾變數只是說明這個變數不能出現在賦值符號的左邊,但是依然可以更改,但是假如如果cc是全域性變數,那就不一定了,如果是ANSI編譯器是可以更改的(你可以用BCC編譯器試下,BCC就是早期的ANSI編譯器)因為早期編譯器把const修飾的變數還是儲存在全域性資料區可以更改,如果是現在的VC或者GCC編譯器是不可以更改的,因為現代編譯器把const修飾的全域性變數儲存在全域性只讀儲存區中,更改會出錯 printf("cc = %d *p = %d\n",cc,*p); p = (int*)&g_cc; printf("g_cc = %d *p = %d\n",g_cc,*p); //*p = 5; //編譯通過,執行錯誤,因為g_cc被定義成const全域性變數,又因為GCC屬於現代編譯器所以g_cc被分配到全域性只讀儲存區,不能更改,更改導致段錯誤 //printf("g_cc = %d *p = %d\n",g_cc,*p); return 0; }
上面的程式碼你可以把遮蔽部分程式碼開啟自己除錯,其實是不能執行的,程式碼註釋解釋的很清楚,這裡不說了,看下輸出結果:
其實我開始除錯時cc變數的型別是unsigend char,出現了一個意外問題,你們看下輸出,然後自己想下為什麼?(這其實是指標型別問題,後面我會講這個問題)
第2個例子是const修飾函式返回值、函式引數、字串情況
注意:我會在程式裡面提問18個問題,你看看你們能不能回答出來答案
#include <stdio.h>
const unsigned char *s1 = "G_hello world";
unsigned char a = 9;
unsigned char* fun(const unsigned char s,const unsigned char *str)
{
unsigned char *p = (unsigned char*)&s;
//s = 2; //(1)為什麼編譯錯誤
*p = 1; //(2)p指標指向s,但是s是const修飾的,不能更改,但是這裡為啥能夠更改
printf("s = %d *p = %d\n",s,*p);
p = (unsigned char*)str;
//*p = '_'; //(3)為什麼編譯通過,執行段錯誤
printf("str = %s\np = %s\n",str,p);
return "ABCDEF GHIJK"; //(4)這種定義的字串字面量和使用字元指標指向字串字面量有啥區別,還是一樣的?
}
int main()
{
const unsigned char *s2 = "Hello world";//(5)s2指標指向的內容能不能更改?
unsigned char *s3 = "LMNOP ORST"; //(6)你們看到s3比s2少了一個const,那編譯會不會出錯? s3指向內容能不能更改呢?那和s2有啥區別呢?
unsigned char *s4 = "LMNOP ORST"; //(7)s4和s3指向的內容都是一樣的,那他們地址是不是一樣的呢?
unsigned char s5[] = "LMNOP ORST"; //(8)s5和s3一個是陣列,一個是指標,那他們有啥區別呢?而且他們內容也是相同的,那他們的地址是不是也是一樣的呢
const unsigned char s6[] = "LMNOP ORST";//(9)s6比s5多了一個const,多了這個導致有啥區別麼?
const unsigned char i = 2;
const static unsigned char j = 3; //(10)j比i多了一個static,多了這個導致有啥區別麼?
unsigned char *pc = fun(i,s2);
printf("&i = %p &a = %p\n",&i,&a);
printf("&j = %p j = %d\n",&j,j);
printf("s1 = %p s2 = %p\n",s1,s2);
printf("s3 = %p s4 = %p\n",s3,s4);
printf("s5 = %p s5 = %s\n",s5,s5);
printf("s6 = %p s6 = %s\n",s6,s6);
printf("pc = %p\npc = %s\n",pc,pc);
//(11)通過觀測這麼多變數,字元指標,陣列你發現什麼規律沒(從地址去觀察)
//j = 4; //編譯出錯,我們通過終端打印發現j是儲存在全域性只讀區域中,所以不能更改
pc = &j; //編譯出現警告,執行通過,因為指標可以指向任何地方
//*pc = 5; //編譯通過,執行段錯誤,因為pc指向的是全域性只讀區域,所以不能更改
//*pc = '!'; //(12)編譯通過,為什麼執行段錯誤
//*s2 = '_'; //(13)為什麼編譯錯誤
pc = s2; //編譯出現警告,因為型別不一樣
//*pc = '$'; //(13)編譯通過,為什麼執行段錯誤
//*s3 = 'A'; //(14)編譯通過,為什麼執行段錯誤
pc = s3;
//*pc = '_'; //(15)編譯通過,為什麼執行段錯誤
printf("更改前:s5 = %s\n",s5);
s5[0] = 'A';
pc = s5;
*(pc + 1) = 'B';
printf("更改後:pc = %s s5 = %s\n",pc,s5);
printf("更改前:s6 = %s\n",s6);
//s6[0] = 'A'; //(16)為什麼不能更改
pc = &s6[0];
*pc = 'A'; //(17)為什麼用一個指標卻可以更改s6呢,再從地址觀察s5和s6,有啥發現
printf("更改後:pc = %s s6 = %s\n",pc,s6);
return 0;
}
我們看下終端輸出:
現在回答上面的17個答案:
(1):因為被const關鍵字修飾變數,不能出現在賦值符號左邊,所以編譯出錯
(2):因為用const定義變數只是告訴編譯器不能出現賦值符號左邊,但是本質還是變數,這裡就是區域性變數,還是可以通過指標修改它的值
(3):編譯肯定通過,因為p是指標當然可以指向任何地方,執行錯誤是因為p指標指向的是字串字面量,而字串字面量是儲存在全域性只讀儲存區,所以執行錯誤(具體為什麼是全域性只讀區域,後面我在(11)提問裡面會說)
(4):其實是一樣的,因為我們從終端地址發現他們都在0x80487XXH記憶體區域裡面,而這個區域就是全域性只讀區域,都是不能更改的(具體為什麼是全域性只讀區域,後面我在(11)提問裡面會說)
(5):不能更改的,因為定義的字串指標是指向字串字面量,而字串字面量儲存的區域是全域性只讀區,所以不能更改,有的人問,你怎麼知道是全域性只讀區域,這個在(11)的提問裡面回答這個問題
(6):編譯是不會出錯的(包括編譯和執行),s3指向的內容也是不能更改的,這個在後面我會給你驗證的,其實你從終端列印的地址也能看出來的,因為你發現他們都是儲存在0x80487XX的地址區域,而這個區域都是全域性只讀區域,所以不能更改,還有和s2有什麼區別,其實我認為是沒有區別的,因為他們都不能更改,而且儲存的區域也都一樣,所以我認為沒有區別
(7):通過終端列印我們發現地址居然一樣,編譯器居然為了節省空間(我猜想的),只儲存一個"LMNOP ORST",當然他們都是存在只讀記憶體空間,不能更改,比較安全,如果是可更改空間,那可就出大事了,修改其中一個內容值,另外一個變數內容也跟著更改了
(8):首先s3是字元指標,指向內容是一個字串字面量,而且s3指向內容的區域是全域性只讀區域,所以不能更改,而s5是陣列是可以更改的,而且s5是區域性的,也就是儲存在棧中,臨時分配的記憶體,函式執行完釋放掉,同時通過終端列印我們也發現s3和s5記憶體地址也是完全不一樣的,相差很多,因為一個是全域性只讀區域,另一個是區域性記憶體區域(就是棧)
(9):s6和s5的區別是,s5可以直接更改,就是s[0] = 'A';,而s6是不能直接更改的,s6[0] = 'A'編譯器在編譯時就會報錯,但是他們都是儲存在區域性記憶體區域(就是棧),這個區域是可以通過指標進行更改的,所以s6還是可以更改的,通過終端打印發現他們地址也很接近
(10):j和i的區別是,j儲存在全域性只讀區域,不能更改,i是儲存在棧中,是可以更改的,但是不能直接更改,必須通過指標進行更改,通過終端列印也發現,j的地址和s1、a的地址都很接近,所以j肯定是全域性只讀區域(為什麼是隻讀區域,後面我會驗證,因為經過驗證它不能更改)
(11):總結:
首先我們肯定知道s1肯定是全域性區域,又由於s1不能更改(這個我沒寫程序序裡面,你們可以自己去驗證下, 其實真的不能更改),所以s1儲存在全域性只讀區域,又因為s1跟j、s2、s3、s4、pc,所以這些變數儲存的內容都是儲存在全域性只讀區域內,不能更改,但是你們發現沒,a變數肯定也是全域性變數,但是它確是可以更改的,所以a和s1地址肯定不一樣,通過終端打印發現,他們確實不挨著,而且相差也不是很多,因為他們都在全域性區域內
其次:通過這個例子我們知道用const unsigned char*定義的指標指向了字串字面量是不能更改的,而且是儲存在全域性只讀儲存區的(這裡記住,即使沒有const也是全域性只讀區域,s3就是這樣的),要是也想把區域性變數也定義到全域性只讀儲存區中,需要用const static關鍵字(比如這裡的j變數就是),而且我們還發現,字串指標如果只向內容是一樣的,編譯器居然為了省空間,地址居然是一樣的
再次: 字串指標和陣列,是有區別的,他們只向的內容儲存的區域不一樣,字串指標是全域性只讀區域,而陣列是棧中,可以更改,雖然有的加了onst但是通過指標還是可以更改
(12):因為pc指標指向fun函式返回的內容是儲存在全域性只讀區域,不能更改,所以執行錯誤
(13):因為const定義變數是不能出現在賦值符號左邊,而且s2指標,指向的內容是字串字面量,是儲存在全域性只讀區域內,是不能更改的
(14):因為s3指標,指向的內容是字串字面量,是儲存在全域性只讀區域內,是不能更改的
(15):同上
(16):因為s6是const關鍵字定義的區域性變數,是不能出現在賦值符號左邊,但是可以更改,不能這樣直接更改,需要用指標進行更改
(17):通過終端打印發現s5和s6地址很接近,因為他們都是區域性變數,儲存在棧中,但是因為s6是用const關鍵字定義變數,是不能出現在賦值符號左邊的,但是又因為s6是儲存在區域性變數區域,所以可以通過指標進行更改
講的很囉嗦,希望不要見怪
本文主要參考了"狄泰軟體C進階視訊教程”