

【文章摘要】 C 語言作為許多現代作業系統與底層應用的核心,是程式設計學習者的必修基石。然而,其高自由度也伴隨著許多初學者極易掉入的陷阱。本文轉錄自 PTT C_and_CPP 板的經典文章《C 語言新手十誡(增修一版)》,由資深開發者 Khoguan Phuann 整理,詳細列出十條開發時必須嚴格遵守的編程「誡律」,包含變數初始化、記憶體管理、指標安全操作等,並附上詳盡的代碼範例對比,是每一位 C 語言新手必備的防錯指南。
作者: khoguan (Khoguan Phuann)
看板: C_and_CPP
標題: C 語言新手十誡(增修一版)
請注意:
- 本篇旨在提醒新手,避免初學常犯的錯誤(其實老手也常犯 😅)。但不能取代完整的學習,請務必研讀一兩本 C 語言的好書,並多多實作練習。
- 強烈建議新手先看過此文再發問,您的問題極可能在此文已經提出並解答了。
- 以下所舉的錯誤例子如果在您的電腦上印出和正確例子相同的結果,那只是不足為恃的一時僥倖。
- 不守十誡者,輕則執行結果的輸出數據錯誤,或是程式當掉,重則可能導致嚴重的系統崩潰或記憶體洩漏。
一、你不可以使用尚未給予適當初值的變數。
在 C 語言中,區域變數(local variables)宣告後如果沒有賦予初值,其記憶體空間內存放的數值將會是隨機的「垃圾值」,直接使用會導致計算錯誤。
// ❌ 錯誤例子:區域變數 sum 未初始化
int accumulate(int max)
{
int sum; // 其內容值是垃圾
int num;
for (num = 1; num <= max; num++) {
sum += num;
}
return sum;
}
// 正確例子:正確賦予初值為 0
int accumulate(int max)
{
int sum = 0;
int num;
for (num = 1; num <= max; num++) {
sum += num;
}
return sum;
}
二、你不可以存取超過陣列既定範圍的空間。
宣告陣列時,所給的陣列元素個數值如果是 N,那麼我們在後面透過索引值存取元素時,索引值範圍是從 0 到 N-1。C/C++ 為了效率,並不會自動檢查陣列索引是否越界,我們必須自己寫程式確保安全。
// ❌ 錯誤例子:索引越界存取到 str[5]
int str[5];
int i;
for (i = 0; i <= 5; i++) {
str[i] = i;
}
// 正確例子:索引限制在 0 至 4 之間
int str[5];
int i;
for (i = 0; i < 5; i++) {
str[i] = i;
}
三、你不可以提取 (dereference) 不知指向何方的指標(包含 null 指標)。
指標變數必須先指向一個明確的、合法的記憶體空間(變數位址或動態分配空間),才可以進行取值或寫入操作。
// ❌ 錯誤例子:操作未初始化指標或空指標
char *pc1; // 未給予初值,不知指向何方
char *pc2 = 0; // pc2 起始化為 null pointer
*pc1 = 'a'; // 將 'a' 寫到不知何方,危險!
*pc2 = 'b'; // 將 'b' 寫到「位址0」,程式會 crash
// 正確例子:將指標指向合法記憶體
char c;
char *pc1 = &c; // pc1 指向字元變數 c
char *pc2 = (char *)malloc(10); // 動態分配 10 個 char
*pc1 = 'a'; // c 的內容變為 'a'
pc2[0] = 'b'; // 動態配置的空間寫入資料
free(pc2); // 最後記得 free 掉分配的空間
四、你不可以將字串常數賦值給 char* 變數,然後修改其內容。
字串常數(例如 "john")的內容在編譯後是儲存在唯讀記憶體區段的,只能讀不能寫。要修改字串,應該將其宣告為字元陣列。
// ❌ 錯誤例子:企圖修改唯讀字串常數
char* pc = "john";
*pc = 'J'; // 執行期錯誤,唯讀記憶體無法修改
printf("Hello, %s\n", pc);
// 正確例子:宣告字元陣列以進行修改
char pc[] = "john"; // 複製字串內容到陣列中
*pc = 'J'; // pc[0] 變為 'J'
printf("Hello, %s\n", pc);
// ❌ 錯誤例子 (2):strcat 寫入至唯讀空間
char *s1 = "Hello, ";
char *s2 = "world!";
char *s3 = strcat(s1, s2); // 寫入到 s1 的唯讀空間
// 正確例子 (2):保留足夠空間的陣列
char s1[20] = "Hello, ";
char *s2 = "world!";
strcat(s1, s2); // 合法附加
五、你不可以對尚未分配所指空間的 char* 變數進行陣列相關操作。
未分配空間的指標沒有位置儲存輸入的字串,容易發生緩衝區溢位(Buffer Overflow)。
// ❌ 錯誤例子:name 沒有指向有效的空間
char *name;
printf("Your name, please: ");
gets(name); // 嚴重安全漏洞
// 正確例子 (1):若長度固定,使用 char[]
char name[21]; // 字串最長 20 個字元,另加 '\0'
printf("Your name, please: ");
fgets(name, sizeof(name), stdin); // 較安全的讀取方式
// 正確例子 (2):動態分配空間
size_t length;
char *name;
printf("請輸入字串的最大長度: ");
scanf("%u", &length);
name = (char *)malloc(length);
printf("Your name, please: ");
scanf("%s", name);
free(name); // 釋放記憶體
六、你不可以在函式中回傳一個指向區域自動變數的指標。
區域自動變數在函式結束時其生命週期隨即結束,其原本佔用的記憶體會被釋放。若返回該變數的指標,呼叫端會讀取到隨機的垃圾值。
// ❌ 錯誤例子:傳回區域變數指標
char *getstr(char *name)
{
char buf[30] = "hello, ";
strcat(buf, name);
return buf; // buf 記憶體在函式結束後失效
}
// 正確例子:將緩衝區由外部傳入
void getstr(char buf[], int buflen, char const *name)
{
char const s[] = "hello, ";
assert(strlen(s) + strlen(name) < buflen);
strcpy(buf, s);
strcat(buf, name);
}
// C++ 替代方案:使用 std::string class 傳回物件
#include <string>
using std::string;
string getstr(string const &name)
{
return string("hello, ") += name;
}
七、你不可以只做 malloc(), 而不做相應的 free().
每次使用 malloc()、calloc() 或 realloc() 動態分配記憶體後,若不再使用,必須以 free() 釋放。否則會造成記憶體漏失 (Memory Leak),耗盡系統記憶體資源。已經 free() 的指標不應再次 free() 或對其進行存取(避免 Dangling Pointer 指標懸空錯誤)。
註:C++ 中對應的 new 與 delete 也必須成對出現。
八、你不可以在數值運算、賦值中隨意混用不同型別的數值。
混用有號(signed)與無號(unsigned)型別,或者忽略變數寬度導致算術溢位,會帶來意想不到的計算錯愕。必須隨時確保計算結果不會超出變數型別的極限值。
// ❌ 錯誤例子 (1):溢位問題
unsigned int sum = 2000000000 + 2000000000; // int 相加放不下 40 億
double f = 10 / 3; // 整數除法結果為 3
// 正確例子 (1)
unsigned int sum = 2000000000u + 2000000000u; // 使用無號整數字尾
double f = 10.0 / 3.0; // 浮點數除法
// ❌ 錯誤例子 (2):不同平台 char 寬度不同可能導致的邏輯失效
unsigned char a = 0x80;
char b = 0x80;
if( a == 0x80 ) { printf( "a ok\n" ); }
if( b == 0x80 ) { printf( "b ok\n" ); } // 在 char 為 signed (-128~127) 的系統上,此判斷會失效
九、你不可以在一個運算式中對同一個基本型態變數修改其值超過一次。
在一個運算式(Expression)中多次修改同一個基本變數的值,會導致編譯器產生未定義行為 (Undefined Behavior),在不同的編譯器下會得到不同的計算結果。
// ❌ 錯誤例子:對 i 的遞增行為無順序保證
int i = 7;
int j = ++i + i++; // 未定義行為
// 正確例子:拆開來寫
int i = 7;
int j = ++i;
j += i++;
// ❌ 錯誤例子:在存取的同時對同一個變數進行自增修改
arr[i] = i++;
// 正確例子
arr[i] = i;
i++;
十、你不可以在 macro 定義中不為它的參數個別加上括號。
宏定義(Macro)是純粹的文字替換,如果參數沒有個別加上括號,在遇到複合運算式傳入時會因為運算子優先權(Operator Precedence)而導致計算順序錯誤。
// ❌ 錯誤例子:SQUARE 宏參數未加括號
#define SQUARE(x) (x * x)
// SQUARE(10-5) 會被展開為 (10-5 * 10-5) -> 10 - 50 - 5 = -45
// 正確例子:為參數加上小括號與外層大括號
#define SQUARE(x) ((x) * (x))
// SQUARE(10-5) 會被展開為 ((10-5) * (10-5)) -> 5 * 5 = 25
// C++ 最佳替代方案:使用 inline 函式,不僅安全且能進行型別檢查
inline int square(int x) { return x * x; }
本文原作者潘科元(Khoguan Phuann)於 2005 年整理此篇,旨在提醒所有 C 語言初學者(甚至老手)避免掉入這些深坑。不論是用於底層韌體開發,亦或是作為大學程式檢測(如 APCS)的準備,這十誡都是避開 C 語言陷阱的核心法則。
相關即時資訊與推薦閱讀:
- [自由時報] 程式設計先驅丹尼斯·里奇 Dennis Ritchie 的偉大貢獻與歷史意義 – 介紹 C 語言與 Unix 系統的共同創始人里奇對全球軟體開發產業與硬體科技所帶來的巨大歷史變革。
- [自由時報] Google Fuchsia OS 核心採用 C/C++ 開發 – 報導 Google 自主研發的新一代 Fuchsia 作業系統,介紹底層核心採用 C 與 C++ 編寫的設計考量。
- [自由時報] 板橋高中智慧機器人學苑 APCS 課程扎根 – 報導新北市教育局推動智慧機器人學苑,為高中生開設 APCS 程式設計與資料結構相關專班。
- [自由時報] 大學程式設計先修檢測 APCS 優化措施 – 報導教育部為推動資通訊人才扎根,針對 APCS 大學程式能力檢定所做的優化改進與測驗次數提升。
- [自由時報] 板橋高中奪得學生程式競賽世界冠軍 – 報導新北市板橋高中學生團隊運用 C/C++ 等程式語言在國際學生程式設計競賽中奪得佳績的傑出表現。