一維數組
- 元素類型角度:數組是相同類型的變量的有序集合
- 內存角度:連續的一大片內存空間
在討論多維數組之前,我們還需要學習很多關於一維數組的知識。首先讓我們學習一個概念。
數組名
考慮下面這些聲明:
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個元素的指針。
我們不單可以創建指向普通變量的指針,也可創建指向數組的指針。