C語言指標安全及指標使用問題

codingwu發表於2014-09-11

指標是C語言的靈魂,同時也是最讓初學者頭痛的一個知識點,本文主要分項了C語言指標安全及指標使用問題。

指標的宣告與初始化

1、不恰當的指標宣告

考慮如下的宣告:

int* ptr1, ptr2; // ptr1為指標,ptr2為整數

正確的寫法如下:

int* ptr1, *ptr2;

用型別定義代替巨集定義是一個好的習慣,型別定義允許編譯器檢查作用域規則,而巨集定義不一定會。

使用巨集定義輔助宣告變數,如下所示:

#define PINT int*
PINT ptr1, ptr2;

不過結果和前面所說的一致,更好的方法是使用下面的型別定義:

typedef int* PINT;
PINT ptr1, ptr2;

2、使用指標未初始化

在使用指標之前未初始化會導致執行時錯誤,如下面的程式碼:

int* p;
...
printf("%d\n", *p);

指標p未被初始化,可能含有垃圾資料

3、處理未初始化指標

  • 總是用NULL來初始化指標
  • 用assert函式
  • 用第三方工具

把指標初始化為NULL更容易檢查是否使用正確,即便這樣,檢查空值也比較麻煩,如下所示:

int *pi = NULL;
...
if(pi == NULL) {
//不應該解引pi
} else {
//可以使用pi
}

我們可以使用assert函式來測試指標是否為空值:

assert(pi != NULL);

指標的使用問題

緩衝區溢位

緩衝區溢位是指當計算機向緩衝區內填充資料位數時超過了緩衝區本身的容量,使得溢位的資料覆蓋在合法資料上,理想的情況是程式檢查資料長度並不允許輸入超過緩衝區長度的字元,但是絕大多數程式都會假設資料長度總是與所分配的儲存空間相匹配,這就為緩衝區溢位埋下隱患。作業系統所使用的緩衝區又被稱為”堆疊”.。在各個操作程式之間,指令會被臨時儲存在”堆疊”當中,”堆疊”也會出現緩衝區溢位。

下面幾種情況可能導致緩衝區的溢位:

  • 訪問陣列元素時沒有檢查索引值
  • 對陣列指標做指標算術運算時不夠小心
  • 用gets這樣的函式從標準輸入讀取字串
  • 誤用strcpy和strcat這樣的函式

1、測試NULL

使用malloc這樣的函式的時候一定要檢查返回值,否則可能會導致程式的非正常終止,下面是一般的方法:

float *vector = malloc(20 * sizeof(float));
if(vector == NULL) {
    //malloc分配記憶體失敗
} else {
    //處理vector
}

2、錯誤使用解引操作

宣告和初始化指標的常用方法如下:

int num;
int *pi = #

下面是一種看似等價但是錯誤的宣告方法:

int num;
int *pi;
*pi = #

3、迷途指標

參見《C迷途指標

4、越過陣列邊界訪問記憶體

沒有什麼可以阻止程式訪問為陣列分配的空間以外的記憶體,下面的例子中,我們宣告並初始化了三個陣列來說明這種行為:

#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;
}

執行結果如下:

下圖說明了記憶體分配情況:

5、錯誤計算陣列長度

將陣列傳給函式時,一定要同時傳遞陣列長度,這個資訊幫助函式避免越過陣列邊界

#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;
}

6、錯誤使用sizeof操作符

其中一個例子是試圖檢查指標邊界但方法錯誤

#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);

7、有界指標

有界指標是指指標的使用被限制在有效的區域內,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平臺執行結果如下:

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

另一種方法是利用ANSI-C和C++的邊界檢查工具(CBMC)

相關文章