指針詳解

星號的祕密
1、乘法運算符
 
2、定義指針
int *p = 0; 還是 int* p = 0;?
後一種比較容易這樣理解:定義了一個變量p,它是指針型的(更詳細一點,是指向int的指針型),相比而言,前面一種定義似乎是定義了*P這個奇怪的東西。但是後面一種寫法會帶來一個容易產生的誤解:
int* p1, p2;
這兒給人的感覺似乎是定義了兩個指針型變量p1和p2,但是,事實上,這種直覺是錯誤的,正確的理解方式是int *p1, p2;即p1是指針型的,而p2確是整型的。
在MS VC++ 6.0中,是按照後面一種格式寫的。
 
3、何謂指針?
指針僅僅表示一個內存中的某個地址?
非也,注意到,我們在定義指針的時候,都關聯了一個類型,如int,char,或者是string等等,如果說指針僅僅表示一個內存中的地址,那何必要關聯這麼多變化的東西呢?完全可以DWORD p=0;這樣解決問題。
關聯了的數據類型是作何用的呢?
它可以指示編譯器怎樣解釋特定地址上內存的內容,以及該內存區域應該跨越多少內存單元。如 int *p;
編譯器可以從這個定義中獲得信息:1、p指向的內存存放的是整型數據,2、由於該內存區域只存放了一個數據,跨越的內存區域爲4個字節,即p+1的效果是跳過了四個字節。
另一個複雜一點的例子,如
struct a
{int x1;
short x2;
a *next;
}
定義指針 a *p;那麼編譯器對這個指針又作何解釋呢?
1、p指向的內存區域依次存放了三種類型的數據,分別是int,short和一個指針型數據。
2、p指向的內存區域跨越了12個字節,即p+1的效果是跳過了12個字節。(爲何不是10?對齊的原因)
 
但是,C++中定義了一種特殊的指針,它去處了一般指針中對內存區域內容以及大小的解釋,以滿足特定定的需要,如我們只需要某塊內存的首地址,不需要考慮其中的數據類型以及大小。這種形式爲 void *; 這種類型的指針可以被任意數據類型的指針賦值,如上面的a* 型,void *q = p; 唯一例外的是,不能把函數指針賦給它。
 
4、關於const修飾符
當const遇到指針,麻煩事就來了,看:const int* p; int* const p; const int* const p;
這三個表達式,第一個表示p是一個指針,p本身平凡無比,但是p所指向的對象是一個特殊的對象--整型常量;第二個表示:這個p指針不是一個普通的指針,它是個常量指針,即只能對其初始化,而不能賦值,另外,這個指針所指向的對象是一平凡的int型變量;第三個則結合了前兩者:指針和指向的對象都非同尋常,都是常量。
有了const,賦值的問題就變得麻煩起來,
首先,對於 const int* p;這兒由於p指向的對象是個常量,所以在通過p來引用這個對象的時候不可對其進行賦值!對於一個常量對象,不可以用普通的指針指向,而必須用這種指向常量的指針,原因很簡單,通過普通指針可以改變指向的那個值,但是對於一個非常量對象,即普通變量,可不可以將其地址賦給指向常量的指針呢?是可以的,但是一旦這樣指向之後,由於這個指針本身定義的是指向常量的指針,因而編譯器統一認爲其是指向變量的,因而此時不可以通過該指針修改所指向的對象的值。
第二,對於 int* const p;這兒p本身是個常量指針,所以根本就不能賦值,所以不存在賦值的問題。不可以用常量對其進行初始化,因爲這個指針不是指向常量的;只能用變量對其初始化。
第三,對於 const int* const p;這兒,只能初始化,不能賦值。可以利用常量進行初始化;也可以利用變量對其初始化,不過不可以利用該指針對該變量進行賦值。
 
const int* p這種指向常量對象的指針常用來用作某些函數的形參,用意是從編譯器的角度防止用戶在函數中將傳遞進去的參數修改,雖然用戶本身也可以避免,但是這樣更可靠一點--當用戶不小心作出修改實參的行爲時,編譯器發現並阻止這種行爲。
 
this指針是const xx* const型的。
 
5、函數與指針
指向函數的指針:可以利用它代替函數名來調用函數。
如何定義函數指針,由於一個程序中可以用多個函數名相同的情形(即函數的重載),因而,定義函數指針的時候,必須包含函數的參數,這樣才能準確地將指針指向某函數。
定義:int (*p)(const char*, int); 表示p是一個指向函數的指針,該函數的兩個參數爲const char* 和int,另外該函數返回int型值。
容易混淆的是:int *p(const char *, int); 缺少了一個括號,此時編譯器的解釋是 int* p(const char*, int);即其含義是一個函數的聲明,函數名爲p,返回一個指向int型的指針。那麼 int* (*p)(const char*, int);則是定義了一個函數指針p,它指向一個函數,該函數的兩個參數爲const char*和int,該函數返回一個指向int型的指針
 
函數指針的初始化與初始化:
函數名如同數組名,編譯器將其解釋爲指向該類型函數的指針,故而,可以領用函數名,或者&函數名對函數指針進行初始化或者賦值,另外,可以用另一個函數指針對該指針進行初始化以及賦值。重要的一點是指針與函數名,指針與指針必須具有完全相同的參數表和返回類型(必須完全完全一樣,任何一點不同都不可以)。不存在隱式的類型轉換,用戶必須保證完全的一致性。
初始化或者賦值爲0,表示不指向任何函數。
 
利用函數指針調用函數是可以p(x,y)這樣調用,也可以(*p)(x,y)這樣調用,前提是p已經正確的賦值或者初始化。
 
函數返回指針:可以返回一個非基本類型的對象。
 
6、數組與指針
int a[3] = {1,2,3};
考慮 a,a[0], &a, 以及 &a[0]這三個表達式的含義:
首先這三個表達式的數值結果是一樣的--數組的首地址(即數組中第0個元素的地址),但是編譯器對三者的解釋不同:
對於a,編譯器將其解釋爲一個指針,指向的是一個整型數據,因而利用a+1即指向數組中的第一的元素,a+2指向第二個元素。
對於a這個指針有些特殊的性質:
a不是一個普通的指針,它同時是一個數組名,即關聯了一個數組,因而某些性質上與普通的指針不同。
普通的指針可以被賦值,即可以用一個地址或者另一個指針修改當前指針的指向,然而對於a這種關聯了一個數組的指針,如果允許這樣賦值的話,那麼數組中的元素將無法被訪問,所以不允許對數組名代表的指針進行賦值。在這一點上a相當於指針常量,即只能被初始化,不可以進行賦值。
   雖然a不可以被賦值,但是將a賦給其他的元素是完全可以的,這一點同普通的指針沒有不同。
綜上,a相當於一個指針常量。(type* const型的)
 
本質上a[i]操作被編譯器解釋爲*(a+i)操作,即[]運算符是通過數組名指針實現的,因而&a[0]的含義即&(*a),顯然對一個指針先*(解引用),再&(引用),等價於什麼都沒做,還是這個指針本身,因而a完全等價於&a[o],--(&a[0])[i]等價於a[i],形式有點詭異,呵呵。而對於&a這個表達式,奇怪的是這個也是數組的首地址,那麼就是說,這個數組的首地址中存放了一個指針常量(即數組名),但是數組的首地址中不是存放的一個int型的數字嗎?這是怎麼回事呢?難道一個地址能存放兩個東西?
暫時無法解釋,可以這樣認爲編譯器發現這種&和數組名的結合運算時,即返回數組首地址,只不過,這是,這僅僅是個純粹的地址,它不再具有指針的特性,即編譯器不再將其解釋爲指針,用戶不可以通過+1運算來訪問下一個數組元素。它的+1就是數學上的+1。
 
當數組變爲多維,問題變成怎麼樣了呢?
考慮二維數組 int b[4][3] = {{1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}}; b,&b, b[0], &b[0], &b[0][0]幾個表達式的含義:
首先,c++中數組元素的存放是以行序爲主序,即第一段存放第一行的數據,第二段存放第二行的數據,....,如此。
首先考慮數組名b,編譯器同樣將數組名b解釋爲一個指針,但是顯然這個指針不是普通的指針。這個b指向數組所有元素的首地址,這一點是勿庸置疑的,那麼這個指針一步跨越的內存是多大呢?在本例中,b一步跨越12個字節,即b一步跨越數組中的一行元素,實際上b是一個指針的指針,或者說指向指針的指針,即b所指向的內容是一個指針,(同樣對於b+1,b+2),b[i][j]這種訪問方式本質上即:先通過+i,將指針跳躍到第i行,從而獲得了指向第i行首地址的指針b[i],然後通過這個指針,再通過+j,跳躍j步,到達了第j個元素,即找到第i行,第j列的元素。
所以b是指針的指針,b[i]是指針,這兒,b[i]類似於一維中的a。
那麼&b呢?&b仍然是數組的首地址,但是跟一維類似的是,這是個純粹的地址,不再具有指針的特性,它的+1就是數學上的+1,不可以利用+1來訪問下一個元素。同樣的道理對於&b[i],&運算符加上之後,本來是作爲指針的b[i]被剝奪的指針的資格,返回一個純粹的地址。
實際上,由於[]本質上是對指針的解引用,那麼我們訪問數組元素時可以不拘於a[i][j]這種方式,可以這樣:(*(a+i))[j], 或者*(a[i]+j),或者 *(*(a+i)+j),這幾種寫法是等價的。
對於&b[i][j]呢?我們把b[i][j]換一種寫法,寫成*(*(b+i)+j),這樣問題就容易看清楚了,原來的*b[i][j]就等價於&(*(*(b+i)+j)),我們可以把最外層的括號脫掉,就成了*(b+i)+j,即b[i]+j,顯然這是一個指針,指向第i行,第j列元素的指針,對該指針的解釋是一次跨越一個int型的數據。
 
讓我們再變態一點,考慮三維的情形,雖然三維的數組不多見,還是考慮一下吧,畢竟空間的座標是用三維表示的。
int c[2][3][4] = {{{1,2,3,4},{5,6,7,8},{9,10,11,12}}, {{13,14,15,16},{17,18,19,20},{21,22,23,24}}};
首先,數組名c,編譯器將c解釋爲一個指針,指向數組的首地址,由於行序是主序,所以,該指針一步跨越12個整型數,共48個字節,實際上即跨越了一個二維數組。
對於&c,跟一維二維的情形類似,是一個純粹的地址.
c[i]呢?可以推測,c[i]與二維中的b類似,即指向指針的指針,c[i]一步跨越4個整數,16個字節。c[i]是指向指針的指針,那麼c便是指向指向指針的指針的指針(暈~)。
c[i]亦等價於*(c+i)
至於c[i][j],這纔是真正的int型的指針,即指向真實數據的指針,一步跨越一個int型,4個字節。跟二維類似,對於&c[i][j],編譯器返回一個地址,雖然跟c[i][j]的值一樣,但是隻是一個純粹的地址,跨越單元爲一個字節。
對於c[i][j][k],不需要廢話,對於&c[i][j][k],這是一個地址嗎?這是一個指針嗎?我們還是要藉助[]的另一種表示方法:c[i][j][k]等價於*(*(*(c+i)+j)+k),那麼&c[i][j][k]就等價於*(*(c+i)+j)+k,即c[i][j]+k,即指向第(i,j,k)個元素的指針,一步跨越單元爲一個int型。
讓我們來看一看,尋找第(i,j,k)個元素有哪些寫法:
1、c[i][j][k]
2、*(c[i][j]+k)
3、*(*(c[i]+j)+k)
4、*(*(*(c+i)+j)+k)
5、(*(c+i))[j][k]
6、(*(*(c+i)+j))[k]
7、*((*(c+i))[j]+k)
8、(*(c[i]+j))[k]
可見,共八種寫法,實際上就是一共有三個解引用,選擇用[]還是用*,這樣,總共有8個組合。(那麼二維的就是4種,一維的2種)
 
7、typedef與指針
typedef似乎很簡單,如typedef int integer;然而,這些簡單的typedef語句容易讓人產生一種誤解,typedef就是一種宏替換,把後面的自定義類型替換成前面的已知類型,事實是這樣的嗎?顯然不是!
考慮這樣的問題:如何定義一個指向整型的指針類型?如何定義一個函數指針類型?
第一個問題很簡單:typedef int* int_pointer;即可,對於第二個問題,似乎就沒有那麼簡單了,首先,看函數指針的定義方法:int (*p)(const&, int); 這個p指向的函數必須返回int,形參必須是const&和int。現在要將這種指針類型命名爲func_pointer,其定義的方法如下:
typedef int (*func_pointer)(const&, int);
可以這樣來理解:typedef int integer;將typedef去掉,那就是個變量的定義,這兒即定義了一個int型的變量integer,考慮這個integer是什麼類型的,那麼這個typedef語句就是將integer定義爲這個類型的。將typedef int (*func_pointer)(const&, int);中的typedef去掉,就成了一個函數指針定義,即func_pointer被定義爲函數指針類型變量,那麼原來的typedef即將func_pointer定義爲函數指針類型。
 
8、函數,數組與指針
int (*testCases[10])();
這個表達式是什麼意思?指針,數組,函數糅合在了一起問題變得複雜起來。它定義了數組,testCases[10],數組中的元素是函數指針,函數指針的類型是 int (*)();
怎麼來理解這種定義呢?首先考慮數組的定義,數組的定義一般模式是:
類型 數組名[大小];
考慮這個表達式,似乎是定義了一個數組,但是數組名[大小]被夾在了中間,那麼類型是什麼呢,發現類型並不是簡單的數據類型,而是一個函數指針類型int (*p)(),這個函數沒有參數,返回int型。從而這個表達式的含義是:定義了一個函數指針型的數組,大小是10。
可以利用typedef來簡化這種定義:
typedef int (*PFV)();
PFV testCases[10];
 
其實int (*testCases[10])();這兒我們定義了一個函數指針數組,數組是主體。
下面考慮這樣的問題:如何定義一個指向數組的指針?
指向數組的指針,好像比較新鮮,所謂指向數組的指針,即指針的一步跨越是一個數組,跟指向整型的指針一步跨越一個整型一個道理。事實上前面已經碰到了指向數組的指針,如二維數組名,實際上就是一個指向數組的指針,它一次跨越一行的數據,實際上即是跨越了一個一維數組,而三維數組名呢,也是一個指向數組的指針,它一次跨越的是低維組成的一個二維數組。
數組指針(即指向數組的指針)的定義:
int (*ptr)[3];  這個表達式定義了一個數組指針ptr,ptr一次跨越一個由3個int型組成的一維數組。發現其定義的方式與函數指針定義的方式很相似,只是把()換作了[]。
更進一步,如果要定義一個指向數組的指針,而數組中的元素不是簡單的int型,而是比較複雜的類型,那該如何定義呢?事實上數組指針這種東西就已經夠稀有的了,一般編程絕對不會用到,我們只需要能讀懂一些比較複雜的東西就行了,自己沒有必要構造這麼複雜的類型。
 
比較複雜的表達式:
      1、int (*(*(*p())[])())[];
首先,根據p()判斷p是一個函數,再根據p()前面的*號判斷該函數返回一個指針,下面就看這個指針指向的是什麼類新了,我們可以把*p()替換成一個*pointer,這個pointer就是函數p返回的指針,那麼就成了int (*(*(*pointer)[])())[];再根據(*pointer)[],這說明了指針pointer是指向的一個數組,那麼這個數組中的元素是什麼類型呢?由於數組名實際上就是個指針,我們把(*pointer)[](即(*p())[])替換成一個array,這樣就成了 int (*(*array)())[];發現array是一個函數指針,從而數組中的每個元素是函數指針,而這個函數呢,又返回一個指針類型,把(*array)()用func代替,就成了int (*func)[];這說明了func函數返回的是指向數組的指針,數組中的元素是int型。
這個表達式夠酷!!!
2、p = (int( * (*)[20])[10])q;
這是一個強制類型轉換,q被強制類型轉換成一個這樣的指針類型,這個指針呢直線一個具有20個元素的數組,這個數組中的元素也是指針,是指向另外一種數組,這種數組是含有10個int型數據的一維數組。
 
可見,分析複雜的表達式時(所謂複雜,即糅合了指針,數組,函數三樣,缺少了一樣就不會複雜了),從括號的最裏層做起,最裏層的東西是複雜表達式的“根節點”,然後一層一層脫,脫的時候,是這樣的,比如裏層是個數組,那麼就是說這個數組的元素是什麼呢,那就是外層的東西,如果裏層是個有返回值的函數,那麼就是說這個函數返回什麼值呢?那就是外層的東西,就這樣一層一層地把表達式解析清楚。
 
 
關於typedef還有一些要說的:
typedef  int (*PFV)(); 這是定義了一個函數指針,那麼PFV p;就可以定義了一個指向函數的指針。
typedef int (*p[10])(); 這是把p定義爲函數指針數組,那麼 p array;語句就可以定義了一個函數指針數組,數組名即爲array,array這個數組含10個元素。
typedef int (*parray)[3];這是定義了一個指向整型數組的指針,那麼 parray ptr;就定義了一個指向數組的指針。如何對這個ptr賦值或者初始化呢?事實上,是通過二維數組名來對其進行賦值(初始化)的,因爲二維數組名作爲指針來講,就是一個指向數組的指針,一次跨越一個數組。
typedef int a[3][3]; 這個語句什麼意思呢?這是把a定義爲一個3*3的整型數組類型。當a b = {1}時就完成了一個3×3的整型數組的定義初始化的工作。
同樣,簡單一點 typedef int a[3];這個語句是把a定義爲一個一維數組類型。
typedef  void func(int); 這個語句定義了一個函數類型。通過這個typedef,我們可以比較清晰地定義出函數指針,func* p;即可。
 
typedef char* string;
const string str;
這個str是什麼類型的呢?const char * str,即指向常量的指針類型?事實上,答案有些不可思議,str是一個常量指針,而不是指針常量,即const修飾符針對的是指針,而不是char。
 
9、引用與指針
引用類似與指針常量,只可初始化,不可賦值。別名(alias)是引用(reference)的另一種叫法。通過引用可以間接地操操縱對象。
常量引用,即類似與指向常量的常量指針,對常量引用的初始化,有一點特殊,可以用常量,變量,甚至是常數對其進行初始化。
對於用變量初始化常量引用,那麼不能通過這個引用修改這個變量,但是本來的變量名可以。這一點,類似變量地址賦給常量指針。通過常量指針不可以修改變量,但是變量自身的變量名可以。
可以有指針的引用,如 int a = 1; int* p = &a; int* &r = p; 那麼r就成了指針p的引用。如果是const int* p = &a;說明是常量指針,那麼定義引用的時候,就要如此定義,const int* &r = p;這個語句說明r是一個指針的引用,這個指針是個指向常量的指針變量,而並不意味着這個引用是個常量引用。那如果說這個指針不僅僅是常量指針,而且是個指針常量,即const int* const p;那麼定義引用時要注意應該const int* const &r = p;這樣表明該引用是個常量引用。這裏有一個問題當用一個變量的地址初始化引用時如,int a = 22; int* const &pi_ref = &a;需要注意應該定以爲常量引用,因爲&a不是變量名,而是類似常數。而若const int a = 22;則應該const int* const &pi_ref = &a;即中間的const是用來定義常量引用的,而前面的const反映的是引用指向的對象(指針)是指向的的const對象。
 
我們有對象名,或者對象的指針,這些都可以操縱對象,爲何要引入引用的概念呢?
事實上,引用最常用的是用作函數的形參。要在函數中操縱一個外部對象的時候,利用引用是一個好辦法。
 
關於引用
首先,引用只可初始化,不可被賦值,因爲,被初始化後的引用,就成了被引用的對象的別名,再行賦值,就不是對引用本身的賦值了,而是對所引用的對象的賦值了。
 


 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章