談C/C++指針精髓

[摘要] 

 

指針是C和C++語言編程中最重要的概念之一,也是最容易產生困惑並致使程序出錯的問題之一。利用指針編程能夠表示各類數據結構, 經過指針可以使用主調函數和被調函數之間共享變量或數據結構,便於實現雙向數據通信;並能像彙編語言同樣處理內存地址,從而編出精練而高效的程序。指針極大地豐富了C和C++語言的功能。 html

在本文中,主要分兩部分對指針進行討論。首先,基礎篇討論關於指針的內容和運算操做等,能夠是讀者對指針的知識有必定了解和認識;隨後在使用篇中重點討論指針的各類應用,揭破指針在平常編程中的精髓,從而使讀者可以真正地瞭解、認識和使用指針。 程序員

[關鍵字] C C++ 指針 引用 數組 結構體 類 編程

第一篇:基礎篇 數組

1.1指針的概念

 

談到指針,它的靈活性和難控制性讓許多程序員談虎色變;但它的直接操做內存,在數據 安全

操做方面有着速度快,節約內存等優勢,又使許多C++程序員的深愛不以.那麼指針到底是怎麼樣一個概念呢? 數據結構

其實, 指針就是一類變量,是一類包含了其餘變量或函數的地址的變量。與其餘變量所不一樣的是,通常的變量包含的是實際的真實的數據,而指針是一個指示器,它告訴程序在內存的哪塊區域能夠找到數據。 函數

好了,在這裏咱們能夠這樣定義指針:指針是一類包含了其餘變量或函數的地址的變量,它裏面存儲的數值被解釋成爲內存的地址. post

1.2指針的內容

 

簡單講,指針有四個方面的內容:即指針的類型,指針所指向的類型,指針的值,指針自己所 測試

佔有的內存區.下面咱們將分別闡述這些內容. spa

1.2.1指針的類型

從語法的角度看,指針的類型是指把指針聲明語句中的指針名字去掉所剩下的部分。這是指針自己所具備的類型。例如:

int*ip;            //指針的類型是int*

char*ip;           //指針的類型是char*

int**ip;           //指針的類型是int**

int(*ip)[5];        //指針的類型是int(*)[5]

1.2.2指針所指向的類型

當你經過指針來訪問指針所指向的內存區時,指針所指向的類型決定了編譯器將把那片內存區裏的內容當作什麼類型來看待。從語法的角度看,指針所指向的類型是指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉所剩下的部分。例如:

int*ip;            //指針所指向的類型是int

char*ip;           //指針所指向的類型是char

int**ip;           //指針所指向的類型是int*

int(*ip)[5];        //指針所指向的類型是int()[5]

1.2.3指針的值(或稱指針所指向的內存區)

    指針的值或者叫指針所指向的內存區或地址,是指針自己存儲的數值,這個值將被編譯器看成一個地址,而不是一個通常的數值。在32位程序裏,全部類型的指針的值都是一個32位整數,由於32位程序裏內存地址全都是32位長。 指針所指向的內存區就是從指針的值所表明的那個內存地址開始,長度爲sizeof(指針所指向的類型)的一片內存區。之後,咱們說一個指針的值是XX,就至關於說該指針指向了以XX爲首地址的一片內存區域;咱們說一個指針指向了某塊內存區域,就至關於說該指針的值是這塊內存區域的首地址。

指針所指向的內存區和指針所指向的類型是兩個徹底不一樣的概念。在上例中,指針所指向的類型已經有了,但因爲指針還未初始化,因此它所指向的內存區是不存在的,或者說是無心義的。

之後,每遇到一個指針,都應該問問:這個指針的類型是什麼?指針指的類型是什麼?該指針指向了哪裏?

1.2.4指針自己所佔有的內存區

指針自己所佔有的內存區是指針自己佔內存的大小,這個你只要用函數sizeof(指針的

類型)測一下就知道了。在32位平臺裏,指針自己佔據了4個字節的長度。

指針自己佔據的內存這個概念在判斷一個指針表達式是不是左值時頗有用。

 

談C/C++指針精髓(二)

2008-02-18 16:09 by 馬偉, 4212 visits, 網摘收藏編輯

 

1.3指針與內存管理

 

    利用指針你能夠將數據寫入內存中的任意位置,可是,一旦你的程序中有一個野指針("wild」pointer),即指向一個錯誤位置的指針,你的數據就危險了—存放在堆中的數據可能會被破壞,用來管理堆的數據結構也可能會被破壞,甚至操做系統的數據也可能會被修改,有時,上述三種破壞狀況會同時發生。因此合理的正確的分配指針的地址是很是重要的。

 

1.3.1內存分配的方式

內存分配方式有三種:

(1)從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static變量。

(2)在棧上建立。在執行函數時,函數內局部變量的存儲單元均可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,可是分配的內存容量有限。

(3) 從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員本身負責在什麼時候用free或delete釋放內存。動態內存的生存期由咱們決定,使用很是靈活,但問題也最多,如下咱們重點講解動態內存分配。

1.3.2 malloc/free 的使用要點

 malloc與free是C/C++語言的標準庫函數,它用於申請動態內存和釋放內存。

函數malloc的原型以下:

void * malloc(size_t size);

用malloc申請一塊長度爲length的整數類型的內存,程序以下:

int *ip = (int *) malloc(sizeof(int) * length);

咱們應當把注意力集中在兩個要素上:「類型轉換」和「sizeof」。

 malloc函數返回值的類型是void *,因此在調用malloc時要顯式地進行類型轉換,將void * 轉換成所須要的指針類型。

malloc函數自己並不識別要申請的內存是什麼類型,它只關心內存的總字節數。例如int變量在16位系統下是2個字節,在32位下是4個字節;而float變量在16位系統下是4個字節,在32位下也是4個字節。這個你能夠用sizeof(類型)去測試。

在malloc的「()」中使用sizeof運算符是良好的風格,但要小心有時咱們會昏了頭,寫出 ip = malloc(sizeof(ip))這樣的程序來。

函數free的原型以下:

void free( void * memblock );

爲何free函數不象malloc函數那樣複雜呢?這是由於指針p的類型以及它所指的內存的容量事先都是知道的,語句free(p)能正確地釋放內存。若是p是NULL指針,那麼free對p不管操做多少次都不會出問題。若是p不是NULL指針,那麼free對p連續操做兩次就會致使程序運行錯誤。

1.3.3 new/delete 的使用要點

對於非內部數據類型的對象而言,光用maloc/free沒法知足動態對象的要求。對象在建立的同時要自動執行構造函數,對象在消亡以前要自動執行析構函數。因爲malloc/free是庫函數而不是運算符,不在編譯器控制權限以內,不可以把執行構造函數和析構函數的任務強加於malloc/free。

所以C++語言須要一個能完成動態內存分配和初始化工做的運算符new,以及一個能完成清理與釋放內存工做的運算符delete。注意new/delete不是庫函數,只是C++的運算符。咱們來看以下例子就知道怎麼回事了。

class Object

{

public :

Object(void){std::cout << 「Initialization」<< std::endl; }

~Object(void){std::cout << 「Destroy」<< std::endl; }

void Initialize(void){std:: cout << 「Initialization」<< std::endl; }

void Destroy(void){ std::cout << 「Destroy」<< std::endl; }

}

void UseMallocFree(void)

{

Object *ip = (Object *)malloc(sizeof(Object));    // 申請動態內存

ip->Initialize();                             // 初始化

//…

ip->Destroy();                              // 清除工做

free(ip);                                   // 釋放內存

}

void UseNewDelete(void)

{

Object *ip = new Object;                     // 申請動態內存而且初始化

//…

Delete ip;                                  // 清除而且釋放內存

}

          用malloc/free和new/delete如何實現對象的動態內存管理

類Object的函數Initialize模擬了構造函數的功能,函數Destroy模擬了析構函數的功能。函數UseMallocFree中,因爲malloc/free不能執行構造函數與析構函數,必須調用成員函數Initialize和Destroy來完成初始化與清除工做。函數UseNewDelete則簡單得多。

因此咱們不要企圖用malloc/free來完成動態對象的內存管理,應該用new/delete。因爲內部數據類型的「對象」沒有構造與析構的過程,對它們而言malloc/free和new/delete是等價的。new內置了sizeof、類型轉換和類型安全檢查功能, ,對於非內部數據類型的對象而言,new在建立動態對象的同時完成了初始化工做。

new/delete 常使用的方法以下:

typeof *ip = new typeof[length];

類/結構 *ip = new 類結構;

通常釋放以下:delete ip;

數組的釋放以下:delete [] ip;

1.3.4內存耗盡怎麼辦?

若是在申請動態內存時找不到足夠大的內存塊,malloc和new將返回NULL指針,宣告內存申請失敗。一般有三種方式處理「內存耗盡」問題。

(1)判斷指針是否爲NULL,若是是則立刻用return語句終止本函數。例如:

void Func(void)

{

A *a = new A;

if(a == NULL)

{

return;

}

}

(2)判斷指針是否爲NULL,若是是則立刻用exit(1)終止整個程序的運行。例如:

void Func(void)

{

A *a = new A;

if(a == NULL)

{

std::cout << 「Memory Exhausted」 << std::endl;

exit(1);

}

}

(3)爲new和malloc設置異常處理函數。例如Visual C++能夠用_set_new_hander函數爲new設置用戶本身定義的異常處理函數,也可讓malloc享用與new相同的異常處理函數。詳細內容請參考C++使用手冊。

 有一個很重要的現象要告訴你們。對於32位以上的應用程序而言,不管怎樣使用malloc與new,幾乎不可能致使「內存耗盡」。由於32位操做系統支持「虛存」,內存用完了,自動用硬盤空間頂替。我不想誤導讀者,必須強調:不加錯誤處理將致使程序的質量不好,千萬不可因小失大。

1.3. 5杜絕「野指針」

「野指針」不是NULL指針,是指向「垃圾」內存的指針。人們通常不會錯用NULL指針,由於用if語句很容易判斷。可是「野指針」是很危險的,if語句對它不起做用。 「野指針」的緣由主要有以下幾種:

(1)指針變量沒有被初始化。任何指針變量剛被建立時不會自動成爲NULL指針,它的缺省值是隨機的,它會亂指一氣。因此,指針變量在建立的同時應當被初始化,要麼將指針設置爲NULL,要麼讓它指向合法的內存。例如

char *ip = NULL;

char *ip = new char;

(2)指針ip被free或者delete以後,沒有置爲NULL,讓人誤覺得ip是個合法的指針。

(3)指針操做超越了變量的做用範圍。這種狀況讓人防不勝防,示例程序以下:

class A

{

public:

void Func(void){ std::cout << 「Func of class A」 << std::endl; }

};

void Test(void)

{

A *p;

{

A a;

p = &a; // 注意 a 的生命期

}

p->Func(); // p是「野指針」

}

函數Test在執行語句p->Func()時,對象a已經消失,而p是指向a的,因此p就成了「野指針」。但奇怪的是有些編譯器運行這個程序時竟然沒有出錯,這可能與編譯器有關。

1.3.6指針參數是如何傳遞內存的?

 若是函數的參數是一個指針,不要期望用該指針去申請動態內存。見以下例子:

void GetMemory(char *ip, int num)

{

ip = (char *)malloc(sizeof(char) * num);

}

void Test(void)

{

char *str = NULL;

GetMemory(str, 100); // str 仍然爲 NULL

strcpy(str, "hello"); // 運行錯誤

}

試圖用指針參數申請動態內存

毛病出在函數GetMemory中。編譯器老是要爲函數的每一個參數製做臨時副本,指針參數ip的副本是 _ip,編譯器使 _ip = ip。若是函數體內的程序修改了_ip的內容,就致使參數ip的內容做相應的修改。這就是指針能夠用做輸出參數的緣由。在本例中,_ip申請了新的內存,只是把_ip所指的內存地址改變了,可是ip絲毫未變。因此函數GetMemory並不能輸出任何東西。事實上,每執行一次GetMemory就會泄露一塊內存,由於沒有用free釋放內存。

若是非得要用指針參數去申請內存,那麼應該改用「指向指針的指針」,見以下示例:

void GetMemory(char **p, int num)

{

*ip = (char *)malloc(sizeof(char) * num);

}

void Test(void)

{

char *str = NULL;

GetMemory(&str, 100); // 注意參數是 &str,而不是str

strcpy(str, "hello");

std::cout<< str << std::endl;

free(str);

}

用指向指針的指針申請動態內存

固然,咱們也能夠用函數返回值來傳遞動態內存。這種方法更加簡單,見以下示例:

char *GetMemory(int num)

{

char *ip = (char *)malloc(sizeof(char) * num);

return ip;

}

void Test(void)

{

char *str = NULL;

str = GetMemory(100);

strcpy(str, "hello");

std::cout<< str << std::endl;

free(str);

}

用函數返回值來傳遞動態內存

用函數返回值來傳遞動態內存這種方法雖然好用,可是經常有人把return語句用錯了。這裏強調不要用return語句返回指向「棧內存」的指針,由於該內存在函數結束時自動消亡,見以下示例:

char *GetString(void)

{

char p[] = "hello world";

return p; // 編譯器將提出警告

}

void Test(void)

{

char *str = NULL;

str = GetString(); // str 的內容是垃圾

std::cout<< str << std::endl;

}

return語句返回指向「棧內存」的指針

最後,根據以上闡述,咱們總結以下使用規則供你們參考:

【規則1】用malloc或new申請內存以後,應該當即檢查指針值是否爲NULL。防止使用指針值爲NULL的內存。

【規則2】不要忘記爲數組和動態內存賦初值。防止將未被初始化的內存做爲右值使用。

【規則3】避免數組或指針的下標越界,特別要小心發生「多1」或者「少1」操做。

【規則4】動態內存的申請與釋放必須配對,防止內存泄漏。

【規則5】用free或delete釋放了內存以後,當即將指針設置爲NULL,防止產生「野指針」。

 

1.4指針的運算

 

1.4.1賦值運算

指針變量的賦值運算有如下幾種形式:

1.4.1.1指針變量初始化賦值以下:

int a;

int *ip=&a;

1.4.1.2把一個變量的地址賦予指向相同數據類型的指針變量。例如:

int a;

int *ip;

ip=&a;             //把整型變量a的地址賦予整型指針變量ip

1.4.1.3把一個指針變量的值賦予指向相同類型變量的另外一個指針變量。例如:

int a;

int *pa=&a;

int *pb;

pb=pa;              //把a的地址賦予指針變量pb

因爲pa,pb均爲指向整型變量的指針變量,所以能夠相互賦值。

1.4.1.4把數組的首地址賦予指向數組的指針變量。例如:

int a[5],*pa;

pa=a;               //數組名錶示數組的首地址,故可賦予指向數組的指針變量pa

也可寫爲:

pa=&a[0];           //數組第一個元素的地址也是整個數組的首地址也可賦予pa

固然也可採起初始化賦值的方法:

int a[5],*pa=a;

以上是一些基本的數組賦值方法,後面咱們會詳細討論指針在數組中的使用。

1.4.1.5把字符串的首地址賦予指向字符類型的指針變量。例如:

char *pc;

pc="c language";

或用初始化賦值的方法寫爲:

char *pc=" c language ";

這裏應說明的是並非把整個字符串裝入指針變量, 而是把存放該字符串的字符數組的首地址裝入指針變量。

1.4.1.6把函數的入口地址賦予指向函數的指針變量。例如:

int (*pf)();

pf=f;                //f爲函數名

1.4.2加減運算

對於指向數組的指針變量,能夠加上或減去一個整數n。設ip是指向數組a的指針變量,則ip+n,ip-n,ip++,++ip,ip--,--ip 運算都是合法的。指針變量加或減一個整數n的意義是把指針指向的當前位置(指向某數組元素)向前或向後移動n個位置。應該注意,數組指針變量向前或向後移動一個位置和地址加1或減1 在概念上是不一樣的。由於數組能夠有不一樣的類型, 各類類型的數組元素所佔的字節長度是不一樣的。如指針變量加1,即向後移動1 個位置表示指針變量指向下一個數據元素的首地址。而不是在原地址基礎上加1。看以下例子:

char a[20];

int*ip=a;

...

ip++;

在上例中,指針ip的類型是int*,它指向的類型是int,它被初始化爲指向整形變量a。接下來的第3句中,指針ip被加了1,編譯器是這樣處理的:它把指針ip的值加上了sizeof(int),在32位程序中,是被加上了4。因爲地址是用字節作單位的,故ip所指向的地址由原來的變量a的地址向高地址方向增長了4個字節。

因爲char類型的長度是一個字節,因此,原來ptr是指向數組a的第0號單元開始的四個字節,此時指向了數組a中從第4號單元開始的四個字節。再看以下例子:

char a[20];

int*ip=a;

...

ip+=5;

在這個例子中,ip被加上了5,編譯器是這樣處理的:將指針ip的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20。因爲地址的單位是字節,故如今的ip所指向的地址比起加5後的ip所指向的地址來講,向高地址方向移動了20個字節。在這個例子中,沒加5前的ip指向數組a的第0號單元開始的四個字節,加5後,ptr已經指向了數組a的合法範圍以外了。雖然這種狀況在應用上會出問題,但在語法上倒是能夠的。這也體現出了指針的靈活性。

若是上例中,ip是被減去5,那麼處理過程大同小異,只不過ip的值是被減去5乘sizeof(int),新的ip指向的地址將比原來的ip所指向的地址向低地址方向移動了20個字節。

總結一下,一個指針ipold加上一個整數n後,結果是一個新的指針ipnew,ipnew的類型和ipold的類型相同,ipnew所指向的類型和ipold所指向的類型也相同。ipnew的值將比ipold的值增長了n乘sizeof(ipold所指向的類型)個字節。就是說,ipnew所指向的內存區將比ipold所指向的內存區向高地址方向移動了n乘sizeof(ipold所指向的類型)個字節。

一個指針ipold減去一個整數n後,結果是一個新的指針ipnew,ipnew的類型和ipold的類型相同,ipnew所指向的類型和ipold所指向的類型也相同。ipnew的值將比ipold的值減小了n乘sizeof(ipold所指向的類型)個字節,就是說,ipnew所指向的內存區將比ipold所指向的內存區向低地址方向移動了n乘sizeof(ipold所指向的類型)個字節。

1.4.3關係運算

指向同一個數組中的不一樣元素的兩個指針能夠進行各類關係運算。例如:

ip1==ip2表示ip1和ip2指向同一數組元素

ip1>ip2表示ip1處於高地址位置

ip1<ip2表示ip2處於低地址位置

指針變量還能夠與0比較。設ip爲指針變量,則ip==0代表ip是空指針,它不指向任何變量;ip!=0表示ip不是空指針。空指針是由對指針變量賦予0值而獲得的。例如:

#define NULL 0

int *ip=NULL;

 對指針變量賦0值和不賦值是不一樣的。指針變量未賦值時,能夠是任意值,是不能使用的。不然將形成意外錯誤。而指針變量賦0值後,則可使用,只是它不指向具體的變量而已。

1.4.4取地址運算符‘&’和取內容運算符‘*’

取地址運算符&是單目運算符,其結合性爲自右至左,其功能是取變量的地址。

取內容運算符*是單目運算符,其結合性爲自右至左,用來表示指針變量所指的變量。在*運算符以後跟的變量必須是指針變量。須要注意的是指針運算符*和指針變量說明中的指針說明符* 不是一回事。在指針變量說明中,‘*’是類型說明符,表示其後的變量是指針類型。而表達式中出現的‘*’則是一個運算符用以表示指針變量所指的變量。以下例子:

int a=12;

int b;

int *p;

int **ptr;

p=&a;   //&a的結果是一個指針,類型是int*,指向的類型是int,指向的地址是a的

//地址。

*p=24;   //*p的結果,在這裏它的類型是int,它所佔用的地址是p所指向的地址。

ptr=&p; //&p的結果是個指針,該指針的類型是p的類型加個*,在這裏是int **。該

//指針所指向的類型是p的類型,這裏是int*。該指針所指向的地址就是指針

//p本身的地址。

*ptr=&b;//*ptr是個指針,&b的結果也是個指針,且這兩個指針的類型和所指向的類型//是同樣的,因此用&b來給*ptr賦值就是毫無問題的了。

**ptr=34;//*ptr的結果是ptr所指向的東西,在這裏是一個指針,對這個指針再作一次*

//運算,結果就是一個int類型的變量。

1.4.5關於括號組合

在解釋組合說明符時, 標識符右邊的方括號和圓括號優先於標識符左邊的「*」號,而方括號和圓括號以相同的優先級從左到右結合。但能夠用圓括號改變約定的結合順序。

閱讀組合說明符的規則是「從裏向外」。從標識符開始,先看它右邊有無方括號或園括號,若有則先做出解釋,再看左邊有無*號。 若是在任什麼時候候遇到了閉括號,則在繼續以前必須用相同的規則處理括號內的內容。

1.5指針表達式

 一個表達式的最後結果若是是一個指針,那麼這個表達式就叫指針表式。因此指針表達式也具備指針所具備的四個要素:指針的類型,指針所指向的類型,指針指向的內存區,指針自身佔據的內存。

相關文章
相關標籤/搜索