C語言學習筆記(二)——函數,數組與指針

C Primer Plus學習筆記2

 


一  函數

函數(function)是完成特定任務的獨立程序代碼單元。語法規則定義了函數的結構和使用方式。

函數可以作爲組成大型程序的構件塊。每個函數都應該有一個單獨且定義好的功能。使用參數把值傳給函數,使用關鍵字return把值返回函數。如果函數返回的值不是int類型,則必須在函數定義和函數原型中指定函數的類型。如果需要在被調函數中修改主調函數的變量,使用地址或指針作爲參數。

1 ANSI C風格

1.1) 原型:void starbar(void)

ANSI  C提供了一個強大的工具——函數原型,允許編譯器驗證函數調用中使用的參數個數和類型是否正確。

函數原型指明瞭函數的返回值類型和函數接受的參數類型。這些信息稱爲該函數的簽名(signature)。

也接受過去聲明函數的形式,即圓括號內沒有參數列表:void show_n_char();

分號表明這是在聲明函數,不是定義函數。

1.2)參數

  • 形式參數:void show_n_char(char ch, int num)//每個變量要前都聲明其類型,可以省略變量名

兩個參數ch和num,這兩個變量被稱爲形式參數。是局部變量,屬該函數私有。

  • 實際參數:show_n_char(SPACE, 12)//實際參數是具體的值,該值要被賦給作爲形式參數的變量

形式參數是被調函數(called function)中的變量,實際參數是主調函數(calling  function)賦給被調函數的具體值。被調函數不知道也不關心傳入的數值是來自常量、變量還是一般表達式。

實際參數是出現在函數調用圓括號中的表達式。形式參數是函數定義的函數頭中聲明的變量。

1.3)return返回值

函數的返回值可以把信息從被調函數傳回主調函數。

“return; ”——這條語句會導致終止函數,並把控制返回給主調函數。因爲 return 後面沒有任何表達式,所以沒有返回值,只有在void函數中才會用到這種形式。

1.4)函數類型

聲明函數時必須聲明函數的類型。帶返回值的函數類型(不是void聲明的)應該與其返回值類型相同,而沒有返回值的函數應聲明爲void類型。

注意:函數類型指的是返回值的類型,不是函數參數的類型。

程序在第 1 次使用函數之前必須知道函數的類型。方法之一是,把完整的函數定義放在第1次調用函數的前面。通常的做法是提前聲明函數:

int imin(int, int);
int main(void)
{
}
或:
int main(void)
{
int imin(int, int); 
}

2 遞歸基本原理

  1. 每級函數調用都有自己的變量。
  2. 每次函數調用都會返回一次。
  3. 遞歸函數中位於遞歸調用之前的語句,均按被調函數的順序執行。
  4. 遞歸函數中位於遞歸調用之後的語句,均按被調函數相反的順序執行。
  5. 雖然每級遞歸都有自己的變量,但是並沒有拷貝函數的代碼。
  6. 遞歸函數必須包含能讓遞歸調用停止的語句。
  7. 遞歸在處理倒序時非常方便

3 函數間通信

#include <stdio.h>
void interchange(int * u, int * v);
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x, y);
interchange(&x, &y); // 把地址發送給函數
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}
void interchange(int * u, int * v)
{
int temp;
temp = *u;  // temp獲得 u 所指向對象的值
*u = *v;
*v = temp;
}
該程序是否能正常運行?下面是程序的輸出:
Originally x = 5 and y = 10.
Now x = 10 and y = 5.

編寫程序時,可以認爲變量有兩個屬性:名稱和值;

計算機編譯和加載程序後,認爲變量也有兩個屬性:地址和值。地址就是變量在計算機內部的名稱。

普通變量把值作爲基本量,把地址作爲通過&運算符獲得的派生量,而指針變量把地址作爲基本量,把值作爲通過*運算符獲得的派生量。

二 數組

2.1)定義

數組(array)是按順序儲存的一系列類型相同的值。數組有一個數組名,通過整數下標訪問數組中單獨的項或元素。

C 把數組看作是派生類型,因爲數組是建立在其他類型的基礎上。也就是說,在聲明數組時必須說明其元素的類型。

下標:

用於識別數組元素的數字被稱爲下標(subscript)、索引(indice)或偏移量(offset)。

下標必須是整數,而且要從0開始計數。數組的元素被依次儲存在內存中相鄰的位置。

2.2)實現

聲明:

用方括號[]表明是數組,用以逗號分隔的值列表(用花括號括起來)來初始化數組,a[8]={1,2,4,6,8,16,32,64}。

有時需要把數組設置爲只讀。這樣,程序只能從數組中檢索值,不能把新值寫入數組。用const聲明和初始化數組。

#define MONTHS 12
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

可以省略方括號中的數字,讓編譯器自動匹配數組大小和初始化列表中的項數。數組大小必須大於0且爲整數。

編譯器不會檢查數組下標是否使用得當。在C標準中,使用越界下標的結果是未定義的。這意味着程序看上去可以運行,但是運行結果很奇怪,或異常中止。

存儲:

如果不初始化數組,數組元素和未初始化的普通變量一樣,其中儲存的都是垃圾值;但是,如果部分初始化數組,剩餘的元素就會被初始化爲0。

自動存儲類別:數組在函數內部聲明,且聲明時未使用關鍵字static。 對於一些其他存儲類別的變量和數組,如果在聲明時未初始化,編譯器會自動把它們的值設置爲0。

2.3)指定初始化器

C99規定,在初始化列表中使用帶方括號的下標指明待初始化的元素:int arr[6] = {[5] = 212};

初始化器的兩個重要特性:

  1. 如果指定初始化器後面有更多的值,後面這些值將被用於初始化指定元素後面的元素。
  2. 如果再次初始化指定的元素,那麼最後的初始化將會取代之前的初始化。

如 int days[MONTHS] = { 31, 28, [4] = 31, 30, 31, [1]= 29 };

該例中的初始化列表中的片段:[4] = 31,30,31,那麼在days[4]被初始化爲31後,days[5]和days[6]將分別被初始化爲30和31。

2.4)多維數組

二維數組:數組的數組。

C 語言傳遞多維數組的傳統方法是把數組名(即數組的地址)傳遞給類型匹配的指針形參。聲明這樣的指針形參要指定所有的數組維度,除了第1個維度。傳遞的第1個維度通常作爲第2個參數。例如,爲了處理前面聲明的sales數組,函數原型和函數調用如下:void display(double ar[][12], int rows); 

2.5)可變長數組

來源:

要創建一個能處理任意大小二維數組的函數,比較繁瑣。C99新增了變長數組(variable-length array,VLA),允許使用
變量表示數組的維度。

介紹:

int sum2d(int rows, int cols, int ar[rows][cols]); // ar是一個變長數組(VLA)

注意前兩個形參(rows和cols)用作第3個形參二維數組ar的兩個維度。因爲ar的聲明要使用rows和cols,所以在形參列表中必須在聲明ar之前先聲明這兩個形參。因此int sum2d(int ar[rows][cols], int rows, int cols); // 無效的順序

C99/C11標準規定,可以省略原型中的形參名,但是在這種情況下,必須用星號來代替省略的維度:int sum2d(int, int, int ar[*][*]);

注意:

變長數組不能改變大小。變長數組必須是自動存儲類別,這意味着無論在函數中聲明還是作爲函數形參聲明,都不能使用static或extern存儲類別說明符,不能在聲明中初始化它們。

變長數組中的“變”不是指可以修改已創建數組的大小。一旦創建了變長數組,它的大小則保持不變。這裏的“變”指的是:在創建數組時,可以使用變量指定數組的維度。

const:

允許在聲明變長數組時使用 const 變量。所以該數組的定義必須是聲明在塊中的自動存儲類別數組。變長數組還允許動態內存分配,這說明可以在程序運行時指定數組的大小。

2.6)複合字面量

背景:

給帶int類型形參的函數傳遞一個值,要傳遞int類型的變量,但是也可以傳遞int類型常量,如5。在C99 標準以前,對於帶數組形參的函數,情況不同,可以傳遞數組,但是沒有等價的數組常量。

產生:

C99新增了複合字面量(compound  literal)。

字面量是除符號常量外的常量。例如,5是int類型字面量, 81.3是double類型的字面量,'Y'是char類型的字面量,"elephant"是字
符串字面量。

定義:

複合字面量類似數組初始化列表,前面是用括號括起來的類型名。

int diva[2] = {10, 20};//普通的數組聲明

(int [2]){10, 20} // 複合字面量

去掉聲明中的數組名,留下的int [2]即是複合字面量的類型名。複合字面量也可以省略大小:

(int []){50, 20, 90}

用法:

因爲複合字面量是匿名的,所以不能先創建然後再使用它,必須在創建的同時使用它。

複合字面量的類型名也代表首元素的地址,所以可以把它賦給指向int的指針。

  • 使用指針記錄地址就是一種用法:

int * pt1;
pt1 = (int [2]) {10, 20};

  • 典型用法:

int total3;
total3 = sum((int []){4,4,4,5,5,5}, 6);

  • 二維數組:

int (*pt2)[4];

pt2 = (int [2][4]) { {1,2,3,-9}, {4,5,6,-8} };

小結:

複合字面量是提供只臨時需要的值的一種手段。複合字面量具有塊作用域,這意味着一旦離開定義複合字面量的塊,程序將無法保證該字面量是否存在。

三 指針

3.1)運算符

指針提供一種以符號形式使用地址的方法,指針(pointer)是一個值爲內存地址的變量

ptr = &pooh; // 把pooh的地址賦給ptr

對於這條語句,我們說ptr“指向”pooh。ptr和&pooh的區別是ptr是變量,而&pooh是常量。

printf("%d %p\n", pooh, &pooh);//%p是輸出地址的轉換說明

將輸出如下內容:
24 0B76
  • 一元&運算符給出變量的存儲地址。如果pooh是變量名,那麼&pooh是變量的地址。
  • 使用間接運算符*(indirection operator)找出儲存在bah中的,該運算符有時也稱爲解引用運算符(dereferencing operator)。val = *ptr; // 找出ptr指向的值

3.2)聲明

聲明指針變量時必須指定指針所指向變量的類型,因爲不同的變量類型佔用不同的存儲空間,另外,程序必須知道儲存在指定地址上的數據類型。

int * pi; // pi是指向int類型變量的指針

指針實際上是一個新類型,不是整數類型。

3.3)操作

賦值:可以把地址賦給指針。例如,用數組名、帶地址運算符(&)的變量名、另一個指針進行賦值。地址應該和指針類型兼容。

解引用:*運算符給出指針指向地址上儲存的值。

取址:和所有變量一樣,指針變量也有自己的地址和值。對指針而言,&運算符給出指針本身的地址。

指針與整數相加:可以使用+運算符把指針與整數相加,或整數與指針相加。無論哪種情況,整數都會和指針所指向類型的大小(以字節爲單位)相乘,然後把結果與初始地址相加。因此ptr1  +4與&urn[4]等價。

遞增指針:遞增指向數組元素的指針可以讓該指針移動至數組的下一個元素。因此,ptr1++相當於把ptr1的值加上4(我們的系統中int爲4字節),ptr1指向urn[1]

指針求差:可以計算兩個指針的差值。通常,求差的兩個指針分別指向同一個數組的不同元素,通過計算求出兩元素之間的距離。差值的單位與數組類型的單位相同。例如,ptr2 - ptr1得2,意思是這兩個指針所指向的兩個元素相隔兩個int,而不是2字節。

4 指針和數組

數組和指針的關係密切,同一個操作可以用數組表示法或指針表示法。它們之間的關係允許你在處理數組的函數中使用數組表示法,即使函數的形式參數是一個指針,而不是數組。

4.1 數組和指針

數組名是數組首元素的地址。

short dates[SIZE];
short * pti;
pti = dates; // 把數組地址賦給指針

dates + 2 == &date[2]   // 相同的地址
*(dates + 2) == dates[2] // 相同的值

在C中,指針加1指的是增加一個存儲單元。對數組而言,這意味着把加1後的地址是下一個元素的地址,而不是下一個字節的地址 。

 定義ar[n]的意思是*(ar + n)。可以認爲*(ar + n)的意思是“到內存的ar位置,然後移動n個單元,檢索儲存在那裏的值”。

4.2 函數,數組和指針

通常編寫一個函數來處理數組,這樣在特定的函數中解決特定的問題,有助於實現程序的模塊化。在把數組名作爲實際參數時,傳遞給函數的不是整個數組,而是數組的地址。

1)函數中的數組與指針

假設要編寫一個處理數組的函數,該函數返回數組中所有元素之和,待處理的是名爲marbles的int類型數組。

int sum(int * ar, int n)   // 更通用的方法
{
int i;
int total = 0;
for (i = 0; i < n; i++)  // 使用 n 個元素
total += ar[i];    // ar[i] 和 *(ar + i) 相同
return total;
}

解釋

數組名是該數組首元素的地址,所以實際參數marbles是一個儲存int類型值的地址,應把它賦給一個指針形式
參數,即該形參是一個指向int的指針 。

既然能使用指針表示數組名,也可以用數組名錶示指針。

在函數原型或函數定義頭中,可以用int ar[]代替int * ar:int sum (int ar[], int n);

2)保護數組中的元素

編寫一個處理基本類型(如,int)的函數時,要選擇是傳遞int類型的值還是傳遞指向int的指針。通常都是直接傳遞數值,因爲這樣做可以保證數據的完整性。只有程序需要在函數中改變該數值時,纔會傳遞指針。

對於數組別無選擇,必須傳遞指針,因爲這樣做效率高。add_to(double ar[])

如果函數的意圖不是修改數組中的數據內容,那麼在函數原型和函數定義中聲明形式參數時應使用關鍵字const。

int sum(const int ar[], int n); /* 函數原型 */

這樣使用const並不是要求原數組是常量,而是該函數在處理數組時將其視爲常量,不可更改。

注意:可以創建const數組、const指針和指向const的指針。

4.3 指針和多維數組

處理多維數組的函數要用到指針。

1)int zippo[4][2]; /* 內含int數組的數組 */

分析:

數組名zippo是該數組首元素的地址,zippo的首元素是一個內含兩個int值的數組,所以zippo是這個內含兩個int值的數組的地址。

zippo[0]是一個佔用一個int大小對象的地址,而zippo是一個佔用兩個int大小對象的地址。由於這個整數和內含兩個整數的數組都開始於同一個地址,所以zippo和zippo[0]的值相同。

解引用一個指針(在指針前使用*運算符)或在數組名後使用帶下標的[]運算符,得到引用對象代表的值。

地址的地址或指針的指針是就是雙重間接(double indirection)。

因爲zippo[0]是該數組首元素(zippo[0][0])的地址,所以*(zippo[0])表示儲存在zippo[0][0]上的值(即一個int類型的值)。與此類似,*zippo代表該數組首元素(zippo[0])的值,但是zippo[0]本身是一個int類型值的地址。該值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]。對兩個表達式應用解引用運算符表明,**zippo與*&zippo[0][0]等價,這相當於zippo[0][0],即一個int類型的值。

2)寫個程序分析:

int zippo[4][2] = { {2,4},{6,8},{12,3},{5,6 }};
printf("zippo = %p, zippo+1 = %p\n",zippo,zippo+1);
printf("zippo[0] = %p,zippo[0]+1 = %p\n",zippo[0],zippo[0]+1);
printf("*zippo = %p, *zippo + 1 = %p\n", *zippo, *zippo + 1);
printf("*zippo[0] = %p, *zippo[0] + 1 = %p\n", *zippo[0], *zippo[0] + 1);
printf("zippo[0][0] = %d\n", zippo[0][0]);
printf("**zippo = %d\n", **zippo);
printf("*(*(zippo+2) + 1) = %d\n", *(*(zippo + 2) + 1));

解析:

二維數組zippo的地址和一維數組zippo[0]的地址相同。它們的地址都是各自數組首元素的地址;

zippo[0]指向一個4 字節的數據對象,數組名zippo 是一個內含2個int類型值的數組的地址,所以zippo指向一個8字節的數據對象;

zippo[0]和*zippo完全相同;

二維數組名解引用兩次,得到儲存在數組中的值。

 3)那麼如何聲明一個指針變量pz指向一個二維數組?

int (* pz)[2]; // pz指向一個內含兩個int類型值的數組

int * pax[2]; // pax是一個內含兩個指針元素的數組,每個元素都指向int的指針

因爲[]的優先級高於*。

4)指針的兼容性

無效的賦值表達式語句中涉及的兩個指針都是指向不同的類型。

把const指針賦給非const指針不安全,因爲這樣可以使用新的指針改變const指針指向的數據。

但是把非const指針賦給const指針沒問題,前提是隻進行一級解引用。

C++允許在聲明數組大小時使用const整數,而C卻不允許。

 

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