7.4 內(nèi)存的使用 指針是一個非常靈活且強大的編程工具,有非常廣泛的應(yīng)用。大多數(shù)C程序都在某種程度上使用了指針。C語言還進一步增強了指針的功能,為在代碼中使用指針提供了很強的激勵機制,它允許在執(zhí)行程序時動態(tài)分配內(nèi)存。只有使用指針,才能動態(tài)分配內(nèi)存。 第5章的一個程序計算一組學(xué)生的平均分,當(dāng)時它只處理10個學(xué)生。假設(shè)要編寫一個程序,但事先不知道要處理多少個學(xué)生,若使用動態(tài)內(nèi)存分配(dynamic memory allocation),所使用的內(nèi)存就不會比指定的學(xué)生分數(shù)所需的內(nèi)存多??梢栽趫?zhí)行時創(chuàng)建足以容納所需數(shù)據(jù)量的數(shù)組。 在程序的執(zhí)行期間分配內(nèi)存時,內(nèi)存區(qū)域中的這個空間稱為堆(heap)。還有另一個內(nèi)存區(qū)域,稱為堆棧(stack),其中的空間分配給函數(shù)的參數(shù)和本地變量。在執(zhí)行完該函數(shù)后,存儲參數(shù)和本地變量的內(nèi)存空間就會釋放。堆中的內(nèi)存是由程序員控制的。如本章后面所述,在分配堆上的內(nèi)存時,由程序員跟蹤所分配的內(nèi)存何時不再需要,并釋放這些空間,以便于以后重用它們。 7.4.1 動態(tài)內(nèi)存分配:malloc()函數(shù) 在運行時分配內(nèi)存的最簡單的標(biāo)準(zhǔn)庫函數(shù)是malloc()。使用這個函數(shù)時,需要在程序中包含頭文件 動態(tài)內(nèi)存分配的一個例子如下:
這條語句請求100個字節(jié)的內(nèi)存,并把這個內(nèi)存塊的地址賦予pNumber。只要不修改它,任何時間使用這個變量pNumber,它都會指向所分配的100個字節(jié)的第一個int的位置。這個內(nèi)存塊能保存25個int值,每個int占4個字節(jié)。 注意,類型轉(zhuǎn)換(int*)將函數(shù)返回的地址轉(zhuǎn)換成int類型的指針。這么做是因為malloc()是一般用途的函數(shù),可為任何類型的數(shù)據(jù)分配內(nèi)存。這個函數(shù)不知道要這個內(nèi)存作什么用,所以它返回的是一個void類型的指針,寫做void*。類型void*的指針可以指向任意類型的數(shù)據(jù),然而不能取消對void指針的引用,因為它指向未具體說明的對象。許多編譯器會把malloc()返回的地址自動轉(zhuǎn)換成適當(dāng)?shù)念愋停也粫唧w指定的對象。 可以請求任意數(shù)量的字節(jié),字節(jié)數(shù)僅受制于計算機中未用的內(nèi)存以及malloc()的運用場合。如果因某種原因而不能分配請求的內(nèi)存,malloc()會返回一個NULL指針。這個指針等于0。最好先用if語句檢查請求動態(tài)分配的內(nèi)存是否已分配,再使用它。就如同金錢,沒錢又想花費,會帶來災(zāi)難性的后果。因此,應(yīng)編寫如下語句:
如果指針是NULL,最好執(zhí)行適當(dāng)?shù)牟僮鳌@?,至少可以顯示一條信息"內(nèi)存不足",然后中止程序。這比允許程序繼續(xù)執(zhí)行,使之使用NULL地址存儲數(shù)據(jù)導(dǎo)致崩潰要好得多。然而,在某些情況下,可以釋放在別的地方使用的內(nèi)存,以便程序有足夠的內(nèi)存繼續(xù)執(zhí)行下去。 7.4.2 分配內(nèi)存時使用sizeof運算符 前一個例子很不錯,但我們不常處理字節(jié),而常常處理int、double等數(shù)據(jù)類型。例如給75個int類型的數(shù)據(jù)項分配內(nèi)存,可以使用以下的語句:
如前所述,sizeof是一個運算符,它返回一個size_t類型的無符號整數(shù),該整數(shù)是存儲它的參數(shù)需要的字節(jié)數(shù)。它把關(guān)鍵字如int或float等作為參數(shù),返回存儲該類型的數(shù)據(jù)項所需的字節(jié)數(shù)。它的參數(shù)也可以是變量或數(shù)組名。把數(shù)組名作為參數(shù)時,sizeof返回存儲整個數(shù)組所需的字節(jié)數(shù)。前一個例子請求分配足以存儲75個int數(shù)據(jù)項的內(nèi)存。以這種方式使用sizeof,可以根據(jù)不同的C編譯器為int類型的值自動調(diào)整所需的內(nèi)存空間。 試試看:動態(tài)內(nèi)存分配 下面使用指針來計算質(zhì)數(shù),將動態(tài)內(nèi)存分配的概念應(yīng)用于實踐。質(zhì)數(shù)是只能被1和這個數(shù)本身整除的整數(shù)。 查找質(zhì)數(shù)的過程非常簡單。首先,由觀察得知,2、3和5是前三個質(zhì)數(shù),因為它們不能被除了1以外更小的數(shù)整除。其他質(zhì)數(shù)必定都是奇數(shù)(否則它們可以被2整除),所以要找出下一個質(zhì)數(shù),可以從最后一個質(zhì)數(shù)開始,給它加2。檢查完這個數(shù)后,再給它加2,繼續(xù)檢查。 檢查一個數(shù)是否為質(zhì)數(shù),而不只是奇數(shù),可以用這個數(shù)除以比它小的所有奇數(shù)。其實不需要這么麻煩。如果一個數(shù)不是質(zhì)數(shù),它必定能被比它小的質(zhì)數(shù)整除。我們要按順序查找質(zhì)數(shù),所以可以把已經(jīng)找到的質(zhì)數(shù)作為除數(shù),確定所檢查的數(shù)是否為質(zhì)數(shù)。 這個程序?qū)⑹褂弥羔樅蛣討B(tài)內(nèi)存分配:
代碼的說明 在這個例子中,可以輸入要程序產(chǎn)生的質(zhì)數(shù)個數(shù)。指針變量primes引用一塊用于存儲所計算的質(zhì)數(shù)的內(nèi)存區(qū)。然而,在程序中沒有一開始就定義內(nèi)存。這塊空間是在輸入質(zhì)數(shù)個數(shù)后分配的:
在提示后,輸入的值存儲在total中。下一行語句確保total至少是4。這是因為程序?qū)⒍x并存儲已知的前三個質(zhì)數(shù)。 然后,使用total的值分配適當(dāng)數(shù)量的內(nèi)存來存儲質(zhì)數(shù):
質(zhì)數(shù)的大小增長得比其數(shù)量快,所以把它們存儲在unsigned long類型中。但如果要指定可以處理的最大質(zhì)數(shù),可以使用unsigned long long類型。程序把每個質(zhì)數(shù)存儲為類型long,所以需要的字節(jié)數(shù)是total*sizeof(unsigned long)。如果malloc()函數(shù)返回NULL,就不分配內(nèi)存,而是顯示一條信息,并結(jié)束程序。 可以指定最大的質(zhì)數(shù)個數(shù)取決于計算機的可用內(nèi)存和編譯器使用malloc()一次能分配的內(nèi)存量,前者是主要的限制。malloc()函數(shù)的參數(shù)是size_t類型,所以size_t對應(yīng)的整數(shù)類型限制了可以指定的字節(jié)數(shù)。如果size_t對應(yīng)4字節(jié)的無符號整數(shù),則一次至多可以分配4 294 967 295個字節(jié)。 一旦有了分配給質(zhì)數(shù)的內(nèi)存,就定義前三個質(zhì)數(shù),將它們存儲到primes指針指向的內(nèi)存區(qū)的前三個位置:
可以看到,引用連續(xù)的內(nèi)存位置是很簡單的。primes是unsigned long類型的指針,所以primes+1引用第二個位置的地址-- 這個地址是primes加上存儲一個unsigned long類型數(shù)據(jù)項所需的字節(jié)數(shù)。使用間接運算符存儲每個值;否則就要修改這個地址本身。 有了三個質(zhì)數(shù),就把count變量設(shè)定為3,用最后一個質(zhì)數(shù)5初始化變量trial:
開始查找下一個質(zhì)數(shù)時,給trial中的值加2,得到下一個要測試的數(shù)。所有的質(zhì)數(shù)都在while循環(huán)內(nèi)查找:
在循環(huán)內(nèi)每找到一個質(zhì)數(shù),就遞增count變量,當(dāng)它到達total值時,循環(huán)就結(jié)束。 在while循環(huán)內(nèi),首先將trial的值加2UL,然后測試它是否是質(zhì)數(shù):
for循環(huán)用于測試。在這個循環(huán)內(nèi),把trial除以每個質(zhì)數(shù)的余數(shù)存放到found中。如果除盡,余數(shù)就是0,因此found設(shè)置為false。如果余數(shù)是0,就表示trial中的值不是質(zhì)數(shù),可以繼續(xù)測試下一個數(shù)。 賦值表達式的值存儲到賦值運算符左邊的變量中。因此,表達式(found= (trial %*(primes+i))) 的結(jié)果存儲到found中。如果除盡,found就是false,表達式!(found=(trial%*(primes+i)))將是true,執(zhí)行break語句。因此,如果trial能整除任一個先前存儲的質(zhì)數(shù),for循環(huán)就會結(jié)束。 如果沒有一個質(zhì)數(shù)除trial是整除,當(dāng)所有的質(zhì)數(shù)都試過后,就結(jié)束for循環(huán),found的結(jié)果是把最后一個余數(shù)(它是某個正整數(shù))轉(zhuǎn)換為bool類型的值。如果trial能被某個質(zhì)數(shù)整除,循環(huán)會通過break語句結(jié)束,found會含有false。因此,可以在完成for循環(huán)時,使用存儲在found中的值確定是否找到一個新的質(zhì)數(shù):
如果found是true,就將trial的值存儲到內(nèi)存區(qū)的下一個位置上。下一個位置的地址是primes+count。第一個位置是primes,所以當(dāng)有count個質(zhì)數(shù)時,最后一個質(zhì)數(shù)所占的位置是primes+count-1。這個語句存儲了新的質(zhì)數(shù)后,遞增count的值。 while循環(huán)重復(fù)這個過程,直到找出所有的質(zhì)數(shù)為止。然后,以5個一行輸出質(zhì)數(shù):
7.4.3 用calloc()函數(shù)分配內(nèi)存 在 下面的語句使用calloc()為包含75個int元素的數(shù)組分配內(nèi)存:
如果不能分配所請求的內(nèi)存,返回值就是NULL,也可以檢查分配內(nèi)存的結(jié)果,這非常類似于malloc(),但calloc()分配的內(nèi)存區(qū)域都會初始化為0。 將程序7.11改為使用calloc()代替malloc()來分配需要的內(nèi)存,只需修改一條語句,如下面的粗體顯示,其他代碼不變:
7.4.4 釋放動態(tài)分配的內(nèi)存 在動態(tài)分配內(nèi)存時,應(yīng)總是在不需要該內(nèi)存時釋放它們。堆上分配的內(nèi)存會在程序結(jié)束時自動釋放,但最好在使用完這些內(nèi)存后立即釋放,甚至是在退出程序之前,也應(yīng)立即釋放。在比較復(fù)雜的情況下,很容易出現(xiàn)內(nèi)存泄漏。當(dāng)動態(tài)分配了一些內(nèi)存時,沒有保留對它們的引用,就會出現(xiàn)內(nèi)存泄漏,此時無法釋放內(nèi)存。這常常發(fā)生在循環(huán)內(nèi)部,由于沒有釋放不再需要的內(nèi)存,程序會使用越來越多的內(nèi)存,最終占用所有內(nèi)存。 當(dāng)然,要釋放用malloc()或calloc()分配的內(nèi)存,必須使用函數(shù)返回的引用內(nèi)存塊的地址。要釋放動態(tài)分配的內(nèi)存,而該內(nèi)存的地址存儲在pNumber指針中,可以使用下面的語句:
free()函數(shù)的形參是void *類型,所有指針類型都可以自動轉(zhuǎn)換為這個類型,所以可以把任意類型的指針作為參數(shù)傳送給這個函數(shù)。只要pNumber包含分配內(nèi)存時malloc()或calloc()返回的地址,就會釋放所分配的整個內(nèi)存塊,以備以后使用。 如果給free()函數(shù)傳送一個空指針,該函數(shù)就什么也不做。應(yīng)避免兩次釋放相同的內(nèi)存區(qū)域,因為在這種情況下,free()函數(shù)的操作是不確定的,因此也就無法預(yù)料。如果多個指針變量引用已分配的內(nèi)存,就有可能兩次釋放相同的內(nèi)存,所以要特別小心。 下面修改前面的例子,使用calloc(),并在程序的最后釋放內(nèi)存。 試試看:釋放動態(tài)分配的內(nèi)存 這個程序使用指針和動態(tài)分配的內(nèi)存:
7.4.5 重新分配內(nèi)存 realloc()函數(shù)可以重用前面通過malloc()或calloc() (或realloc())分配的內(nèi)存。函數(shù)需要兩個參數(shù):一個是指針,它包含前面調(diào)用malloc()、calloc()或realloc()返回的地址,另一個是要分配的新內(nèi)存的字節(jié)數(shù)。 realloc()函數(shù)釋放第一個指針參數(shù)引用的之前分配的內(nèi)存,然后重新分配該內(nèi)存區(qū)域,以滿足第二個參數(shù)指定的新請求。顯然,第二個參數(shù)的值不應(yīng)超過以前分配的字節(jié)數(shù)。否則,新分配的內(nèi)存將與以前分配的內(nèi)存區(qū)域大小相同。 下面的代碼演示了如何使用realloc()函數(shù):
很容易通過注釋理解這段代碼。循環(huán)讀取任意個由用戶提供的數(shù)據(jù)項,如果以前分配過內(nèi)存空間,且該空間足以滿足新請求,就再次使用該空間。如果以前沒有分配過內(nèi)存空間,或空間不夠大,代碼就使用calloc()分配一塊新內(nèi)存。 從這段代碼中可以看出,重新分配內(nèi)存需要做許多工作,因為一般需要確保已有的內(nèi)存塊足以滿足新請求。在大多數(shù)情況下,最好明確釋放舊內(nèi)存塊,再分配一塊全新的內(nèi)存。 下面是使用動態(tài)分配的內(nèi)存的基本規(guī)則: ●避免分配大量的小內(nèi)存塊。分配堆上的內(nèi)存有一些系統(tǒng)開銷,所以分配許多小的內(nèi)存塊比分配幾個大內(nèi)存塊的系統(tǒng)開銷大。 ●僅在需要時分配內(nèi)存。只要使用完堆上的內(nèi)存塊,就釋放它。 ●總是確保釋放已分配的內(nèi)存。在編寫分配內(nèi)存的代碼時,就要確定在代碼的什么地方釋放內(nèi)存。 ●在釋放內(nèi)存之前,確保不會無意中覆蓋堆上分配的內(nèi)存的地址,否則程序就會出現(xiàn)內(nèi)存泄漏。在循環(huán)中分配內(nèi)存時,要特別小心。 |
|