1. 程式人生 > 實用技巧 >第8章 深入理解C指標

第8章 深入理解C指標

8.1 指標概述

1.1.1 指標概述

 指標是一個特殊的變數,它裡面儲存的數值被解釋成為記憶體裡的一個地址。

指標的型別:從語法的角度看,你只要把指標宣告語句裡的指標名字去掉,剩下的部分就是這個指標的型別。這是指標本身所具有的型別。

  1) int *ptr;  //指標的型別是int *

  2) char *ptr;  //指標的型別是char *

  3) int **ptr;  //指標的型別是int **

  4) int (*ptr)[3];  //指標的型別是int (*)[3]

  5) int *(*ptr)[4];  //指標的型別是int *(*)[4]

指標所指向的型別:從語法的角度上看,你只須把指標宣告語句中的指標名字和名字左邊的指標宣告符 * 去掉,剩下的就是指標所指向的型別。當你通過指標來訪問指標所指向的記憶體區時,指標所指向的型別決定了

編譯器將把那片記憶體區裡的內容當做什麼來看待。

  1) int *ptr;   //指標所指向的型別是int

  2) char *ptr;   //指標所指向的的型別是char

  3) int **ptr;   //指標所指向的的型別是int *

  4) int (*ptr)[3];   //指標所指向的的型別是int ( )[3]

  5) int *(*ptr)[4];   //指標所指向的的型別是int *( )[4]

變數地址---系統分配給變數的儲存單元的起始地址。一個變數無論佔據有幾個儲存單元,都只有一個地址(首地址)可以作為它的地址。

指標變數:存放變數地址的變數是指標變數。即在C語言中,允許用一個變數來存放指標,這種變數稱為指標變數。一個指標變數的內容一定是另一個變數在記憶體中的地址。

把普通變數的地址賦予一個指標變數,那麼就稱該指標變數指向了普通變數。指標變數中存放那個變數的地址,就稱指標變數指向哪個變數。

char *names[] = { "Tom","Jerry","Anderson" };
printf("%c\n", *(*(names + 1) + 4));  y
printf("%c\n", names[1][4]);      y

1.1.2 指標變數的定義:

   資料型別名 * 指標變數名       int* pi;  int * pi;  int *pi;  int*pi; 這些宣告都是等價的。

 *兩邊的空白符無關緊要,*將變數宣告為指標,這是一個過載過的符號也用在乘法和解引用指標上。

指標變數賦值只能賦予地址,有趣的是,我們可以給指標賦 0,但是不能賦任何別的整數值。

1.1.3 如何閱讀指標宣告:倒過來讀。

    const int *pci;

  1) pci 是一個變數

  2)*pci 是一個指標變數

  3)int *pci 是一個指向整數的指標變數

  4)const int *pci 是一個指向整數常量的指標變數

1.1.4 地址操作符

  C語言提供了地址運算子 & 來表示變數的地址。  地址操作符&會返回運算元的地址。

1.1.5 列印指標的值

  %p 和 %x 的不同之處在於:%p一般會把數字顯示為十六進位制的大寫。

  虛擬記憶體和指標

    每個程式都假定自己能夠訪問機器的整個實體記憶體空間,實際上卻不是。程式使用的地址是虛擬地址。作業系統會在需要時把虛擬地址對映為實體地址。應用程式的虛擬地址不會變,就是我們在檢視指標內容時看到的地址。作業系統會幫我們將虛擬地址對映為真實地址。

1.1.6 用間接引用操作符解引指標

  間接引用操作符 * 返回指標變數指向的值,一般稱為解引指標。我們也可以把解引操作符的結果看作左值,“左值”是賦值操作符左邊的操作符,所有的左值都必須可以被修改,因為它們會被賦值。

1.1.8 NULL概念

  • NULL被賦給指標,就意味著指標不指向任何東西。
  • null 概念是指指標包含一個特殊的值,和別的指標不一樣,它們有指向任何記憶體區域。
  • NULL巨集是強制型別轉換為void指標的整數常量0。在很多庫中定義如下:#define NULL ( (void *) 0
  • ASCII字元NUL定義為全0的位元組。
  • null字串是空字串,不包含任何字元。
  • null語句就是隻有一個分號的語句。

1. 用不用NULL

  對於指標,使用NULL或0都可以,但NULL不能用於指標之外的上下文中。尤其是替代ASCII字元NUL肯定是有問題的。它等於字元 '\0' ,其值等於十進位制 0。

2. void 指標

  void指標是通用指標,用來存放任何資料型別的引用。任何指標都可以賦給void指標。

  • void指標具有與char指標相同的形式和記憶體對齊方式;
  • void指標和別的指標永遠不會相等,不過,兩個賦值為NULL的void指標是相等的。
  • void指標只能用做資料指標,而不能用做函式指標。

  sizeof操作符可以用在void指標上,不過我們無法把這個操作符用在void上。如下所示:

    size_t size = sizeof(void *);  //合法
  size_t size = sizeof(void);  //不合法    

3. 全域性和靜態指標

  指標被宣告為全域性或靜態,就會在程式啟動時被初始化為NULL。

1.2 指標的長度和型別

1.2.1 記憶體模型

  模型取決於作業系統和編譯器,一種作業系統可能支援多種模型,這通常是用編譯器選項來控制的。

1.2.2 指標相關的預定義型別

  • size_t:用於安全的表示長度
  • ptrdiff_t:用於處理指標算術運算
  • intptr_t 和 uintptr_t:用於儲存指標地址

1. 理解size_t

  size_t型別表示C中任何物件所能達到的最大長度。它是無符號整數,因為負數在這裡沒有意義。它的目的是提供一種可移植的方法來宣告與系統中可定址的記憶體區域一致的長度。size_t用做sizeof操作符的返回值型別,同時也是很多函式的引數型別,包括malloc和strlen。

  •   在宣告諸如字元數或者陣列索引這樣的長度變數時用size_t是好的做法。
  •   它經常用於迴圈計數器、陣列索引、有時候還用在指標算術運算上。

列印size_t型別的值要小心,因為這是無符號的值。推薦的格式說明符是%zu。不過,某些情況下不能用這個說明符,作為替代,可以考慮%u或%lu。

因為size_t是無符號的,一定要給這種型別的變數賦正數。

2. 對指標使用sizeof操作符

  sizeof操作符可以用來判斷指標長度。當需要使用指標長度時,一定要用sizeof操作符。

3. 使用intptr_t和uintptr_t

  ntptr_t和uintptr_t型別用來存放指標地址。他們提供了一種可移植且安全的方法宣告指標,而且和系統中使用的指標長度相同,對於把指標轉化成為整數形式來說很有用。
  uintptr_t是intptr_t的無符號版本。對於大部分操作,用intptr_t比較好。uintptr_t不像intptr_t那樣靈活。下面例子說明如何使用intptr_t:

  int num;
  intptr_t *pi = #

如果像下面那樣試圖把整數地址賦給uintptr_t型別的指標,得到一個語法錯誤。

  uintptr_t *pu = #

使用強制型別轉換可以消除警告:  uintptr_t *pu = (uintptr_t *) #

如果不強制轉換型別,不能將uintptr_t用於其它型別:

    char ch;
  uintptr_t *pu = (uintptr_t *)&ch;

當可移植性和安全性變得重要時,就應該使用這些型別。

1.3 指標操作符

1.3.1 指標算術運算

1.給指標加減整數

  給指標加上一個整數實際上加的數是這個整數和指標資料型別對應位元組數的乘積。其實,p是一個指標,n是一個正整數,對指標p進行加減n後實際地址為:

 p +或- n * sizeof(指標資料型別)    指標自增、自減的運算式是使指標指向下一個或上一個資料的首地址。

  例如:設 p是指標變數:

  C = *p++ 先取指標變數p的值賦給C ,p做自增運算,使p指向下一目標變數

  C = *++p 先使指標p做自增運算指向下一目標變數,再將p所指變數的值賦給C

  C = (*p)++ 取指標變數*p的值賦給C,然後變數*p的值增1

  C = ++(*p) 變數*p先增1再把值賦給C

4.指標相減

  一個指標減去另一個指標會得到兩個地址的差值。這個差值通常沒什麼用,但可以判斷陣列中的元素順序。指標之間的差值是他們之間相差的“單位”數,差的符號取決於運算元的順序。

1.3.2 比較指標

  指標可以用標準的比較操作符來比較。當把指標和陣列元素相比時,比較結果可以用來判斷陣列元素的相對順序。

1.4 指標的常見用法

1.4.1 多層間接引用第一個陣列用來儲存書名列表的字元陣列。

char *titles[] = { "A tale of Two Cities","Wuthering Heights","Don Quixote",
"Odyssey","Moby-Dick","Hamlet","Gulliver's Travels" };
  //宣告兩個指向字元指標的指標的陣列
char **bestBooks[3]; //陣列元素會儲存titles陣列中元素的地址
bestBooks[0] = &titles[1];
bestBooks[1] = &titles[3];
bestBooks[2] = &titles[5];

char **englishBooks[4]; //每個陣列元素都包含一個指向char 指標的指標
englishBooks[0] = &titles[0];
englishBooks[1] = &titles[2];
englishBooks[2] = &titles[4];
englishBooks[3] = &titles[6];
printf("%s", *englishBooks[0]);  //A tale of Two Cities

1.4.2 常量與指標

1.指向常量的指標

  可以將指標定義為指向常量,這意味著不能通過指標修改它所引用的值。

我們不能解引指向常量的指標並改變指標所引用的值,但可以改變指標。指標的值不是常量。指標可以改為引用另一個整數常量(const int),或者普通整數(int)。這樣做不會有問題。宣告只是限制我們不能通過指標來修改引用的值。

  const int limit = 123;
  const int *pci;
  pci = &limit;

把pci宣告為指向整數常量的指標意味著:

  • pci可以被修改為指向不同的整數常量
  • pci可以被修改為指向不同的非整數常量
  • 可以解引pci以讀取資料
  • 不能解引pci從而修改他指向的資料  

資料型別和const關鍵字的順序不重要。下面兩個語句是等價的:

  const int *pci;

  int const *pci;

2.指向非常量的常量指標

  一個指向非常量的常量指標,意味著指標不可變,但是它指向的資料可變。

  int num;
  int *const cpi = #

  • cpi必須被初始化為指向非常量變數;
  • cpi不能被修改
  • cpi指向的資料可以被修改。

無論cpi引用了什麼,都可以解引cpi然後賦一個新值。所以如果試圖把cpi初始化為指向常量limit,那麼常量就可以修改了,這樣是不對的,因為常量不可以被修改,所以會產生一個警告。

3.指向常量的常量指標

  這種指標本身不能被修改,它指向的資料也不能通過它來修改。

  const int * const cpci = &limit;    與指向常量的指標類似,不一定只能將常量的地址賦給cpci。

    const int * const cpci = #    宣告指標時必須進行初始化,如果不進行初始化就會產生語法錯誤。

4.指向“指向常量的常量指標”的指標

  const int * const * pcpci = &limit;