C語言中指針和數組

C語言數組與指針的那些事兒

在C語言中,要說到哪一部分最難搞,首當其衝就是指針,指針永遠是個讓人又愛又恨的東西,用好了能夠事半功倍,用很差,就會有改不完的bug和通不完的宵。可是程序員通常都有一種迷之自信,總認爲本身是天選之人,明知山有虎,偏向虎山行,直到最後用C的人都要被指針虐一遍。linux

指針

首先,明確一個概念,指針是什麼,一旦提到這個老生常談且富有爭議性的話題,那真是1000我的有1000種見解。程序員

在國內的不少教材中,給出的定義通常就是"指針就是地址",從初步理解指針的角度來講,這種說法是最容易理解的,可是這種說法明顯有它的缺陷所在。
"指針就是地址"這種說法至關於"指針=字面值地址(或者說一個具體的右值)",這種說法的錯誤所在就是弄錯了指針的本質屬性:指針是變量!編程


試想一下,若是指針是地址成立,那麼二級指針怎麼理解呢?地址的地址嗎,這明顯是錯誤的。數組

下面咱們從指針是變量這個原則出發,來分析什麼是指針:網絡

  1. 做爲一個變量,確定有本身的地址
  2. 做爲一個變量,確定有本身的值,和普通變量的區別就是指針變量的值是地址。
  3. 從第二點延伸過來,既然指針變量的值是地址,那麼那個地址上的內容就是指針變量指向的數據,指針的類型就是指針變量指向數據的類型。
  4. 指針有自己的類型,這個自己的類型區別於指向對象的類型。

在這裏,最容易弄混的就是指針自己的類型和指針的類型,指針自己的類型是int型,通常狀況下同一平臺上全部類型指針都是同樣的(注①),長度則是平臺相關,通常狀況下32位機中爲4字節,64位機中爲8字節,事實上,指針的大小由處理器中所使用的地址總線寬度決定,指針自己的類型有什麼意義呢?
(爲何說通常狀況下同一平臺上全部類型指針都是同樣,而不是全部狀況呢?事實上,在某些地址總線寬度與數據總線寬度不一樣的特殊機器上指針類型可能不一致)函數

內存的訪問是以字節爲單位的,同時指針的值爲一個地址,指針的類型就直接決定了指針的所能表示地址的上界和下界,32位指針訪問範圍爲0~2^32字節,因此是4GB。
注:如下討論中,對於指針指向數據的類型統一稱爲指針的類型,這篇博客主要討論指針的類型而非指針自己的類型學習

而指針指向數據的類型則是在定義時指定的,好比int ptr,char str,在這裏,ptr指針的數據類型就是int型,而str指針指向的類型是char型,區分指針指向數據的類型主要是用在對指針解引用時的不一樣,指針的值是具體的某一個位置,指向數據的不一樣則表明解引用的時候所取數據的不一樣,當ptr爲int*類型時,表示在ptr表示的地址處取sizeof(int)個數據,依次類推。指針

指針的地址:若是一個指針變量存儲的值是另外一個指針的地址,那這個指針就是二級指針,一樣的定義能夠遞推到多級指針。code

指針的操做

解引用:用*來獲取指針指向的數據,這個不用多說。
指針的運算:加減運算,須要注意的是,指針的加減運算的粒度是基於指針類型的長度,在下例中:對象

int *p = (int*)0x1000;
char *str = (char*)0x1000;
p++;
str++;
print("p=%d,str=%d\r\n",p,str);

輸出結果:
p=0x1004,str=0x1001
能夠看到,p指向int型數據,p++就至關於p+sizeof(int),而str++就至關於str+sizeof(char).

關於指針定義的爭議

怎麼樣定義一個指針你們都知道,在編程時一般有兩種寫法:

int* ptr;
int *ptr;

咋一看,這倆不是同樣嗎?若是你仔細觀察就能夠發現其中的不一樣,第一種定義方法中靠近類型,而第二種靠近變量,看到這裏,有些朋友就要說了,你個槓精!這不就是個寫法問題嗎,至於這麼糾結嗎!

這還真不只僅是個寫法問題。這兩種寫法背後表明着不一樣的邏輯:

  • 第一種寫法的背後的邏輯是,將int做爲一個總體,將其視爲一個類型,即int、char*與int、char這些同樣,都是一種獨立的類型,再用這些類型來定義指針變量,從這個角度來看,指針是比較好理解的,並且看起來更能解釋得通。
  • 第二種寫法的背後邏輯是,在指針的定義中,僅僅是一個標識符,如int p,代表*後面所接的變量p是一個指針變量,指向數據類型爲int型。
    其實在早期,你們一直都更傾向於經過第一種去理解指針,後來又有第二種看起來比較生澀的理解,爲何會這樣呢?咱們來看下面的例子:

    int* p1,p2;
    p2=p1;
    咱們來編譯這個例子,結果是這樣:

    warning: assignment makes integer from pointer without a cast [-Wint-conversion]
    編譯信息顯示,p2爲普通int型變量,而p1是int型指針變量,這明顯違背咱們的初衷。若是要定義兩個指針變量,咱們應該這麼作:

    int p1,p2;
    p2=p1;
    相信到這裏,你們可以看出來了,第一種寫法背後邏輯的缺陷所在。

因此如今愈來愈多的專業書籍都推薦第二種寫法,畢竟做爲一門底層語言,嚴謹性比易讀性要重要。

對教材錯誤寫法的小見解

說實話,博主學習C語言也是從國內教材開始,一開始接觸到的也是「指針就是地址」的概念,其實於我而言,這種說法讓我快速地理解了指針,後來慢慢接觸到複雜的邏輯,看了一些更好的教材,慢慢地纔開始有了更深刻的理解。

其實博主更傾向於這樣去理解這個事情:就像小學老師會告訴咱們0是最小的數,這個概念固然是錯的,可是這種教法正是能夠剝去語言的外殼,讓咱們避免陷入繁雜的分支和細節中,快速地理解使用和培養興趣,至於後面的進階,天然會有進階的書籍來糾正,就像高中或者大學以致於更高的平臺,總會告訴你你以前創建的部分概念並不徹底正確,關鍵是從新創建這個概念並不會太難,由於須要從新創建的時候每每是初級到中級的進階過程。

至於網絡上的一些比較過激的言論,我是不抱以支持態度的,不管如何,在咱們沒有能力接觸國外教材且資源缺少的時候,是這些不完美的教材使咱們踏入了計算機的世界。

指針和數組的區別

廢話說了那麼多,咱們來回到正題,看看指針和數組。不得不說,指針和數組就像孿生兄弟,有時候讓人分不清楚,這種狀況主要發生在函數參數傳遞的時候,當一個函數須要一個數組做爲一個參數時,咱們並不會將整個數組做爲參數傳遞給函數,而是傳入一個同類型指針p,而後在函數中就可使用p[N]來訪問數組中元素(這個你們都懂,就不放示例了)。

那麼,指針和數組究竟是不是同一個東西呢?
咱們來看看下面的例子:

file1.c:
    int buf[10];
file2.c:
    extern int *buf;

編譯結果:

error: conflicting types for ‘buf’。

從這裏能夠看出,數組和指針並不相等。至於具體的區別,且聽我細細道來。

數據訪問的本質區別

毫無疑問,咱們常用指針的數組,也常常混用。可是咱們有沒有關注過它們背後的執行原理呢?咱們看下面的代碼:

int buf[10] = {5};
int *p = buf;
*p = 10;

首先,有必要來說講數組的初始化,在定義時,若是咱們不對數組進行初始化操做,有兩種狀況:

  • 數組爲全局變量或者靜態變量時,在程序加載階段默認全部元素都被初始化爲0。
  • 數組爲局部變量,由於數組數據在棧上分配,就延續了了棧上上一次的值,因此這個值是不肯定的。

同時,咱們能夠對其進行初始化,能夠所有初始化或者部分初始化,部分初始化時,未被初始化部分所有默認被初始化爲0.因此咱們經常使用buf[N]={0}來在定義時初始化一個數組。

根據C語言的規定,數組名=數組首元素指針,因此直接能夠用數組名的解引用buf來訪問第一個元素,也可使用(buf+N)來訪問第N個元素。

咱們須要知道的是,在程序編譯的時候,會對全部的變量分配一個地址,這個地址和變量的對應在符號表中被呈現,數組和指針在符號表中的區別就體如今這裏:

  • 對於數組而言,符號表中存在的地址爲數組首元素地址,因此當咱們使用素組下標訪問元素N時,它執行的是這樣的操做
    • 先取出數組首元素地址
    • 目標地址=首地址+sizeof(type)*N,獲得被訪問元素的地址,type是指針指向數據類型,指針加法參考上面。
    • 解引用(至關於在變量前加*),從地址上取出被訪問元素。
  • 對於指針變量而言,符號表中存儲的是指針變量的地址,它訪問元素時這樣的過程:
    • 取出指針變量的地址,解引用以獲取指針變量
    • 繼續對指針變量進行解引用,獲取目標元素的值。

看到這裏,我想你已經知道了指針和數組訪問數據的本質區別,可是,咱們在這裏須要討論的狀況並不是這兩種.

而是:參數定義爲指針,可是以數組的方式引用。這個在函數調用時纔是發生得最頻繁的,那這時候會發生什麼呢?

這個時候其實就是兩種訪問方式的結合了,假設定義了指針buf,那麼在符號表中存在的就是buf指針的地址(注意是buf的地址,並且buf自己是個指針),參考上述指針的訪問方式.以獲取buf中第二個元素爲例:

  • 首先,根據buf變量的地址,獲取buf指針。
  • 使用第一步中獲取的地址進行偏移,獲得目標數組元素的地址,此時目標地址爲(&buf[0]+2)
  • 解引用(至關於在變量前加),從地址上取出被訪問元素,至關於執行(&buf[0]+2)。

到這裏,我想你已經大概清楚了數組和指針的區別,以及參數傳遞時,指針的下標引用背後的原理。

數組指針和數組元素指針

在上一小節中,我指出了數組名=數組首元素指針的概念,若是朋友們不仔細看,或者本身不去寫代碼嘗試,很容易把它記成了數組名=數組的指針 這個概念,請特別注意,數組名=數組的指針這個概念是徹底錯誤的,這也是數組中很是容易混淆和犯錯的地方,咱們不妨來看下面的例子:

char buf[5]={0};
printf("address of origin buf = %x\r\n",buf);
printf("address of changed buf = %x\r\n",&buf+1);

輸出結果:

address of origin buf = de157880
address of changed buf = de157885

咱們先定義一個長度爲5的buf,buf中首元素地址爲0xde157880,而後再打印&buf+1的值,顯示爲0xde157885,那麼問題就來了,爲何明明只是+1,而地址卻加了5,5正好是sizeof(buf)。咱們再來看看下面的例子:

char buf[5]={0};
printf("address of changed buf = %x\r\n",(&buf+1)-buf);

編譯時信息以下:

error: invalid operands to binary - (have ‘char (*)[5]’ and ‘char *’)

從這個報錯信息,咱們能夠看出,&buf的類型爲char ()[5],爲數組指針類型,而buf類型爲char ,字符指針類型。

看到這裏,問題也就慢慢地清晰了。在C語言中,數組名是一個特殊的存在,與咱們慣有的思惟相反,數組名錶明數組首元素的指針,而不是數組指針,若是要聲明一個數組指針,咱們能夠這樣來聲明:char (*p)[5] = buf;

說了這麼多,那麼,區分數組指針和數組元素指針的意義在哪裏呢?參考上面所說的指針的加減運算,即:指針的加減運算的粒度是基於指針類型的長度,數組指針的長度爲sizeof(數組),而數組元素指針是sizeof(單個元素)(再囉嗦一次!數組名爲數組元素指針而不是數組指針)。

指針數組和二維數組

數組指針是一個指針類型爲數組的指針,好比定義一個帶有5個char元素數組的指針:char (*buf)[5]。

那麼指針數組又是什麼東西呢?其實指針數組要比數組指針容易理解,它就是一個普通數組,只不過特殊的是數組內全部元素都是指針,好比定義一個字符指針數組:char *buf[5],注意它們之間的區別;數組指針是一個指針,指針數組是一個數組。


二維數組,你們可能沒有使用過,可是必定聽過,二維數組的定義:char buf[x][y],其中x可缺省,y不能缺省。對於二維數組,咱們能夠這樣理解:二維數組是一維數組的嵌套,即一維數組中全部元素爲同類型數組。 例如:char array[3][3],咱們能夠將其理解成array數組是一個一維數組,數組的元素分別是array[0],array[1],array[2]三個char[3]型數組,這種理解能夠遞推到多維數組,從而來理解二維數組的內存模型。

下面詳細說說爲何須要將多維數組當作一維數組。

二維數組和二級指針

"既然一維數組和指針在必定程度上能夠"混合使用",那麼二維數組確定也是可使用二維指針來訪問了" —— 某不知名程序員語錄

問:上面這句話有沒有什麼問題?

答:大錯特錯!

很慚愧,博主曾經也是這麼認爲的,二維數組確定是能夠像一維數組那樣使用指針訪問,只不過要用二級指針(二維嘛)。

話很少說,咱們先看下面代碼:

char buf[2][2]={{1,2},{3,4}};
char **p = buf;
printf("buf[] = %d,%d,%d,%d\r\n",p[0][0],p[0][1],p[1][1],p[1][2]);

輸出結果:

Segmentation fault (core dumped)

在這個示例中,博主的本意是使用二級指針p賦值爲二維數組名,而後使用p訪問數組中元素,可是結果明顯跑偏了,這是爲何?

有些朋友可能在學習上面的"數組和指針數據訪問的本質區別"的時候會想,我只要會用就好了,我要去關注這些底層細節有什麼做用?在簡單的應用中固然沒什麼做用,可是在這種時刻就須要對底層紮實的理解了。


咱們來詳細分析一下上面代碼中的背後訪問邏輯:

  • 第一點,咱們須要確認的是,二維數組的數組名究竟是什麼類型的指針。是二維數組中第一個char型元素的指針嗎?仍是按照上一節"指針數組和二維數組"中說的那樣,將二維數組當作一個一維數組,從一維數組的角度看,首元素爲buf0,那二維數組名就是一個數組指針,類型爲char (*)[2]。要驗證這個很簡單,咱們分別編譯兩份代碼:

    代碼1:
    char buf[2][2]={{1,2},{3,4}};
    char *p = buf;
    編譯結果:

    warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]

    代碼2:
    char buf[2][2]={{1,2},{3,4}};
    char (*p)[2] = buf;
    編譯結果:
    無警告信息
    所謂實踐出真知,結果很顯然,答案是第二種:咱們應該將二維數組當成嵌套的一維數組,而數組名爲首元素地址,注意,這裏的首元素是從一維數組的角度出發,這個首元素的類型多是普通變量,數組甚至是多維數組。

  • 第二點,char **p = buf;這一條怎麼去理解呢?根據上面的結論二維數組名buf是char (*)[2]類型,而p是char型二級指針,參數天然不匹配。
  • 即便是參數不匹配,可是編譯只是警告,而非報錯,咱們仍然能夠執行它。那麼執行這個程序的時候又發生了什麼呢?咱們根據"指針與數組數據訪問的本質區別"小節部分來分析:
    • 首先,p的地址是在編譯時已知的,程序運行時,經過指針p的地址獲得p的值,通過上面的分析,此時p = &buf[0],雖然&buf[0]是數組指針,可是p爲char** 類型,因此&buf[0]被強制轉換成char**型指針。
    • 在printf函數中訪問p[0][0],事實上訪問P[0][0]就先得訪問p[0],那麼就先找到p的值,那麼p的值又是多少呢?答案是p=buf[0][0],p不是一個地址,而是一個字面值1,因此此時p[0] = 1,訪問*p[0]天然會致使Segmentation fault (core dumped)。

鑑於上面的解析部分很是難以理解,並且僅僅是字面講解幾乎沒法講清楚,博主就嘗試經過幾個示例來進行講解:

示例1:
char buf[2][2]={{1,2},{3,4}};
char **p = buf;
printf("array name--buf address = %x\r\n",buf);
printf("&buf[0] address = %x\r\n",&buf[0]);
printf("Secondary pointer address = %x\r\n",p);
輸出:

array name--buf address = a836a2c0
&buf[0] address = a836a2c0
&buf[0][0] address = a836a2c0
Secondary pointer address = a836a2c0

儘管編譯過程有好幾個Warning,暫時不去理會,結果顯示,至少從數值上來講 p = buf = &buf[0] = &buf[0][0]。


示例2:

char buf[2][2]={{1,2},{3,4}};
char **p = buf;
printf("p[0] = %x\r\n",p[0]);

輸出:
p[0] = 04030201
這個結果就很是有意思了,能夠看到,指針p[0]的值,正好是數組buf的四個元素的值(內存中存儲順序將01020304反序存儲,這裏涉及到大小端的存儲問題,不過多贅述)。可想而知,訪問p[0][0]的時候會發生什麼?按照以前的講解,咱們先將p[0]作相應位移,即p[0]=p[0]+sizeof(char)*0,而後再解引用獲取地址上的值,那就是直接取0x04030201地址上的值,結果固然不會是咱們所期待的!

再回到示例,爲何p[0]的值會是0x04030201?

  • 首先,咱們要知道,p[0]是什麼類型,p[0]即爲*p,p是二級指針,*p也是一個指針,因此*p的自己的類型爲int*,因此它的值爲4個字節。
  • 根據前面的分析,p = buf = &buf[0] = &buf[0][0],對p解引用(即p)至關於取出p地址處的數據,根據int類型,取四個字節數據,而這四個字節正好就是buf中四個元素。

那若是咱們要使用指針來訪問二維數組中的元素,該怎麼作呢?
看下面的代碼:

#define ROW     2
#define COLUMN  2
char buf[ROW][COLUMN]={{1,2},{3,4}};
char *p = (char*)buf;
//訪問buf[x][y],即訪問p[x*COLUMN+y]
printf("buf = %d,%d,%d,%d\r\n",p[COLUMN*0+0],p[COLUMN*0+1],p[COLUMN*1+0],p[COLUMN*1+1]);

若是你看懂了以前博主介紹的內容,理解這一份代碼是很是簡單的。

好了,關於C語言中指針和數組的討論就到此爲止了,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

我的郵箱:linux_downey@sina.com
原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身.
(完)

結語:爲了寫這一篇博文,感受髮際線又往上走了一公分...

相關文章
相關標籤/搜索