今天給大家來講解一下指針。
由淺到深,最后結(jié)合實(shí)際應(yīng)用講解,讓大家學(xué)會(huì)指針的同時(shí),知道大佬們都用指針來干嘛! 長(zhǎng)文預(yù)警!全文大約5200多字,學(xué)指針看這篇文章就夠了! 很多人跟我剛學(xué)習(xí)c語言一樣,都害怕指針。 我也是后面做了一些物聯(lián)網(wǎng)網(wǎng)關(guān)才知道,指針是c語言的靈魂這句話真正含義。 沒有指針,很多功能實(shí)現(xiàn)起來確實(shí)很不方便,比如做不到真正的模塊化編程。 Ok,廢話不多說,下面正式進(jìn)入主題。
一、通過這篇文章你能掌握以下知識(shí):
指針的相關(guān)概念
掌握指針與數(shù)組之間的關(guān)系
掌握指針指向的指針
掌握如何使用指針變量做函數(shù)參數(shù)
掌握如何使用指針函數(shù)
掌握如何使用指針數(shù)組函數(shù)
那么這篇文章對(duì)應(yīng)有視頻教程,如果不喜歡看文章的可以去看視頻,教程在小破站可以搜無際單片機(jī)編程找到,也可以找我們拿。
二、指針的作用: 指針是C語言中一個(gè)比較重要的東西,有人說指針是C語言的靈魂這句話說的一點(diǎn)也沒錯(cuò)。 正確靈活地運(yùn)用它,可以有效地表達(dá)一些復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如系統(tǒng)的動(dòng)態(tài)分配內(nèi)存、消息機(jī)制、任務(wù)調(diào)度、靈活矩陣定時(shí)等等。 掌握指針可以使你的程序更加簡(jiǎn)潔、緊湊、高效。 那么在單片機(jī)領(lǐng)域,如果是做稍微大一點(diǎn)的項(xiàng)目,需要把每個(gè)功能做成模塊化,硬件驅(qū)動(dòng)層和應(yīng)用層分別獨(dú)立運(yùn)行。 即使更換單片機(jī)型號(hào)也不用修改應(yīng)用層程序,即移植性非常強(qiáng),這些都離不開指針。 甚至沒指針會(huì)很難實(shí)現(xiàn),即使實(shí)現(xiàn)代碼的可移植性也很差。
三、指針的概念
前面講了指針的作用,這里再?gòu)?qiáng)調(diào)一點(diǎn),指針是一把雙刃劍。 用好了能十分靈活而且提高程序的效率,但是如果使用不當(dāng),則會(huì)出現(xiàn)程序”死機(jī)”等致命問題。 而這些問題往往是由于錯(cuò)誤地使用指針而造成的,最常見的就是內(nèi)存溢出錯(cuò)誤,指針指向未知地址。 1.地址與指針 指針是一個(gè)比較抽象的概念,如果想真正了解指針,那么要先從數(shù)據(jù)是如何存儲(chǔ)的說起,我們通過一個(gè)圖來看一下數(shù)據(jù)在內(nèi)存里存儲(chǔ)的情況。
在這個(gè)圖中,都是以16進(jìn)制顯示。 紅色標(biāo)注的0x00000400代表地址內(nèi)存地址,綠色37,30代表數(shù)據(jù),而橙色標(biāo)注的00 01代表地址遞增量,即代表0x00000400和0x00000401,每個(gè)地址存儲(chǔ)1個(gè)字節(jié)數(shù)據(jù)。 那么我們把這個(gè)圖看作是數(shù)據(jù)在內(nèi)存里的存儲(chǔ)形式,0x00000400這個(gè)內(nèi)存地址存儲(chǔ)著數(shù)據(jù)37,0x00000401這個(gè)內(nèi)存地址存儲(chǔ)著數(shù)據(jù)30。
當(dāng)我們?cè)诔绦蚶锒x一個(gè)字節(jié)的變量,那么在編譯器編譯時(shí)就會(huì)給這個(gè)變量分配一個(gè)這樣的內(nèi)存地址來存儲(chǔ)。 假設(shè)我們定義以下變量 unsigned char a; a = 0x37; 對(duì)應(yīng)這個(gè)圖就是,編譯器在編譯時(shí)會(huì)為變量a分配一個(gè)字節(jié)的內(nèi)存空間并把0x37這個(gè)數(shù)據(jù)存儲(chǔ)進(jìn)去,并將變量名a改成地址0x00000400,以便CPU的訪問。 通過這個(gè)地址就能找到變量a數(shù)據(jù)的存儲(chǔ)位置,而這個(gè)地址0x00000400其實(shí)就是指針,通過這個(gè)指針可以訪問變量a的數(shù)據(jù)。
2.指針變量 通過上面講解我們明白了通過地址能訪問內(nèi)存的數(shù)據(jù),這個(gè)地址啊就是指針。 那么指針和指針變量呢是不一樣的概念,大家一定要記住了。
指針是概念、指針變量是這個(gè)概念的具體應(yīng)用之一,我們先來看一下C語言里怎么定義指針變量。 指針變量定義的一般形式: 變量類型 *變量名 unsigned char *p; 通過這種語法,我們就能夠定義一個(gè)指針變量p。 指針變量賦值 指針和指針變量是兩個(gè)概念,指針變量跟普通變量一樣,在使用前一定要定義和賦值(指向地址)。 給指針變量賦的值和普通變量不同,給指針變量賦值只能賦地址,而不能賦予其他任何值,否則會(huì)引起錯(cuò)誤。 那么怎么獲取普通變量的地址呢,在C語言里可以使用”&”來獲取普通變量的地址,一般用以下格式來表示: &變量名 那么通過&變量名取得變量地址后就可以賦值給指針變量。 舉例: ?unsigned char a; ?unsigned char *p ?int main() ?{ ?????? p = &a;
} 這個(gè)代碼里,我們定義了一個(gè)變量a, 定義了一個(gè)指針變量p。 我們通過運(yùn)算符&把變量a的內(nèi)存地址賦值給變量p,所以p指向了變量a的內(nèi)存存儲(chǔ)地址。 上面說了指針變量賦值的問題,那么怎么獲取和改變指針變量指向那個(gè)內(nèi)存地址的數(shù)據(jù)呢?我們可以通過: *指針變量 = 數(shù)值 如:*p = 10; 這樣的操作可以改變指針變量指向那個(gè)內(nèi)存地址的數(shù)據(jù)。 通過: a = *p; 來獲取指針變量指向那個(gè)內(nèi)存地址的數(shù)據(jù)。 下面我們通過一個(gè)代碼實(shí)驗(yàn)來舉例。
這里我們定義了變量a和指針變量p,然后a的值初始化為10。 把a(bǔ)的地址賦值給指針變量p,接著我們輸出a的地址是0x60ff33。 由于前面我們把a(bǔ)的地址賦值給了指針變量p,所以p指向的地址也是0x60ff33。 那么我們?cè)賮砜匆幌?,指針變量的在?nèi)存里的存儲(chǔ)地址是0x60ff2c。 所以大家這里要注意了,我們定義指針變量時(shí),即便指針變量是指向地址用的,但是編譯器也會(huì)分配一塊內(nèi)存地址來存儲(chǔ)指針變量。
我們接著來看下變量a的輸出值。 a=10, *p是獲取指針指向內(nèi)存地址的數(shù)據(jù),所以也是10。 下面就是通過指針變量來改變變量a的值,因?yàn)橹羔樧兞縫指向的是變量a的地址,所以改變指針變量p指向內(nèi)存地址的數(shù)據(jù)就可以改變變量a的值。 那么通過這么原理,我們是不是不用指針變量,也不用a等于多少來改變a的值呢?當(dāng)然可以! 我們看下面通過內(nèi)存地址改變變量a的值,我們前面知道a的地址是0x60ff33,那我們可以直接寫0x60ff33=12來改變變量a的值。 當(dāng)然這里要注意,編譯器編譯時(shí)并不知道0x60ff33是什么東西,所以要把這個(gè)整形地址轉(zhuǎn)換成指針類型。 最后通過*+地址語法改變這個(gè)地址里面的數(shù)據(jù)。 我們看輸出結(jié)果,可以發(fā)現(xiàn)a的值已經(jīng)成功被改成了12。 其實(shí)通過指針變量改變某個(gè)內(nèi)存地址的數(shù)據(jù)就是這個(gè)原理,但是指針變量好處可以任意起名字。 也不用像這樣先把變量a的地址讀出來,然后通過地址去改變它的值,用起來就很方便,所以通過指針變量來替代了這種做法。 ?
四、數(shù)組與指針
一般系統(tǒng)或編譯器會(huì)分配連續(xù)地址的內(nèi)存來存儲(chǔ)數(shù)組里的元素,如果把數(shù)組地址賦值給指針變量,那么就可以通過指針變量來引用數(shù)組,讀寫數(shù)組里的元素了。我們來做個(gè)實(shí)驗(yàn):
從這個(gè)代碼來看,定義了一個(gè)數(shù)組buff并初始化為1,2,3,4,5。 定義了2個(gè)指針變量p1和p1,分別指向buff, &buff[0]。 buff默認(rèn)的是數(shù)組下標(biāo)為0元素的存儲(chǔ)地址。 所以這里buff和&buff[0]是同一個(gè)內(nèi)存地址,只是寫法不一樣。 我們從輸出結(jié)果可以看的出來,數(shù)組和指針變量的地址都是一樣的,所以大家用這幾種寫法都是可以的。 那么我們來看下輸出結(jié)果,都是1,說明操作是對(duì)的。
指針自加自減運(yùn)算
指針變量除了可以用來獲取內(nèi)存地址的值以外,還可以用來進(jìn)行加減運(yùn)算。 但是這個(gè)加減呢跟普通變量加減不一樣,普通變量加減的是數(shù)值,而指針變量加減的是地址,我們來通過代碼來講解下。
同樣這里定義了數(shù)組buff并初始化為1,2,3,4,5。 我們把指針變量p1指向數(shù)組第一個(gè)元素的地址,即0x402000。 然后我們直接看p1++的操作,p1++后我們看到p=0x402001,所以指針變量的加減等運(yùn)算是指向地址的運(yùn)算。 其他減法乘除法也是基于地址的運(yùn)算。 ? 二維數(shù)組與指針 通過一維數(shù)組與指針的講解,相信大家已經(jīng)掌握。 那么二維數(shù)組與指針的操作也是一樣的, 二維數(shù)組和一維數(shù)組一樣,都是分配連續(xù)的地址來存儲(chǔ)的數(shù)據(jù)的。 我們還是通過一個(gè)例子來實(shí)踐一下:
首先我們定義了一個(gè)二維數(shù)組buff和指針變量p1。 p1指向二維數(shù)組的[0][0]這個(gè)元素地址,這個(gè)就是為這個(gè)數(shù)組分配時(shí)的首地址。 然后打印二維數(shù)組里每個(gè)元素的地址和值,接著打印指針變量地址和值,這些就是指針和二維數(shù)組的用法,比較簡(jiǎn)單,這些代碼大家可以去做下實(shí)驗(yàn)。 ? 四、指向指針的指針 一個(gè)指針變量指向整型變量或者字符型變量,當(dāng)然也可以指向指針變量,這種指針變量用于指向指針類型變量時(shí),就稱為指向指針的變量,也叫雙重指針。 定義方法: 數(shù)據(jù)類型 **指針變量名; 例如:unsigned char **p; 這個(gè)含義就是定義一個(gè)指向指針的指針變量p,它指向另一個(gè)指針變量,我們通過代碼來說明一下會(huì)更好理解一點(diǎn)。
我們定義一個(gè)變量a, 定義一個(gè)指針變量p1,定義一個(gè)雙重指針變量p2,然后打印這3個(gè)變量的內(nèi)存地址。 編譯器在編譯的時(shí)候呢,也會(huì)為指針變量和雙重指針變量分配一個(gè)存儲(chǔ)空間。 雖然指針變量是指向別的內(nèi)存地址的,但是變量本身還是需要一個(gè)地址空間來存儲(chǔ)的。 指針容易把人搞暈的就是,指針變量本身的存儲(chǔ)地址和指向的地址分不清楚,這個(gè)是兩個(gè)概念,大家要記住了。 下面我們通過實(shí)驗(yàn)來看下雙重指針怎么用:
這里我們定義了變量a并初始化值為10,指針變量p1,雙重指針變量p2。 我們把p1指向變量a,p2指向變量p1的存儲(chǔ)地址,這里要注意,不是p1指針指向的地址。 然后我們打印看下結(jié)果,可以看到a的地址是0x404090。 指針變量p1的存儲(chǔ)地址通過&運(yùn)算符獲得即0x4040b0,p1指向a的地址,所以p1也等于0x404090。 所以指針變量分為存儲(chǔ)地址和指向地址,這兩個(gè)是不一樣的概念。 而p2是雙重指針,p2指向p1的存儲(chǔ)地址0x4040b0,通過*p2獲得0x4040b0這個(gè)地址里指向的地址0x404090,即p1指向的地址或變量a的地址。 再通過**p2來獲取0x404090地址里的值,得到10。 這里還有一個(gè)問題需要注意,”*”這個(gè)運(yùn)算符是從右到左進(jìn)行運(yùn)算的。 所以,**p2就是*(*p2),先取指向地址,再取指向地址里面存儲(chǔ)的值。 一般在單片機(jī)程序中,盡量少使用這種指向指針的指針,防止出現(xiàn)Bug的時(shí)候非常難排查,目前我就在隊(duì)列中使用過。 ?
五、指針變量作為函數(shù)形參
一般我們都是以字符型、整型、數(shù)組等作為函數(shù)的形參帶入。 除此以外,指針變量也可以作為形參使用,而且用的非常多,主要目的是為了改變指針指向地址的值,專業(yè)術(shù)語是通過形參改變實(shí)參的值。 我們直接寫個(gè)代碼來舉個(gè)例子:
這個(gè)代碼中,我們定義一個(gè)SetValue函數(shù),并且形參為指針變量p1。 我們調(diào)用SetValue時(shí)把&a的地址賦值給形參指針變量p1。 當(dāng)我們通過*p1=5后就能把p1指向地址的值改成5,所以a的值也從1變成了5。 這個(gè)就是指針變量作為函數(shù)形參的一種作用。 實(shí)際當(dāng)中使用功能當(dāng)然不會(huì)這么簡(jiǎn)單。 比如說我們常用的memset庫(kù)函數(shù),他的原型就是: Void *memset(void *s, int ch, size_t n); 這個(gè)函數(shù)的作用是給某個(gè)數(shù)組或者結(jié)構(gòu)體初始化用的。 那么這個(gè)函數(shù)就使用了無指定數(shù)據(jù)類型的指針變量s,這樣我們就可以很輕易的把實(shí)現(xiàn)某些功能的代碼封裝起來,使用者不用關(guān)心功能代碼的實(shí)現(xiàn),只需要了解函數(shù)怎么用即可。 這樣的話代碼很簡(jiǎn)潔緊湊,移植性也好,這是把指針作為形參的一種作用,不過這些都只是冰山一角,在后面的學(xué)習(xí)當(dāng)中,你會(huì)慢慢發(fā)現(xiàn)指針的魅力和強(qiáng)大。 六、函數(shù)指針 如果在程序中定義了一個(gè)函數(shù),那么在編譯時(shí)系統(tǒng)就會(huì)為這個(gè)函數(shù)代碼分配一段存儲(chǔ)空間,這段存儲(chǔ)空間的首地址稱為這個(gè)函數(shù)的地址。 而且函數(shù)名表示的就是這個(gè)地址。 既然是地址我們就可以定義一個(gè)指針變量來存放,這個(gè)指針變量就叫作函數(shù)指針變量,簡(jiǎn)稱函數(shù)指針。 在這個(gè)章節(jié)我們大家只要學(xué)會(huì)怎么定義和使用就行了,后面章節(jié)課程我們無際單片機(jī)編程會(huì)教大家函數(shù)指針的一些實(shí)際應(yīng)用。 我們學(xué)東西主要還是看能運(yùn)用在哪里是吧? 那么這個(gè)函數(shù)指針怎么定義呢?我們定義函數(shù)指針的格式如下: 函數(shù)返回值類型?(*?指針變量名) (函數(shù)參數(shù)列表);
這樣就定義了一個(gè)函數(shù)指針變量func, 該函數(shù)指針返回值為unsigned char類型,然后有2個(gè)形參,分別是unsigned char類型。 那么我們定義了這個(gè)函數(shù)指針變量以后要怎么使用呢?我們寫個(gè)代碼來解析一下。
我們看下這個(gè)代碼,首先我們定義一個(gè)函數(shù)指針func,再定義一個(gè)加法函數(shù)add,函數(shù)返回值為形參1+形參2的值。 然后我們把func指向加法函數(shù)add,因?yàn)楹瘮?shù)名稱就是函數(shù)首地址,所以我們直接func=add就可以實(shí)現(xiàn)func指向add了。 接著(*func)(1,2)代表執(zhí)行func函數(shù)指針指向的函數(shù),所以結(jié)果等于3。 函數(shù)指針func的返回參數(shù)和形參不一定要和函數(shù)add定義成一樣,func也可以不設(shè)置返回值或者形參,但是一般不建議這樣做,避免引起一些不必要的錯(cuò)誤。 那么這里呢其實(shí)還有一個(gè)知識(shí)點(diǎn)要和大家說一下,我們先來寫一段代碼:
這段代碼調(diào)用函數(shù)指針的時(shí)候沒有使用(*func)(1,2),這種用法也是可以的,執(zhí)行的效果是一樣,那么到底有什么區(qū)別呢? 其實(shí)這個(gè)是編譯器實(shí)現(xiàn)的問題,我們不用去糾結(jié)這種對(duì)我們沒有意義的東西,除非你想去做編譯器。 大家只要記住函數(shù)指針是這樣用的就行了。 后期應(yīng)用時(shí)再把它們多練幾遍,以后做產(chǎn)品都用上,那么基本就熟了,而且產(chǎn)品的程序架構(gòu)也更好了。 七、函數(shù)指針數(shù)組 像字符型,整形都是可以單獨(dú)定義,也可以定義成數(shù)組,同樣函數(shù)指針也可以定義成數(shù)組,同樣,這里我們不講那么多理論上的概念,直接記住怎么定義,怎么使用、用在哪里就行了。 函數(shù)指針數(shù)組定義格式如下: 函數(shù)返回值類型?(*?指針變量名[數(shù)組大小]) (函數(shù)參數(shù)列表); 我們用程序表示如下: 這樣就定義了一個(gè)可以指向3個(gè)函數(shù)的函數(shù)指針數(shù)組。 定義了以后,我們函數(shù)指針需要賦值,賦值的意思就是讓它們指向函數(shù)首地址,一般初始化的方式有兩種。
這是第一種,定義函數(shù)指針數(shù)組的時(shí)候直接初始化。
這是第二種,先定義然后再初始化,這里我們主要是要記住它們這兩種的寫法就行了。 函數(shù)指針數(shù)組賦值以后通過以下代碼來執(zhí)行。
我們可以看到直接寫func[0]();就可以執(zhí)行函數(shù)指針數(shù)值指向的函數(shù)了。 那么這種函數(shù)指針數(shù)組到底有什么用呢? 其實(shí)真正產(chǎn)品應(yīng)用中函數(shù)指針數(shù)組是非常有用的。 我舉一個(gè)例子,寫控制5個(gè)LED燈亮的函數(shù),如果用傳統(tǒng)方式,流程是先要判斷控制哪個(gè)LED,然后再控制指定GPIO口高低電平。 而函數(shù)指針只要一條語句,這就是所謂的代碼的簡(jiǎn)潔、緊湊的特點(diǎn),代碼簡(jiǎn)潔緊湊以后自然也能節(jié)約cpu和內(nèi)存的資源。 下面是演示代碼:
?
編輯:黃飛
評(píng)論