1. 程式人生 > >理解C語言——從小菜到大神的晉級之路(8)——陣列、指標和字串

理解C語言——從小菜到大神的晉級之路(8)——陣列、指標和字串

本期視訊點選這裡

       在前面幾次我們接觸的資料型別都是簡單資料型別,使用一個數據個體表示一個元素。C語言中還提供了多種複雜資料型別,其中最簡單的一種就是陣列。陣列這一結構使用記憶體中一段連續的記憶體空間儲存一組相同型別的變數,這些變數通過陣列的下標/索引的不同相互區分。陣列與指標有著十分緊密的聯絡,通常使用陣列下標能實現的操作都可以使用指標完成,而且使用指標的程式通常效率更高。但是指標和陣列也存在著一些明顯的差別,如果誤用將導致錯誤。另外,C語言中還定義了一種極為常用的特殊的陣列——字串,其本質與字元陣列類似,但是又有一些特殊的特性,並且可以使用一些特殊的操作提高對字串操作的效率。

1、陣列的定義和使用

        陣列的定義方法同定義變數類似,都是採用資料型別+變數名的方法定義。例如如果希望定義一個包含5個整型資料,陣列名為array的陣列,則通過以下方式定義:

int array[5];

        陣列定義完成後,可以進行初始化操作。陣列的初始化有多重方式,可以對陣列中所有資料初始化,也可以只初始化前面的幾個資料。如果賦初值的個數是確定的,甚至還可以通過初始化類指定陣列的大小。例如:
int nArray[5] = {1,2,3,4,5};           //顯式初始化陣列中所有的元素
char cArray[10] = {0};                  //將所有資料初始化為0
float fArray[5] = {0, 1.5, -3.87};    //初始化前三個資料,後面的預設為0
int nArray[] = {3,2,1};                   //初值指定陣列元素的大小

       陣列定義和初始化完成後,可以使用陣列名和下標來索引陣列的元素。陣列中起始的元素下標為0,通常稱作第0個元素,一個包含n個元素的陣列下標最大值為n-1。如對上文定義的nArray,5個元素分別可以用nArray[0]、nArray[1]、nArray[2]、nArray[3]、nArray[4]表示。使用迴圈來輸出每個元素的方法如:
for(int n = 0; n<5; n++)
{
     printf("%d\n", nArray[n]);
}

        需要注意的是,陣列的讀寫絕對不可以超過定義的邊界。如果陣列讀寫越界,則會導致獲取資料錯誤甚至程式崩潰。

2、陣列和指標的關係

        幾乎所有的陣列資料都可以用對應型別的指標進行操作,甚至在很多場合,指標和陣列本質上起的作用就是一致的。究其根本原因在於,一個數組的陣列名,本身就是一個指標常量,其值為陣列第一個元素的地址。 例如我們定義一個數組,並定義一個指標指向該陣列:
int nArray[10] = {1,4,3,16,25,36,49,64,81,100};
int *pArray = nArray;

        這樣,由於陣列名nArray實際上儲存了該陣列的首地址,指標變數pArray便指向了陣列中第一個元素0。第二條語句實際上等價於:
int *pArray = &nArray[0];
        陣列和指標變數的關係如圖所示:         當指標變數指向陣列的一個元素後,使用間接運算子就可以獲取陣列中的元素:
int num = *pArray;     //num = nArray[0];

3、指標遍歷陣列

        前面我們使用的指標變數,只是研究了其指向地址、獲取目標記憶體單元的資料等最簡單的功能,指標變數本身並沒有考慮移動的問題。實際上,指標變數也是變數的一種,可以進行相應的運算,只要在指向一串連續的有效記憶體地址,指標變數可以進行前移和後移運算來遍歷記憶體中的資料,這種特性使得指標變數在程式設計開發中擁有強大的功能。         如果我們已經將指標變數pArray指向了陣列中某一個特定元素,那麼指標pArray+1將指向陣列的下一個元素,而pArray-1將指向指標的上一個元素。如下圖所示:
        例如,我們將指標變數指向陣列中的第4個元素,並將指標變數前後移動:
int *pArray = &nArray[3];     //pArray指向nArray[3];
pArray--;                              //pArray向前移動一個單元,指向nArray[2];
pArray += 2;                        //pArray向後移動一個單元,指向nArray[4];

        這樣,遍歷陣列中的元素除了使用陣列下標之外又多了一種選擇——使用指標運算。首先將指標指向陣列首地址,隨後不斷獲取指向下一個位置的指標直至陣列結束,並通過間接運算子獲取指向的資料,即可以實現陣列的遍歷功能。
int nArray[10] = {1,4,3,16,25,36,49,64,81,100}, *pArray;
for(int idx = 0, pArray = nArray; idx < 10; idx++)
{
     printf("%d\n", *(pArray+idx));
}

        或者可以使指標變數持續向後移動1位來遍歷整個陣列:
for(int idx = 0, pArray = nArray; idx < 10; idx++)
{
     printf("%d\n", *(pArray++));
}

        實際上,從獲取連續記憶體地址中的某個資料這一功能考慮,陣列下標法nArray[n]和指標解引用法*(pArray+n)是等價的,二者大部分時間可以無條件互換。不僅如此,當作為函式的引數時,二者也可以相互調換使用。如以下兩個函式的宣告,實際上沒有任何區別:
void foo(int *pArray);
void foo(int nArray[10]);

        需要注意的一點是,使用陣列作為引數時,編譯器只關注陣列名(也就是指向陣列的指標)而並不關心陣列的長度。只是如果形引數組宣告的長度超過了實際值,在執行時可能出現問題,因此應避免。實際上,如果使用陣列名作為引數,我們更推薦直接省略陣列的長度:
void foo(int nArray[]);

        雖然指標和陣列有著諸多相同之處,但是我們決不能忽略二者的本質區別。指標是一個變數,可以進行上文中提到的各種運算(只支援前移、後移等運算;如果兩個指標指向同一段陣列,還可以根據差值計算距離;對指標進行乘除等運算非法);而陣列名是一個常量,自定義之後便不可以改變,更不能進行移動等運算。另外,對陣列名利用sizeof運算子求大小得到的是整個陣列佔記憶體的大小,而對指標求sizeof得到的是變數本身所佔據的長度,通常為4。另外,如果將陣列名作為函式引數,子函式內部對這個引數計算sizeof得到的依然是4,這是因為子函式已經把實引數組名轉化為了一個指標,因此計算結果與sizeof其他指標變數相同。

4、字串

(1)字串的定義

        字串是C語言的一種資料型別,由多個字元型資料組成。在C語言中,字串原本都是常量,並且在程式執行時儲存在專門的字串常量區。在我們的程式碼中可以定義字元型的指標指向一個字串常量,而後我們就可以在程式中使用該指標表示字串常量。
int main()
{
  char * pStr = "hello world!" ;
  printf ( "%s\n" , pStr );
  return 0 ;
}

        在這段程式碼中,如果檢視對指標指向的內容進行修改,那麼雖然編譯可以順利通過,但是執行時程式一定會崩潰,因為我們檢視向非法的位置寫入資料:
int main()
{
  char * pStr = "hello world" ;
  pStr[0] = 'H';     //這行程式碼將導致程式崩潰
  printf ( "%s\n" , pStr );
  return 0 ;
}

        如果我們希望獲得一個我們可以修改的字串變數,那麼我們就必須將字串賦給一個字元型陣列中。然而字串這一資料型別有其自身的特殊之處,所有字串的末尾都自動會新增一個\0作為結束符。因此,在為字串陣列賦初值時需格外注意不要遺漏:
char str[12] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'}; //正確,但是太繁瑣
char str[12] = {'h','e','l','l','o',' ','w','o','r','l','d'};       //正確,當陣列長度大於初始化的資料長度時,末尾自動補0,但依舊太繁瑣
char str[11] = {'h','e','l','l','o',' ','w','o','r','l','d'};       //錯誤,陣列沒有留出結束符0的位置來,不是一個完整的字串
char str[12] = "hello world";                              //正確,直接用字串初始化陣列
char str[] = "hello world";                                      //推薦最優寫法,自動確定陣列長度,並補齊結束符0

(2)處理字串

        在將字串儲存到我們自己定義的陣列中之後,處理字串有多種方法。最基本的方法就是同其他型別的陣列一樣迴圈遍歷處理,但是這樣不但繁瑣而且效率較低。對於字串通常有其他更為方便快捷的方法。         字串的輸入和輸出:         第一種方法是使用標準輸入輸出函式printf和scanf,將字串整體輸入和輸出。表示字串的格式說明符為%s:
printf("%s\n", "This is a string.");
char str[20];
scanf("%s", str);

        使用這種方法輸入字串時,必須注意要分配足夠的記憶體空間儲存輸入的資料。另外,空格、回車都會被作為字串結束符處理。         第二種方法是使用字串專用的輸入和輸出函式puts和gets:
puts("Input a string:\n");
char str[20];
gets(str);
puts(str);

        這種方法通常比格式化輸入和輸出更為方便,而且輸入資料時只認回車作為結束符,可以讀入空格。 其他字串處理:         C語言中並沒有對字串這一型別定義整體的操作符,但是提供了多種字串操作的庫函式:
  • strcpy:字串拷貝函式
  • strcmp:字串比較函式
  • strlen:計算字串長度函式
  • strcat:字串拼接函式
        值得注意的是,strlen和sizeof的功能不同。sizeof用於計算某一個量在記憶體中佔據的大小,而strlen用於計算字串的長度。如果將二者對一字串計算,得到的值可能完全不同。
char *pStr = "String";
int string_len = strlen(pStr);          //string_len == 7,字串中有7個字元
int sizeof_pStr = sizeof(pStr);        // sizeof_pStr == 4,指標變數佔據4個位元組