一、C简介
1.1 什么是C语言?
C语言是一门通用计算机编程语言,C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。
尽管C语言提供了许多低级处理的功能,但仍然保持着良好跨平台的特性,以一个标准规格写出的C语言程序可在许多电脑平台上进行编译,甚至包含一些嵌入式处理器(单片机或称MCU)以及超级电脑等作业平台。
关于C
- C 语言是为了编写 UNIX 操作系统而被发明的。
- C 语言是以 B 语言为基础的,B 语言大概是在 1970 年被引进的。
- C 语言标准是于 1988 年由美国国家标准协会(ANSI,全称 American National Standard Institute)制定的。
- 截至 1973 年,UNIX 操作系统完全使用 C 语言编写。
- 目前,C 语言是最广泛使用的系统程序设计语言。
- 大多数先进的软件都是使用 C 语言实现的。
- 当今最流行的 Linux 操作系统和 RBDMS MySQL 都是使用 C 语言编写的。
1.2 C语言能做什么?
- 操作系统开发
- 软件开发
- 嵌入式开发
- 语言编译器
1.3 C语言有什么优点?
- 可移植性:C语言是高度可移植的,你在不改动或者只做很小改动的情况下,就可以把C语言的程序运行在不同平台;
- C语言很小:C语言完全基于变量,宏命令,函数和架构,整体非常小,因此C语言可以嵌入几乎现代所有微型处理器中,从冰箱到闹钟;
- 学会C学会一切:几乎所有编程语言都由C语言实现,或者有着和C语言一样相似的语法和逻辑规则,因此,学会C语言能使你很快学会其他语言。
1.4 C语言有什么缺点?
- 运行时间:C语言没有运行时间检查机制;
- 面向过程:C语言不支持面向对象编程,这就是为什么创造C++;
- 不安全:指针是C语言的一大特色,可以说是C语言优于其它高级语言的一个重要原因,但也就是因为它有指针,可以直接进行靠近硬件的操作,所以带来很多不安全的因素。
记住:语言终究只是工具,算法才是核心,思路才是灵魂。
二、C基本语法
2.1 C 程序主要构成
- 预处理器指令
- 函数
- 变量
- 语句 & 表达式
- 注释
2.2 C 程序编写流程
- 编辑
- 预编译
- 编译
- 汇编
- 链接
- 运行
2.3 C语言结束符
C程序语句的结束是通过分号‘;’实现的,它表明一个逻辑实体的结束。
2.4 C语言注释
C语言的注释是以/*开始,以*/终止的。单行的注释,也可以使用双斜杠‘//’表示。
您不能在注释内嵌套注释,注释也不能出现在字符串或字符值中。
2.5 C语言标识符
C 标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。一个标识符以字母A-Z或a-z或下划线_开始,后跟零个或多个字母、下划线和数字(0-9)。
C 标识符内不允许出现标点字符,比如 @、$ 和 %。C 是区分大小写的编程语言。
2.6 C语言关键字
下列C中的保留字不能作为常量名、变量名或其他标识符名称。
auto | else | long | switch |
---|---|---|---|
case | extern | return | union |
char | float | short | unsigned |
const | for | signed | void |
continue | goto | sizeof | volatile |
default | if | static | while |
do | int | struct | _Packed |
double | define | typedef |
2.7 C语言空格
只包含空格的行,被称为空白行,可能带有注释,C 编译器会完全忽略它。
在 C 中,空格用于描述空白符、制表符、换行符和注释。空格分隔语句的各个部分,让编译器能识别语句中的某个元素(比如int)在哪里结束,下一个元素在哪里开始。
三、C基础数据
3.1 数据类型
C语言中的类型包含以下几种:
序号 | 类型 | 描述 |
---|---|---|
1 | 基本类型 | 它们是算术类型,包括两种类型:整数类型和浮点数类型 |
2 | 枚举类型 | 它们也是算术类型,被用来定义在程序中只能赋予其一定的散列整数值的变量 |
3 | void类型 | 类型说明符void表明没有可用的值 |
4 | 派生类型 | 它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型 |
数组类型和结构类型统称为聚合类型;函数的类型指的是函数返回值的类型。
为了得到某个类型或某个变量在特定平台上的准确大小,可以使用sizeof运算符。
整数类型
类型 | 存储大小 | 取值范围 |
---|---|---|
char | 1 byte | -128 ~ 127或0 ~ 255 |
unsigned char | 1 byte | 0 ~ 255 |
signed char | 1 byte | -128 ~ 127 |
int | 2或4 bytes | -32,768 ~ 32,767或-2,147,483,648 ~ 2,147,483,647 |
unsigned int | 2或4 bytes | 0 ~ 65,535或0 ~ 4,292,967,295 |
short | 2 bytes | -32,768 ~ 32,767 |
unsigned short | 2 bytes | 0 ~ 65,535 |
long | 4 bytes | -2,147,483,648 ~ 2,147,483,647 |
unsigned long | 4 bytes | 0 ~ 4,294,967,295 |
浮点类型
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4 bytes | 1.2E-38 ~ 3.4E+38 | 6位小数 |
double | 8 bytes | 2.3E-308d ~ 1.7E+308 | 15位小数 |
long double | 10 bytes | 3.4E-4932 ~ 1.1E+4932 | 19位小数 |
void类型:常用于下面三种情况
序号 | 类型 | 描述 |
---|---|---|
1 | 函数返回为空 | C中有各种函数不返回值,或者说它们返回空。无返回值的函数的返回类型为空 |
2 | 函数参数为空 | C中有各种函数不接受参数;不带参数的函数可以接受一个void |
3 | 指针指向void | 类型为void *的指针代表对象的地址,而不是类型。 |
转义字符
转义序列 | 含义 |
---|---|
\\ | \字符 |
\' | '字符 |
\" | "字符 |
\? | ?字符 |
\a | 报警警报铃声 |
\b | 退格键 |
\f | 换页符 |
\n | 换行符 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ooo | 一到三位的八进制数 |
\xhh... | 一个或多个数字的十六进制数 |
3.2 C变量
C语言中每个变量都有特定的类型,类型决定了变量存储的大小和布局。变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。变量基本类型与数据类型相对应。
变量可以是如下类型:char、int、float、double、void、枚举、指针、数组、结构、共用体等。
不带初始化的变量定义:带有静态存储持续时间的变量会被隐式初始化为NULL(所有字节的值都是0),其他所有变量的初始值是未定义的。
左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达式;左值可以出现在赋值号的左边或右边。
右值(rvalue):指的是存储在内存中某些地址的数值;右值是不能对其进行赋值的表达式,也就是说右值只能出现在赋值号的右边。
3.3 C常量
3.3.1 什么是常量?
常量是固定值,在定义之后就不能进行修改(程序执行期间不会发生改变)。这种固定的值,又叫做字面量。常量就像常规的变量,只不过常量的值在定以后不能进行修改。
常量也可以是任意的基本数据类型,比如:整数常量、浮点常量、字符常量、字符串常量,或枚举常量。
整数常量前面可以添加前缀:0x或0X表示十六进制,0表示八进制,不带前缀表示默认十进制;也可以带U或L后缀:U表示无符号整数,L表示长整数。U或L可以大写,也可以小写,顺序任意。
浮点常量由整数部分、小数点、小数部分和指数部分组成。指数是用e或E来表示的。
字符常量是括在单引号''中的,可以是普通字符(如'x')、转义字符(如‘\t’,键转义字符表),或一个通用字符(如‘\u02c0’)。
字符串常量是括在双引号""中的, 可以是普通的字符、转义序列或通用字符。可以用空格做分隔符,表示一个很长的字符串常量。
3.3.2 常量的定义
在C中,有两种简单的方式定义常量:
- 使用#define预处理器;
- 使用const关键字;
使用#define预处理器定义常量的形式为:
#define identifier value
也可以使用const前缀声明指定类型的常量:
const type variable = value;
通常,我们使用大写字母来表示常量,这是一个很好的编程习惯。
3.4 C存储类
存储类定义了C程序中变量/函数的可见性范围和生命周期,这些存储类说明符放置在所修饰的类型之前。常见的C存储类说明符如下:
- auto
- register
- static
- extern
3.4.1 auto存储类
auto存储类是所有局部变量默认的存储类,且auto只能用在函数内,即auto只能修饰局部变量。
3.4.2 register存储类
register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 '&' 运算符(因为它没有内存位置)。
寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 'register' 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。
3.4.3 static存储类
static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
在 C 编程中,当 static 用在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。
3.4.4 extern存储类
extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 'extern' 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。
extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候。
四、C复合数据
4.1 数组
数组是可以存储一个固定大小的相同类型元素的顺序集合。所有的数组都是由连续的内存位置组成。在C中声明一个数组,需要指定元素的类型的元素的数量:
type arrayName[ arraySize ];
arraySize必须是一个大于等于0的整数常量,type可以是任意有效的C数据类型。
在C中,您可以逐个初始化数组,也可以使用一个大括号{}的初始化语句。
多维数组
C支持多维数组,多维数组最简单的形式是二维数组。
type name[size1][size2]...[sizeN];
传递数组给函数
传递数组作为函数的参数,可使用如下三种方式来实现:
# 方式 1
void myFunction(int *param)
{
...
}
# 方式 2
void myFunction(int param[10])
{
...
}
# 方式 3
void myFunction(int param[])
{
...
}
函数返回数组
C语言不允许返回一个完整的数组,但是可以通过不带索引的数组名来返回一个指向数组的指针:
# 返回type类型的数组指针
type * myFunction()
{
...
}
注意:C不支持函数返回局部变量的地址,除非局部变量定义为static。
指向数组的指针
数组名是一个指向数组中第一个元素的常量指针。
# array是一个指向array[0]的指针,值为&array[0]
type array[N];
# p是一个指向type数据类型的指针
type *p;
# 给p赋值,让p指向array数组的第一个元素
p = array;
# 使用指针p来访问数组:
*p;
*p++;
*(p+3);
4.2 指针
每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问其地址的方法,它表示了内存中的一个地址。
指针是一个变量,其值为另一个变量的地址,即内存位置的直接地址。
type *var-name;
所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
NULL指针
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。NULL 指针是一个定义在标准库中的值为零的常量。
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。
C指针算术运算
C指针是一个用数值表示的地址,因此,可以对指针执行算术运算:++、--、+、-、==、<、>。
C指针数组
声明ptr为一个数组,每一个ptr数组的元素都是一个指向int类型值的指针。
int *ptr[MAX];
C数组指针
声明ptr为一个指针,指向一个元素个数为MAX个int类型的数组的指针。
int (*ptr)[MAX];
C指向指针的指针
指向指针的指针是一种两级间接寻址的形式,是一个指针链。当然也可以定义多级间接寻址的形式。定义指向指针的指针:
int var;
int *ptr1 = &var;
int **ptr2 = &ptr1;
C传递指针给函数
当通过指针的方式传递给函数时,我们可以在函数内部修改外部变量的值。
C从函数返回指针
C语言可以通过返回指针的方式来返回多个值,返回静态变量,返回动态分配的内存。但是要注意:C不支持在函数外返回局部变量的地址,除非定义局部变量为static变量。
4.3 字符串
在C语言中,字符串实际上是使用null字符('\0')终止的一维字符数组。C中有大量操作字符串的函数:
序号 | 函数 & 目的 |
---|---|
1 | strcpy(s1, s2); 复制字符串s2到字符串s1 |
2 | strcat(s1, s2); 连接字符串s2到字符串s1的末尾 |
3 | strlen(s1); 返回字符串s1的长度 |
4 | strcmp(s1, s2); 如果s1和s2是相同的,则返回0;如果s1 < s2则返回小于0;如果s1 > s2则返回大于0 |
5 | strchr(s1, ch); 返回一个指针,指向字符串s1中字符ch的第一次出现的位置。 |
6 | strstr(s1, s2); 返回一个指针,指向字符串s1中字符串s2的第一次出现的位置。 |
4.4 结构体
C数组允许定义可存储相同类型数据的变量;结构是C中另一种用户自定义的可用数据类型,它可以存储不同类型的数据项。
结构体定义
struct [struct tag]
{
member definition;
member definition;
...
member definition;
}[one or more structure variable];
struct tag是可选的,每个member definition是标准的变量定义;在结构体定义的末尾,最后一个分号之前,可以指定一个或多个结构体变量,也是可选的。
访问结构成员
为了访问结构的成员,我们使用成员访问运算符(.),如果是指针的话,则使用(->)。
结构作为参数
可以把结构体作为参数,传参方式与其他基本类型的变量或指针类似,都是值传递方式。
4.5 共用体
共用体是一种特殊的数据结构,允许你在相同的内存位置存储不同的数据类型。你可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带值。
union [union tag]
{
member definition;
member definition;
...
member definition;
}[one or more union variable];
union tag是可选的,每个member definition是标准的变量定义;在共用体定义的末尾,最后一个分号之前,你可以定义一个或多个共用体变量,也是可选的。
共用体的访问方式与结构体一样,只是要注意任何时候只能有一个成员带值
4.6 位域
有些信息在存储时,并不需要占用一个完整字节,而只需要占几个或一个二进制位。为了节省存储空间,并使处理简便,C语言提供了一种数据结构,称为“位域”或“位段”。
所谓“位域”就是把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
struct 位域结构名
{
类型说明符 位域名1:位域长度;
类型说明符 位域名2:位域长度;
...
类型说明符 位域名n:位域长度;
}
位域的使用与结构体和共用体类似。但应注意以下几点:
- 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一个位域时,应从下一个单元存放该位域;也可以有意使某位域从下一单元开始,比如:
struct bs{
unsigned a:4;
unsigned :4;
unsigned b:4;
unsigned c:4;
}
- 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。如果最大长度大于计算机的整数字长,一些编译器可能会允许域的内存重叠,另外一些编译器可能会把大于一个域的部分存储在下一个字中。
- 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。
4.7 typedef
typedef关键字可以用来为类型取一个新名字。按照惯例,定义新名字时会使用大写字母,以便提醒用户类型名称是一个象征性的缩写。如下:
typedef unsigned char BYTE
你也可以使用typedef来为用户自定义的数据类型(结构体、枚举、共用体、位域结构)取一个新名字。
typedef与define
- typedef仅限于类型定义符号名称,#define不仅可以为类型定义别名,也能为数值定义别名(1定义为ONE);
- typedef是由编译器执行解释的,#define语句是预编译器进行扩展处理的。
4.8 强制类型转换
强制类型转换是把变量从一种类型转换为另一种数据类型。使用强制类型转换运算符显示的转换一种类型:
(type_name) expression
类型的转换经常是隐式的,由编译器自动执行的。但是,作为一个有追求的程序员,建议都是用显示的类型转换,这也是一个良好的编程习惯。
整数提升:整数提升是指把小于int或unsigned int的整数类型转换为int或unsigned int的过程。
常用的算是转换不适用于赋值运算符、逻辑运算符&&和||。比如:
int i = 17;
char c = 'c';
float sum = i + c;
由于最后的值是float类型,所以编译器会先把i和c转换为浮点型,然后将其相加后赋值给sum。
五、C作用域规则
任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。C 语言中有三个地方可以声明变量:
- 在函数或块内部的局部变量
- 在所有函数外部的全局变量
- 在形式参数的函数参数定义中
5.1 局部变量
在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。
5.2 全局变量
全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的。
在程序中,局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值。
5.3 形式参数
函数的参数,形式参数,被当作该函数内的局部变量,它们会优先覆盖全局变量。
5.3 初始化局部变量和全局变量
当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化。
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | '' |
float | 0 |
double | 0 |
pointer | NULL |
正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。
注意:变量的作用域可以通过前面讲解的static和extern关键字修改。
六、C运算符
运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C语言提供了如下类型的运算符:
- 算符运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 杂项运算符
6.1 算术运算符
假设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 |
a++与++a:a++是先操作后再+1,++a是先+1后再操作。
6.2 关系运算符
假设A值为10,B值为20。
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真 | (A == B)为假 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真 | (A != B)为真 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真 | (A > B)为假 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真 | (A < B)为真 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真 | (A >= B)为假 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真 | (A <= B)为真 |
6.3 逻辑运算符
假设A的值为1,B的值为0。
运算符 | 描述 | 实例 |
---|---|---|
&& | 逻辑与运算符:如果两个操作数都非0,则条件为真 | (A && B)为假 |
| | 逻辑或运算符:如果两个操作数中有任意一个非0,则条件为真 | (A || B)为真 |
! | 逻辑非运算符:用来逆转操作数的逻辑状态,如果条件为真则逻辑非运算符将使其为假 | !( A && B)为真 |
6.4 位运算符
假设A值为60,B值为13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与操作,按二进制位进行‘与’运算。运算规则见下面真值表 | (A & B)将得到12 |
按位或运算符,按二进制位进行‘或’运算。运算规则见下面真值表 | (A | B)将得到61 | |
^ | 按位异或运算符,按二进制位进行‘异或’运算。运算规则见下面真值表 | (A ^ B)将得到48 |
~ | 按位取反运算符,按二进制位进行‘取反’运算 | (~A)将得到带符号的-61 |
<< | 二进制左移运算符,左操作数的值向左移动右操作数指定的位数(左边的二进制位丢弃,右边补0) | A << 2将得到240 |
>> | 二进制右移运算符,左操作数的值向右移动右操作数指定的位数(正数左补0,负数左补1,右边位丢弃) | A >> 2将得到15 |
&、|和^位操作真值表如下:
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
6.5 赋值运算符
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | 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 |
6.6 杂项运算符
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量占用内存的大小 | sizeof(a)将返回变量a占用的内存大小 |
& | 返回变量的地址 | &a将给出变量的实际地址 |
* | 指向一个变量 | *a将指向一个变量 |
?: | 条件表达式 | 如果条件为真?则值为X : 否则值为Y |
6.7 运算符优先级
运算符的优先级确定表达式中项的组合优先顺序,这会影响到一个表达式如何计算。较高优先级的运算符会被优先计算。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () [] -> . ++ -- | 从左到右 |
一元 | + - ! ~ ++ -- (type) * & sizeof | 从右到左 |
乘除 | * / % | 从左到右 |
加减 | + - | 从左到右 |
移位 | << >> | 从左到右 |
关系 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与AND | & | 从左到右 |
位异或XOR | ^ | 从左到右 |
位或OR | | | 从左到右 |
逻辑与AND | && | 从左到右 |
逻辑或OR | || | 从左到右 |
条件 | ?: | 从右到左 |
赋值 | = += -= *= /= %= >>= <<= &= ^= |= | 从右到左 |
逗号 | , | 从左到右 |
七、C程序控制
7.1 C判断
C语言把任何非零和非空的值假定为true,把零或null假定为false。
7.1.1 if语句
一个if语句由一个布尔表达式后跟一个或多个语句组成。
if(boolean_expression)
{
/* 如果布尔表达式为真将执行的语句 */
}
7.1.2 if...else语句
一个if语句后可跟一个可选的else语句,else语句在布尔表达式为假时执行。
if(boolean_expression)
{
/* 如果布尔表达式为真将执行的语句 */
}
else
{
/* 如果布尔表达式为假将执行的语句 */
}
注意:条件运算符 ? : 可以用来替代if...else语句。
7.1.3 if...else if...else
一个if语句后可跟一个可选的else if ... else语句,这可用于测试多种条件。
if(boolean_expression 1)
{
/* 当布尔表达式1为真时执行 */
}
else if(boolean_expression 2)
{
/* 当布尔表达式2为真时执行 */
}
else if(boolean_expression 3)
{
/* 当布尔表达式3为真时执行 */
}
else
{
/* 当上面条件都不为真时执行 */
}
7.1.4 嵌套if语句
可以在一个if或else if语句内使用另一个if或else if语句。
if(boolean_expression 1)
{
/* 当布尔表达式1为真时执行 */
if(boolean_expression 2)
{
/* 当布尔表达式2为真时执行 */
}
else
{
/* 当布尔表达式2为假时执行 */
}
}
else
{
/* 当布尔表达式1为假时执行 */
}
7.1.5 switch语句
一个switch语句允许测试一个变量等于多个值时的情况。每个值称为一个case,且被测试的变量会对每个switch case进行检查。
switch(expression){
case constant-expression :
statement(s);
break; /* 可选的 */
case constant-expression :
statement(s);
break; /* 可选的 */
/* 你可以有任意数量的case语句 */
default : /* 可选的 */
statement(s);
break; /* 可选的 */
}
switch语句遵循如下规则:
- switch 语句中的 expression 必须是一个整型或枚举类型,或者是一个 class 类型,其中 class 有一个单一的转换函数将其转换为整型或枚举类型。
- 在一个 switch 中可以有任意数量的 case 语句。每个 case 后跟一个要比较的值和一个冒号。
- case 的 constant-expression 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量或字面量。
- 当被测试的变量等于 case 中的常量时,case 后跟的语句将被执行,直到遇到 break 语句为止。
- 当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
- 不是每一个 case 都需要包含 break。如果 case 语句不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。
- 一个 switch 语句可以有一个可选的 default case,出现在 switch 的结尾。default case 可用于在上面所有 case 都不为真时执行一个任务。default case 中的 break 语句不是必需的。
7.1.6 嵌套switch语句
可以把一个 switch 作为一个外部 switch 的语句序列的一部分,即可以在一个 switch 语句内使用另一个 switch 语句。即使内部和外部 switch 的 case 常量包含共同的值,也没有矛盾。
switch(ch1) {
case 'A':
printf("这个 A 是外部 switch 的一部分" );
switch(ch2) {
case 'A':
printf("这个 A 是内部 switch 的一部分" );
break;
case 'B': /* 内部 B case 代码 */
}
break;
case 'B': /* 外部 B case 代码 */
}
当然,if语句与switch之间也是可以互相嵌套使用的,只是咱们编程的时候注意嵌套层数最好不好超过3层,否则不利于代码的理解。
7.2 C循环
7.2.1 while循环
当给定条件为真时,重复语句或语句组,它会在执行循环主体之前测试条件
while(condition)
{
statement(s);
}
7.2.2 for循环
多次执行一个语句序列,简化管理循环变量的代码。
for (init; condition; increment)
{
statement(s);
}
下面是for循环的控制流程:
- init 会首先被执行,且只会执行一次。这一步允许您声明并初始化任何循环控制变量。您也可以不在这里写任何语句,只要有一个分号出现即可。
- 接下来,会判断 condition。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧接着 for 循环的下一条语句。
- 在执行完 for 循环主体后,控制流会跳回上面的 increment 语句。该语句允许您更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
- 条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for 循环终止。
7.2.3 do...while循环
除了它是在循环主体结尾测试条件外;其他与while语句类似,但do...while循环至少执行一次循环。
do
{
statement(s);
}while(condition);
7.2.4 嵌套循环
可以在while、for或do...while循环内使用一个或多个循环。
// for循环嵌套
for (init; condition; increment)
{
for(init; condition; increment)
{
statement(s);
}
statements(s);
}
// while循环嵌套
while(condition)
{
while(condition)
{
statement(s);
}
statement(s);
}
// do...while循环嵌套
do
{
statement(s);
do
{
statement(s);
}while(condition);
}while(condition);
7.2.5 循环控制语句
break语句:终止loop或switch语句,程序流将继续执行紧接着loop或switch的下一条语句;若是嵌套循环,break语句会停止最内层的循环。
continue语句:引起循环跳过主体的剩余部分,立即重新开始测试条件。
goto语句:将控制转移到被标记的语句,但不建议在程序中使用goto语句。
注意:在任何编程语言中,都不建议使用 goto 语句。因为它使得程序的控制流难以跟踪,使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语句的写法。
7.2.6 无限循环
如果条件永远为真,则循环将变成无限循环。一般情况下,使用for(;;)结构来表示一个无限循环。
可以按Ctrl + C键终止一个无限循环。
八、C函数
8.1 函数定义
C语言定义函数的一般形式为:
return_type function_name(parameter_list)
{
body of the functioin
}
在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:
- 返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void。
- 函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
- 参数:参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
- 函数主体:函数主体包含一组定义函数执行任务的语句。
8.2 函数声明
函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。函数声明包括以下几个部分:
return_type function_name(parameter_list)
8.3 函数调用
当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务,当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。
8.3 函数参数
如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。
形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。当调用函数时,有两种向函数传递参数的方式:
调用类型 | 描述 |
---|---|
传值调用 | 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。 |
引用调用 | 该方法把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 |
默认情况下,C使用传值调用来传递参数(函数内的代码不能改变用于调用函数的实际参数)。
8.4 递归
递归是以自相似的方式重复项目的处理过程。同样地,在编程语言中,在函数内部调用函数自身,称为递归调用。
在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入无限循环。递归函数在解决许多数学问题上起了至关重要的作用,比如计算一个数的阶乘、生成斐波那契数列,等等。
数的阶乘:
int factorial(unsigned int i)
{
if(i <= 1)
return 1;
return i*factorial(i - 1);
}
斐波那契数列:
int fibonaci(int i)
{
if(i == 0)
return 0;
if(i == 1)
return 1;
return fibonaci(i - 1) + fibonaci(i - 2);
}
九、C文件读写
一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节。C语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。
9.1 打开文件
fopen()函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息:
FILE *fopen(const char *filename, const char *mode);
filename是字符串,用来命名文件,访问模式mode的值可以是下列值的一个:
模式 | 描述 |
---|---|
r | 打开一个已有的文本文件,允许读取文件 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容 |
r+ | 打开一个文本文件,允许读写文件 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式 |
9.2 关闭文件
当使用完文件时,需要调用fclose()关闭对应的文件,否则会造成资源浪费:
int fclose(FILE *fp);
如果成功关闭文件,fclose()函数返回零,如果关闭文件时发生错误,函数返回EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。
9.3 写入文件
int fputc(int c, FILE *fp);
int fputs(const char *s, FILE *fp);
int fprintf(FILE *fp, const char *format, ...);
fputc()把参数c的字符值写入到fp所指向的输出流中,写入成功返回写入的字符,发生错误则返回EOF;使用fputs()可以把以null结尾字符串s写入fp指向的输出流中,写入成功返回一个非负值,发生错误则返回EOF;也可以使用fprintf()函数来格式化输出字符串到fp指向的输出流中。
9.4 读取文件
int fgetc(FILE *fp);
char *fgets(char *buf, int n, FILE *fp);
int fscanf(FILE *fp, const char *format, ...);
fgetc()函数从fp所指向的输入文本中读取一个字符,返回读取的字符,错误则返回EOF;fgets()从fp所指向的输入流中读取n-1个字符,将读取到的字符串复制到缓冲区buf,并在最后追加一个null字符来终止字符串;也可使用fscanf()函数从文件格式化读取要获取的值。
9.5 二进制I/O函数
下面两个函数用于存储块的读写,常用于数组或结构体:
size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
十、C其他相关
10.1 C预处理器
C预处理不是编译器的组成部分,但它是编译过程中的一个步骤。
所有的预处理器命令都是以井号(#)开头,它必须是第一个非空字符,为了增加可读性,预处理器指令应从第一列开始。常见的预处理器指令如下:
指令 | 描述 |
---|---|
#define | 宏定义 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if的替代方案 |
#elif | 如果前面的#if给定条件不为真,elif后面为真则编译下面的代码 |
#endif | 结束一个#if ... #else条件编译块 |
#error | 当遇到标准错误时,输出错误信息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
ANSI C定义了许多宏,在编译中你可以使用这些宏,但不能直接修改这些预定义的宏:
宏 | 描述 |
---|---|
_DATE_ | 当前日期,一个以“MM DD YYYY”格式表示的字符常量 |
_TIME_ | 当前时间,一个以“HH:MM:SS”格式表示的字符常量 |
_FILE_ | 这会包含当前文件名,一个字符串常量 |
_LINE_ | 这会包含当前行号,一个十进制常量 |
_STDC_ | 当编译器以ANSI标准编译时,则定义为1 |
一个宏通常写在一个单行上,但如果宏太长,单行容不下则可以使宏延续运算符(\)。
在宏定义中,当需要把一个宏的参数转换为字符串常量时,可以使用字符串常量化运算符(#)。
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
message_for(Carole, Debra)的结果是Carole and Debra: We love you!
宏定义内的标记粘贴运算符(##)会合并两个参数,它允许在宏定义中两个独立的标记被合并为一个标记。
#define tokenpaster(n) printf("token" #n " = %d\n", token##n)
int token32 = 40;
tokenpaster(32);
以上结果会打印:token32 = 40
预处理器defined运算符用在常量表达式中,用来确定一个标识符是否已经使用#define定义过,如果已定义则为真(非零),否则为假(零)。
#if !defined (MESSAGE)
#define MESSAGE "You wish!"
#endif
10.2 C头文件
头文件是扩展名为.h的文件,包含了C函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。
A simple practice in C或C++程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这个头文件。
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中:
#ifndef HEAD_FILE
#define HAED_FILE
the entire header file file
#endif // HEAD_FILE
如上这种结构就是常说的包装器#ifndef。有时候需要从多个不同的头文件中根据条件选择一个引用到程序中,则可以通过一系列宏条件来实现:
#if SYSTEM_1
#include "system_1.h"
#elif SYSTEM_2
#include "system_2.h"
#elif SYSTEM_3
...
#endif
10.3 C错误处理
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的C或UNIX函数调用返回1或NULL,同时会设置一个错误代码errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 <error.h>头文件中找到各种各样的错误代码。
所以,C程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把errno设置为0,这是一种良好的编程习惯。0值表示程序中没有错误。
C语言提供了perror()和strerror()函数来显示与errno相关的文本消息:
- perror()函数显示您传给它的字符串,后跟一个冒号、一个空格和当前errno值的文本表示形式。
- strerror()函数返回一个指针,指针指向当前error值的文本表示形式。
10.5 C可变参数
有时候可能会碰到希望函数带有可变数量的参数,而不是预定义数量的参数。在C语言中,函数func()最后一个参数写成省略号(...),省略号之前的那个参数总是int,代表了要传递的可变参数的总个数:
int func(int, ...)
{
...
}
C语言还提供了stdarg.h实现了可变参数功能的函数和宏,具体操作步骤如下:
- 定义一个函数,最有一个参数为省略号,省略号前面的那个参数总是int,表示可变参数的个数;
- 在函数定义中创建一个va_list类型变量,该类型是在stdarg.h头文件中定义的;
- 使用int参数和va_start宏来初始化va_list变量为一个参数列表。宏va_start是在stdarg.h头文件定义的;
- 使用va_arg宏和va_list变量来访问参数列表中的每个项;
- 使用宏va_end来清理赋予va_list变量的内存。
下面函数实现计算数据平均值的功能:
double average(int num, ...)
{
int i;
va_list valist;
double sum = 0.0;
va_start(valist, num);
for(i = 0; i < num; i++)
sum += va_arg(valist, int);
va_end(valist);
return sum/num;
}
10.6 C命令行参数
执行程序时,可以从命令行传值给C程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。
命令行参数是使用main()函数参数来处理的,其中,argc是指传入参数的个数,argv[]是一个指针数组,指向传递给程序的每个参数。
argv[0]存储程序的名称,argv[1]是一个指向第一个命令行参数的指针,*argv[n]是最后一个参数。如果没有提供任何参数,argc将为1,否则,如果传递了一个参数,argc 将被设置为2。
多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号""或单引号''内部。
10.7 内存管理
C语言为内存的分配和管理提供了如下接口(在stdlib.h头文件中):
序号 | 函数和描述 |
---|---|
1 | void *calloc(int num, int size);该函数分配一个带有num个元素的数组,每个元素的大小为size字节 |
2 | void free(void *address);该函数释放address所指向的address内存块 |
3 | void *malloc(int num);该函数分配一个num字节的数组,并把它们进行初始 |
4 | void *realloc(void *address, int newsize);该函数重新分配内存,把内存扩展到newsize大小 |