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 遞歸基本原理
- 每級函數調用都有自己的變量。
- 每次函數調用都會返回一次。
- 遞歸函數中位於遞歸調用之前的語句,均按被調函數的順序執行。
- 遞歸函數中位於遞歸調用之後的語句,均按被調函數相反的順序執行。
- 雖然每級遞歸都有自己的變量,但是並沒有拷貝函數的代碼。
- 遞歸函數必須包含能讓遞歸調用停止的語句。
- 遞歸在處理倒序時非常方便
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};
初始化器的兩個重要特性:
- 如果指定初始化器後面有更多的值,後面這些值將被用於初始化指定元素後面的元素。
- 如果再次初始化指定的元素,那麼最後的初始化將會取代之前的初始化。
如 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卻不允許。