指針

在計算機科學中,指針(Pointer)是程式語言中的一個對象,利用地址,它的值直接指向(points to)存在電腦存儲器中另一個地方的值。由於通過地址能找到所需的變數單元,可以說,地址指向該變數單元。因此,將地址形象化的稱為“指針”。意思是通過它能找到以它為地址的記憶體單元。在高級語言中,指針有效地取代了在低級語言,如彙編語言與機器碼,直接使用通用暫存器的地方,但它可能只適用於合法地址之中。指針參考了存儲器中某個地址,通過被稱為反參考指針的動作,可以取出在那個地址中存儲的值。作個比喻,假設將電腦存儲器當成一本書,一張內容記錄了某個頁碼加上行號的便利貼,可以被當成是一個指向特定頁面的指針;根據便利貼上面的頁碼與行號,翻到那個頁面,把那個頁面的那一行文字讀出來,就相當於是對這個指針進行反參考的動作。

在信息工程中指針是一個用來指示一個記憶體地址的計算機語言的變數或中央處理器(CPU)中暫存器(Register)【用來指向該記憶體地址所對應的變數或數組】。指針一般出現在比較接近機器語言的語言,如彙編語言或C語言面向對象的語言如Java一般避免用指針。指針一般指向一個函式或一個變數。在使用一個指針時,一個程式既可以直接使用這個指針所儲存的記憶體地址,又可以使用這個地址里儲存的函式的值。

另外,指針也指鐘錶中用來指示對應時間的部件。

基本介紹

  • 中文名:指針
  • 外文名:pointer
  • 類別:指示測量的數據的裝置
  • 適用範圍:計算機語言
  • 作用:通過它找到以它為地址的記憶體單元
簡介,信息工程,按值傳遞,*和&運算,另類*和&,雙級指針,指針的初始化,與數組關係,與“引用”的區別,其他,

簡介

使用指針來讀取數據,在重複性操作的狀況下,可以明顯改善程式性能,例如在遍歷字元串,查取表格,控制表格及樹狀結構上。對指針進行複製,之後再解引用指針以取出數據,無論在時間或空間上,都比直接複製及訪問數據本身來的經濟快速。
指針的機制比較簡單,其功能可以被集中重新實現成更抽象化的引用(reference)數據形別。許多程式語言中都支持某種形式的指針,最著名的是C語言,但是有些程式語言對指針的運用採取比較嚴格的限制,如Java一般避免用指針,改為使用引用。
有兩種含義,一是作為數據類型,二是作為實體。
指針作為實體,是一個用來保存一個記憶體地址的計算機語言中的變數。指針一般出現在比較底層的程式設計語言中,如C語言。高層的語言如Java一般避免用指針,而是引用。
指針作為數據類型,可以從一個函式類型、一個對象類型或者一個不完備類型中導出。從中導出的數據類型稱之為被引用類型(referenced type)。指針類型描述了一種對象,其值為對被引用類型的實體的引用。
C++標準中規定,“指針”概念不適用於成員指針(不包含指向靜態成員的指針)。C++標準規定,指針分為兩類:
  • object pointer type:指向void或對象類型,表示對象在記憶體中的位元組地址或空指針。
  • function pointer type:指代一個函式

信息工程

指針與C語言
大家都認為,c語言之所以強大,以及其自由性,很大部分體現在其靈活的指針運用上。因此,說指針是c語言的靈魂,一點都不為過。同時,這種說法也讓很多人產生誤解,似乎只有C語言的指針才能算指針。basic不支持指針,在此不論。其實,pascal語言本身也是支持指針的。從最初的pascal發展至今的object pascal,可以說在指針運用上,絲毫不會遜色於c語言的指針。
記憶體分配表
計算機中的記憶體都是編址的,就像你家的地址一樣。在程式編譯或者運行的時候,系統(可以不關心具體是什麼,可能是編譯器,也可能是作業系統)開闢了一張表。每遇到一次聲明語句(包括函式的傳入參數的聲明)都會開闢一個記憶體空間,並在表中增加一行紀錄。記載著一些對應關係。(如圖1所示)
圖1圖1
Declaration | ID Name Address Length
int nP; | 1 nP 2000 2B
char myChar; | 2 myChar 2002 1B
int *myPointer; | 3 myPointer 2003 2B
char *myPointer2; | 4 myPointer2 2005 2B
指針,是一個無符號整數(unsigned int),它是一個以當前系統定址範圍為取值範圍的整數。32位系統下定址能力(地址空間)是4G Bytes(0~2^32-1)二進制表示長度為32bits(也就是4Bytes), unsigned int類型也正好如此取值。
例證(一)
例證就是程式1得到的答案和程式2的答案一致。(不同機器可能需要調整一下pT的取值)
程式1
#include <stdio.h>
main()
{
char *pT;
char t='h';
pT=&t;
putchar(*pT);
}
程式2
#include <stdio.h>
main()
{
char *pT;
char t='h';
pT=(char *)1245048;
putchar(*pT);
}
加上(char *)是因為畢竟int 和char *不是一回事,需要強制轉換,否則會有個警告。因為char *聲明過的類型,一次訪問1個sizeof(char)長度,double *聲明過的類型,一次訪問1個sizeof(double)長度。
在彙編里int 類型和指針就是一回事了。因為不論是整數還是指針,執行自增的時候,都是其值加一。如果上文聲明char *pT;,彙編語言中pT自增之後值為1245049,C語言中pT++之後pT值為1245049。如果32 位系統中,上文聲明int *pT;,彙編語言中pT 自增之後值為1245049,可是C 語言中pT++之後pT值為1245052。
為什麼DOS下面的Turbo C,和Windows下VC的int類型不一樣長。因為DOS是16位的,Windows是32位的,可以預見,在64位Windows 中編譯,上文聲明int *pT;,pT++之後pT值為1245056。
例證(二)
對於複雜的結構,如C語言的結構體(彙編語言對應為Record類型)按順序分配空間。(如圖2所示)
圖2圖2
int a[20];
typedef struct st
{
double val;
char c;
struct st
pst pT[10
在32 位系統下,記憶體裡面做如下分配(單位:H,16 進制);(如圖3所示)
圖3圖3
這就說明了為什麼sizeof(pst)=16而不是8。編譯器把結構體的大小規定為結構體成員中大小最大的那個類型的整數倍。
至於pT的存儲,可以依例推得。總長為160,此不贅述。
有個問題,如果執行pT++,答案是什麼?是自增16,還是160?別忘了,pT 是常量,不能加減。
所以,我們就可以
typedef struct BinTree
{
int value;
struct BinTree *LeftChild;
struct BinTree *RightChild;
} BTree;
用一個整數,代表一棵樹的結點。把它賦給某個結點的LeftChild/RightChild 值,就形成了上下級關係。只要無法找到一個路徑,使得A->LC/RC->LC/RC...->LC/RC==A,這就構成了一棵二叉樹。反之就成了圖。

按值傳遞

C中函式調用是按值傳遞的,傳入參數在子函式中只是一個初值相等的副本,無法對傳入參數作任何改動。但實際編程中,經常要改動傳入參數的值。這一點我們可以用傳入參數的地址而不是原參數本身,當對傳入參數(地址)取(*)運算時,就可以直接在記憶體中修改,從而改動原想作為傳入參數的參數值
編程參數值
#include <stdio.h>
void inc(int *val)
{
(*val)++;
}
main()
{
int a=3;
inc(&a);
printf("%d" , a);
}
在執行inc(&a);時,系統在記憶體分配表里增加了一行“inc 中的val”,其地址為新地址,值為&a。操作*val,即是在操作a 了。

*和&運算

(*p)操作是這樣一種運算,返回p 的值作為地址的那個空間的取值。(&p)則是這樣一種運算,返回當時聲明p 時開闢的地址。顯然可以用賦值語句對記憶體地址賦值。我們假設有這么兩段記憶體地址空間,他們取值如下:(單位:H,16 進制)(如圖4所示)
圖4圖4
假設有這么一段代碼:(假設開闢空間時p 被分配給了3001H、3002H 兩個位置)
int *p;
p=2003H;
*p=3000H
**p的值為多少?
**p=*(*(p))=*(*(2003H))=*(3000H)=3000H。
那么&&p、*(&p)和&(*p)又等於多少?
&&p=&(&(p))=&(3001H),此時出錯了,3001H 是個常數怎么可能有地址呢?
*&p=*(&(p))=*(3001H)=2003H,也就是*&p=p。
&*p=&(*p)=&(3000H)=2003H,之前有人認為這個是不成立的,實際上&(3000H)是求存儲3000H這個變數所在的記憶體地址,仍然是p的值。下面的代碼是個很簡單的例子:
#include<iostream>using namespace std;//環境vc6.0int main(){    int *a;    a=(int*)5;    cout<<(unsigned int)&*a<<endl;}
輸出的結果為5

另類*和&

兩個地方要注意: 在程式聲明變數的時候的*,只是表明“它是一個無符號整數,這個整數指向某個記憶體地址,一次訪問sizeof(type)長度”。這點不要和(*)操作符混淆;
在C++程式聲明變數的時候的&,只是表明“它是一個引用,這個引用聲明時不開闢新空間,它在記憶體分配表加入新的一行,該行記憶體地址等於和調用時傳入的對應參數記憶體地址”。
這點不要和(*)聲明符,(&)操作符混淆。

雙級指針

對於一棵樹,我們通常用它的根節點地址來表示這棵樹。這就是“擒賊先擒王”。找到了樹的根,其每個節點都可以找到。但是有時候我們需要對樹進行刪除節點,增加節點操作,往往考慮到刪除根節點,增加的節點取代原來的根節點作為新根節點的情況。為了修改根節點這個“整數”,我們需要退一步,使用這個“整數”的記憶體地址,也就是指向這個“整數”的指針。在聲明時,我們用2 個*號,聲明指向指針的指針。它的意思是“它是一個整數,這個整數指向某個記憶體地址,一次訪問sizeof(int)長度,其值是一個整數,那個整數值指向某個記憶體地址,一次訪問sizeof(BTree)長度。”。詳見<數據結構>有關“樹”的程式代碼。由於存放的指針變數的地址,因此是指向指針變數的指針變數,或稱二級指針變數。

指針的初始化

對指針進行初始化或賦值只能使用以下四種類型的值:
1. 0 值常量表達式,例如,在編譯時可獲得 0 值的整型 const對象或字面值常量 0。
2. 類型匹配的對象的地址。
3. 另一對象末的下一地址。
4. 同類型的另一個有效指針。
把 int 型變數賦給指針是非法的,儘管此 int 型變數的值可能為 0。但允
許把數值 0 或在編譯時可獲得 0 值的 const 量賦給指針:
int ival;
int zero = 0;
const int c_ival = 0;
int *pi = ival; // error: pi initialized from int value of ival
pi = zero;// error: pi assigned int value of zero
pi = c_ival;// ok: c_ival is a const with compile-time value of 0
pi = 0;// ok: directly initialize to literal constant 0
除了使用數值 0 或在編譯時值為 0 的 const 量外,還可以使用 C++ 語言從 C 語言中繼承下來的預處理器變數 NULL,該變數在 cstdlib頭檔案中定義,其值為 0。如果在代碼中使用了這個預處理器變數,則編譯時會自動被數值 0 替換。因此,把指針初始化為 NULL 等效於初始化為 0 值:
// cstdlib #defines NULL to 0
int *pi = NULL; // ok: equivalent to int *pi = 0;

與數組關係

指針數組:就是一個由指針組成的數組,那個數組的各個元素都是指針,指向某個記憶體地址。 char *p[10];//p是一個指針數組
數組指針:數組名本身就是一個指針,指向數組的首地址。注意這是一個常數。
example:
char (*p)[10]//p是一個數組指針
函式指針:本身是一個指針,指向一個函式入口地址,通過該指針可調用其指向的函式,使用函式指針可實現回調函式
example:
#include <stdio.h>
void inc(int *val)
{
(*val)++;
}
main()
{
void (*fun)(int *);
int a=3;
fun=inc;//fun是一個函式指針
(*fun)(&a);
printf("%d" , a);
}
指針函式:本身是一個函式,其返回值是一個指針。
example:
void * fun(void);// fun是一個指針函式
這個問題大家應該都碰到過,指針數組和數組指針,剛開始看時覺得還是能看懂,但是過些時又搞混了,最後發現還是沒有真正理解。
下面就簡單說說這兩個概念:一:指針數組,顧名思義,就是說的首先是一個數組吧,然後數組的元素是指針而已。說明形式為:type *pointer_array[constant1][constant2]...[constantn]; 例如:int *pai[4]; 由於‘*’是自右向左結合,因此從右向左看,首先看到[4]說明是一個數組,是一個包含4個元素的數組,然後看到‘*’,顯然是指針類型,由此可以看出數組中存放的是指針而不是一般的類型。同理,char *pac[2][3]是包含有6個元素,每一個元素都是一個字元型指針。再來說說他們的初始化: int *pai[3];既然是一個包含4個整形指針的數組那么其對應的將是一個二維整形數組,因為一個整形指針對應一個一維整形數組。那我就用一個二維整形數組來初始化它,事實上一般也都是這么做的,這裡有一個二維數組,int arr[3][2]={{1,2},{3,4},{5,6}},一個三行兩列的整形數組,注意這裡的行必須和你的指針數組的維數一致,否則是不允許的,不信你可以試試。這個初始化有很多種選擇,以下只列舉常見的兩種:第一種也是很好理解的: for(int i=0;i<3;i++) pai[i]=arr[i]; 顯然arr[i]是每一行的首地址,相當於一個一維數組的數組名,如是把它送給一個整形指針pai[i]是理所當然的了。
第二種方法:在說明數組指針時就初始化:int (*ap)[2]={{1,2},{3,4},{5,6}};
注意:不能將二維數組的數組名賦給指針數組的數組名,pai=arr(錯),因為兩者的類型不一致,二維數組名的類型是指向int[][]型的指針,而指針數組的的數組名是指向int *[]類型的指針。
在c/c++語言中,指針數組最常用的場合就是說明一個字元串數組。即說明一個數組,它的每個元素都是一個字元串。
二:數組指針:指向一個數組的指針。說明形式為:type (*pointer_array)[constant1][constant2]...[constantn]; 注意這裡的圓括弧是必須就將這是由於方括弧[],較指針說明符“*”的優先權高,若無此圓括弧,編譯器將把上述說明解釋成成了一個數組指針。例如:int (*ap)[2]; 這樣就說明了一個指向包含有2個元素的整形數組的數組指針,聽起來確實有點彆扭。不過仔細分析應該還是能理解的,就是說ap是一個指針,而它指向的對象是一個指針,注意不要將它和一個指向一個整形變數的指針搞混了。同樣以一個二維數組來說明其初始化問題, int arr[3][2]={{1,2},{3,4},{5,6}};注意這裡的列數必須和數組指針所指的數組的列數相同。第一種方法: ap=arr; 為什麼這裡能這樣將二維數組名送給ap呢,你可以這樣理解,二維數組不就可以看成是一維數組的數組嗎,而一個數組指針它指向的內容就是一個一維數組,那么你就可以把這個數組指針當做是一個數組名,只不過這個數組裡的元素不是像int,char之類型的,而是一個數組,這樣你就可以把它和二維數組的數組名聯繫在一起了。
第二種方法: ap=&arr[0]; 這裡arr[0]其實就是一維數組的數組名,將它的地址給ap是很自然的,因為ap本來就是指向一個一維數組的。注意這裡不能這樣初始化:int (*a)[2]={{1,2},{3,4},{5,6}};大家可以想想為什麼。當然他們也可以動態賦值,由於篇幅就不探討了。

與“引用”的區別

C++編程中指針與引用的區別
一、指針和引用的區別
(1)引用總是指向一個對象,沒有所謂的 null reference .所有當有可能指向一個對象也有可能不指向對象則必須使用 指針.
由於C++ 要求 reference 總是指向一個對象所以 reference要求有初值.
String & rs = string1;
由於沒有所謂的 null reference 所以在使用前不需要進行測試其是否有值,而使用指針則需要測試其的有效性.
(2)指針可以被重新賦值而reference則總是指向最初或地的對象.
(3)必須使用reference的場合. Operator[] 操作符 由於該操作符很特別地必須返回 [能夠被當做assignment 賦值對象] 的東西,所以需要給他返回一個 reference.
(4)其實引用在函式的參數中使用很經常.
void Get***(const int& a) //這樣使用了引用又可以保證不修改被引用的值
{
}
★ 相同點:
1. 都是地址的概念;
指針指向一塊記憶體,它的內容是所指記憶體的地址;引用是某塊記憶體的別名。
★ 區別:
1. 指針是一個實體,而引用僅是個別名;
2. 引用使用時無需解引用(*),指針需要解引用;
3. 引用只能在定義時被初始化一次,之後不可變;指針可變;
引用“從一而終”
4. 引用沒有 const,指針有 const,const 的指針不可變;
5. 引用不能為空,指針可以為空;
6. “sizeof 引用”得到的是所指向的變數(對象)的大小,而“sizeof 指針”得到的是指針本身(所指向的變數或對象的地址)的大小;
typeid(T) == typeid(T&) 恆為真,sizeof(T) == sizeof(T&) 恆為真,
但是當引用作為成員時,其占用空間與指針相同(沒找到標準的規定)。
7. 指針和引用的自增(++)運算意義不一樣;
二、C++中指針傳遞與引用傳遞(進一步整理)
從概念上講。指針從本質上講就是存放變數地址的一個變數,在邏輯上是獨立的,它可以被改變,包括其所指向的地址的改變和其指向的地址中所存放的數據的改變。
而引用是一個別名,它在邏輯上不是獨立的,它的存在具有依附性,所以引用必須在一開始就被初始化,而且其引用的對象在其整個生命周期中是不能被改變的(自始至終只能依附於同一個變數)。
在C++中,指針和引用經常用於函式的參數傳遞,然而,指針傳遞參數和引用傳遞參數是有本質上的不同的:
指針傳遞參數本質上是值傳遞的方式,它所傳遞的是一個地址值。值傳遞過程中,被調函式的形式參數作為被調函式的局部變數處理,即在中開闢了記憶體空間以存放由主調函式放進來的實參的值,從而成為了實參的一個副本。值傳遞的特點是被調函式對形式參數的任何操作都是作為局部變數進行,不會影響主調函式的實參變數的值。(這裡是在說實參指針本身的地址值不會變)
而在引用傳遞過程中,被調函式的形式參數雖然也作為局部變數在棧中開闢了記憶體空間,但是這時存放的是由主調函式放進來的實參變數的地址。被調函式對形參的任何操作都被處理成間接定址,即通過棧中存放的地址訪問主調函式中的實參變數。正因為如此,被調函式對形參做的任何操作都影響了主調函式中的實參變數。
引用傳遞和指針傳遞是不同的,雖然它們都是在被調函式棧空間上的一個局部變數,但是任何對於引用參數的處理都會通過一個間接定址的方式操作到主調函式中的相關變數。而對於指針傳遞的參數,如果改變被調函式中的指針地址,它將影響不到主調函式的相關變數。如果想通過指針參數傳遞來改變主調函式中的相關變數,那就得使用指向指針的指針,或者指針引用。
為了進一步加深大家對指針和引用的區別,下面我從編譯的角度來闡述它們之間的區別:
程式在編譯時分別將指針和引用添加到符號表上,符號表上記錄的是變數名及變數所對應地址。指針變數在符號表上對應的地址值為指針變數的地址值,而引用在符號表上對應的地址值為引用對象的地址值。符號表生成後就不會再改,因此指針可以改變其指向的對象(指針變數中的值可以改),而引用對象則不能修改。
最後,總結一下指針和引用的相同點和不同點:
★相同點:
●都是地址的概念;
指針指向一塊記憶體,它的內容是所指記憶體的地址;而引用則是某塊記憶體的別名。
★不同點:
●指針是一個實體,而引用僅是個別名;
●引用只能在定義時被初始化一次,之後不可變;指針可變;引用“從一而終”,指針可以“見異思遷”;
●引用沒有const,指針有const,const的指針不可變;(具體指沒有int& const a這種形式,而const int& a是有 的, 前者指引用本身即別名不可以改變,這是當然的,所以不需要這種形式,後者指引用所指的值不可以改變)
●引用不能為空,指針可以為空;
●“sizeof 引用”得到的是所指向的變數(對象)的大小,而“sizeof 指針”得到的是指針本身的大小;
●指針和引用的自增(++)運算意義不一樣;
●引用是類型安全的,而指針不是(引用比指針多了類型檢查)

其他

指針可以用來有效地表示複雜的數據結構,可以用於函式參數傳遞並達到更加靈活使用函式的目的.使C語言程式的設計具有靈活、實用、高效的特點。

相關詞條

熱門詞條

聯絡我們