重新看懂指標與陣列之間的交互關係

文章推薦指數: 80 %
投票人數:10人

C 語言中,指標與陣列之間的關係一直是一個初學者很難理解的坑。

... 陣列的變數名稱其實就是一個指標,指向陣列開頭元素的記憶體位置。

C語言中,指標與陣列之間的關係一直是一個初學者很難理解的坑。

又或是很多人只知道寫法,但卻從來沒有理解過背後的原因。

相信各位理解了原因之後,在撰寫C語言時會對自己操作這個語言更加有自信。

這篇文章重新試著釐清這指標與陣列之間的關係,希望對大家能有幫助。

陣列宣告與指標宣告陣列可以用以下的方式宣告: 宣告但是尚未初始化 1intarr_init0[3];//宣告但是尚未初始化,此時陣列中的值是沒有意義的。

宣告並且初始化 1intarr_init1[3]={1,2,3}; 不指定大小,大小會依照後面元素個數來決定 1intarr_init2[]={1,2,3}; C99新增。

指定特定元素,其他未被指定元素會被設定為0 1intarr_init3[]={[2]=2}; 指標的宣告,需要關鍵字*,該關鍵字可以緊鄰變數或是型態,本質上沒有差別: 12int*ptr1;int*ptr2; 但若是我們宣告指標卻不指定初始值,這件事是非常危險的! 12int*ptr1;printf("%p\n",ptr1);//0x1125ce025 該指標在未宣告的情況下,編譯器會直接指定該位置上本來就存有的值(這個值沒有意義,是作業系統殘留的值),如果沒有特別注意,操作這個位置會發生錯誤,甚至覆寫到需要的資料。

我們會建議指標在宣告時還沒有特定的空間或位置可以指定時,先使用NULL來指定。

12int*ptr=NULL;printf("%p\n",ptr);//0x0 陣列索引與指標陣列的變數名稱其實就是一個指標,指向陣列開頭元素的記憶體位置。

這也就是為什麼陣列索引會從0開始計算,因為索引的意義其實是與起始位置的位移量。

我們可以用以下範例看到起始位置的值就是直接對陣列名稱取值(dereference)。

123intarr[3]={1,2,3};printf("Address:%p,Value:%d\n",arr,*arr);//Address:0x7ffee98be05c,Value:1 如果對該變數遞增,會發現記憶體位置相差了4格,原因是因為我們在宣告陣列時,指定了陣列的型態是int,如此以來我們在進行取值的動作時,CPU才知道下一個位置在哪邊。

有趣的是,一般我們所使用的取值動作arr[1]其實就等於*(arr+1)。

12printf("Address:%p,Value:%d\n",arr+1,*(arr+1));//Address:0x7ffee1dca060,Value:2 有了以上的概念之後,不難理解為何我們在設定函數的參數引述時,可以將陣列設定為: 1voidfoo(int*arr); 或是這樣設定: 1voidfoo(intarr[]); 這兩種指定方式是一模一樣的(對於編譯器來說)。

在函式原型中,參數的名稱是沒有意義的,只有型態有意義,所以也可以指定為: 1voidfoo(int*); 但是陣列名稱與指標也不能說是完全一模一樣的東西。

陣列傳遞與指標在C語言中,傳遞陣列就是傳遞陣列的起始記憶體位置,所以我們可以用指標來接收陣列: 12345678910111213#includevoidfoo(int*);intmain(){intarr[3]={1,2,3};printf("%lu\n",sizeofarr);//12foo(arr);}voidfoo(int*ar){printf("%lu\n",sizeofar);//8} 以上程式會印出: 12128 先印出12的原因是,陣列在宣告時就已經知道了陣列長度,int型態在我的作業系統中佔用了4bytes,所以4x3=12。

8的話則是代表指標變數本身佔據了8bytes的空間。

非常合理,因為我的電腦是64-bit的作業系統,要可以完整定址全部空間需要8bytes(64/8=8)。

所以為什麼剛才提到:「陣列名稱與指標也不能說是完全一模一樣的東西」。

用sizeof進行操作時,會發現兩者還是有一點差別!(不過在沒有進行參數傳遞前,幾乎是沒有差別的。

) 由以上的例子可以發現,陣列的變數名稱可以進一步得知陣列長度的,只要使用sizeof運算子即可: 123intarr[3]={1,2,3};intlen=sizeofarr/sizeofarr[0];printf("%d\n",len);//3 sizeof後面如果不是接基本型態,是不需要括號的。

如果接上基本型態才需要括號,像是: 12345printf("%lu\n",sizeof(char));//1printf("%lu\n",sizeof(short));//2printf("%lu\n",sizeof(int));//4printf("%lu\n",sizeof(long));//8//請注意!不同型態的空間大小是由編譯器依照作業系統去分配以及實作,所以有可能不同電腦上面的結果不一致。

對於函式來說,它只有辦法得知陣列起始記憶體位置,無法得知總長度。

或是可以直接說,對於函數來說,他並不知道傳進來的東西是一個陣列,只知道是一個記憶體位置,指向的型態也知道,其他事情對於這個函數來說都無法得知。

這也是為何我們時常在接收陣列時,會額外接收一個參數,用來表示陣列的總長度。

1voidfoo(int*arr,intn); 二維陣列在宣告一維陣列時,可以直接填上元素,不指定陣列大小,可是在二維陣列這樣操作的話,會發生錯誤: 12345678inttd_arr[][]={{1,2},{3,4},{5,6}};/*app.c:7:12:error:arrayhasincompleteelementtype'int[]'inttd_arr[][]={{1,2},{3,4},{5,6}};^1errorgenerated.*/ 我們要先重新理解二維陣列,二維陣列不過是一個陣列,該陣列的值也是一個陣列({1,2},{3,4},{5,6}),沒有多特別,僅此而已。

陣列只接受第一層不指定大小而已,用後面的元素個數自己推算(一維陣列只有一層,所以你可能會認為一維陣列比較聰明)。

所以我們應該要告訴編譯器,內層陣列的大小,這樣他才有辦法幫我們將所需要的空間準備好,我們應該這樣子撰寫程式: 1inttd_arr[][2]={{1,2},{3,4},{5,6}}; 第一層有幾個元素可以不用指定(就像一維陣列),但是我們需要告訴編譯器,內容陣列的寬度有多大。

我們總共花了24bytes的空間,4bytes(intsize)x6(elements)=24。

接下來,我們嘗試將二維陣列中我們需要的值取出: 12printf("%d\n",*td_arr[1]);//3printf("%d\n",(*td_arr)[1]);//2 []的優先權比*還要高,所以在第一個範例中我們會先找到td_arr中的第二個元素(第1個是索引0)然後取值。

第二個元素就是{3,4},在文章前段的一維陣列有講過直接取值就是對第一個元素(索引0)取值。

所以{3,4}的第一個元素就是3。

第二個範例則是先取值,我們會拿到td_arr的第1個元素(索引0),也就是{1,2}接下來取出第二個元素(索引1),得到2。

也可以將上述寫成是不含有[]的表示法,如下: 12printf("%d\n",**(td_arr+1));//3printf("%d\n",*(*td_arr+1));//2 前面段落也有提到[]與*(arr+offset)的寫法可以互相替換,就不再贅述。

指標與二維陣列接下來我們要理解如何用指標去操作二維陣列,首先我們需要先宣告一個指向二維陣列的指標: 1int(*td_ptr)[2]; 該宣告的意思是宣告一個指標,指向大小為2的陣列,該陣列內容為int型態。

為何不直接寫: 1int*td_ptr[2];//可以看成int*(td_ptr[2]); 原因是因為優先權帶來的影響並不同([]的優先權較大),以上宣告的意思是產生一個大小為2的陣列,陣列內容是兩個指向int的指標。

如下: {ptr1,ptr2} 宣告完指標之後,我們可以將該指標,指向我們的二維陣列: 123inttd_arr[][2]={{1,2},{3,4},{5,6}};int(*td_ptr)[2];td_ptr=td_arr; 接著一樣可以用指標來操作該陣列: 12printf("%d\n",td_ptr[2][0]);//5printf("%d\n",td_ptr[2][1]);//6 如果需要設定函數原型的話: 1voidfoo(int(*ar)[2]); 或是: 1voidfoo(intarr[][2]); 皆可以用來接收。

我想看到這邊,如果你沒有其他疑問的話。

應該可以稍微理解為何我們在傳遞二維陣列時,會使用這樣子的寫法了!這樣子理解的話也可以推廣到多維陣列中,像是: 12voidfoo(intarr[][2][3][4][5][6]);voidfoo(int(*arr)[2][3][4][5][6]); 後記希望這篇文章可以讓大家更加理解C語言陣列與指標撰寫過程中的背景原因,在網路上看到太多文章只有提到宣告或是使用的方式,但是卻沒有加以描述任何原因,導致很多人只知道寫法但不清楚為何應該這樣子撰寫。

希望大家看完文章有所收穫😄! #c ←從駭客角度告訴你為何不要隨意複製指令 · 優雅的在macOS上使用Python→ 陣列宣告與指標宣告陣列索引與指標陣列傳遞與指標二維陣列指標與二維陣列後記 理解Kubernetes中的CPULimit 一次CI/CD調教經驗 如何備份Kubernetes中的etcd CI/CDDNSDockerDroneCIKubernetesLinuxccgroupctfdockeres6fphttp2javascriptkuberneteslinuxmacOSmacosmysqlnginxnodejsoauth2pythonraspberry_pishellsockettelegramtoolstraefik個人資訊安全



請為這篇文章評分?