一天,兩個變量在街上遇到了:
“老兄,你家住哪兒啊?改天找你玩兒去。”
“哦,我家在靜態存儲區的0x0049A024號,你家呢?”
“我家在動態存儲區的0x0022FF0C號。有空來玩兒啊。”
在前面的章節中,我們學會了用int等數值數據類型表達各種數字數據,用char等字符數據類型表達文字數據,我們甚至還可以用結構體將多個基本數據類型組合形成新的數據類型,用以表達更加復雜的事物。除了這些現實世界中常見的數據之外,在程序設計當中,我們還有另外一種數據經常需要處理,那就是變量或者函數在內存中的地址數據。比如,上面對話中的0x0049A024和0x0022FF0C就是兩個變量在內存中的地址。而就像對話中所說的那樣,我們可以通過變量在內存中的地址便捷地對其進行讀寫訪問,因而內存地址數據在程序中經常被用到。在C++中,表示內存地址數據的變量被稱為指針變量,簡稱指針。
指針,是C++從C語言中繼承過來的,它提供了一種簡便、高效地直接訪問內存的方式。特別是當要訪問的數據量比較大時,比如某個體積比較大的結構體變量,通過指針直接訪問變量所在的內存,要比移動復制變量來對其進行訪問要快得多,可以起到四兩撥千斤的效果。正確地使用指針,可以寫出更加緊湊、高效的代碼。但是,如果指針使用不當,就很容易產生嚴重的錯誤,並且這些錯誤還具有一定的隱蔽性,極難發現與修正,因而它也成為千千萬萬程序員痛苦的根源。愛恨交織,是程序員們對指針的最大感受,而學好指針,用好指針,也成為每個C++程序員的必修課。
指針是專門用來表示內存地址的,它的使用跟內存訪問密切相關。為了更好地理解指針,我們先來看看C++中內存空間的訪問形式。
在C++程序中,有兩種途徑可以對內存進行訪問。一種是通過變量名間接訪問。為了保存數據,通常會先定義保存數據的變量。定義變量也就意味著系統分配一定的內存空間用於存儲某個數據,而變量名就成了這塊內存區域的標識。通過變量名,我們可以間接地訪問到這塊內存區域,在其中進行數據的讀取或者寫入。
另外一種方式就是直接通過這些數據所在內存的地址,也就是通過指針來訪問這個地址上的數據。
這兩種都是C++中訪問內存的方式,只是一個間接一個直接。打個不太恰當的比喻,比如我們要送一個包裹(數據)到某個地方(內存中的某塊區域)去。按照第一種方式,我們說:送到亞美大廈(變量名)。而按照第二種方式,我們會說:送到科技路83號(內存地址)。雖然這兩種方式表達形式不同,但實際上說的是同一件事。
在典型的32位計算機平台上,可以把內存空間看成是由很多個連續的小房間構成的,每個房間就是一個小存儲單元,大小是一個字節(byte),而數據們就住在這些房間當中。有的數據比較小,比如一個char類型的字符,它只需要一個房間就夠了。而有的數據比較大,就需要占用好幾個房間。比如一個int類型的整數,其大小是4個字節,就需要4個房間才可以安置。為了方便地找到住在這些房間中的數據,房間都被按照某種規則進行了編號,這個編號,就是通常所說的內存地址。這些編號通常用一個32位的十六進制數來表示,比如上面例子中的0x0049A024、0x0022FF0C等如圖3-6所示。
圖3-6 住在內存中的數據
一旦知道某個數據所在的房間編號,就可以直接通過這個編號來對相應房間中的數據進行讀寫訪問。就像上面的例子中把包裹直接送到科技路83號一樣,我們也可以把數據直接保存到0x0022FF0C。
指針,作為一種表示內存地址的特殊變量,其定義的形式也有一定的特殊性:
數據類型* 變量名;
其中,我們用指針所表示地址上的數據類型來作為定義指針變量時用的數據類型。比如,我們要定義一個指針來表示某個int類型數據的地址,那麼指針定義中的數據類型就是int。這個數據類型是由指針所指向的數據來決定的,可以是int、string和double等基本數據類型,也可以是自定義的結構體等復雜數據類型。簡而言之,指針指向的數據是什麼類型,就用這種類型作為指針變量定義時的數據類型。數據類型之後的“*”符號表示定義的是一個指針變量。“變量名”就是給這個指針指定的名字。例如:
// 定義指針變量p,它可以記錄某個int類型數據的地址 int* p; // 定義指針變量pEmp,它可以記錄某個Employee類型數據的地址 Employee* pEmp
最佳實踐:選擇合適的定義指針變量的方式
實際上,下面兩種定義指針變量的形式都是合乎C++語法的:
int* p; int *p;
這兩種形式都可以編譯通過,並表示相同的語法含義。但是,這兩種形式所反映的編程風格和對代碼閱讀者所強調的意義不同。
“int* p”強調的是“p為一個指向int類型整數的指針”,這裡,可以把int*看成為一種特殊的數據類型,而整個語句強調的是p為這種數據類型(int*)的一個變量。
“int *p”則是把*p當成一個整體,強調的是“這個指針指向的是一個int類型的整數”,而p就是指向這個整數的指針。
這兩種形式沒有對與錯的區別,只有個人喜好的區別。本書推薦第一種形式,它把指針也當成是一種數據類型,定義指針變量的語句更加清晰明了,可讀性更強。
特別地,當在一條語句中定義多個指針變量時,可能會讓人混淆,例如:
// p是一個int類型的指針變量,而q實際上是一個int類型的變量 // 可能會讓人誤認為p和q都是int類型指針 int* p, q; // 清楚一些:*p是一個整數,p是指向這個整數的指針,q也是一個整數 int *p, q; // 定義兩個指向int類型數據的指針p和q int *p, *q;
在開發實踐中,有這樣一條編碼規范:“一條語句只完成一件事情”。按照這條規范,只要我們分開定義p和q,就可以很好地避免上述問題。
如果我們確實需要定義多個相同類型的指針變量,我們也可以用typedef關鍵字將指針類型定義成新的數據類型,然後用這個新的數據類型定義多個指針變量:
// 將Employee指針類型定義成新的數據類型EMPointer typedef Employee* EMPointer; // 用EMPointer類型定義多個指針變量,這些變量都是“Employee*”類型 EMPointer pCAO,pCBO,pCCO,pCDO;
3.9.3 指針的賦值和使用
在定義得到一個指針變量之後,指針變量的值還是一個隨機值。它所指向的可能是某個無關緊要的數據,但也可能是重要的數據或者程序代碼,如果直接使用其後果是不可預期的。也許啥事兒沒有,也許因此而引起地球毀滅。所以在使用指針之前,必須對其賦值進行初始化,將其指向某個有意義的合法內存位置。對指針變量進行賦值的語法格式如下:
指針變量 = 內存地址;
可以看到,對指針變量的賦值,實際上就是將這個指針指向某一內存地址,而這個內存地址上存放的就是這個指針想要指向的數據。我們知道,數據是用變量來表示的,獲得變量的內存地址也就相當於獲得這個數據所在的內存地址,進而也就可以用它對指針變量賦值了。在C++中,我們可以利用“&”取地址運算符,將它放在某個變量的前面,就可以獲得這個變量所在的內存地址。例如:
// 定義一個整型變量,用以表示整型數據1003 int N = 1003; // 定義整型指針變量pN,用“&”符號取得整型變量N的地址, // 並將其賦值給整型指針變量pN int* pN = &N;
這裡,我們用“&”符號取得整型變量N的內存地址,這也就是1003這個整型數據所在的內存地址,然後將其賦值給整型指針變量pN,也就是將指針pN指向了1003這個數據。如圖3-7所示。
圖3-7 指針和指針所指向的數據
指針的初始化賦值最好是在定義指針的時候同時進行,比如上面的例子中,在定義指針pN的同時即取得變量N的內存地址賦值給它,從而使得指針在一開始就有一個合理的初始值,避免未初始化的指針被錯誤地使用。如果在定義指針時,確實沒有一個合理的初始值,我們可以將其賦值為nullptr關鍵字,它表示這個指針沒有指向任何內存地址,是一個空指針(null pointer),還不能使用。例如:
// 定義一個指針變量pN,賦值為nullptr表示它沒有指向任何內存位置 // 這裡只是定義變量,後面才會用到 int* pN = nullptr; // … // 判斷pN是否指向了某個數據 // 如果pN的值不是nullptr初始值,就表示它被重新賦值指向了某個數據 if(nullptr != pN) { // 使用pN指針訪問它所指向的數據 }
可以用“&”獲得一個數據的內存地址,反過來,我們也可以用“*”獲得一個內存地址上的數據。“*”稱為指針運算符,也稱為解析運算符。它所執行的是跟“&”運算符完全相反的操作。如果把它放在一個指針變量的前面,就可以取得這個指針所指向內存地址上的數據。例如:
// 輸出pN指向的內存地址0x0016FA38 cout<<pN<<endl; // 通過“*”符號獲取pN所指向內存地址上的數據“1003”並輸出 // 等同於cout<<N<<endl; cout<<*pN<<endl; // 通過指針修改它所指向的數據 // 等同於N = 1982; *pN = 1982; // 輸出修改後的數據“1982” cout<<*pN<<endl;
通過“*”運算符可以取得pN這個指針所指向的數據變量N,雖然“N”和“*pN”的形式不同,但是它們都代表內存中的同一份數據,都可以對這個數據進行讀/寫操作,並且是等效的。
特別地,如果一個指針指向的是一個結構體類型的變量,與結構體變量使用“.”符號引出成員變量不同的是,如果是指向結構體的指針,則應該用“->”符號引出其成員變量。這個符號,多像一個指針。例如:
// 定義一個結構體變量 Employee Zengmei; // 定義一個指針,並將其指向這個結構體變量 Employee* pZengmei = &Zengmei; // 用“->”運算符,引用這個結構體指針變量的成員變量 pZengmei->m_strName = "Zengmei"; pZengmei->m_nAge = 28;
最佳實踐:盡量避免把兩個指針指向同一變量
當指針變量被正確賦值指向某個變量後,它也就成為了一個有效的內存地址,也可以用它對另外一個指針賦值。這樣,兩個指針擁有相同的內存地址,指向同一內容。例如:
// 定義一個整型變量 int a = 1982; // 得到變量a的內存地址並賦值給指針pa int* pa = &a; // 使用pa對另外一個指針pb賦值 int* pb = pa;
在這裡,我們用已經指向變量a的指針pa對指針pb賦值,這樣,pa和pb的值是相同的,都是變量a的地址,也就是說,兩個指針指向了同一個變量。
值得特別指出的是,雖然兩個指針指向同一變量在語法上是合法的,可是在實際的開發中,卻是應當盡量避免的。稍不留意,這樣的代碼就會給人帶來困擾。繼續上面的例子:
// 輸出pa指向的數據,為1982 cout<<*pa<<endl; // 通過pb修改它所指向的數據為1003 *pb = 1003; // 再次輸出pa指向的數據,變為了1003 cout<<*pa<<endl;
如果我們僅僅看這段程序的輸出,一定會感到奇怪:為什麼沒有通過pa進行任何修改,而前後兩次輸出的內容卻不同?如果我們結合前面的代碼,就會明白,pa和pb指向的是同一個變量a,當我們通過指針pb修改變量a後,再通過pa來獲得變量a的數據,自然就是更新過後的了。表面上看起來沒有通過pa對變量a作修改,而pb卻早已暗渡陳倉,偷偷地將變量a的數據做了修改。在程序當中,是最忌諱這種偷偷摸摸的行為的,因為一旦這種行為導致了程序運行錯誤,將很難被發現。所以,應盡量避免兩個指針指向同一變量,就如同一個人最好不要取兩個名字一樣。
正常是作不到的,不知道你是怎麼得到這個地址的(最好補充一下我看看)。
只有編譯器分配給程序的地址程序才能調用成功,而編譯器的分配是取決於操作系統的,你無法指定某個內存,就算你能指定指針指向那個地址,在運行時也會提示你調用了不可用內存。
但如果你的地址有針對硬件的特殊含義,例如某個端口或者是中斷向量的地址什麼的,用c的一些硬件操作函數可以做到,例如inportb()什麼的。
int main
{
int **matrix;
matrix = creatmatrix(3, 4);
for(i=0;i<3;i++)
free(matrix[i]);//直接釋放就可以了
free matrix;
matrix=NULL;
return 0;
}