• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            A Za, A Za, Fighting...

            堅信:勤能補拙

                 摘要: 程序分析是以某種語言書寫的程序為對象,對其內(nèi)部的運作流程進行分析。程序分析的目的主要有三點:一是通過程序內(nèi)部各個模塊之間的調(diào)用關(guān)系,整體上把握程序的運行流程,從而更好地理解程序,從中汲取有價值的內(nèi)容。二是以系統(tǒng)優(yōu)化為目的,通過對程序中關(guān)鍵函數(shù)的跟蹤或者運行時信息的統(tǒng)計,找到系統(tǒng)性能的瓶頸,從而采取進一步行動對程序進行優(yōu)化。最后一點,程序分析也有可能用于系統(tǒng)測試和程序調(diào)試中。當(dāng)系統(tǒng)跟蹤起來比較復(fù)雜,...  閱讀全文
            posted @ 2011-08-12 10:33 simplyzhao 閱讀(446) | 評論 (0)編輯 收藏
            轉(zhuǎn)載: http://www.ibm.com/developerworks/cn/linux/l-cn-valgrind/

            應(yīng)用 Valgrind 發(fā)現(xiàn) Linux 程序的內(nèi)存問題


            Valgrind 概述

            體系結(jié)構(gòu)

            Valgrind是一套Linux下,開放源代碼(GPL V2)的仿真調(diào)試工具的集合。Valgrind由內(nèi)核(core)以及基于內(nèi)核的其他調(diào)試工具組成。內(nèi)核類似于一個框架(framework),它模擬了一個CPU環(huán)境,并提供服務(wù)給其他工具;而其他工具則類似于插件 (plug-in),利用內(nèi)核提供的服務(wù)完成各種特定的內(nèi)存調(diào)試任務(wù)。Valgrind的體系結(jié)構(gòu)如下圖所示:


            圖 1 Valgrind 體系結(jié)構(gòu)
            Valgrind 體系結(jié)構(gòu) 

            Valgrind包括如下一些工具:

            1. Memcheck。這是valgrind應(yīng)用最廣泛的工具,一個重量級的內(nèi)存檢查器,能夠發(fā)現(xiàn)開發(fā)中絕大多數(shù)內(nèi)存錯誤使用情況,比如:使用未初始化的內(nèi)存,使用已經(jīng)釋放了的內(nèi)存,內(nèi)存訪問越界等。這也是本文將重點介紹的部分。
            2. Callgrind。它主要用來檢查程序中函數(shù)調(diào)用過程中出現(xiàn)的問題。
            3. Cachegrind。它主要用來檢查程序中緩存使用出現(xiàn)的問題。
            4. Helgrind。它主要用來檢查多線程程序中出現(xiàn)的競爭問題。
            5. Massif。它主要用來檢查程序中堆棧使用中出現(xiàn)的問題。
            6. Extension。可以利用core提供的功能,自己編寫特定的內(nèi)存調(diào)試工具。

            Linux 程序內(nèi)存空間布局

            要發(fā)現(xiàn)Linux下的內(nèi)存問題,首先一定要知道在Linux下,內(nèi)存是如何被分配的?下圖展示了一個典型的Linux C程序內(nèi)存空間布局:


            圖 2: 典型內(nèi)存空間布局
            典型內(nèi)存空間布局 

            一個典型的Linux C程序內(nèi)存空間由如下幾部分組成:

            • 代碼段(.text)。這里存放的是CPU要執(zhí)行的指令。代碼段是可共享的,相同的代碼在內(nèi)存中只會有一個拷貝,同時這個段是只讀的,防止程序由于錯誤而修改自身的指令。
            • 初始化數(shù)據(jù)段(.data)。這里存放的是程序中需要明確賦初始值的變量,例如位于所有函數(shù)之外的全局變量:int val=100。需要強調(diào)的是,以上兩段都是位于程序的可執(zhí)行文件中,內(nèi)核在調(diào)用exec函數(shù)啟動該程序時從源程序文件中讀入。
            • 未初始化數(shù)據(jù)段(.bss)。位于這一段中的數(shù)據(jù),內(nèi)核在執(zhí)行該程序前,將其初始化為0或者null。例如出現(xiàn)在任何函數(shù)之外的全局變量:int sum;
            • 堆(Heap)。這個段用于在程序中進行動態(tài)內(nèi)存申請,例如經(jīng)常用到的malloc,new系列函數(shù)就是從這個段中申請內(nèi)存。
            • 棧(Stack)。函數(shù)中的局部變量以及在函數(shù)調(diào)用過程中產(chǎn)生的臨時變量都保存在此段中。

            內(nèi)存檢查原理

            Memcheck檢測內(nèi)存問題的原理如下圖所示:


            圖 3 內(nèi)存檢查原理
            內(nèi)存檢查原理 

            Memcheck 能夠檢測出內(nèi)存問題,關(guān)鍵在于其建立了兩個全局表。

            1. Valid-Value 表:

            對于進程的整個地址空間中的每一個字節(jié)(byte),都有與之對應(yīng)的 8 個 bits;對于 CPU 的每個寄存器,也有一個與之對應(yīng)的 bit 向量。這些 bits 負(fù)責(zé)記錄該字節(jié)或者寄存器值是否具有有效的、已初始化的值。

            1. Valid-Address 

            對于進程整個地址空間中的每一個字節(jié)(byte),還有與之對應(yīng)的 1 個 bit,負(fù)責(zé)記錄該地址是否能夠被讀寫。

            檢測原理:

            • 當(dāng)要讀寫內(nèi)存中某個字節(jié)時,首先檢查這個字節(jié)對應(yīng)的 A bit。如果該A bit顯示該位置是無效位置,memcheck 則報告讀寫錯誤。
            • 內(nèi)核(core)類似于一個虛擬的 CPU 環(huán)境,這樣當(dāng)內(nèi)存中的某個字節(jié)被加載到真實的 CPU 中時,該字節(jié)對應(yīng)的 V bit 也被加載到虛擬的 CPU 環(huán)境中。一旦寄存器中的值,被用來產(chǎn)生內(nèi)存地址,或者該值能夠影響程序輸出,則 memcheck 會檢查對應(yīng)的V bits,如果該值尚未初始化,則會報告使用未初始化內(nèi)存錯誤。

            回頁首

            Valgrind 使用

            第一步:準(zhǔn)備好程序

            為了使valgrind發(fā)現(xiàn)的錯誤更精確,如能夠定位到源代碼行,建議在編譯時加上-g參數(shù),編譯優(yōu)化選項請選擇O0,雖然這會降低程序的執(zhí)行效率。

            這里用到的示例程序文件名為:sample.c(如下所示),選用的編譯器為gcc。

            生成可執(zhí)行程序 gcc –g –O0 sample.c –o sample


            清單 1
            清單 1 

            第二步:在valgrind下,運行可執(zhí)行程序。

            利用valgrind調(diào)試內(nèi)存問題,不需要重新編譯源程序,它的輸入就是二進制的可執(zhí)行程序。調(diào)用Valgrind的通用格式是:valgrind [valgrind-options] your-prog [your-prog-options]

            Valgrind 的參數(shù)分為兩類,一類是 core 的參數(shù),它對所有的工具都適用;另外一類就是具體某個工具如 memcheck 的參數(shù)。Valgrind 默認(rèn)的工具就是 memcheck,也可以通過“--tool=tool name”指定其他的工具。Valgrind 提供了大量的參數(shù)滿足你特定的調(diào)試需求,具體可參考其用戶手冊。

            這個例子將使用 memcheck,于是可以輸入命令入下:valgrind <Path>/sample.

            第三步:分析 valgrind 的輸出信息。

            以下是運行上述命令后的輸出。


            清單 2
            清單 2 
            • 左邊顯示類似行號的數(shù)字(32372)表示的是 Process ID。
            • 最上面的紅色方框表示的是 valgrind 的版本信息。
            • 中間的紅色方框表示 valgrind 通過運行被測試程序,發(fā)現(xiàn)的內(nèi)存問題。通過閱讀這些信息,可以發(fā)現(xiàn):
              1. 這是一個對內(nèi)存的非法寫操作,非法寫操作的內(nèi)存是4 bytes。
              2. 發(fā)生錯誤時的函數(shù)堆棧,以及具體的源代碼行號。
              3. 非法寫操作的具體地址空間。
            • 最下面的紅色方框是對發(fā)現(xiàn)的內(nèi)存問題和內(nèi)存泄露問題的總結(jié)。內(nèi)存泄露的大小(40 bytes)也能夠被檢測出來。

            示例程序顯然有兩個問題,一是fun函數(shù)中動態(tài)申請的堆內(nèi)存沒有釋放;二是對堆內(nèi)存的訪問越界。這兩個問題均被valgrind發(fā)現(xiàn)。

            回頁首

            利用Memcheck發(fā)現(xiàn)常見的內(nèi)存問題

            在Linux平臺開發(fā)應(yīng)用程序時,最常遇見的問題就是錯誤的使用內(nèi)存,我們總結(jié)了常見了內(nèi)存錯誤使用情況,并說明了如何用valgrind將其檢測出來。

            使用未初始化的內(nèi)存

            問題分析:

            對于位于程序中不同段的變量,其初始值是不同的,全局變量和靜態(tài)變量初始值為0,而局部變量和動態(tài)申請的變量,其初始值為隨機值。如果程序使用了為隨機值的變量,那么程序的行為就變得不可預(yù)期。

            下面的程序就是一種常見的,使用了未初始化的變量的情況。數(shù)組a是局部變量,其初始值為隨機值,而在初始化時并沒有給其所有數(shù)組成員初始化,如此在接下來使用這個數(shù)組時就潛在有內(nèi)存問題。


            清單 3
            清單 3 

            結(jié)果分析:

            假設(shè)這個文件名為:badloop.c,生成的可執(zhí)行程序為badloop。用memcheck對其進行測試,輸出如下。


            清單 4
            清單 4 

            輸出結(jié)果顯示,在該程序第11行中,程序的跳轉(zhuǎn)依賴于一個未初始化的變量。準(zhǔn)確的發(fā)現(xiàn)了上述程序中存在的問題。

            內(nèi)存讀寫越界

            問題分析:

            這種情況是指:訪問了你不應(yīng)該/沒有權(quán)限訪問的內(nèi)存地址空間,比如訪問數(shù)組時越界;對動態(tài)內(nèi)存訪問時超出了申請的內(nèi)存大小范圍。下面的程序就是一個典型的數(shù)組越界問題。pt是一個局部數(shù)組變量,其大小為4,p初始指向pt數(shù)組的起始地址,但在對p循環(huán)疊加后,p超出了pt數(shù)組的范圍,如果此時再對p進行寫操作,那么后果將不可預(yù)期。


            清單 5
            清單 5 

            結(jié)果分析:

            假設(shè)這個文件名為badacc.cpp,生成的可執(zhí)行程序為badacc,用memcheck對其進行測試,輸出如下。


            清單 6
            清單 6 

            輸出結(jié)果顯示,在該程序的第15行,進行了非法的寫操作;在第16行,進行了非法讀操作。準(zhǔn)確地發(fā)現(xiàn)了上述問題。

            內(nèi)存覆蓋

            問題分析:

            C 語言的強大和可怕之處在于其可以直接操作內(nèi)存,C 標(biāo)準(zhǔn)庫中提供了大量這樣的函數(shù),比如 strcpy, strncpy, memcpy, strcat 等,這些函數(shù)有一個共同的特點就是需要設(shè)置源地址 (src),和目標(biāo)地址(dst),src 和 dst 指向的地址不能發(fā)生重疊,否則結(jié)果將不可預(yù)期。

            下面就是一個 src 和 dst 發(fā)生重疊的例子。在 15 與 17 行中,src 和 dst 所指向的地址相差 20,但指定的拷貝長度卻是 21,這樣就會把之前的拷貝值覆蓋。第 24 行程序類似,src(x+20) 與 dst(x) 所指向的地址相差 20,但 dst 的長度卻為 21,這樣也會發(fā)生內(nèi)存覆蓋。


            清單 7
            清單 7 

            結(jié)果分析:

            假設(shè)這個文件名為 badlap.cpp,生成的可執(zhí)行程序為 badlap,用 memcheck 對其進行測試,輸出如下。


            清單 8
            清單 8 

            輸出結(jié)果顯示上述程序中第15,17,24行,源地址和目標(biāo)地址設(shè)置出現(xiàn)重疊。準(zhǔn)確的發(fā)現(xiàn)了上述問題。

            動態(tài)內(nèi)存管理錯誤

            問題分析:

            常見的內(nèi)存分配方式分三種:靜態(tài)存儲,棧上分配,堆上分配。全局變量屬于靜態(tài)存儲,它們是在編譯時就被分配了存儲空間,函數(shù)內(nèi)的局部變量屬于棧上分配,而最靈活的內(nèi)存使用方式當(dāng)屬堆上分配,也叫做內(nèi)存動態(tài)分配了。常用的內(nèi)存動態(tài)分配函數(shù)包括:malloc, alloc, realloc, new等,動態(tài)釋放函數(shù)包括free, delete。

            一旦成功申請了動態(tài)內(nèi)存,我們就需要自己對其進行內(nèi)存管理,而這又是最容易犯錯誤的。下面的一段程序,就包括了內(nèi)存動態(tài)管理中常見的錯誤。


            清單 9
            清單 9 

            常見的內(nèi)存動態(tài)管理錯誤包括:

              • 申請和釋放不一致

            由于 C++ 兼容 C,而 C 與 C++ 的內(nèi)存申請和釋放函數(shù)是不同的,因此在 C++ 程序中,就有兩套動態(tài)內(nèi)存管理函數(shù)。一條不變的規(guī)則就是采用 C 方式申請的內(nèi)存就用 C 方式釋放;用 C++ 方式申請的內(nèi)存,用 C++ 方式釋放。也就是用 malloc/alloc/realloc 方式申請的內(nèi)存,用 free 釋放;用 new 方式申請的內(nèi)存用 delete 釋放。在上述程序中,用 malloc 方式申請了內(nèi)存卻用 delete 來釋放,雖然這在很多情況下不會有問題,但這絕對是潛在的問題。

              • 申請和釋放不匹配

            申請了多少內(nèi)存,在使用完成后就要釋放多少。如果沒有釋放,或者少釋放了就是內(nèi)存泄露;多釋放了也會產(chǎn)生問題。上述程序中,指針p和pt指向的是同一塊內(nèi)存,卻被先后釋放兩次。

              • 釋放后仍然讀寫

            本質(zhì)上說,系統(tǒng)會在堆上維護一個動態(tài)內(nèi)存鏈表,如果被釋放,就意味著該塊內(nèi)存可以繼續(xù)被分配給其他部分,如果內(nèi)存被釋放后再訪問,就可能覆蓋其他部分的信息,這是一種嚴(yán)重的錯誤,上述程序第16行中就在釋放后仍然寫這塊內(nèi)存。

            結(jié)果分析:

            假設(shè)這個文件名為badmac.cpp,生成的可執(zhí)行程序為badmac,用memcheck對其進行測試,輸出如下。


            清單 10
            清單 10 

            輸出結(jié)果顯示,第14行分配和釋放函數(shù)不一致;第16行發(fā)生非法寫操作,也就是往釋放后的內(nèi)存地址寫值;第17行釋放內(nèi)存函數(shù)無效。準(zhǔn)確地發(fā)現(xiàn)了上述三個問題。

            內(nèi)存泄露

            問題描述:

            內(nèi)存泄露(Memory leak)指的是,在程序中動態(tài)申請的內(nèi)存,在使用完后既沒有釋放,又無法被程序的其他部分訪問。內(nèi)存泄露是在開發(fā)大型程序中最令人頭疼的問題,以至于有人說,內(nèi)存泄露是無法避免的。其實不然,防止內(nèi)存泄露要從良好的編程習(xí)慣做起,另外重要的一點就是要加強單元測試(Unit Test),而memcheck就是這樣一款優(yōu)秀的工具。

            下面是一個比較典型的內(nèi)存泄露案例。main函數(shù)調(diào)用了mk函數(shù)生成樹結(jié)點,可是在調(diào)用完成之后,卻沒有相應(yīng)的函數(shù):nodefr釋放內(nèi)存,這樣內(nèi)存中的這個樹結(jié)構(gòu)就無法被其他部分訪問,造成了內(nèi)存泄露。

            在一個單獨的函數(shù)中,每個人的內(nèi)存泄露意識都是比較強的。但很多情況下,我們都會對malloc/free 或new/delete做一些包裝,以符合我們特定的需要,無法做到在一個函數(shù)中既使用又釋放。這個例子也說明了內(nèi)存泄露最容易發(fā)生的地方:即兩個部分的接口部分,一個函數(shù)申請內(nèi)存,一個函數(shù)釋放內(nèi)存。并且這些函數(shù)由不同的人開發(fā)、使用,這樣造成內(nèi)存泄露的可能性就比較大了。這需要養(yǎng)成良好的單元測試習(xí)慣,將內(nèi)存泄露消滅在初始階段。


            清單 11
            清單 1 

            清單 11.2
            清單 11.2 

            清單 11.3
            清單 11.3 

            結(jié)果分析:

            假設(shè)上述文件名位tree.h, tree.cpp, badleak.cpp,生成的可執(zhí)行程序為badleak,用memcheck對其進行測試,輸出如下。


            清單 12
            清單 12 

            該示例程序是生成一棵樹的過程,每個樹節(jié)點的大小為12(考慮內(nèi)存對齊),共8個節(jié)點。從上述輸出可以看出,所有的內(nèi)存泄露都被發(fā)現(xiàn)。Memcheck將內(nèi)存泄露分為兩種,一種是可能的內(nèi)存泄露(Possibly lost),另外一種是確定的內(nèi)存泄露(Definitely lost)。Possibly lost 是指仍然存在某個指針能夠訪問某塊內(nèi)存,但該指針指向的已經(jīng)不是該內(nèi)存首地址。Definitely lost 是指已經(jīng)不能夠訪問這塊內(nèi)存。而Definitely lost又分為兩種:直接的(direct)和間接的(indirect)。直接和間接的區(qū)別就是,直接是沒有任何指針指向該內(nèi)存,間接是指指向該內(nèi)存的指針都位于內(nèi)存泄露處。在上述的例子中,根節(jié)點是directly lost,而其他節(jié)點是indirectly lost。

            回頁首

            總結(jié)

            本文介紹了valgrind的體系結(jié)構(gòu),并重點介紹了其應(yīng)用最廣泛的工具:memcheck。闡述了memcheck發(fā)現(xiàn)內(nèi)存問題的基本原理,基本使用方法,以及利用memcheck如何發(fā)現(xiàn)目前開發(fā)中最廣泛的五大類內(nèi)存問題。在項目中盡早的發(fā)現(xiàn)內(nèi)存問題,能夠極大地提高開發(fā)效率,valgrind就是能夠幫助你實現(xiàn)這一目標(biāo)的出色工具。

            posted @ 2011-08-11 17:22 simplyzhao 閱讀(321) | 評論 (0)編輯 收藏
            一、什么是對齊,以及為什么要對齊:

            1. 現(xiàn)代計算機中內(nèi)存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定變量的時候經(jīng)常在特定的內(nèi)存地址訪問,這就需要各類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

            2. 對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數(shù)據(jù)只能從某些特定地址開始存取。其他平臺可能沒有這種情況, 但是最常見的是如果不按照適合其平臺的要求對數(shù)據(jù)存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設(shè)為 32位)如果存放在偶地址開始的地方,那么一個讀周期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀周期,并對兩次讀出的結(jié)果的高低 字節(jié)進行拼湊才能得到該int數(shù)據(jù)。顯然在讀取效率上下降很多。這也是空間和時間的博弈。

            二、對齊的實現(xiàn)

            通常,我們寫程序的時候,不需要考慮對齊問題。編譯器會替我們選擇適合目標(biāo)平臺的對齊策略。當(dāng)然,我們也可以通知給編譯器傳遞預(yù)編譯指令而改變對指定數(shù)據(jù)的對齊方法。
            但是,正因為我們一般不需要關(guān)心這個問題,所以因為編輯器對數(shù)據(jù)存放做了對齊,而我們不了解的話,常常會對一些問題感到迷惑。最常見的就是struct數(shù)據(jù)結(jié)構(gòu)的sizeof結(jié)果,出乎意料。為此,我們需要對對齊算法所了解。
            對齊的算法:
            由于各個平臺和編譯器的不同,現(xiàn)以本人使用的gcc version 3.2.2編譯器(32位x86平臺)為例子,來討論編譯器對struct數(shù)據(jù)結(jié)構(gòu)中的各成員如何進行對齊的。
            設(shè)結(jié)構(gòu)體如下定義:
            struct A {
                int a;
                char b;
                short c;
            };
            結(jié)構(gòu)體A中包含了4字節(jié)長度的int一個,1字節(jié)長度的char一個和2字節(jié)長度的short型數(shù)據(jù)一個。所以A用到的空間應(yīng)該是7字節(jié)。但是因為編譯器要對數(shù)據(jù)成員在空間上進行對齊。
            所以使用sizeof(strcut A)值為8。
            現(xiàn)在把該結(jié)構(gòu)體調(diào)整成員變量的順序。
            struct B {
                char b;
                int a;
                short c;
            };
            這時候同樣是總共7個字節(jié)的變量,但是sizeof(struct B)的值卻是12。
            下面我們使用預(yù)編譯指令#pragma pack (value)來告訴編譯器,使用我們指定的對齊值來取代缺省的。
            #progma pack (2) /*指定按2字節(jié)對齊*/
            struct C {
                char b;
                int a;
                short c;
            };
            #progma pack () /*取消指定對齊,恢復(fù)缺省對齊*/
            sizeof(struct C)值是8。

            修改對齊值為1:
            #progma pack (1) /*指定按1字節(jié)對齊*/
            struct D {
                char b;
                int a;
                short c;
            };
            #progma pack () /*取消指定對齊,恢復(fù)缺省對齊*/
            sizeof(struct D)值為7。

            對于char型數(shù)據(jù),其自身對齊值為1,對于short型為2,對于int,float,double類型,其自身對齊值為4,單位字節(jié)。
            這里面有四個概念值:
            1)數(shù)據(jù)類型自身的對齊值:就是上面交代的基本數(shù)據(jù)類型的自身對齊值。

            2)指定對齊值:#pragma pack (value)時的指定對齊值value。

            3)結(jié)構(gòu)體或者類的自身對齊值:其成員中自身對齊值最大的那個值。

            4)數(shù)據(jù)成員、結(jié)構(gòu)體和類的有效對齊值:自身對齊值和指定對齊值中較小的那個值。

                   有了這些值,我們就可以很方便的來討論具體數(shù)據(jù)結(jié)構(gòu)的成員和其自身的對齊方式。有效對齊值N是最終用來決定數(shù)據(jù)存放地址方式的值,最重要。有效對齊N,就是表示“對齊在N上”,也就是說該數(shù)據(jù)的"存放起始地址%N=0".而數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)變量都是按定義的先后順序來排放的。第一個數(shù)據(jù)變量的起始地址就是 數(shù)據(jù)結(jié)構(gòu)的起始地址。結(jié)構(gòu)體的成員變量要對齊排放,結(jié)構(gòu)體本身也要根據(jù)自身的有效對齊值圓整(就是結(jié)構(gòu)體成員變量占用總長度需要是對結(jié)構(gòu)體有效對齊值的整 數(shù)倍,結(jié)合下面例子理解)。這樣就不難理解上面的幾個例子的值了。
            例子分析:
            分析例子B;
            struct B {
                char b;
                int a;
                short c;
            };
            假設(shè)B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環(huán)境下,該值默認(rèn)為4。第一個成員變量b的自身對齊值是1,比指定或者默認(rèn)指 定對齊值4小,所以其有效對齊值為1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值為4,所以有效對齊值也為 4,所以只能存放在起始地址為0x0004到0x0007這四個連續(xù)的字節(jié)空間中,復(fù)核0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊 值為2,所以有效對齊值也是2,可以存放在0x0008到0x0009這兩個字節(jié)空間中,符合0x0008%2=0。所以從0x0000到0x0009存 放的都是B內(nèi)容。再看數(shù)據(jù)結(jié)構(gòu)B的自身對齊值為其變量中最大對齊值(這里是b)所以就是4,所以結(jié)構(gòu)體的有效對齊值也是4。根據(jù)結(jié)構(gòu)體圓整的要求, 0x0009到0x0000=10字節(jié),(10+2)%4=0。所以0x0000A到0x000B也為結(jié)構(gòu)體B所占用。故B從0x0000到0x000B 共有12個字節(jié),sizeof(struct B)=12;

            同理,分析上面例子C:
            #pragma pack (2) /*指定按2字節(jié)對齊*/
            struct C {
                char b;
                int a;
                short c;
            };
            #pragma pack () /*取消指定對齊,恢復(fù)缺省對齊*/
            第一個變量b的自身對齊值為1,指定對齊值為2,所以,其有效對齊值為1,假設(shè)C從0x0000開始,那么b存放在0x0000,符合0x0000%1= 0;第二個變量,自身對齊值為4,指定對齊值為2,所以有效對齊值為2,所以順序存放在0x0002、0x0003、0x0004、0x0005四個連續(xù) 字節(jié)中,符合0x0002%2=0。第三個變量c的自身對齊值為2,所以有效對齊值為2,順序存放
            在0x0006、0x0007中,符合0x0006%2=0。所以從0x0000到0x00007共八字節(jié)存放的是C的變量。又C的自身對齊值為4,所以 C的有效對齊值為2。又8%2=0,C只占用0x0000到0x0007的八個字節(jié)。所以sizeof(struct C)=8.

            有 了以上的解釋,相信你對C語言的字節(jié)對齊概念應(yīng)該有了清楚的認(rèn)識了吧。在網(wǎng)絡(luò)程序中,掌握這個概念可是很重要的喔,在不同平臺之間(比如在Windows 和Linux之間)傳遞2進制流(比如結(jié)構(gòu)體),那么在這兩個平臺間必須要定義相同的對齊方式,不然莫名其妙的出了一些錯,可是很難排查的哦^_^。
            posted @ 2011-08-10 19:18 simplyzhao 閱讀(130) | 評論 (0)編輯 收藏

            原文標(biāo)題:Anatomy of a Program in Memory

            原文地址:http://duartes.org/gustavo/blog/

             

            [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復(fù)習(xí),二來與大家分享。]

             

                內(nèi)存管理模塊是操作系統(tǒng)的心臟;它對應(yīng)用程序和系統(tǒng)管理非常重要。今后的幾篇文章中,我將著眼于實際的內(nèi)存問題,但也不避諱其中的技術(shù)內(nèi)幕。由于不少概念是通用的,所以文中大部分例子取自32x86平臺的LinuxWindows系統(tǒng)。本系列第一篇文章講述應(yīng)用程序的內(nèi)存布局。

             

                在多任務(wù)操作系統(tǒng)中的每一個進程都運行在一個屬于它自己的內(nèi)存沙盤中。這個沙盤就是虛擬地址空間virtual address space),在32位模式下它總是一個4GB的內(nèi)存地址塊。這些虛擬地址通過頁表page table)映射到物理內(nèi)存,頁表由操作系統(tǒng)維護并被處理器引用。每一個進程擁有一套屬于它自己的頁表,但是還有一個隱情。只要虛擬地址被使能,那么它就會作用于這臺機器上運行的所有軟件,包括內(nèi)核本身。因此一部分虛擬地址必須保留給內(nèi)核使用:

             

             

             

             

             

                這并不意味著內(nèi)核使用了那么多的物理內(nèi)存,僅表示它可支配這么大的地址空間,可根據(jù)內(nèi)核需要,將其映射到物理內(nèi)存。內(nèi)核空間在頁表中擁有較高的特權(quán)級ring 2或以下),因此只要用戶態(tài)的程序試圖訪問這些頁,就會導(dǎo)致一個頁錯誤(page fault)。在Linux中,內(nèi)核空間是持續(xù)存在的,并且在所有進程中都映射到同樣的物理內(nèi)存。內(nèi)核代碼和數(shù)據(jù)總是可尋址的,隨時準(zhǔn)備處理中斷和系統(tǒng)調(diào)用。與此相反,用戶模式地址空間的映射隨進程切換的發(fā)生而不斷變化:

             

             

             

             

                藍色區(qū)域表示映射到物理內(nèi)存的虛擬地址,而白色區(qū)域表示未映射的部分。在上面的例子中,Firefox使用了相當(dāng)多的虛擬地址空間,因為它是傳說中的吃內(nèi)存大戶。地址空間中的各個條帶對應(yīng)于不同的內(nèi)存段(memory segment),如:堆、棧之類的。記住,這些段只是簡單的內(nèi)存地址范圍,與Intel處理器的段沒有關(guān)系。不管怎樣,下面是一個Linux進程的標(biāo)準(zhǔn)的內(nèi)存段布局:

             

             

             

                當(dāng)計算機開心、安全、可愛、正常的運轉(zhuǎn)時,幾乎每一個進程的各個段的起始虛擬地址都與上圖完全一致,這也給遠(yuǎn)程發(fā)掘程序安全漏洞打開了方便之門。一個發(fā)掘過程往往需要引用絕對內(nèi)存地址:棧地址,庫函數(shù)地址等。遠(yuǎn)程攻擊者必須依賴地址空間布局的一致性,摸索著選擇這些地址。如果讓他們猜個正著,有人就會被整了。因此,地址空間的隨機排布方式逐漸流行起來。Linux通過對內(nèi)存映射的起始地址加上隨機的偏移量來打亂布局。不幸的是,32位地址空間相當(dāng)緊湊,給隨機化所留下的空當(dāng)不大,削弱了這種技巧的效果

             

                進程地址空間中最頂部的段是棧,大多數(shù)編程語言將之用于存儲局部變量和函數(shù)參數(shù)。調(diào)用一個方法或函數(shù)會將一個新的棧楨stack frame)壓入棧中。棧楨在函數(shù)返回時被清理。也許是因為數(shù)據(jù)嚴(yán)格的遵從LIFO的順序,這個簡單的設(shè)計意味著不必使用復(fù)雜的數(shù)據(jù)結(jié)構(gòu)來追蹤棧的內(nèi)容,只需要一個簡單的指針指向棧的頂端即可。因此壓棧(pushing)和退棧(popping)過程非常迅速、準(zhǔn)確。另外,持續(xù)的重用棧空間有助于使活躍的棧內(nèi)存保持在CPU緩存中,從而加速訪問。進程中的每一個線程都有屬于自己的棧。

             

                通過不斷向棧中壓入的數(shù)據(jù),超出其容量就有會耗盡棧所對應(yīng)的內(nèi)存區(qū)域。這將觸發(fā)一個頁故障(page fault),并被Linuxexpand_stack()處理,它會調(diào)用acct_stack_growth()來檢查是否還有合適的地方用于棧的增長。如果棧的大小低于RLIMIT_STACK(通常是8MB),那么一般情況下棧會被加長,程序繼續(xù)愉快的運行,感覺不到發(fā)生了什么事情。這是一種將棧擴展至所需大小的常規(guī)機制。然而,如果達到了最大的棧空間大小,就會棧溢出(stack overflow),程序收到一個段錯誤(Segmentation Fault)。當(dāng)映射了的棧區(qū)域擴展到所需的大小后,它就不會再收縮回去,即使棧不那么滿了。這就好比聯(lián)邦預(yù)算,它總是在增長的。

             

                動態(tài)棧增長是唯一一種訪問未映射內(nèi)存區(qū)域(圖中白色區(qū)域)而被允許的情形。其它任何對未映射內(nèi)存區(qū)域的訪問都會觸發(fā)頁故障,從而導(dǎo)致段錯誤。一些被映射的區(qū)域是只讀的,因此企圖寫這些區(qū)域也會導(dǎo)致段錯誤。

             

                在棧的下方,是我們的內(nèi)存映射段。此處,內(nèi)核將文件的內(nèi)容直接映射到內(nèi)存。任何應(yīng)用程序都可以通過Linuxmmap()系統(tǒng)調(diào)用(實現(xiàn))或WindowsCreateFileMapping() MapViewOfFile()請求這種映射。內(nèi)存映射是一種方便高效的文件I/O方式,所以它被用于加載動態(tài)庫。創(chuàng)建一個不對應(yīng)于任何文件的匿名內(nèi)存映射也是可能的,此方法用于存放程序的數(shù)據(jù)。在Linux中,如果你通過malloc()請求一大塊內(nèi)存,C運行庫將會創(chuàng)建這樣一個匿名映射而不是使用堆內(nèi)存。‘大塊’意味著比MMAP_THRESHOLD還大,缺省是128KB,可以通過mallopt()調(diào)整。

             

                說到堆,它是接下來的一塊地址空間。與棧一樣,堆用于運行時內(nèi)存分配;但不同點是,堆用于存儲那些生存期與函數(shù)調(diào)用無關(guān)的數(shù)據(jù)。大部分語言都提供了堆管理功能。因此,滿足內(nèi)存請求就成了語言運行時庫及內(nèi)核共同的任務(wù)。在C語言中,堆分配的接口是malloc()系列函數(shù),而在具有垃圾收集功能的語言(如C#)中,此接口是new關(guān)鍵字。

             

                如果堆中有足夠的空間來滿足內(nèi)存請求,它就可以被語言運行時庫處理而不需要內(nèi)核參與。否則,堆會被擴大,通過brk()系統(tǒng)調(diào)用(實現(xiàn))來分配請求所需的內(nèi)存塊。堆管理是很復(fù)雜的,需要精細(xì)的算法,應(yīng)付我們程序中雜亂的分配模式,優(yōu)化速度和內(nèi)存使用效率。處理一個堆請求所需的時間會大幅度的變動。實時系統(tǒng)通過特殊目的分配器來解決這個問題。堆也可能會變得零零碎碎,如下圖所示:

             

             

             

                最后,我們來看看最底部的內(nèi)存段:BSS,數(shù)據(jù)段,代碼段。在C語言中,BSS和數(shù)據(jù)段保存的都是靜態(tài)(全局)變量的內(nèi)容。區(qū)別在于BSS保存的是未被初始化的靜態(tài)變量內(nèi)容,它們的值不是直接在程序的源代碼中設(shè)定的。BSS內(nèi)存區(qū)域是匿名的:它不映射到任何文件。如果你寫static int cntActiveUsers,則cntActiveUsers的內(nèi)容就會保存在BSS中。

             

                另一方面,數(shù)據(jù)段保存在源代碼中已經(jīng)初始化了的靜態(tài)變量內(nèi)容。這個內(nèi)存區(qū)域不是匿名的。它映射了一部分的程序二進制鏡像,也就是源代碼中指定了初始值的靜態(tài)變量。所以,如果你寫static int cntWorkerBees = 10,則cntWorkerBees的內(nèi)容就保存在數(shù)據(jù)段中了,而且初始值為10。盡管數(shù)據(jù)段映射了一個文件,但它是一個私有內(nèi)存映射,這意味著更改此處的內(nèi)存不會影響到被映射的文件。也必須如此,否則給全局變量賦值將會改動你硬盤上的二進制鏡像,這是不可想象的。

             

                下圖中數(shù)據(jù)段的例子更加復(fù)雜,因為它用了一個指針。在此情況下,指針gonzo4字節(jié)內(nèi)存地址)本身的值保存在數(shù)據(jù)段中。而它所指向的實際字符串則不在這里。這個字符串保存在代碼段中,代碼段是只讀的,保存了你全部的代碼外加零零碎碎的東西,比如字符串字面值。代碼段將你的二進制文件也映射到了內(nèi)存中,但對此區(qū)域的寫操作都會使你的程序收到段錯誤。這有助于防范指針錯誤,雖然不像在C語言編程時就注意防范來得那么有效。下圖展示了這些段以及我們例子中的變量:

             

             

             

                你可以通過閱讀文件/proc/pid_of_process/maps來檢驗一個Linux進程中的內(nèi)存區(qū)域。記住一個段可能包含許多區(qū)域。比如,每個內(nèi)存映射文件在mmap段中都有屬于自己的區(qū)域,動態(tài)庫擁有類似BSS和數(shù)據(jù)段的額外區(qū)域。下一篇文章講說明這些“區(qū)域”(area)的真正含義。有時人們提到“數(shù)據(jù)段”,指的就是全部的數(shù)據(jù)段 + BSS + 堆。

             

                你可以通過nmobjdump命令來察看二進制鏡像,打印其中的符號,它們的地址,段等信息。最后需要指出的是,前文描述的虛擬地址布局在Linux中是一種“靈活布局”(flexible layout),而且以此作為默認(rèn)方式已經(jīng)有些年頭了。它假設(shè)我們有值RLIMIT_STACK。當(dāng)情況不是這樣時,Linux退回使用“經(jīng)典布局”(classic layout),如下圖所示:

             

             

             

                對虛擬地址空間的布局就講這些吧。下一篇文章將討論內(nèi)核是如何跟蹤這些內(nèi)存區(qū)域的。我們會分析內(nèi)存映射,看看文件的讀寫操作是如何與之關(guān)聯(lián)的,以及內(nèi)存使用概況的含義

            posted @ 2011-08-10 17:11 simplyzhao 閱讀(461) | 評論 (0)編輯 收藏
            數(shù)組與指針,究竟有什么關(guān)聯(lián)?什么情況下等同,什么時候不等同? 老問題,新收獲
            前兩天看《C專家編程》,結(jié)果硬是卡在數(shù)組與指針這塊好久,原本以為自己已經(jīng)掌握的東西,原來還是沒有掌握精髓

            拋出兩個問題,然后我們在解決這兩個問題的過程中review指針與數(shù)組:
            問題一:
            char arr[] = "abcdefg";
            char *ptr = "abcdefg";
            我們知道,arr[i]與ptr[i]都是正確的,但兩者又有什么根本的差異呢?如果是char (*pa)[10]與char **pp,那么pa[1][2]與pp[1][2]有什么區(qū)別呢?

            問題二:
            qsort在針對指針數(shù)組與二維數(shù)組排序時的區(qū)別?

            ----------------------------------------------------------------------------------------------------------

            問題一:
            首先,要明確一點,編譯器為每個標(biāo)識符分配一個地址,這個地址在編譯階段可知,相反,存儲在標(biāo)識符中的值只有在運行時才可知

            對于數(shù)組,有 &arr = arr = &(arr[0])
            對于指針,有 &ptr != ptr = &(ptr[0])
            對于編譯器,它所知道的是&arr與&ptr,這兩個標(biāo)識符的地址
            基于上述三條(前兩條,寫個簡單的程序即可測試),即可得出結(jié)論: 雖然arr[i]與ptr[i]都能正確訪問某個字符(事實上,都會被編譯器改寫成*(arr+i)或者*(ptr+i)的形式),但是兩者生成的匯編代碼是不同的

            驗證:
            C語言代碼:
            char arr[] = "abcdefg";
            char *ptr = "abcdefg";

            void
            difference()
            {
                
            char a = arr[3];
                
            char b = ptr[3];
            }

            匯編代碼(gcc -S test.c)
            difference:
                pushl    
            %ebp
                movl    
            %esp, %ebp
                subl    $
            16%esp

                /* char a = arr[3] */
                movzbl    arr
            +3%eax /* 取地址(arr+3)處的值,放入寄存器 */
                movb    
            %al, -1(%ebp)

                /* char b = ptr[3] */
                movl    ptr, 
            %eax /* 取ptr的值(Mem[ptr]),放入寄存器 */
                addl    $
            3%eax
                movzbl    (
            %eax), %eax /* 將寄存器%eax的值作為地址,取該地址的值放入寄存器 */
                movb    
            %al, -2(%ebp)

                leave
                ret

            略微對上述匯編代碼進行了注釋,可以發(fā)現(xiàn)兩者的差異,指針跟數(shù)組相比,前者多了“一層間接引用”
            (Note: 匯編尋址,例如,$Imm是立即數(shù),Imm是絕對尋址(=Mem[Imm]),Ea是寄存器尋址,(Ea)則是間接尋址(=Mem[Register[Ea]]);可參考《深入理解計算機系統(tǒng)》第3章)

            如果是char (*pa)[10]與char **pp,pa是指向數(shù)組的指針,而pp是指向指針的指針,這里有點類似于之前的討論
            需要明白的就是: pa是指向數(shù)組的指針,保存的就是數(shù)組的地址;數(shù)組的地址等于數(shù)組首元素的地址;pa[i]等同于一元數(shù)組名稱,就是指向數(shù)組首元素的指針,也保存的是數(shù)組首元素的地址,因此有: pa+i(指向數(shù)組的指針) = pa[i] = &(pa[i][0])
            表達式pa[1][2]與pp[1][2]所產(chǎn)生的匯編代碼也是不同的
            (Note: 指向數(shù)組的指針p與數(shù)組名a(指向數(shù)組首元素的指針),兩者的值相同,但含義不一樣,例如p+1與a+1就完全不同)
            驗證:
            C語言代碼:
            void
            array_print(
            char (*pa)[10])
            {
                
            char c = pa[1][2];
            }

            void
            pointer_print(
            char **pp)
            {
                
            char c = pp[1][2];
            }

            匯編代碼:
            array_print:
                pushl    
            %ebp
                movl    
            %esp, %ebp
                subl    $
            16%esp

                movl    
            8(%ebp), %eax /* 將pa的值(Mem[pa])放入寄存器 */
                addl    $
            10%eax 
                movzbl    
            2(%eax), %eax
                movb    
            %al, -1(%ebp)

                leave
                ret

            pointer_print:
                pushl    
            %ebp
                movl    
            %esp, %ebp
                subl    $
            16%esp

                movl    
            8(%ebp), %eax /* 將pp的值放入寄存器 */    addl    $4%eax
                movl    (
            %eax), %eax
                addl    $
            2%eax
                movzbl    (
            %eax), %eax
                movb    
            %al, -1(%ebp)

                leave
                ret

            問題二:
            有了前面的講解,這里直接上代碼
            #include "basic.h"

            char *strings[] = { "zhao"
                
            "pan",
                
            "anan",
                NULL };

            char arr[][5= { "zhao",
                
            "pan",
                
            "anan" };

            void
            test_a2_addr()
            {
                printf(
            "Addr Testing\n");
                printf(
            "addr(arr+1): %p\n", arr+1);
                printf(
            "addr(arr[1]): %p\n", arr[1]);
                printf(
            "addr(arr[1][0]): %p\n"&arr[1][0]);
            }

            int
            compare_pp(
            const void *arg1, const void *arg2)
            {
                
            return strcmp(*(char **)arg1, *(char **)arg2);
            }

            int
            compare_a2(
            const void *arg1, const void *arg2)
            {
                
            return strcmp(*(char (*)[5])arg1, *(char (*)[5])arg2);
                
            //return strcmp((char *)arg1, (char *)arg2); //ok,too
            }

            void
            test_pp()
            {    
                qsort(strings, 
            3sizeof(char *), compare_pp);

                printf(
            "\nResult of qsort for POINTER-ARRAY\n");
                
            char **= strings;
                
            while(*!= NULL) {
                    printf(
            "%s\n"*s);
                    
            ++s;
                }
            }

            void
            test_a2()
            {
                qsort(arr, 
            3sizeof(char)*5, compare_a2);
                
                printf(
            "\nResult of qsort for TWO-ARRAY\n");
                
            int i;
                
            for(i=0; i<3++i)
                    printf(
            "%s\n", arr[i]);
            }

            int
            main(
            int argc, char **argv)
            {
                test_a2_addr();
                test_pp();
                test_a2();
            }

            posted @ 2011-08-10 16:38 simplyzhao 閱讀(119) | 評論 (0)編輯 收藏
            比較排序算法的下界: O(nlogn)

            非比較排序: 位排序,計數(shù)排序,基數(shù)排序

            位排序適用于: 1. 輸入數(shù)據(jù)限制在相對較小的范圍內(nèi); 2. 數(shù)據(jù)沒有重復(fù); 3. 對于每條記錄而言,除了單一整數(shù)外,沒有任何其他關(guān)聯(lián)數(shù)據(jù)

            計數(shù)排序
            前提: n個輸入數(shù)據(jù)的每一個都是介于0到k之間的整數(shù)
            時間復(fù)雜度: O(n),如果k=O(n)
            基本思想: 對于每個輸入元素x,統(tǒng)計出小于等于x的元素的個數(shù),有了這個信息,就可以把x直接放到最終輸出數(shù)組的正確位置上
            特點: 計數(shù)排序是穩(wěn)定的。計數(shù)排序的穩(wěn)定性,對于基數(shù)排序是至關(guān)重要的
            代碼:
            /*
             * counting-sort: stable, O(n)
             * precondition: the value of n input integers must between 0 and k, k = O(n)
             
            */
            void
            counting_sort(
            int *array, int *ret, int n, int k)
            {
                
            int i, j, *count = (int *)malloc((k+1* sizeof(int));
                assert(count 
            != NULL);
                memset(count, 
            0sizeof(int)*(k+1));

                
            for(i=0; i<n; ++i) {
                    assert(array[i]
            >=0 && array[i]<=k);
                    
            ++count[array[i]];
                }

                
            for(i=1; i<=k; ++i)
                    count[i] 
            += count[i-1];

                
            for(j=n-1; j>=0--j) {
                    ret[count[array[j]]
            -1= array[j];
                    
            --count[array[j]];
                }

                free(count);
            }

            基數(shù)排序
            基本思想: 按照最低有效位到最高有效位的順序(這里的位,可以看作是關(guān)鍵值),采用一種穩(wěn)定性排序算法對該位進行排序
            偽代碼:
                 RADIX-SORT(A, d)
                        for i = 1..d /* 每一位,按低到高的順序 */
                              do use a stable sort to sort array A on digit i
            應(yīng)用舉例:
            假設(shè)我們想根據(jù)三個關(guān)鍵字年,月,日來對日期進行排序
            對于這個問題,可以用帶有比較函數(shù)的排序算法來做,而另一方面,我們可以采用另一種方法,即用一種穩(wěn)定的排序方法對所給的信息進行三次排序:先以日,其次對月,再對年
            posted @ 2011-08-10 15:15 simplyzhao 閱讀(135) | 評論 (0)編輯 收藏
            堆排序: 時間復(fù)雜度 O(nlogn),非穩(wěn)定排序

            比如:3 27 36 27
            如果堆頂3先輸出,則,第三層的27(最后一個27)跑到堆頂,然后堆穩(wěn)定,繼續(xù)輸出堆頂,是剛才那個27,這樣說明后面的27先于第二個位置的27輸出,不穩(wěn)定。

            #define LEFT(i) (((i)<<1)+1)
            #define RIGHT(i) (((i)+1)<<1)
            #define PARENT(i) (((i)-1)>>1)

            void
            min_heapify(
            int index, int *heap, int heap_size) //precondition: LEFT(index) and RIGHT(index) are both already min-heap
            {
                
            int min = index;
                
            if(LEFT(index)<heap_size && heap[LEFT(index)]<heap[min])
                    min 
            = LEFT(index);
                
            if(RIGHT(index)<heap_size && heap[RIGHT(index)]<heap[min])
                    min 
            = RIGHT(index);

                
            if(min != index) {
                    swap(heap
            +min, heap+index);
                    min_heapify(min, heap, heap_size);
                }
            }

            void
            build_heap(
            int *heap, int heap_size)
            {
                
            int i = (heap_size>>1);
                
            for( ; i>=0--i)
                    min_heapify(i, heap, heap_size);
            }

            void
            heap_sort(
            int *heap, int heap_size)
            {
                build_heap(heap, heap_size);

                
            while(heap_size > 1) {
                    swap(heap, heap
            +heap_size-1);
                    
            --heap_size;
                    min_heapify(
            0, heap, heap_size);
                }
            }
            posted @ 2011-07-29 20:25 simplyzhao 閱讀(100) | 評論 (0)編輯 收藏
            歸并排序: 平均時間復(fù)雜度與最壞時間復(fù)雜度都是O(nlogn),穩(wěn)定排序

            歸并排序是把序列遞歸地分成短序列,遞歸出口是短序列只有1個元素(認(rèn)為直接有序)或者2個序列(1次比較和交換),然后把各個有序的段序列合并成一個有序的長序列,不斷合并直到原序列全部排好序。可以發(fā)現(xiàn),在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩(wěn)定性。那么,在短的有序序列合并的過程中,穩(wěn)定是是否受到破壞?沒有,合并過程中我們可以保證如果兩個當(dāng)前元素相等時,我們把處在前面的序列的元素保存在結(jié)果序列的前面,這樣就保證了穩(wěn)定性。所以,歸并排序也是穩(wěn)定的排序算法。


            void
            merge(
            int *array, int *aux, int begin, int mid, int end)
            {
                
            int len = end - begin + 1;
                memcpy(aux
            +begin, array+begin, sizeof(int)*len);

                
            int *first, *second, *ptr = array+begin;
                first 
            = aux+begin;
                second 
            = aux+mid+1;
                
            while(first<=aux+mid && second<=aux+end) {
                    
            if(*first <= *second)
                        
            *ptr++ = *first++;
                    
            else
                        
            *ptr++ = *second++;
                }
                
            if(first <= aux+mid)
                    
            while(first <= aux+mid)
                        
            *ptr++ = *first++;
                
            if(second <= aux+end)
                    
            while(second <= aux+end)
                        
            *ptr++ = *second++;
            }

            void
            merge_sort_dc(
            int *array, int *aux, int begin, int end)
            {
                
            if(begin >= end)
                    
            return;
                
            int mid = begin + ((end-begin)>>1);
                merge_sort_dc(array, aux, begin, mid);
                merge_sort_dc(array, aux, mid
            +1, end);
                merge(array, aux, begin, mid, end);
            }

            void 
            merge_sort(
            int *array, int len)
            {
                
            int *aux = (int *)malloc(sizeof(int* len);
                merge_sort_dc(array, aux, 
            0, len-1);
                free(aux);
            }

            struct Node {
                
            int val;
                
            struct Node *next;
            };

            struct Node *
            list_merge(
            struct Node *first, struct Node *second)
            {
                
            if(first == NULL)
                    
            return second;
                
            if(second == NULL)
                    
            return first;

                
            struct Node *node = NULL;
                
            if(first->val <= second->val) {
                    node 
            = first;
                    first 
            = first->next;
                } 
            else {
                    node 
            = second;
                    second 
            = second->next;
                }
                node
            ->next = list_merge(first, second);
                
            return node;
            }

            struct Node *
            list_merge_sort(
            struct Node *list)
            {
                
            if(list==NULL || list->next==NULL)
                    
            return list;
                
            struct Node *once = list;
                
            struct Node *twice = list;
                
            while(twice->next && twice->next->next) {
                    once 
            = once->next;
                    twice 
            = twice->next->next;
                }
                twice 
            = once->next;
                once
            ->next = NULL;
                once 
            = list;
                list_merge(list_merge_sort(once), list_merge_sort(twice));
            }
            posted @ 2011-07-29 19:39 simplyzhao 閱讀(330) | 評論 (0)編輯 收藏
            快速排序: 平均時間復(fù)雜度O(nlogn),最壞時間復(fù)雜度O(n^2),非穩(wěn)定排序

            快速排序有兩個方向,左邊的i下標(biāo)一直往右走,當(dāng)a[i] <= a[center_index],其中center_index是中樞元素的數(shù)組下標(biāo),一般取為數(shù)組第0個元素。而右邊的j下標(biāo)一直往左走,當(dāng)a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重復(fù)上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩(wěn)定性打亂,比如序列為 5 3 3 4 3 8 9 10 11, 現(xiàn)在中樞元素5和3(第5個元素,下標(biāo)從1開始計)交換就會把元素3的穩(wěn)定性打亂,所以快速排序是一個不穩(wěn)定的排序算法,不穩(wěn)定發(fā)生在中樞元素和a[j]交換的時刻(這里所展示的是partition的第二種方案, partition2)。

            partition1與partition2的優(yōu)劣具體參考《編程珠璣》 第11章

            int
            partition1(
            int *array, int begin, int end)
            {
                
            int pivot = array[begin];
                
            int i, j = begin;
                
            for(i=begin+1; i<=end; ++i) {
                    
            if(array[i] < pivot) {
                        
            ++j;
                        
            if(i != j)
                            swap(array
            +i, array+j);
                    }
                }
                
            if(begin != j)
                    swap(array
            +begin, array+j);
                
            return j;
            }

            int
            partition2(
            int *array, int begin, int end)
            {
                
            int i, j, pivot = array[begin];
                i 
            = begin+1;
                j 
            = end;
                
            while(1) {
                    
            while(i<=end && array[i]<pivot)
                        
            ++i;
                    
            while(j>begin && array[j]>pivot)
                        
            --j;
                    
            if(i >= j)
                        
            break;
                    swap(array
            +i, array+j);
                    
            ++i;
                    
            --j;
                }
                
            if(begin != j)
                    swap(array
            +begin, array+j);
                
            return j;
            }

            void
            quick_sort(
            int *array, int begin, int end)
            {
                
            if(begin >= end)
                    
            return;
                
            int mid = partition2(array, begin, end);
                quick_sort(array, begin, mid
            -1);
                quick_sort(array, mid
            +1, end);
            }
            posted @ 2011-07-29 12:14 simplyzhao 閱讀(137) | 評論 (0)編輯 收藏
            希爾排序: 針對插入排序的改進,縮小增量的思想,非穩(wěn)定排序

            希爾排序(Shell sort)也稱“縮小增量排序”。它的做法不是每次一個元素挨一個元素的比較。而是先將整個待排記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄基本有序時,再對全體記錄進行一次直接插入排序。這樣大大減少了記錄移動次數(shù),提高了排序效率。 
            算法思路:先取一個正整數(shù)d1(d1<n),把全部記錄分成d1個組,所有距離為dl的倍數(shù)的記錄看成是一組,然后在各組內(nèi)進行插入排序;接著取d2(d2<d1),重復(fù)上述分組和排序操作;直到di=1 (i>=1),即所有記錄成為一個組為止。希爾排序?qū)υ隽啃蛄械倪x擇沒有嚴(yán)格規(guī)定,一般選d1約為n/2,d2為d1/2,d3為d2/2,…,di=1。

            在希爾排序開始時增量較大,分組較多,每組的記錄數(shù)目少,故各組內(nèi)直接插入較快,后來增量di逐漸縮小,分組數(shù)逐漸減少,而各組的記錄數(shù)目逐漸增多,但由于已經(jīng)按di-1作為距離排過序,使文件較接近于有序狀態(tài),所以新的一趟排序過程也較快。因此,希爾排序在效率上較直接插人排序有較大的改進。

            希爾排序是按照不同步長對元素進行插入排序,當(dāng)剛開始元素很無序的時候,步長最大,所以插入排序的元素個數(shù)很少,速度很快;當(dāng)元素基本有序了,步長很小,插入排序?qū)τ谟行虻男蛄行屎芨摺K裕柵判虻臅r間復(fù)雜度會比o(n^2)好一些。由于多次插入排序,我們知道一次插入排序是穩(wěn)定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最后其穩(wěn)定性就會被打亂,所以shell排序是不穩(wěn)定的。

            void
            shell_sort(
            int *array, int len)
            {
                
            int i, j, step, backup;
                
            for(step=len>>1; step>0; step>>=1) {
                    
            for(i=step; i<len; ++i) {
                        backup 
            = array[i];
                        
            for(j=i-step; j>=0 && array[j]>backup; j-=step) 
                            array[j
            +step] = array[j];
                        array[j
            +step] = backup;
                    }
                }
            }
            posted @ 2011-07-28 18:10 simplyzhao 閱讀(139) | 評論 (0)編輯 收藏
            僅列出標(biāo)題
            共21頁: 1 2 3 4 5 6 7 8 9 Last 

            導(dǎo)航

            <2025年7月>
            293012345
            6789101112
            13141516171819
            20212223242526
            272829303112
            3456789

            統(tǒng)計

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            久久一区二区三区免费| 精品久久久久久久国产潘金莲| 久久精品国产亚洲AV麻豆网站| 久久青青草原精品国产| 热re99久久精品国产99热| 久久人人爽人人澡人人高潮AV| 亚洲人成电影网站久久| 久久久女人与动物群交毛片| 99热热久久这里只有精品68| 国产精品久久新婚兰兰| 成人a毛片久久免费播放| 漂亮人妻被中出中文字幕久久| 久久99国产精品久久99果冻传媒| 中文字幕无码久久久| 精品久久久久久无码中文字幕 | 亚洲v国产v天堂a无码久久| 人妻精品久久久久中文字幕69| 国产综合免费精品久久久| 麻豆一区二区99久久久久| 久久91精品综合国产首页| 97久久久精品综合88久久| 亚洲综合伊人久久大杳蕉| 亚洲精品视频久久久| 99久久精品免费国产大片| 久久99国产精品久久99果冻传媒| 午夜人妻久久久久久久久| 人妻无码久久精品| 国产精品综合久久第一页| 亚洲国产二区三区久久| 亚洲国产精品久久66| 久久精品视频网| 国产一区二区三区久久| 国产精品美女久久久久久2018| 日韩乱码人妻无码中文字幕久久| 久久热这里只有精品在线观看| 久久免费国产精品| 色综合久久中文字幕综合网| 伊人久久精品影院| 久久99热这里只有精品国产| 亚洲日本va中文字幕久久| 精品久久久噜噜噜久久久 |