天天看點

C語言 指針(指針的定義、數組和指針、函數和指針.......)

指針

    • 1.記憶體
      • 1.1記憶體是什麼
      • 1.2記憶體的特點
      • 1.3記憶體的随機通路
      • 1.4記憶體的尋址方式
        • 1.4.1 直接通路
        • 1.4.2 間接通路
    • 2.指針變量
      • 2.1 指針是什麼
      • 2.2 為什麼需要指針
      • 2.3 指針的定義與使用
      • 2.4 如何判斷指針變量的類型
      • 2.5 如何判斷指針指向的變量類型
    • 3. 左值與右值
    • 4.野指針和空指針
      • 4.1 野指針是什麼
      • 4.2 野指針的危害
      • 4.3 怎麼避免野指針
      • 4.4 空指針和NULL
    • 5.const關鍵字與指針
      • 5.1 const關鍵字
      • 5.2 const關鍵字的使用
    • 6.指針和數組
      • 6.1 變量名、變量、變量的值、變量類型
      • 6.2 回顧數組
      • 6.3 int a[10]; a a[0] &a &a[0]
      • 6.4 以指針方式通路數組元素
      • 6.5 指針數組與數組指針
        • 6.5.1 指針數組與數組指針的概念
        • 6.5.2指針數組和數組指針的表達式
      • 6.6 指針和二維數組
        • 6.6.1 二維數組
        • 6.6.2 如何使用數組指針通路二維數組
    • 7. 指針和函數
      • 7.1 經典例題 ---交換問題
      • 7.2 函數指針
        • 7.2.1 函數指針的本質
        • 7.2.2 函數指針的定義
        • 7.2.3 typedef關鍵字
    • 8.結構體和指針
      • 8.1 結構體
      • 8.2 結構體指針
    • 9.二重指針(指針的指針)
      • 9.1 二重指針的用法
    • 結語

對指針的概念以及的指針與C語言中數組、函數等的放在一起的使用的講解

文章中所有的程式都是在VS2019下調試并且可以運作的

1.記憶體

1.1記憶體是什麼

記憶體是計算機的重要部件之一。 它是外存與CPU進行溝通的橋梁,計算機中所有程式的運作都在記憶體中進行。

記憶體性能的強弱影響計算機整體發揮的水準。

記憶體(Memory)也稱記憶體儲器和主存儲器,它用于暫時存放CPU中的運算資料,與硬碟等外部存儲器交換的資料。

隻要計算機開始運作,作業系統就會把需要運算的資料從記憶體調到CPU中進行運算。當運算完成,CPU将結果傳送出來。

記憶體的運作也決定計算機整體運作快慢的程度。(百度百科)

1.2記憶體的特點

  • 計算機内的存儲部件,所有指令和資料都儲存在記憶體内
  • 速度快,但是掉電即失
  • 可以随機通路

1.3記憶體的随機通路

計算機最小的存儲機關是位元組而最小的傳輸機關是bit

1byte = 8bit

隻要指明要通路的記憶體單元的位址,就可以立即通路到該單元

位址是一個無符号整數,其字長一般與主機相同

  • 記憶體中的每個位元組都有唯一的一個位址
  • 位址按位元組編号,按類型配置設定空間

    看下面程式(VS2019)

    C語言 指針(指針的定義、數組和指針、函數和指針.......)
    程式輸出:
    C語言 指針(指針的定義、數組和指針、函數和指針.......)
    可以在記憶體中通過a的位址值通路到 a
    C語言 指針(指針的定義、數組和指針、函數和指針.......)

    因為vs2019生成的項目是32位的,是以看到位址值以十六進制顯示有8位。如果以2進制顯示的話就是4X8=32位,而卻32位電腦有4G記憶體

    它的計算方法就是:

    4x1024MBx1024KBx1024B

    ,1024為210,(210)3x22=232.是以是32位

1.4記憶體的尋址方式

1.4.1 直接通路

直接按變量位址(或變量名)來存取變量内容的通路方式

1.4.2 間接通路

通過指針變量來間接存取它所指向的變量的通路方式

C語言 指針(指針的定義、數組和指針、函數和指針.......)

圖中左邊的2000、2002等都是位址值】

C語言的指針就是為了使C語言可以實作間接通路。

2.指針變量

2.1 指針是什麼

      指針的實質就是一個變量和int a那些變量一樣都是變量,隻不過指針變量(指針是指針變量的簡稱)的類型和它們不一樣,指針變量存的值也不一樣

指針變量存儲的是另外一個變量的位址

2.2 為什麼需要指針

  1. 指針的出現時為了實作間接通路(彙編中都有間接通路),間接通路是CPU的一種尋址方式。
    C語言 指針(指針的定義、數組和指針、函數和指針.......)
  2. CPU的間接通路是CPU設計時決定的,這個決定了彙編語言必須實作間接尋址,也決定了彙編之上的C語言也必須實作間接通路
  3. 其他的一些語言,比如Java,沒有用到指針,那是因為語言本身幫我們封裝好了。
  4. 可以通過指針修改函數中的變量值

2.3 指針的定義與使用

* 
 1.指針定義時,*結合前面的類型用于表示要定義的指針類型
 2.指針解引用是,*p表示指向的變量的本身(給出指針指向位址上儲存的值)

	&
 1.&+變量名表示這個變量的位址
           
#include<stdio.h>

int main() {
	int a = 10;		//定義一個整形變量a

	printf("a = %d, &a = 0x%p\n", a, &a);	//列印出a的值和a的位址值
		
	int* p;		//定義一個int* 型的指針,用來指向整形變量
	p = &a;		//給指針變量指派,将變量的位址值賦給指針變量
//或者 int *p = &a;

	*p = 555;	//通過解引用通路值或者改變值
	printf("修改後a = %d, p = 0x%p\n", a, p);	//列印出a修改後的值,以及指針p的值

	return 0;
}
           

運作結果:

C語言 指針(指針的定義、數組和指針、函數和指針.......)

       當然每次給變量配置設定的位址都是随機的,是以下次運作a的位址會不同,但指針的值和a的位址值是相同的

2.4 如何判斷指針變量的類型

       從文法的角度看,你隻要把指針聲明語句裡的指針名字去掉,剩下的部分就是這個指針的類型。這是指針本身所具有的類型

int *ptr;//指針的類型是int*
char *ptr;//指針的類型是char*
int **ptr;//指針的類型是int**
int (*ptr)[3];//指針的類型是int(*)[3]
int *(*ptr)[4];//指針的類型是int*(*)[4]
           

2.5 如何判斷指針指向的變量類型

       當你通過指針來通路指針所指向的記憶體區時,指針所指向的類型決定了編譯器将把那片記憶體區裡的内容當做什麼來看待。

       從文法上看,你隻須把指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉,剩下的就是指針所指向的類型

int *ptr; //指針所指向的類型是int
char *ptr; //指針所指向的的類型是char
int **ptr; //指針所指向的的類型是int*
int (*ptr)[3]; //指針所指向的的類型是int()[3]
int *(*ptr)[4]; //指針所指向的的類型是int*()[4]
           

指針的類型,指針所指向變量的類型以及指針的值是非常重要的,遇到每一個指針變量都要非常清楚這幾個

3. 左值與右值

左值與右值

定義很簡單,在指派運算符左邊的為左值,在右邊的為右值,比如

int a = 0;

int b = 10;

其中a,b都為左值,0,10為右值

當然變量既可以做左值也可以做右值

  • 當一個變量做左值時,編譯器認為這個變量符号的真實含義是這個變量對應的記憶體空間
  • 當一個變量做右值時,這個變量符号的含義就是這個變量的值
int a = 10;
int b = a;
/* 現在再來了解一下指派
	int b = a;
	此時b的含義為b所對應的記憶體空間,a為10。
	這樣就可以把a=10的值直接存到b的記憶體位址中,是以可以進行指派
*/
           

4.野指針和空指針

4.1 野指針是什麼

       在定義指針後沒有進行初始化的指針

4.2 野指針的危害

  • .野指針就是指向随機的位置或者不正确的位置的指針
  • 指針變量在定義時未初始化,值也是随機的,指針變量指向了一個不可指向的位址值
  • 野指針因為指向位址是不可預估的,是以會出現3種情況
    1. 第一種指向不可通路的(作業系統不允通路的位址)結果就是觸發段錯誤
    2. 指向一個可用的,而且沒什麼特别意義的空間(比如曾經使用過但已經不用的棧空間或者堆空間)這時候程式運作不會出錯,對目前程式也沒有什麼危害
    3. 指向了一個可用的空間,而且這個空間在程式正在被使用(比如程式中的一個變量a),野指針的解引用就會改變這個變量a的值,程式就會出錯如果一個代碼很長的項目中出現了這種錯誤将會很難找到。

4.3 怎麼避免野指針

       在對指針進行解引用之前,一定要確定指針指向一個絕對可用的空間

常用的做法:

  1. 定義指針時,同時初始化為NULL
  2. 解引用前,先去判斷這個指針是不是NULL
  3. 指針使用完之後,将其指派為NULL
  4. 在指針使用之前,将其指派綁定給一個可用位址空間
#include<stdio.h>

int main() {

	int a;
	int* p = NULL; 		//定義指針時,同時初始化為NULL

	p = &a;				//在指針使用之前,将其指派綁定給一個可用位址空間
	//中間省略1000行代碼
	if (p != NULL) {	//解引用前,先去判斷這個指針是不是NULL
		*p = 4;
	}

	p = NULL;			//指針使用完之後,将其指派為NULL

	printf("%d", a);

	return 0;
}
           

4.4 空指針和NULL

       指向NULL或者說是0的指針為空指針

NULL在C/C++中的定義為

#ifdenf _cplusplus_			//定義這個符号表示目前是C/C++環境,這句話的意思是如果是C++環境
#define NULL 0				//NULL就是0
#else						//如果是C
#define NULL (void *)0		//NULL為強制類型轉換為void * 0
#endif
           

是以NULL的實質就是0,然後我們給指針賦初值為NULL,其實就是讓指針指向0位址。

為什麼指向0位址?

  • 0位址作為一個特殊位址(我們認為指針指向這裡就表示沒有初始化)
  • 這個0位址在一般的作業系統中都是不可被通路的,如果使用者不檢查是否等于NULL就去解引用,寫出的代碼運作後會直接觸發段錯誤。可以更好的找到自己的代碼錯在哪裡

5.const關鍵字與指針

5.1 const關鍵字

       C語言中const代表着“不可變”,基本和常量一樣不可修改,但是應用場景不一樣。其實const指針指向的值也是可以改變的。是以它起到的僅僅是一個限制作用。可以通過記憶體改變const的值。主要講解const運用在變量、指針以及函數參數。

5.2 const關鍵字的使用

const關鍵字的使用主要在三個方面

  • const與變量
  • const與指針
  • const與函數

1.應用在變量

const char a=‘A’;

a=‘B’; //錯誤,變量a的值不可以修改。

此時代表變量a值不可改變,任何企圖修改a變量值的語句(例如a=20;)都會報錯。

2.應用在指針

1.const int *p;			//p本身不是const的,而p指向的變量是const的
2.int const *p;			//p本身不是const的,而p指向的變量是const的
3.int * const p;		//p本身是const的,p指向的變量不是const的
4.const int * const p;	//p本身是const的,p指向的變量也是const的
           

1.const int *p; //p本身不是const的,而p指向的變量是const的

#include<stdio.h>

int main() {
	int a = 10;
	int b = 0;
	
	int const * p1 = &a;
	int* p2 = &b;
	//*p1 = 5;	//會報錯,報錯原因為常量*p1的值不能改變
	
	printf("a = %d, &a = %p\n", a, &a);
	printf("&b = %p\n", &b);

	p1 = p2;	//會運作,并且p1的位址從指向變量a,變成指向變量b
	printf("*p1 = %d, p = %p", *p1, p1);
	
	/*運作結果:
		a = 10, &a = 0133F944		
		&b = 0133F938
		*p1 = 0, p = 0133F938	
	*/
	
	return 0;
}
           

2.int const *p; //p本身不是const的,而p指向的變量是const的

#include<stdio.h>

int main() {
	int a = 10;
	int b = 2;
	
	const int * p1 = &a;
	int* p2 = &b;
	
	//*p1 = 100;  //不能運作,錯誤原因p1:不能給常量指派
	p1 = p2;	  //可以運作,和情況1相同.
	
	printf("a = %d, &a = %p\n", a, &a);
	printf("&b = %p\n", &b);


	printf("*p1 = %d, p = %p", *p1, p1);
	
	/*
	*	運作結果:
		a = 10, &a = 004FF898
		&b = 004FF88C
		*p1 = 2, p = 004FF88C
*/
	
	return 0;
}
           

3.int * const p; //p本身是const的,p指向的變量不是const的

#include<stdio.h>

int main() {
	int a = 10;
	int b = 2;
	
	int * const p1 = &a;
	int* p2 = &b;
	
	*p1 = 100;  //可以運作,a的值被成功的改成了100
	//p1 = p2;	  //不能運作,"p1":不能給常量指派
	
	printf("a = %d, &a = %p\n", a, &a);
	printf("&b = %p\n", &b);


	printf("*p1 = %d, p = %p", *p1, p1);
	
	/*
	*	運作結果:
		a = 100, &a = 004FF8C4
		&b = 004FF8B8
		*p1 = 100, p = 004FF8C4
*/
	
	return 0;
}
           

4.const int * const p; //p本身是const的,p指向的變量也是const的

#include<stdio.h>

int main() {
	int a = 10;
	int b = 2;
	
	const int * const p1 = &a;
	int* p2 = &b;
	
	//*p1 = 100;  //不能運作,"p1":不能給常量指派
	//p1 = p2;	  //不能運作,"p1":不能給常量指派
	
	printf("a = %d, &a = %p\n", a, &a);
	printf("&b = %p\n", &b);

	printf("*p1 = %d, p = %p", *p1, p1);
	
	return 0;
}
           

       總之,const在 *左邊,表示指針指向的值不可以修改。const在 *右邊,表示指針值(也就是指針指向的位置)不可以修改。

       雖然加了這些很影響代碼的可讀性,但卻大大加強了代碼的安全性。在不該改的變量前加上const關鍵字

3.應用在函數參數

strcat(char *a,char const *b),将參數b指向的字元串,添加到參數a字元串的末尾。
           

此時,參數 *a值可以改變,但是表示參數 *b值不可改變

也就是表示字元串b不能變,隻是單純的和a連接配接到一起

6.指針和數組

6.1 變量名、變量、變量的值、變量類型

       變量是一個位址,這個位址在編譯器中決定具體數值。這個具體數值和變量名綁定(是以變量名又叫辨別符,起一個标志作用。)。變量類型決定了這個位址的延續長度。比如int占四個位元組。變量所在位址所儲存的值叫變量的值。

6.2 回顧數組

(1)從記憶體角度來看,數組變量就是一個一次配置設定多個變量,而且這多個變量在記憶體中的存儲單元是連續的

(2)分開定義多個變量(int a,b,c,d;)和定義個數組(int a[4];)都定義了4個int型變量,而且都是4個獨立的變量。但是,單獨定義出來的變量,位址不一定連續,是以數組的第一個優點就顯現出來了,那就是更容易通路一組資料。

C語言 指針(指針的定義、數組和指針、函數和指針.......)

可以看出數組存儲變量的位址是連續的。

6.3 int a[10]; a a[0] &a &a[0]

  1. a是數組名,
    • a做左值時表示整個數組所有的空間(也就是4X10=40byte的空間),又因為C語言規定數組操作時,要獨立單個元素操作,是以a不能做左值。
    • a做右值時,表示數組首元素的首位址(首位址就是起始位址 也就是相當于&a[0])
  2. a[0]表示數組的首元素,也就是數組的第0個元素
    • a[0]做左值時,表示數組第0個元素對應的記憶體空間
    • a[0]做右值時,表示數組第0個元素的的值
  3. &a,對數組名取位址表示什麼?先看代碼
    C語言 指針(指針的定義、數組和指針、函數和指針.......)
    運作結果都在注釋裡了,我們可以很容易看出,&a,a,a[0]都是數組的首位址,但是他們+1後&a的位址值就會改變。是以&a表示數組的位址,而a[0]和a表示的是數組首元素的首位址,雖然它們的值是一樣的但是表達的意義有所不同,對&a進行加1就會到下一個數組,而不是下一個元素。數組指針的定義

    int (*p)[4] = &a;

    int *p = a;

    這是數組名。

是以&a表示的是數組的位址,又因為多用于二維數組是以又别稱為行指針,&a隻能做右值不能做左值

      4. &a[0],數組首元素的首位址和a[0]相似。

指針能表示兩個次元的資訊,第一個是記憶體當中的位址,第二個是通路記憶體的“尺度”。 數組名 a、數組首元素的位址 &a[0]、數組名取位址

&a,這三者在記憶體中其實是同一個位址,但通路記憶體的尺度有所不同,其中 a 和 &a[0] 是以 int 類型所占記憶體空間為尺度來通路記憶體,而

&a 是以數組 int a[4] 所占記憶體空間為尺度來通路記憶體。

因為C語言規定數組操作時,要獨立單個元素操作,是以a不能做左值。因為這個非常重要的規定,我們在字元串中看到的更為明顯,對一個字元串指派

#include<stdio.h>
#include<string.h>

int main(){
	char str[20];
	char *pStr = str;
	/*
		*pStr = "123";	[Error] invalid conversion from 'const char*' to 'char' [-fpermissive]
		會報錯(報錯原因自己百度,知道這樣不行就可以了);
		是以隻能通過strcpy()函數來對字元串指派 
		
	*/
	strcpy(pStr,"123");
		
	printf("%s",str);
	return 0;
} 
           

6.4 以指針方式通路數組元素

*(指針 + 偏移量)

#include<stdio.h>

int main() {
	int a[4] = { 1,2,3,4 };
	int* p = a;

	printf("a[0] = %d\n", a[0]);
	printf("*(p+0) = %d\n", *p);
	
	printf("a[3] = %d\n", a[3]);
	printf("*(p+3) = %d\n", *(p + 3));

	getchar();
	return 0;

	/*
	運作結果:
		a[0] = 1
		*(p+0) = 1
		a[3] = 4
		*(p+3) = 4
	*/
}
           

6.5 指針數組與數組指針

6.5.1 指針數組與數組指針的概念

       指針數組的實質是一個數組,這個數組裡面存儲的内容全部是指針變量

int *p[5];

       數組指針的實質是一個指針,這個指針指向的是一個數組,通常用于二維數組中,所有又叫行指針

int (*p)[5];

6.5.2指針數組和數組指針的表達式

int a[4] = { 1,2,3,4 };

	int* p[5];			//指針數組
	int(*p)[4] = &a;	//數組指針
	/**
	對于語句“int(*p)[4]”,“()”的優先級比“[]”高,“*”号和 p 構成一個指針的定義,
	指針變量名為 p,而 int 修飾的是數組的内容,即數組的每個元素。
	也就是說,p 是一個指針,它指向一個包含 4 個 int 類型資料的數組a
	*/
           
#include<stdio.h>

int main() {
	int a[4] = { 1,2,3,4 };

	int* p1[5];			//指針數組
	int(*p2)[4] = &a;	//數組指針	
		
	printf("a = %p\n", a);
	printf("p2 = %p\n", p2);
	printf("&a = %p\n", &a);

	printf("a+1 = %p\n", a+1);
	printf("p2+1 = %p\n", p2+1);
	printf("&a+1 = %p\n", &a+1);

	getchar();
	return 0;

	/*
	運作結果:
		a = 006FFA4C
		p2 = 006FFA4C
		&a = 006FFA4C
		a+1 = 006FFA50
		p2+1 = 006FFA5C
		&a+1 = 006FFA5C
	*/
}
           

6.6 指針和二維數組

6.6.1 二維數組

二維數組在C語言程式中經常用來計算矩陣,行列式或者表示一個xy的坐标軸。其實,二維數組在記憶體中所存的資料也是連續的,就相當于把矩陣按一行一行的展開。

比如定義一個

Array[3][5];

我們可以把這個定義了解為。定義了三個數組

Array[0]、Array[1]、Array[2]

,每個數組又有五個元素。這樣就展現出來剛剛看過的數組指針的作用,我們可以通過對數組指針的解引用來通路二維數組中的值

C語言 指針(指針的定義、數組和指針、函數和指針.......)

使用數組指針指向二維數組

#include<stdio.h>
#include<cstring>

int main() {
	int a[2][3] = { 1, 2, 3,
					4, 5, 6		};
	
	int(*p)[3] = &a[0];

	printf("&a[0][0] = %p\n", &a[0][0]);

	printf("p = %p\n", p);

	printf("&a[1][0] = %p\n", &a[1][0]);

	printf("p+1 = %p\n", p + 1);

	getchar();

	
	return 0;

	/*運作結果:
		&a[0][0] = 00ECFC28
		p = 00ECFC28
		&a[1][0] = 00ECFC34
		p+1 = 00ECFC34
	*/
}
           

6.6.2 如何使用數組指針通路二維數組

#include<stdio.h>
#include<string.h>

int main() {
	int a[2][3] = {	1, 2, 3,
					4, 5, 6	};
	
	int(*p1)[3] = a;		//使用數組指針


	printf("a[0][1] = %d\n", a[0][1]);	//使用數組下标通路數組中的元素
	printf("a[1][2] = %d\n", a[1][2]);

	printf("*(*p1 + 1)= %d\n", *(*p1 + 1));		//使用數組指針通路
												//這裡的*(*p1 + 1)相當于*(*(p1 + 0) + 1)
	printf("*(*(p1 + 1) + 2) = %d\n ", *(*(p1 + 1) + 2));

	getchar();

	/*運作結果:
		a[0][1] = 2
		a[1][2] = 6
		*(*p1 + 1)= 2
		*(*(p1 + 1) + 2) = 6
	*/
	return 0;
}
           

       從程式中可以看出,使用數組指針可以進行通路二維數組。使用下标來通路數組元素都比較熟悉。程式中使用數組指針通路元素有兩句代碼一個是

*(*p1 + 1)

,另一個是

*(*(p1 + 1) + 2)

。這兩句話該如何了解。

       我們一步一步來看,主要分析

*(*(p1 + 1) + 2)

,第一步是

*(p1 + 1)

,p1是數組指針它現在的值是數組的位址也就是首元素的首位址,

int(*p1)[3] = a; //使用數組指針

這是p1的定義,現在對p1+1也就是移動一個數組(3int元素的空間)然後對它解引用就是 a[1]的值,也就是二維數組a中的第二個數組的首位址。然後

*(p1 + 1) + 2

這樣指針就會指向a[1]中第3個元素,最後

*(*(p1 + 1) + 2)

對它解引用就得到了它的值6.

總結:使用數組指針通路二維數組

int a[2][3];
int (*p)[3] = a;
a[i][j] == *(*(p + i) + j);
           

7. 指針和函數

7.1 經典例題 —交換問題

#include<stdio.h>
#include<cstring>

void swap1(int, int);
void swap2(int*, int*);

int main() {
	int x = 3, y = 5;
	
	/*swap1(x, y);*/		/*執行swap1(),雖然成功改變了函數中x和y的值,
							  但是原本主函數中的x和y的值依然沒有變*/
	
	swap2(&x, &y);		//使用swap2(),成功改變了主函數中的x和y的值
	printf("x = %d, y = %d\n", x, y);

	return 0;
}

void swap1(int x, int y) {
	int temp = 0;
	temp = x;
	x = y;
	y = temp;
	printf("函數中的x = %d, y = %d\n", x, y);
}

void swap2(int* x, int* y) {
	int temp;
	temp = *x;
	*x = *y;
	*y = temp;
}
           

為什麼swap1不能改變主函數的值但swap2函數可以改變?

通過對代碼的分析可以了解,首先了解函數中的形參和實參

  • 形式參數(形參):定義函數名和函數體時需要用的參數,目的是用來接收調用該函數時傳遞的參數。
  • 實際參數(實參):傳遞給被調用函數的值。
差別 形式參數(形參) 實際參數(實參)
1 :形參隻能是變量,在被定義的函數中,必須指定形參的類型。 實參可以是常量、變量、表達式、函數等,
2 沒有确定的值 無論實參是何種類型的量,在進行函數調用時,它們都必須具有确定的值,以便把這些值傳送給形參。 是以應預先用指派,輸入等辦法使實參獲得确定值。
3 形參變量在未出現函數調用時,并不占用記憶體,隻在調用時才占用。調用結束後,将釋放記憶體。 開辟記憶體存儲資料

       通過上面這個表格可以得知,雖然在主函數中将x,y的值傳給了swap1()中的x,y但swap1中的x,y隻能在函數調用時存在,是以函數調用完傳回主函數并不會影響主函數中x,y的值

       但是swap2()函數傳入的是x和y的位址值。通過對位址值上的值進行修改,是以,不管x,y在哪個位置,x和y的值都會被改變

7.2 函數指針

7.2.1 函數指針的本質

  1. 函數指針的實質還是指針(指針變量),占四個位元組(在32位系統中,所有指針都是4個位元組)
  2. 函數指針、數組指針、普通指針本質上沒有差別都存儲了位址值,隻不過他們指向的東西不同
  3. 函數的本質是一段代碼,這一段代碼在記憶體中是連續分布的(函數的{}中所有的語句),是以對于函數來講,很關鍵的就是函數中的第一句代碼所在的位址,這個位址就是所謂的函數位址,在C語言中用函數名這個符号來表示
  4. 函數指針其實就是一個普通變量,這個普通變量的類型是函數指針類型,它的值就是函數位址(也就是它的函數名這個符号在編譯器中所對應的值)

7.2.2 函數指針的定義

#include<stdio.h>
#include<cstring>

void func(void);

int main() {
	//也可以寫成void (*pFunc)(void) = func;
	void (*pFunc)(void);			//func函數對應的函數指針類型 viod (*)(void);
	pFunc = func;
	printf("func = %p\npFunc = %p\n", func, pFunc);/*	func = 000513C0
														pFunc = 000513C0
														func和pFunc所指向的位址相同都表示函數
														func的函數位址
														*/
	pFunc();		/*	運作結果: ...
						
						用函數指針可以調用該函數,代替函數名的作用
					*/

	return 0;
}

void func(void) {
	printf("...\n");
}

 

           
  • 函數名是數組名的差別:函數名做右值時,加不加&效果和意義都是一樣的,但是數組名會不一樣

函數指針的舉例

#include<string.h>
#include <stdio.h>

//vs準備棄用strcpy的,安全性較低,是以微軟提供了strcpy_s來代替,如果想繼續使用strcpy的,main前面加上
#pragma warning(disable:4996)

int main(){
	//char *strcpy(char *dst, const char *src); strcpy函數原型
	
	char str1[20], str2[] = "123";
	char* (*p)(char*, const char*) = strcpy;
	
	p(str1, str2);		//和直接使用strcpy()的效果一樣。
	printf("a = %s\n", str1);
	return 0;
}
           

通過上面這段代碼可以看到,如果定義一個函數指針,這個函數的聲明比較複雜,那麼在定義多個時就會很費時間,是以這個時候我們可以使用typedef來進行重命名

7.2.3 typedef關鍵字

  1. 用來定義或者重命名類型
  2. 編譯器的原生類型(基礎資料類型,比如int,double),使用者自定義的類型,程式員自己定義的(結構體類型,數組類型)
  3. 如果自定義類型太長,定義多個變量時會浪費時間,是以使用typedef來重命名一個短點的名字
#include<string.h>
#include <stdio.h>

//vs準備棄用strcpy的,安全性較低,是以微軟提供了strcpy_s來代替,如果想繼續使用strcpy的,main前面加上
#pragma warning(disable:4996)

//使用typedef将原來很長的 char* (*)(char*, const char*)轉換為pType;
typedef char* (*pType)(char*, const char*);

int main(){
	//char *strcpy(char *dst, const char *src); strcpy函數原型
	
	char str1[20], str2[] = "123";
	pType p = strcpy;
	
	p(str1, str2);		//和直接使用strcpy()的效果一樣。
	printf("a = %s\n", str1);
	return 0;
}
           

       可以很明顯的看出,這樣定義多個函數指針的時候會節約很多。指針函數沒什麼好說的,無非就是傳回值是一個指針也就是傳回一個位址的函數。

typedef char* (*pType)(char*, const char*);	//函數指針
typedef char* (*pType[5])(char*, const char*);	//函數指針數組
typedef char* (*(*pType)[5])(char*, const char*);	//函數指針數組指針
           

出一個小問題:

int (*(*p5)(int*))[5]; 該怎麼了解?

  • 點這裡看答案

8.結構體和指針

8.1 結構體

    首先我們為什麼要用到結構體,我們都已經學了很多int char …等類型還學到了同類型元素構成的數組,但是,在我們實際應用中,每一種變量進行一次聲明,再結合起來顯然是不太實際的,類如一位學生的資訊管理,他可能有,姓名(char),學号(int)成績(float)等多種資料。如果把這些資料分别單獨定義,就會特别松散、複雜,難以規劃,是以我們需要把一些相關的變量組合起來,以一個整體形式對對象進行描述,這就是結構體的好處。

    隻有定義了結構體變量才配置設定位址,而結構體的定義是不配置設定空間的。

    結構體中各成員的定義和之前的變量定義一樣,但在定義時也不配置設定空間。

在連結清單中,我們需要使用stdlib頭檔案裡的malloc函數來配置設定位址。這樣可以通過free函數進行删除,比較好管理。現在不需要删除,是以不使用malloc。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

struct Student {	//定義一個結構體Student
	int age;		//int型 年齡
	char name[20];	//姓名	
	float score;	//得分
};

int main() {
	struct Student s1;		//定義一個結構體變量s1
	scanf("%d%s%f", &s1.age, s1.name, &s1.score);

	printf("學生的姓名:%s\n學生的年齡:%d\n學生的成績:%.2f\n", s1.name, s1.age, s1.score);

	return 0;
}
           
C語言 指針(指針的定義、數組和指針、函數和指針.......)

    當然結構體還可以定義結構體數組的變量,這裡不做過多解釋。可以看到如果想要通路到結構體中的元素的話,需要用

結構體變量.元素

的形式,其中的 . 又被叫做 . 運算符。如果通路的元素是結構體的話則繼續通路一直通路到最低一級的成員變量

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

struct Student {	//定義一個結構體Student
	int age;		//int型 年齡
	char name[20];	//姓名	
	float score;	//得分
	struct monitor{	//班長
		char sex = 'F';
	}m1;	//定義一個班長的結構體變量m1
};

int main() {
	struct Student s1;		//定義一個結構體變量s1
	printf("班長的性别:%c", s1.m1.sex);
	return 0;
}
           
C語言 指針(指針的定義、數組和指針、函數和指針.......)

8.2 結構體指針

通常情況下,結構體指針一般用于連結清單、樹等資料結構的書寫

1.結構體指針的定義方法

1)struct 結構體名 *指針;

2)直接在定義結構體的時候添加結構體指針的聲明

//在main()方法中定義
struct student *p1;//定義結構體指針

或者
struct student{
	int num;
	char name[20];
	char sex;
}*p2;

struct student{
	int num;
	struct student *stu;
};
           

2.利用結構體指針的通路結構體變量(輸出)

結構體指針的通路變量方法

1)p->結構體成員;

2)(*p).結構體成員;

#include<stdio.h>
#include<stdlib.h>

struct student {
	int num;
	char name[20];
	int age;
};

int main() {

	struct student s1 = { 1,"zzy",18 };
	struct student* p = &s1;

	printf("學号:%d\n姓名:%s\n年齡:%d\n", (*p).num, (*p).name, (*p).age);
	printf("學号:%d\n姓名:%s\n年齡:%d\n", p->num, p->name, p->age);
	
	return 0;
}
           

最後通路結果一樣,隻要注意使用指針時必須要使用->來通路

9.二重指針(指針的指針)

(1)二重指針也是指針變量,和普通指針的差别就是它指向的變量類型必須是一個一重指針,二重指針其實也是一種資料類型,編譯器在編譯時會根據二重指針的資料類型來做靜态類型檢查

(2)沒有二重指針其實也可以(可以用一重指針代替),但是之是以會有二重指針(或者函數指針、數組指針)就是為了讓編譯器了解這個指針被定義的時,定義它的程式員希望這個指針被用來指向什麼東西。編譯器知道指針類型後可以幫我們做靜态類型檢查,編譯的這種靜态類型檢查可以輔助程式員發現一些隐含性的錯誤,這是C語言給程式員提供的一種編譯時的查錯機制

9.1 二重指針的用法

(1)二重指針指向一重數組的位址

(2)二重指針指向指針數組

(3)實際中,二重指針的運用比較少,大部分就是和指針數組一起使用

(4)有的函數在傳參時,為了通過函數内部改變外部的一個指針變量(用的很少)

#include<stdio.h>

void func(int** p, int* pCh) {
	*p = pCh;
}

int main() {
	int a = 0;
	int b = 100;

	int* p = &a;
	printf("p: %d\n", *p);	//列印的結果為p: 0 此時還是a的位址

	func(&p, &b);	//通過函數func後改變成b的位址
	printf("func p: %d\n", *p);	//func p: 100

	return 0;
}
           

這裡通過二重指針改變了主函數中的p指針的值,但是實踐中用的很少

結語

    通過本篇部落格,大家可以了解到C語言中指針的大部分用法。當然還有一些更深奧的東西。也可以展現到C語言中指針是非常非常非常重要的!在遇到一個指針時,不要覺得它很複雜,記得先判斷指針的類型,指針指向的變量的類型,然後在分層分析指針究竟指向了什麼。當然多敲代碼是最重要的…

函數指針答案

int (*(*p5)(int*))[5]; //——p5是個指針,指向一個具有一個int *型形參的函數,這個函數傳回一個指向具有5個int元素的數組的指針。

繼續閱讀