#### 第10章: #### C语言 由于PHP底层运行的代码以及PHP的虚拟机(Zend主机)用的C语言,所以我们会学习一下C语言。 C是一门高级语言。由于汇编语言依赖于硬件体系,并且该语言中的主机符号数量较多,所以运用起来不够方便。为了使程序语言能贴近人类的自然语言,同时又不依赖于计算机硬件,于是产生了高级语言。C之后的很多高级语言的底层运行的是C语言,例如:PHP、Java、Golong等。 开发C程序可以使用 Microsoft Visual C++编辑器。 :-: ![](https://img.kancloud.cn/e3/9e/e39ed433ec2517f9b447d4a649e1106b_1042x609.png) #### 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; } ~~~ :-: ![](https://img.kancloud.cn/0d/83/0d8358713cf6346562a1e9d4db8b59fc_424x228.png) 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; } ~~~ :-: ![](https://img.kancloud.cn/0f/ba/0fba7f13e729edfef3f5e2ddcd1441d2_508x217.png) ##### 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位小数。![](https://www.runoob.com/wp-content/uploads/2014/09/v2-749cc641eb4d5dafd085e8c23f8826aa_hd.png) | | double | 双精度浮点值。双精度是1位符号,11位指数,52位小数。![](https://www.runoob.com/wp-content/uploads/2014/09/v2-48240f0e1e0dd33ec89100cbe2d30707_hd.png) | | 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(); } ​ ~~~ :-: ![](https://img.kancloud.cn/44/3d/443deebd57a680e31657dd675e202a23_472x263.png) 第一次调用: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(); } ~~~ :-: ![](https://img.kancloud.cn/ce/00/ce00c51668b0a79ec11ebeb2ba72f7c6_518x277.png) 第一次调用:2 第二次调用:3 因为它们的变量ilnt的作用域都在mian函数块里。 ##### register变量 register变量称为寄存器变量。表示这个变量是存在CPU中的寄存器里的,不占内存。因此运算速度会更快。 ~~~ #include<stdio.h> ​ int main() {   register int ilnt; //定义寄存器变量类型   ilnt = 100;   printf("%d\n",ilnt);   return 0; } ~~~ :-: ![](https://img.kancloud.cn/d5/2a/d52acdd280efbd6faf8c12a911938d62_452x211.png) ##### 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++程序内存布局: :-: ![](https://img.kancloud.cn/23/74/23740a1a779ffc6c4a3282847177b01d_295x163.png) 一个由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产生一次制表符和两次换行符。 :-: ![](https://img.kancloud.cn/f5/c0/f5c0ac8b4ab420c4741c8ae77037ae93_449x259.png) ##### 字符串常量 字符串字面值或常量是括在双引号 "" 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。 您可以使用空格做分隔符,把一个很长的字符串常量进行分行。 下面的实例显示了一些字符串常量。下面这三种形式所显示的字符串是相同的。 ~~~ "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; } ~~~ :-: ![](https://img.kancloud.cn/49/c4/49c4d99d0e27b6cee18ffa95f17f0f6c_479x230.png) ##### 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\] 来代表一个个单独的变量。数组中的特定元素可以通过索引访问。 所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。 :-: ![](https://img.kancloud.cn/54/74/54742ac5c72bd4e4cdfea11e31c1e0b6_442x109.png) ##### 声明数组 在 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。以下是上面所讨论的数组的的图形表示: :-: ![](https://img.kancloud.cn/74/20/74201b66a5c07e200f3b9940f7c6901d_540x65.png) ##### 访问数组元素 数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如: ~~~ 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]; ~~~ :-: ![](https://img.kancloud.cn/c7/e2/c7e2724a7c1c0772862c5a63fd8b8cfb_523x143.png) 因此,数组中的每个元素是使用形式为 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; } ~~~ :-: ![](https://img.kancloud.cn/28/ed/28ed79ed63a52aef519bc8009f2024ec_531x245.png) 在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; } ~~~ :-: ![](https://img.kancloud.cn/12/d1/12d1a00a0e2b0af79cc54563f3e799bc_447x261.png) 输出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 ~~~ :-: ![](https://img.kancloud.cn/11/0d/110da2cb70cde844dee99e75b90d7c5c_777x431.png) 什么是指针? 指针是一个变量,其值为另一个变量的地址。 注意:定义指针的数据类型要和指向的数据的数据类型一致。比如指针指向一个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; } ~~~ :-: ![](https://img.kancloud.cn/4f/8c/4f8c813b61c37cbab0b47ca2f6bb6820_509x235.png) ##### 回调函数 简单讲:回调函数是由别人的函数执行时调用你实现的函数。 ~~~ #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++ 中定义的字符串的内存表示: :-: ![](https://img.kancloud.cn/90/36/903680156fc356a8dfee0f91be66dbbd_665x188.png) ~~~ #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 ~~~ :-: ![](https://img.kancloud.cn/fe/d3/fed3a6e87d49721114baff24b02858bc_414x223.png) ##### 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 ~~~ :-: ![](https://img.kancloud.cn/4f/00/4f000901ee78128110c96afe32a16b93_448x220.png) 在这里,应当指出的是,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 值。 ##### 常用的算术转换 **常用的算术转换**是隐式地把值强制转换为相同的类型。编译器首先执行**整数提升**,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型: :-: ![](https://img.kancloud.cn/53/be/53be14ed216f09a1c7e9b3585c99d2ca_204x486.png) 常用的算术转换不适用于赋值运算符、逻辑运算符 && 和 || ~~~ #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 没有参数 ~~~ :-: ![](https://img.kancloud.cn/d4/ce/d4ce3cba04bd10c568474d0110d00f70_290x62.png) 应当指出的是,**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 有多个参数. ~~~ :-: ![](https://img.kancloud.cn/61/01/6101e576e0c119e97be2edff1a7be6e0_331x77.png) #### 10.25 的排序算法 冒泡排序 冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。 :-: ![](https://img.kancloud.cn/7a/fc/7afc110bc6c2a636fd76f7ea1a555688_364x259.png) :-: ![](https://img.kancloud.cn/40/e4/40e43371ee74a5259c9f65a344e71e5e_294x252.png) :-: ![](https://img.kancloud.cn/27/06/2706f978978c6c28cc71c1f607e1828d_340x253.png) ~~~ #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)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 ![](https://www.runoob.com/wp-content/uploads/2018/09/Selection-Sort-Animation.gif) ~~~ 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)}的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后 挪位,为最新元素提供插入空间。 希尔排序 希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。 希尔排序是基于插入排序的以下两点性质而提出改进方法的: * 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率 * 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位 过程演示: ![](https://www.runoob.com/wp-content/uploads/2018/09/Sorting_shellsort_anim.gif) ~~~ 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; } } ~~~ 归并排序 把数据分为两段,从两段中逐个选最小的元素移入新数据段的末尾。 可从上到下或从下到上进行。 ![](https://www.runoob.com/wp-content/uploads/2018/09/Merge_sort_animation2.gif) 递归法 ~~~ 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); } ~~~ 快速排序 在区间中随机挑选一个元素作基准,将小于基准的元素放在基准之前,大于基准的元素放在基准之后,再分别对小数区与大数区进行排序。 ![](https://www.runoob.com/wp-content/uploads/2018/09/Sorting_quicksort_anim.gif) 迭代法 ~~~ 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); } ~~~