C/C++指標本質論
C/C++指標本質論
6.1.1 指標與記憶體
本章內容是重點
1.地址值、地址型別、地址變數、地址常量的引入
(1)地址值和記憶體單元。
計算機中的記憶體,一般都是以位元組為單位進行劃分的。也就是說,計算機記憶體的最小單位是1個位元組(8位)。
記憶體單元:一個記憶體單元佔據1個位元組。
地址值:每個記憶體單元使用一個值來標識,這個值就是地址值。地址值使用整數來表示,但它的型別不是整型。可見,一個地址值對應一個記憶體單元,可以使用一個地址值來指示一個記憶體單元。
地址值的大小:在32位機器上,要表示所有的記憶體地址,需要使用4個位元組(32位)的二進位制整數才能標識出所有的記憶體單元,因此標識一個記憶體單元的地址值應是4個位元組的二進位制整數(一般使用十六進位制形式書寫)。
(2)地址型別:地址值應該擁有一種資料型別,就像浮點數擁有浮點型一樣,本文把地址值擁有的型別稱為“地址型別”。因此,當記憶體中儲存的資料為地址值時,它是地址型別,就像記憶體中儲存的資料是浮點數,為浮點型一樣。在32位機器上地址型別佔據的記憶體大小一般為4個位元組。
(3)讀者可能認為地址型別的值是一個整數不好理解,其實這就好比一個整數值既可以是浮點型也可以是整型,程式能區分出一個整數值按什麼型別進行儲存。比如將一個整數值賦給浮點型變數,則這個整數值會被按照浮點型的格式儲存在記憶體中。
(4)地址型別變數(地址變數):擁有地址型別之後,就可以建立地址型別的變數,就好比整型是一種型別,整型變數是表示整型的變數,整型變數儲存的值是整數值。
(5)地址常量:十進位制的整數常量就是地址常量,就好比2.3是浮點數常量一樣。因此,一個整數值既是整數常量又是地址常量,但在C++中對地址常量的使用是有限制的。
2.指標型別、指標變數、指標值、指標常量的引入
在C++中沒有地址型別變數、地址型別、地址值、地址常量的說法,取而代之的是指標型別變數(指標變數)、指標型別、指標值、指標常量。也就是說,在C++中指標型別就是指地址型別,指標變數就是指地址變數,指標值就是指地址值,指標常量就是指地址常量。
3.指標變數如何儲存地址值
(1)指標型別的大小:在32位機器上指標型別的大小一般是4個位元組(當然也可以更大,依系統而不同)。
(2)指標變數用於儲存變數的地址值,而變數一般不止佔據一個記憶體單元的大小,但指標變數只能儲存一個記憶體單元的地址值,因此指標變數只能選擇儲存變數的首地址值或尾地址值(依機器而定),一般機器儲存的都是變數的首地址值,然後根據指標指向的變數型別,就能確定該變數應占據多少個記憶體單元。指標變數相當於一個指向某個物件的箭頭。
(3)地址值是一個整數值,這就意味著可以直接將一個整數值賦給指標變數,但C++禁止直接將整數值賦給指標變數(0值除外),因此在C++中這種操作不會成功。雖然現在禁止直接將整數值賦給指標變數,但是可以通過將整數值強制轉換為地址型別(指標型別)後再使用。比如(int *)100=1;,表示把值1儲存在記憶體中地址值為100的位置;int *p=(int *)100;,表示把記憶體中地址值為100的位置賦給指標p。
4.圖解物件、變數、型別、左值、記憶體、記憶體的劃分、指標的引入
圖解物件、變數、型別、左值、記憶體、記憶體的劃分、指標的引入如圖6.1所示。
說明:
(1)一個記憶體單元佔據1個位元組(8位),其中每一位都可以儲存一個二進位制數值,每個記憶體單元都使用一個地址值進行標識,在32位機器上,一般使用4個位元組的地址值來標識一個記憶體單元。比如圖6.1中的記憶體單元1使用地址值0x0012FF60進行標識,這樣更方便我們訪問記憶體。0x0012FF60是以十六進位制形式表示的整數值,是地址(或指標)型別。
(2)資料型別決定了分配多少個連續的記憶體單元,可以儲存什麼樣的資料,以及進行什麼樣的運算。說簡單一點,就是資料型別決定了怎樣解釋記憶體單元中的資料。比如int型別一般佔據4個位元組,則int就決定了應為資料分配4個位元組的記憶體單元,即要分配4個記憶體單元。
(3)C++使用“物件”這一概念來表示多個連續的記憶體單元(注意,並不僅僅指一個記憶體單元)。在圖6.1中,假設某資料為int型別,且為該資料分配的記憶體單元分別是記憶體單元1至4,則物件指的就是記憶體單元1至4總共4個記憶體單元。
(4)變數是命名的物件,左值則為指示一個物件的表示式。假設圖中的記憶體單元1至4為一個物件,併為該物件取一個名字為a,則a就是變數,表示的就是記憶體單元1到4這4個位元組的記憶體單元。左值是一個表示式,這個表示式應能表明指示的是一個物件,比如a=1;表示式,a指示了物件表示的記憶體單元1至4,因此a是左值。因為單獨的變數就是一個表示式,所以a就是一個左值。因此,變數名既可以代表該物件本身,同時也是左值。
(5)記憶體中儲存的資料依宣告時的型別而被解釋,若圖6.1中的記憶體單元1至4的資料為整型,則該資料被解釋為整數1094713344;若為浮點型,則該資料是浮點數12(按IEEE 754標準進行轉換),若此資料被解釋為一個記憶體地址值,則該資料為地址值0x41400000(十六進位制)。
(6)指標變數儲存的值應是地址型別的值(地址值)。假設有一個指標變數p,則儲存的值應是記憶體單元的地址值,比如圖6.1中的0x0012FF60、0x0012FF61等。假設int型別被分配到記憶體單元1至4,並將這個物件命名為a(即變數),且把變數a的地址賦給指標p,則指標p儲存的值就是變數a所代表的記憶體單元的首地址(依機器而定),即記憶體單元1的地址值0x0012FF60。
5.易混概念
以下概念雖然被混合使用,但從上下文中應能很容易區分開來。
(1)地址、地址值、地址型別:單獨說“地址”一詞時既可以表示地址值,也可以表示地址型別,這能從上下文區分開來,這就好比我們說3是一個整數,這個整數既可以代表整數值,也可以代表整數型別。
(2)由於習慣的原因,指標型別、指標變數值、指標值常被簡單地稱為指標,比如int *p;,把p說成是指向int的指標,這裡既可以強調p是一個指標型別的變數,也可以強調p的型別是指標;還可以說p是一個int指標,同樣既可以強調p是指標型別,也可以強調p是指標型別的變數。
(3)“地址”和“指標”兩個概念也常被混合使用,比如&a;既可以說成是返回a的地址,也可以說成是返回a的指標。
(4)指標的型別與指標指向的型別:經常把指標指向的型別說成是指標的型別,比如int *p;,表示指標p的型別為“指向int的指標”。本書會經常使用“指標型別”這個概念。
&與運算子
注:讀取記憶體地址時,本章假設讀取的是首地址,而不是尾地址。
1.理解&(取址)與(解引用)運算子
注意:一個記憶體單元佔據1個位元組的大小。
(1)對&(取址)運算子的簡單理解:就是讀取物件所示連續記憶體單元的地址(值),但只能讀取首地址,即第一個記憶體單元的地址值。比如&a,表示讀取運算元a的地址值。當然,&該運算子並不會把運算元所表示物件的所有記憶體單元地址值都讀取出來,一般只讀取第一個記憶體單元的地址(即首地址),然後根據運算元的型別就能知道共佔據多少個記憶體單元。若a是int型別,佔4個位元組記憶體單元,地址分別是0x0000 0002~0x0000 0005,則&a將只會讀取運算元a所在的連續記憶體單元的首地址0x000 0002,而不會全部讀出。
(2)&運算子的運算元應是表示一個物件的左值,該物件不能是位段,且宣告時無register(暫存器)儲存類區分符(暫存器是沒有地址可取的)。因為左值是指示一個物件的表示式,也就是說,左值本身就是表示的物件,所以說&運算子的運算元應是一個左值。比如&3是錯誤的,因為整數3是右值;再比如register int a=1;,則&a;是錯誤的,因為運算元是register變數。
(3)注意:register關鍵字只是C++標準對編譯器的一個建議,編譯器不一定會執行該操作,因此在某些編譯器上對register變數使用&運算子不一定會出錯。為了使相容性最好,不要對register變數使用&運算子。
(4)ANSI C中對&運算子描述的原話:“&(地址)運算子的結果是指向由運算元所指示的物件或函式的指標,其結果型別是指向type的指標”。理解標準:雖然&運算子的結果是指標,但並沒有為該指標分配記憶體地址,因此&運算子的結果不能作為左值,只能作為右值。因為指標指向的是運算元所指示的物件或函式的指標,所以這個右值就是運算元所指示物件或函式的地址,在這裡也可將&運算子的結果作為指標常量(即地址值)來理解。比如int a=1;,則&a的結果是指向運算元a所指示物件的指標,這個指標並沒有分配記憶體地址,只能作為右值使用,這時該指標的值就是運算元a所代表的一片連續的記憶體單元(4個位元組)的首地址或尾地址,該指標的型別是“指向int的指標”。此處混用了指標和地址的概念。
(5)(解引用)運算子,其運算元應是指標型別,若運算元指向一個物件(即運算元是指標),則結果是一個指示該物件的左值。若運算元的型別是指向某型別的指標,則結果型別就是該型別。運算子的運算元應是右值,但其型別必須是指標型別。
理解1:運算子的結果是運算元指向的物件的左值或指標所指物件的左值。意思是說使用
為什麼強調左值?主要是為了說明運算子的結果不是右值,因此可以使用運算子間接改變指標所指向的物件的值。若運算子的結果是右值,則不能間接改變指標所指向的物件的值。比如int a=1; int p=&a;,則p=2;會間接改變a的值。
理解2: 運算子能得到運算元所指物件處儲存的值。比如int b=1; int p=&b;,則p在需要將p作為右值的地方(會經過從左值到右值的轉換),確實得到了p所指物件處儲存的值1。但運算子的結果是左值,還可以對p進行賦值,而所指地址處儲存的值一般都是右值,不能對右值進行賦值操作。當然,左值可以作為右值使用。
(6)示例:
比如int a=1;,則*&a;的運算元&a是一個指向物件a的指標。也就是說,運算元指向物件a,因此*&a的結果就是運算元&a指向的物件a的左值,即*&a是名稱為a的物件的左值。而a本身也是名稱為a的物件的左值。也就是說,&a和a都是指名稱為a的物件的記憶體單元,因此&a與a等價,即*&a的值是1,同時*&a是左值,可以對其賦值,比如*&a=3;會間接改變a的值。因為&a是指向int的指標,所以*&a的結果型別是int。
再比如int b=1; int p=&b;,則p的結果是運算元p指向的物件b的左值,即p是名稱為b的物件的左值,而b本身就是物件b的左值。也就是說,p與b表示的都是名稱為b的物件的記憶體單元,因此p與b是等價的,即p的值是1,同時p的結果是左值,可以對其賦值,比如p=3;會間接改變b的值。因為p是指向int的指標,所以p的結果型別是int。
(7)注意:(解引用)運算子與*(乘法)運算子是相同的,&(取址)運算子與&(按位與)運算子是相同的,這些運算子都能根據程式的上下文區分開來。
2.&運算子總結要點
(1)&運算子能讀取物件所示連續記憶體單元的地址(值),一般為首地址或尾地址。
(2)&運算子的運算元是左值。
(3)&運算子的結果是右值,不能作為左值使用。
(4)&運算子的結果型別是“指向某型別的指標”。
3.運算子總結要點
(1)使用運算子之後,可以獲取運算元(或指標)所指向的物件。比如int a=1; int p=&a;,則p就是表示運算元p所指向的物件a。
(2)運算子的運算元應是右值,且是指標型別。
(3)運算子的結果是左值,因此可以對使用運算子之後的結果進行賦值。
(4)運算子的結果型別就是運算元指向的物件的型別。
(5)運算子會間接改變運算元所指向的物件的值。
4.圖解指標的概念、地址、&與運算子
圖解指標的概念、地址、&與運算子如圖6.2所示。
說明:
(1)假設有這樣的宣告:int a=128; int p=&a;(這是宣告指標p的語法形式)。
(2)在宣告語句int p=&a;中,&a表示讀取變數a所代表的一片連續的記憶體單元的首地址(即記憶體單元1的地址)。然後將該首地址用於初始化指標變數p,這時p的值應是變數a所指示的記憶體單元1的地址值,也就是0x0012FF60。因此,指標p相當於一個箭頭,這個箭頭指向了變數a所表示的一片記憶體單元的首個記憶體單元處(即記憶體單元1)。
(3)從圖6.2中可見,指標變數p的值就是變數a的首地址值, p的值雖然是一個整數,但其型別是地址型別(指標型別),而不是整數型別。
(4)若在以後的表示式或語句中使用形如p的形式,比如p=3;,則是解引用運算子,而不是在宣告時表示所宣告的物件是一個指標的宣告符,這時運算子的結果是指標p所指物件a的左值,因此p和a是等價的,即*p和a都表示名稱為a的物件所代表的記憶體單元1至4。所以,*p=3就相當於對a進行賦值,*p間接改變了變數a儲存在記憶體單元中的值;cout<<*p;相當於輸出a的值。
以上內容摘自本人所作《C++語法詳解》一書,電子工業出版社出版。