首頁 > 軟體

C語言指標和陣列深入探究使用方法

2022-08-09 14:01:46

1、陣列引數和指標引數

1.1 一維陣列傳參

這裡在前幾期我們已經初略的見識過了,但是這裡我們要提一個概念,陣列給函數傳參是會發生降維的,降維成什麼呢?我們看程式碼:

這裡通過列印形參的大小,發現是 4,其實也不奇怪,目前我們是 32 位元運算環境,所以一個指標也就是 4 個位元組,所以從這裡我們可以看出,陣列傳參的時候,是發生降維的,陣列名除了 &陣列名 和 sizeof(陣列名) 其他所有情況都是首元素地址,所以本質上我們是降維成指向其陣列內部元素型別的指標,為什麼呢,因為他是陣列首元素的地址,首元素是int 型別,所以傳過去的也是對應的 int 型別的指標,同理我們需要拿同型別指標變數來接收,所以本質上我們 p 變數中儲存的就是 arr[0] 的地址!

我們在看一段程式碼:

void printSize(int arr[100], int n)
{
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("n");
}
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    printSize(arr, 10);
    return 0;
}

如上這段程式碼有問題嗎?其實是沒有問題的,實際傳遞陣列大小與函數形參指定的陣列大小沒有關係,因為他已經是指標了,只是存取方式被打通了,第二期我們有講過,那麼既然如此,我們也可以不要裡面的元素個數直接成 printSize(int arr[], int n) 這樣也是可以的,至少不會讓閱讀者感到誤會。

1.2 一級指標傳參

void print(int* p, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", *(p + i));
	}
    printf("n");
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一級指標p,傳給函數
	print(p, sz);
	return 0;
}

這裡我們需要討論一個問題,指標作為引數需要發生拷貝嗎?

答案是需要的,因為指標變數也是變數,在傳參上得符合變數的要求,也就是在棧上開闢空間,同時我們也知道,main 函數中的 p 是一個區域性變數,它只在 main 函數內有效,所以只能對實參做一份拷貝,並傳遞給被呼叫的函數。

1.3 二維陣列引數和二級指標引數

這個例子我們發現,二維陣列傳參的時候也會發生降維,如何理解呢?上一期我們用了陣列指標來接收了二級指標傳參,這裡我們就來做一個總結:

任何維度的陣列,傳參的時候都要發生降維,降維成指向其內部元素型別的指標,那麼,二維陣列內部元素我們可以看成是多個一維陣列,所以,二維陣列傳參其實是降維成指向一維陣列的指標,而這裡的 arr 也就代表著首元素地址,也就是第一行一維陣列的地址!這也就是我們之前可以拿指標陣列來接收的原因了。

這裡我們還是可以省略第一個下標的值:char arr[][4] ,但是為什麼不能省略第二個下標值呢?我們可以想一下,之前寫用陣列指標接收是這樣寫的 char (*p)[4] ,上面我們提到過,int arr[] 用來接收實參,它本質上就是個指標,所以 char arr[][4] 本質上是個陣列指標,從他的角度看,他指向了一個存放 4 個 char 型別元素的陣列,所以如果省略了第二個下標則指標型別不明確!

1.4 野指標的問題

這個問題其實很多書中都會有寫,我們這裡就簡單提一下:

  • 指標未初始化,預設是隨機值,如果直接存取會非法存取記憶體
  • 指標越界存取,當指標指向不屬於我們的記憶體,p就是野指標
  • 指標指向的空間被釋放,如果動態開闢的記憶體被釋放但是指標沒置NULL,就會形成野指標,他仍然記錄者已經不屬於他的記憶體
  • 返回區域性變數的地址,如果我們一個函數被銷燬後但是仍然返回函數內區域性變數的地址也會造成也會造成野指標

2、函數指標

指標變數是用來儲存地址的,那麼函數有地址嗎?有!函數是由我們自己寫的一些語句構成的,程式執行的時候就會把定義好的函數中的語句呼叫到記憶體中去,那麼函數程式碼在記憶體中開始的那個記憶體空間的地址也就是函數的地址!

這裡我們也能發現,函數是有地址的,而且 &函數名 和 單獨的函數名 都能表示函數的地址。

那麼我們如果想把函數的地址存起來該如何做呢?有了上面學習指標陣列和陣列指標的經驗,其實函數指標也很好理解:

void (*pfun) () 其實這麼寫可以了,我們來解讀下這句程式碼:pfun 先和 * 結合,正如我們之前所說,就能說明他是一個指標,指向的是一個無引數並且無返回型別的函數。

那我們如果要指向一個 int add (int x, int y) 這樣的一個函數,我們應該如何定義函數指標呢?

int (*p) (int, int) 如同上面一樣,首先要保證 p 是指標,所以帶上括號,指向的是一個返回值為 int 引數為 int int 的函數。

接下來我們來使用函數指標,使用方法跟函數一樣,直接把指標變數名當函數名使用即可:

讓我們來看一道有意思的題:

int main()
{
	(*(void (*)())0)();
	return 0;
}

首先這道題的解法肯定先從 0 下手,我們先分析,0 前面的 (void (*) ()) 是什麼?這很明顯是一個函數指標型別,所以可以理解成把 0 強轉成函數指標,也就是把 0 當成了一個函數的地址,然後再 * 參照這個地址,也就是找到 0 地址處的函數進行呼叫。所以此程式碼就是一次函數呼叫,被調函數無參,返回型別是void。

3、函數指標陣列

有了上面的學習就很好理解了,無非就是儲存函數地址的陣列,那麼它的語法格式是什麼呢?

int (*arr[10]) (int, int)

這裡我們可以分析到:首先 arr 跟 [ ] 先結合,所以它是個陣列,這個陣列的每個元素是 int (*) (int int) 型別的函數指標,它的作用主要是轉移表,那我們這裡就簡單用一下即可

假設我們需要兩個整數的 + - * / 我們寫完了四個函數是不是可以放到一個陣列裡,然後通過存取陣列下標就能呼叫我們想用的函數了:

int add(int x, int y)
{
	return x + y;
}
int sub(int x, int y)
{
	return x - y;
}
int mul(int x, int y)
{
	return x * y;
}
int div(int x, int y)
{
	return x / y;
}
int main()
{
	int (*arr[4]) (int, int) = { add, sub, mul, div };
	printf("加法:%dn", arr[0](1, 2));
	printf("減法:%dn", arr[1](5, 2));
	printf("乘法:%dn", arr[2](3, 3));
	printf("除法:%dn", arr[3](6, 2));
	return 0;
}

4、指向函數陣列的指標

看到這可能有的小夥伴覺得越來越套娃了,但其實這個也很好理解,無非就是一個指標指向了一個陣列,陣列每個元素是函數指標,這裡我們簡單瞭解下概念即可,用的其實也不是很多,當別人如果寫了這種程式碼我們能看懂就行:

函數指標如何定義:

int test(char* str)
{
	if (str == NULL) {
		return 0;
	}
	else
	{
		printf("%sn", str);
		return 1;
	}
}
int main()
{
	//函數指標pfun
	int (*pfun)(char*) = test;
	//函數指標的陣列pfunArr
	int (*pfunArr[5])(char* str);
	pfunArr[0] = test;
	//指向函數指標陣列pfunArr的指標ppfunArr
	int (*(*ppfunArr)[5])(char*) = &pfunArr;
	return 0;
}

我們來分析一下這個:int(*(*ppfunArr)[5])(char*),首先看到 (*ppfunArr) 這括號括起來先跟 * 結合證明它是一個指標,指向的型別是什麼呢?把它去掉剩下的就是它的型別,int(*[5])(char*),通過這個可以發現,是一個帶有5個元素的陣列,每個元素的型別是一個函數指標,而函數的返回值為int,引數為 char*

這裡我們能看懂即可。

5、回撥函數

回撥函數指的就是一個通過函數指標呼叫的函數,如果你把函數的指標(地址),作為引數傳遞給另一個函數的話,當這個指標被用來呼叫其指向的函數,這裡就被稱為回撥函數。其實 qsort 函數就是很典型使用了回撥函數的例子,感興趣的可以自行下來了解一下,這裡我們就簡單的演示下如何使用,用回撥函數實現三個數比較大小:

int max(int x, int y, int z, int(*pfun)(int, int))
{
	if (x > pfun(y, z)) {
		return x;
	}
	else
	{
		return pfun(y, z);
	}
}
int tmp(int x, int y) 
{
	return x > y ? x : y;
}
int main()
{
	int ret = max(10, 20, 30, tmp);
	printf("%dn", ret);
	return 0;
}

比較三個數的最大值是有更優的解決方案的,我們這裡只是演示一下回撥函數的簡單使用,跟上面一樣,會用即可,其實不用研究的特別深入

6、一道筆試題

int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };
	char*** cpp = cp;
	printf("%sn", **++cpp);
	printf("%sn", *-- * ++cpp + 3);
	printf("%sn", *cpp[-2] + 3);
	printf("%sn", cpp[-1][-1] + 1);
	return 0;
}

這道題我就不講解了,學習一定得有自己研究的一個過程,包括後續 Java 的文章,每一期基本上都會留一個小疑問讓大家自己下去解答,其實這道題很簡單,耐心畫畫圖就能理解了,如果你能自己解決這道題,說明你的指標的陣列這兩章的內容已經通關了,實在是難以解決的話,可以問一下博主。

後續其實還有動態記憶體管理,但是這個知識點無非就是掌握對 malloc calloc realloc free 的使用,如果你是以後 C++ 方向可學習一下,如果你是 Java 方向其實有個基本認識就行,畢竟 Java接觸底層不多,有了前面學習的鋪墊,去網上看看記憶體管理的文章是很輕鬆學會的,學習最主要是培養學習的能力,

最後來個大總結:從剛開始我們一共講解了32個關鍵字,在關鍵字中也穿插了很多內容,比如大小端,結構體,往後就是符號的理解了,包括我們平常用的註釋,以及各種運運算元但是除法和取模我們沒有放進去,這個在JavaSE系列中會介紹,再往後就是對預處理的深入理解了,最終我們以陣列和指標結尾,C語言系列就到此結束了。

到此這篇關於C語言指標和陣列深入探究使用方法的文章就介紹到這了,更多相關C語言指標和陣列內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com