dlist_to_upper

文章出處:http://www.limodev.cn/blog
作者聯繫方式:李先靜 <xianjimli at hotmail dot com>

對於初學者來說這道題有點難度,很少有人能完全做對的。不過沒關係,我們的目標並不是要難倒讀者,而是要刺激讀者去思考,加深學習的印象。有了前面兩次的經驗,我想沒有人再去寫一個dlist_to_upper的函數了,大家都會調用dlist_foreach來實現。不過新的問題又出現了,初學者常犯的錯誤有以下幾種:

1. 轉換大寫的方法不對。

    char* p = str;
if(p != NULL)
{
while(*p != '/0')
{
if('a' <= *p && *p <= 'z')
{
*p = *p - ('a' - 'A');
}
p++;
}
}

這是我們在課本里學到的寫法,但在工程中是不能這樣做的。因爲大小寫字母在不同語言中的定義是不一樣的,’a’是一個字符常量,它的值在任何時候都是97,但在不同語言中,97卻不一定代表’a’。我們不能簡單的認爲在97(‘a’)-122(‘z’)之間的字符就是小寫字母,而是應該調用標準C函數islower來判斷,同樣轉換爲大寫應該調用toupper而不是減去一個常量。

2. 在雙向鏈表中存放常量字符串,轉換時出現段錯誤。

    DList* dlist = dlist_create();
dlist_append(dlist, "It");
dlist_append(dlist, "is");
dlist_append(dlist, "OK");
dlist_append(dlist, "!");
dlist_foreach(dlist, str_toupper, NULL);
dlist_destroy(dlist);

運行時會出現Segmentation fault錯誤。原因是”It”等字符串是常量,常量是不能修改的。

3. 在雙向鏈表中存放的是臨時變量,轉換後發現所有字符串都一樣。

    char str[256] = {0};
DList* dlist = dlist_create();
strcpy(str, "It");
dlist_append(dlist, str);
strcpy(str, "is");
dlist_append(dlist, str);
strcpy(str, "OK");
dlist_append(dlist, str);
strcpy(str, "!");
dlist_append(dlist, str);
dlist_foreach(dlist, str_toupper, NULL);
dlist_foreach(dlist, str_print, NULL);
dlist_destroy(dlist);

運行時發現打印出幾個感嘆號。原因是dlist_append時沒有拷貝一份,所以在dlist中存放的是同一個地址。而且這個dlist在當前函數返回後,裏面保存的數據都無效了,因爲這些數據指向的是臨時變量。

4. 存放時拷貝了數據,但沒有free分配的內存。

    DList* dlist = dlist_create();
dlist_append(dlist, strdup("It"));
dlist_append(dlist, strdup("is"));
dlist_append(dlist, strdup("OK"));
dlist_append(dlist, strdup("!"));
dlist_foreach(dlist, str_toupper, NULL);
dlist_foreach(dlist, str_print, NULL);
dlist_destroy(dlist);

這裏看起來工作正常了,但存在內存泄露的BUG。strdup調用malloc分配了內存,但沒有地方去free它們。

初學者對內存和指針只有一知半解的認識,常常犯一些連自己都莫名其妙的錯誤。爲了避免這些不必要的錯誤,今天我們要學習各種數據存放的位置以及它們的特性,讓初學者對編程有更進一步的認識。在程序中,數據存放的位置主要有以下幾個:

1.未初始化的全局變量(.bss段)

已經記不清bss代表Block Storage Start還是Block Started by Symbol。像我這種沒有用過那些史前計算機的人,終究無法明白這樣怪異的名字,記不住也是不足爲奇的。不過沒有關係,重要的是,我們要清楚什麼數據是存放在bss段中的,這些數據有什麼樣的特點以及如何使用它們。

通俗的說,bss段是用來存放那些沒有初始化的和初始化爲0的全局變量的。它有什麼特點呢,讓我們來看看一個小程序的表現。

int bss_array[1024 * 1024];

int main(int argc, char* argv[])
{
return 0;
}
# gcc -g bss.c -o bss.exe
# ls -l bss.exe
-rwxrwxr-x 1 root root 5975 Nov 16 09:32 bss.exe
# objdump -h bss.exe |grep bss
24 .bss 00400020 080495e0 080495e0 000005e0 2**5

變量bss_array的大小爲4M,而可執行文件的大小隻有5K。 由此可見,bss類型的全局變量只佔運行時的內存空間,而不佔用文件空間。

現代大多數操作系統,在加載程序時,會把所有的bss全局變量清零。但爲保證程序的可移植性,手工把這些變量初始化爲0也是一個好習慣,這樣這些變量都有個確定的初始值。

當然作爲全局變量,在整個程序的運行週期內,bss數據是一直存在的。

2.初始化過的全局變量 (.data段)

與bss相比,data段就容易明白多了,它的名字就暗示着裏面存放着數據。當然,如果數據全是零,爲了優化考慮,編譯器把它當作bss處理。通俗的說,data段用來存放那些初始化爲非零的全局變量。它有什麼特點呢,我們還是來看看一個小程序的表現。

int data_array[1024 * 1024] = {1};

int main(int argc, char* argv[])
{
return 0;
}
# ls -l data.exe
-rwxrwxr-x 1 root root 4200313 Nov 16 09:34 data.exe
# objdump -h data.exe |grep //.data
23 .data 00400020 080495e0 080495e0 000005e0 2**5

僅僅是把初始化的值改爲非零了,文件就變爲4M多。由此可見,data類型的全局變量是即佔文件空間,又佔用運行時內存空間的。

同樣作爲全局變量,在整個程序的運行週期內,data數據是一直存在的。

3.常量數據 (.rodata段)

rodata的意義同樣明顯,ro代表read only,rodata就是用來存放常量數據的。關於rodata類型的數據,要注意以下幾點:

o 常量不一定就放在rodata裏,有的立即數直接和指令編碼在一起,存放在代碼段(.text)中。

o 對於字符串常量,編譯器會自動去掉重複的字符串,保證一個字符串在一個可執行文件(EXE/SO)中只存在一份拷貝。

o rodata是在多個進程間是共享的,這樣可以提高運行空間利用率。

o 在有的嵌入式系統中,rodata放在ROM(或者norflash)裏,運行時直接讀取,無需加載到RAM內存中。

o 在嵌入式linux系統中,也可以通過一種叫作XIP(就地執行)的技術,也可以直接讀取,而無需加載到RAM內存中。

o 常量是不能修改的,修改常量在linux下會出現段錯誤。

由此可見,把在運行過程中不會改變的數據設爲rodata類型的是有好處的:在多個進程間共享,可以大大提高空間利用率,甚至不佔用RAM空間。同時由於rodata在只讀的內存頁面(page)中,是受保護的,任何試圖對它的修改都會被及時發現,這可以提高程序的穩定性。

字符串會被編譯器自動放到rodata中,其它數據要放到rodata中,只需要加const關鍵字修飾就好了。

4.代碼 (.text段)

text段存放代碼(如函數)和部分整數常量,它與rodata段很相似,相同的特性我們就不重複了,主要不同在於這個段是可以執行的。

5. 棧(stack)

棧用於存放臨時變量和函數參數。棧作爲一種基本數據結構,我並不感到驚訝,用來實現函數調用,這也司空見慣的作法。直到我試圖找到另外一種方式實現遞歸操作時,我才感嘆於它的巧妙。要實現遞歸操作,不用棧不是不可能,只是找不出比它更優雅的方式。

儘管大多數編譯器在優化時,會把常用的參數或者局部變量放入寄存器中。但用棧來管理函數調用時的臨時變量(局部變量和參數)是通用做法,前者只是輔助手段,且只在當前函數中使用,一旦調用下一層函數,這些值仍然要存入棧中才行。

通常情況下,棧向下(低地址)增長,每向棧中PUSH一個元素,棧頂就向低地址擴展,每從棧中POP一個元素,棧頂就向高地址回退。一個有興趣的問題:在x86平臺上,棧頂寄存器爲ESP,那麼ESP的值在是PUSH操作之前修改呢,還是在PUSH操作之後修改呢?PUSH ESP這條指令會向棧中存入什麼數據呢?據說x86系列CPU中,除了286外,都是先修改ESP,再壓棧的。由於286沒有CPUID指令,有的OS用這種方法檢查286的型號。

要注意的是,存放在棧中的數據只在當前函數及下一層函數中有效,一旦函數返回了,這些數據也自動釋放了,繼續訪問這些變量會造成意想不到的錯誤。

6.堆(heap)

堆是最靈活的一種內存,它的生命週期完全由使用者控制。標準C提供幾個函數:

malloc 用來分配一塊指定大小的內存。
realloc 用來調整/重分配一塊存在的內存。
free 用來釋放不再使用的內存。

使用堆內存時請注意兩個問題:

alloc/free要配對使用。內存分配了不釋放我們稱爲內存泄露(memory leak),內存泄露多了遲早會出現Out of memory的錯誤,再分配內存就會失敗。當然釋放時也只能釋放分配出來的內存,釋放無效的內存,或者重複free都是不行的,會造成程序crash。

分配多少用多少。分配了100字節就只能用100字節,不管是讀還是寫,都只能在這個範圍內,讀多了會讀到隨機的數據,寫多了會造成的隨機的破壞。這種情況我們稱爲緩衝區溢出(buffer overflow),這是非常嚴重的,大部分安全問題都是由緩衝區溢出引起的。

手工檢查有沒有內存泄露或者緩衝區溢出是很困難的,幸好有些工具可以使用,比如linux下有valgrind,它的使用方法很簡單,大家下去可以試用一下,以後每次寫完程序都應該用valgrind跑一遍。

最後,我們來看看在linux下,程序運行時空間的分配情況:

# cat /proc/self/maps 

00110000-00111000 r-xp 00110000 00:00 0 [vdso]
009ba000-009d6000 r-xp 00000000 08:01 768759 /lib/ld-2.8.so
009d6000-009d7000 r--p 0001c000 08:01 768759 /lib/ld-2.8.so
009d7000-009d8000 rw-p 0001d000 08:01 768759 /lib/ld-2.8.so
009da000-00b3d000 r-xp 00000000 08:01 768760 /lib/libc-2.8.so
00b3d000-00b3f000 r--p 00163000 08:01 768760 /lib/libc-2.8.so
00b3f000-00b40000 rw-p 00165000 08:01 768760 /lib/libc-2.8.so
00b40000-00b43000 rw-p 00b40000 00:00 0
08048000-08050000 r-xp 00000000 08:01 993652 /bin/cat
08050000-08051000 rw-p 00007000 08:01 993652 /bin/cat
0805f000-08080000 rw-p 0805f000 00:00 0 [heap]
b7fe8000-b7fea000 rw-p b7fe8000 00:00 0
bfee7000-bfefc000 rw-p bffeb000 00:00 0 [stack]

每個區間都有四個屬性:

r 表示可以讀取。
w 表示可以修改。
x 表示可以執行。
p/s 表示是否爲共享內存。

有文件名的內存區間,屬性爲r—p表示存放的是rodata。
有文件名的內存區間,屬性爲rw-p表示存放的是bss和data
有文件名的內存區間,屬性爲r-xp表示存放的是text數據。
沒有文件名的內存區間,表示用mmap映射的匿名空間。
文件名爲[stack]的內存區間表示是棧。
文件名爲[heap]的內存區間表示是堆。

發佈了17 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章