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