指針是C語言的靈魂,同時也是最讓初學者頭痛的一個知識點,本文主要分項了C語言指針安全及指針使用問題。
考慮如下的聲明:
int* ptr1, ptr2; // ptr1為指針,ptr2為整數
正確的寫法如下:
int* ptr1, *ptr2;
用類型定義代替宏定義是一個好的習慣,類型定義允許編譯器檢查作用域規則,而宏定義不一定會。
使用宏定義輔助聲明變量,如下所示:
#define PINT int* PINT ptr1, ptr2;
不過結果和前面所說的一致,更好的方法是使用下面的類型定義:
typedef int* PINT; PINT ptr1, ptr2;
在使用指針之前未初始化會導致運行時錯誤,如下面的代碼:
int* p; ... printf("%d\n", *p);
指針p未被初始化,可能含有垃圾數據
把指針初始化為NULL更容易檢查是否使用正確,即便這樣,檢查空值也比較麻煩,如下所示:
int *pi = NULL; ... if(pi == NULL) { //不應該解引pi } else { //可以使用pi }
我們可以使用assert函數來測試指針是否為空值:
assert(pi != NULL);
緩沖區溢出
緩沖區溢出是指當計算機向緩沖區內填充數據位數時超過了緩沖區本身的容量,使得溢出的數據覆蓋在合法數據上,理想的情況是程序檢查數據長度並不允許輸入超過緩沖區長度的字符,但是絕大多數程序都會假設數據長度總是與所分配的儲存空間相匹配,這就為緩沖區溢出埋下隱患。操作系統所使用的緩沖區又被稱為”堆棧”.。在各個操作進程之間,指令會被臨時儲存在”堆棧”當中,”堆棧”也會出現緩沖區溢出。
下面幾種情況可能導致緩沖區的溢出:
使用malloc這樣的函數的時候一定要檢查返回值,否則可能會導致程序的非正常終止,下面是一般的方法:
float *vector = malloc(20 * sizeof(float)); if(vector == NULL) { //malloc分配內存失敗 } else { //處理vector }
聲明和初始化指針的常用方法如下:
int num; int *pi = #
下面是一種看似等價但是錯誤的聲明方法:
int num; int *pi; *pi = #
參見《C迷途指針》
沒有什麼可以阻止程序訪問為數組分配的空間以外的內存,下面的例子中,我們聲明並初始化了三個數組來說明這種行為:
#include<stdio.h> int main() { char firstName[8] = "1234567"; char middleName[8] = "1234567"; char lastName[8] = "1234567"; middleName[-2] = 'X'; middleName[0] = 'X'; middleName[10] = 'X'; printf("%p %s\n", firstName, firstName); printf("%p %s\n", middleName, middleName); printf("%p %s\n", lastName, lastName); return 0; }
運行結果如下:
下圖說明了內存分配情況:
將數組傳給函數時,一定要同時傳遞數組長度,這個信息幫助函數避免越過數組邊界
#include<stdio.h> void replace(char buffer[], char replacement, size_t size) { size_t count = 0; while(*buffer && count++ < size) { *buffer = replacement; buffer++; } } int main() { char name[8]; strcpy(name, "Alexander"); replace(name, '+', sizeof(name)); printf("%s\n", name); return 0; }
其中一個例子是試圖檢查指針邊界但方法錯誤
#include<stdio.h> int main() { int buffer[20]; int *pbuffer = buffer; for(int i = 0; i < sizeof(buffer); i++) { *(pbuffer++) = 0; } return 0; }
改為:i < sizeof(buffer) / sizeof(int);
有界指針是指指針的使用被限制在有效的區域內,C沒有對這類指針提供直接的支持,但是可以自己顯示地確保。如下所示:
#define SIZE 32 char name[SIZE]; char *p = name; if(name != NULL) { if(p >= name && p < name + SIZE) { //有效指針,繼續 } else { //無效指針,錯誤分支 } }
一種有趣的變化是創建一個指針檢驗函數;
下面的代碼定義一個函數消除無效指針:
int valid(void *ptr) { return (ptr != NULL); }
下面的代碼依賴於_etext的地址,定義於很多的類linux操作系統,在windows上無效:
#include <stdio.h> #include <stdlib.h> int valid(void *p) { extern char _etext; return (p != NULL) && ((char*) p > &_etext); } int global; int main(void) { int local; printf("pointer to local var valid? %d\n", valid(&local)); printf("pointer to static var valid? %d\n", valid(&global)); printf("pointer to function valid? %d\n", valid((void *)main)); int *p = (int *) malloc(sizeof(int)); printf("pointer to heap valid? %d\n", valid(p)); printf("pointer to end of allocated heap valid? %d\n", valid(++p)); free(--p); printf("pointer to freed heap valid? %d\n", valid(p)); printf("null pointer valid? %d\n", valid(NULL)); return 0; }
在linux平台運行結果如下:
另一種方法是利用ANSI-C和C++的邊界檢查工具(CBMC)  pointer to local var valid? 1
pointer to static var valid? 1
pointer to function valid? 0
pointer to heap valid? 1
pointer to end of allocated heap valid? 1
pointer to freed heap valid? 1
null pointer valid? 0