深入理解C指針
第1章 認識指針
理解指針的關鍵在於理解C程序如何管理內存,指針包含的就是內存地址。
1.1 指針和內存
C程序在編譯后,以三種方式使用內存:
1. 靜態、全局內存
在程序開始運行時分配,直到程序終止才消失。所有函數都能訪問全局變量,靜態變量的作用域則局限在定義它們的函數內部。
2. 自動變量
在函數內部聲明,在函數被調用時才創建。作用域局限於函數內部,而且生命周期局限在函數的執行時間內。
3. 動態內存
動態內存分配在堆中,可根據需要釋放,直到釋放才消失。指針引用分配的內存,作用域局限於引用內存的指針。

不同內存中變量的作用域和生命周期
內存類型 | 作用域 | 生命周期 |
全局內存 | 整個文件 | 應用程序的生命周期 |
靜態內存 | 聲明它的函數內部 | 應用程序的生命周期 |
自動內存(局部內存) | 聲明它的函數內部 | 限制在函數執行時間內 |
動態內存 | 由引用該內存的指針決定 | 直到內存釋放 |
第2章 C的動態內存管理
內存管理:
自動變量,內存在它所處的函數的棧幀上;
靜態或全局變量,內存處於程序的數據段,會被自動清零;
注:
C99引入了變長數組(VLA)。數組長度不是在編譯時確定,而是在運行時確定。不過,數組一旦創建出來就不能再改變長度了。
動態內存管理:
用分配和釋放函數手動實現。
2.1 動態內存分配
動態分配內存的基本步驟:
1) 用malloc類的函數分配內存
2) 用這些內存支持應用程序
3) 用free函數釋放內存
int *pi = (int *)malloc(sizeof(int) * 1); *pi = 5; ... free(pi);
malloc 函數的參數指定要分配的字節數。
如果成功,返回從堆上分配的內存的指針。
如果失敗,返回空指針。
注:
每次調用malloc (或類似函數),程序結束時必須有對應的free函數調用,以防止內存泄漏。
內存被釋放后,就不應該再訪問了,為防止這種錯誤,通常的做法是把被釋放的指針賦值為NULL。
分配內存時,堆管理器維護的數據結構中會保存額外的信息,包括塊大小和其他信息,通常放在緊挨分配塊的內存中。如果寫入操作超出了動態分配的內存塊,即越界寫入,數據結構會被破壞。
內存泄漏
如果不再使用已分配的內存,卻沒有將其釋放,就會發生內存泄漏,導致內存泄漏的情況可能如下:
1) 丟失內存地址
2) 應該調用free函數卻沒有調用(隱式泄漏)
1. 丟失地址
在需要進行指針偏移的地方,原來指向動態分配內存的指針發生了偏移,丟失內存塊的起始地址。
2. 隱式內存泄漏
如果程序應該釋放內存,而實際卻沒有釋放,會發生內存泄漏。
在釋放用struct關鍵字創建的結構體時,可能發生內存泄漏。如果結構體包含指向動態分配的內存的指針,就需要在釋放結構體之前先釋放這些指針。
2.2 動態內存分配函數
2.3 用free函數釋放內存
2.4 迷途指針
2.5 動態內存分配技術
2.6 小結
第3章 指針和函數
使用函數時,兩種情況指針很有用:
1. 將指針傳遞給函數
這時函數可以修改指針所引用的數據,也可以更高效地傳遞大塊信息。
2. 聲明函數指針
函數表示法就是指針表示法,函數名字經過求值會變成函數的地址,然后函數參數被傳遞給函數,函數指針可以很好地控制程序的執行流。
3.1 程序的棧和堆
局部變量,也稱為自動變量,被分配在
棧幀上。
1. 程序棧
程序棧存放棧幀,棧幀存放函數參數和局部變量。堆管理動態內存。
棧在下部,向上長出,棧幀在數據彈出后,內存並不會被清理。
堆在上部,向下生長,碎片。
2. 棧幀的組織
系統在創建棧幀時,將函數參數以跟聲明時相反的順序推到幀上,接下來推入函數調用的返回地址,最后推入局部變量。
每個程序都有自己的程序棧。一個或多個線程訪問內存中同一個對象可能會導致沖突。
3.2 通過指針傳遞和返回數據
傳遞參數(包括指針)時,傳遞的是它們的值。即,傳遞給函數的是參數值的一個副本。
傳遞對象的指針意味着不需要復制對象,但可以通過指針訪問對象。
1. 用指針傳遞數據
一個主要原因是函數可以修改數據。
void swap(int *num1, int *num2) { int tmp; tmp = *num1; *num1 = *num2; *num2 = tmp; } swap(&data1, &data2);
2. 用值傳遞數據
在函數中修改傳遞的數據,不會影響函數外的數據,因為在函數中修改的是形參,修改形參不會影響實參。
3. 傳遞指向常量的指針
傳遞指向常量的指針,效率很高,因為只傳遞了數據的地址,能避免某些情況下復制大量內存。傳遞指針,數據就能夠被修改,如果不希望數據被修改,就要傳遞指向常量的指針。
4. 返回指針
從函數返回對象時,有兩種:
1. 使用malloc在函數內部分配內存並返回其地址。調用者負責釋放返回的內存。
2. 傳遞一個對象給函數並讓函數修改它。
用函數為整數數組分配內存:
1 int *mallocArray(int size, int value) 2 { 3 int i = 0; 4 int *arr = (int *)malloc(size * sizeof(int)); 5 for (i=0; i<size; i++) 6 { 7 arr[i] = value; 8 } 9 10 return arr; 11 } 12 13 int *vector = mallocArray(5, 0); 14 ... 15 free(vector);
從函數返回指針時可能存在幾個潛在問題:
返回未初始化的指針;
返回指向無效地址的指針;
返回局部變量的指針;
返回指針但是沒有釋放內存。
從函數返回動態分配的內存意味着函數的調用者有責任釋放內存,否則就會產生內存泄漏。
5. 局部數據指針
函數返回指向局部數據的指針,一旦函數返回,局部數據空間就被釋放,在函數外使用會發生錯誤。
把變量聲明為static,會把變量的作用域限制在函數內部,分配在棧幀外面,避免其它函數覆寫變量值。不過每次調用該函數就會重復利用該變量,會覆蓋上個數據。
6. 傳遞空指針
將指針傳遞給函數時,使用之前先判斷它是否為空是個好習慣。
7. 傳遞指針的指針
將指針傳遞給函數時,傳遞的是值。如果要修改原指針而不是指針的副本,就需要傳遞指針的指針。
實現自己的free()函數:
free函數不會檢查傳入的指針是否是NULL,也不會在返回前把指針置為NULL。
釋放指針之后將其置為NULL是個好習慣。
1 void saferFree(void **pp) //調用時需要將指針類型顯示地強制轉換為void 2 { 3 if (pp != NULL && *pp != NULL) 4 { 5 free(*pp); 6 *pp = NULL; 7 } 8 } 9 10 #define saferFree(p) saferFree((void **)&p)
3.3 函數指針
函數指針是持有函數地址的指針。為以編譯時未確定的順序執行函數提供了方便,不需要使用條件語句。
1. 聲明函數指針
void (*foo)();

第一個括號讓這個聲明變成了一個名為foo的函數指針。星號表示這是個指針。
注:
使用函數指針時一定要小心,因為C不會檢查參數傳遞是否正確。
對函數指針在命名約定上建議用 fptr 做前綴。
2. 使用函數指針
為函數指針聲明一個類型定義。
1 typedef int (*funcptr)(int); 2 int square(int, int); 3 ... 4 5 funcptr fptr; 6 fptr = square; 7 printf("%d squared is %d\n", n, fptr(n));
3. 傳遞函數指針
1 #include <stdio.h> 2 3 int sum(int num1, int num2) 4 { 5 return num1 + num2; 6 } 7 8 int sub(int num1, int num2) 9 { 10 return num1 - num2; 11 } 12 13 typedef (*fptrOperation)(int, int); 14 15 int computer(fptrOperation operation, int num1, int num2) 16 { 17 printf("num1 = %d num2 = %d resule = %d\n", num1, num2, operation(num1, num2)); 18 return ; 19 } 20 21 int main(int argc, char *argv[]) 22 { 23 int num1; 24 int num2; 25 26 printf("Please input two numbers:\n"); 27 scanf("%d %d", &num1, &num2); 28 29 computer(sum, num1, num2); 30 computer(sub, num1, num2); 31 32 return 0; 33 }
4. 返回函數指針
返回函數指針需要把函數的返回類型聲明為函數指針。
1 #include <stdio.h> 2 3 int sum(int num1, int num2) 4 { 5 return num1 + num2; 6 } 7 8 int sub(int num1, int num2) 9 { 10 return num1 - num2; 11 } 12 13 typedef (*fptrOperation)(int, int); 14 15 fptrOperation select(char opcode) 16 { 17 switch(opcode) 18 { 19 case '+': 20 return sum; 21 case '-': 22 return sub; 23 default: 24 printf("Wrong operation code!\n"); 25 break; 26 } 27 } 28 29 int evaluate(char opcode, int num1, int num2) 30 { 31 fptrOperation operation = select(opcode); 32 return operation(num1, num2); 33 } 34 35 int main(int argc, char *argv[]) 36 { 37 int num1; 38 int num2; 39 40 printf("Please input two numbers:\n"); 41 scanf("%d %d", &num1, &num2); 42 43 printf("num1 = %d num2 = %d resule = %d\n", num1, num2, evaluate('+', num1, num2)); 44 printf("num1 = %d num2 = %d resule = %d\n", num1, num2, evaluate('-', num1, num2)); 45 46 return 0; 47 }
5. 使用函數指針數組
定義及初始化:
typedef int (*operation)(int, int); operation operations[128] = {NULL}; int (*operations[128])(int, int) = {NULL};
6. 比較函數指針
可以用相等和不等操作符來比較函數指針。
fptrOperation fptr = add; if (fptr == add) { ...; }
7. 轉換函數指針
可以將指向某個函數的指針轉換為其他類型的指針,不過要謹慎使用,因為運行時系統不會驗證函數指針所用的參數是否正確。轉換后函數指針的長度不一定相同。
注:
無法保證函數指針和數據指針相互轉換后能正常工作。
void * 指針不一定能用在函數指針上。
一定要確保給函數指針傳遞正確的參數。
3.4 小結
第4章 指針和數組
常見的錯誤觀點:數組和指針是可以完全互換的。 錯誤!!!
數組使用自身的名字可以返回數組地址,但是名字本身不能作為賦值操作的目標。
聲明數組時需要指定該數組有多大,有多少個元素。數組索引從0開始,到聲明的長度-1結束。用無效的索引訪問數組會造成不可預期的行為。
數組元素個數 = 數組長度 除以 元素長度(size = sizeof(array) / sizeof(array[0])))。
n = sizeof(array) / sizeof(array[0]); //或者 n = sizeof(array) / sizeof(int);
第6章 指針和結構體
6.4 用指針支持數據結構
2. 用指針支持隊列
用鏈表實現隊列
先進先出(FIFO),第一個進入隊列的元素,第一個被取出。
最常見操作:入隊,出隊
3. 用指針支持棧
用鏈表實現棧
先進后出(FILO),第一個進入棧的元素,最后一個被取出。
最常見操作:入棧,出棧
4. 用指針支持樹
用鏈表實現二叉樹
子節點連接到父節點,從整體上看就像一顆倒過來的樹,根節點表示這種數據結構的開始元素。
常見的二叉樹,每個節點可能有0、1、2個子節點,子節點要么是左節點,要么是右節點,
二叉查找樹,插入新節點后,這個節點的所有左子節點的值都比父節點小,所有右子節點的值都比父節點大。

typedef struct _tree { void *data; struct _tree *left; struct _tree *right; } TreeNode;
遍歷二叉樹的方式有三種:前序、中序、后序。
第7章 安全問題和指針誤用
7.1 處理未初始化指針
int *pi = NULL; ... if (pi == NULL) { //空指針 } else { //指針可以使用 }
可以用assert()函數測試指針是否為空值,如果表達式為真,什么也不會發生,如果表達式為假,程序會終止,即指針為空程序會終止。
assert(pi != NULL);
//如果指針為空值,輸出:
Assertion failed: pi != NULL
7.2 指針的使用問題
可能導致緩沖區溢出的幾種情況:
- 訪問數組元素時沒有檢查索引值
- 對數組指針做指針算術運算時不夠小心
- 用gets這樣的函數從標准輸入讀取字符串
- 誤用strcpy和strcat這樣的函數
用malloc這類函數時一定要檢查返回值,否則可能會導致程序非正常終止。
int *vector = malloc(20 * sizeof(int));
if (vector == NULL)
{
//malloc分配內存失敗
}
else
{
//處理vector
}
3. 迷途指針
釋放指針后仍然在引用原來的內存。
4. 越過數組邊界訪問內存。
5. 錯誤計算數組長度
將數組傳遞給函數時,一定要同時傳遞數組長度,有助於函數避免越過數組邊界。
6. 錯誤使用sizeof操作符。
sizeof(int) = 4;
sizeof(char) = 1;
//易搞混
//常用 sizeof(buffer) / sizeof(int) 來代替buffer元素的個數
7. 要匹配指針類型
用合適的指針類型來裝數據,再指針類型強制轉換時一定要注意。
8. 有界指針
有界指針是指指針的使用被限制在有效的區域內。如,禁止對數組使用的指針訪問數組前面或后面的任何內存。
9. 字符串
字符串相關的安全問題,一般發生在越過字符串末尾寫入的情況。
使用strcpy和strcat這類字符串函數,容易引發緩沖區溢出。
printf、fprintf、snprintf、syslog這些函數都接受格式化字符串作為參數,避免這類攻擊的一種簡單方法是永遠不要把用戶提供的格式化字符串傳遞給這些函數。
10. 指針算術運算和結構體
應該只對數組使用指針算術運算,因為數組肯定分配在連續的內存塊上,指針算術運算可以得到有效的偏移量。不應該對結構體使用指針算術運算,因為結構體的字段可能分配在不連續的內存區域。
在結構體中盡量不要使用指針算術運算。
11. 函數指針問題
函數和函數指針用來控制程序的執行順序。
只用函數名本身,而不加函數名后的括號時,調用的是函數的地址。
函數指針可以執行不同的函數,這取決於分配給它的地址。
7.3 內存釋放問題
1. 重復釋放
避免將同一塊內存釋放多次的簡單方法是,釋放指針后總是將其置為NULL。
char *name = (char *)malloc(...);
...
free(name);
name = NULL;
2. 清除敏感數據
當應用程序終止后,大部分操作系統都不會把用到的內存清空或者執行別的操作。系統可能會將之前用過的寫有敏感數據的空間分配給別的程序,別的程序就能訪問內存中的敏感數據。
應該在不需要使用內存時,將內存清空。
char name[32];
int userID;
char *string = (char *)malloc(...);
...
//刪除敏感信息
memset(name, 0, sizeof(name));
userID = 0;
memset(string, 0, sizeof(string));
free(string);
string = NULL;
7.4 使用靜態分析工具
可以使用GCC編譯器的-Wall選項啟用編譯器警告。
7.5 總結
指針影響程序的安全性和可靠性的問題,基本上都是圍繞聲明和初始化指針、使用指針和釋放內存組織的。
第8章 重要內容
8.1 轉換指針
兩種常見的字節序:大字節序和小字節序,即大小端。
大字節序:將低位字節存儲在低地址中;
小字節序:將高位字節存儲在低地址中。
1. 訪問特殊用途的地址
底層內核需要訪問地址0,有幾種方法:
- 把指針置為0(可能被識別為NULL)
- 把整數置為0,再把這個整數轉換為指針
- 聯合體
- 用memset()函數把指針置為0
memset((void *)&ptr, 0, sizeof(ptr));
2. 訪問端口
端口即硬件概念
如何用指針訪問端口:
#define PORT 0xB0000000;
unsigned int volatile * const port = (unsigned int *)PORT;
*port = 0x0001;
value = *port;
機器使用十六進制地址表示端口,將數據作為無符號整數處理。
volatile關鍵字可以阻止運行時系統使用寄存器暫存端口值,每次訪問端口都需要系統讀寫端口,而不是從寄存器讀取一個舊值。
4. 判斷機器的字節序
可以使用類型轉換操作來判斷架構的字節序。
字節序:內存單元中字節存儲的順序。
把整數的地址從指針轉換為char,可以判斷字節序:
int num = 0x12345678;
char *pc = (char *)#
for (i = 0; i < 4; i++)
{
printf("%p: %02x \n", pc, (unsigned char) *pc++);
}
8.2 別名、強別名和restrict關鍵字
別名:如果兩個指針引用同一內存地址,稱一個指針是另一個指針的別名。
強別名:是另一種別名,它不允許一種類型的指針成為另一種類型的指針的別名。
為了避免別名問題,可以采用以下方法:
使用聯合體
關閉強別名
使用char指針
GCC編譯器的編譯器選項:
-fno-strict-aliasing 關閉強別名
-fstrict-aliasing 打開強別名
-Wstrict-aliasing 打開跟強別名相關的警告信息
編譯器總是假定char指針是任意對象的潛在別名。
1. 用聯合體以多種方式表示值
C語言有時候需要把一種類型轉換為另一種類型,一般通過類型轉換實現。也可以使用聯合體。
2. 強別名
即使兩個結構體的字段完全一樣,但如果名字不同的話,這兩種結構體的指針就不應該引用同一對象。不過,如果定義了同一個結構體的兩種類型,那么指向不同名字的指針可以引用同一對象。