- ?p 稱為指針變量,p 里存儲的內存地址處的內存稱為 p 所指向的內存。 指針變量 p 里存儲的任何數據都將被當作地址來處理
- 一個基本的數據類型(包括結構體等自定義類型)加上“*” 號就構成了一個指針類型的模子。這個模子的大小是一定的,與“*”號前面的數據類型無 關。
- “*”號前面的數據類型只是說明指針所指向的內存里存儲的數據類型。所以,在 32 位 系統下,不管什么樣的指針類型,其大小都為 4byte。可以測試一下 sizeof(void *)。
- int *p = NULL;? 這時候通過編譯器查看 p 的值為 0x00000000。這句代碼的意思是:定義一個指針 變量 p,其指向的內存里面保存的是 int 類型的數據;
- 在定義變量 p 的同時把 p 的值設置為 0x00000000,而不是把*p 的值設置為 0x00000000。
- 這個過程叫做初始化,是在編譯的時候 進行的。避免野指針,指向一段關鍵數據的地址造成數據的損毀
- int *p;? *p = NULL; 同樣,我們可以在編譯器上調試這兩行代碼。第一行代碼,定義了一個指針變量 p,其指向 的內存里面保存的是 int 類型的數據;但是這時候變量 p 本身的值是多少不得而知,也就是 說現在變量 p 保存的有可能是一個非法的地址。第二行代碼,給*p 賦值為 NULL,即給 p 指向的內存賦值為 NULL;但是由于 p 指向的內存可能是非法的,所以調試的時候編譯器可 能會報告一個內存訪問錯誤。這樣的話,我們可以把上面的代碼改寫改寫,使 p 指向一塊合 法的內存:
- int i = 10; int *p = &i; *p = NULL;? 在編譯器上調試一下,我們發現 p 指向的內存由原來的 10 變為 0 了;而 p 本身的值, 即內存地址并沒有改變。經過上面的分析,相信你已經明白它們之間的區別了。不過這里還有一個問題需要注 意,也就是這個 NULL。初學者往往在這里犯錯誤。注意 NULL 就是 NULL,它被宏定義為 0:
#define NULL 0 - 很多系統下除了有 NULL 外,還有 NUL(Visual C++ 6.0 上提示說不認識 NUL)。NUL 是 ASCII 碼表的第一個字符,表示的是空字符,其 ASCII 碼值為 0。其值雖然都為 0,但表示的意思 完全不一樣。同樣,NULL 和 0 表示的意思也完全不一樣。一定不要混淆。
- 另外還有初學者在使用 NULL 的時候誤寫成 null 或 Null 等。這些都是不正確的,C 語 言對大小寫十分敏感啊。當然,也確實有系統也定義了 null,其意思也與 NULL 沒有區別, 但是你千萬不用使用 null,這會影響你代碼的移植性。
將數值存儲到指定的內存地址
- 假設現在需要往內存 0x12ff7c 地址上存入一個整型數 0x100。我們怎么才能做到呢?我 們知道可以通過一個指針向其指向的內存地址寫入數據,那么這里的內存地址 0x12ff7c 其 本質不就是一個指針嘛。所以我們可以用下面的方法:? ?int *p = (int *)0x12ff7c;? ?*p = 0x100;
- 需要注意的是將地址 0x12ff7c 賦值給指針變量 p 的時候必須強制轉換。至于這里為什么選擇內存地址 0x12ff7c,而不選擇別的地址,比如 0xff00 等。這僅僅是為了方便在 Visual C++ 6.0 上測試而已。如果你選擇 0xff00,也許在執行*p = 0x100;這條語句的時候,編譯器 會報告一個內存訪問的錯誤,因為地址 0xff00 處的內存你可能并沒有權力去訪問。
- 既然這 樣,我們怎么知道一個內存地址是可以合法的被訪問呢?也就是說你怎么知道地址 0x12ff7c 處的內存是可以被訪問的呢?其實這很簡單,我們可以先定義一個變量 i,比如:int i = 0;? 變量 i 所處的內存肯定是可以被訪問的。然后在編譯器的 watch 窗口上觀察&i 的值不就 知道其內存地址了么?這里我得到的地址是 0x12ff7c,僅此而已(不同的編譯器可能每次給 變量 i 分配的內存地址不一樣,而剛好 Visual C++ 6.0 每次都一樣)。你完全可以給任意一個 可以被合法訪問的地址賦值。得到這個地址后再把“int i = 0;”這句代碼刪除。
- *(int *)0x12ff7c = 0x100;? 這行代碼其實和上面的兩行代碼沒有本質的區別。先將地址 0x12ff7c 強制轉換,告訴編譯 器這個地址上將存儲一個 int 類型的數據;然后通過鑰匙“*”向這塊內存寫入一個數據。
- 如上圖所示,當我們定義一個數組 a 時,編譯器根據指定的元素個數和元素的類型分配確定 大小(元素類型大小*元素個數)的一塊內存,并把這塊內存的名字命名為 a。名字 a 一旦 與這塊內存匹配就不能被改變。a[0],a[1]等為 a 的元素,但并非元素的名字。數組的每一個 元素都是沒有名字的。那現在再來回答第一章講解 sizeof 關鍵字時的幾個問題:
- sizeof(a)的值為 sizeof(int)*5,32 位系統下為 20。
- sizeof(a[0])的值為 sizeof(int),32 位系統下為 4。
- sizeof(a[5])的值在 32 位系統下為 4。并沒有出錯,為什么呢?我們講過 sizeof 是關鍵字 不是函數。函數求值是在運行的時候,而關鍵字 sizeof 求值是在編譯的時候。雖然并不存在 a[5]這個元素,但是這里也并沒有去真正訪問 a[5],而是僅僅根據數組元素的類型來確定其 值。所以這里使用 a[5]并不會出錯。
- sizeof(&a[0])的值在 32 位系下為 4,這很好理解。取元素 a[0]的首地址。 sizeof(&a)的值在 32 位系統下也為 4,這也很好理解。取數組 a 的首地址。
- &a[0]和&a 的區別? ?a[0]是一個元素,a 是整個數組,雖然&a[0]和&a 的值一樣,但其意義不一樣。前者是數組首元素的首地址,而后者是數組的首地址。舉個 例子:湖南的省政府在長沙,而長沙的市政府也在長沙。兩個政府都在長沙,但其代表的 意義完全不同。這里也是同一個意思。?
數組名 a 作為左值和右值的區別
- 簡單而言,出現在賦值符“=”右邊的就是右值,出現在賦值符“=”左邊的就是左值。? 比如,x=y。
- 左值:在這個上下文環境中,編譯器認為 x 的含義是 x 所代表的地址。這個地址只有 編譯器知道,在編譯的時候確定,編譯器在一個特定的區域保存這個地址,我們完全不必考慮這個地址保存在哪里。
- 右值:在這個上下文環境中,編譯器認為 y 的含義是 y 所代表的地址里面的內容。這 個內容是什么,只有到運行時才知道。
- C 語言引入一個術語-----“可修改的左值”。意思就是,出現在賦值符左邊的符號所代 表的地址上的內容一定是可以被修改的。換句話說,就是我們只能給非只讀變量賦值。
- 當 a 作為右值的時候代表的是什么意思呢?很多書認為是數組的首地址,其實這是非常 錯誤的。a 作為右值時其意義與&a[0]是一樣,代表的是數組首元素的首地址,而不是數組 的首地址。這是兩碼事。但是注意,這僅僅是代表,并沒有一個地方(這只是簡單的這么 認為,其具體實現細節不作過多討論)來存儲這個地址,也就是說編譯器并沒有為數組 a 分配一塊內存來存其地址,這一點就與指針有很大的差別。
- a 不能作為左值!編譯器會認為數組名作為左值代表 的意思是 a 的首元素的首地址,但是這個地址開始的一塊內存是一個總體,我們只能訪問數組的某個元素而無法把數組當一個總體進行訪問。所以我們可以把 a[i]當左值,而無法把 a 當左值。其實我們完全可以把 a 當一個普通的變量來看,只不過這個變量內部分為很多小塊, 我們只能通過分別訪問這些小塊來達到訪問整個變量 a 的目的。
- A),char *p = “abcdef”;
- B),char a[] = “123456”;
以指針的形式訪問和以下標的形式訪問指針
- 例子 A)定義了一個指針變量 p,p 本身在棧上占 4 個 byte,p 里存儲的是一塊內存的首地址。這塊內存在靜態區,其空間大小為 7 個 byte,這塊內存也沒有名字。對這塊內存的訪 問完全是匿名的訪問。比如現在需要讀取字符‘e’,我們有兩種方式:
- 1),以指針的形式:*(p+4)。先取出 p 里存儲的地址值,假設為 0x0000FF00,然后加 上 4 個字符的偏移量,得到新的地址 0x0000FF04。然后取出 0x0000FF04 地址上的值。
- 2),以下標的形式:p[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。p[4]這個操作會被解析成:先取出 p 里存儲的地址值,然后加上中括號中 4 個元素的偏 移量,計算出新的地址,然后從新的地址中取出值。也就是說以下標的形式訪問在本質上 與以指針的形式訪問沒有區別,只是寫法上不同罷了。
-
偏移量的單位是元 素的個數而不是 byte 數
#include <iostream>int main(){int a[] = {1,2,3,4,5,6,7,8,9,10};int *ptr = (int*)(&a + 1);printf("%d,%d",*(a+1),*(ptr-1));
}
- 對指針進行加 1 操作,得到的是下一個元素的地址,而不是原有地址值直接加 1。所以,一個類型為 T 的指針的移動,以 sizeof(T) 為移動單位。 因此,對上題來說,a 是一個一 維數組,數組中有 5 個元素; ptr 是一個 int 型的指針。?
- 這就是為什么 extern char a[]與 extern char a[100]等價的原因。因為這只是聲明,不分配 空間,所以編譯器無需知道這個數組有多少個元素。這兩個聲明都告訴編譯器 a 是在別的文 件中被定義的一個數組,a 同時代表著數組 a 的首元素的首地址,也就是這塊內存的起始地 址。數組內地任何元素的的地址都只需要知道這個地址就可以計算出來。
- 但是,當你聲明為 extern char *a 時,編譯器理所當然的認為 a 是一個指針變量,在 32 位系 統下,占 4 個 byte。這 4 個 byte 里保存了一個地址,這個地址上存的是字符類型數據。雖 然在文件 1 中,編譯器知道 a 是一個數組,但是在文件 2 中,編譯器并不知道這點。大多數 編譯器是按文件分別編譯的,編譯器只按照本文件中聲明的類型來處理。所以,雖然 a 實際 大小為 100 個 byte,但是在文件 2 中,編譯器認為 a 只占 4 個 byte。
- 編譯器會把存在指針變量中的任何數據當作地址來處理。所以,如果需要 訪問這些字符類型數據,我們必須先從指針變量 a 中取出其保存的地址。如下圖:?
?
- 在文件 1 中,編譯器分配 4 個 byte 空間,并命名為 p。同時 p 里保存了字符串常量“abcdefg” 的首字符的首地址。這個字符串常量本身保存在內存的靜態區,其內容不可更改。
- 在文件 2 中,編譯器認為 p 是一個數組,其大小為 4 個 byte,數組內保存的是 char 類型的數據。
- 在 文件 2 中使用 p 的過程如下圖:?
?
?指針數組和數組指針
- 初學者總是分不出指針數組與數組指針的區別。其實很好理解:
- 指針數組:首先它是一個數組,數組的元素都是指針,數組占多少個字節由數組本身 決定。它是“儲存指針的數組”的簡稱。
- 數組指針:首先它是一個指針,它指向一個數組。在 32 位系統下永遠是占 4 個字節, 至于它指向的數組占多少字節,不知道。它是“指向數組的指針”的簡稱。
- 下面到底哪個是數組指針,哪個是指針數組呢:
- A),int *p1[10];? ? ? ? ?指針數組
- B),int (*p2)[10];? ? ? ?數組指針
- 這里需要明白一個符號之間的優先級問題。 “[]”的優先級比“*”要高。p1 先與“[]”結合,構成一個數組的定義,數組名為 p1,int * 修飾的是數組的內容,即數組的每個元素。那現在我們清楚,這是一個數組,其包含 10 個 指向 int 類型數據的指針,即指針數組。
- 至于 p2 就更好理解了,在這里“()”的優先級比 “[]”高,“*”號和 p2 構成一個指針的定義,指針變量名為 p2,int 修飾的是數組的內容, 即數組的每個元素。數組在這里并沒有名字,是個匿名數組。那現在我們清楚 p2 是一個指 針,它指向一個包含 10 個 int 類型數據的數組,即數組指針。
- 我們可以借助下面的圖加深 理解:
- char a[5]={'A','B','C','D'};
- char (*p3)[5] = &a;? ? ?正確
- char (*p4)[5] = a;? ? ? ?需要類型轉換
- &a 是整個數組的首地址,a 是數組首元素的首地址,其值相同但意義不同
?
- ?根據上面的講解,&a+1 與 a+1 的區別已經清楚。
- ptr1:將&a+1 的值強制轉換成 int*類型,賦值給 int* 類型的變量 ptr,ptr1 肯定指到數 組 a 的下一個 int 類型數據了。ptr1[-1]被解析成*(ptr1-1),即 ptr1 往后退 4 個 byte。所以其 值為 0x4。
- ptr2:按照上面的講解,(int)a+1 的值是元素 a[0]的第二個字節的地址。然后把這個地址 強制轉換成 int*類型的值賦給 ptr2,也就是說*ptr2 的值應該為元素 a[0]的第二個字節開始的 連續4個byte的內容。
- 其內存布局如下圖:
?
?
- ?使用一個指針分配內存空間,復制數據
- 在運行 strcpy(str,”hello”)語句的時候發生錯誤。這時候觀察 str 的值,發現仍然為 NULL。 也就是說 str 本身并沒有改變,我們 malloc 的內存的地址并沒有賦給 str,而是賦給了_str。 而這個_str 是編譯器自動分配和回收的,我們根本就無法使用。所以想這樣獲取一塊內存是 不行的。那怎么辦?
- 兩個辦法: 使用return返回內存空間?
- 使用二級指針
?
- 注意,這里的參數是&str 而非 str。這樣的話傳遞過去的是 str 的地址,是一個值。在函 數內部,用鑰匙(“*”)來開鎖:*(&str),其值就是 str。所以 malloc 分配的內存地址是真正 賦值給了 str 本身。?
函數指針
- char * (*fun1)(char * p1,char * p2);? ?fun1 指針變量指向的是一個函數
?
- ?給函數指針賦值時,可以用&fun 或直接用函數名 fun。這是因為函數名被 編譯之后其實就是一個地址,所以這里兩種用法沒有本質的差別
*(int*)&p ----這是什么?

- void (*p)();? ? 這行代碼定義了一個指針變量 p,p 指向一個函數,這個函數的參數和返回值都是 void。
- &p 是求指針變量 p 本身的地址,這是一個 32 位的二進制常數(32 位系統)。
- (int*)&p 表示將地址強制轉換成指向 int 類型數據的指針。
- (int)Function 表示將函數的入口地址強制轉換成 int 類型的數據。
- *(int*)&p=(int)Function;表示將函數的入口地址賦值給指針變量 p。
- 那么(*p) ();就是表示對函數的調用。
- 其實函數指針與普通指針沒什么差別,只是指向的內容不同而已。 使用函數指針的好處在于,可以將實現同一功能的多個模塊統一起來標識,這樣一來更
容易后期的維護,系統結構更加清晰。或者歸納為:便于分層設計、利于系統抽象、降低耦 合度以及使接口與實現分開。
(*(void(*) ())0)();
- 第一步:void(*) (),可以明白這是一個函數指針類型。這個函數沒有參數,沒有返回值。
- 第二步:(void(*) ())0,這是將 0 強制轉換為函數指針類型,0 是一個地址,也就是說一個函數存在首地址為 0 的一段區域內。
- 第三步:(*(void(*) ())0),這是取 0 地址開始的一段內存里面的內容,其內容就是保存在首地址為 0 的一段區域內的函數。
- 第四步:(*(void(*) ())0)(),這是函數調用。
函數指針數組??char * (*pf[3])(char * p);
- 這是定義一個函數指針數組。它是一個數組,數組名為 pf,數組內存儲了 3 個指向函數的 指針。這些指針指向一些返回值類型為指向字符的指針、參數為一個指向字符的指針的函 數。這念起來似乎有點拗口。不過不要緊,關鍵是你明白這是一個指針數組,是數組
?
函數指針數組指針?char * (*(*pf)[3])(char * p);
- 注意,這里的pf和上一節的pf就完全是兩碼事了。上一節的pf并非指針,而是一個數組名; 這里的 pf 確實是實實在在的指針。這個指針指向一個包含了 3 個元素的數組;這個數字里 面存的是指向函數的指針;這些指針指向一些返回值類型為指向字符的指針、參數為一個 指向字符的指針的函數。
?
?