#### 第10章:
#### C語言
由于PHP底層運行的代碼以及PHP的虛擬機(Zend主機)用的C語言,所以我們會學習一下C語言。
C是一門高級語言。由于匯編語言依賴于硬件體系,并且該語言中的主機符號數量較多,所以運用起來不夠方便。為了使程序語言能貼近人類的自然語言,同時又不依賴于計算機硬件,于是產生了高級語言。C之后的很多高級語言的底層運行的是C語言,例如:PHP、Java、Golong等。
開發C程序可以使用 Microsoft Visual C++編輯器。
:-: 
#### 10.1 C語言基礎
##### C語言特點
* 高效性:友好的可讀性和便攜性。只比匯編語言執行效率低10%-20%。
* 靈活性:C語言的語法不拘一格,可在原有的基礎上進行創作、復合。從而會給程序員更多的想象和發揮空間。
* 功能豐富:除了C語言中具有的類型,還可以使豐富的運算符和自定義的結構類型,用來表達任何復雜的數據類型,完成所需要的功能。
* 表達力強:C語言的特點體現在它的語法形式與人們所使用的語言形式相似,書寫形式自由,結構規范,并且只需簡單的控制語句即可輕松控制程序流程,完成繁瑣的程序要求。
* 移植性好:C語言具有良好的移植性,在不同的操作系統下只需要簡單修改即可進行跨平臺的程序操作。
##### 的編譯器
可以使用GUN的gcc編譯器,它是免費的,適用于C/C++,它將正確的C程序編譯成`.c`的形式。
##### 程序結構
C 程序主要包括以下部分:
* 預處理器指令
* 函數
* 變量
* 語句 & 表達式
* 注釋
~~~
#include <stdio.h>
int main()
{
? /* 我的第一個 C 程序 */
? printf("Hello, World! \n");
?
? return 0;
}
~~~
:-: 
1. 程序的第一行`#include <stdio.h>`是預處理器指令,告訴 C 編譯器在實際編譯之前要包含 stdio.h 文件,是C的標準庫(在需要使用某種功能的時候引入)。
2. 下一行int main()是主函數,程序從這里開始執行。
3. 下一行 /\* ... \*/ 將會被編譯器忽略,這里放置程序的注釋內容。它們被稱為程序的注釋。
4. 下一行printf()是 C 中另一個可用的函數,會在屏幕上顯示消息 "Hello, World!"。
5. 下一行 return 0; 終止 main() 函數,并返回值 0(有些函數沒有返回值,就是沒又return,直接執行)。
#### 10.2 C的基本語法
C 程序由各種令牌組成,令牌可以是關鍵字、標識符、常量、字符串值,或者是一個符號。
~~~
printf("Hello, World! \n");
~~~
這五個令牌分別是:
~~~
printf
(
"Hello, World! \n"
)
;
~~~
##### 分號
在 C 程序中,分號是語句結束符。
~~~
printf("Hello, World! \n");
return 0;
~~~
##### 注釋
使用注釋可以增加程序的可讀性。注釋的行不會運行。
C 語言有兩種注釋方式:
~~~
// 單行注釋
~~~
以 // 開始的單行注釋,這種注釋可以單獨占一行。
~~~
/* 單行注釋 */
/*
多行注釋
多行注釋
多行注釋
*/
~~~
/\* \*/ 這種格式的注釋可以單行或多行。也不能在注釋內嵌套注釋,注釋也不能出現在字符串或字符值中。
~~~
include <stdio.h>
int main()
{
int a = 1;
? /* 我的第一個C程序 */
? printf("Hello, World! \n");
? return 0;
}
~~~
~~~
include <stdio.h>
int main()
{
int a = 1;
? //我的第一個C程序
? printf("Hello, World! \n");
? return 0;
}
~~~
~~~
include <stdio.h>
int main()
{
int a = 1;
? /* 我的第一個C程序
? 我已經開始學習C語言
? */
? printf("Hello, World! \n");
? return 0;
}
~~~
##### 標識符
C標識符是用來標識變量、函數,或任何其他用戶自定義項目的名稱。一個標識符以字母 A-Z 或 a-z 或下劃線 \_ 開始,后跟零個或多個字母、下劃線和數字(0-9)。
C標識符內不允許出現標點字符,比如@、$和%。C是區分大小寫的編程語言。因此,在C中,Manpower和 manpower是兩個不同的標識符。下面列出幾個有效的標識符:
~~~
mohd ? ? ? zara ? abc ? move_name a_123
myname50 ? _temp ? j ? ? a23b9 ? ? retVal
~~~
##### 關鍵字
下表列出了 C 中的保留字。這些保留字不能作為常量名、變量名或其他標識符名稱。
| 關鍵字 | 說明 |
| --- | --- |
| auto | 聲明自動變量 |
| break | 跳出當前循環 |
| case | 開關語句分支 |
| char | 聲明字符型變量或函數返回值類型 |
| const | 定義常量,如果一個變量被 const 修飾,那么它的值就不能再被改變 |
| continue | 結束當前循環,開始下一輪循環 |
| default | 開關語句中的"其它"分支 |
| do | 循環語句的循環體 |
| double | 聲明雙精度浮點型變量或函數返回值類型 |
| else | 條件語句否定分支(與 if 連用) |
| enum | 聲明枚舉類型 |
| extern | 聲明變量或函數是在其它文件或本文件的其他位置定義 |
| float | 聲明浮點型變量或函數返回值類型 |
| for | 一種循環語句 |
| goto | 無條件跳轉語句 |
| if | 條件語句 |
| int | 聲明整型變量或函數 |
| long | 聲明長整型變量或函數返回值類型 |
| register | 聲明寄存器變量 |
| return | 子程序返回語句(可以帶參數,也可不帶參數) |
| short | 聲明短整型變量或函數 |
| signed | 聲明有符號類型變量或函數 |
| sizeof | 計算數據類型或變量長度(即所占字節數) |
| static | 聲明靜態變量 |
| struct | 聲明結構體類型 |
| switch | 用于開關語句 |
| typedef | 用以給數據類型取別名 |
| unsigned | 聲明無符號類型變量或函數 |
| union | 聲明共用體類型 |
| void | 聲明函數無返回值或無參數,聲明無類型指針 |
| volatile | 說明變量在程序執行中可被隱含地改變 |
| while | 循環語句的循環條件 |
##### C99 新增關鍵字
`_Bool` `_Complex` `_Imaginary` `inline` `restrict`
##### C11 新增關鍵字
`_Alignas` `_Alignof` `_Atomic` `_Generic` `_Noreturn` `_Static_assert` `_Thread_local`
##### 空格
只包含空格的行,被稱為空白行,可能帶有注釋,C編譯器會完全忽略它。
在C中,空格用于描述空白符、制表符、換行符和注釋。空格分隔語句的各個部分,讓編譯器能識別語句中的某個元素(比如 int)在哪里結束,下一個元素在哪里開始。因此,在下面的語句中:
~~~
int age;
~~~
在這里,int 和 age 之間必須至少有一個空格字符(通常是一個空白符),這樣編譯器才能夠區分它們。另一方面,在下面的語句中:
~~~
fruit = apples + oranges; ? // 獲取水果的總數
~~~
fruit 和 =,或者 = 和 apples 之間的空格字符不是必需的,但是為了增強可讀性,您可以根據需要適當增加一些空格。
#### 10.3 C的數據類型
C的數據類型分有4種:
`基本類型`、`枚舉類型`、`派生類型`、`空類型`
基本類型有2種:
整型、實型(浮點型)
其中
整型分為:基本整型(int 2-4B)、短整型(short int 2B)、長整型(long int 4B)、雙長整型(longlong int 8B)、布爾型(bool)
實型(浮點型)有三種:單精度浮點型(float)、雙精度浮點型(double)、復數浮點型
派生類型有3種:
數組類型、結構體類型、共用體類型、指針類型、函數類型
##### 整數(或的情況是不同的系統,例如32位和64位的差別):
| char | 1 字節 | \-128 到 127 或 0 到 255 |
| --- | --- | --- |
| unsigned char(無符號) | 1 字節 | 0 到 255 |
| signed char | 1 字節 | \-128 到 127 |
| int | 2 或 4 字節 | \-32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647 |
| unsigned int(無符號) | 2 或 4 字節 | 0 到 65,535 或 0 到 4,294,967,295 |
| short | 2 字節 | \-32,768 到 32,767 |
| unsigned short(無符號) | 2 字節 | 0 到 65,535 |
| long | 4 字節 | \-2,147,483,648 到 2,147,483,647 |
| unsigned long(無符號) | 4 字節 | 0 到 4,294,967,295 |
##### 浮點類型
下表列出了關于標準浮點類型的存儲大小、值范圍和精度的細節:
| 類型 | 存儲大小 | 值范圍 | 精度 |
| --- | --- | --- | --- |
| float | 4 字節 | 1.2E-38 到 3.4E+38 | 6 位小數 |
| double | 8 字節 | 2.3E-308 到 1.7E+308 | 15 位小數 |
| long double | 16 字節 | 3.4E-4932 到 1.1E+4932 | 19 位小數 |
頭文件 float.h 定義了宏,在程序中可以使用這些值和其他有關實數二進制表示的細節。下面的實例將輸出浮點類型占用的存儲空間以及它的范圍。
~~~
#include <stdio.h>
#include <float.h>
int main()
{
? printf("float 存儲最大字節數 : %lu \n", sizeof(float));
? printf("float 最小值: %E\n", FLT_MIN );
? printf("float 最大值: %E\n", FLT_MAX );
? printf("精度值: %d\n", FLT_DIG );
?
? return 0;
}
~~~
:-: 
##### viod類型
void 類型(空類型)指定沒有可用的值。它通常用于以下三種情況下:
| 序號 | 類型與描述 |
| --- | --- |
| 1 | 函數返回為空 C 中有各種函數都不返回值,或者您可以說它們返回空。不返回值的函數的返回類型為空。例如 void exit (int status); |
| 2 | 函數參數為空C 中有各種函數不接受任何參數。不帶參數的函數可以接受一個 void。例如 int rand(void); |
| 3 | 指針指向 void 類型為 void \* 的指針代表對象的地址,而不是類型。例如,內存分配函數 void malloc( size\_t size ); 返回指向 void 的指針,可以轉換為任何數據類型。 |
#### 10.4 C 變量
`變量其實只不過是程序可操作的存儲區的名稱`。C 中每個變量都有特定的類型,類型決定了變量存儲的大小和布局,該范圍內的值都可以存儲在內存中,運算符可應用于變量上。
變量的名稱可以由字母、數字和下劃線字符組成。它必須以字母或下劃線開頭。大寫字母和小寫字母是不同的,因為 C 是大小寫敏感的。基于前一章講解的基本類型,有以下幾種基本的變量類型:
| 類型 | 描述 |
| --- | --- |
| char | 通常是一個字節(八位)。這是一個整數類型。 |
| int | 對機器而言,整數的最自然的大小。 |
| float | 單精度浮點值。單精度是這樣的格式,1位符號,8位指數,23位小數。 |
| double | 雙精度浮點值。雙精度是1位符號,11位指數,52位小數。 |
| void | 表示類型的缺失。 |
##### 變量定義
語法:
~~~
type variable_list;
~~~
type 是數據類型,variable\_list是變量名稱列表。
可以在C程序中申明變量類型和變量
~~~
int ? i, j, k; ? ? //數據類型是int整型,變量名分別是i,j,k
char ? c, ch; //數據類型是字符型,變量名分別是c,ch
float f, salary; ? //數據類型是單精度浮點,變量名分別是f,salary
double d; ? ? ? ? ? //數據類型是雙精度浮點,變量名只有d
~~~
也可以在C程序中申明變量類和變量的同時,為變量初始化一個值
~~~
extern int d = 3, f = 5; ? // d 和 f 的聲明與初始化
int d = 3, f = 5; ? ? ? ? ? // 定義并初始化 d 和 f
byte z = 22; ? ? ? ? ? ? ? // 定義并初始化 z
char x = 'x'; ? ? ? ? ? ? ? // 變量 x 的值為 'x'
~~~
不帶初始化的定義:帶有靜態存儲持續時間的變量會被隱式初始化為 NULL(所有字節的值都是 0),其他所有變量的初始值是未定義的。
##### auto變量
auto關鍵字用戶定義一個局部變量為自動的,意味著每次執行到該變量時都會產生一個新的變量,并且重新對其進行初始化。事實上:關鍵字auto是可以省略的,如果不特別指定,局部變量存儲方式默認是auto。
~~~
#include<stdio.h>
?
AddOne()
{
? auto int ilnt = 1;
? ilnt = ilnt + 1;
? printf("%d\n",ilnt);
}
?
int main()
{
? printf("第一次用:");
? AddOne();
? printf("第二次用:");
? AddOne();
}
?
~~~
:-: 
第一次調用:2
第二次調用:2
##### static變量
static變量是靜態變量,將函數的內部變量和外部變量聲明成static變量的意義是不一樣的(作用域)。
在作用域內(同一個函數空間或同域內),static變量將始終保持它的值。并且初始化操作只在第一次執行時其作用。在隨后的運行過程中,變量將保持語塊上一次執行的值。
~~~
#include<stdio.h>
?
AddOne()
{
? static int ilnt = 1;
? ilnt = ilnt + 1;
? printf("%d\n",ilnt);
}
?
int main()
{
? printf("第一次用:");
? AddOne();
? printf("第二次用:");
? AddOne();
}
~~~
:-: 
第一次調用:2
第二次調用:3
因為它們的變量ilnt的作用域都在mian函數塊里。
##### register變量
register變量稱為寄存器變量。表示這個變量是存在CPU中的寄存器里的,不占內存。因此運算速度會更快。
~~~
#include<stdio.h>
?
int main()
{
? register int ilnt; //定義寄存器變量類型
? ilnt = 100;
? printf("%d\n",ilnt);
? return 0;
}
~~~
:-: 
##### exterm變量
exterm變量稱為外部存儲變量。exterm聲明了程序中將要用到但尚未定義的外部變量。通常,外部存儲類都用于聲明在另一個轉換單元中定義的變量。
~~~
/*
在Extern1文件中
--------
*/
#include<stdio.h>
int main()
{
? exterm int iExterm; ? //定義外部變量類型
? printf("%d",iExterm);
? return 0;
}
?
/*
在Extern2文件中
--------
*/
#include<stdio.h>
int iExterm = 100; //定義一個整型變量,賦值為100
?
~~~
##### 變量的作用域規則
任何一種編程中,作用域是程序中定義的變量所存在的區域,超過該區域變量就不能被訪問。C 語言中有三個地方可以聲明變量:
1. 在函數或塊內部的**局部**變量
2. 在所有函數外部的**全局**變量
3. 在**形式**參數的函數參數定義中
##### 局部變量
在某個函數或塊的內部聲明的變量稱為局部變量。它們只能被該函數或該代碼塊內部的語句使用。局部變量在函數外部是不可知的。下面是使用局部變量的實例。在這里,所有的變量 a、b 和 c 是 main() 函數的局部變量。
~~~
#include <stdio.h>
int main ()
{
/* 局部變量聲明 */
int a, b;
int c;
/* 實際初始化 */
a = 10;
b = 20;
c = a + b;
printf ("value of a = %d, b = %d and c = %d\n", a, b, c);
return 0;
}
~~~
##### 全局變量
全局變量是定義在函數外部,通常是在程序的頂部。全局變量在整個程序生命周期內都是有效的,在任意的函數內部能訪問全局變量。
全局變量可以被任何函數訪問。也就是說,全局變量在聲明后整個程序都是可用的。下面是使用全局變量和局部變量的實例:
~~~
#include <stdio.h>
/* 全局變量聲明 */
int g;
int main ()
{
/* 局部變量聲明 */
int a, b;
/* 實際初始化 */
a = 10;
b = 20;
g = a + b;
printf ("value of a = %d, b = %d and g = %d\n", a, b, g);
return 0;
}
~~~
在程序中,局部變量和全局變量的名稱可以相同,但是在函數內,如果兩個名字相同,會使用局部變量值,全局變量不會被使用。下面是一個實例:
~~~
#include <stdio.h>
/* 全局變量聲明 */
int g = 20;
int main ()
{
/* 局部變量聲明 */
int g = 10;
printf ("value of g = %d\n", g);
return 0;
}
~~~
結果
~~~
value of g = 10
~~~
##### 形式參數
函數的參數,形式參數,被當作該函數內的局部變量,如果與全局變量同名它們會優先使用。下面是一個實例:
實例:
~~~
#include <stdio.h>
/* 全局變量聲明 */
int a = 20;
int main ()
{
/* 在主函數中的局部變量聲明 */
int a = 10;
int b = 20;
int c = 0;
int sum(int, int);
printf ("value of a in main() = %d\n", a);
c = sum( a, b);
printf ("value of c in main() = %d\n", c);
return 0;
}
/* 添加兩個整數的函數 */
int sum(int a, int b)
{
? printf ("value of a in sum() = %d\n", a);
? printf ("value of b in sum() = %d\n", b);
? return a + b;
}
~~~
當上面的代碼被編譯和執行時,它會產生下列結果:
~~~
value of a in main() = 10
value of a in sum() = 10
value of b in sum() = 20
value of c in main() = 30
~~~
**全局變量與局部變量在內存中的區別**:
* 全局變量保存在內存的全局存儲區中,占用靜態的存儲單元;
* 局部變量保存在棧中,只有在所在函數被調用時才動態地為變量分配存儲單元。
##### 初始化局部變量和全局變量
當局部變量被定義時,系統不會對其初始化,您必須自行對其初始化。定義全局變量時,系統會自動對其初始化,如下所示:
| 數據類型 | 初始化默認值 |
| --- | --- |
| int | 0 |
| char | '\\0' |
| float | 0 |
| double | 0 |
| pointer | NULL |
##### 棧區和堆區(程序中內存布局)
C/C++程序內存布局:
:-: 
一個由C/C++編譯的程序占用的內存分為以下幾個部分,
1)`全局區(靜態區)(static)`存放全局變量、靜態數據,const常量。程序結束后由系統釋放。
2)`棧區(stack)` 函數運行時分配,函數結束時釋放。由編譯器自動分配釋放 ,存放為運行函數而分配的局部變量、函數參數、返回數據、返回地址等。其操作方式類似于數據結構中的棧。
3)`堆區(heap)` 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS(操作系統)回收。分配方式類似于鏈表。
4)`文字常量區` 常量字符串就是放在這里的。 程序結束后由系統釋放。
5)`程序代碼區`存放函數體(類成員函數和全局函數)的二進制代碼。
##### 靜態存儲和動態存儲
根據變量的產生時間,可將其分為靜態存儲和動態存儲。
靜態存儲是指程序運行時為其分配的固定的存儲空間,動態存儲則是在程序運行期間根據需要動態分配的存儲空間。
#### 10.5 C的常量
常量是固定值,在程序執行期間不會改變。這些固定的值,又叫做**字面量**。
常量可以是任何的基本數據類型,比如整數常量、浮點常量、字符常量,或字符串字面值,也有枚舉常量。
**常量**就像是常規的變量,只不過常量的值在定義后不能進行修改。
##### C的整數常量
`前綴`指定基數:0x或者0X表示十六進制,0表示八進制,不帶前綴表示十進制。
`后綴`指定:后綴U表示無符號整數,后綴L表示長整數。后綴可大寫可小寫,U、L順序任意。
~~~
212 ? ? ? ? /* 合法的 */
215u ? ? ? /* 合法的 */
0xFeeL ? ? /* 合法的 */
078 ? ? ? ? /* 非法的:8 不是八進制的數字 */
032UU ? ? ? /* 非法的:不能重復后綴 */
?
85 ? ? ? ? /* 十進制 */
0213 ? ? ? /* 八進制 */
0x4b ? ? ? /* 十六進制 */
30 ? ? ? ? /* 整數 */
30u ? ? ? /* 無符號整數 */
30l ? ? ? /* 長整數 */
30ul ? ? ? /* 無符號長整數 */
~~~
##### C的浮點常量
浮點常量由整數部分、小數點、小數部分和指數部分組成。您可以使用小數形式或者指數形式來表示浮點常量。
當使用小數形式表示時,必須包含整數部分、小數部分,或同時包含兩者。當使用指數形式表示時, 必須包含小數點、指數,或同時包含兩者。帶符號的指數是用 e 或 E 引入的。
~~~
3.14159 ? ? ? /* 合法的 */
314159E-5L ? /* 合法的 */
510E ? ? ? ? /* 非法的:不完整的指數 */
210f ? ? ? ? /* 非法的:沒有小數或指數 */
.e55 ? ? ? ? /* 非法的:缺少整數或分數 */
~~~
##### 字符常量
`字符常量是括在單引號中`,例如,'x' 可以存儲在 **char** 類型的簡單變量中。
字符常量可以是一個普通的字符(例如 'x')、一個轉義序列(例如 '\\t'),或一個通用的字符(例如 '\\u02C0')。
在 C 中,有一些特定的字符,當它們前面有反斜杠時,它們就具有特殊的含義,被用來表示如換行符(\\n)或制表符(\\t)等。下表列出了一些這樣的轉義序列碼:
| 轉義序列 | 含義 |
| --- | --- |
| \\ | \\ 字符 |
| ' | ' 字符 |
| " | " 字符 |
| \\? | ? 字符 |
| \\a | 警報鈴聲 |
| \\b | 退格鍵 |
| \\f | 換頁符 |
| \\n | 換行符 |
| \\r | 回車 |
| \\t | 水平制表符 |
| \\v | 垂直制表符 |
| \\ooo | 一到三位的八進制數 |
| \\xhh . . . | 一個或多個數字的十六進制數 |
~~~
#include <stdio.h>
int main()
{
? printf("Hello\tWorld\n\n two");
? return 0;
}
~~~
c產生一次制表符和兩次換行符。
:-: 
##### 字符串常量
字符串字面值或常量是括在雙引號 "" 中的。一個字符串包含類似于字符常量的字符:普通的字符、轉義序列和通用的字符。
您可以使用空格做分隔符,把一個很長的字符串常量進行分行。
下面的實例顯示了一些字符串常量。下面這三種形式所顯示的字符串是相同的。
~~~
"hello, dear"
?
"hello, \
?
dear"
?
"hello, " "d" "ear"
~~~
##### 定義常量
在 C 中,有兩種簡單的定義常量的方式:
1. 使用 **#define** 預處理器。
2. 使用 **const** 關鍵字。
#difine預處理器
~~~
#include <stdio.h>
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
int main()
{
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
~~~
const關鍵字
~~~
#include <stdio.h>
int main()
{
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
~~~
#### C運算符
* 算術運算符
* 關系運算符
* 邏輯運算符
* 位運算符
* 賦值運算符
* 雜項運算符
### 注意
C語言中變量進行運算會伴隨`自動類型轉換`。也就是兩個聲明數據類型不一樣的變量進行運算時會伴隨數據類型自動轉換。這時候可以用強制轉換進行操作。
~~~
float a =10.1f;
int j = (int)i;
~~~
算術運算符
下表顯示了 C 語言支持的所有算術運算符。假設變量 **A** 的值為 10,變量 **B** 的值為 20,則:
| 運算符 | 描述 | 實例 |
| --- | --- | --- |
| + | 把兩個操作數相加 | A + B 將得到 30 |
| \- | 從第一個操作數中減去第二個操作數 | A - B 將得到 -10 |
| \* | 把兩個操作數相乘 | A \* B 將得到 200 |
| / | 分子除以分母 | B / A 將得到 2 |
| % | 取模運算符,整除后的余數 | B % A 將得到 0 |
| ++ | 自增運算符,整數值增加 1 | A++ 將得到 11 |
| \-- | 自減運算符,整數值減少 1 | A-- 將得到 9 |
關系運算符
下表顯示了 C 語言支持的所有關系運算符。假設變量 **A** 的值為 10,變量 **B** 的值為 20,則:
| 運算符 | 描述 | 實例 |
| --- | --- | --- |
| \== | 檢查兩個操作數的值是否相等,如果相等則條件為真。 | (A == B) 為假。 |
| != | 檢查兩個操作數的值是否相等,如果不相等則條件為真。 | (A != B) 為真。 |
| \> | 檢查左操作數的值是否大于右操作數的值,如果是則條件為真。 | (A > B) 為假。 |
| < | 檢查左操作數的值是否小于右操作數的值,如果是則條件為真。 | (A < B) 為真。 |
| \>= | 檢查左操作數的值是否大于或等于右操作數的值,如果是則條件為真。 | (A >= B) 為假。 |
| <= | 檢查左操作數的值是否小于或等于右操作數的值,如果是則條件為真。 | (A <= B) 為真。 |
邏輯運算符
下表顯示了 C 語言支持的所有關系邏輯運算符。假設變量 **A** 的值為 1,變量 **B** 的值為 0,則:
| 運算符 | 描述 | 實例 |
| --- | --- | --- |
| && | 稱為邏輯與運算符。如果兩個操作數都非零,則條件為真。 | (A && B) 為假。 |
| || | 稱為邏輯或運算符。如果兩個操作數中有任意一個非零,則條件為真。 | (A || B) 為真。 |
| ! | 稱為邏輯非運算符。用來逆轉操作數的邏輯狀態。如果條件為真則邏輯非運算符將使其為假。 | !(A && B) 為真。 |
位運算符
假設如果 A = 60,且 B = 13,現在以二進制格式表示,它們如下所示:
A = 0011 1100
B = 0000 1101
\-\----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
~A = 1100 0011
| 運算符 | 描述 | 實例 |
| --- | --- | --- |
| & | 按位與操作,按二進制位進行"與"運算。運算規則:`0&0=0; 0&1=0; 1&0=0; 1&1=1;` | (A & B) 將得到 12,即為 0000 1100 |
| | | 按位或運算符,按二進制位進行"或"運算。運算規則:`0|0=0; 0|1=1; 1|0=1; 1|1=1;` | (A | B) 將得到 61,即為 0011 1101 |
| ^ | 異或運算符,按二進制位進行"異或"運算。運算規則:`0^0=0; 0^1=1; 1^0=1; 1^1=0;` | (A ^ B) 將得到 49,即為 0011 0001 |
| ~ | 取反運算符,按二進制位進行"取反"運算。運算規則:`~1=0; ~0=1;` | (~A ) 將得到 -61,即為 1100 0011,一個有符號二進制數的補碼形式。 |
| << | 二進制左移運算符。將一個運算對象的各二進制位全部左移若干位(左邊的二進制位丟棄,右邊補0)。 | A << 2 將得到 240,即為 1111 0000 |
| \>> | 二進制右移運算符。將一個數的各二進制位全部右移若干位,正數左補0,負數左補1,右邊丟棄。 | A >> 2 將得到 15,即為 0000 1111 |
賦值運算符
下表列出了 C 語言支持的賦值運算符:
| 運算符 | 描述 | 實例 |
| --- | --- | --- |
| \= | 簡單的賦值運算符,把右邊操作數的值賦給左邊操作數 | C = A + B 將把 A + B 的值賦給 C |
| += | 加且賦值運算符,把右邊操作數加上左邊操作數的結果賦值給左邊操作數 | C += A 相當于 C = C + A |
| \-= | 減且賦值運算符,把左邊操作數減去右邊操作數的結果賦值給左邊操作數 | C -= A 相當于 C = C - A |
| \*= | 乘且賦值運算符,把右邊操作數乘以左邊操作數的結果賦值給左邊操作數 | C \*= A 相當于 C = C \* A |
| /= | 除且賦值運算符,把左邊操作數除以右邊操作數的結果賦值給左邊操作數 | C /= A 相當于 C = C / A |
| %= | 求模且賦值運算符,求兩個操作數的模賦值給左邊操作數 | C %= A 相當于 C = C % A |
| <<= | 左移且賦值運算符 | C <<= 2 等同于 C = C << 2 |
| \>>= | 右移且賦值運算符 | C >>= 2 等同于 C = C >> 2 |
| &= | 按位與且賦值運算符 | C &= 2 等同于 C = C & 2 |
| ^= | 按位異或且賦值運算符 | C ^= 2 等同于 C = C ^ 2 |
| |\= | 按位或且賦值運算符 | C |\= 2 等同于 C = C | 2 |
雜項運算符 ? sizeof & 三元
下表列出了 C 語言支持的其他一些重要的運算符,包括 **sizeof** 和 **? :**。
| 運算符 | 描述 | 實例 |
| --- | --- | --- |
| sizeof() | 返回變量的大小。 | sizeof(a) 將返回 4,其中 a 是整數。 |
| & | 返回變量的地址。 | &a; 將給出變量的實際地址。 |
| \* | 指向一個變量。 | \*a; 將指向一個變量。 |
| ? : | 條件表達式 | 如果條件為真 ? 則值為 X : 否則值為 Y |
C 中的運算符優先級
運算符的優先級確定表達式中項的組合。這會影響到一個表達式如何計算。某些運算符比其他運算符有更高的優先級,例如,乘除運算符具有比加減運算符更高的優先級。
例如 x = 7 + 3 \* 2,在這里,x 被賦值為 13,而不是 20,因為運算符 \* 具有比 + 更高的優先級,所以首先計算乘法 3\*2,然后再加上 7。
下表將按運算符優先級從高到低列出各個運算符,具有較高優先級的運算符出現在表格的上面,具有較低優先級的運算符出現在表格的下面。在表達式中,較高優先級的運算符會優先被計算。
| 類別 | 運算符 | 結合性 |
| --- | --- | --- |
| 后綴 | () \[\] -> . ++ - - | 從左到右 |
| 一元 | \+ - ! ~ ++ - - (type)\* & sizeof | 從右到左 |
| 乘除 | \* / % | 從左到右 |
| 加減 | \+ - | 從左到右 |
| 移位 | > | 從左到右 |
| 關系 | >= | 從左到右 |
| 相等 | \== != | 從左到右 |
| 位與 AND | & | 從左到右 |
| 位異或 XOR | ^ | 從左到右 |
| 位或 OR | | | 從左到右 |
| 邏輯與 AND | && | 從左到右 |
| 邏輯或 OR | || | 從左到右 |
| 條件 | ?: | 從右到左 |
| 賦值 | \= += -= \*= /= %=>>= <<= &= ^= |\= | 從右到左 |
| 逗號 | , | 從左到右 |
#### 10.7 流程控制
#### 判斷
C 語言提供了以下類型的判斷語句。點擊鏈接查看每個語句的細節。
| 語句 | 描述 |
| --- | --- |
| if 語句 | 一個 **if 語句** 由一個布爾表達式后跟一個或多個語句組成。 |
| if...else 語句 | 一個 **if 語句** 后可跟一個可選的 **else 語句**,else 語句在布爾表達式為假時執行。 |
| 嵌套 if 語句 | 您可以在一個 **if** 或 **else if** 語句內使用另一個 **if** 或 **else if** 語句。 |
| switch 語句 | 一個 **switch** 語句允許測試一個變量等于多個值時的情況。 |
| 嵌套 switch 語句 | 您可以在一個 **switch** 語句內使用另一個 **switch** 語句。 |
? : 運算符(三元運算符)
我們已經在前面的章節中講解了 **條件運算符 ? :**,可以用來替代 **if...else** 語句。它的一般形式如下:
~~~
Exp1 ? Exp2 : Exp3;
~~~
~~~
#include <stdio.h>
int main()
{
int a = 1;
if(a==1){
printf("%d\n",a);
}else{
printf("a!=1");
}
//上面這的可以這樣寫
(a==1)?printf("%d\n",a):printf("a!=1");
return 0;
}
~~~
:-: 
##### C 循環
循環類型
C 語言提供了以下幾種循環類型。點擊鏈接查看每個類型的細節。
| 循環類型 | 描述 |
| --- | --- |
| while 循環 | 當給定條件為真時,重復語句或語句組。它會在執行循環主體之前測試條件。 |
| for 循環 | 多次執行一個語句序列,簡化管理循環變量的代碼。 |
| do...while 循環 | 除了它是在循環主體結尾測試條件外,其他與 while 語句類似。 |
| 嵌套循環 | 您可以在 while、for 或 do..while 循環內使用一個或多個循環。 |
循環控制語句
循環控制語句改變你代碼的執行順序。通過它你可以實現代碼的跳轉。
C 提供了下列的循環控制語句。點擊鏈接查看每個語句的細節。
| 控制語句 | 描述 |
| --- | --- |
| break 語句 | 終止**循環**或 **switch** 語句,程序流將繼續執行緊接著循環或 switch 的下一條語句。 |
| continue 語句 | 告訴一個循環體立刻停止本次循環迭代,重新開始下次循環迭代。 |
| goto 語句 | 將控制轉移到被標記的語句。但是不建議在程序中使用 goto 語句。 |
無限循環
如果條件永遠不為假,則循環將變成無限循環。**for** 循環在傳統意義上可用于實現無限循環。由于構成循環的三個表達式中任何一個都不是必需的,您可以將某些條件表達式留空來構成一個無限循環。
~~~
#include <stdio.h>
int main ()
{
for( ; ; )
{
printf("該循環會永遠執行下去!\n");
}
return 0;
}
~~~
特別說明下goto語句
C 語言中的 **goto** 語句允許把控制無條件轉移到同一函數內的被標記的語句。
**注意:**在任何編程語言中,`不建議使用 goto 語句`。因為它使得程序的控制流難以跟蹤,使程序難以理解和難以修改。任何使用 goto 語句的程序可以改寫成不需要使用 goto 語句的寫法。
語法
~~~
goto label;
..
.
label: statement;
~~~
實例
~~~
#include <stdio.h>
int main ()
{
/* 局部變量定義 */
int a = 10;
/* do 循環執行 */
LOOP:do
{
if( a == 15)
{
/* 跳過迭代 */
a = a + 1;
goto LOOP;
}
printf("a 的值: %d\n", a);
a++;
}while( a < 20 );
return 0;
}
~~~
輸出
~~~
a 的值: 10
a 的值: 11
a 的值: 12
a 的值: 13
a 的值: 14
a 的值: 16
a 的值: 17
a 的值: 18
a 的值: 19
~~~
#### 10.8 函數
C和其他高級語言一樣,有自定義函數和內置函數。函數還有很多叫法,比如方法、子例程或程序,等等
* 自定義函數:
您可以把代碼劃分到不同的函數中。如何劃分代碼到不同的函數中是由您來決定的,但在邏輯上,劃分通常是根據每個函數執行一個特定的任務來進行的。
函數**聲明**告訴編譯器函數的名稱、返回類型和參數。函數**定義**提供了函數的實際主體。
* 內置函數:
C 標準庫提供了大量的程序可以調用的內置函數。例如,函數 **strcat()** 用來連接兩個字符串,函數 **memcpy()** 用來復制內存到另一個位置。
##### 自定義函數
語法
~~~
return_type function_name( parameter list )
{
body of the function
}
~~~
在 C 語言中,函數由一個函數頭和一個函數主體組成。下面列出一個函數的所有組成部分:
* **返回類型:**一個函數可以返回一個值。**return\_type** 是函數返回的值的數據類型。有些函數執行所需的操作而不返回值,在這種情況下,return\_type 是關鍵字 **void**。
* **函數名稱:**這是函數的實際名稱。函數名和參數列表一起構成了函數簽名。
* **參數:**參數就像是占位符。當函數被調用時,您向參數傳遞一個值,這個值被稱為實際參數。參數列表包括函數參數的類型、順序、數量。參數是可選的,也就是說,函數可能不包含參數。
* **函數主體:**函數主體包含一組定義函數執行任務的語句
例子
~~~
/* 函數返回兩個數中較大的那個數 */
int max(int num1, int num2)
{
/* 局部變量聲明 */
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
~~~
##### 調用函數
創建 C 函數時,會定義函數做什么,然后通過調用函數來完成已定義的任務。
當程序調用函數時,程序控制權會轉移給被調用的函數。被調用的函數執行已定義的任務,當函數的返回語句被執行時,或到達函數的結束括號時,會把程序控制權交還給主程序。
調用函數時,傳遞所需參數,如果函數返回一個值,則可以存儲返回值
~~~
#include <stdio.h>
/* 函數聲明 */
int max(int num1, int num2);
int main ()
{
/* 局部變量定義 */
int a = 100;
int b = 200;
int ret;
/* 調用函數來獲取最大值 */
ret = max(a, b);
printf( "Max value is : %d\n", ret );
return 0;
}
/* 函數返回兩個數中較大的那個數 */
int max(int num1, int num2)
{
/* 局部變量聲明 */
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
~~~
結果
~~~
Max value is : 200
~~~
#### 函數可變參數
有時,您可能會碰到這樣的情況,您希望函數帶有可變數量的參數,而不是預定義數量的參數。C 語言為這種情況提供了一個解決方案,它允許您定義一個函數,能根據具體的需求接受可變數量的參數。下面的實例演示了這種函數的定義。
~~~
int func(int num,...){
? ...
}
int main()
{
func(2, 2, 3);
func(3, 2, 3, 4);
}
~~~
注意:func的第一個int類型參數num代表將要傳入的參數的數量。
#### 10.9 數組
C 語言支持**數組**數據結構,它可以存儲一個`固定大小的相同類型元素的`順序集合。數組是用來存儲一系列數據,但它往往被認為是一系列相同類型的變量。
數組的聲明并不是聲明一個個單獨的變量,比如 number0、number1、...、number99,而是聲明一個數組變量,比如 numbers,然后使用 numbers\[0\]、numbers\[1\]、...、numbers\[99\] 來代表一個個單獨的變量。數組中的特定元素可以通過索引訪問。
所有的數組都是由連續的內存位置組成。最低的地址對應第一個元素,最高的地址對應最后一個元素。
:-: 
##### 聲明數組
在 C 中要聲明一個數組,需要指定元素的類型和元素的數量,如下所示:
~~~
type arrayName [ arraySize ];
~~~
例子
~~~
double balance[10];
~~~
現在 balance 是一個可用的數組,可以容納 10 個類型為 double 的數字。
##### 始化數組
在 C 中,您可以逐個初始化數組,也可以使用一個初始化語句,如下所示:
~~~
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
~~~
大括號 { } 之間的值的數目不能大于我們在數組聲明時在方括號 \[ \] 中指定的元素數目。
果您省略掉了數組的大小,數組的大小則為初始化時元素的個數。因此,如果:
~~~
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
~~~
您將創建一個數組,它與前一個實例中所創建的數組是完全相同的。
為數組中某個元素賦值的實例:
~~~
balance[4] = 50.0;
~~~
上述的語句把數組中第五個元素的值賦為 50.0。所有的數組都是以 0 作為它們第一個元素的索引,也被稱為基索引,數組的最后一個索引是數組的總大小減去 1。以下是上面所討論的數組的的圖形表示:
:-: 
##### 訪問數組元素
數組元素可以通過數組名稱加索引進行訪問。元素的索引是放在方括號內,跟在數組名稱的后邊。例如:
~~~
double salary = balance[9];
~~~
#### 多維數組
語法
~~~
type name[size1][size2]...[sizeN];
~~~
例如,下面的聲明創建了一個三維 5 . 10 . 4 整型數組:
~~~
int threedim[5][10][4];
~~~
##### 二維數組
~~~
type arrayName [ x ][ y ];
~~~
其中,type可以是任意有效的C數據類型,arrayName 是一個有效的C標識符。一個二維數組可以被認為是一個帶有 x 行和 y 列的表格。下面是一個二維數組,包含 3 行和 4 列:
~~~
int x[3][4];
~~~
:-: 
因此,數組中的每個元素是使用形式為 a\[ i , j \] 的元素名稱來標識的,其中 a 是數組名稱,i 和 j 是唯一標識 a 中每個元素的下標。
初始化二維數組
多維數組可以通過在括號內為每行指定值來進行初始化。下面是一個帶有 3 行 4 列的數組。
~~~
int a[3][4] = {
{0, 1, 2, 3} , /* 初始化索引號為 0 的行 */
{4, 5, 6, 7} , /* 初始化索引號為 1 的行 */
{8, 9, 10, 11} /* 初始化索引號為 2 的行 */
};
~~~
內部嵌套的括號是可選的,下面的初始化與上面是等同的:
~~~
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
~~~
訪問二維數組元素
二維數組中的元素是通過使用下標(即數組的行索引和列索引)來訪問的。例如:
~~~
int val = a[2][3];
~~~
我們將使用嵌套循環來處理二維數組
~~~
#include <stdio.h>
int main ()
{
/* 一個帶有 5 行 2 列的數組 */
int a[5][2] = { {0,0}, {1,2}, {2,4}, {3,6},{4,8}};
int i, j;
/* 輸出數組中每個元素的值 */
for ( i = 0; i < 5; i++ )
{
for ( j = 0; j < 2; j++ )
{
printf("a[%d][%d] = %d\n", i,j, a[i][j] );
}
}
return 0;
}
~~~
當上面的代碼被編譯和執行時,它會產生下列結果:
~~~
a[0][0] = 0
a[0][1] = 0
a[1][0] = 1
a[1][1] = 2
a[2][0] = 2
a[2][1] = 4
a[3][0] = 3
a[3][1] = 6
a[4][0] = 4
a[4][1] = 8
~~~
#### 10.10 enum(枚舉)
語法
~~~
enum 枚舉名 {枚舉元素1,枚舉元素2,……};
~~~
枚舉是 C 語言中的一種基本數據類型,它可以讓數據更簡潔,更易讀。
枚舉語法定義格式為:
~~~
enum 枚舉名 {枚舉元素1,枚舉元素2,……};
~~~
接下來我們舉個例子,比如:一星期有 7 天,如果不用枚舉,我們需要使用 #define 來為每個整數定義一個別名:
~~~
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
~~~
這個看起來代碼量就比較多,接下來我們看看使用枚舉的方式:
~~~
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
~~~
這樣看起來是不是更簡潔了。
**注意:**第一個枚舉成員的默認值為整型的 0,后續枚舉成員的值在前一個成員上加 1。我們在這個實例中把第一個枚舉成員的值定義為 1,第二個就為 2,以此類推。
> 可以在定義枚舉類型時改變枚舉元素的值:
>
> ~~~
> enum season {spring, summer=3, autumn, winter};
>
> ~~~
>
> 沒有指定值的枚舉元素,其值為前一元素加 1。也就說 spring 的值為 0,summer 的值為 3,autumn 的值為 4,winter 的值為 5
##### 枚舉變量的定義
前面我們只是聲明了枚舉類型,接下來我們看看如何定義枚舉變量。
我們可以通過以下三種方式來定義枚舉變量
**1、先定義枚舉類型,再定義枚舉變量**
~~~
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
~~~
**2、定義枚舉類型的同時定義枚舉變量**
~~~
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
~~~
**3、省略枚舉名稱,直接定義枚舉變量**
~~~
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
~~~
實例
~~~
#include <stdio.h>
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
int main()
{
enum DAY day;
day = WED;
printf("%d\n",day);
return 0;
}
~~~
:-: 
在C 語言中,枚舉類型是被當做 int 或者 unsigned int 類型來處理的,所以按照 C 語言規范是沒有辦法遍歷枚舉類型的。(一般請款下數組可以拿來遍歷)
不過在一些特殊的情況下,枚舉類型必須連續是可以實現有條件的遍歷。
以下實例使用 for 來遍歷枚舉的元素:
實例
~~~
#include <stdio.h>
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main()
{
// 遍歷枚舉元素
for (day = MON; day <= SUN; day++) {
printf("枚舉元素:%d \n", day);
}
}
~~~
以上實例輸出結果為:
~~~
枚舉元素:1
枚舉元素:2
枚舉元素:3
枚舉元素:4
枚舉元素:5
枚舉元素:6
枚舉元素:7
~~~
以下枚舉類型不連續,這種枚舉無法遍歷。
~~~
enum
{
ENUM_0,
ENUM_10 = 10,
ENUM_11
};
~~~
##### 將整數轉換為枚舉
以下實例將整數轉換為枚舉:
實例
~~~
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum day
{
saturday,
sunday,
monday,
tuesday,
wednesday,
thursday,
friday
} workday;
int a = 1;
enum day weekend;
weekend = ( enum day ) a; //類型轉換
//weekend = a; //錯誤
printf("weekend:%d",weekend);
return 0;
}
~~~
:-: 
輸出weekend:1。
#### 10.11 指針
正如您所知道的,每一個變量都有一個內存位置,每一個內存位置都定義了可使用`&` 運算符訪問的地址,它表示了在內存中的一個地址。
~~~
#include <stdio.h>
int main ()
{
int var_runoob = 10;
int *p; // 定義指針變量
p = &var_runoob;
printf("var_runoob 變量的地址: %p\n", p);
printf("var_runoob 變量的值為: %p\n", *p);
printf("var_runoob 變量的地址: %d\n", p);
printf("var_runoob 變量的值為: %d\n", *p);
return 0;
}
~~~
輸出
~~~
var_runoob 變量的地址: 0019FF2C
var_runoob 變量的值為: 0000000A
var_runoob 變量的地址: 1703724
var_runoob 變量的值為: 10
~~~
:-: 
什么是指針?
指針是一個變量,其值為另一個變量的地址。
注意:定義指針的數據類型要和指向的數據的數據類型一致。比如指針指向一個double a,這個指向a的指針\*p
應該申明為 double \*p;且謹記把指向的變量的地址賦值給指針變量。
~~~
type *var-name;
~~~
type是指針的基類型,它必須是一個有效的C數據類型,var-name是指針變量的名稱。用來聲明指針的星號 \* 與乘法中使用的星號是相同的。但是,在這個語句中,星號是用來指定一個變量是指針。以下是有效的指針聲明:
~~~
int *ip; /* 一個整型的指針 */
double *dp; /* 一個 double 型的指針 */
float *fp; /* 一個浮點型的指針 */
char *ch; /* 一個字符型的指針 */
~~~
##### \*間接取值符
例子
~~~
#include <stdio.h>
int main ()
{
int var = 20; /* 實際變量的聲明 */
int *ip; /* 指針變量的聲明 */
ip = &var; /* 在指針變量中存儲 var 的地址 */
printf("var 變量的地址: %p\n", &var );
/* 在指針變量中存儲的地址 */
printf("ip 變量存儲的地址: %p\n", ip );
/* 使用指針訪問值 */
printf("*ip 變量的值: %d\n", *ip );
return 0;
}
~~~
當上面的代碼被編譯和執行時,它會產生下列結果:
~~~
var 變量的地址: 0x7ffeeef168d8
ip 變量存儲的地址: 0x7ffeeef168d8
*ip 變量的值: 20
~~~
這里的ip直接取到了ip變量存儲的變量地址里的值,所以\*又稱為間接取值符號。
##### NULL 指針
在變量聲明的時候,如果沒有確切的地址可以賦值,為指針變量賦一個 NULL 值是一個良好的編程習慣。賦為 NULL 值的指針被稱為空指針。
NULL 指針是一個定義在標準庫中的值為零的常量。請看下面的程序:
~~~
#include <stdio.h>
int main ()
{
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr );
return 0;
}
~~~
當上面的代碼被編譯和執行時,它會產生下列結果:
~~~
ptr 的地址是 0x0
~~~
在大多數的操作系統上,程序不允許訪問地址為 0 的內存,因為該內存是操作系統保留的。然而,內存地址 0 有特別重要的意義,它表明該指針不指向一個可訪問的內存位置。但按照慣例,如果指針包含空值(零值),則假定它不指向任何東西。
如需檢查一個空指針,您可以使用 if 語句,如下所示:
~~~
if(ptr) /* 如果 p 非空,則完成 */
if(!ptr) /* 如果 p 為空,則完成 */
~~~
#### 10.12 指針函數
函數指針是指向函數的指針變量。通常我們說的指針變量是指向一個整型、字符型或數組等變量,而函數指針是指向函數。函數指針可以像一般函數一樣,用于調用函數、傳遞參數。
函數指針變量的聲明:
~~~
typedef int (*fun_ptr)(int,int); // 聲明一個指向同樣參數、返回值的函數指針類型
~~~
實例
~~~
#include <stdio.h>
int max(int x, int y)
{
return x > y ? x : y;
}
int main(void)
{
/* p 是函數指針 */
// &可以省略 函數、數組、數組下標第一位本身指的是它的物理地址,也就只指向它的指針
int (* p)(int, int) = & max;
int a, b, c, d;
printf("請輸入三個數字:");
scanf("%d %d %d", & a, & b, & c);
/* 與直接調用函數等價,d = max(max(a, b), c) */
d = p(p(a, b), c);
printf("最大的數字是: %d\n", d);
return 0;
}
~~~
:-: 
##### 回調函數
簡單講:回調函數是由別人的函數執行時調用你實現的函數。
~~~
#include <stdlib.h>
#include <stdio.h>
// 回調函數
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
}
// 獲取隨機值
int getNextRandomValue(void)
{
return rand();
}
int main(void)
{
int myarray[10];
/* getNextRandomValue 不能加括號,否則無法編譯,因為加上括號之后相當于傳入此參數時傳入了 int , 而不是函數指針*/
populate_array(myarray, 10, getNextRandomValue);
for(int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}
~~~
輸出
~~~
16807 282475249 1622650073 984943658 1144108930 470211272 101027544 1457850878 1458777923 2007237709
~~~
#### 10.13 字符串
在 C 語言中,字符串實際上是使用null字符 '\\0' 終止的一維字符數組。因此,一個以 null 結尾的字符串,包含了組成字符串的字符。
注意:C的字符串是字符組成的字符數組。
初始化數組的方式:
~~~
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
~~~
~~~
char greeting[] = "Hello";
~~~
以下是 C/C++ 中定義的字符串的內存表示:
:-: 
~~~
#include <stdio.h>
int main ()
{
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("Greeting message: %s\n", greeting );
return 0;
}
~~~
輸出
~~~
Greeting message: Hello
~~~
##### 處理字符串的函數
| 1 | **strcpy(s1, s2);** 復制字符串 s2 到字符串 s1。 |
| --- | --- |
| 2 | **strcat(s1, s2);** 連接字符串 s2 到字符串 s1 的末尾。 |
| 3 | **strlen(s1);** 返回字符串 s1 的長度。 |
| 4 | **strcmp(s1, s2);** 如果 s1 和 s2 是相同的,則返回 0;如果 s1s2 則返回大于 0。 |
| 5 | **strchr(s1, ch);** 返回一個指針,指向字符串 s1 中字符 ch 的第一次出現的位置。 |
| 6 | **strstr(s1, s2);** 返回一個指針,指向字符串 s1 中字符串 s2 的第一次出現的位置。 |
~~~
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[12] = "Hello";
char str2[12] = "World";
char str3[12];
int len ;
/* 復制 str1 到 str3 */
strcpy(str3, str1);
printf("strcpy( str3, str1) : %s\n", str3 );
/* 連接 str1 和 str2 */
strcat( str1, str2);
printf("strcat( str1, str2): %s\n", str1 );
/* 連接后,str1 的總長度 */
len = strlen(str1);
printf("strlen(str1) : %d\n", len );
return 0;
}
~~~
上面加載了stdio.h 和string.h標準庫,一個有對數字進行處理的函數,一個有對字符串處理的函數。
輸出:
~~~
strcpy( str3, str1) : Hello
strcat( str1, str2): HelloWorld
strlen(str1) : 10
~~~
#### 10.14 結構體
C 數組允許定義可存儲相同類型數據項的變量,而結構體是 C 編程中另一種用戶自定義的可用的數據類型,它允許您存儲不同類型的數據項。
結構用于表示一條記錄,假設您想要跟蹤圖書館中書本的動態,您可能需要跟蹤每本書的下列屬性:
* Title
* Author
* Subject
* Book ID
##### 定義結構
~~~
struct tag {
member-list
member-list
member-list
...
} variable-list ;
~~~
**tag** 是結構體標簽。
**member-list** 是標準的變量定義,比如 int i; 或者 float f,或者其他有效的變量定義。
**variable-list** 結構變量,定義在結構的末尾,最后一個分號之前,您可以指定一個或多個結構變量。下面是聲明 Book 結構的方式:
~~~
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
~~~
在一般情況下,**tag、member-list、variable-list** 這 3 部分至少要出現 2 個。以下為實例:
~~~
//此聲明聲明了擁有3個成員的結構體,分別為整型的a,字符型的b和雙精度的c
//同時又聲明了結構體變量s1
//這個結構體并沒有標明其標簽
struct
{
int a;
char b;
double c;
} s1;
//此聲明聲明了擁有3個成員的結構體,分別為整型的a,字符型的b和雙精度的c
//結構體的標簽被命名為SIMPLE,沒有聲明變量
struct SIMPLE
{
int a;
char b;
double c;
};
//用SIMPLE標簽的結構體,另外聲明了變量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
//也可以用typedef創建新類型
typedef struct
{
int a;
char b;
double c;
} Simple2;
//現在可以用Simple2作為類型聲明新的結構體變量
Simple2 u1, u2[20], *u3;
~~~
在上面的聲明中,第一個和第二聲明被編譯器當作兩個完全不同的類型,即使他們的成員列表是一樣的,如果令 t3=&s1,則是非法的。
結構體的成員可以包含其他結構體,也可以包含指向自己結構體類型的指針,而通常這種指針的應用是為了實現一些更高級的數據結構如鏈表和樹等。
~~~
//此結構體的聲明包含了其他的結構體
struct COMPLEX
{
char string[100];
struct SIMPLE a;
};
//此結構體的聲明包含了指向自己類型的指針
struct NODE
{
char string[100];
struct NODE *next_node;
};
~~~
如果兩個結構體互相包含,則需要對其中一個結構體進行不完整聲明
~~~
struct B; //對結構體B進行不完整聲明
//結構體A中包含指向結構體B的指針
struct A
{
struct B *partner;
//other members;
};
//結構體B中包含指向結構體A的指針,在A聲明完后,B也隨之進行聲明
struct B
{
struct A *partner;
//other members;
};
~~~
##### 結構體變量的初始化
~~~
#include <stdio.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 語言", "RUNOOB", "編程語言", 123456};
int main()
{
printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
}
~~~
輸出
~~~
title : C 語言
author: RUNOOB
subject: 編程語言
book_id: 123456
~~~
##### 訪問結構成員
~~~
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
int main( )
{
struct Books Book1; /* 聲明 Book1,類型為 Books */
struct Books Book2; /* 聲明 Book2,類型為 Books */
/* Book1 詳述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* Book2 詳述 */
strcpy( Book2.title, "Telecom Billing");
strcpy( Book2.author, "Zara Ali");
strcpy( Book2.subject, "Telecom Billing Tutorial");
Book2.book_id = 6495700;
/* 輸出 Book1 信息 */
printf( "Book 1 title : %s\n", Book1.title);
printf( "Book 1 author : %s\n", Book1.author);
printf( "Book 1 subject : %s\n", Book1.subject);
printf( "Book 1 book_id : %d\n", Book1.book_id);
/* 輸出 Book2 信息 */
printf( "Book 2 title : %s\n", Book2.title);
printf( "Book 2 author : %s\n", Book2.author);
printf( "Book 2 subject : %s\n", Book2.subject);
printf( "Book 2 book_id : %d\n", Book2.book_id);
return 0;
}
~~~
輸出
~~~
Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id : 6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700
~~~
##### 結構作為函數參數
~~~
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 函數聲明 */
void printBook( struct Books book );
int main( )
{
struct Books Book1; /* 聲明 Book1,類型為 Books */
struct Books Book2; /* 聲明 Book2,類型為 Books */
/* Book1 詳述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* Book2 詳述 */
strcpy( Book2.title, "Telecom Billing");
strcpy( Book2.author, "Zara Ali");
strcpy( Book2.subject, "Telecom Billing Tutorial");
Book2.book_id = 6495700;
/* 輸出 Book1 信息 */
printBook( Book1 );
/* 輸出 Book2 信息 */
printBook( Book2 );
return 0;
}
void printBook( struct Books book )
{
printf( "Book title : %s\n", book.title);
printf( "Book author : %s\n", book.author);
printf( "Book subject : %s\n", book.subject);
printf( "Book book_id : %d\n", book.book_id);
}
~~~
輸出
~~~
Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407
Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700
~~~
##### 指向結構的指針
可以定義指向結構的指針,方式與定義指向其他類型變量的指針相似
~~~
struct Books *struct_pointer;
~~~
可以在上述定義的指針變量中存儲結構變量的地址。為了查找結構變量的地址,請把 & 運算符放在結構名稱的前面
~~~
struct_pointer = &Book1;
~~~
例子
~~~
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 函數聲明 */
void printBook( struct Books *book );
int main( )
{
struct Books Book1; /* 聲明 Book1,類型為 Books */
struct Books Book2; /* 聲明 Book2,類型為 Books */
/* Book1 詳述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* Book2 詳述 */
strcpy( Book2.title, "Telecom Billing");
strcpy( Book2.author, "Zara Ali");
strcpy( Book2.subject, "Telecom Billing Tutorial");
Book2.book_id = 6495700;
/* 通過傳 Book1 的地址來輸出 Book1 信息 */
printBook( &Book1 );
/* 通過傳 Book2 的地址來輸出 Book2 信息 */
printBook( &Book2 );
return 0;
}
void printBook( struct Books *book )
{
printf( "Book title : %s\n", book->title);
printf( "Book author : %s\n", book->author);
printf( "Book subject : %s\n", book->subject);
printf( "Book book_id : %d\n", book->book_id);
}
~~~
輸出
~~~
Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407
Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700
~~~
#### 位域
有些信息在存儲時,并不需要占用一個完整的字節,而只需占幾個或一個二進制位。例如在存放一個開關量時,只有 0 和 1 兩種狀態,用 1 位二進位即可。為了節省存儲空間,并使處理簡便,C 語言又提供了一種數據結構,稱為"位域"或"位段"。
所謂"位域"是把一個字節中的二進位劃分為幾個不同的區域,并說明每個區域的位數。每個域有一個域名,允許在程序中按域名進行操作。這樣就可以把幾個不同的對象用一個字節的二進制位域來表示。
典型的實例:
* 用 1 位二進位存放一個開關量時,只有 0 和 1 兩種狀態。
* 讀取外部文件格式——可以讀取非標準的文件格式。例如:9 位的整數。
##### 位域的定義和位域變量的說明
位域定義與結構定義相仿,其形式為:
~~~
struct 位域結構名
{
位域列表
};
~~~
其中位域列表的形式為:
~~~
類型說明符 位域名: 位域長度
~~~
例如:
~~~
struct bs{
int a:8;
int b:2;
int c:6;
}data;
~~~
說明 data 為 bs 變量,共占兩個字節。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6 位。
讓我們再來看一個實例:
~~~
struct packed_struct {
unsigned int f1:1;
unsigned int f2:1;
unsigned int f3:1;
unsigned int f4:1;
unsigned int type:4;
unsigned int my_int:8;
} pack;
~~~
在這里,packed\_struct 包含了 6 個成員:四個 1 位的標識符 f1..f4、一個 4 位的 type 和一個 8 位的 my\_int。
**對于位域的定義尚有以下幾點說明:**
* 一個位域存儲在同一個字節中,如一個字節所剩空間不夠存放另一位域時,則會從下一單元起存放該位域。也可以有意使某位域從下一單元開始。例如:
~~~
struct bs{
unsigned a:4;
unsigned :4; /* 空域 */
unsigned b:4; /* 從下一單元開始存放 */
unsigned c:4
}
~~~
在這個位域定義中,a 占第一字節的 4 位,后 4 位填 0 表示不使用,b 從第二字節開始,占用 4 位,c 占用 4 位。
* 由于位域不允許跨兩個字節,因此位域的長度不能大于一個字節的長度,也就是說不能超過8位二進位。如果最大長度大于計算機的整數字長,一些編譯器可能會允許域的內存重疊,另外一些編譯器可能會把大于一個域的部分存儲在下一個字中。
* 位域可以是無名位域,這時它只用來作填充或調整位置。無名的位域是不能使用的。例如:
~~~
struct k{
int a:1;
int :2; /* 該 2 位不能使用 */
int b:3;
int c:2;
};
~~~
`位域在本質上就是一種結構類型`,不過其成員是按二進位分配的。
##### 位域的使用
位域的使用和結構成員的使用相同,其一般形式為:
~~~
位域變量名.位域名
位域變量名->位域名
~~~
#### 10.15 共用體
共用體是一種特殊的數據類型,允許您在相同的內存位置存儲不同的數據類型。您可以定義一個帶有多成員的共用體,但是`任何時候只能有一個成員帶有值`。共用體提供了一種使用相同的內存位置的有效方式。
定義共用體,您必須使用 union語句,方式與定義結構類似。union 語句定義了一個新的數據類型,帶有多個成員
~~~
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
~~~
union tag是可選的,每個 member definition 是標準的變量定義,比如 int i; 或者 float f; 或者其他有效的變量定義。在共用體定義的末尾,最后一個分號之前,您可以指定一個或多個共用體變量,這是可選的。下面定義一個名為 Data 的共用體類型,有三個成員 i、f 和 str:
~~~
union Data
{
int i;
float f;
char str[20];
} data;
~~~
現在,Data類型的變量可以存儲一個整數、一個浮點數,或者一個字符串。這意味著一個變量(相同的內存位置)可以存儲多個多種類型的數據。您可以根據需要在一個共用體內使用任何內置的或者用戶自定義的數據類型。
共用體占用的內存應足夠存儲共用體中最大的成員。例如,在上面的實例中,Data 將占用 20 個字節的內存空間,因為在各個成員中,字符串所占用的空間是最大的。下面的實例將顯示上面的共用體占用的總內存大小:
~~~
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
printf( "Memory size occupied by data : %d\n", sizeof(data));
return 0;
}
~~~
編譯后輸出
~~~
Memory size occupied by data : 20
~~~
##### 訪問共用體成員
為了訪問共用體的成員,我們使用**成員訪問運算符(.)**。成員訪問運算符是共用體變量名稱和我們要訪問的共用體成員之間的一個句號。您可以使用 **union** 關鍵字來定義共用體類型的變量。
~~~
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
~~~
輸出
~~~
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
~~~
在這里,我們可以看到共用體的 **i** 和 **f** 成員的值有損壞,因為最后賦給變量的值占用了內存位置,這也是 **str** 成員能夠完好輸出的原因。現在讓我們再來看一個相同的實例,這次我們在同一時間只使用一個變量,這也演示了使用共用體的主要目的:
~~~
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
return 0;
}
~~~
當上面的代碼被編譯和執行時,它會產生下列結果:
~~~
data.i : 10
data.f : 220.500000
data.str : C Programming
~~~
在這里,所有的成員都能完好輸出,因為同一時間只用到一個成員。
#### 10.16 typedef
**typedef** 關鍵字可以用為類型取一個新的名字:
~~~
typedef unsigned char BYTE;
~~~
在這個類型定義之后,標識符 BYTE 可作為類型 **unsigned char** 的縮寫,例如:
~~~
BYTE b1, b2;
~~~
按照慣例,定義時會大寫字母,以便提醒用戶類型名稱是一個象征性的縮寫,但您也可以使用小寫字母,如下:
~~~
typedef unsigned char byte;
~~~
您也可以使用 **typedef** 來為用戶自定義的數據類型取一個新的名字。例如,您可以對結構體使用 typedef 來定義一個新的數據類型名字,然后使用這個新的數據類型來直接定義結構變量,如下:
~~~
#include <stdio.h>
#include <string.h>
typedef struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} B;
int main( )
{
B book;
strcpy( book.title, "C 教程");
strcpy( book.author, "Runoob");
strcpy( book.subject, "編程語言");
book.book_id = 12345;
printf( "書標題 : %s\n", book.title);
printf( "書作者 : %s\n", book.author);
printf( "書類目 : %s\n", book.subject);
printf( "書 ID : %d\n", book.book_id);
return 0;
}
~~~
當上面的代碼被編譯和執行時,它會產生下列結果:
~~~
書標題 : C 教程
書作者 : Runoob
書類目 : 編程語言
書 ID : 12345
~~~
##### typedef vs #define
**#define** 是 C 指令,用于為各種數據類型定義別名,與 **typedef** 類似,但是它們有以下幾點不同:
* **typedef** 僅限于為類型定義符號名稱,**#define** 不僅可以為類型定義別名,也能為數值定義別名,比如您可以定義 1 為 ONE。
* **typedef** 是由編譯器執行解釋的,**#define** 語句是由預編譯器進行處理的。
~~~
#include <stdio.h>
#define TRUE 1
#define FALSE 0
int main( )
{
printf( "TRUE 的值: %d\n", TRUE);
printf( "FALSE 的值: %d\n", FALSE);
return 0;
}
~~~
輸出
~~~
TRUE 的值: 1
FALSE 的值: 0
~~~
#### 10.17 輸入 & 輸出
當我們提到輸入時,這意味著要向程序填充一些數據。輸入可以是以文件的形式或從命令行中進行。C 語言提供了一系列內置的函數來讀取給定的輸入,并根據需要填充到程序中。
當我們提到輸出時,這意味著要在屏幕上、打印機上或任意文件中顯示一些數據。C 語言提供了一系列內置的函數來輸出數據到計算機屏幕上和保存數據到文本文件或二進制文件中。
##### 標準文件
C 語言把所有的設備都當作文件。所以設備(比如顯示器)被處理的方式與文件相同。以下三個文件會在程序執行時自動打開,以便訪問鍵盤和屏幕。
| 標準文件 | 文件指針 | 設備 |
| --- | --- | --- |
| 標準輸入 | stdin | 鍵盤 |
| 標準輸出 | stdout | 屏幕 |
| 標準錯誤 | stderr | 您的屏幕 |
文件指針是訪問文件的方式,C 語言中的 I/O (輸入/輸出) 通常使用 printf() 和 scanf() 兩個函數。
scanf() 函數用于從標準輸入(鍵盤)讀取并格式化, printf() 函數發送格式化輸出到標準輸出(屏幕)。
~~~
#include <stdio.h> // 執行 printf() 函數需要該庫
int main()
{
printf("這是輸出例子"); //顯示引號中的內容
return 0;
}
~~~
輸出
~~~
這是輸出例子
~~~
* 所有的 C 語言程序都需要包含 **main()** 函數。 代碼從 **main()** 函數開始執行。
* **printf()** 用于格式化輸出到屏幕。**printf()** 函數在 **"stdio.h"** 頭文件中聲明。
* **stdio.h** 是一個頭文件 (標準輸入輸出頭文件) and **#include** 是一個預處理命令,用來引入頭文件。 當編譯器遇到 **printf()** 函數時,如果沒有找到 **stdio.h** 頭文件,會發生編譯錯誤。
* **return 0;** 語句用于表示退出程序。
##### 輸出格式化
%d輸出整數
~~~
#include <stdio.h>
int main()
{
int testInteger = 5;
printf("Number = %d", testInteger);
return 0;
}
~~~
輸出
~~~
5
~~~
%f輸出浮點型
~~~
#include <stdio.h>
int main()
{
float f;
printf("Enter a number: ");
// %f 匹配浮點型數據
scanf("%f",&f);
printf("Value = %f", f);
return 0;
}
~~~
##### getchar() & putchar() 函數
**int getchar(void)** 函數從屏幕讀取下一個可用的字符,并把它返回為一個整數。這個函數在同一個時間內只會讀取一個單一的字符。您可以在循環內使用這個方法,以便從屏幕上讀取多個字符。
**int putchar(int c)** 函數把字符輸出到屏幕上,并返回相同的字符。這個函數在同一個時間內只會輸出一個單一的字符。您可以在循環內使用這個方法,以便在屏幕上輸出多個字符。
~~~
#include <stdio.h>
int main( )
{
int c;
printf( "Enter a value :");
c = getchar( );
printf( "\nYou entered: ");
putchar( c );
printf( "\n");
return 0;
}
~~~
當上面的代碼被編譯和執行時,它會等待您輸入一些文本,當您輸入一個文本并按下回車鍵時,程序會繼續并只會讀取一個單一的字符
~~~
$./a.out //這是C程序編譯后的可執行文件
Enter a value :runoob
You entered: r
~~~
##### gets() & puts() 函數
**char \*gets(char \*s)** 函數從 **stdin** 讀取一行到 **s** 所指向的緩沖區,直到一個**終止符或 EOF**。
**int puts(const char \*s)** 函數把字符串 s 和一個尾隨的換行符寫入到 **stdout**。
~~~
#include <stdio.h>
int main( )
{
char str[100];
printf( "Enter a value :");
gets( str );
printf( "\nYou entered: ");
puts( str );
return 0;
}
~~~
當上面的代碼被編譯和執行時,它會等待您輸入一些文本,當您輸入一個文本并按下回車鍵時,程序會繼續并讀取一整行直到該行結束
~~~
$./a.out //這是C程序編譯后的可執行文件
Enter a value :runoob
You entered: runoob
~~~
:-: 
##### scanf() 和 printf() 函數
**int scanf(const char \*format, ...)** 函數從標準輸入流 **stdin** 讀取輸入,并根據提供的 **format** 來瀏覽輸入。
**int printf(const char \*format, ...)** 函數把輸出寫入到標準輸出流 **stdout** ,并根據提供的格式產生輸出。
**format** 可以是一個簡單的常量字符串,但是您可以分別指定 %s、%d、%c、%f 等來輸出或讀取字符串、整數、字符或浮點數。還有許多其他可用的格式選項,可以根據需要使用。
~~~
#include <stdio.h>
int main( ) {
char str[100];
int i;
printf( "Enter a value :");
scanf("%s %d", str, &i); //數組本身和數組第一個下標本身只一個指向數組首位地址的指針
printf( "\nYou entered: %s %d ", str, i);
printf("\n");
return 0;
}
~~~
正確輸出:
~~~
$./a.out
Enter a value :runoob 123
You entered: runoob 123
~~~
:-: 
在這里,應當指出的是,scanf() 期待輸入的格式與您給出的 %s 和 %d 相同,這意味著您必須提供有效的輸入,比如 "string integer",如果您提供的是 "string string" 或 "integer integer",它會被認為是錯誤的輸入。另外,在讀取字符串時,只要遇到一個空格,scanf() 就會停止讀取,所以 "this is test" 對 scanf() 來說是三個字符串。
#### 10.18 文件讀寫
C 語言不僅提供了訪問頂層的函數,也提供了底層(OS)調用來處理存儲設備上的文件。
##### 打開文件
您可以使用 **fopen( )** 函數來創建一個新的文件或者打開一個已有的文件,這個調用會初始化類型 **FILE** 的一個對象,類型 **FILE** 包含了所有用來控制流的必要的信息。
~~~
FILE *fopen( const char * filename, const char * mode );
~~~
在這里,**filename** 是字符串,用來命名文件,訪問模式 **mode** 的值可以是下列值中的一個:
| 模式 | 描述 |
| --- | --- |
| r | 打開一個已有的文本文件,允許讀取文件。 |
| w | 打開一個文本文件,允許寫入文件。如果文件不存在,則會創建一個新文件。在這里,您的程序會從文件的開頭寫入內容。如果文件存在,則該會被截斷為零長度,重新寫入。 |
| a | 打開一個文本文件,以追加模式寫入文件。如果文件不存在,則會創建一個新文件。在這里,您的程序會在已有的文件內容中追加內容。 |
| r+ | 打開一個文本文件,允許讀寫文件。 |
| w+ | 打開一個文本文件,允許讀寫文件。如果文件不存在,則會創建一個新文件。如果文件已存在,則文件會被截斷為零長度. |
| a+ | 打開一個文本文件,允許讀寫文件。如果文件不存在,則會創建一個新文件。讀取會從文件的開頭開始,寫入則只能是追加模式。 |
如果處理的是二進制文件,則需使用下面的訪問模式來取代上面的訪問模式:
~~~
"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"
~~~
##### 關閉文件
為了關閉文件,請使用 fclose( ) 函數。函數的原型如下:
~~~
int fclose( FILE *fp );
~~~
如果成功關閉文件,**fclose( )** 函數返回零,如果關閉文件時發生錯誤,函數返回 **EOF**。這個函數實際上,會清空緩沖區中的數據,關閉文件,并釋放用于該文件的所有內存。EOF 是一個定義在頭文件 **stdio.h** 中的常量。
##### 寫入文件
下面是把字符寫入到流中的最簡單的函數:
~~~
int fputc( int c, FILE *fp );
~~~
函數 **fputc()** 把參數 c 的字符值寫入到 fp 所指向的輸出流中。如果寫入成功,它會返回寫入的字符,如果發生錯誤,則會返回 **EOF**。您可以使用下面的函數來把一個以 null 結尾的字符串寫入到流中:
~~~
int fputs( const char *s, FILE *fp );
~~~
函數 **fputs()** 把字符串 **s** 寫入到 fp 所指向的輸出流中。如果寫入成功,它會返回一個非負值,如果發生錯誤,則會返回 **EOF**。您也可以使用 **int fprintf(FILE \*fp,const char \*format, ...)** 函數來寫把一個字符串寫入到文件中。嘗試下面的實例:
**注意:**請確保您有可用的 **tmp** 目錄,如果不存在該目錄,則需要在您的計算機上先創建該目錄。
/tmp 一般是 Linux 系統上的臨時目錄,如果你在 Windows 系統上運行,則需要修改為本地環境中已存在的目錄,例如: C:\\tmp、D:\\tmp等。
~~~
#include <stdio.h>
int main()
{
FILE *fp = NULL;
fp = fopen("/tmp/test.txt", "w+");
fprintf(fp, "This is testing for fprintf...\n"); //用fprintf向文件指針寫入信息
fputs("This is testing for fputs...\n", fp); //用fputs向文件指針寫入信息
fclose(fp);
}
~~~
當上面的代碼被編譯和執行時,它會在 /tmp 目錄中創建一個新的文件test.txt,并使用兩個不同的函數寫入兩行。
##### 讀取文件
下面是從文件讀取單個字符的最簡單的函數:
~~~
int fgetc( FILE * fp );
~~~
**fgetc()** 函數從 fp 所指向的輸入文件中讀取一個字符。返回值是讀取的字符,如果發生錯誤則返回 **EOF**。
~~~
char *fgets( char *buf, int n, FILE *fp );
~~~
函數 **fgets()** 從 fp 所指向的輸入流中讀取 n - 1 個字符。
它會把讀取的字符串復制到緩沖區 **buf**,并在最后追加一個 **null** 字符來終止字符串。
如果這個函數在讀取最后一個字符之前就遇到一個換行符 '\\n' 或文件的末尾 EOF,則只會返回讀取到的字符,包括換行符。
**int fscanf(FILE \*fp, const char \*format, ...)** 函數來從文件中讀取字符串,但是在遇到第一個空格和換行符時,它會停止讀取。
~~~
#include <stdio.h>
int main()
{
FILE *fp = NULL;
char buff[255];
fp = fopen("/tmp/test.txt", "r");
fscanf(fp, "%s", buff);
printf("1: %s\n", buff );
fgets(buff, 255, (FILE*)fp); //獲得fp的當前指針
printf("2: %s\n", buff );
fgets(buff, 255, (FILE*)fp); //獲得fp的當前指針
printf("3: %s\n", buff );
fclose(fp);
}
~~~
輸出
~~~
1: This
2: is testing for fprintf...
3: This is testing for fputs...
~~~
#### 10.19 預處理器
C 預處理器不是編譯器的組成部分,但是它是編譯過程中一個單獨的步驟。簡言之,C 預處理器只不過是一個文本替換工具而已,它們會指示編譯器在實際編譯之前完成所需的預處理。我們將把 C 預處理器(C Preprocessor)簡寫為`CPP`。C源代碼需要先通過預處理器處理再給編譯器、匯編器、鏈接器處理形成可執行的二進制文件。
所有的預處理器命令都是以井號(#)開頭。它必須是第一個非空字符,為了增強可讀性,預處理器指令應從第一列開始。下面列出了所有重要的預處理器指令:
| 指令 | 描述 |
| --- | --- |
| #define | 定義宏 |
| #include | 包含一個源代碼文件 |
| #undef | 取消已定義的宏 |
| #ifdef | 如果宏已經定義,則返回真 |
| #ifndef | 如果宏沒有定義,則返回真 |
| #if | 如果給定條件為真,則編譯下面代碼 |
| #else | #if 的替代方案 |
| #elif | 如果前面的 #if 給定條件不為真,當前條件為真,則編譯下面代碼 |
| #endif | 結束一個 #if……#else 條件編譯塊 |
| #error | 當遇到標準錯誤時,輸出錯誤消息 |
| #pragma | 使用標準化方法,向編譯器發布特殊的命令到編譯器中 |
##### 預處理器實例
~~~
#define MAX_ARRAY_LENGTH 20
~~~
這個指令告訴 CPP 把所有的 MAX\_ARRAY\_LENGTH 替換為 20。使用 *#define* 定義常量來增強可讀性。
~~~
#include <stdio.h>
#include "myheader.h"
~~~
這些指令告訴 CPP 從**系統庫**中獲取 stdio.h,并添加文本到當前的源文件中。下一行告訴 CPP 從本地目錄中獲取 **myheader.h**,并添加內容到當前的源文件中。
~~~
#undef FILE_SIZE
#define FILE_SIZE 42
~~~
這個指令告訴 CPP 取消已定義的 FILE\_SIZE,并定義它為 42。
~~~
#ifndef MESSAGE
#define MESSAGE "You wish!"
#endif
~~~
這個指令告訴 CPP 只有當 MESSAGE 未定義時,才定義 MESSAGE。
~~~
#ifdef DEBUG
/* Your debugging statements here */
#endif
~~~
這個指令告訴 CPP 如果定義了 DEBUG,則執行處理語句。在編譯時,如果您向 gcc 編譯器傳遞了 *\-DDEBUG* 開關量,這個指令就非常有用。它定義了 DEBUG,您可以在編譯期間隨時開啟或關閉調試。
##### 預定義宏
ANSI C 定義了許多宏。在編程中您可以使用這些宏,但是不能直接修改這些預定義的宏。
| 宏 | 描述 |
| --- | --- |
| **DATE** | 當前日期,一個以 "MMM DD YYYY" 格式表示的字符常量。 |
| **TIME** | 當前時間,一個以 "HH:MM:SS" 格式表示的字符常量。 |
| **FILE** | 這會包含當前文件名,一個字符串常量。 |
| **LINE** | 這會包含當前行號,一個十進制常量。 |
| **STDC** | 當編譯器以 ANSI 標準編譯時,則定義為 1。 |
~~~
#include <stdio.h>
main()
{
printf("File :%s\n", __FILE__ );
printf("Date :%s\n", __DATE__ );
printf("Time :%s\n", __TIME__ );
printf("Line :%d\n", __LINE__ );
printf("ANSI :%d\n", __STDC__ );
}
~~~
輸出
~~~
File :test.c
Date :Jun 2 2012
Time :03:36:24
Line :8
ANSI :1
~~~
##### 預處理器運算符
C 預處理器提供了下列的運算符來幫助您創建宏:
##### 宏延續運算符(\\)
一個宏通常寫在一個單行上。但是如果宏太長,一個單行容納不下,則使用宏延續運算符(\\)。例如:
~~~
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
~~~
##### 字符串常量化運算符(#)
在宏定義中,當需要把一個宏的參數轉換為字符串常量時,則使用字符串常量化運算符(#)。在宏中使用的該運算符有一個特定的參數或參數列表
~~~
#include <stdio.h>
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
//將message_for這個宏的參數a和b常量化,注意message_for這個宏是個函數
int main(void)
{
message_for(Carole, Debra);
return 0;
}
~~~
輸出
~~~
Carole and Debra: We love you!
~~~
##### 標記粘貼運算符(##)
宏定義內的標記粘貼運算符(##)會合并兩個參數。它允許在宏定義中兩個獨立的標記被合并為一個標記。
~~~
#include <stdio.h>
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
int main(void)
{
int token34 = 40;
tokenpaster(34);
return 0;
}
~~~
輸出
~~~
token34 = 40
~~~
這是怎么發生的,因為這個實例會從編譯器產生下列的實際輸出:
~~~
printf ("token34 = %d", token34);
~~~
這個實例演示了 token##n 會連接到 token34 中,在這里,我們使用了**字符串常量化運算符(#)**和**標記粘貼運算符(##)**。
##### defined() 運算符
預處理器 **defined** 運算符是用在常量表達式中的,用來確定一個標識符是否已經使用 #define 定義過。如果指定的標識符已定義,則值為真(非零)。如果指定的標識符未定義,則值為假(零)。
~~~
#include <stdio.h>
#if !defined (MESSAGE)
#define MESSAGE "You wish!"
#endif
int main(void)
{
printf("Here is the message: %s\n", MESSAGE);
return 0;
}
~~~
輸出
~~~
Here is the message: You wish!
~~~
##### 參數化的宏
CPP 一個強大的功能是可以使用參數化的宏來模擬函數。
~~~
int square(int x) {
return x * x;
}
~~~
我們可以使用宏重寫上面的代碼,如下:
~~~
#define square(x) ((x) * (x))
~~~
在使用帶有參數的宏之前,必須使用 **#define** 指令定義。參數列表是括在圓括號內,且必須緊跟在宏名稱的后邊。宏名稱和左圓括號之間不允許有空格。
~~~
#include <stdio.h>
#define MAX(x,y) ((x) > (y) ? (x) : (y))
int main(void)
{
printf("Max between 20 and 10 is %d\n", MAX(10, 20));
return 0;
}
~~~
輸出
~~~
Max between 20 and 10 is 20
~~~
#### 10.20 C 頭文件
頭文件是擴展名為 **.h** 的文件,包含了 C 函數聲明和宏定義,被多個源文件中引用共享。有兩種類型的頭文件:`程序員編寫的頭文件`和`編譯器自帶的頭文件`。
在程序中要使用頭文件,需要使用 C 預處理指令 **#include** 來引用它。前面我們已經看過 **stdio.h** 頭文件,它是編譯器自帶的頭文件。
引用頭文件相當于復制頭文件的內容,但是我們不會直接在源文件中復制頭文件的內容,因為這么做很容易出錯,特別在程序是由多個源文件組成的時候。
A simple practice in C 或 C++ 程序中,`建議把所有的常量、宏、系統全局變量和函數原型寫在頭文件中,在需要的時候隨時引用這些頭文件`。
引用頭文件的語法
使用預處理指令 **#include** 可以引用用戶和系統頭文件。它的形式有以下兩種:
~~~
#include <file>
~~~
這種形式用于引用系統頭文件。它在系統目錄的標準列表中搜索名為 file 的文件。在編譯源代碼時,您可以通過 -I 選項把目錄前置在該列表前。
~~~
#include "file"
~~~
這種形式用于引用用戶頭文件。它在包含當前文件的目錄中搜索名為 file 的文件。在編譯源代碼時,您可以通過 -I 選項把目錄前置在該列表前。
##### 引用頭文件的操作
**#include** 指令會指示 C 預處理器瀏覽指定的文件作為輸入。預處理器的輸出包含了已經生成的輸出,被引用文件生成的輸出以及 **#include** 指令之后的文本輸出。例如,如果您有一個頭文件 header.h,如下:
~~~
char *test (void);
~~~
和一個使用了頭文件的主程序 *program.c*,如下:
~~~
int x;
#include "header.h"
int main (void)
{
puts (test ());
}
~~~
編譯器會看到如下的代碼信息:
~~~
int x;
char *test (void);
int main (void)
{
puts (test ());
}
~~~
##### 只引用一次頭文件
如果一個頭文件被引用兩次,編譯器會處理兩次頭文件的內容,這將產生錯誤。為了防止這種情況,標準的做法是把文件的整個內容放在條件編譯語句中,如下:
~~~
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
~~~
這種結構就是通常所說的包裝器 **#ifndef**。當再次引用頭文件時,條件為假,因為 HEADER\_FILE 已定義。此時,預處理器會跳過文件的整個內容,編譯器會忽略它。
##### 有條件引用
有時需要從多個不同的頭文件中選擇一個引用到程序中。例如,需要指定在不同的操作系統上使用的配置參數。您可以通過一系列條件來實現這點,如下:
~~~
#if SYSTEM_1
# include "system_1.h"
#elif SYSTEM_2
# include "system_2.h"
#elif SYSTEM_3
...
#endif
~~~
但是如果頭文件比較多的時候,這么做是很不妥當的,預處理器使用宏來定義頭文件的名稱。這就是所謂的**有條件引用**。它不是用頭文件的名稱作為 **#include** 的直接參數,您只需要使用宏名稱代替即可:
~~~
#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H
~~~
SYSTEM\_H 會擴展,預處理器會查找 system\_1.h,就像 **#include** 最初編寫的那樣。SYSTEM\_H 可通過 -D 選項被您的 Makefile 定義。
#### 10\. 21 強制類型轉換
強制類型轉換是把變量從一種類型轉換為另一種數據類型。例如,如果您想存儲一個 long 類型的值到一個簡單的整型中,您需要把 long 類型強制轉換為 int 類型。您可以使用強制類型轉換運算符來把值顯式地從一種類型轉換為另一種類型
~~~
(type_name) expression
~~~
~~~
#include <stdio.h>
int main()
{
int sum = 17, count = 5;
double mean;
mean = (double) sum / count;
printf("Value of mean : %f\n", mean );
}
~~~
輸出
~~~
Value of mean : 3.400000
~~~
這里要注意的是強制類型轉換運算符的優先級大于除法,因此 **sum** 的值首先被轉換為 **double** 型,然后除以 count,得到一個類型為 double 的值。
類型轉換可以是`隱式`的,由編譯器自動執行,也可以是顯式的,通過使用**強制類型轉換運算符**來指定。在編程時,有需要類型轉換的時候都用上強制類型轉換運算符,是一種良好的編程習慣。
##### 整數提升
整數提升是指把小于 **int** 或 **unsigned int** 的整數類型轉換為 **int** 或 **unsigned int** 的過程。
~~~
#include <stdio.h>
int main()
{
int i = 17;
char c = 'c'; /* ascii 值是 99 */
int sum;
sum = i + c;
printf("Value of sum : %d\n", sum );
}
~~~
輸出
~~~
Value of sum : 116
~~~
在這里,sum 的值為 116,因為編譯器進行了整數提升,在執行實際加法運算時,把 'c' 的值轉換為對應的 ascii 值。
##### 常用的算術轉換
**常用的算術轉換**是隱式地把值強制轉換為相同的類型。編譯器首先執行**整數提升**,如果操作數類型不同,則它們會被轉換為下列層次中出現的最高層次的類型:
:-: 
常用的算術轉換不適用于賦值運算符、邏輯運算符 && 和 ||
~~~
#include <stdio.h>
int main()
{
int i = 17;
char c = 'c'; /* ascii 值是 99 */
float sum;
sum = i + c;
printf("Value of sum : %f\n", sum );
}
~~~
輸出
~~~
Value of sum : 116.000000
~~~
在這里,c 首先被轉換為整數,但是由于最后的值是 float 型的,所以會應用常用的算術轉換,編譯器會把 i 和 c 轉換為浮點型,并把它們相加得到一個浮點數。
#### 10.22 錯誤處理
C 語言不提供對錯誤處理的直接支持,但是作為一種系統編程語言,它以返回值的形式允許您訪問底層數據。在發生錯誤時,大多數的 C 或 UNIX 函數調用返回 1 或 NULL,同時會設置一個錯誤代碼 **errno**,該錯誤代碼是全局變量,表示在函數調用期間發生了錯誤。您可以在 errno.h 頭文件中找到各種各樣的錯誤代碼。
所以,C 程序員可以通過檢查返回值,然后根據返回值決定采取哪種適當的動作。開發人員應該在程序初始化時,把 errno 設置為 0,這是一種良好的編程習慣。0 值表示程序中沒有錯誤。
##### errno、perror() 和 strerror()
C 語言提供了 **perror()** 和 **strerror()** 函數來顯示與 **errno** 相關的文本消息。
* **perror()** 函數顯示您傳給它的字符串,后跟一個冒號、一個空格和當前 errno 值的文本表示形式。
* **strerror()** 函數,返回一個指針,指針指向當前 errno 值的文本表示形式。
讓我們來模擬一種錯誤情況,嘗試打開一個不存在的文件。您可以使用多種方式來輸出錯誤消息,在這里我們使用函數來演示用法。另外有一點需要注意,您應該使用 **stderr** 文件流來輸出所有的錯誤。
~~~
#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno ;
int main ()
{
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL)
{
errnum = errno;
fprintf(stderr, "錯誤號: %d\n", errno);
perror("通過 perror 輸出錯誤");
fprintf(stderr, "打開文件錯誤: %s\n", strerror( errnum ));
}
else
{
fclose (pf);
}
return 0;
}
~~~
輸出
~~~
錯誤號: 2
通過 perror 輸出錯誤: No such file or directory
打開文件錯誤: No such file or directory
~~~
##### 被零除的錯誤
在進行除法運算時,如果不檢查除數是否為零,則會導致一個運行時錯誤。
~~~
#include <stdio.h>
#include <stdlib.h>
main()
{
int dividend = 20;
int divisor = 0;
int quotient;
if( divisor == 0){
fprintf(stderr, "除數為 0 退出運行...\n");
exit(-1);
}
quotient = dividend / divisor;
fprintf(stderr, "quotient 變量的值為 : %d\n", quotient );
exit(0);
}
~~~
輸出
~~~
除數為 0 退出運行...
~~~
##### 程序退出狀態
通常情況下,程序成功執行完一個操作正常退出的時候會帶有值 EXIT\_SUCCESS。在這里,EXIT\_SUCCESS 是宏,它被定義為 0。如果程序中存在一種錯誤情況,當您退出程序時,會帶有狀態值 EXIT\_FAILURE,被定義為 -1
~~~
#include <stdio.h>
#include <stdlib.h>
main()
{
int dividend = 20;
int divisor = 5;
int quotient;
if( divisor == 0){
fprintf(stderr, "除數為 0 退出運行...\n");
exit(EXIT_FAILURE);
}
quotient = dividend / divisor;
fprintf(stderr, "quotient 變量的值為: %d\n", quotient );
exit(EXIT_SUCCESS);
}
~~~
輸出
~~~
quotient 變量的值為 : 4
~~~
#### 10.23 內存管理
本章將講解 C 中的動態內存管理。C 語言為內存的分配和管理提供了幾個函數。這些函數可以在 **** 頭文件中找到。
| 函數和描述 |
| --- |
| **void \*calloc(int num, int size);** 在內存中動態地分配 num 個長度為 size 的連續空間,并將每一個字節都初始化為 0。所以它的結果是分配了 num\*size 個字節長度的內存空間,并且每個字節的值都是0。 |
| **void free(void \*address);** 該函數釋放 address 所指向的內存塊,釋放的是動態分配的內存空間。 |
| **void \*malloc(int num);** 在堆區分配一塊指定大小的內存空間,用來存放數據。這塊內存空間在函數執行完成后不會被初始化,它們的值是未知的。 |
| **void \*realloc(void \*address, int newsize);** 該函數重新分配內存,把內存擴展到 **newsize**。 |
**注意:**void \* 類型表示未確定類型的指針。C、C++ 規定 void \* 類型可以通過類型轉換強制轉換為任何其它類型的指針。
##### 動態分配內存
編程時,如果您預先知道數組的大小,那么定義數組時就比較容易。例如,一個存儲人名的數組,它最多容納 100 個字符,所以您可以定義數組,如下所示:
~~~
char name[100];
~~~
但是,如果您預先不知道需要存儲的文本長度,例如您想存儲有關一個主題的詳細描述。在這里,我們需要定義一個指針,該指針指向未定義所需內存大小的字符,后續再根據需求來分配內存
~~~
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char name[100];
char *description;
strcpy(name, "Zara Ali");
/* 動態分配內存 */
description = (char *)malloc( 200 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcpy( description, "Zara ali a DPS student in class 10th");
}
printf("Name = %s\n", name );
printf("Description: %s\n", description );
}
~~~
輸出
~~~
Name = Zara Ali
Description: Zara ali a DPS student in class 10th
~~~
上面的程序也可以使用 **calloc()** 來編寫,只需要把 malloc 替換為 calloc 即可
~~~
calloc(200, sizeof(char));
~~~
當動態分配內存時,您有完全控制權,可以傳遞任何大小的值。而那些預先定義了大小的數組,一旦定義則無法改變大小。
##### 重新調整內存的大小和釋放內存
當程序退出時,操作系統會自動釋放所有分配給程序的內存,但是,建議您在不需要內存時,都應該調用函數 **free()** 來釋放內存。或者,您可以通過調用函數 **realloc()** 來增加或減少已分配的內存塊的大小
~~~
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char name[100];
char *description;
strcpy(name, "Zara Ali");
/* 動態分配內存 */
description = (char *)malloc( 30 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcpy( description, "Zara ali a DPS student.");
}
/* 假設您想要存儲更大的描述信息 */
description = (char *) realloc( description, 100 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcat( description, "She is in class 10th");
}
printf("Name = %s\n", name );
printf("Description: %s\n", description );
/* 使用 free() 函數釋放內存 */
free(description);
}
~~~
輸出
~~~
Name = Zara Ali
Description: Zara ali a DPS student.She is in class 10th
~~~
您可以嘗試一下不重新分配額外的內存,strcat() 函數會生成一個錯誤,因為存儲 description 時可用的內存不足。
#### 10.24命令行參數
執行程序時,可以從命令行傳值給 C 程序。這些值被稱為**命令行參數**,它們對程序很重要,特別是當您想從外部控制程序,而不是在代碼內對這些值進行硬編碼時,就顯得尤為重要了。
命令行參數是使用 main() 函數參數來處理的,其中,**argc** 是指傳入參數的個數,**argv\[\]** 是一個指針數組,指向傳遞給程序的每個參數。
~~~
#include <stdio.h>
int main( int argc, char *argv[] )
{
int i=1;
if(argc>1)
{
for(i;i<argc;i++){
printf("%s\n", argv[i]);
}
}
else
{
printf("沒有參數\n");
}
}
~~~
使用一個參數,編譯并執行上面的代碼
~~~
[root@VM-0-8-centos wwwroot]# ./test 1
1
~~~
使用三個參數,編譯并執行上面的代碼
~~~
[root@VM-0-8-centos wwwroot]# ./test 1 2 3
1
2
3
~~~
不傳任何參數,編譯并執行上面的代碼
~~~
[root@VM-0-8-centos wwwroot]# ./test
沒有參數
~~~
:-: 
應當指出的是,**argv\[0\]** 存儲程序的名稱,**argv\[1\]** 是一個指向第一個命令行參數的指針,\*argv\[n\] 是最后一個參數。如果沒有提供任何參數,argc 將為 1,否則,如果傳遞了一個參數,**argc** 將被設置為 2。
多個命令行參數之間用空格分隔,但是如果參數本身帶有空格,那么傳遞參數的時候應把參數放置在雙引號 "" 或單引號 '' 內部。
~~~
#include <stdio.h>
int main( int argc, char *argv[] )
{
printf("執行文件是 %s\n", argv[0]);
if( argc == 2 )
{
printf("第一個參數是 %s\n", argv[1]);
}
else if( argc > 2 )
{
printf("有多個參數.\n");
}
else
{
printf("沒有參數.\n");
}
}
~~~
輸出
~~~
[root@VM-0-8-centos wwwroot]# ./test 1 2 3 4
執行文件是 ./test
有多個參數.
~~~
:-: 
#### 10.25 的排序算法
冒泡排序
冒泡排序(英語:Bubble Sort)是一種簡單的排序算法。它重復地走訪過要排序的數列,一次比較兩個元素,如果他們的順序(如從大到小、首字母從A到Z)錯誤就把他們交換過來。
:-: 
:-: 
:-: 
~~~
#include <stdio.h>
void bubble_sort(int arr[], int len) {
int i, j, temp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
int i;
for (i = 0; i < len; i++)
printf("%d ", arr[i]);
return 0;
}
~~~
選擇排序
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小(大)元素,然后放到已排序序列的末尾。

~~~
void swap(int *a,int *b) //交換兩個變數
{
int temp = *a;
*a = *b;
*b = temp;
}
void selection_sort(int arr[], int len)
{
int i,j;
for (i = 0 ; i < len - 1 ; i++)
{
int min = i;
for (j = i + 1; j < len; j++) //走訪未排序的元素
if (arr[j] < arr[min]) //找到目前最小值
min = j; //紀錄最小值
swap(&arr[min], &arr[i]); //做交換
}
}
~~~
插入排序
插入排序(英語:Insertion Sort)是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對于未排序數據,在已排序序列中從后向前掃描,找到相應位置并插入。插入排序在實現上,通常采用in-place排序(即只需用到 {\\displaystyle O(1)} {\\displaystyle O(1)}的額外空間的排序),因而在從后向前掃描過程中,需要反復把已排序元素逐步向后
挪位,為最新元素提供插入空間。
希爾排序
希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序算法。
希爾排序是基于插入排序的以下兩點性質而提出改進方法的:
* 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
* 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位
過程演示:

~~~
void shell_sort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap = gap >> 1)
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
arr[j + gap] = arr[j];
arr[j + gap] = temp;
}
}
~~~
歸并排序
把數據分為兩段,從兩段中逐個選最小的元素移入新數據段的末尾。
可從上到下或從下到上進行。

遞歸法
~~~
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int* a = arr;
int* b = (int*) malloc(len * sizeof(int));
int seg, start;
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
int* temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
free(b);
}
~~~
迭代法
~~~
void merge_sort_recursive(int arr[], int reg[], int start, int end) {
if (start >= end)
return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
reg[k++] = arr[start1++];
while (start2 <= end2)
reg[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = reg[k];
}
void merge_sort(int arr[], const int len) {
int reg[len];
merge_sort_recursive(arr, reg, 0, len - 1);
}
~~~
快速排序
在區間中隨機挑選一個元素作基準,將小于基準的元素放在基準之前,大于基準的元素放在基準之后,再分別對小數區與大數區進行排序。

迭代法
~~~
typedef struct _Range {
int start, end;
} Range;
Range new_Range(int s, int e) {
Range r;
r.start = s;
r.end = e;
return r;
}
void swap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}
void quick_sort(int arr[], const int len) {
if (len <= 0)
return; // 避免len等於負值時引發段錯誤(Segment Fault)
// r[]模擬列表,p為數量,r[p++]為push,r[--p]為pop且取得元素
Range r[len];
int p = 0;
r[p++] = new_Range(0, len - 1);
while (p) {
Range range = r[--p];
if (range.start >= range.end)
continue;
int mid = arr[(range.start + range.end) / 2]; // 選取中間點為基準點
int left = range.start, right = range.end;
do
{
while (arr[left] < mid) ++left; // 檢測基準點左側是否符合要求
while (arr[right] > mid) --right; //檢測基準點右側是否符合要求
if (left <= right)
{
swap(&arr[left],&arr[right]);
left++;right--; // 移動指針以繼續
}
} while (left <= right);
if (range.start < right) r[p++] = new_Range(range.start, right);
if (range.end > left) r[p++] = new_Range(left, range.end);
}
}
~~~
遞歸法
~~~
void swap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}
void quick_sort_recursive(int arr[], int start, int end) {
if (start >= end)
return;
int mid = arr[end];
int left = start, right = end - 1;
while (left < right) {
while (arr[left] < mid && left < right)
left++;
while (arr[right] >= mid && left < right)
right--;
swap(&arr[left], &arr[right]);
}
if (arr[left] >= arr[end])
swap(&arr[left], &arr[end]);
else
left++;
if (left)
quick_sort_recursive(arr, start, left - 1);
quick_sort_recursive(arr, left + 1, end);
}
void quick_sort(int arr[], int len) {
quick_sort_recursive(arr, 0, len - 1);
}
~~~