1. 程式人生 > >你的C/C++程式為什麼無法執行?揭祕Segmentation fault (1)

你的C/C++程式為什麼無法執行?揭祕Segmentation fault (1)

什麼讓你對C/C++如此恐懼?

晦澀的語法?還是優秀IDE的欠缺?
我想那都不是問題,最多的可能是一個類似這樣的錯誤:

這裡寫圖片描述

段錯誤(Segmentation fault)

這是新手無法避免的錯誤,也是老手極力迴避也經常遇到的錯誤。
本篇,試圖簡略地剖析一段會引發這個錯誤的程式,帶來一些啟發。

先看兩份程式碼,一份是錯誤的.

錯誤程式碼

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

void func1(char ** dest,char * src,int n) {
    (*dest) = (char
*)malloc(sizeof(char)*n); strcpy(*dest,src); } int main(int argc,char** args) { char ** p = NULL; char str[] = "foreach_break"; int len = sizeof(str); printf("%d\n",len); func1(p,str,len); printf("%s\n",*p); free(p); p = NULL; }

正確程式碼

#include "stdio.h"
#include "string.h" #include "stdlib.h" void func1(char ** dest,char * src,int n) { (*dest) = (char*)malloc(sizeof(char)*n); strcpy(*dest,src); } int main(int argc,char** args) { char * p = NULL; char str[] = "foreach_break"; int len = sizeof(str); printf("%d\n",len); func1(&p,str,len); printf
("%s\n",p); free(p); //p = NULL; }

程式碼意圖來自技術問答中的一個huffman樹不能執行的問題。
當然,我剝離掉了大部分關於huffman的部分,並稍加改動。

它們最大的不同:
錯誤程式碼:

char ** p = NULL;
func1(p,str,len);

正確程式碼:

char * p = NULL;
func1(&p,str,len);

也許你會奇怪,你看到“正確”的程式碼中居然註釋了這行:

//p = NULL;

引數傳遞

同時,可能有人會覺得指標char ** p向函式func1傳遞*p,與指標char * p向函式func1傳遞&p沒什麼不同啊?

嗯。這種想法也很有慣性,因為C語言沒有類似這樣的函式宣告:
void func(int &)

同時,對char * p&p操作不也得到個char **嗎?

執行程式

那麼,我們看看程式自己怎麼說?
這裡寫圖片描述

如果你經常遇到段錯誤,希望你仔細看明白上面的圖在說什麼。

野指標

所謂野指標,就是很野的指標,你不知道它指向了哪個地址,也不知道對這個地址取值是否會出錯,但,野指標也是指標,有一個存放它的記憶體地址.

正確的程式碼中,存放指標p的地址是0x7fffffffddc0;
錯誤的程式碼中,存放指標p的地址是0x7fffffffdd78

零指標

所謂零指標,就是指向了0x0的指標.對這個0x0取值是否會出錯呢?你想一想.

這裡寫圖片描述

懸浮指標

你對於在正確的程式中註釋了p = NULL而感到不解?
其實這沒什麼,取決於這個指標p在後續程式碼中怎麼使用.

free(p);

這句程式碼的執行,會釋放掉指標p所指向的記憶體地址,歸還給作業系統.
當然前提是這個地址確有所指、你也有權訪問它.

p = NULL;

這句程式碼的執行,是讓指標p指向了0x0,變回了空指標.

雖然它指向的記憶體已經被釋放,但是它還指向那個地址.

這就是懸浮指標,指向的地址已經不可用確還指向,就不是確有所指.

由於我們的main函式即將執行完畢,所以在它返回後,存放空指標p的地址會被釋放.

因為指標pmain函式的一個臨時變數.

所以我們可以毫無顧慮的註釋掉p = NULL.

記憶體洩露

另外,如果free(p)沒有被執行,而先執行了p = NULL,那麼p原來指向的記憶體空間可能就無法被正確釋放,如果再也沒有其它的引用指向了那塊地址,那塊地址就被遺忘在那裡,同時不能被回收.

這個,叫記憶體洩露 (Memory Leak).

接著看程式

現在,我們來看看函式func1的呼叫。

首先,是C程式碼:
這裡寫圖片描述

然後是兩段程式碼的對比:
這裡寫圖片描述

你應該已經看出了差別。

錯誤的程式碼的dest引數傳入了0x0.

接著是執行:

(*dest) = (char*)malloc(sizeof(char)*n);

這句程式碼在進行(*dest)時就會發生段錯誤.

究其原因,就在於char ** p = NULLp變成了零指標,*p相當於對0x0這個地址取值.

應用程式啟動時,作業系統會建立一個程序(process),這個程序擁有自己獨立的地址空間,稱作虛擬地址空間(virtual memory space).

0x0在這個空間中,不能被訪問.

試圖訪問一個不能被訪問的空間,就會段錯誤.

總結

段錯誤的一種,我們探索完畢.
現在你知道以下兩種操作的含義了嗎?

/* p指向的地址,對其取值,如果這個地址有東西,也有權取,沒問題.*/
char ** p -> *p; (1)
/* 存放p的地址,或者指向p的引用,這個地址必然有東西,所以沒問題*/
char * p -> &p   (2);

本篇結束.