[C 語言] 程式設計教學:指標(Pointer) 和記憶體管理(Memory ...

文章推薦指數: 80 %
投票人數:10人

指標(pointer) 是C 語言的衍生型別之一。

指標的值並非資料本身,而是另一塊記憶體的虛擬位址(address)。

我們可利用指標間接存該指標所指向的記憶體的值。

在C 語言中, ... Togglenavigation開源教學精選項目C語言Golang資料結構網頁程式電子書籍現代C語言程式設計C語言應用程式設計多平台Objective-C程式設計跨平台CommonLisp程式設計社群媒體臉書粉絲團臉書社團推特GitHubGumroad本站資訊關於著作權免責聲明隱私權開源教學C程式設計指標(Pointer)和記憶體管理(MemoryManagement)最後修改日期為JUL12,2018前言指標(pointer)是C語言的衍生型別之一。

指標的值並非資料本身,而是另一塊記憶體的虛擬位址(address)。

我們可利用指標間接存該指標所指向的記憶體的值。

在C語言中,有些和記憶體相關的操作必需使用指標,實作動態資料結構時也會用到指標,所以我們學C時無法避開指標。

許多C語言教材將指標放在整本書的後半段,集中在一章到兩章來講。

但我們很早就介紹指標,並在日後介紹其他C語言特性時,順便提到指標相關的內容。

這樣的編排,是希望讀者能儘早習慣指標的使用方式。

記憶體階層(MemoryLayout)使用指標,不必然要手動管理記憶體。

記憶體管理的方式,得看資料在C程式中的記憶體階層而定。

C語言有三種記憶體階層:靜態記憶體配置(staticmemoryallocation)自動記憶體配置(automaticmemoryallocation)動態記憶體配置(dynamicmemoryallocation)靜態記憶體儲存程式本身和全域變數,會自動配置和釋放,但容量有限。

自動記憶體儲存函式內的局部變數,會自動配置和釋放,在函式結束時自動釋放,容量有限。

動態記憶體儲存函式內的變數,需手動配置和釋放,可跨越函式的生命週期,可於執行期動態決定記憶體容量,可用容量約略等於系統的記憶體量。

雖然靜態記憶體和自動記憶體可自動配置,但各自受到一些限制,故仍要會用動態記憶體。

靜態記憶體配置(StaticMemoryAllocation)我們來看一個使用靜態記憶體配置的簡短範例:#include/*1*/ /*DON'TDOTHISINPRODUCTION.*//*2*/ inti=3;/*3*/ intmain(void)/*4*/ {/*5*/ int*i_p=&i;/*6*/ assert(*i_p==3);/*7*/ *i_p=4;/*8*/ assert(i==4);/*9*/ return0;/*10*/ }/*11*/ 在本範例的第3行,我們宣告變數i為3時,C程式自動為我們配置記憶體。

由於該變數屬於全域變數,會自動配置記憶體,不需人為介入。

在範例程式的第6行中,我們宣告了指向int的指標i_p。

int*表示指向int型別的指標。

C語言需要知道指標所指向的型別,才能預知記憶體的大小。

&i代表回傳變數i的虛擬記憶體位址。

因為指標的值是記憶體位址,在本例中,我們的指標i_p指向一塊已經配置好的記憶體,即為變數i所在的位址。

接著,我們在範例的第7行確認確認指標i_p所指向的值的確是3。

在這行敘述中,*i_p代表解址,解址後會取出該位址的值。

在本例中即取回3。

接著,我們在第8行藉由修改指標i_p所指向的記憶體間接修改變數i的值。

最後,我們在第9行藉由assert(i==4);敘述確認指標i_p確實間接修改到變數i。

代表兩者的確指向同一塊記憶體。

如果讀者用Valgrind或其他記憶體檢查軟體去檢查此程式,可發現此範例程式沒有配置動態記憶體,也沒有記憶體洩露的問題。

代表使用指標不必然要手動管理記憶體。

附帶一提,在本範例中,我們使用了全域變數。

這在撰碼上是不良的習慣,因全域變數很容易在不經意的情形下誤改。

我們的程式只是為了展示靜態記憶體的特性,不建議在實務上使用全域變數。

自動記憶體配置(AutomaticMemoryAllocation)接著,我們來看一個使用自動記憶體配置的實例:#include/*1*/ intmain(void)/*2*/ {/*3*/ inti=3;/*4*/ int*i_p=&i;/*5*/ assert(*i_p==3);/*6*/ *i_p=4;/*7*/ assert(i==4);/*8*/ return0;/*9*/ }/*10*/ 在本例的第4行中,當我們宣告變數i的值為3時,同樣會自動配置記憶體。

由於變數i存在於主函式中,故使用自動記憶體配置。

在本例的第5行中,我們宣告了指向變數i的指標i_p。

這時候i的記憶體已經配置好了。

這個範例除了記憶體配置的方式外,其他的指令和前一節的範例雷同,故不再詳細說明,請讀者自行閱讀。

如果讀者用Valgrind或其他記憶體檢查軟體檢查此範例程式,同樣可發現此程式沒有配置動態記憶體,也沒有記憶體洩露的問題。

動態記憶體配置(DynamicMemoryAllocation)我們來看一個動態記憶體配置的實例:#include/*1*/ #include/*2*/ #include/*3*/ intmain(void)/*4*/ {/*5*/ int*i_p=(int*)malloc(sizeof(int));/*6*/ if(!i_p){/*7*/ fprintf(stderr,"Failedtoallocatememory\n");/*8*/ gotoERROR;/*9*/ }/*10*/ *i_p=3;/*11*/ if(!(*i_p==3)){/*12*/ fprintf(stderr,"Wrongvalue:%d\n",*i_p);/*13*/ gotoERROR;/*14*/ }/*15*/ free(i_p);/*16*/ return0;/*17*/ ERROR:/*18*/ if(i_p)/*19*/ free(i_p);/*20*/ return1;/*21*/ }/*22*/ 在第6行中,我們配置一塊大小為int的記憶體。

C標準函式庫中配置記憶體的函式為malloc(),該函式接收的參數為記憶體的大小。

我們甚少手動寫死記憶體的大小,而會使用sizeof直接取得特定資料型別的大小。

這幾乎是固定的手法了。

malloc()回傳的型別是void*,即為大小未定的指標。

我們會自行手動轉型為指向特定型別的指標。

有些C編譯器會自動幫我們轉型,就不用自行轉型。

由於配置記憶體是有可能失敗的動作,我們在第7行至第10行檢查是否成功地配置記憶體。

當malloc()失敗時,會回傳NULL。

而NULL在布林語境中會視為偽,故!i_p在i_p的值為NULL時會變為真。

若!i_p為真,代表malloc()未成功配置記憶體,這時候我們會中止一般的流程,改走錯誤處理流程。

我們先在標準錯誤印出錯誤訊息,然後用goto跳到標籤ERROR所在的地方。

由於C沒有內建的錯誤處理機制,使用goto跳離一般程式流程算是窮人版的例外(exception)。

fprintf()敘述用到了標準輸出入和巨集的概念,稍微超出現在的範圍。

先知道該敘述會在標準錯誤印出訊息所在的檔案名稱和行數即可。

如果malloc()成功地配置記憶體,我們就繼續一般的程式流程。

我們在第11行將i_p指向的記憶體賦值為3,然後在第12行至第15行檢查是否正確地賦值。

一般來說,這行敘述是不需檢查的。

這裡僅是展示這項特性。

由於i_p所指向的記憶體是手動配置的,我們在第16行釋放i_p所占用的記憶體。

基本上,malloc()和free()應成對出現。

每配置一次記憶體就要在確定不使用時釋放回去。

由於這個範例相當短,這似乎顯而易見。

但是在撰寫動態資料結構時,會跨越多個函式,比這個範例複雜得多,就有可能會忘了釋放記憶體。

當程式發成錯誤時,我們會改走錯誤處理的流程。

在本例中,錯誤流程在第18行至第21行。

在進行錯誤處理時,我們同樣會釋放記憶體,但程式最後會回傳非零值1,代表程式異常結束。

由於在錯誤發生時,我們無法確認i_p是否已配置記憶體,所以要用if敘述來確認。

請讀者再回頭把整個程式運行的流程看一次,即可了解。

空指標(NullPointer)當C程式試圖去存取系統資源時,該指令有可能失敗,故要撰寫錯誤處理相關的程式碼。

以下範例程式試圖打開一個文字檔案file.txt:#include/*1*/ intmain(void)/*2*/ {/*3*/ FILE*fp;/*4*/ fp=fopen("file.txt","r");/*5*/ if(!fp){/*6*/ fprintf(stderr,"Failedtoopenthefile\n");/*7*/ return1;/*8*/ }/*9*/ fclose(fp);/*10*/ return0;/*11*/ }/*12*/ 在第5行中,我們試圖用fopen()函式以讀取模式開啟file.txt。

當檔案無法開啟時,會回傳空指標NULL。

所以我們在第6行至第9行檢查指標fp是否為空。

當fp為空指標時,!fp會負負得正,這時候程式會中止一般流程,改走錯誤處理流程。

在這個範例中,我們在終端機印出錯誤訊息,並回傳非零值1代表程式異常結束。

以下是另一種檢查空指標的寫法:fp=fopen("file.txt","r"); if(fp==NULL){ fprintf(stderr,"Failedtoopenthefile\n"); return1; } 基本上,兩者皆可使用,這僅是風格上的差異。

比較指標是否相等當我們使用指標時,會處理兩個值,一個是指標所指向的位址,另一個是指標所指向的值。

我們用以下範例來看兩者的差別:#include/*1*/ intmain(void)/*2*/ {/*3*/ inta=3;/*4*/ intb=3;/*5*/ int*p=&a;/*6*/ int*q=&a;/*7*/ int*r=&b;/*8*/ if(!(p==q)){/*9*/ fprintf(stderr,"Wrongaddresses:%p%p\n",p,q);/*10*/ gotoERROR;/*11*/ }/*12*/ if(p==r){/*13*/ fprintf(stderr,"Wrongaddresses:%p%p\n",p,r);/*14*/ gotoERROR;/*15*/ }/*16*/ if(!(*p==*q)){/*17*/ fprintf(stderr,"Wrongvalues:%d%d\n",*p,*q);/*18*/ gotoERROR;/*19*/ }/*20*/ if(!(*p==*r)){/*21*/ fprintf(stderr,"Wrongvalues:%d%d\n",*p,*r);/*22*/ gotoERROR;/*23*/ }/*24*/ return0;/*25*/ ERROR:/*26*/ return1;/*27*/ }/*28*/ 一開始,我們分別在第4行及第5行配置兩塊自動記憶體。

雖然變數a和變數b的值是相同的,但兩者存在於不同的記憶體區塊。

接著,我們第6行至第8行宣告三個指標,分別指向這兩個變數。

我們可以預期p和q同時會指向變數a所在的記憶體,而r則指向變數b所在的記憶體。

但p、q、r所指向的值皆相等。

從範例程式中即可確認這樣的狀態,讀者可自行閱讀一下。

野指標(WildPointer)若我們宣告了指標但未對指標賦值,這時候指標的位址是未定義的,由各家C編譯器自行決定其行為。

宣告但未賦值的指標稱為野指標,這時候指標的值視為無意義的垃圾值,不應依賴其結果。

我們來看一個野指標的範例程式:#include/*1*/ #include/*2*/ #definePUTS(format,...){\ fprintf(stdout,"(%s:%d)"format"\n",\ __FILE__,__LINE__,##__VA_ARGS__);\ } #definePUTERR(format,...){\ fprintf(stderr,"(%s:%d)"format"\n",\ __FILE__,__LINE__,##__VA_ARGS__);\ } intmain(void)/*3*/ {/*4*/ int*i_p;/*Wildpointer.*//*5*/ if(i_p==NULL){/*6*/ PUTS("i_pisNULL");/*7*/ }/*8*/ if(!i_p){/*9*/ PUTS("i_pisEMPTY");/*10*/ }/*11*/ i_p=NULL;/*NULLpointer.*//*12*/ if(i_p==NULL){/*13*/ PUTS("i_pisNULL");/*14*/ }/*15*/ if(!i_p){/*16*/ PUTS("i_pisEMPTY");/*17*/ }/*18*/ i_p=(int*)malloc(sizeof(int));/*19*/ if(!i_p){/*20*/ PUTERR("Failedtoallocatememory");/*21*/ gotoERROR;/*22*/ }/*23*/ if(i_p==NULL){/*24*/ PUTS("i_pisNULL");/*25*/ }/*26*/ if(!i_p){/*27*/ PUTS("i_pisEMPTY");/*28*/ }/*29*/ free(i_p);/*30*/ return0;/*31*/ ERROR:/*32*/ if(i_p)/*33*/ free(i_p);/*34*/ return1;/*35*/ }/*36*/ 在開頭的地方,我們宣告了兩個巨集PUTS和PUTERR。

若讀者跟著我們這個系列的文章讀下來,到目前為止我們還沒講過巨集。

我們這裡使用巨集是為了節省版面。

請讀者暫時把巨集當成另類函式即可,後文會再說明。

在第5行時,指標i_p尚未賦值,其值為垃圾值。

使用不同C編譯器編譯此範例程式時,會得到不同的結果。

在第12行時,我們將i_p以NULL賦值,這時候i_p就不再是野指標,轉為空指標。

在第19行時,我們手動配置了一塊記憶體,這時候i_p就是一般的指標。

由這個例子可知,我們在宣告指標時,若未馬上配置記憶體或其他系統資源時,應該立即以NULL賦值,讓該指標成為空指標。

迷途指標(DanglingPointer)原本指向某塊記憶體的指標,當該記憶體中途消失時,該指標所指向的位址不再合法,這時候的指標就成為迷途指標。

如同野指標,迷途指標所指向的值視為垃圾值,不應依賴其結果。

以下是一個迷途指標的範例程式:#include/*1*/ intmain(void)/*2*/ {/*3*/ int*i_p=NULL;/*4*/ {/*5*/ inti=3;/*6*/ i_p=&i;/*7*/ }/*8*/ /*i_pisadanglingpointernow.*//*9*/ if(*i_p==3){/*10*/ fprintf(stderr,"Itshouldnotbe3\n");/*11*/ return1;/*12*/ }/*13*/ return0;/*14*/ }/*15*/ 在第7行時,指標i_p指向i。

但在第8行時,該區塊結束,i的記憶體會自動釋放掉,這時候i_p所指向的記憶體不再合法,i_p變成迷途指標。

之後的運算基本上是無意義且不可靠的。

我們再來看另一個迷途指標的例子:#include/*1*/ #include/*2*/ #definePUTERR(format,...){\ fprintf(stderr,"(%s:%d)"format"\n",\ __FILE__,__LINE__,##__VA_ARGS__);\ } intmain(void)/*3*/ {/*4*/ int*i_p=(int*)malloc(sizeof(int));/*5*/ if(!i_p){/*6*/ PUTERR("Failedtoallocateint");/*7*/ gotoERROR;/*8*/ }/*9*/ free(i_p);/*10*/ /*i_pisadanglingpointernow.*//*11*/ *i_p=3;/*12*/ if(*i_p==3){/*13*/ PUTERR("Itshouldnotbe3");/*14*/ gotoERROR;/*15*/ }/*16*/ return0;/*17*/ ERROR:/*18*/ if(i_p)/*19*/ free(i_p);/*20*/ return1;/*21*/ }/*22*/ 在第5行時,我們配置一塊記憶體到i_p。

在第10行時這塊記憶體釋放掉了,這時候i_p所指的位址不再合法,故i_p成為迷途指標。

即使之後的運算能夠成功,那也只是一時僥倖而已。

結語在本文中,我們介紹了數個指標的基本用法。

我們把指標放在前半部,是為了要讓大家及早適應指標。

在後續介紹各種C語言的特性時,我們會再加入指標相關的使用方式。

電子書籍如果你覺得這篇C語言的技術文章對你有幫助,可以看看以下完整的C語言程式設計電子書:分享本文追蹤本站



請為這篇文章評分?