血淚教訓!這 17 個 C 語言段錯誤陷阱害慘了無數程序員
哈嘍大家好!我是小康。
今天咱們來聊聊 C 語言里最讓新手崩潰的東西——段錯誤(Segmentation Fault)!
你是不是也有過這樣的經歷:代碼寫得好好的,一運行就彈出"段錯誤",然后程序直接閃退?那種心情就像是精心準備的飯菜,結果一上桌就翻了!
別慌!今天我就帶你揪出這些隱藏的"坑",看完之后保證你恍然大悟:"原來如此!"
坑1:空指針的致命一擊
這個絕對是新手殺手第一名!看這個代碼:
#include <stdio.h>
int main() {
int *ptr = NULL;
printf("準備訪問空指針...\n");
*ptr = 100; // ?? 炸了!段錯誤!
printf("這句話永遠不會執行\n");
return 0;
}
運行結果:
準備訪問空指針...
Segmentation fault (core dumped)
為什么會炸? 就像你想往一個不存在的地址寄快遞一樣,NULL指針指向的是"虛無",你往虛無里塞東西,系統當然要炸毛?。?/p>
正確姿勢:
#include <stdio.h>
int main() {
int *ptr = NULL;
if (ptr != NULL) { // 先檢查一下
*ptr = 100;
} else {
printf("指針是空的,不能用!\n");
}
return 0;
}
坑2:數組越界——跑出安全區
這個坑也是相當經典!就像在游戲里跑出了地圖邊界:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("正常訪問:%d\n", arr[2]); // 輸出:3
printf("危險操作:%d\n", arr[100]); // ?? 可能段錯誤!
// 更危險的寫操作
arr[10000] = 999; // ?? 幾乎必定段錯誤!
return 0;
}
為什么會出事? 數組就像一排房子,你有5間房(索引0-4),結果你跑到第100間房去放東西,那不是別人家的地盤嗎?系統肯定不答應!
安全做法:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int index = 10;
if (index >= 0 && index < 5) {
printf("安全訪問:%d\n", arr[index]);
} else {
printf("索引%d超出范圍了!\n", index);
}
return 0;
}
坑3:野指針——指向未知的危險
野指針就像一個喝醉酒的人,不知道會指向哪里:
#include <stdio.h>
int main() {
int *ptr; // 沒有初始化,是個野指針!
printf("野指針的值:%p\n", ptr); // 打印一個隨機地址
*ptr = 42; // ?? 向隨機地址寫數據,危險!
return 0;
}
// 輸出:
/*
野指針的值:(nil)
Segmentation fault (core dumped)
*/
為什么危險? 野指針就像一個沒有目標的導彈,你不知道它會炸到哪里!可能是系統重要的內存區域,那就完蛋了!
正確初始化:
#include <stdio.h>
int main() {
int value = 10;
int *ptr = &value; // 讓指針指向一個確定的地址
printf("安全操作:%d\n", *ptr); // 輸出:10
*ptr = 42; // 安全的寫操作
printf("修改后:%d\n", *ptr); // 輸出:42
return 0;
}
坑4:釋放后繼續使用——鞭尸行為
這個錯誤就像你把房子賣了,還想回去住一樣:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 100;
printf("釋放前:%d\n", *ptr); // 輸出:100
free(ptr); // 釋放內存
// 致命錯誤:繼續使用已釋放的內存
printf("釋放后:%d\n", *ptr); // ?? 未定義行為,可能段錯誤!
*ptr = 200; // ?? 更加危險!
return 0;
}
正確做法:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 100;
printf("使用中:%d\n", *ptr); // 輸出:100
free(ptr);
ptr = NULL; // 釋放后立即置空,防止誤用
if (ptr != NULL) {
printf("繼續使用:%d\n", *ptr);
} else {
printf("指針已釋放,不能再用了!\n");
}
return 0;
}
坑5:棧溢出——遞歸的無底洞
遞歸用不好就像掉進了無底洞:
#include <stdio.h>
int badRecursion(int n) {
printf("遞歸層數:%d\n", n);
return badRecursion(n + 1); // ?? 永遠不會停止!
}
int main() {
badRecursion(1); // ?? 棧溢出段錯誤!
return 0;
}
為什么會炸? 就像一個人不停地往地下室走,總有一天會撞到地板!??臻g有限,遞歸太深就會溢出。
安全的遞歸:
#include <stdio.h>
int safeRecursion(int n) {
if (n <= 0) { // 遞歸出口
return1;
}
printf("遞歸層數:%d\n", n);
return n * safeRecursion(n - 1);
}
int main() {
int result = safeRecursion(5);
printf("結果:%d\n", result); // 輸出:120
return 0;
}
坑6:字符串操作越界
字符串操作不小心就會越界:
#include <stdio.h>
#include <string.h>
int main() {
char str[5] = "Hi"; // 只能裝4個字符+'\0'
printf("原字符串:%s\n", str); // 輸出:Hi
// 危險操作:拷貝太長的字符串
strcpy(str, "Hello World!"); // ?? 緩沖區溢出!
printf("拷貝后:%s\n", str); // 可能段錯誤或垃圾數據
return 0;
}
/*
輸出結果:
原字符串:Hi
拷貝后:Hello World!
*** stack smashing detected ***: terminated
Aborted (core dumped)
*/
安全做法:
#include <stdio.h>
#include <string.h>
int main() {
char str[20] = "Hi"; // 給足夠的空間
printf("原字符串:%s\n", str); // 輸出:Hi
// 安全拷貝
if (strlen("Hello World!") < sizeof(str)) {
strcpy(str, "Hello World!");
printf("拷貝后:%s\n", str); // 輸出:Hello World!
} else {
printf("字符串太長,裝不下!\n");
}
return 0;
}
坑7:返回局部變量地址——過期的房產證
這個錯誤就像把一個即將拆遷的房子地址給別人:
#include <stdio.h>
int* getDangerousPointer() {
int localVar = 42;
return &localVar; // ?? 返回局部變量地址!
}
int main() {
int *ptr = getDangerousPointer();
printf("危險的值:%d\n", *ptr); // ?? 可能段錯誤或垃圾值!
return 0;
}
/*
輸出:
Segmentation fault (core dumped)
*/
為什么危險? 局部變量存在棧上,函數結束后就被銷毀了。你返回它的地址,就像給別人一個已經被拆掉的房子的鑰匙!
正確做法:
#include <stdio.h>
#include <stdlib.h>
int* getSafePointer() {
int *ptr = (int*)malloc(sizeof(int)); // 在堆上分配
*ptr = 42;
return ptr; // 安全返回
}
int main() {
int *ptr = getSafePointer();
printf("安全的值:%d\n", *ptr); // 輸出:42
free(ptr); // 記得釋放
ptr = NULL;
return 0;
}
坑8:多次釋放同一塊內存——重復拆房子
這就像你把同一棟房子拆了兩次:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 100;
printf("使用內存:%d\n", *ptr); // 輸出:100
free(ptr); // 第一次釋放,正常
free(ptr); // ?? 第二次釋放,段錯誤!
return0;
}
/*
輸出:
使用內存:100
free(): double free detected in tcache 2
Aborted (core dumped)
*/
安全做法:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 100;
printf("使用內存:%d\n", *ptr); // 輸出:100
if (ptr != NULL) {
free(ptr);
ptr = NULL; // 釋放后立即置空
}
if (ptr != NULL) { // 再次檢查
free(ptr);
} else {
printf("指針已經是空的,不需要釋放\n");
}
return 0;
}
坑9:格式化字符串漏洞
printf系列函數用不好也會出事:
#include <stdio.h>
int main() {
char dangerous[] = "Name: %s, Age: %s, City: %s, Job: %s";
// 危險:直接把用戶輸入當格式字符串
printf(dangerous); // 4個%s都會從棧上取隨機指針 ?? 可能段錯誤!
return 0;
}
為什么危險? printf會按照格式字符串去棧上找參數,但你沒提供參數,它就會讀取棧上的垃圾數據,甚至越界訪問!
正確做法:
#include <stdio.h>
int main() {
char userInput[] = "Name: %s, Age: %s, City: %s, Job: %s";
// 安全:用%s格式化輸出字符串
printf("%s\n", userInput);
return 0;
}
坑10:忘記檢查malloc返回值
malloc也有失敗的時候,不檢查就是在玩火:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main() {
// 申請超大內存,可能失敗
int *ptr = (int*)malloc(SIZE_MAX);
*ptr = 100; // ?? 如果malloc失敗,ptr是NULL,段錯誤!
printf("值:%d\n", *ptr);
free(ptr);
return 0;
}
//輸出:Segmentation fault (core dumped)
安全做法:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) { // 檢查分配是否成功
printf("內存分配失敗!\n");
return1;
}
*ptr = 100;
printf("值:%d\n", *ptr); // 輸出:100
free(ptr);
return 0;
}
坑11:二維數組指針混亂
二維數組和指針一起用,新手最容易搞混:
#include <stdio.h>
#include <stdlib.h>
int main() {
int **matrix;
// 錯誤:只分配了指針數組,沒分配實際存儲空間
matrix = (int**)malloc(3 * sizeof(int*));
matrix[0][0] = 10; // ?? 段錯誤!matrix[0]是垃圾值
return 0;
}
// 輸出:Segmentation fault (core dumped)
正確的二維數組分配:
#include <stdio.h>
#include <stdlib.h>
int main() {
int **matrix;
int rows = 3, cols = 4;
// 先分配指針數組
matrix = (int**)malloc(rows * sizeof(int*));
// 再為每一行分配空間
for(int i = 0; i < rows; i++) {
matrix[i] = (int*)malloc(cols * sizeof(int));
}
// 現在可以安全使用了
matrix[0][0] = 10;
printf("安全賦值:%d\n", matrix[0][0]); // 輸出:10
// 釋放內存
for(int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
return 0;
}
坑12:結構體內指針未初始化
結構體里的指針成員經常被忘記初始化:
#include <stdio.h>
#include <stdlib.h>
struct Student {
char *name;
int age;
};
int main() {
struct Student stu;
stu.age = 18;
// 危險:name指針沒有初始化就使用
strcpy(stu.name, "小明"); // ?? 可能段錯誤!
return 0;
}
正確做法:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Student {
char *name;
int age;
};
int main() {
struct Student stu;
stu.age = 18;
// 先為name分配空間
stu.name = (char*)malloc(20 * sizeof(char));
strcpy(stu.name, "小明");
printf("學生姓名:%s,年齡:%d\n", stu.name, stu.age);
free(stu.name); // 記得釋放
return 0;
}
坑13:函數參數傳遞陷阱
指針作為參數傳遞時的常見錯誤:
#include <stdio.h>
#include <stdlib.h>
void allocateMemory(int *ptr) {
ptr = (int*)malloc(sizeof(int)); // ?? 這樣寫沒用!
*ptr = 100;
}
int main() {
int *myPtr = NULL;
allocateMemory(myPtr);
printf("值:%d\n", *myPtr); // ?? 段錯誤!myPtr還是NULL
return 0;
}
// 輸出:Segmentation fault (core dumped)
正確的指針參數傳遞:
#include <stdio.h>
#include <stdlib.h>
void allocateMemory(int **ptr) { // 傳遞指針的指針
*ptr = (int*)malloc(sizeof(int));
**ptr = 100;
}
int main() {
int *myPtr = NULL;
allocateMemory(&myPtr); // 傳遞指針的地址
printf("值:%d\n", *myPtr); // 輸出:100
free(myPtr);
return 0;
}
坑14:字符串字面量修改
這個坑特別隱蔽,很多人不知道:
#include <stdio.h>
int main() {
char *str = "Hello"; // 字符串字面量存在只讀區域
printf("原字符串:%s\n", str); // 輸出:Hello
str[0] = 'h'; // ?? 試圖修改只讀內存,段錯誤!
return 0;
}
// 輸出:原字符串:Hello
// Segmentation fault (core dumped)
正確做法:
#include <stdio.h>
int main() {
char str[] = "Hello"; // 數組,可以修改
// 或者 char str[10] = "Hello";
printf("原字符串:%s\n", str); // 輸出:Hello
str[0] = 'h'; // 安全修改
printf("修改后:%s\n", str); // 輸出:hello
return 0;
}
坑15:聯合體內存覆蓋陷阱
union使用不當也會造成意外:
#include <stdio.h>
union Data {
int intVal;
float floatVal;
char *strVal;
};
int main() {
union Data data;
data.strVal = "Hello";
printf("字符串:%s\n", data.strVal); // 輸出:Hello
data.intVal = 100; // 覆蓋了strVal的值
printf("整數:%d\n", data.intVal); // 輸出:100
// 危險:strVal現在是垃圾值
printf("字符串:%s\n", data.strVal); // ?? 可能段錯誤!
return 0;
}
/* 輸出:
字符串:Hello
整數:100
Segmentation fault (core dumped)
*/
安全使用聯合體:
#include <stdio.h>
enum DataType {
TYPE_INT,
TYPE_FLOAT,
TYPE_STRING
};
struct SafeData {
enum DataType type;
union {
int intVal;
float floatVal;
char *strVal;
} value;
};
int main() {
struct SafeData data;
// 設置字符串
data.type = TYPE_STRING;
data.value.strVal = "Hello";
if (data.type == TYPE_STRING) {
printf("字符串:%s\n", data.value.strVal);
}
// 改為整數類型
data.type = TYPE_INT;
data.value.intVal = 100;
if (data.type == TYPE_INT) {
printf("整數:%d\n", data.value.intVal);
}
return 0;
}
坑16:函數指針未檢查
函數指針為NULL時調用會段錯誤:
#include <stdio.h>
void sayHello() {
printf("Hello!\n");
}
int main() {
void (*funcPtr)() = NULL;
// 某些情況下可能給funcPtr賦值,某些情況下可能忘記
if (rand() % 2 == 0) {
funcPtr = sayHello;
}
funcPtr(); // ?? 如果funcPtr還是NULL,段錯誤!
return 0;
}
安全調用函數指針:
#include <stdio.h>
void sayHello() {
printf("Hello!\n");
}
void sayGoodbye() {
printf("Goodbye!\n");
}
int main() {
void (*funcPtr)() = NULL;
// 根據條件設置函數指針
if (rand() % 2 == 0) {
funcPtr = sayHello;
} else {
funcPtr = sayGoodbye;
}
// 安全調用
if (funcPtr != NULL) {
funcPtr();
} else {
printf("函數指針為空,無法調用!\n");
}
return 0;
}
坑17:競態條件導致的段錯誤
多線程環境下的段錯誤更難調試:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int *global_ptr = NULL;
void* thread_func(void* arg) {
if (global_ptr != NULL) {
sleep(1);
// ?? 另一個線程可能在這里釋放了global_ptr
*global_ptr = 100; // 可能段錯誤!
}
return NULL;
}
int main() {
pthread_t thread;
global_ptr = (int*)malloc(sizeof(int));
pthread_create(&thread, NULL, thread_func, NULL);
// 主線程釋放內存
free(global_ptr);
global_ptr = NULL;
pthread_join(thread, NULL);
return 0;
}
正確做法:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
int *global_ptr = NULL;
void* thread_func(void* arg) {
if (global_ptr != NULL) {
sleep(1); // 模擬一些操作
*global_ptr = 100;
printf("設置值成功:%d\n", *global_ptr);
}
return NULL;
}
int main() {
pthread_t thread;
global_ptr = (int*)malloc(sizeof(int));
pthread_create(&thread, NULL, thread_func, NULL);
// 等子線程完成再釋放!這才是關鍵
pthread_join(thread, NULL);
// 現在可以安全釋放了
free(global_ptr);
global_ptr = NULL;
return 0;
}
避坑終極秘籍
看完這 17 個經典陷阱,相信你已經對段錯誤有了全新的認識!讓我總結幾個終極避坑秘籍:
內存管理黃金法則:
- malloc必須配free - 有借有還,再借不難
- free后立即置NULL - 防止野指針復活
- 使用前必須檢查 - NULL指針是大敵
- 邊界時刻要注意 - 數組越界是噩夢
指針使用終極口訣:
- 初始化 - 聲明時就給個明確的值
- 驗證 - 使用前檢查是否為NULL
- 保護 - 操作時注意邊界和權限
- 清理 - 用完立即釋放并置空
多級指針避坑技巧:
- 二維數組 - 先分配行指針,再分配每行數據
- 函數傳參 - 想改指針本身,就傳指針的地址
- 結構體指針 - 內部的指針成員別忘了初始化
字符串操作安全守則:
- 只讀區別碰 - 字符串字面量不能修改
- 空間要充足 - strcpy前確保目標夠大
- 邊界要檢查 - 防止緩沖區溢出
高級避坑技巧:
- 聯合體慎用 - 記住類型,避免數據覆蓋
- 函數指針檢查 - NULL函數指針不能調用
- 多線程加鎖 - 共享資源要保護
記住這些,再配合調試工具(gdb、valgrind、AddressSanitizer),段錯誤再也不是你的攔路虎!