1內存泄露編程
2.內存越界訪問小程序
3.野指針數組
4.訪問空指針網絡
5.引用未初始化的變量多線程
6.不清楚指針運算
併發
7.結構順序變化錯誤函數
8.結構大小變化引起的錯誤工具
9.分配/釋放不配對佈局
10.返回值指向臨時變量指針測試
11.試圖修改變量
12.誤解傳值與傳引用
13.重名符號
14.棧溢出
15.誤用sizeof
16.字節對齊
17.字節順序
18.多線程共享變量沒有用valotitle修飾
19.忘記函數返回值
o 內存泄露
你們都知道,在堆上分配的內存,若是再也不使用了,應該把它釋放掉,以便後面其它地方能夠重用。在C/C++中,內存管理器不會幫你自動回收再也不使用的內存。若是你忘了釋放再也不使用的內存,這些內存就不能被重用了,這就形成了所謂的內存泄露。
把內存泄露列爲首位,倒並非由於它有多麼嚴重的後果,而由於它是最爲常見的一類錯誤。一兩處內存泄露一般不至於讓程序崩潰,也不會出現邏輯上的錯 誤,加上進程退出時,系統會自動釋放該進程全部相關的內存(共享內存除外),因此內存泄露的後果相對來講仍是比較溫和的。可是,量變會致使質變,一旦內存 泄露過多以至於耗盡內存,後續內存分配將會失敗,程序可能所以而崩潰。
如今PC機的內存夠大了,加上進程有獨立的內存空間,對於一些小程序來講,內存泄露已經不是太大的威脅。但對於大型軟件,特別是長時間運行的軟件,或者嵌入式系統來講,內存泄露仍然是致命的因素之一。
無論在什麼狀況下,採起謹慎的態度,杜絕內存泄露的出現,都是可取的。相反,認爲內存有的是,對內存泄露聽任自流都不是負責的。儘管一些工具能夠幫助咱們檢查內存泄露問題,我認爲仍是應該在編程時就仔細一點,及早排除這類錯誤,工具只是用做驗證的手段。
o內存越界訪問
內存越界訪問有兩種:一種是讀越界,即讀了不屬於本身的數據,若是所讀的內存地址是無效的,程度馬上就崩潰了。若是所讀內存地址是有效的,在讀的時 候不會出問題,但因爲讀到的數據是隨機的,它會產生不可預料的後果。另一種是寫越界,又叫緩衝區溢出,所寫入的數據對別人來講是隨機的,它也會產生不可 預料的後果。
內存越界訪問形成的後果很是嚴重,是程序穩定性的致命威脅之一。更麻煩的是,它形成的後果是隨機的,表現出來的症狀和時機也是隨機的,讓BUG的現象和本質看似沒有什麼聯繫,這給BUG的定位帶來極大的困難。
一些工具能夠夠幫助檢查內存越界訪問的問題,但也不能太依賴於工具。內存越界訪問一般是動態出現的,即依賴於測試數據,在極端的狀況下才會出現,除 非精心設計測試數據,工具也無能爲力。工具自己也有一些限制,甚至在一些大型項目中,工具變得徹底不可用。比較保險的方法仍是在編程是就當心,特別是對於 外部傳入的參數要仔細檢查。
咱們來看一個例子:
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[])
{
char str[10];
int array[10] = {0,1,2,3,4,5,6,7,8,9};
int data = array[10];
array[10] = data;
if(argc == 2)
{
strcpy(str, argv[1]);
}
return 0;
}
這個例子中有兩個錯誤是新手常犯的:
其一:int array[10] 定義了10個元素大小的數組,因爲C語言中數組的索引是從0開始的,因此只能訪問array[0]到array[9],訪問array[10]就形成了越界錯誤。
其二:strcpy(str, argv[1]);這裏是否存在越界錯誤依賴於外部輸入的數據,這樣的寫法在正常下可能沒有問題,但受到一點惡意攻擊就完蛋了。除非你肯定輸入數據是在你 控制內的,不然不要用strcpy、strcat和sprintf之類的函數,而要用strncpy、strncat和snprintf代替。
o 野指針。
野指針是指那些你已經釋放掉的內存指針。當你調用free(p)時,你真正清楚這個動做背後的內容嗎?你會說p指向的內存被釋放了。沒錯,p自己有變化嗎?答案是p自己沒有變化。它指向的內存仍然是有效的,你繼續讀寫p指向的內存,沒有人能攔得住你。
釋放掉的內存會被內存管理器從新分配,此時,野指針指向的內存已經被賦予新的意義。對野指針指向內存的訪問,不管是有意仍是無心的,都爲此會付出巨大代價,由於它形成的後果,如同越界訪問同樣是不可預料的。
釋放內存後當即把對應指針置爲空值,這是避免野指針經常使用的方法。這個方法簡單有效,只是要注意,固然指針是從函數外層傳入的時,在函數內把指針置爲 空值,對外層的指針沒有影響。好比,你在析構函數裏把this指針置爲空值,沒有任何效果,這時應該在函數外層把指針置爲空值。
o 訪問空指針。
空指針在C/C++中佔有特殊的地址,一般用來判斷一個指針的有效性。空指針通常定義爲0。現代操做系統都會保留從0開始的一塊內存,至於這塊內存有多大,視不一樣的操做系統而定。一旦程序試圖訪問這塊內存,系統就會觸發一個異常/信號。
操做系統爲何要保留一塊內存,而不是僅僅保留一個字節的內存呢?緣由是:通常內存管理都是按頁進行管理的,沒法單純保留一個字節,至少要保留一個頁面。保留一塊內存也有額外的好處,能夠檢查諸如p=NULL; p[1]之類的內存錯誤。
在一些嵌入式系統(如arm7)中,從0開始的一塊內存是用來安裝中斷向量的,沒有MMU的保護,直接訪問這塊內存好像不會引起異常。不過這塊內存是代碼段的,不是程序中有效的變量地址,因此用空指針來判斷指針的有效性仍然可行。
o 引用未初始化的變量。
未初始化變量的內容是隨機的(有的編譯器會在調試版本中把它們初始化爲固定值,如0xcc),使用這些數據會形成不可預料的後果,調試這樣的BUG也是很是困難的。
對於態度嚴謹的程度員來講,防止這類BUG很是容易。在聲明變量時就對它進行初始化,是一個好的編程習慣。另外也要重視編譯器的警告信息,發現有引用未初始化的變量,當即修改過來。
在下面這個例子中,全局變量g_count是肯定的,由於它在bss段中,自動初始化爲0了。臨時變量a是沒有初始化的,堆內存str是沒有初始化 的。但這個例子有點特殊,由於程序剛運行起來,不少東西是肯定的,若是你想把它們看成隨機數的種子是不行的,由於它們還不夠隨機。
#include <stdlib.h>
#include <string.h>
int g_count;
int main(int argc, char* argv[])
{
int a;
char* str = (char*)malloc(100);
return 0;
}
o 不清楚指針運算。
對於一些新手來講,指針經常讓他們犯糊塗。
好比int *p = …; p+1等於(size_t)p + 1嗎
老手天然清楚,新手可能就搞不清了。事實上, p+n 等於 (size_t)p + n * sizeof(*p)
指針是C/C++中最有力的武器,功能很是強大,不管是變量指針仍是函數指針,都應該很是熟練的掌握。只要有不肯定的地方,立刻寫個小程序驗證一下。對每個細節瞭然於胸,在編程時會省下很多時間。
o 結構的成員順序變化引起的錯誤。
在初始化一個結構時,老手可能不多像新手那樣老老實實的,一個成員一個成員的爲結構初始化,而是採用快捷方式,如:
Struct s
{
int l;
char* p;
};
int main(int argc, char* argv[])
{
struct s s1 = {4, "abcd"};
return 0;
}
以上這種方式是很是危險的,緣由在於你對結構的內存佈局做了假設。若是這個結構是第三方提供的,他極可能調整結構中成員的相對位置。而這樣的調整往 往不會在文檔中說明,你天然不多去關注。若是調整的兩個成員具備相同數據類型,編譯時不會有任何警告,而程序的邏輯可能相距十萬八千里了。
正確的初始化方法應該是(固然,一個成員一個成員的初始化也行):
struct s
{
int l;
char* p;
};
int main(int argc, char* argv[])
{
struct s s1 = {.l=4, .p = "abcd"};
return 0;
}
(有的編譯器可能不支持新標準)
o 結構的大小變化引起的錯誤。
咱們看看下面這個例子:
struct base
{
int n;
};
struct s
{
struct base b;
int m;
};
在OOP中,咱們能夠認爲第二個結構繼承了第一結構,這有什麼問題嗎?固然沒有,這是C語言中實現繼承的基本手法。
如今假設第一個結構是第三方提供的,第二個結構是你本身的。第三方提供的庫是以DLL方式分發的,DLL最大好處在於能夠獨立替換。但隨着軟件的進化,問題可能就來了。
當第三方在第一個結構中增長了一個新的成員int k;,編譯好後把DLL給你,你直接把它給了客戶了,讓他們替換掉老版本。程序加載時不會有任何問題,在運行邏輯可能徹底改變!緣由是兩個結構的內存佈局重疊了。
解決這類錯誤的惟一辦法就是從新編譯所有代碼。由此看來,動態庫並不見得能夠動態替換,若是你想了解更多相關內容,建議你閱讀《COM本質論》。
o 分配/釋放不配對。
你們都知道malloc要和free配對使用,new要和delete/delete[]配對使用,重載了類new操做,應該同時重載類的delete/delete[]操做。這些都是書上反覆強調過的,除非當時暈了頭,通常不會犯這樣的低級錯誤。
而有時候咱們卻被矇在鼓裏,兩個代碼看起來都是調用的free函數,實際上卻調用了不一樣的實現。好比在Win32下,調試版與發佈版,單線程與多線 程是不一樣的運行時庫,不一樣的運行時庫使用的是不一樣的內存管理器。一不當心連接錯了庫,那你就麻煩了。程序可能動則崩潰,緣由在於在一個內存管理器中分配的 內存,在另一個內存管理器中釋放時就會出現問題。
o 返回指向臨時變量的指針
你們都知道,棧裏面的變量都是臨時的。當前函數執行完成時,相關的臨時變量和參數都被清除了。不能把指向這些臨時變量的指針返回給調用者,這樣的指針指向的數據是隨機的,會給程序形成不可預料的後果。
下面是個錯誤的例子:
char* get_str(void)
{
char str[] = {"abcd"};
return str;
}
int main(int argc, char* argv[])
{
char* p = get_str();
printf("%s/n", p);
return 0;
}
下面這個例子沒有問題,你們知道爲何嗎?
char* get_str(void)
{
char* str = {"abcd"};
return str;
}
int main(int argc, char* argv[])
{
char* p = get_str();
printf("%s/n", p);
return 0;
}
"Hello world"做爲靜態字符串實際上存儲在數據區,但寫程序的人不知道這個地址,而程序自己知道。當某一函數以
{ char p[] = "Hello world"; ...}
方式使用此靜態字符串時,實際上至關於:
char p[12];
strcpy(p, "Hello world");
....
p[12]是在棧裏臨時分配的。雖然p指向的內容是"Hello world", 可是這是複製品,不是原件。當函數結束,char p[]就被程序回收了,因此p[]的內容就再也不是"Hello world"了。
但若是以char *p="Hello world"的方式使用,p指向的是靜態字符串存儲的位置,也就是說指向"Hello world"的原件,固然沒有問題了。
若是想堅持用char p[]而不使用char *p, 有效方法必須是:
{
static char p[]="Hello world";
return p;
}
緣由我想很清楚了。static char []是靜態的,存儲在數據區。
o 試圖修改常量
在函數參數前加上const修飾符,只是給編譯器作類型檢查用的,編譯器禁止修改這樣的變量。但這並非強制的,你徹底能夠用強制類型轉換繞過去,通常也不會出什麼錯。
而全局常量和字符串,用強制類型轉換繞過去,運行時仍然會出錯。緣由在於它們是放在.rodata裏面的,而.rodata內存頁面是不能修改的。試圖對它們修改,會引起內存錯誤。
下面這個程序在運行時會出錯:
int main(int argc, char* argv[])
{
char* p = "abcd";
*p = '1';
return 0;
}
o 誤解傳值與傳引用
在C/C++中,參數默認傳遞方式是傳值的,即在參數入棧時被拷貝一份。在函數裏修改這些參數,不會影響外面的調用者。如:
#include <stdlib.h>
#include <stdio.h>
void get_str(char* p)
{
p = malloc(sizeof("abcd"));
strcpy(p, "abcd");
return;
}
int main(int argc, char* argv[])
{
char* p = NULL;
get_str(p);
printf("p=%p/n", p);
return 0;
}
在main函數裏,p的值仍然是空值。固然在函數裏修改指針指向的內容是能夠的。
o 重名符號。
不管是函數名仍是變量名,若是在不一樣的做用範圍內重名,天然沒有問題。但若是兩個符號的做用域有交集,如全局變量和局部變量,全局變量與全局變量之 間,重名的現象必定要堅定避免。gcc有一些隱式規則來決定處理同名變量的方式,編譯時可能沒有任何警告和錯誤,但結果一般並不是你所指望的。
下面例子編譯時就沒有警告:
t.c
#include <stdlib.h>
#include <stdio.h>
int count = 0;
int get_count(void)
{
return count;
}
main.c
#include <stdio.h>
extern int get_count(void);
int count;
int main(int argc, char* argv[])
{
count = 10;
printf("get_count=%d/n", get_count());
return 0;
}
若是把main.c中的int count;修改成int count = 0;,gcc就會編輯出錯,說multiple definition of `count’。它的隱式規則比較奇妙吧,因此仍是不要依賴它爲好。
o 棧溢出。
咱們在前面關於堆棧的一節講過,在PC上,普通線程的棧空間也有十幾M,一般夠用了,定義大一點的臨時變量不會有什麼問題。
而在一些嵌入式中,線程的棧空間可能只5K大小,甚至小到只有256個字節。在這樣的平臺中,棧溢出是最經常使用的錯誤之一。在編程時應該清楚本身平臺的限制,避免棧溢出的可能。
o 誤用sizeof。
儘管C/C++一般是按值傳遞參數,而數組則是例外,在傳遞數組參數時,數組退化爲指針(即按引用傳遞),用sizeof是沒法取得數組的大小的。
從下面這個例子能夠看出:
void test(char str[20])
{
printf("%s:size=%d/n", __func__, sizeof(str));
}
int main(int argc, char* argv[])
{
char str[20] = {0};
test(str);
printf("%s:size=%d/n", __func__, sizeof(str));
return 0;
}
[root@localhost mm]# ./t.exe
test:size=4
main:size=20
o 字節對齊。
字節對齊主要目的是提升內存訪問的效率。但在有的平臺(如arm7)上,就不光是效率問題了,若是不對齊,獲得的數據是錯誤的。
所幸的是,大多數狀況下,編譯會保證全局變量和臨時變量按正確的方式對齊。內存管理器會保證動態內存按正確的方式對齊。要注意的是,在不一樣類型的變量之間轉換時要當心,如把char*強制轉換爲int*時,要格外當心。
另外,字節對齊也會形成結構大小的變化,在程序內部用sizeof來取得結構的大小,這就足夠了。若數據要在不一樣的機器間傳遞時,在通訊協議中要規定對齊的方式,避免對齊方式不一致引起的問題。
o 字節順序。
字節順序從來是設計跨平臺軟件時頭疼的問題。字節順序是關於數據在物理內存中的佈局的問題,最多見的字節順序有兩種:大端模式與小端模式。
大端模式是高位字節數據存放在低地址處,低位字節數據存放在高地址處。
小端模式指低位字節數據存放在內存低地址處,高位字節數據存放在內存高地址處;
在普通軟件中,字節順序問題並不引人注目。而在開發與網絡通訊和數據交換有關的軟件時,字節順序問題就要特殊注意了。
o 多線程共享變量沒有用valotile修飾。
關鍵字valotile的做用是告訴編譯器,不要把變量優化到寄存器裏。在開發多線程併發的軟件時,若是這些線程共享一些全局變量,這些全局變量最好用valotile修飾。這樣能夠避免由於編譯器優化而引發的錯誤,這樣的錯誤很是難查。
o 忘記函數的返回值
函數須要返回值,若是你忘記return語句,它仍然會返回一個值,由於在i386上,EAX用來保存返回值,若是沒有明確返回,EAX最後的內容被返回,因此EAX的內容是隨機的。