1. 程式人生 > >指標(上)--指標存放,陣列指標及const的瞭解

指標(上)--指標存放,陣列指標及const的瞭解

 之前一直聽別人說指標是如何如何難,導致在寫程式碼時一直對指標是誠惶誠恐。現在,是時候應該系統地學一下指標了,既然怕為什麼不去克服,不去克服又怎麼提高,不管是否困難。事實證明指標也沒什麼好怕的,只要掌握呼叫邏輯,使用時多個心眼,相信我們都能熟練精準地使用。
 下面我們開始指標的理解之旅,這篇文章我將從指標的”起源”開始說起,以便有一個階段性的瞭解。
 同樣借鑑了以下幾篇文章,寫得很詳細,具體可以轉以下連結進行更深入的瞭解,同時如果有什麼不對的地方煩請及時告知。
徹底搞定C語言指標詳解-完整版-時候初學者-必備
C指標,C語言中的指標詳解
c語言指標學習
C語言陣列與指標詳解


 下面開始正文。

指標與記憶體

 我們在學習指標的過程中不只一次地看見人們會把指標比喻成門牌號,比喻成電影院座位,這些比喻不可否認都很好的將指標抽象到了生活中,方便我們的理解,當然我們也可以借鑑這種方法來理解生活中那些讓我們一時琢磨不透的知識。

記憶體怎麼放變數

 先上一張圖,以便後續講解。
這裡寫圖片描述

變數?

 首先知道下變數是什麼意思,怎麼出現的?
 從上圖可以看出來,記憶體地址中6至7儲存了一個值為10,12中儲存了一個’x’。當然實際中的記憶體地址通常不會這麼淺顯地為6,為12,他可能是一長串的值,比如0x7fff8b6a378c這樣的值,如果就是用這種值來對我們需要的值進行訪問和處理無疑將加重我們的負擔,所以高階語言特性之一中就提供了通過名字而不是地址來訪問記憶體,進行對記憶體的讀與寫。
 就如上圖中虛線框出的i

為值10的記憶體地址,c為字元值’x’的記憶體地址,這裡的i和c我們可以稱之為變數。如此再配上我們的貼近實際作用的變數名,比如currentTime,recvbuf等等就能給我們的程式設計和後續的程式碼閱讀提供很大的方便。
 那麼我們又不經要問這些變數又是誰將他們轉變成類似0x7fff8b6a378c這樣的地址的呢?===>是由編譯器為我們實現的。即如“變數=====>編譯器=====>具體地址”的流程。

變數在記憶體中的存放

 而由上圖可知:
 1、記憶體中一個單位空間佔一個位元組,並且這些空間連續;
 2、在記憶體中整型變數佔4個位元組,字元型變數佔1個位元組;
 3、變數會根據其自身大小在記憶體中佔用適合自身大小的空間。

記憶體怎麼放指標變數

 開始前先區別一下指標和地址,指標是儲存地址一個變數,地址是記憶體分配。指標可以指向一個地址,也可以指向其他地址。
 指標變數是一個用於指向記憶體中的地址。那麼這個指標的值,也就是地址在記憶體中是怎麼放的呢?
 首先需要清楚的是,指標變數也是一個變數,只不過這個變數能存放一個記憶體的地址,程式中可以通過這個地址來找到對應地址的內容罷了。
 清楚了指標變數是一個變數,那麼他在記憶體中的存放也是如下圖所示:
這裡寫圖片描述
 同時應該清楚32位系統中指標變數在記憶體中所佔的長度為4個位元組,所以上圖中的指標變數p_i佔用了記憶體中的13~16位置,儲存的是6,即變數i的起始地址。
 需要了解的是指標變數所儲存的指向一個變數的地址是否是一個該指標指向的變數的起始地址是由編譯器決定的。也就是說上圖中如果指標變數p_i是變數i的地址,那麼他的值是否是6由編譯器決定,一般的編譯器是以第一個地址編號為指標變數的值。
 所以圖中,p_i為變數i的地址,(*p_i)能訪問到i的值,&i的值與p_i的值相等。

指標的指標

 有了上面的瞭解,對於指標的指標的理解也就輕鬆許多了,通用如下圖:
這裡寫圖片描述
 圖中又命名了一個變數pp_i,他的值為13,這裡的13我們可以看成一個普通的值,當然也可以看成我們現在需要講的指標地址,可以發現如果將pp_i看成一個指標,那麼他指向的內容即為指標p_i的地址,而p_i又是i的地址。所以我們可以這樣說:
 p_i值是i的指標;
 pp_i是p_i的指標;
 pp_i是i的指標的指標。
 恩,就是這樣了。如果要通過pp_i訪問i,可以利用**pp_i進行訪問。

陣列中的指標

 說完單個值,我們再來看看組合的。即陣列,在我們程式設計時陣列中可以儲存很多個值。對於指標,我們又不禁要問既然能儲存很多個值,那麼這些值肯定也是有他們自己的地址,那麼這些地址是怎麼表示和一個一個訪問的?
 對於上面的問題,用以下一張圖來進行解釋:
這裡寫圖片描述
 相比於之前的幾張圖片,該記憶體中又增加了一個以a為起始地址的連續的儲存方式。這種方式也就是我們的陣列的儲存方式,起始地址其後面的值可以通過a[1],a[2]…或者(a+1),(a+2)..來進行訪問。具體的訪問操作我們可以看下面一段程式碼。

int main()
{
    int i = 0;
    int a[7] = {1,2,3,4,5,6,7};
    int *p = a;

    for(i = 0; i < 7; i++)
    {
        printf("%d\n", a[i]);
        printf("%d\n", *(a+i));
        printf("%d\n", p[i]);
        printf("%d\n", *(p+i));  //或者printf("%d\n", *p++);
    }
    return 0;
}

 以上幾種列印方式都能將a陣列的內容打印出來,那麼這樣看來是不是可以將a和p看做是同樣性質的指標變數呢?其實不是,我們可以自己編寫程式碼試試列印*a++的結果,a=&i的結果。我們會發現編譯不過。
 為什麼會這樣,因為a是一個數組的首地址時,a表示的就是一個指標常量,而不是p那樣的指標變數,他的值不能被改變。也就是說其效果與宣告int* const pi = &i;是類似的,這時候的指標pi不能被改變。

const的變位

 之前一直被這個const搞得不知所措,現在正是好機會整理一下了。

const?

什麼是const

 const常型別是指使用型別修飾符const說明的型別,常型別的變數或物件的值是不能被更新的(除區域性定義情況下的利用指標改變)。

為什麼要用const

 首先,瞭解一下為什麼要用const。我們宣告一個變數。
 有時候我們需要定義一個變數名x用來存放一個我們不想讓他改變的值,這樣我們就可以在其他地方使用這個x,而不用再去重複地獲取這個固定的值。
 以上就是我的簡單理解,當然下面還有更專業的解釋,下面引用網路上搜到的一段話,感覺系統解釋了使用const的原因,閱讀下面引用時請帶上這個問題思考(const常量在記憶體中是怎麼存放的?):

程式編譯階段

(1)可以定義const常量,具有不可變性。 例如:
  const int Max=100; int Array[Max];
(2)便於進行型別檢查,使編譯器對處理內容有更多瞭解,消除了一些隱患。例如: void f(const int i) { ………} 編譯器就會知道i是一個常量,不允許修改;
(3)可以保護被修飾的東西,防止意外的修改,增強程式的健壯性。 還是上面的例子,如果在函式體內修改了i,編譯器就會報錯; 例如:
  void f(const int i) { i=10;//error! }
(4) 在編譯階段提高了效率編譯器通常不為普通*const常量分配儲存空間,而是將它們儲存在符號表*中,這使得它在成為一個編譯期間的常量,沒有了儲存與讀記憶體的操作,使得它的效率也很高。

程式執行階段

(1)可以避免意義模糊的數字出現,同樣可以很方便地進行引數的調整和修改。 同巨集定義一樣,可以做到不變則已,一變都變!如(1)中,如果想修改Max的內容,只需要:const int Max=you want;即可!
(2) 為函式過載提供了一個參考
  class A { ……
  void f(int i) {……} //一個函式
  void f(int i) const {……} //上一個函式的過載 ……
  };
(3) 可以節省空間,避免不必要的記憶體分配。 例如:
  #define PI 3.14159 //常量巨集
  const doulbe Pi=3.14159; //此時並未將Pi放入ROM中 ……
  double i=Pi; //此時為Pi分配記憶體,以後不再分配!
  double I=PI; //編譯期間進行巨集替換,分配記憶體
  double j=Pi; //沒有記憶體分配
  double J=PI; //再進行巨集替換,又一次分配記憶體!
const定義常量從彙編的角度來看,只是給出了對應的記憶體地址,而不是象#define一樣給出的是立即數,所以,const定義的常量在程式執行過程中只有一份拷貝,而#define定義的常量在記憶體中有若干個拷貝。
  (注意這裡說明的是程式執行期間const常量給其他變數賦值時分配空間,上面編譯階段的第(4)是編譯器間,兩者在不同階段,兩者不衝突)

 所以針對上面的問題“const常量在記憶體中是怎麼存放的?”我想說的是在閱讀相關資料時主要還是要區分我們說的const是在編譯階段還是在程式執行階段
編譯階段:是不會給普通的const常量分配儲存空間的,同時這又涉及到一個叫常量摺疊及符號表的內容,這裡就暫時不一 一講述了,後面有時間會在下面貼出對const的這些內容的具體講述連線。
C++常量摺疊
符號表及其基本實現
程式執行階段:又區分全域性變數與區域性變數。const全域性變數存放在.rodata只讀資料段裡,即使是值修改也會造成SEGV崩潰;const區域性變數存放在棧上,和普通區域性變數無差別,可以用指標間接修改const值。
 來看下面一段程式碼:

const int data = 1;        //使用指標修改引起程式SEGV崩潰
main()
{
int *p = &data;
*p = 2;                     //導致程式崩潰
}
//編譯完後使用objdump -t命令,可以發現data被放在.rodata section

(上面的objdump -t命令說明可以看以下連結:objdump 反彙編

const的使用

 瞭解了以上的const闡述,應該對const有一定的瞭解了。接下來說明下程式中const的使用。
 首先陳列出通常情況下const的使用方式:
 1、int const x = 10; (等同於const int x = 10;)
 2、int const *x; (等同於const int *x;)
 3、int* const x;
 4、const int* const x;
 這裡先說明一點,在1和2的宣告中const放在int前還是int後並沒有什麼區別,主要的區別在於符號”*”。下面將給一 一進行說明。

1、int const x;

 說明x的值不能被改變,對等於(const int x)當然除了使用強制型別指標改變外。請看下面一段程式碼:

int main()
{
    int const x = 10;
    int *p = (int*)&x;  //這裡如果沒有強制型別轉換會編譯警告
    //x = 20;           //如果這樣賦值編譯時會出錯
    *p = 30;
    printf("x = [%d], *p = [%d]\n", x, *p);
    return 0;
}

 編譯以上程式碼並執行,得到本來使用const宣告的x的值居然被改變了。
 這不是我們原本想要的結果,我們定義這個值的初衷是要確定這個x的值不被改變。所以建議如果不是特殊情況,請不要用強制型別轉換去改變這個值,因為這違反了我們當初定義這個值的初衷。

2、int const *x;

 等同於(int const* x;)這裡我們聲明瞭一個(int*)型的指標常量,該宣告表示指標x指向的內容不能被修改,而指標x的值可以被修改
 怎麼看呢,我們可以這樣簡單的理解。const位於符號” * “前面,所以const修飾的是(*x),所以表示的是( *x)的內容不能被修改。
 所以如果利用上面的方法來看const的修飾內容,雖然int const *x與const int *x等同,個人建議還是使用int const *x更好,因為這樣更加利於理解。
 同樣下面看一段程式碼:

int main()
{
    int i = 20;
    int a = 10;
    int const *x = &i;   //這裡,x可以在任意時候重新賦值一個新記憶體地址
    x = &a;       //可以給指標x賦值
    a = 40;       //改變a的值即也改變了指向a的x指標指向的值
    //*x = 50;    //這裡不能給*x 賦值,因為其不能被改變
    printf("*x = %d\n", *x); //打印出來是40
}

3、int* const x;

 相信通過上面的解釋,已經可以看出來int* const x所要表達的意思了,他表示指標x不能被修改,而*x 則可以被修改
 同樣利用上面第二點的認識方法,這裡的const是修飾x,而不是修飾*x 所以為這個效果。
 請看下面一段程式碼:

int main()
{
    int i = 20;
    int a = 10;
    int* const x = &i;
    //x = &a;        //這裡const修飾的是指標x,所以x不能被修改
    *x = 30;         //這裡const沒有修飾*x,所以*x可以被修改
    printf("*x = [%d]\n", *x); //結果為30
    return 0;
}

const int* const x;

 這個宣告就很明顯了,即指標x和指標x指向的值都不可以被修改

const使用總結

 引用上面部落格的一段話來進行const的使用總結:

1) 如果const 修飾在*x 前則不能改的是 *x (即不能 類似這樣:*x = 50;賦值)而不是指x.
2) 如果const 是直接寫在x前則x不能改(即不能類似 這樣:x=&i;賦值)。

 最後看下面兩種情況,他們說明這些嘗試變著法地去改變不能被改變的值都是不允許的。
*情況一:**int pi指標指向const int i常量的情況

int main()
{
    const int i1=40;
    int *pi;
    pi=&i1; //這樣可以嗎?不行,會出現警告,但可以用強制型別轉換        (int*)&i1來,但不推薦。
         //const int 型別的i1的地址 是不能賦值給指向int 型別地址的指標pi的。否則pi豈不是能修改i1的值了嗎!
}

*情況二:**const int pi指標指向const int i1的 情況

int main()
{
const int i1=40;
const int * pi;
pi=&i1;//兩個型別相同,可以這樣賦值。很顯然,i1的值無論是通過pi還是i1都不能修 改的。
}

 好了這一篇我要說的就這麼多,再說下去就有點找不到北了,下一篇我將總結下指標在函式及指標型別的注意事項。
 如果有什麼理解不對的地方還請儘快幫忙指出,共同提高哈。