曾凡舒
指針是C語言中一種廣泛使用的數據類型,也是C語言的重要特性。在C語言中,使用指針能夠編寫出高效、精煉、簡潔的程序代碼。因此,在C語言的學習過程中,能否正確理解和使用指針是檢驗是否掌握C語言的一個重要標志,但是,指針也是C語言中最為困難的部分之一。指針的學習像其他內容一樣也必須從理解基本概念開始。
一、計算機基本原理
半個多世紀以來,雖然計算機制造技術發生了巨大變化,但仍然沿用馮·諾依曼體系結構。在馮·諾依曼計算機體系結構理論中,有三個基本思想:1)計算機處理的數據和指令采均用二進制數表示;2)計算機運行過程中,指令和數據首先存入主存儲器(內存),計算機將自動地并按順序從主存儲器中取出指令一條一條地執行,這一概念稱作順序存儲程序;3)計算機硬件由運算器、控制器、存儲器、輸入設備和輸出設備五大部分組成。
計算機的主存儲器是由一系列連續編號或編碼的存儲單元組成,要運行的程序指令以及相關的數據都按照一定的順序存儲在內存中,如圖1所示。存儲單元中存儲的內容有三種:1)指令,如0x03000、0x03001存儲單元,2)直接數據,如0x05027、0x05028、0x05029、0x05030存儲單元,3)地址數據,如0x03002、0x03003存儲單元,它們存儲的是存儲單元0x05030、0x05027的地址值。
二、指針的基本概念
C語言的設計者Brian W.Kernighan在《C程序設計語言》一書中給指針的定義是:指針是一種保存變量地址的變量,因此要真確理解指針概念,先要理解幾個指針相關的概念,如地址、變量等。
地址:在計算機中,通過尋址機構將物理存儲介質映射成一維線性空間,并以字節為單位進行統一編碼,使得每個字節都具有唯一的編碼,類似于街道的門牌號碼,該編碼稱為字節的地址,也稱內存地址。內存地址采用無符號整數來表示,例如在32位計算機中,內存地址編碼為0x00000000 ~0xFFFFFFFF,能夠支持最大4GB的內存空間,每個字節的地址采用32位的無符號整數表示。
存儲單元:在計算機系統中,絕大多數的數據往往需要多個字節來存儲,如Unicode字符、整數、浮點小數、字符串等。所以,需要分配一個或連續的多個字節來存儲這些基本數據,我們把一個或連續的多個字節的存儲空間稱為一個存儲單元,每個存儲單元的地址用該存儲單元的第一個字節的地址來表示。例如,C語言中的short int、long int、char、double等基本數據類型在32位計算機系統中分別占用2、4、1、8個字節,如果在內存0x0F00地址處依次存儲short int、long int、char、double類型數據各一個,那么這四個存儲單元的地址則分別為0x0F00、0x0F02、0x0F06、0x0F07,而下一個存儲單元的地址則為0x0F0F,如圖2所示。
變量:是計算機存儲空間或存儲單元的具體化,通過一個易辨別的字符序列來標識一個存儲空間或存儲單元,這樣能夠大大提高編程效率和代碼的可讀性。變量具有三個要素:1)名稱,2)類型,3)值,此外,變量還有一個隱含屬性,即地址。例如,圖1中的變量s、i三個變量,它們的要素及屬性如表1所示。
指針:回頭再看Brian W.Kernighan的指針定義,指針本質上也是一個變量,只不過它存儲的數據是一個表示地址的無符號整數。如圖1所示,變量pi、ps就是指針變量,我們把表1加上變量pi、ps后得到表2。
三、指針定義及運算
在C語言中,表2中各變量的定義如下:
char s = ‘a, *ps; //定義并初始化char類型變量s,定義一個指向char類型變量的指針
int i = 56, *pi; //定義并初始化int類型變量i,定義一個指向int類型變量的指針
ps = &s; //將變量s的地址保存到ps變量中,則ps將指向s變量
pi = &i; //將變量i的地址保存到pi變量中,則pi將指向i變量
或者
char s = ‘a, *ps = &s ; //指針定義與賦值同時進行
int i = 56, *pi = &i;
其中,*ps稱為指針,指針變量的定義格式為:
數據類型 *指針變量名;
在上述代碼中,通過ps = &s賦值后,*ps等價于s。結合圖1,對于0x05027存儲單元中的‘a有兩種訪問方式:1)通過變量s來訪問內存地址0x05027的‘a,這種訪問方式稱為直接訪問方式;2)指針變量ps中存放的是變量s的地址,通過*ps來訪問0x05027的‘a需要兩個步驟,首先要從指針變量ps中讀取變量s的地址0x05027,再從地址0x05027中讀出‘a,這種通過指針變量存取數據的訪問方式稱為間接訪問方式。
跟指針密切相關的運算符是*和&,它們均為一元運算符,且互為逆運算。
&運算符:取地址運算符。放在普通變量的前面,作用是獲取對應變量的存儲地址,例如變量i的值為56,而表達式&i的值則為0x05030,可使用以下語句進行測試:
inti = 56;
printf ("%d, %p", i, &i);
其運行結果為:56, 0060FF34
*運算符:指針運算符,又稱間接訪問運算符。放在指針變量的前面,作用是獲取指針變量中的地址所對應的存儲單元中的數據??捎孟率龃a進行驗證。
inti = 56, *pi; // 第1行
pi = &i; // 第2行
printf ("%d, %p, %p, %d", i, &i, pi, *pi); // 第3行
運行結果為: 56, 0060FF34, 0060FF34, 56
其中,第1行的*表示定義指針變量,第3行中的*則表示指針運算。
四、指針的應用
學習指針的難點除了概念上的理解比較難以外,在應用上有以下幾個比較難理解的地方。
1.指針的初始化、釋放與NULL指針
指針在使用中容易出問題的主要環節就是指針的初始化和釋放后的處理。指針必須先申明,再賦值,然后才能使用,使用完后,需要用free()函數釋放所占用的存儲空間,最后還要對指針變量進行置空,否則會出現意想不到的問題。
首先,在聲明指針變量后,如果不進行初始化,那么該指針就是一個未初始化的指針,指針變量中的數據為內存殘存數據,如果不進行初始化,就會指向一個未知的地方,得到的是一個無效數據。如下面的程序代碼。
int *pt;
printf ("%p, %p, %d\n", &pt, pt, *pt);
運行結果:0060FF30, 004013A0, -2082109099
結果中第一個數字0060FF30表示的是指針變量pt本身的地址值;第二個數字004013A0是表示指針變量pt中的地址值,由于指針pt沒有初始化,所以指針pt變量中的地址值是原來內存中的殘存數據;第三個數字-2082109099則是殘存數據作為地址值所指內存中的數據,不是我們想要的數據,屬于垃圾數據。
其次,在指針使用完畢后,第一,要釋放指針所占的內存空間,通過free()函數完成,第二,需要對不用的指針變量置空,即設置不再使用的指針變量的值為NULL,否則釋放空間后,指針中的地址值不會變,但是指針原來所指的存儲單元將成為未使用空間或分配給其他的變量使用。
char *pta = (char *) malloc(30); //第1行
strcpy(pta, "Hello C Language!"); //第2行
printf ("%p \n", pta); //第3行
free(pta); //第4行
printf ("%p \n", pta); //第5行
//if(pta != NULL); //第6行
//strcpy(pta, "zfs"); //第7行
pta = NULL; //第8行
printf ("%p \n", pta); //第9行
結果為:
009C0DC0
009C0DC0
00000000
第一個數字009C0DC0為malloc()函數申請30個字符空間的首個字節的地址,并將該地址值保存到了指針變量pta中;第二個數字009C0DC0是對指針變量pta進行釋放處理后其中的值,可以看出,雖然釋放了pta所指向的存儲空間,但pta中的地址值仍然不變,pta所指向的存儲空間已經回收或另作他用,pta指向了不可用內存區域,成了野指針,而且不能使用(pta != NULL)進行檢測,所以第6、7行運行會出錯;第三個數字00000000是將pta變量置空后pta中的值,即將NULL賦值給pta指針變量,pta即成為NULL指針(空指針),NULL的定義在C語言標準庫頭文件stddef.h中。宏定義如下:
#undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif
2.指針與數組
C語言的數組表示的是一段連續的內存空間,用來存儲多個指定類型的數據,每個數組成員稱為一個數組元素,每個數組元素占用的字節數相等。數組和指針不是同一種結構,不可以互相轉換,但是數組變量則是指向了數組的第一個元素的內存地址,如圖3所示。
short int ai[5]= {27, 36, 51, 110, 97}; //第1行
short int *ptai; //第2行
ptai = ai; //第3行
在C語言中可以把數組變量直接賦值給指針,但不能把指針變量賦值給數組。如果把一個數組變量值賦給指針,實際上是把指向數組第一個元素的地址賦給指針。第2、3行可以理解為:
short int *ptai = &ai[0];
或者
short int *ptai;
ptai = &ai[0];
所以ai[0]與*ptai是等價,由于數組中的元素是等長的,因此,ai[1]與*(ptai + 1)、ai[2]與*(ptai + 2)也是等價的,以此類推。但是,要注意的是ptai + 1、ptai + 2中的1、2并不是按字節計算的偏移量,而是按short int類型計算的偏移量。
五、總結
指針雖然比較難以理解和掌握,需要許多計算機底層的知識作支撐,但它卻是C語言的精髓所在,是C語言的重要特性,能夠充分展現C語言的強大魅力。實際上,我們只要從變量的本質、內存地址、計算機尋址原理等多方面進行了解和貫通,指針概念以及指針的相關應用也就不難理解了。而且,通過對指針的深入了解和學習,能夠更加高效和靈活地使用數組、字符串、結構體、各種線性非線性數據結構、內存的動態分配以及文件的存取等。所以說,學好了指針,才能夠學好C語言。
責任編輯朱守鋰