C語言指針和數組深入探究使用方法

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("加法:%d\n", arr[0](1, 2));
	printf("減法:%d\n", arr[1](5, 2));
	printf("乘法:%d\n", arr[2](3, 3));
	printf("除法:%d\n", arr[3](6, 2));
	return 0;
}

4、指向函數數組的指針

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

函數指針如何定義:

int test(char* str)
{
	if (str == NULL) {
		return 0;
	}
	else
	{
		printf("%s\n", 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("%d\n", 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("%s\n", **++cpp);
	printf("%s\n", *-- * ++cpp + 3);
	printf("%s\n", *cpp[-2] + 3);
	printf("%s\n", cpp[-1][-1] + 1);
	return 0;
}

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

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

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

到此這篇關於C語言指針和數組深入探究使用方法的文章就介紹到這瞭,更多相關C語言指針和數組內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: