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