真的很可怕的C語言ch12--指標與陣列讀書筆記 - PL-learning Blog
文章推薦指數: 80 %
唯一要記住的是指標就是「指標」,不是陣列也不是運算元。
... 還有,byte order是在多byte的data type才有差別,所以和字串的陣列沒有關係(因為char ...
RSS
Blog
Archives
AboutMe
在本章,終於要說明C語言的最難關--pointer(指標)了。
如果能理解指標的話,就能使用OS的機能跟標準的library,C語言能作到的事也幾乎能做到了。
然後,因為一定要意識到記憶體,所以對電腦的理解也會加深。
雖然指標常常被說很難,但是其實沒這麼難,要說為什麼很多人感覺很難,是因為程式寫法很囉唆,而且也需要理解電腦的基礎。
程式寫法習慣就好了,這裡會盡力的把電腦的基礎講的好懂一些,讀到現在的讀者一定可以理解的。
唯一要記住的是指標就是「指標」,不是陣列也不是運算元。
C語言為了讓指標的寫法簡單一些,而利用了syntaxsugar(語法糖),造成了如果不深入注意的話會覺得看起來一樣,但是這兩個東西是完全兩回事。
還有,C語言同一個記號可能意義不只一種(不記得的話,複習一下第八章)。
指標常常用到*跟&記號,但是這絕不是乘法跟and!
有了上面這些念頭,一定不會混亂的吧。
12.1何謂陣列
關於陣列在第七章做過一些說明,這裡再重新說一次吧。
陣列是同一個datatype的變數併排在一起的東西
陣列是同一個datatype的變數在連續的記憶體上併排在一起的東西。
普通變數的宣告是:
data_type變數名;
比如:
intnumber;
記憶體示意圖
32bit的CPU電腦,一般的int是4bytes(32bit),記憶體會長的像下圖這樣,這個圖縱軸是記憶體address,橫軸是該address記憶體的bit。
上圖0xFFFFFFFC這一塊記憶體會是32bit的CPU的位址中最高的,而0x00000000這一塊通常是不給程式使用的。
假設上面宣告的變數number在address0x00001040這塊記憶體上,而且number=1000;(2進位是1111101000),那麼記憶體會變成下圖:
address0x00001044跟address0x00001036可能會被其他程式用到。
陣列的宣告
用法如下:
data_type變數名[元素數量];
範例:
intnumber_array[8];
記憶體示意圖
32bit的CPU電腦,一般的int是4bytes(32bit),記憶體會長的像下圖這樣。
圖中number_array這個陣列佔有了0x00001040~0x0000105C這組address。
這裡也是每4bytes跳一個。
灰色部份由下到上八個長條,分別是number_array[0]到number_array[7]可使用的記憶體(number_array[0]就是address0x00001040的那一整條,以此類推)。
address0x00001060跟address0x00001036可能會被其他程式用到。
不過這樣實在是太難看了,所以我們在用圖解釋陣列的時候,都是把一個address當成一格,由左往右排,如下圖:
0就是代表number_array[0],以此類推。
0~8這些數字被稱為「index」,可以拿來指定要存取陣列的哪個元素。
也可以在初始化陣列時放值(這些值被稱為初期值)進去,方法如下:
data_type變數名[元素數量]={變數[0]的值,變數[1]的值...變數[元素數量-1]的值};
範例:
intnumber_array[8]={-3,-2,-1,0,1,2,3,4};
在初始化時,如果初期值寫的比元素數目還少,剩下沒寫的都是0。
舉例:
intnumber_array[8]={-1};
那麼這個陣列的初始化結果會如下圖所示:
還有,寫了初期值可以省略元素數量。
如下例:
intnumber_array[]={0,1,2,3};
結果如下所示:
到目前為止說明了陣列的概念,來搭配範例程式加深理解吧。
這個程式內容跟第9章的求1加到10的和是相同的。
sourcecode
for_array.c
#include
陣列number_array[]的最後放入了-1,這個值被稱為sentinelvalue(又被稱為flagvalue,tripvalue,roguevalue,signalvalue,dummydata等等,日文漢字是"番兵"),如果loop(重複)時遇見了這個值,那麼就會停止loop。
sentinelvalue也被用在像是linkedlist的最尾端,用NULL表示。
在這個例子因為沒使用負數,所以sentinelvalue設成-1,不過陣列中有其他負數的話就不能設成這個了,通常是把中止條件換成重複的回數。
如果是把重複的回數當成中止條件的話,可以不用直接指定數字,而是利用sizeof運算元。
因為int陣列一個元素是4bytes(一般來說),所以i
雖然不可思議,但是這個實驗可以感覺到「什麼都是記憶體的操作」吧。
12.2何謂指標
所謂指標,直譯的話就是「指示」。
那麼,C語言指示什麼呢?答案是「變數或函數所在的場所」。
也就是說,考慮成記憶體的位址(address)就可以了。
求出變數所在的場所
首先,從求出變數的位址開始好了。
想取得位址的話要用到&運算元,這個運算元和同樣使用&的運算元沒有關係,沒有什麼「因為是address所以用and阿...」(筆者注:這裡是指在第八章原書作者為了解釋&的用法而舉的例子)這種想法,請不要誤解了。
用法如下:
data_type變數名;
&變數名//取得此變數位址
只是拿到位址的話一點用都沒有,來用printf()來表示吧。
printf()有為了表示指標的值的變換指定子%p,趕快來實驗看看吧。
sourcecode
addr.c
#include
可以發現,即使代入了值,位址也不會改變。
一開始屋子的名字(變數名)跟位址就已經決定了,把值代入變數相當於把一個值放進去屋子而已,位址當然不會變。
陣列也是變數的一種,也可以取得位址並表示之。
用法:
data_type變數名[元素數量];
&變數名[index]//取得位址
這裡也用範例程式確認一下吧。
sourcecode
array_addr.c
#include
到這裡大家應該都可以理解,不過,陣列是有特殊對待的,也有不用&就可以取得位址的方法。
比如說&char_array[0],跟寫成char_array是一樣的意思。
來實驗看看吧。
sourcecode
char_array.c
#include
因為是char陣列,所以也可以看到index每多1個位址就加1,這裡的再度實驗,當作複習。
第七章的話,說過為了處理字串,char陣列的最後會放入\0,所以如果要用printf()來指定陣列的話,就只要寫one_string就可以。
printf("%s",one_string);
因為對陣列來說,寫one_string跟&one_string[0]都等價於one_string[]這陣列的第一個元素的位址,所以可以說處理字串的函數(如上例的printf())所接受的是陣列第一個元素的位址。
如下圖所示:
charone_string[]="hello,world\n";
握們把這個位址,稱作「陣列開頭位址」。
再做個實驗看看吧。
sourcecode
char_array_2.c
#include
因為one_string跟&one_string[0]都是同一個位址,所以顯示相同。
而&one_string[1]則是從e才開始顯示字串,所以變成「ello,world\n」。
所以接下來的&one_string[2]會是「llo,world\n」。
簡單來說,C語言中藥成為字串的條件就只是最後有\0而已,而函數根據我們給的開頭位址輸出也會改變。
只是說每次都寫&one_string[0]很麻煩,寫one_string可以跟&one_string[0]一樣,有這想法就可以了。
就算是字串以外也一樣,int還有其他datatype只要寫下「變數名」,就跟「&變數名[0]」都是指定相同的位址。
雖然很囉唆,請好好的記起來。
指標變數
現在已經知道了如果只是要取得變數的位址,用&就可以了。
還有,不管接受什麼datatype的陣列的函數,都跟字串一樣:不是接受整個字串或陣列,只是接收了陣列的開頭位址。
整理一下,就像下面這樣:
把&加在已被宣告的變數名前面,代表那個變數的所在地(address)。
Example:
inta=1000;
寫a的話,代表著變數a所存著的int值1000
寫&a的話,代表著變數a在記憶體上的位址(address)
宣告成陣列的變數名,如果直接寫變數名的話,代表那個陣列的開頭(第一個)位址(address)
Example:
inta[4]={1000,2000,3000,4000};
寫a[0]的話,代表著陣列a第一個元素所存著的int值1000
寫a的話,代表著陣列a的第一個元素在記憶體上的位址(address)
宣告成陣列的變數名,如果加上index還有&的話,代表那個陣列的index的位址(address)
Example:
inta[4]={1000,2000,3000,4000};
寫&a[0]的話,代表著陣列a的第一個元素在記憶體上的位址(address)
像接受字串的函數一樣接受陣列的函數,如果給它已宣告的陣列的名字的話,就相當於指定了那個陣列的開頭位址
Example:
chara[]="test";
printf("%s",a);
a代表陣列a的開頭位址(就是存放t的地方)
就像本章開頭說明的,指標是「指示」。
目前為止,單單知道變數是在哪裡,用途也只知道把字串交給函數而已。
那麼,這跟「指示」有怎樣的關聯呢?
為了回答這問題,先要理解所謂的「指標變數」。
只是取得了變數所在地(位址),是什麼都做不到的。
指標應該是為了在程式上做什麼東西,才會把地址本身放入變數。
看了addr.c這個範例程式就能明白,變數的位址是0x7fffe8127ea0,0x7fffe8127e9c之類的,怎麼看都是整數。
話說回來,記憶體上的位址從0開始,32bit電腦的話到0xffffffff(32bit能表示的最大的數,換算成10進位是4294967295)都是記憶體位址的範圍。
64bit電腦的話到0xffffffffffffffff(64bit能表示的最大的數)都是記憶體位址的範圍。
4294967295以KB,MB,GB來表示的話,是4G。
幾乎所有的32bit系統的最大ram容量都是4G,原因就在這裡。
那麼,可能有人會覺得要記憶變數的位址的話,只要用int就可以了,但是不行;有專用的指標型的變數,就用那個。
說是指標型的變數,跟普通變數的宣告也差不多。
用法如下:
data_type*變數名;
就像上面這樣,只是在變數名的前面加上了*。
趕快來改造一下addr.c範例程式,把取得的變數位址存起來吧。
雖然想讓讀者自己考慮範例程式的改造,但是因為指標是難關,所以把全sourcecode都寫上去了。
sourcecode
addr_2.c
#include
這可以確認變數pnumber_1的確可以紀錄位址。
而變數pnumber_1,pnumber_2,pcharacter等等就是指標變數,就是單純的擺放number_1,number_2,character的位址而已,要如何活用呢?
比如說,變數number_2放了數字的「1」。
如果想作加法,那麼寫number_2+1就可以了。
變數pnumber_2,因為是指標變數,所以擺的是位址「0x7ffe54470b24」。
因為「0x7ffe54470b24」指示的位址是變數number_2,在這位址中放了一個數字「1」。
那麼,想把變數number_2加上什麼數時,利用變數pnumber_2,寫成pnumber_2+1會如何呢?
這樣的話,不會是期待的結果,因為pnumber_2+1其實是0x7ffda246da64+1;本來變數pnumber_2這個地方所存的「值」就是0x7ffda246da64,不是1。
因為是64bit的電腦來執行程式,執行結果位數太多了實在很囉唆,我們就以32bit的記憶體來畫示意圖吧(就是只保留上面寫的位址的後8位)。
記憶體的示意圖的條件有下列:
變數number_2的值是1
變數number_2的addrsss是0x54470b24
指標變數pnumber_2的值是變數number_2的addrsss,0x54470b24
指標變數pnumber_2的addrsss是0x54470b30
所以這個程式的記憶體示意圖如下:
0x54470b30是變數pnumber_2的記憶體位址,其中存著0x54470b24(由左往右,5是最左邊的0101,以此類推)。
0x54470b24變數number_2的記憶體位址,其中存著1。
這裡的圖只列出number_2跟pnumber_2的值,其他像是0x54470b28是number_1的地址,pnumber_1的內存值,上面的圖都沒寫。
因為還是很囉唆,再更簡化一下好了。
變數number_2的值是1
變數number_2的addrsss是2
指標變數pnumber_2的值是變數number_2的addrsss:2。
那麼上面的圖可以變成下面這樣:
看圖可以知道,變數pnumber_2的值是位址,並不是用通常的int進行運算。
但是,如果加上一個記號的話,就可以利用pnumber_2指示的位址所放的值,那個記號就是*。
如下圖:
如果寫**pnumber_2+1的話,就可以用放在pnumber_2*的address,2,利用放在這個位址(2)的值來做處理。
可能很囉唆,這裡就執行範例程式弄清楚吧。
sourcecode
addr_3.c
#include
如果目前為止都能理解的話,之後的就不是這麼難了。
如果還是不能理解的話,就把範例程式的值變一變來實驗看看吧。
像這樣使用指標,利用指標所指的值,我們稱作「間接參照」,所以剛剛用的記號又叫「間接參照運算元」。
會這麼稱呼是因為,原本我們可以直接用number_2就好,但是我們卻用了pnumber_2*這個指標變數來「間接的」參照。
指標與陣列
要學間接參照,首先先搞懂什麼是指標吧。
其實,如本章標題「指標與陣列」所示,指標與陣列有很深的關係,可以使用指標的話也可以很方便的使用陣列。
陣列和普通變數不同,變數名稱後會加[]。
但是,要儲存陣列位址的指標變數,並不會因為是陣列而需要做什麼特別的事。
趕快來把陣列位址存到指標變數吧。
sourcecode
array_addr_2.c
#include
要說為什麼指標變數用在普通變數跟用在陣列沒什麼分別的原因,是因為在記憶體上普通變數跟陣列的一個元素利用的記憶體大小都是一樣的。
理解了指標與陣列後,來介紹陣列便利的使用方法吧。
我們把陣列那一節介紹的for_array.c這個範例程式,用指標重寫一遍。
sourcecode
white_ptr.c
#include
這個程式應該要注意的是下面的地方
1.
while(*pnumber!=-1){
指標變數pnumber裡面的值是number_array[]的開頭位址。
因為這裡有間接參照運算元(*),所以一開始可以得到*pnumber可以得到「1」這個值;因為不是-1,所以繼續處理while裡面的指令。
2.
answer+=*pnumber;
這裡的pnumber一樣是1,所以把1加到answer*上。
3.
pnumber++;
這句是什麼意思呢?從字面上來看,就是把指標變數pnumber給++。
如果是普通的整數型變數的話++就是+1了。
但是,指標變數pnumber給++的話,並不是單純的+1。
因為pnumber是int的指標變數,所以pnumber所指的地方也是有int的大小(32bit電腦通常是4bytes),也就是說,如果把位址只加上1的話,會指向奇怪的地方。
這也是為何位址不能用普通的int來儲存的原因。
由於這個pnumber++,在下次的重複處理時,**pnumber*就會指向「2」了。
因為使用指標可以把處理寫的很簡潔,所以C語言很常用。
再舉一個例子,來考慮把字串代入別的陣列。
charstring_1[16]="hello,world";
charstring_2[16];
如果是普通變數的話,只要寫string_2=string_1就可以了,但是在陣列的情況下,這種寫法會讓compiler噴出「error:incompatibletypesinassignment」這種錯誤。
因為string_1代表的是string_1的開頭位址,想代入陣列的話就一定要指定代入哪一個元素,像string_2這裡也要寫成string_2[0]等等。
在這種情況下,同常就會像下面這樣做重複的處理。
charstring_1[16]="hello,world";
charstring_2[16];
inti;
for(i=0;i
sourcecode
string_cpy.c
#include
來用指標重寫一遍吧。
sourcecode
string_cpy_2.c
#include
(這個指標版本因為不檢查error,所以被copy的領域(string_2)如果比string_1小會出現異常的執行結果)。
這種程度的程式感受不到使用指標的好處,不過如果處理的字串複雜一點,使用不需要記index的指標是比較便利的。
還有,如果陣列變大的話,用指標比較好處理。
現階段範例程式的元素數量也就是4或16這種很小的,實用的程式的元素數量一般會指定到像是1024或8192之類的。
比如說把char陣列的元素數量指定到8192的話,光是字母就有8192個,而要處理一行長的文字資料就需要確保很大的陣列,圖像的話就更大了(如果是更大的資料的話,會無法存在stack上,需要別的方法來確保記憶體)。
每當呼叫出一次函數,就要去copy這麼大的記憶體領域,速度會很慢。
作為代替只交給陣列的開頭位址的話,就可以達成用最少情報量處理大量的資料,所以指標的概念很重要。
好好的精通使用方法吧。
恐怖實驗:注意交給sizeof運算元的東西(筆者注:其實這實驗是在解釋陣列跟指標的不同)
我們剛剛毫不在意了用了i
這個程式是先+1再用printf()顯示的,我們來試試直接在printf()中才+1。
sourcecode
array_addr_4.c
#include
也就是說,想把指標當陣列使用的話,就是用**(ptr+n)*。
這樣的話,即使是接收指標為參數的函數,也可以像陣列一樣使用,比較安心了。
但是,請想起一件事。
作為陣列宣告的變數double_array[],在程式中寫成double_array時,是表示double_array的開頭位址。
也就是說,只寫double_array其實是位址記號。
而作為指標宣告的變數ptr,原本就是儲存位址,所以也是一種表示位址的記號。
也就是說,寫double_array跟寫ptr,表示的東西是一樣的。
這樣的話,感覺也可以寫成ptr[0]或ptr[1]吧。
對,這感覺是正確的,可以試試把剛剛的範例程式**(ptr+0)換成ptr[0]、(ptr+1)換成ptr[1]試試。
反過來說,即使宣告成陣列,也可以像指標一樣用間接運算元。
可以試試,把剛剛的範例程式double_array[0]換成**(double_array+0),double_array[1]換成(double_array+1)。
其實,[]這個記號,不過是計算位址並進行間接參照的東西而已。
**(double_array+1)跟double_array[1]相同的話,即使把index跟變數名對調也是可以執行的,因為數字上來看,double_array+1=1+double_array的。
所以,double_array[1]是可以寫成1[double_array]*的。
原本應該寫成加法計算位址,卻為了好懂可以寫成double_array[1],而[]就叫做「語法糖(syntaxsugar)」。
但是指標跟陣列還是不同的東西
到目前為止應該理解了指標跟陣列之間密切的關係,但指標跟陣列是完全不一樣的概念。
如果要立刻了解到兩者是不同的東西,可以執行看看下面的程式碼:
doubledouble_array[2]={0.1,0.2};
double*ptr;
ptr=double_array;
double_array=ptr;
第三行是沒問題的,但第四行不行,會造成compileerror。
原因的話,是因為double_array[]不是為了代入位址的變數。
來看看示意圖吧。
在程式中,不管是寫double_array或是ptr,值都是「位址2」。
所以,不管是寫double_array[0]或是ptr[0],所參照的值都是放在位址2的數字,0.1。
ptr=double_array這個式子,意思是把double_array[]的開頭位址,作為值代入ptr裡。
ptr本身則是一個位於位址1的指標變數。
也就是說,我們是把「位址2」代入(1)裡面。
但是,就算執行double_array=ptr這個式子,double_array本身在宣告時已經是在位址2了,所以這個變更無法實現。
所以double_array=ptr不能用。
因為用同樣的寫法都可以存取到一樣的值,所以不小心就會搞混,請認識到指標跟陣列是不一樣的東西。
12.3指標與函數
理解了指標後,來介紹函數中對指標變數的處理。
不過,給printf()函數字串這件事,本身就是把char陣列的開頭位址交給printf()處理。
所以,記住函數接受指標的方法的話,也不是很難。
指標變數就像這樣宣告:
data_type*變數名;
在宣告函數要用到指標變數時,也是像這樣宣告。
趕快執行個範例程式看看吧。
sourcecode
func_4.c
#include
再來試試把main()函數改成下面這樣,看看用陣列是不是也是一樣的結果。
intmain(){
intnumber_array[2];
intanswer;
number_array[0]=1;
*(number_array+1)=2;
answer=sum(number_array,&number_array[1]);
printf("answer=%d\n",answer);
return0;
}
很簡單吧。
這次,寫一個返回指標的函數吧(main是剛剛改過)。
首先,把prototype宣告改成
int*sum(int*,int*);
把函數的body也寫成
int*sum(int*a,int*b);
在main()函數中,也做一些改變:
intanswer換成
int*answer;
answer=sum(number_array,&number_array[1]);換成
answer=*sum(number_array,&number_array[1]);
printf("answer=%d\n",answer);換成
printf("answer=%d\n",*answer);
總之,程式應該改成這樣:
#include
在原書作者的範例,如果在
answer=sum(number_array,&number_array[1]);
printf("answer=%d\n",*answer);
之間插入一句
printf("Theansweris...\n");
的話,answer會顯示32767。
現在來討論為何原書作者會出現這個結果吧(筆者依然是coredumped)。
這個現象在第七章的auto-var.c也出現過。
因為sum()函數中的變數return_value不是靜態變數,所以在sum()函數執行結束時這個變數的記憶體已經被其他呼叫的函數給覆寫了。
以這個例子來說,就是被printf("Theansweris...\n");給破壞,而被破壞而無效的位址被指標變數answer給間接參照,所以會出現奇怪的值。
對策的話,可以把return_value宣告成靜態變數,或是把想儲存結果的變數的位址給交出去也可以。
因為之前已經說明過宣告成靜態變數的方法,現在就來介紹把想儲存結果的變數的位址給交出去的方法。
sourcecode
func_5.c
#include
"想儲存結果的變數的位址"就是&ans,我們把&ans交給了main中的指標變數,answer。
這個範例程式的sum()函數,把想儲存結果的變數ans的位址當作參數,把計算結果代入那個位址。
所以不會出現先前那樣的現象。
函數雖然返回的值只能有一個,但是使用上面這種寫法的話,不管一個函數想返回多少的值都沒問題。
因為是常用的方法,請記起來。
恐怖實驗:把NULL給指標
使用"把想儲存結果的變數的位址給交出去"這個方法的話,在一些情況,比如說要同時求出座標的x,y,z的時候是非常方便的。
雖然會因為如此而忍不住多用,但是要注意它有很可怕的陷阱--NULL。
NULL是為了讓一個指標變數表示「沒指向任何東西」的特別的值。
可以判斷指標變數有沒有放值或是處理有沒有問題,就像下面用法:
char*str=NULL;
//處理
if(str==NULL){
//處理失敗
}else{
printf("resultis%s\n",str);
}
像這樣,用NULL把指標變數初始化可說是一定的。
讓我們來執行一下範例程式吧;把剛剛的程式(func_5.c)中的answer=&ans;換成answer=NULL;來實行看看。
執行結果:
Theansweris...
程式記憶體區段錯誤(coredumped)
(筆者注:以下內容原書寫的讓人看不懂,怎麼突然說是ans是NULL呢?建議直接寫ans=NULL,answer=&ans不要改吧)
因為我們把NULL代入ans所以無法代入執行結果,所以會異常中止。
像這樣參照NULL指標是常發生的錯誤,而且會造成異常中止。
如果想防範的話,就在sum()進行計算(***ans=*a+b;*)前,加入
if(ans==NULL){
return(-1);
}
caller(也就是main)會在sum()的返回值是-1的時候執行printf("error\n");,所以可以放心。
順代一提,C語言的標準library幾乎都沒有這種錯誤檢查。
像是複製字串的函數strcpy()如果把NULL當參數一定會異常中止,在Solaris的printf()也是一樣。
不過在linux跟MacOSX的printf(),用%s遇到NULL時,只會顯示(null)不會異常中止。
這是需要注意的。
大部分的系統,都把NULL當作0。
所以把0代入也是跟上述一樣的結果。
恐怖實驗:不管什麼值都能用指標參照
除了NULL指標參照以外,再介紹其他指標可怕的地方吧。
就像我們拿指標去參照NULL一樣,系統完全不管指標變數放入了什麼值。
也就是說,不管指標變數的內容是什麼,都是可以間接參照的。
如果放入的值是NULL的話可能就是異常中止,或是參照到奇怪的記憶體位址的話就會讀出奇怪的值,總之寫程式的人要付全責。
而且是NULL的話因為會異常中止所以還可以知道有bug,但是參照到奇怪的值程式依然會繼續下去,在實際應用上可能會發生意想不到的事態。
來執行一下範例程式來實證一下吧。
sourcecode
ptr_bug.c
#include
但是我們寫了
pnum_1+=2;//pointerbug
pnum_2+=2;
所以參照的地方就改變了,計算結果也跟著改變。
因為變數num_1,num_2並不是陣列,所以把位址作加法就會超出變數的記憶體範圍外。
但是對compiler來說不過是把指標變數pnum_1,pnum_2給加2而已,合法的句子所以不會有警告。
結果就是,剛好指到了範圍外的num_1跟num_2,所以可以看到普通的結果。
如果是很單純的程式的話,這種bug可以很快發現,但是程式變得複雜的話就很難注意到了,直到成品出來後才被客人說:「好像有點怪怪的...」才第一次注意到。
指標的運算真的要很慎重。
所指向的地方,真的是我們想要的位址嗎...?
12.4指標與cast(強制轉型)
C語言準備了一種功能,可以讓一個變數代入另一個不同datatype的變數,叫做「轉型(型態轉換)」。
先詳細說明轉型吧,int一般在32bit電腦上是4bytes的大小,而char則是1byte的大小,如果像下面這樣寫會發生什麼事呢?
intnum_1=100;
charnum_2=num_1;
只要自己去實驗就知道,num_2是可以好好的儲存100的,這是因為num_1變成左邊的datatype。
但是,像下面這樣寫又會發生什麼呢?
intnum_1=1000;
charnum_2=num_1;
自己實驗的話,很可能會發現num_2變成-24這奇怪的值。
C語言的轉型,對於雙方都是需用正負號表示的整數型(有號整數),是根據以下兩條規則處理:
原來型態<=後來型態:值不變
原來型態>後來型態:可以表示的話不變,不可以的話看compiler怎麼做
這次的例子符合第二條,所以變成奇怪的值。
因為1000用2進位表示是1111101000,至少需要10bit才能表示,但是char只有1byte(8bit),所以放不下。
放不下的部份,也就是最左邊兩位被扔掉,變成11101000。
由於最左邊是1,所以char會把它當負數處理,其結果就是-24(這裡不詳述是怎麼算出-24的)。
轉型還有其他規則,如果雙方都是無號正整數,規則如下:
原來型態<=後來型態:值不變
原來型態>後來型態:原來型態%(後來型態可以表示的最大值+1)
舉例:
unsignedintnum_1=1000;
unsignedcharnum_2=num_1;
num_2的值會是1000%(255+1)=232。
如果原來型態是有號整數,後來型態是無號正整數,那麼規則如下:
原來型態>=0
原來型態<=後來型態:值不變
原來型態>後來型態:原來型態%(後來型態可以表示的最大值+1)
原來型態是負數
原來型態<=後來型態:原來型態%(後來型態可以表示的最大值+1)
原來型態>後來型態:(後來型態可以表示的最大值+1)-(-原來型態%(後來型態可以表示的最大值+1))
舉例:
charnum_1=-128;
unsignedintnum_2=num_1;
num_2=-128+(4294967295+1)=4294967168
intnum_1=-1000;
unsignedcharnum_2=num_1;
num_2=(255+1)-(-(-1000)%(255+1))=24
如果原來型態是無號正整數,後來型態是有號整數,那麼規則如下:
原來型態<=後來型態:值不變
原來型態>後來型態:可以表示的話不變,不可以的話看compiler怎麼做
如果原來型態是浮點數(double,float),後來型態是有號整數,那麼規則如下:
如果原來型態的整數部份可以讓有號整數表示的話,就只丟掉小數點,如果不能表示,則看compiler怎麼做
舉例:
doublenum_1=3.14;
charnum_2=num_1;
num_2=3
如果原來型態是無號整數,後來型態是浮點數(double,float),那麼規則如下:
看compiler怎麼做,會變成近似值
舉例:
unsignedlonglongnum_1=1844674407370955161511u;
floatnum_2=num_1;
num_2=18446744073709551616.00000(MacOSX64bit)
unsignedlonglongnum_1=1844674407370955161511u;
longdoublenum_2=num_1;
num_2=18446744073709551615.00000(MacOSX64bit)
都是浮點數
原來型態<=後來型態:值不變
原來型態>後來型態:可以表示的話不變,不可以的話看compiler怎麼做
以上都是討論在代入時如何轉型,但就算是式子的右邊也可能是不同性太的值再做運算。
這時候,就是以右邊中最大的datatype為準。
比如說double+int的話,就是把int轉成double再做運算。
轉型這東西,與其把上面這些囉唆的規則通通記起來,不如注意「從大的datatype代入到小的datatype要注意」。
如果一直跟自己的預想不同才來查這個表。
12.5明示的轉型
目前為止所說的都是偷偷轉型,不過也可以明確的指定要轉成什麼型,這種明示的轉型稱為cast。
cast很簡單,只要在變數左側()裡寫入想轉的型態就可以。
用法如下:
(想轉的data_type)變數名;
這裡來實驗一下吧。
sourcecode
cast.c
#include
接下來轉型成short,因為short足夠表示1000所以就直接表示1000。
第三個,在代入short前有(char),所以先轉成char,變成了-24,再轉成short,依然是-24。
這個cast,如果是合理的變換的話是什麼都可以轉換的。
先不管結果正不正確,指標變數的內部也是「數值」,也可以變換成整數。
這裡也來實驗看看吧,addr.c範例程式的指標的值用printf()來表示的話需要用%p這個變換指定,所以我們做如下變更來執行看看。
printf("addressofnumber_1is%p\n",&number_1);
printf("addressofnumber_2is%p\n",&number_2);
printf("addressofcharacteris%p\n",&character);
改成
printf("addressofnumber_1is%llx\n",
(unsignedlonglong)&number_1);
printf("addressofnumber_2is%llx\n",
(unsignedlonglong)&number_2);
printf("addressofcharacteris%llx\n",
(unsignedlonglong)&character);
執行結果:
addressofnumber_1is0x7ffef53b0070
addressofnumber_2is0x7ffef53b006c
addressofcharacteris0x7ffef53b006b
number_1is0.100000
number_2is1
characterisa
addressofnumber_1is7ffef53b0070
addressofnumber_2is7ffef53b006c
addressofcharacteris7ffef53b006b
除了前面沒有0x之外,應該是一樣的數字。
既然指標變數可以轉換成普通的整數,當然也可以轉成別的datatype的指標變數。
(data_type*)變數名;
之前說明過,指標變數在運算後會自動決定要前進多少位址。
比如說char*每加1就前進1byte,int*則是4bytes。
如果把一個指標變數換成別的datatype的指標變數,則會改變前進多少。
比如說可以讓int像char一樣1byte的讀取。
因為是有趣的實驗,趕快來實驗範例程式看看吧。
sourcecode
ptr_cast.c
#include
同理,longlong跟double在記憶體裡儲存的值也透過指標顯示出來。
對於2進位跟16進位熟悉的人,是不是有「阿咧?」的感覺呢?2147483647在16進位應該是0x7FFFFFFF的,但顯示出來卻是0xFFFFFF7F,以十進位insignedint來說變成了4294967167這個超越int表示範圍的數。
這是怎麼回事呢?
這是因為筆者(原書作者也是)的電腦是intel系CPU,它的「byteorder」是屬於「littleendian」。
所謂byteorder,就是一個需要多個byte來表示的資料,在記憶體上的byte的排列順序(order)。
比如說某個資料是0x120x340x560x78好了,如果在記憶體中以0x780x560x340x12來排的話就是littleendian,反之就這樣照著0x120x340x560x78來排的話就是bigendian。
intel系CPU是屬於littleendian,而SPARC跟PowerPC是屬於bigendian。
乍想之下,bigendian這個直接照順序排的不是比較好懂嗎?但基本上多byte的計算都是從最低位開始計算,跟人類在筆算的時候,也是從最低位開始計算是一樣的道理。
所以,把最低位放在記憶體最前端的littleendian,可以說是有道理的。
但是,世上有許多的電腦都是bigendian,還有在網路上傳輸的資料也幾乎是用bigendian排列。
所以為了可以轉換,大部分的系統都準備了可以換byteorder的函數。
以本書所用的gcc來說,ntohl()函數跟htonl()函數是可以作到這個轉換的,有興趣的人可以調查一下。
還有,byteorder是在多byte的datatype才有差別,所以和字串的陣列沒有關係(因為char陣列一個元素也就1byte)。
比如說宣告了一個charone_string[]="hello,world\n"好了,那麼記憶體裡也是不會儲存成「\0\ndlrow,olleh」的,而且這樣寫也存不進去。
執行這個程式以後,再看一次intworld.c這個範例程式應該可以理解更深。
跟指標有關的東西很囉唆,推薦重複多學幾次。
恐怖實驗:如果對指標強制轉型
學到這裡也習慣指標,可能可以看compiler的manual來做各種挑戰了。
但是,指標還有陷阱,來介紹一下。
如果使用cast的話,可以自由自在的存取記憶體。
比如說,如果把整數進行cast變成unsignedchar的指標變數的話,就可以讀取任意範圍的位址的內容。
來實驗看看吧。
sourcecode
ptr_scan.c
#include
在執行的瞬間就異常中止了。
現代的OS,像是Windows,Linux,MacOSX之類的,都有著記憶體的保護機能,為了防止程式存取不應該存取的位址導致暴走,會強制中止程式。
而0x1000就是不能讀取的記憶體位址。
想執行類似這樣的程式,就必須宣告一個變數,指定程式員可以自由處理的位址。
比如說,把
ptr=(unsignedchar*)0x1000;
改成
charbuf[1024]="Thisistextmessage.";
ptr=(unsignedchar*)buf;
因為指標出現異常的值而又去存取那個值的內容的話,會在處理的途中強制中止,所以要注意。
到目前為止,其實就跟NULL的說明一樣。
還有一種記憶體位址,是可以讀取但不能寫入的。
在第七章說明字串常數時就有說明過:
charone_string[]="hello,world\n";
這句意思是把陣列one_string[]用"hello,world\n"這個字串給初始化。
講的詳細點,"hello,world\n"這個字串常數在compile跟link時,就配置位址在某處,程式執行時複製給陣列one_string[]。
因為是陣列,所以就算在執行時被其他值給改動都不會有什麼事。
char*one_string="hello,world\n";
這句意思是把指標變數one_string用"hello,world\n"這個字串的初始位址給初始化,"hello,world\n"這個字串常數在compile跟link時,就配置位址在某處,程式執行時把開頭位址傳給one_string。
由於字串常數多數是被確保在寫入禁止的記憶體區域,所以**(one_string+0)='H'或是one_string[0]=H*這種操作會使得程式異常中止。
這裡也實驗看看吧。
sourcecode
ptr_const.c
#include
如果搞混陣列跟指標的話,就會犯下這種錯誤。
要再度複習的話就把第七章跟本章重新讀過吧。
12.6doublepointer,triplepointer
來介紹指標變數最後的變數,「doublepointer(雙重指標)」吧。
用講的很簡單,就是放著指標變數的位址的指標變數;而放著指標變數(b)的位址的指標變數(a),(b)自己又是一個雙重指標,就把(a)叫做「triplepointer(三重指標)」。
宣告也很簡單,就是增加*號而已。
chararray[4]={0,1,2,3};
char*ptr=array;//pointer
char**ptr_double=&ptr;//doublepointer
用講的不好懂就根據剛剛的程式碼上示意圖吧。
上面就是根據程式碼所畫的指標跟雙重指標的示意圖。
假設char陣列array[],它的開頭位址是3。
指標變數ptr,則是儲存陣列array[]的開頭位址3;雙重指標變數ptr_double,則是儲存指標變數ptr的開頭位址2。
如果寫ptr_double的話,則是間接參照一次,輸出的值會是「位址3」。
而只寫ptr的話,就是「位址3」。
所以,寫ptr_double的值跟寫ptr是一樣的。
如果寫ptr_double的話,則是間接參照兩次,輸出的值會是「0」。
像ptr_double這樣的變數,有雙重指標的稱呼,不過也有人稱為「指標的指標」。
如果說成指標的指標,就很容易理解是保存著指標位址的變數了。
執行下面的範例程式就很容易理解了,試試看吧。
範例程式:
ptr_double.c
#include
所以在用printf()的%s時array,ptr,*ptr_double都會顯示ABCD�。
而把array[0]用%c表示時,array[0],*ptr,*ptr_double也都表示的一樣。
指標的指標,會在實做多維陣列、還有從函數傳遞指標時會使用。
雖然沒有現在使用的必要,不過在影像處理還有動態記憶體確保時是必要的。
為了加深理解,試試把範例程式改造成三重指標間接參照吧。
12.7多維陣列
從這裡開始說說陣列吧。
直到目前為止盡是講指標,就當做是換個頭腦來讀一下。
雖然說過很多次了,陣列跟指標是完全不同的!
目前為止看的陣列都是一維的,也就是元素只往同一方向連續排列,如下圖。
01234
+--+--+--+--+--+
||||||
+--+--+--+--+--+
但是,在利用圖形或表格之類的資料時,一維陣列是很難用的。
所以要用表格形式的資料時,會用到二維陣列。
嚴格來說,C語言不存在二維或多維陣列,但是可以用「陣列的陣列」的陣列宣告方式來實現一樣的事。
以下就來介紹。
把二維陣列用圖表示的話,就像下面這樣:
01234
+--+--+--+--+--+
0||||||
+--+--+--+--+--+
1||||||
+--+--+--+--+--+
2||||★||
+--+--+--+--+--+
3||||||
+--+--+--+--+--+
看起來就是單純的表格而已。
而宣告有點囉唆:
data_type變數名[縱向元素數][橫向元素數]
如果要宣告像剛剛的二維陣列,就像下面這樣寫:
inttuple[4][5]
圖中的★是放在tuple[2][3]。
趕快來實驗看看範例程式吧。
sourcecode
tuple.c
#include
雖然很囉唆,還是要再提醒第一個[]是縱方向,第二個[]是橫方向。
初始化也可以一個一個指定,就像下面這樣:
data_type變數名[縱向元素數][橫向元素數]=
{{[0][0]初始值,[0][1]初始值,...},
{[1][0]初始值,[1][1]初始值,...},
{[2][0]初始值,[2][1]初始值,...},
...
};
以這個例子來說,只要把剛剛tuple.c的
inttuple[4][5]={{0}};
tuple[2][3]=1;
換成
inttuple[4][5]=
{{0,0,0,0,0},
{0,0,0,0,0},
{0,0,0,1,0},
{0,0,0,0,0},
};
就是跟剛剛一樣的效果。
就像一維陣列可以省略元素數一樣,二維陣列也可以,但是能省略的只有縱方向,橫方向的元素數是一定要寫的。
如下:
inttuple[][5]=
{{0,0,0,0,0},
{0,0,0,0,0},
{0,0,0,1,0},
{0,0,0,0,0},
};
會不能兩邊都省略,是因為都省略的話compiler會不知道應該是4*5還是5*4。
考慮到下面的例子也跟上面的結果是一樣的,就比較好理解了。
inttuple[][5]=
{0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,
0,0,0,0,0};
可以再把剛剛的範例程式改造自動計算直到12*12的乘法表。
sourcecode
tuple_2.c
#include
四維陣列tuple[][][][]是圖形資料,可以想成1就是*號,每四組一起看
{0,0,0,0},
{0,1,0,0},
{1,1,1,0},
{0,0,0,0}
就是
(空白)
*
***
(空白)
的意思。
陣列可以想成tuple[圖形種類][迴轉狀態][y][x]。
getchar();
printf("\x1b[2J");
printf("\x1b[0;0H");
printf("\n");
每按一次Enter(就是程式中getchar()),圖形就會轉一下。
下一行的印出\x1b[2J則是ascii的ANSIEscapesequences,可以拿來清螢幕。
\x1b[0;0H也是Escapesequences,是把文字畫畫的位置弄回左上角。
程式在Windows的環境中可能無法正常執行,不過可以感受一下氣氛。
for(i=0;i<4;i++){
for(j=0;j<4;j++){
if(tuple[type][rot][i][j]==1){
printf("*");
}
else{
printf("");
}
}
printf("\n");
}
這個重複的處理則是遇到陣列的1的話就輸出*,0的話就輸出空白。
即使很囉唆,好好思考並實做會比較快理解。
請一定要用自己的環境執行一下。
12.8指向函數的指標
變數加上&,可以取得該變數的位址。
那麼在記憶體上存在的函數又是怎樣的呢?其實,是存在著放著函數位址的指標變數的,稱之函數指標,可以自由的代入這種指標變數並執行。
普通的函數的prototype宣告如下:
返回值的datatype函數名(參數們);
範例:
intsum(int,int);
而函數指標的宣告如下:
返回值的datatype(*函數名)(參數們);
範例:
int(*sum)(int,int);
宣告了函數指標後,就可以往裡面代入函數。
趕快來試試範例程式吧。
sourcecode
ptr_func.c
#include
如果使用同一型的函數但處理內容稍微有點不同的時候,使用函數的指標變數(函數指標)是很方便的。
比如說,網路封包的處理、或是UNIX系的「synchronoushandler」,在啟動多執行緒的函數時的參數也會利用到函數指標。
恐怖實驗:可以執行資料!?
通常電腦對於在記憶體上的命令跟資料是沒有區分的。
也就是說,記憶體上的「資料」,比如說"hello,world\n",是可以執行的。
反過來說,可以把應該要執行的「命令」作為資料表示。
這是很危險的事嗎?
趕快來執行看看吧。
sourcecode
ptr_func_2.c
#include
反過來說,只要是CPU可以解釋的命令就不會異常中止,可以作到「正確執行不正當的動作」,比如偷密碼之類的。
作為對策,總之要慎重對待指標,注意不要超過陣列確保的值。
12.9void指標
有時會很稀罕的,希望無論怎樣的datatype的指標都可以保存。
比如說,多執行緒這種在一個process內同時進行複數的處理這種的,UNIX是用pthread來實現的。
pthread是把想用thread來執行的處理當作函數交出去。
因為pthread的開頭處理是以函數實做,所以需要把想執行的函數作為thread交給pthread的開頭函數。
這種時候,就要用到剛剛學的函數指標。
但是,考慮一下把資料交給函數這件事。
資料可能是int或是char,如果搞錯datatype的話就無法正確傳遞資料的。
這個時候,用指標只告訴變數的位址在哪就可以了。
但是就算是指標變數,也是需要指定它的datatype的。
所以在這裡,就會用到可以接受任何指標的void指標了。
void*變數名
利用這個void指標傳遞資料給函數(就是把void指標當作函數的參數),始用時再利用cast轉成需要的datatype。
不同datatype的指標變數的差別,是在指標前進時位址一次前進多少而已,但是資料的大小是相同的(只要是指標變數,不管什麼type,都是儲存位址;32bit電腦的位址大小是4bytes,64bit電腦的位址大小是8bytes),所以可以作到cast。
12.10結語
這個被說C語言最難的部份,指標,怎麼樣呢?
雖然有很囉唆的部份,但是理解意思的話就很簡單。
就算是說指標很難,C語言的字串本來就是把char陣列的開頭位址傳給函數,這是從最初的hello,world的範例程式就一直必要的概念。
在之後想寫C語言的程式,是不可避的一關。
因為如此,如果還是有不懂的,請重讀並多做實驗來習慣。
就算不是100%理解,只要理解八成,之後慢慢習慣後就可以完全理解。
現階段應該有「雖然不太懂,但果然不清楚用途!」的人存在吧。
請安心,第14跟第15章會大量用到的。
←真的很可怕的C語言ch11--Preprocessor(預處理器)讀書筆記
真的很可怕的C語言ch13--結構體與共用體讀書筆記→
RecentPosts
真的很可怕的C語言目錄
beginningc++throughgameprogrammingch3讀書筆記
beginningc++throughgameprogrammingch2讀書筆記
beginningc++throughgameprogrammingch1讀書筆記
scanf用法簡介
Categories
C-language(12)
C-plus-plus(3)
延伸文章資訊
- 1C 語言講座: 5.7 陣列與指標的不同
我們可以把陣列想成是你銀行戶頭裡的現金,指標則是提款卡,空有提款卡而戶頭裡沒有現金是不能領出錢來的,同樣地空有指標而沒有實際可用的記憶體仍然不能存放資料。 許多 ...
- 2重新看懂指標與陣列之間的交互關係
C 語言中,指標與陣列之間的關係一直是一個初學者很難理解的坑。 ... 指標的宣告,需要關鍵字 * ,該關鍵字可以緊鄰變數或是型態,本質上沒有差別: ...
- 3陣列與指標的再次探討. Golang與C的指標觀念 - Medium
- 4真的很可怕的C語言ch12--指標與陣列讀書筆記 - PL-learning Blog
唯一要記住的是指標就是「指標」,不是陣列也不是運算元。 ... 還有,byte order是在多byte的data type才有差別,所以和字串的陣列沒有關係(因為char ...
- 5指標與陣列 - OpenHome.cc
在宣告陣列之後,使用到陣列變數時,會取得首元素的位址,例如在下面的程式中將指出,陣列 arr 與 &arr[0] 的值是相同的: #include <stdio.h> int main(void...