Linux中國

C 語言編程中的 5 個常見錯誤及對應解決方案

即使是最好的程序員也無法完全避免錯誤。這些錯誤可能會引入安全漏洞、導致程序崩潰或產生意外操作,具體影響要取決於程序的運行邏輯。

C 語言有時名聲不太好,因為它不像近期的編程語言(比如 Rust)那樣具有內存安全性。但是通過額外的代碼,一些最常見和嚴重的 C 語言錯誤是可以避免的。下文講解了可能影響應用程序的五個錯誤以及避免它們的方法:

1、未初始化的變數

程序啟動時,系統會為其分配一塊內存以供存儲數據。這意味著程序啟動時,變數將獲得內存中的一個隨機值。

有些編程環境會在程序啟動時特意將內存「清零」,因此每個變數都得以有初始的零值。程序中的變數都以零值作為初始值,聽上去是很不錯的。但是在 C 編程規範中,系統並不會初始化變數。

看一下這個使用了若干變數和兩個數組的示常式序:

#include <stdio.h>
#include <stdlib.h>

int
main()
{
  int i, j, k;
  int numbers[5];
  int *array;

  puts("These variables are not initialized:");

  printf("  i = %dn", i);
  printf("  j = %dn", j);
  printf("  k = %dn", k);

  puts("This array is not initialized:");

  for (i = 0; i < 5; i++) {
    printf("  numbers[%d] = %dn", i, numbers[i]);
  }

  puts("malloc an array ...");
  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("This malloc&apos;ed array is not initialized:");

    for (i = 0; i < 5; i++) {
      printf("  array[%d] = %dn", i, array[i]);
    }

    free(array);
  }

  /* done */

  puts("Ok");
  return 0;
}

這個程序不會初始化變數,所以變數以系統內存中的隨機值作為初始值。在我的 Linux 系統上編譯和運行這個程序,會看到一些變數恰巧有「零」值,但其他變數並沒有:

These variables are not initialized:
  i = 0
  j = 0
  k = 32766
This array is not initialized:
  numbers[0] = 0
  numbers[1] = 0
  numbers[2] = 4199024
  numbers[3] = 0
  numbers[4] = 0
malloc an array ...
This malloc&apos;ed array is not initialized:
  array[0] = 0
  array[1] = 0
  array[2] = 0
  array[3] = 0
  array[4] = 0
Ok

很幸運,ij 變數是從零值開始的,但 k 的起始值為 32766。在 numbers 數組中,大多數元素也恰好從零值開始,只有第三個元素的初始值為 4199024。

在不同的系統上編譯相同的程序,可以進一步顯示未初始化變數的危險性。不要誤以為「全世界都在運行 Linux」,你的程序很可能某天在其他平台上運行。例如,下面是在 FreeDOS 上運行相同程序的結果:

These variables are not initialized:
  i = 0
  j = 1074
  k = 3120
This array is not initialized:
  numbers[0] = 3106
  numbers[1] = 1224
  numbers[2] = 784
  numbers[3] = 2926
  numbers[4] = 1224
malloc an array ...
This malloc&apos;ed array is not initialized:
  array[0] = 3136
  array[1] = 3136
  array[2] = 14499
  array[3] = -5886
  array[4] = 219
Ok

永遠都要記得初始化程序的變數。如果你想讓變數將以零值作為初始值,請額外添加代碼將零分配給該變數。預先編好這些額外的代碼,這會有助於減少日後讓人頭疼的調試過程。

2、數組越界

C 語言中,數組索引從零開始。這意味著對於長度為 10 的數組,索引是從 0 到 9;長度為 1000 的數組,索引則是從 0 到 999。

程序員有時會忘記這一點,他們從索引 1 開始引用數組,產生了 「大小差一」 off by one 錯誤。在長度為 5 的數組中,程序員在索引「5」處使用的值,實際上並不是數組的第 5 個元素。相反,它是內存中的一些其他值,根本與此數組無關。

這是一個數組越界的示常式序。該程序使用了一個只含有 5 個元素的數組,但卻引用了該範圍之外的數組元素:

#include <stdio.h>
#include <stdlib.h>

int
main()
{
  int i;
  int numbers[5];
  int *array;

  /* test 1 */

  puts("This array has five elements (0 to 4)");

  /* initalize the array */
  for (i = 0; i < 5; i++) {
    numbers[i] = i;
  }

  /* oops, this goes beyond the array bounds: */
  for (i = 0; i < 10; i++) {
    printf("  numbers[%d] = %dn", i, numbers[i]);
  }

  /* test 2 */

  puts("malloc an array ...");

  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("This malloc&apos;ed array also has five elements (0 to 4)");

    /* initalize the array */
    for (i = 0; i < 5; i++) {
      array[i] = i;
    }

    /* oops, this goes beyond the array bounds: */
    for (i = 0; i < 10; i++) {
      printf("  array[%d] = %dn", i, array[i]);
    }

    free(array);
  }

  /* done */

  puts("Ok");
  return 0;
}

可以看到,程序初始化了數組的所有值(從索引 0 到 4),然後從索引 0 開始讀取,結尾是索引 9 而不是索引 4。前五個值是正確的,再後面的值會讓你不知所以:

This array has five elements (0 to 4)
  numbers[0] = 0
  numbers[1] = 1
  numbers[2] = 2
  numbers[3] = 3
  numbers[4] = 4
  numbers[5] = 0
  numbers[6] = 4198512
  numbers[7] = 0
  numbers[8] = 1326609712
  numbers[9] = 32764
malloc an array ...
This malloc&apos;ed array also has five elements (0 to 4)
  array[0] = 0
  array[1] = 1
  array[2] = 2
  array[3] = 3
  array[4] = 4
  array[5] = 0
  array[6] = 133441
  array[7] = 0
  array[8] = 0
  array[9] = 0
Ok

引用數組時,始終要記得追蹤數組大小。將數組大小存儲在變數中;不要對數組大小進行 硬編碼 hard-code 。否則,如果後期該標識符指向另一個不同大小的數組,卻忘記更改硬編碼的數組長度時,程序就可能會發生數組越界。

3、字元串溢出

字元串只是特定類型的數組。在 C 語言中,字元串是一個由 char 類型值組成的數組,其中用一個零字元表示字元串的結尾。

因此,與數組一樣,要注意避免超出字元串的範圍。有時也稱之為 字元串溢出

使用 gets 函數讀取數據是一種很容易發生字元串溢出的行為方式。gets 函數非常危險,因為它不知道在一個字元串中可以存儲多少數據,只會機械地從用戶那裡讀取數據。如果用戶輸入像 foo 這樣的短字元串,不會發生意外;但是當用戶輸入的值超過字元串長度時,後果可能是災難性的。

下面是一個使用 gets 函數讀取城市名稱的示常式序。在這個程序中,我還添加了一些未使用的變數,來展示字元串溢出對其他數據的影響:

#include <stdio.h>
#include <string.h>

int
main()
{
  char name[10];                       /* Such as "Chicago" */
  int var1 = 1, var2 = 2;

  /* show initial values */

  printf("var1 = %d; var2 = %dn", var1, var2);

  /* this is bad .. please don&apos;t use gets */

  puts("Where do you live?");
  gets(name);

  /* show ending values */

  printf("<%s> is length %dn", name, strlen(name));
  printf("var1 = %d; var2 = %dn", var1, var2);

  /* done */

  puts("Ok");
  return 0;
}

當你測試類似的短城市名稱時,該程序運行良好,例如伊利諾伊州的 Chicago 或北卡羅來納州的Raleigh

var1 = 1; var2 = 2
Where do you live?
Raleigh
<Raleigh> is length 7
var1 = 1; var2 = 2
Ok

威爾士的小鎮 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 有著世界上最長的名字之一。這個字元串有 58 個字元,遠遠超出了 name 變數中保留的 10 個字元。結果,程序將值存儲在內存的其他區域,覆蓋了 var1var2 的值:

var1 = 1; var2 = 2
Where do you live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
<Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58
var1 = 2036821625; var2 = 2003266668
Ok
Segmentation fault (core dumped)

在運行結束之前,程序會用長字元串覆蓋內存的其他部分區域。注意,var1var2 的值不再是起始的 12

避免使用 gets 函數,改用更安全的方法來讀取用戶數據。例如,getline 函數會分配足夠的內存來存儲用戶輸入,因此不會因輸入長值而發生意外的字元串溢出。

4、重複釋放內存

「分配的內存要手動釋放」是良好的 C 語言編程原則之一。程序可以使用 malloc 函數為數組和字元串分配內存,該函數會開闢一塊內存,並返回一個指向內存中起始地址的指針。之後,程序可以使用 free 函數釋放內存,該函數會使用指針將內存標記為未使用。

但是,你應該只使用一次 free 函數。第二次調用 free 會導致意外的後果,可能會毀掉你的程序。下面是一個針對此點的簡短示常式序。程序分配了內存,然後立即釋放了它。但為了模仿一個健忘但有條理的程序員,我在程序結束時又一次釋放了內存,導致兩次釋放了相同的內存:

#include <stdio.h>
#include <stdlib.h>

int
main()
{
  int *array;

  puts("malloc an array ...");

  array = malloc(sizeof(int) * 5);

  if (array) {
    puts("malloc succeeded");

    puts("Free the array...");
    free(array);
  }

  puts("Free the array...");
  free(array);

  puts("Ok");
}

運行這個程序會導致第二次使用 free 函數時出現戲劇性的失敗:

malloc an array ...
malloc succeeded
Free the array...
Free the array...
free(): double free detected in tcache 2
Aborted (core dumped)

要記得避免在數組或字元串上多次調用 free。將 mallocfree 函數定位在同一個函數中,這是避免重複釋放內存的一種方法。

例如,一個紙牌遊戲程序可能會在主函數中為一副牌分配內存,然後在其他函數中使用這副牌來玩遊戲。記得在主函數,而不是其他函數中釋放內存。將 mallocfree 語句放在一起有助於避免多次釋放內存。

5、使用無效的文件指針

文件是一種便捷的數據存儲方式。例如,你可以將程序的配置數據存儲在 config.dat 文件中。Bash shell 會從用戶家目錄中的 .bash_profile 讀取初始化腳本。GNU Emacs 編輯器會尋找文件 .emacs 以從中確定起始值。而 Zoom 會議客戶端使用 zoomus.conf 文件讀取其程序配置。

所以,從文件中讀取數據的能力幾乎對所有程序都很重要。但是假如要讀取的文件不存在,會發生什麼呢?

在 C 語言中讀取文件,首先要用 fopen 函數打開文件,該函數會返回指向文件的流指針。你可以結合其他函數,使用這個指針來讀取數據,例如 fgetc 會逐個字元地讀取文件。

如果要讀取的文件不存在或程序沒有讀取許可權,fopen 函數會返回 NULL 作為文件指針,這表示文件指針無效。但是這裡有一個示常式序,它機械地直接去讀取文件,不檢查 fopen 是否返回了 NULL

#include <stdio.h>

int
main()
{
  FILE *pfile;
  int ch;

  puts("Open the FILE.TXT file ...");

  pfile = fopen("FILE.TXT", "r");

  /* you should check if the file pointer is valid, but we skipped that */

  puts("Now display the contents of FILE.TXT ...");

  while ((ch = fgetc(pfile)) != EOF) {
    printf("<%c>", ch);
  }

  fclose(pfile);

  /* done */

  puts("Ok");
  return 0;
}

當你運行這個程序時,第一次調用 fgetc 會失敗,程序會立即中止:

Open the FILE.TXT file ...
Now display the contents of FILE.TXT ...
Segmentation fault (core dumped)

始終檢查文件指針以確保其有效。例如,在調用 fopen 打開一個文件後,用類似 if (pfile != NULL) 的語句檢查指針,以確保指針是可以使用的。

人都會犯錯,最優秀的程序員也會產生編程錯誤。但是,遵循上面這些準則,添加一些額外的代碼來檢查這五種類型的錯誤,就可以避免最嚴重的 C 語言編程錯誤。提前編寫幾行代碼來捕獲這些錯誤,可能會幫你節省數小時的調試時間。

via: https://opensource.com/article/21/10/programming-bugs

作者:Jim Hall 選題:lujun9972 譯者:unigeorge 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國