一、指針簡介
什么是指針?相信大家對這個問題其實并不陌生,對指針的概念也不會很模糊,在這里我也大概介紹一下。
1.1、指針定義
指針 其實可以理解為某一對象的內存地址,指針變量就是用來存放內存地址的變量。由此我們發現,其實指針和地址是一種相互對應關系。
1.2、指針變量
指針變量 上面我們談到指針變量是存放地址的變量,故此我們也可稱指針變量我地址變量。在聲明指針變量時,我們通常用數據類型符++標識符(即變量名)的格式定義。如int p1,char* p2等形式。 需要說明的是類型說明符表示指針變量所指向變量的數據類型;**" * "** 表示這是一個指針變量;變量名表示定義的指針變量名,其值是一個地址。例如:int*p1則表示 p1 是一個int型的指針變量,它的值是某個整型變量的地址。
1.2.1、指針類型大小
在 C/C++語言中,指針一般被認為是指針變量,指針變量的內容存儲的是其指向的對象的首地址,指向的對象可以是變量(指針變量也是變量),數組,函數等占據存儲空間的實體。
以上我們知道指針變量是存儲地址的,且其可以指向char型,int型等存儲在占據一定空間的實體。以32位機為例,在32位機的存儲中,int型,char型變量都分別占據不同的字節。
在這里插入圖片描述
那么指針變量的大小如何呢?通過驗證我們發現其實指針類型的占位都是4/8個字節,如下圖所示。由此我們知道,地址值在內存中的存儲空間占4/8個字節,這也是指針類型的大小了。
在這里插入圖片描述
1.2.2、指針注意事項
我們在對指針進行聲明時,切記不可把一個數賦值給指針變量。如下:
?
????int*?p?=?200;//erro ????int?a?=?20; ????int*?p1; ????*p1?=?a;//erro ????//或者 ????int?*p2; ????p2?=?200;//erro ????printf("%d ",*p); ????printf("%d ",*p1); ????printf("%d ",*p2); 12345678910在這里插入圖片描述
?
在以上例子中分別定義了一個指針變量 p,p1,p2,但是不能直接把 200 賦值給指針變量 p2,也不可把一個賦值為20的整型變量 a,再賦值給指針變量p1,p1中只能用來存放整型變量的地址,而不能直接把整型變量a賦值給這個指針變量 p1。但可以把a的地址賦值給 p1;即可改成 **p1 = &a;**同時直接在聲明指針變量p時也是不可以賦給其200這個整型數值的。
注意,這里雖然只有兩個報錯,原因是在wind環境下,語法的不嚴格導致的,在Linux環境下其實是報錯的情況。
1.2.3、野指針
在使用指針時,野指針是我們需要避免的一個重要點。
C 語言中指針初始化是指給所定義的指針變量賦初值。指針變量在被創建后,如果不被賦值,他的缺省值是隨機的,它的指向是不明確的,這樣的指針形象地稱為 “ 野指針 ”。野指針是很危險的,容易造成程序出錯,且程序本身無法判斷指針指向是否合法。
指針變量初始化時避免野指針的方法:可以在指針定義后,賦值 NULL 空值,也可寫成如下形式:
p=0 或 p=‘?’
這兩種形式和 p = NULL 是等價的
如int * p;
p = NULL;
上面兩行代碼的含義是,指針變量 p 被賦值為空。雖然定義了一個指針變量,但是它并不指向任何存儲空間。
以上是我們在使用指針時需要注意的內容。同時也應該避免指針越界等問題,這以后細說。
二、函數
2.1、函數簡介
這里的函數是同我們理解中的數學函數有一定區別。計算機上的函數一般是指一段可以直接被另一段程序或代碼引用的程序或代碼。也叫做子程序,或在OOP中稱為方法,如java等面向對象的程序設計語言。
在C語言中,函數的定義同大多數變量定義意義,都需要聲明,給定類型等且在定義非void型函數時,需要有返回值。值得注意的是,函數名后的()內可進行參數的定義,如定義一個int型的main函數,可接收兩個參數時,參照如下定義:
?
//?定義main函數 int?main(int?x,int?y) { ?printf("%d ",x+y); ?return?0; } 123456
?
2.2、函數地址
函數有地址嗎?我們先來看下面這幾行代碼。在main函數中,通過調用add函數得到一個返回值,這個返回值被在main函數進行打印。我們知道,在聲明一個變量時,內存空間都會為該變量分配一個相應地址值,同樣地,在聲明函數使,內存也會為函數分配一個地址值,如下代碼中的add函數,其在64位機下的內存地址值為0000000000401550;
?
//定義加法函數add() int?add(int?x,int?y) { ?int?z?=?0; ?z?=?x?+?y; ?return?z; } //?定義main函數 int?main() { ?int?a?=?10; ?int?b?=?20; ?printf("%d ",add(a,b));//30 ?printf("%d ",&add);//0000000000401550,這是個地址值,這里用&add和直接用add是一樣的,沒區別。不同于數組! ?return?0; } 12345678910111213141516
?
綜上我們可以很清楚的知道,在函數調用的時候其實就像在主函數中找到了被調函數的地址作為入口進行傳參,進而能很好幫我們解決很多問題。而函數指針就是基于這樣一個思想來實現指針變量存放函數地址的。
2.3、回調函數
最為編程語言中的一種機制,回調函數就是一個被作為參數傳遞的函數。在C語言中,回調函數只能使用函數指針實現。
對于回調函數,經典的如qsort()函數,其格式如下:
?
void?qsort(void*?base, ????????????????size_t?num, ????????????????size_t?width, ????????????????int(*cmp)(const?void?*e1,const?void*?e2) ????????????????);? 12345
?
關于上述代碼,這里不作細講。
回調函數在實際中有許多作用。假設有這樣一種情況:我們要編寫一個庫,它提供了某些排序算法的實現(如冒泡排序、快速排序、shell排序、shake排序等等),為了能讓庫更加通用,不想在函數中嵌入排序邏輯,而讓使用者來實現相應的邏輯;或者,能讓庫可用于多種數據類型(int、float、string),此時,可以使用函數指針,并進行回調。
可見回調函數于函數指針的關系極為密切。在此介紹回調函數,當然也是方便下文很好的理解函數指針的一些應用,對于回調函數感興趣的讀者朋友可以查閱相關資料進行了解。
注意:以上說明僅為本次文章做個鋪墊,下面進入正題。
三、函數指針
顧名思義, 函數指針就是函數與指針的結合,相信大多數初學者可能也同我一樣,開始接觸函數指針時會疑惑,這是函數還是指針呢?下面我們進行淺析。
3.1、函數指針定義
函數指針是指向函數的指針變量。本質上“函數指針”就是指針變量,這點是需要強調的,只不過該指針變量指向函數。其類型就由函數類型所決定。函數在C編譯過程中,編譯器會給每一個函數分配一個入口地址,該入口地址就是函數指針所指向的地址。有了指向函數的指針變量后,可用該指針變量調用函數,就如同用指針變量可引用其他類型變量一樣,在這些概念上是大體一致的。函數指針有兩個用途:調用函數和做函數的參數。
3.2、函數指針聲明
函數指針的聲明問題也是一直困擾大家的一個點,就如同對數組指針的聲明一樣。需要明確的是,函數指針其本質是指針,所以在聲明時應該注意,必須體現指針這一概念。下面我們來看這兩個聲明的異同。
?
//int?fun(int??,int??);//此為一個函數,這點是毫無疑問的,關鍵在于以下兩條語句 int??*?fun(int??,??int???); int??(*fun)(int??,?int??); 123
?
以上“函數”的聲明唯一的不同點在于,*fun有無括號的問題,正因為這個小括號,讓我們認識了函數和指針組合的強大。
首先,對于以上兩種寫法的問題,根本在于括號的優先級。第一條語句在聲明中,*fun沒有括號,其優先與fun后面的括號結合,在不考慮 * 的情況下,這是一個指向int型的函數,但有了 * 后,* 與int結合,形成了 int* 類型,此時這個fun函數指向的是 int* 類型。這時我們稱第一條語句為指針函數,其是指向指針的函數,這后續再討論。
有如上基礎,對于第二條語句我們就很容易理解了。通過將 fun 與括號中的 “ * ” 強制組合在一起,表示定義的 fun 是一個指針,然后與下面的 “ ( ) ” 再次組合,表示的是該指針指向一個函數,括號里表示為 int 類型的參數,最后與前面的 int 組合,此處 int 表示該函數的返回值。因此,fun 是指向函數的指針,該函數的返回值為 int。函數指針與指針函數的含義大不相同。函數指針本身是一個指向函數的指針。指針函數本身是一個返回值為指針的函數。
綜上,函數指針的聲明就顯而易見了,其格式為:
返回值的數據類型 ( * 函數名)(參數1,參數2,…);
如果上述感覺有點啰嗦,我們可以這樣來理解:
通過上述代碼可知函數有一個地址值,那么如果我們想存放這個地址值,該如何存放呢。可能有人會想到的是直接用函數來接收,如int pf(int x,inty) = add;用函數來接收函數,確實是個不錯的想法,但我們說地址是一個指針變量,應該用指針來接收地址值,所以很容易想到直接在int 后加上 * 這個符號表示指針,這時有int *pf(int x,inty) = add;但真的是這樣嗎?
有人可能會發現pf(int x,inty) = add;其實是一個函數,返回類型是int*,這認同我們前面認識的其他指針類型一樣。沒錯,這就是關鍵。由此我們探究出了函數指針的寫法,即先讓pf變量是個指針,用來接收add函數的地址,再讓其指向int型。所以得到了int (*pf)(int x,inty) = add;這樣一個完美的寫法。
3.3、函數指針的應用
上面我們提到,函數指針有兩個用途,即調用函數和做函數的參數,下面我們以此進行一些淺析。
3.3.1函數指針的調用
當我們需要把一個函數作為參數傳給其他參數的時候就必須使用函數指針才能很好的完成。下面的程序說明了函數指針調用函數的方法:這段代碼中主要執行過程主要是把add函數的地址賦給pf指針,而后通過(*pf)進行解引用找到原來的add函數,再對其進行傳參3和5,最后將結果賦給c,再進行打印。得到下圖結果。
?
//定義加法函數add() int?add(int?x,int?y) { ?int?z?=?0; ?z?=?x?+?y; ?return?z; } //?定義main函數 int?main() { ?int?(*pf)(int?x,int?y)?=?add; ?int?c?=?(*pf)(5,8); ?printf("%d ",c);//13 ?return?0; } 123456789101112131415
?
pf 是指向函數的指針變量,【(*pf)(int x,int y)這個里邊的參數 x和y 可以省略】所以可把函數 add() 賦給pf作為 pf 的值,即把add()的入口地址賦給pf,之后就可以用pf對該函數進行調用,實際上pf和add都指向同一個入口地址,不同就是pf是一個指針變量,它可以指向任何函數。在程序中把哪個函數的地址賦給它,它就指向哪個函數。而后用指針變量調用它,因此可以先后指向不同的函數。不過注意,指向函數的指針變量沒有++和–運算,用時要小心。
3.3.2函數的傳參
上面提到函數指針不僅可以調用,還能進行傳參。函數傳參的本質就是把需要傳的函數當成一個參數,然后傳給被調函數使用。下面依然通過代碼來演示,并得到下圖結果。
?
//定義加法函數add() void?chuancan(int?(*pf)(int?x,int?y),int?x,int?y) { ?printf("這是函數指針傳的使用演示 "); ?int?c?=?pf(x,y); ?printf("%d ",c);//5 } int?add(int?x,int?y) { ?int?z?=?0; ?z?=?x?+?y; ?return?z; } //?定義main函數 int?main() { ?int?(*pf)(int?x,int?y)?=?add; ?chuancan(pf,2,3); ?return?0; } 1234567891011121314151617181920
?
我們把函數pf當做一個參數傳到了函數chuancan里面去,也可以實現對函數pf的調用,顯然,這樣的做法讓我們見識到了函數指針的極大魅力。這就意味著,當我們有很多類似的數量極大的函數時,我們怎么管理和使用,這是個棘手的問題,這時候我們可以用函數指針去管理和調用他們,把他們都裝進一個函數指針數組,這無疑讓我們的算法實現了優化。
在這里插入圖片描述
四、指針函數
當讀者朋友讀到這一節時,相信你對指針函數已經不再陌生,因為在前文我們也已然提過這一概念。接下來我們將對指針函數作進一步淺析。
4.1指針函數的定義
如你所想,指針函數,其實就是帶指針的函數,其本質就是函數,這點并不稀奇,稀奇的是它的返回類型是某一類型的指針。
4.2 指針函數的聲明
指針函數的聲明如下:類型標識符?*函數名(參數表)?;
?
int??*?fun(int??,??int?,...?);//這是一個指針函數 1
?
下面是關于指針函數的相關聲明格式解釋。
在 類型名 *函數名(函數參數列表); 格式中,后綴運算符括號“()”表示這是一個函數,其前綴運算符星號“ * ”表示此函數為指針型函數,其函數值為指針,即它帶回來的值的類型為指針,當調用這個函數后,將得到一個“指向返回值為…的指針(地址),“類型名” 表示函數返回的指針指向的類型”。
“(函數參數列表)”中的括號為函數調用運算符,在調用語句中,即使函數不帶參數,其參數表的一對括號也不能省略。其示例如下:
int *pfun(int, int);
由于“*”的優先級低于“()”的優先級,因而pfun首先和后面的“()”結合,也就意味著,pfun是一個函數。即:
int *(pfun(int, int));
接著再和前面的“ * ”結合,說明這個函數的返回值是一個指針。由于前面還有一個int,也就是說,pfun是一個返回值為整型指針的函數
相信到此讀者朋友應該對指針函數的定義,聲明及其注意點有了深度的認識,需要注意的是:函數指針聲明為指針,它與變量指針不同之處是,它不是指向變量,而是指向函數。
下面將對指針函數應用方面作進一步的淺析。
4.3指針函數的應用
指針函數既然本質上是函數,那么其用法與函數的一般用法基本無異。都需要對其進行聲明,調用等。請看一下例子,結果如下圖:
?
char?*?my_strcat(char*?dest,const?char?*?src) { ????char*?ret?=?dest;? ????assert(dest?!=?NULL); ????assert(src); ????//1.找到目的字符串的? ????while(*dest?!=?'?') ????{ ????????dest++; ????} ????//2.追加 ????while?(*dest++?=?*src++) ????{ ????????;/*?code?*/ ????} ????return?ret; ???? } int?main() { ????char?arr1[30]?=?"abcdefghi";//這里目的地要足夠大,否則追加時容易出錯 ????char?arr2[]?=?"abtc"; ????char?arr11[]?=?"abcdefghi"; ????char?arr22[]?=?"abtc"; ????char*?arr3?=?my_strcat(arr1,arr2); ????printf("這是自記實現的追加字符串結果:%s ,這是系統調用strcat函數追加的結果:%s ",arr3,strcat(arr11,arr22)); ????return?0; } 12345678910111213141516171819202122232425262728
?
在本例中,聲明char * my_strcat(char* dest,const char * src)這樣一個返回值類型為char*的指針函數,參數為char* dest,const char * src,該函數用來實現對兩個字符串的后綴字符追加問題。當我們在主函數調用my_strcat函數時,其實和一般函數調用并無區別,值得一提的是,當我們想要把被調函數返回給某個變量時,記住返回類型即可,如char* arr3 = my_strcat(arr1,arr2);
追加字符串
指針函數的返回類型可以是任何基本類型和復合類型。返回指針的函數的用途十分廣泛。事實上,每一個函數,即使它不帶有返回某種類型的指針,它本身都有一個入口地址,該地址相當于一個指針。比如函數返回一個整型值,實際上也相當于返回一個指針變量的值,不過這時的變量是函數本身而已,而整個函數相當于一個“變量”。
當然了,在函數調用過程中,我們會發現,每當封裝一個函數,相當于把這個函數給定死了,今后再使用時發現只能使用固定返回類型的函數,這是令人頭大的問題,為此c語言還是很友好的,在這里我們簡單介紹遇到這種情況該如何解決。
一般而言,為了讓被調函數具有一般通用性,我們可以將其返回值類型聲明為 void * ,這樣就極大方便我們在今后的使用了。
五、函數指針與指針函數的區別
經過上述探索,我們悄然發現,函數指針和指針函數的區別依然明了。在這里需要總結一下即可:
1、指針函數: 即帶指針的函數,其本質是一個函數,函數返回是某一類型的指針。聲明如ret * fun(ret ,ret ,...);
2、函數指針: 即指向函數的指針變量,其本質是一個指針變量。指向函數的入口地址,可以通過它來調用函數。聲明如ret (*fun)(ret ,ret ,...);
綜上,二者在定義,寫法及用法方面都存在不同,至于對二者更進一步的學習,還請各位讀者朋友查詢相關資料進行深度學習了。鑒于個人技術學淺,存在不足之處,歡迎指教。
評論
查看更多