1. 程式人生 > >你真的理解了const和volatile關鍵字麼?(我看不一定)

你真的理解了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進階視訊教程”