跳至內容

指針

維基教科書,自由的教學讀本

概述

[編輯]

信息工程中指針是一個用來指示一個內存地址的計算機語言變量中央處理器(CPU)中的寄存器(Register)。指針一般出現在比較近機器語言的語言,如匯編語言C語言面向對象語言Java一般避免用指針,而是引用。[1]指針一般指向一個函數或一個變量。在使用一個指針時,一個程序既可以直接使用這個指針所儲存的內存地址,又可以使用這個地址里儲存的變量或函數的值。

指針的實質

[編輯]

指針就是一個整數

[編輯]

計算機中的內存都是編址的,每個地址都有一個符號,就像家庭地址或者IP地址一樣。指針,是一個無符號整數(unsigned int,因不致歧義,下簡稱「整數」),它是一個以當前系統尋址範圍為取值範圍的整數。聲明指針和聲明一個無符號整數實質上並無區別。

32位系統的尋址能力(地址空間)是4Gb(0~232-1)二進制表示長度為32比特,也就是4B。不難驗證,在32位系統裡,int類型也正好是4B(32-bit)長度,可以取遍上述範圍。同理,64位系統取值範圍為0~264-1,int類型長度為8B。

例證就是程序1得到的答案和程序2的答案一致。[2]

程序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);
}

指針和整數的區別

[編輯]

既然指針的實質是一個整數,為何不用unsigned int直接聲明,或者統一用int *聲明,而要用不同的類型後面加上一個「*」表示呢?char *聲明過的類型,一次訪問1個sizeof(char)長度,double *聲明過的類型,一次訪問1個sizeof(double)長度。也正因此,程序2第6行加上「(char *)」是因為畢竟unsigned int和char *不是一回事,需要強制轉換,否則會有個警告。

在匯編里,沒有數據類型這一概念,整數類型和指針就是一回事了。不論是整數還是指針,執行自增的時候,都是將原值加一。如果上文聲明char *pT;,匯編語言中pT自增(INC)之後值為1245049,可是C語言中pT++之後pT值為1245049。如果32位系統中,s上文聲明int *pT;,匯編語言中pT自增之後值為1245049,可是C語言中pT++之後pT值為1245052。

為什麼DOS下面的Turbo C,和Windows下的VC的int類型自增時的步進不一樣長?因為DOS是16位的,Windows x86是32位的,int類型長度取決於操作系統的位長。可以預見,在Windows x64中編譯,上文聲明int *pT;,在執行pT++之後pT值為1245056。

指針的空間分配

[編輯]

基本數據類型

[編輯]

在程序編譯或者運行時,系統[3]開闢了一張表。每遇到一次聲明語句(包括變量的聲明、函數的聲明和傳入參數的聲明等等)都會開闢一個內存空間,並在表中增加一行紀錄,記載一些對應關係。以32位操作系統下為例:

聲明 序號 變量名 內存地址 訪問長度
int nP; 1 nP 2000 4B 0xCCCCCCCC[4]
char myChar; 2 myChar 2002 1B 0xCC
int * myPointer; 3 myPointer 2003 4B 0xCCCCCCCC
char * myPointer2; 4 myPointer2 2005 4B 0xCCCCCCCC

高級數據類型

[編輯]

那麼,複雜的結構怎麼分配空間呢?C語言的結構體(匯編語言對應為Record類型)按順序分配空間。

int a[20];
typedef struct st
{
    double val;
    char c;
    struct st *next;
} pst;
pst pT[10];

在32位系統下,內存裡面做如下分配:(單位:H,16進制)

變量 2000 2001 2002 2003 2004 2005 2006 204C 204D 204E 204F
地址 a[0] a[1] a[19]

 

變量 2050 2051 2057 2058 2059 205A 205B 205C 205D 205E 205F
地址 pst.val pst.c pst.next 無效 無效 無效

這就說明了為什麼sizeof(pst)=16而不是8。編譯器把結構體的大小規定為結構體成員中大小最大的那個類型的整數倍。至於pT的存儲,可以推得總長為160。如果執行pT++,答案不是自增,也不是160。因為數組聲明時,pT是常量,不能加減。

指針連接的數據類型

[編輯]

所以,我們就可以聲明:

#typedef struct BiTree
{
    int value;
    struct BiTree *LeftChild;
    struct BiTree *RightChild;
} BTree;

用一個整數,代表一棵樹的結點。把它賦給某個結點的LeftChild/RightChild值,就形成了上下級關係。只要無法找到一個路徑,使得A->LC/RC->LC/RC...->LC/RC==A(A泛指某一結點),這就構成了一棵二叉樹。反之就成了圖。

使用指針的目的

[編輯]

簡化代碼

[編輯]

如果沒有指針,我們很難用一個統一的模式去A的定位並修改一棵樹的結點。例如:不用指針要修改A的左子樹的左子樹的右子結點,只有「A.LC.LC.RC=…」一種表達方式,不能通過賦值而簡化。

參數傳遞

[編輯]

C中函數調用是按值傳遞的,傳入參數在子函數中只是一個初值相等的副本,無法對傳入參數作任何改動。但實際編程中,經常要改動傳入參數的值。這一點我們可以用一個小技巧,即傳入參數的地址而不是原參數本身,當對傳入參數(地址)取「*」運算時,就可以直接在內存中修改,從而改動原想作為傳入參數的參數值。

#include <stdio.h>
void inc(int *val)
{
    (*val)++;
}
main()
{
    int a=3;
    inc(&a);
    printf("%d", a);
}

在執行inc(&a);時,系統在內存分配表里增加了一行「val@inc」,其地址為新地址,值為&a。操作「*val」,即是在操作a了。

指針的運算和聲明

[編輯]

取地址和取值運算

[編輯]

「*p」操作是這樣一種運算,返回p的值作為地址之內存空間的取值。「&p」則是這樣一種運算,返回當時聲明p時開闢的地址。顯然可以用賦值語句對內存地址賦值。

我們假設有這麼一段內存地址空間,他們取值如下:(單位:H,16進制)

地址 0000 2000 2001 2002 2003 2004 3000 3001 3002 3003
取值 ???? 01 30 00 00 30 00 03 20 9A

然後,執行這麼一段代碼「int *p;」,假設開闢空間時p被分配3001H、3002H兩個位置。則p為2003H,*p為3001H。

**p的值為多少?

**p=*(*(p))=*(*(2003H))=*(3000H)=0300H。

那麼&&p、*(&p)和&(*p)又等於多少?

&&p=&(&(p))=&(3001H),此時出錯了,3001H是個常數怎麼可能有地址呢?

*&p=*(&(p))=*(3001H)=2003H,也就是*&p=p。

&*p=&(*(p))=&(3001H),讀者可能以為&*p=p此時出錯了,3001H是個常數怎麼可能有地址呢?

指針和引用的聲明註記

[編輯]

我們再看看另類的*和&。這裡有兩個地方要注意:

(1)在程序聲明變量的時候的*,只是表明「它是一個整數,這個整數指向某個內存地址,一次訪問sizeof(type)長度」。這點不要和(*)操作符混淆;

(2)在C++程序聲明變量的時候的&,只是表明「它是一個引用,這個引用聲明時不開闢新空間,它在內存分配表加入新的一行,該行內存地址等於和調用時傳入的對應參數內存地址」。這一點不要與「*」聲明符和「&」操作符混淆。

指針的複雜形式

[編輯]

雙重指針(指向指針的指針)

[編輯]

雙重指針是指向指針的指針,它是一個整數,這個整數指向某個內存地址,該地址的值是一個整數,指向給另一個內存地址(通常異於前者,但不排除二者相等)。綜合前述的BTree定義,對於一棵樹,我們通常用它的根結點地址來表示這棵樹。正所謂「擒賊先擒王」,找到了樹的根,其每個結點都可以找到。

但是有時候我們需要對樹進行刪除結點,增加節點操作,往往考慮到刪除根結點,增加的結點取代原來的根結點作為新根結點的情況。為了修改根結點這個「整數」,我們需要退一步,使用這個「整數」的內存地址,也就是指向這個「整數」的指針。在聲明時,我們用2個*號,聲明指向指針的指針。它的意思是「它是一個整數,這個整數指向某個內存地址,一次訪問sizeof(unsigned int)長度,其指向的內存地址所存儲的值是一個整數,那個整數值指向某個內存地址,一次訪問sizeof(BTree)長度。」詳見數據結構有關「樹」的程序代碼。

指針數組

[編輯]

指針數組:就是一個整數數組,那個數組的各個元素都是整數,指向某個內存地址。

數組指針

[編輯]

數組指針:數組名本身就是一個指針,指向數組的首地址。注意這是聲明定長數組時,其數組名指向的數組首地址是常量。而聲明數組並使某個指針指向其值指向某個數組的地址(不一定是首地址),指針取值可以改變。

指向函數的指針

[編輯]

指向函數的指針:從二進制角度考慮,數組名是該數組數據段首地址,函數名就是該代碼段的首地址,可以用「int *fun()」。在二進制層面,代碼段和數據段什麼區別?沒什麼區別。很多人都說C語言是一種面向過程的語言,因為它最多只有struct的定義,而沒有class的概念。根據本段所述,我們可以認為C語言能成為面向對象的語言,只是表述比較麻煩而已。事實上很多開源程序都使用這種方式組織他們的代碼。

#include <stdio.h>

void inc(int *val)
{
    (*val)++;
}

main()
{
    void (*fun)(int *);
    int a=3;
    fun=inc;
    (*fun)(&a);
    printf("%d", a);
}

參考

[編輯]

注釋

[編輯]
  1. 實質上Java在傳遞對象的時候用的是按指針(這裡認為指針和引用沒有本質區別)傳遞,在傳遞基本類型(如int)時用的是按值(副本)傳遞。
  2. 不同機器可能需要調整一下pT的取值。
  3. 編譯器決定了地址偏移量,操作系統決定了地址段的初始值和可訪問長度。
  4. 聲明後尚未賦值的變量,其值通常為兩個1兩個0相互交替的序列。

參考資料

[編輯]
  • Wei Huang, 指針, 2008/10/26, (於2008年10月28日查閱)