C/C++學習筆記——C提高:多維數組

一維數組

  • 元素類型角度:數組是相同類型的變量的有序集合
  • 內存角度:連續的一大片內存空間
    在這裏插入圖片描述

在討論多維數組之前,我們還需要學習很多關於一維數組的知識。首先讓我們學習一個概念。

數組名

考慮下面這些聲明:

int a;
int b[10];

我們把a稱作標量,因爲它是個單一的值,這個變量是的類型是一個整數。我們把b稱作數組,因爲它是一些值的集合。下標和數名一起使用,用於標識該集合中某個特定的值。例如,b[0]表示數組b的第1個值,b[4]表示第5個值。每個值都是一個特定的標量。

那麼問題是b的類型是什麼?它所表示的又是什麼?一個合乎邏輯的答案是它表示整個數組,但事實並非如此。在C中,在幾乎所有數組名的表達式中,數組名的值是一個指針常量,也就是數組第一個元素的地址。它的類型取決於數組元素的類型:如果他們是int類型,那麼數組名的類型就是“指向int的常量指針”;如果它們是其他類型,那麼數組名的類型也就是“指向其他類型的常量指針”。

請問:指針和數組是等價的嗎?
答案是否定的。數組名在表達式中使用的時候,編譯器纔會產生一個指針常量。那麼數組在什麼情況下不能作爲指針常量呢?在以下兩種場景下:

  • 當數組名作爲sizeof操作符的操作數的時候,此時sizeof返回的是整個數組的長度,而不是指針數組指針的長度。
  • 當數組名作爲&操作符的操作數的時候,此時返回的是一個指向數組的指針,而不是指向某個數組元素的指針常量。
int arr[10];
//arr = NULL; //arr作爲指針常量,不可修改
int *p = arr; //此時arr作爲指針常量來使用
printf("sizeof(arr):%d\n", sizeof(arr)); //此時sizeof結果爲整個數組的長度
printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*

下標引用

int arr[] = { 1, 2, 3, 4, 5, 6 };

*(arr + 3) ,這個表達式是什麼意思呢?
首先,我們說數組在表達式中是一個指向整型的指針,所以此表達式表示arr指針向後移動了3個元素的長度。然後通過間接訪問操作符從這個新地址開始獲取這個位置的值。這個和下標的引用的執行過程完全相同。所以如下表達式是等同的:

*(arr + 3)
arr[3]

問題1:數組下標可否爲負值?
問題2:請閱讀如下代碼,說出結果:

int arr[] = { 5, 3, 6, 8, 2, 9 };
int *p = arr + 2;
printf("*p = %d\n", *p);
printf("*p = %d\n", p[-1]);

那麼是用下標還是指針來操作數組呢?對於大部分人而言,下標的可讀性會強一些。

數組和指針

指針和數組並不是相等的。爲了說明這個概念,請考慮下面兩個聲明:

int a[10];
int *b;

聲明一個數組時,編譯器根據聲明所指定的元素數量爲數組分配內存空間,然後再創建數組名,指向這段空間的起始位置。聲明一個指針變量的時候,編譯器只爲指針本身分配內存空間,並不爲任何整型值分配內存空間,指針並未初始化指向任何現有的內存空間。

因此,表達式a是完全合法的,但是表達式b卻是非法的。*b將訪問內存中一個不確定的位置,將會導致程序終止。另一方面b++可以通過編譯,a++卻不行,因爲a是一個常量值。

作爲函數參數的數組名

當一個數組名作爲一個參數傳遞給一個函數的時候發生什麼情況呢?我們現在知道數組名其實就是一個指向數組第1個元素的指針,所以很明白此時傳遞給函數的是一份指針的拷貝。所以函數的形參實際上是一個指針。但是爲了使程序員新手容易上手一些,編譯器也接受數組形式的函數形參。因此下面兩種函數原型是相等的:

int print_array(int *arr);
int print_array(int arr[]);

我們可以使用任何一種聲明,但哪一個更準確一些呢?答案是指針。因爲實參實際上是個指針,而不是數組。同樣sizeof arr值是指針的長度,而不是數組的長度。

現在我們清楚了,爲什麼一維數組中無須寫明它的元素數目了,因爲形參只是一個指針,並不需要爲數組參數分配內存。另一方面,這種方式使得函數無法知道數組的長度。如果函數需要知道數組的長度,它必須顯式傳遞一個長度參數給函數。

多維數組

如果某個數組的維數不止1個,它就被稱爲多維數組。接下來的案例講解以二維數組舉例。

void test01(){
	//二維數組初始化
	int arr1[3][3] = {
		{ 1, 2, 3 },
		{ 4, 5, 6 },
		{ 7, 8, 9 }
	};
	int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

	//打印二維數組
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 3; j ++){
			printf("%d ",arr1[i][j]);
		}
		printf("\n");
	}
}

數組名

一維數組名的值是一個指針常量,它的類型是“指向元素類型的指針”,它指向數組的第1個元素。多維數組也是同理,多維數組的數組名也是指向第一個元素,只不過第一個元素是一個數組。例如:

int arr[3][10]

可以理解爲這是一個一維數組,包含了3個元素,只是每個元素恰好是包含了10個元素的數組。arr就表示指向它的第1個元素的指針,所以arr是一個指向了包含了10個整型元素的數組的指針。

指向數組的指針(數組指針)

數組指針,它是指針,指向數組的指針。

數組的類型由元素類型和數組大小共同決定:int array[5] 的類型爲 int[5];C語言可通過typedef定義一個數組類型:
定義數組指針有一下三種方式:

//方式一
void test01(){

	//先定義數組類型,再用數組類型定義數組指針
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	//有typedef是定義類型,沒有則是定義變量,下面代碼定義了一個數組類型ArrayType
	typedef int(ArrayType)[10];
	//int ArrayType[10]; //定義一個數組,數組名爲ArrayType

	ArrayType myarr; //等價於 int myarr[10];
	ArrayType* pArr = &arr; //定義了一個數組指針pArr,並且指針指向數組arr
	for (int i = 0; i < 10;i++){
		printf("%d ",(*pArr)[i]);
	}
	printf("\n");
}

//方式二
void test02(){

	int arr[10];
	//定義數組指針類型
	typedef int(*ArrayType)[10];
	ArrayType pArr = &arr; //定義了一個數組指針pArr,並且指針指向數組arr
	for (int i = 0; i < 10; i++){
		(*pArr)[i] = i + 1;
	}
	for (int i = 0; i < 10; i++){
		printf("%d ", (*pArr)[i]);
	}
	printf("\n");

}

//方式三
void test03(){
	
	int arr[10];
	int(*pArr)[10] = &arr;

	for (int i = 0; i < 10; i++){
		(*pArr)[i] = i + 1;

	}
	for (int i = 0; i < 10; i++){
		printf("%d ", (*pArr)[i]);
	}
	printf("\n");
}

指針數組(元素爲指針)

棧區指針數組

//數組做函數函數,退化爲指針
void array_sort(char** arr,int len){

	for (int i = 0; i < len; i++){
		for (int j = len - 1; j > i; j --){
			//比較兩個字符串
			if (strcmp(arr[j-1],arr[j]) > 0){
				char* temp = arr[j - 1];
				arr[j - 1] = arr[j];
				arr[j] = temp;
			}
		}
	}

}

//打印數組
void array_print(char** arr,int len){
	for (int i = 0; i < len;i++){
		printf("%s\n",arr[i]);
	}
	printf("----------------------\n");
}

void test(){
	
	//主調函數分配內存
	//指針數組
	char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
	//char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //錯誤
	int len = sizeof(p) / sizeof(char*);
	//打印數組
	array_print(p, len);
	//對字符串進行排序
	array_sort(p, len);
	//打印數組
	array_print(p, len);
}

堆區指針數組

//分配內存
char** allocate_memory(int n){
	
	if (n < 0 ){
		return NULL;
	}

	char** temp = (char**)malloc(sizeof(char*) * n);
	if (temp == NULL){
		return NULL;
	}

	//分別給每一個指針malloc分配內存
	for (int i = 0; i < n; i ++){
		temp[i] = malloc(sizeof(char)* 30);
		sprintf(temp[i], "%2d_hello world!", i + 1);
	}

	return temp;
}

//打印數組
void array_print(char** arr,int len){
	for (int i = 0; i < len;i++){
		printf("%s\n",arr[i]);
	}
	printf("----------------------\n");
}

//釋放內存
void free_memory(char** buf,int len){
	if (buf == NULL){
		return;
	}
	for (int i = 0; i < len; i ++){
		free(buf[i]);
		buf[i] = NULL;
	}

	free(buf);
}

void test(){
	
	int n = 10;
	char** p = allocate_memory(n);
	//打印數組
	array_print(p, n);
	//釋放內存
	free_memory(p, n);
}

二維數組三種參數形式

二維數組的線性存儲特性

void PrintArray(int* arr, int len){
	for (int i = 0; i < len; i++){
		printf("%d ", arr[i]);
	}
	printf("\n");
}

//二維數組的線性存儲
void test(){
	int arr[][3] = {
		{ 1, 2, 3 },
		{ 4, 5, 6 },
		{ 7, 8, 9 }
	};

	int arr2[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int len = sizeof(arr2) / sizeof(int);

	//如何證明二維數組是線性的?
	//通過將數組首地址指針轉成Int*類型,那麼步長就變成了4,就可以遍歷整個數組
	int* p = (int*)arr;
	for (int i = 0; i < len; i++){
		printf("%d ", p[i]);
	}
	printf("\n");

	PrintArray((int*)arr, len);
	PrintArray((int*)arr2, len);
}

二維數組的3種形式參數

//二維數組的第一種形式
void PrintArray01(int arr[3][3]){
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 3; j++){
			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
		}
	}
}

//二維數組的第二種形式
void PrintArray02(int arr[][3]){
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 3; j++){
			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
		}
	}
}

//二維數組的第二種形式
void PrintArray03(int(*arr)[3]){
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 3; j++){
			printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
		}
	}
}

void test(){
	
	int arr[][3] = { 
		{ 1, 2, 3 },
		{ 4, 5, 6 },
		{ 7, 8, 9 }
	};
	
	PrintArray01(arr);
	PrintArray02(arr);
	PrintArray03(arr);
}

總結

編程提示

  • 源代碼的可讀性幾乎總是比程序的運行時效率更爲重要
  • 只要有可能,函數的指針形參都應該聲明爲const
  • 在多維數組的初始值列表中使用完整的多層花括號提供可讀性

內容總結

在絕大多數表達式中,數組名的值是指向數組第1個元素的指針。這個規則只有兩個例外,sizeof和對數組名&。
指針和數組並不相等。當我們聲明一個數組的時候,同時也分配了內存。但是聲明指針的時候,只分配容納指針本身的空間。
當數組名作爲函數參數時,實際傳遞給函數的是一個指向數組第1個元素的指針。
我們不單可以創建指向普通變量的指針,也可創建指向數組的指針。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章