前言
本文包含几乎所有C99语法及常用标准库的基本知识,但不包含编程的一些基本知识以及网络以及多线程库的知识。
变量
变量是内存中一个具体的存储空间。这个存储空间存储一个具体类型的值,它还有一个名字叫做变量名。
变量的性质
在C语言中每个变量都具有以下性质:
- 存储期限:存储期限决定了为变量释放内存空间的时间,具有自动存储期限的变量会在所属程序块被执行时获得内存空间,在结束时释放内存空间。具有静态存储期限的变量在程序运行的整个期间都会占用内存空间。
- 作用域:作用域决定了变量的有效范围,具有块作用域变量在变量所属的块中起作用,具有文件作用域的变量在整个源文件内起作用。
- 链接:链接决定了变量在文件之间的共享范围,具有内部链接的变量只在所属文件内起作用,具有外部链接的变量可以在不同文件内起作用。无链接的变量只在所属文件的所属函数内起作用。
变量的默认存储期限、作用域和链接都依赖于变量的声明位置:
- 在程序块内声明的变量具有自动存储权限、块作用域并且无链接。
- 在程序块外层声明的变量具有静态存储权限、文件作用域和外部链接。
变量的声明和变量的定义
变量的定义用于为变量分配存储空间,在程序中一个变量只能被定义一次。变量的声明用于指定变量的声明说明符和声明符,在程序中一个变量可以被声明多次。变量的声明和变量的定义往往是同时发生的,但在使用extern 关键字并且在它不失效的情况下,只有变量的声明而没有变量的定义。使用变量之前一定要对其进行定义和声明。
声明说明符
声明说明符分为三类:存储类型符、类型限定符和类型说明符。
存储类型符
存储类型符在变量声明中最多出现一种,并且必须在声明说明符的最前面。
- auto:
auto 修饰的变量具有自动存储权限、块作用域并且无连接,它只对块作用域的变量有效,因为对于块作用域的变量它是默认的。 - static:
static 修饰的文件作用域的变量具有静态存储权限、文件作用域和内部链接;static 修饰的块作用域的变量具有静态存储权限、块作用域并且无连接。 - extern:
extern 用于在当前作用域引入在当前文件或其它文件中定义的具有外部链接的变量。这些变量会具有静态存储权限和外部连接的性质。使用extern 声明的变量不会占用内存空间。但如果在引入变量时又对变量进行了初始化,那么extern 将失效。 - register:
register 的性质和auto 完全一致。但使用register 声明的变量会请求编译器把它存储在寄存器中,并且由于寄存器没有地址,所以对register 声明的变量使用取地址运算符是非法的,但该请求不一定得到应允。
类型限定符
- const:
const 修饰的变量是只读的,不能被修改。 - volatile:volatile关键字通常用于指向易变内存空间的指针的声明中,它告诉编译器该内存空间的数据是易变的,在每次使用指针取值时都必须从内存中直接获取。
- restrict:见下文受限指针。
类型说明符
整数类型
C语言允许使用十进制、八进制和十六进制书写整数类型值,其中八进制必须以0 开头,十六进制必须以0x 开头。编译器默认将整数常量当作int 型处理,可以通过添加后缀的方式使编译器改变默认处理类型。在不加后缀的情况下,如果int 型存不下十进制数,那么编译器会依次尝试、long int 、long long int 。对于八进制和十六进制,编译器会依次尝试unsigned int 、long int 、unsigned long int 、long long int 、unsigned long long int 。
类型说明符 | 后缀 |
---|
short int | - | unsigned short int | - | int | - | unsigned int | u | long int | l | unsigned long int | ul | long long int | ll | unsigned long long int | ull |
浮点类型
编译器默认将浮点常量当作double 型处理,可以通过添加后缀的方式使编译器改变默认处理类型。
类型说明符 | 后缀 |
---|
float | f | double | - | long double | l |
字符类型
编译器将字符类型当作小整数类型处理。
声明符
声明符包含标识符、标识符前面可能带有的# 号以及后面可能带有的[] 和() 。对于一眼看不出来的声明符,可以使用以下方法进行分析:
- 以标识符为中心由内往外读声明符。
- 在做选择时,始终使
[ ] 和() 的优先级大于* 。
不完整类型
不完整类型是在编写C语言大型程序时极其重要工具,C语言对不完整类型的描述是:描述了变量,但缺少定义变量大小所需要的信息。不完整类型将会在程序的其它地方将信息补充完整。这就起到了很好的类型封装作用。
不完整类型的限制
由于编译器不知道不完整类型的大小,所以它的使用是受限的:
- 不能使用不完整类型来定义变量,但可以定义一个指向不完整类型的指针。
- 不能对不完整类型使用
sizeof 运算符。 - 数组、结构和联合的成员不可以具有不完整类型。
- 函数的形式参数不可以具有不完整类型。
灵活数组成员
当结构的最后一个成员是数组时,其长度是可以忽略的,那么这种结构成员就称为灵活数组成员。灵活数组成员是一种特殊的不完整类型,它的特殊之处在于:
- 灵活数组成员必须出现在结构的最后,而且结构必须至少还有一个其它成员。
- 复制包含灵活数组的结构时,其它结构成员都会被复制,但不会复制灵活数组本身。
- 在使用
sizeof 运算符来确定结构的字节数量时会忽略灵活数组的大小。 - 可以使用包含灵活数组的结构定义变量。
- 包含灵活数组的结构可以作为函数参数。
灵活数组成员的意义在于可以动态分配结构的内存大小。
变量的初始化和变量的赋值
变量的初始化发生在变量定义时,变量的赋值发生在变量定义之后。变量的初始化分为自动初始化和手动初始化。如果访问一个不能自动初始化并且没有手动初始化或赋值的变量,结果将是不确定的。因此在访问变量之前必须进行变量的初始化或赋值。自动初始化的规则如下:
- 具有动态存储权限的变量没有默认的初始值。
- 具有静态存储权限的变量会基于类型初始化零值。
手动初始化的规则如下:
- 具有静态存储器期限的变量的初始化值必须是常量。
- 具有金泰存储权限的块作用域的变量的手动初始化只会执行一次。
表达式和运算符
表达式是表示如何计算的公式,任何一个表达式后加上一个分号就变成了一个语句。一些特殊的表达式如下:
- 常量表达式:不能包含变量和函数调用的表达式,常用于
case 语句后。 - 逗号表达式:可以使用逗号将多个表达式分隔而组成一个新的表达式,这个新表达式的值为最后一个子表达式的值,其它子表达式的值都将被抛弃。
- %的操作数只能是整数。
左值和右值
左值表示一个内存空间,不能是常量或者表达式的计算结果。左值以外的值都是右值。赋值运算符要求它的左操作数必须是左值。
表达式中的类型转换
在c语言中类型转换分为两种:第一种是编译器自己就能处理的隐式转换,第二种是使用强制运算符的显示转换。
隐式转换
当发生下列情况会发生隐式转换:
-
算术表达式或逻辑表达式中操作数的类型不相同:此时会将操作数转换成适用于两个数值的最小类型。当任意操作数为浮点类型时,会按照float 、double 、long double 的顺序转换。当两个操作数都不是浮点类型时,首先将两个操作数中能转换为int 型的转换为int 型,如果此时两个操作数的类型相同,过程结束。否则依次尝试以下规则,如果两个操作数都是有符号数或无符号数,将类型小的操作数转换为类型大的操作数的同类型;如果无符号数的存储范围大于等于有符号数的存储范围,则将有符号操作数转换为无符号操作数的类型;如果有符号数可以表示所有无符号数,则将无符号操作数转换为有符号操作数的类型。 -
赋值运算符右侧表达式的类型和左侧变量的类型不匹配:唯一的转换原则是把赋值运算右边的表达式转换成左边变量的类型。 -
函数调用时实参和形参类型不匹配:如何转化实际参数的规则与编译器是否在调用前遇到函数的原型有关,如果编译器在调用前遇见函数原型,那么就进行上文第一种的转换;如果编译器在调用前没有遇见函数原型,那么就会把float 类型实参转换为double 类型,把char 和short int 类型实参转化为int 型。 -
return 语句中表达式类型和函数返回值类型不匹配:进行上文第一种的转换。
强制转换
强制类型转换使用强制转换表达式进行转化,强制类型转换,当将大类型数据转换为小类型数据是会发生数据丢失。
(type)expression
typedef运算符
typedef 可以用来定义数据类型,typedef定义的类型会被编译器加入到它所识别的类型名列表。
typedef <primitiveType> <newType>
sizeof运算符
sizeof 表达式用于计算存储类型、常量、变量和表达式值的字节数,它的值一个size_t 类型的值,这是一种无符号整数类型,所以在使用sizeof 表达式时最好将它的值转换为unsigned long 类型。
sizeof(value)
预处理器
预处理器的输入是一个C语言程序,程序可能包含预处理指令,预处理器会处理这些指令,并在执行过程中删除它们,它的输出是另一个C程序,也就是原程序编辑后的版本,不再包含预处理指令。这个输出会直接交给编译器,编译器检查程序是否有错误,并将程序翻译为机器代码。预处理指令以井号开头,指令的符号之间以任意的空白字符分隔,预处理指令默认在行尾结束,如果需要在下一行延续指令,那么必须在当前行的末尾使用\ 进行换行,它可以出现在源文件的任何地方,且不需要以分号结尾。根据指令的作用可以将预处理指令分为宏定义指令、条件编译指令、文件包含指令以及其它指令。
宏定义
宏定义的格式如下,宏的替换列表也可以包含对其它宏的调用。一个宏不可以被定义两遍,除非新的定义和旧的定义是一样的。当预处理器遇见宏定义时,会将文件中的标识符全部替换为替换列表。
#define 标识符 替换列表
#undef 标识符
带参数的宏
宏定义也可以带有参数,参数列表也可以为空,参数没有类型,也没有类型检查。标识符和左括号之间必须没有空格,如果有空格,预处理器会将括号右边的内容全部视为替换列表。如果在替换列表中使用了参数,那么每个参数都应该放在括号中,这么做可以保证替换前后的语义一致。
#define 标识符(x1,x2,x3...) 替换列表
在调用宏时可以少传任意数量的参数,但实参列表必须要有和全参调用一样多的逗号。宏也支持可变参数列表,语法和函数相同,__VA_ARGS__ 是一个专门的标识符,只能出现在具有可变参数列表的宏的替换列表中,代表所有与省略号相对应的参数。
多表达式、多语句的宏
当需要在宏定义中包含多个表达式时,可以使用逗号运算符进行分隔:
#define 标识符 (expr1,expr2)
当需要在宏定义中包含多条语句时,可以将语句放在do-while 循环中,并将条件设置为假:
#define 标识符 \
do{ \
expr1; \
expr2; \
}while(0)
宏中的运算符
# 运算符将宏的一个参数转换为字符串字面量。如果空参数成为该运算符的操作数,那么运算结果将是一个空串。
#define input(a,b) scanf(#a"is%d" #b"is%d",&a,&b)
## 运算符可以将两个记号粘合在一起成为一个记号。如果该运算符之后的一个参数为空,那么它将被不可见的位置标记代替,
#define same(a) (i##a)
int same(1),same(2),same(3);
预定义的宏
C语言中一些常用的预定义宏如下:
名称 | 说明 |
---|
__LINE __ | 当前程序行的行号,表示为整型常量 | __FILE __ | 当前源文件名,表示字符串型常量 | __DATE__ | 编译程序的日期,表示为mm dd yy 形式的字符串常量 | __TIME __ | 编译程序的时间 ,表示hh:mm:ss 形式的字符串型常量 | __STDC __ | 如果编译器符合C标准,那么它的值为1 | __STDC_VERSION __ | 支持的C标准版本 |
条件编译
条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片段的行为。
#if和#endif
当编译器遇见#if 和#endif 命令时,会计算常量表达式的值,如果常量表达式的值为假,那么它们之间的程序段将在预处理时从源代码中删除,否则就保留在程序中。值得注意的是#if 命令会把没有定义的标识符当作值为零的宏对待。
#if 常量表达式
...
#elif 常量表达式
...
#else
...
#endif
#ifdef和#ifndef
#ifdef 和#ifndef 命令会判断一个标识符是否是一个定义过的宏。这两个指令的用法和#if 命令一致。
#ifdef 标识符
#ifndef 标识符
文件包含
多数情况下一个程序要分为多个源文件,其中一个源文件必须包含一个main 函数,这个main 函数会被启动代码调用,而启动代码是由编译器添加到程序中的,是程序和操作系统之间的桥梁。
头文件
为了在多个源文件之间共享信息,可以把这些信息放到一个单独的文件中,然后使用#include 命令把该文件的内容带到每个源文件中,把按照这种方式包含的文件称为头文件。通常会为每一个.c 源文件创建相对应的同名头文件并且在源文件中包含同名头文件,然后再建立一个总的头文件将其它头文件都包含进去。如果要共享一个函数,那么首先将函数定义在一个源文件中,然后在同名头文件中包含这个函数的声明。如果要共享一个变量,那么首先将变量声明在一个源文件中,然后在同名头文件中使用extern 关键字进行声明,extern 关键字会告诉编译器该变量是在程序中的其它位置定义的,因此不需要为它分配内存空间,在数组的声明时可以省略数组的长度。
#include
#include 命令告诉预处理器打开指定的文件,并把这些文件的内容插入到当前文件中。该命令有三种格式:第一种格式用于包含C库中的头文件,预处理器在执行此指令时直接搜索系统头文件所在的目录;第二种格式用于包含自己编写的头文件,预处理器在执行此文件时先搜索当前目录,然后再搜索系统头文件所在的目录;第三种格式中的记号就是宏定义的标识符,也就是将头文件名用宏定义一个记号。
#include <文件名>
#include "文件名"
#include 记号
有时一个源文件可能重复包含相同的头文件,这可能会产生编译错误,如果有一个名为test 的头文件那么可以在该文件中通过条件编译来解决问题:
#ifndef TEST_H
#define TEST_H
...
#endif
其他指令
#error
如果预处理器遇见#error 指令,它会显示一条包含该消息的出错消息。
#error 消息
#line
#line 指令可以使程序行从n 开始编号,且该指令行不参数编号。
#line n
它还可以带一个文件名字符串,那么指令后边的行会被认为来自文件,并且行号会从n 开始。
#line n fileName
数组
数组的所有元素类型相同,为了选择数组元素需要指明元素的位置,数组元素在内存中是按顺序存储的。
类型 数组变量标识符[数组长度];
数组变量初始化时按照数组元素定义的顺序提供值,但是提供的元素值可以少于数组元素的数量,但是不能为空,剩下的元素会用零作为初始值,如果在定义数组变量时同时进行初始化,可以省略数组长度。
类型 数组变量标识符[数组长度]={元素值,...};
数组变量的变量名就是指向数组第一维元素的指针,本质是一个地址常量,因此数组变量不能作为左值出现,只能通过索引的方式为数组变量赋值:
数组变量标识符[索引]=元素值;
C语言还支持变长数组,变长数组的长度在程序执行时计算而不是在程序编译时计算,长度变量不能是一个被const 修饰的具有静态存取期限的变量。
int 长度变量标识符;
类型 数组变量标识符[长度变量标识符];
变长数组的限制在于它没有静态存储期限并且没有初始化式。
字符串
字符串是用双引号括起来的字符序列,并以一个空字符\0 来标识字符串的结束。C语言把字符串作为字符数组处理,当编译器在程序中遇到长度为n 的字符串时,它会为字符串分配长度为n+1 的内存空间,这块空间将用于存储字符串中的字符以及一个用来标志字符串末尾的空字符。
字符串常量
使用字符指针初始化的字符串称为字符串常量,字符串常量是不可改变的。
char *ch="abcdef";
如果一行写不开可以在第一行以\ 结尾,第二行顶格写完:
char *ch="abc\
def";
或者将它们分别用双引号引起来仅以空白字符分割,编译器会自动把它们打包成一个字符串常量:
char *ch="abc" "def";
字符串变量
使用字符数组初始化的字符串称为字符串变量,字符串常量可以修改。在初始化的时候字符数组的长度应该比字符串的长度长一,也可以不指定数组长度。编译器会自动追加空字符来标识结尾。
char ch[]="abcdef";
<string.h>
<string.h> 头文件提供了一些字符串处理函数。
char * strcpy(char * restrict dest,const char * restrict src);
char * strncpy(char * restrict dest,const char * restrict src,size_t n);
char * strcat(char * restrict dest,const char * restrict src);
char * strncat(char * restrict dest,const char * restrict src,size_t n);
int strcmp(const char * s1,const char * s2);
int strncmp(const char * s1,const char * s2,size_t n);
char * strchr(const char * s,int c);
char * strrchr(const char * s,int c);
char * strpbrk(const char * s1,const char * s2);
size_t strspn(const char * s1,const char * s2);
size_t strcspn(const char * s1,const char * s2);
char * strstr(const char * s1,const char * s2);
size_t strlen(const char *s);
结构
结构的成员可以类型不同,并且每个成员都有自己的名字,选择结构成员时需要指定成员的名字。结构的成员在内存中按顺序存储。声明结构和定义结构变量有以下几种形式,常用的形式是第三种。
struct {
类型 成员标识符;
...
}结构变量标识符;
typedef struct {
类型 成员标识符;
...
}结构类型标识符;
结构类型标识符 结构变量标识符;
struct 结构标记{
类型 成员标识符;
...
};
struct 结构标记 结构变量标识符;
结构变量初始化时按照成员定义的顺序提供值,但是提供的成员值可以少于结构成员的数量,剩下的成员会用零作为初始值。
struct 结构标记 结构变量标识符={成员值,...};
空洞对齐
在使用sizeof 运算符来确定结构的字节数量时,有时获得的结果大于各个成员字节数相加的值,这是因为有些计算机要求数据项的地址是某个字节的倍数,为了满足这个要求,编译器会在成员之间留存空洞(即不使用的字节),从而使结构的成员对齐。这也导致了结构变量不能作为左值出现,因为即使结构变量对应,结构中的空洞也不一定对应。所以在赋值时只能通过结构成员进行:
结构变量标识符.结构成员标识符=成员值;
结构复制
当两个结构变量使用第一种方式在一起定义时或使用第二种或第三种方式定义时,可以使用赋值运算符对结构进行复制,此时结构变量名就可以是左值,在对结构进行复制时,结构体内的语言结构也会递归复制。
结构中的位域
可以通过以下形式给结构成员指定它们要占用的bit 位:
struct 结构标记{
类型 位域标识符:所占位数;
...
};
那么就将这样的成员称为位域,位域即一片在逻辑上连续的bit 位,它在物理内存上连不连续由具体的实现决定,所以位域通常是没有地址的,C语言也禁止将取址运算符无法作用于位域。结构中位域的类型必须是int 、unsigned int, 或signed int ,但是最好是指明位域有无符号性,以免产生二义性。C语言还允许省略位域的名字,未命名的位域常作为位域间的填充,以保证其它位域存储在适当位置。
联合
联合的成员可以类型不同,并且每个成员都有自己的名字,选择联合成员时需要指定成员的名字。编译器只会给联合分配能够存下联合中最大成员的内存空间,所有联合成员都存储在这同一内存空间中,因此联合成员在这内存空间内会相互影响彼此覆盖。定义联合变量有以下几种形式,常用的形式是第三种。联合的性质和结构一致,唯一的不同在于在进行联合变量的初始化时只能初始化第一个成员。
枚举
枚举是一种由枚举常量组成的结构,在声明枚举时必须为每个常量命名:
enum {
枚举常量标识符,
...
}枚举变量标识符;
typedef enum {
枚举常量标识符,
...
}枚举类型标识符;
枚举类型标识符 枚举变量标识符;
enum 枚举标记{
枚举常量标识符,
...
};
enum 枚举标记 枚举变量标识符;
C语言会把枚举常量当作整数处理,默认情况下,编译器会把0,1,2,... 赋值给枚举中的常量,也可以在声明时手动赋值,当没有为枚举常量指定值时,它的值默认比前一个常量值大一。
enum 枚举标记{
枚举常量标识符=枚举常量值,
...
};
enum 枚举标记 枚举变量标识符;
在为枚举变量初始化或赋值时,它的值只能是枚举常量:
枚举变量标识符=枚举常量标识符;
函数
函数的声明和定义
和变量不同的是,函数的声明和定义是严格区分的,在调用一个函数之前必须对其进行声明或定义,以下形式就是函数的声明:
返回类型 函数名(形参列表);
以下形式就是函数的定义:
返回类型 函数名(形参列表){
...
}
函数的形参具有和块作用域变量一样的默认性质。函数也可以声明和定义存储类型,但函数的选项只有static 和extern 。使用static 声明和定义的函数具有内部连接,使用extern 声明和定义的函数具有外部连接,默认情况下函数具有外部连接。
函数的参数和返回值
C语言中所有函数的参数都是按值传递的。函数的返回类型不能是数组类型,函数的数组类型形参中的数组长度不起作用,因为当数组类型作为实参传递时,实际传递的是数组第一个元素的地址而不是数组的副本。因此虽然sizeof 运算符可以计算出数组的长度,但是却不能计算出数组实参的长度。所以当函数包含数组类型的形参时,最好在包含一个指示数组长度的形参。当变长数组作为形参时,这一要求就是硬性的,并且最好使用以下形式定义函数:
返回类型 函数名(int 长度变量,类型 变长数组标识符[*]){
...
}
可变参数列表和<stdarg.h>
C语言允许函数据有可变参数列表,它的定义方式如下:
返回类型 函数名(固定形参,...){
...
}
可变参数列列表必须有一个固定形参,并且... 必须是形参列表的最后一个。<stdarg.h> 头文件提供了处理函数可变形参的方法。
typedef char* va_list;
void va_start(va_list ap,可变列表前的参数);
类型 va_arg(va_list ap,类型);
void va_copy(va_list dest,va_list src);
void va_end(va_list ap);
其中va_list 是可变参数列表,所有非固定形参都存储在其中。va_start 会指定可变参数列表的起始位置,va_arg 会依次返回可变参数列表的每一个参数,并且可以指定我们希望的参数类型。va_copy 用于将src 中剩下的参数复制给dest 。va_end 用于清理资源,va_start 和va_copy 的使用必须和va_end 成对出现。
__func__
每一个函数都可以访问__func__ 标识符,它的行为像一个存储当前正在执行的函数名称的字符串变量。
指针
计算机内存以字节为单位进行寻址,每个字节都有一个唯一的用数字表示的内存地址,程序中的变量占用一个或多个字节内存,把第一个字节的地址作为变量的地址,这个变量的地址就是指向这个变量的指针。
指针变量
指针变量就是存储地址的变量。C语言要求指针变量只能存储一种指向内存中特定类型对象的指针,指针变量的声明与普通变量基本一致,唯一的不同就是要在变量名字前加一个星号。指针变量的大小与它的类型无关,只与操作系统的寻址位数相关,如果是64位操作系统,那么指针变量的大小将为8字节。
类型 * 指针变量标识符;
指向指针的指针
指针变量也是有地址的,那么把指针变量的地址称为指向指针的指针,存储指向指针的指针的指针变量的定义方式如下:
类型 ** 指针变量标识符;
以此类推通过增加星号的个数就可以定义指向指向指针的指针的指针变量。
取地址运算符和间接寻址运算符
取地址运算符(&)可以将某个变量的地址赋值给一个指针变量:
int b;
a=&b;
一旦指针变量指向了对象,那么就可以使用间接寻址运算符(*)访问存储在对象中的内容。
printf("%d",*a);
通用指针
void * 类型的指针变量存储的指针是通用指针,本质上它只是内存地址。通用指针可以被赋值给任何类型的指针变量。void * 类型的指针变量也可以被任何指针赋值。
空指针
空指针是不指向任何地方的指针,用宏NULL 来表示,在C语言中所有非空指针都为真,只有空指针为假。
受限指针
使用restrict 关键字声明的指针叫做受限指针,如果受限指针指向的变量在之后需要修改,那么该变量不允许通过除该指针之外的任何方式访问。restrict 是一种对编译器优化的建议,有没有restrict 程序的行为不会发生变化。
内存的动态分配与释放
内存的动态分配需要使用<stdlib.h> 头中的函数:
void * malloc(size_t size);
void * calloc(size_t nmemb,size_t size);
void * realloc(void * ptr,size_t size);
内存分配函数分配的内存块全部来自一个称为堆的存储池,程序可能分配了内存块但丢失了指向这些内存块的指针,这就会造成内存泄漏现象,因此在使用完内存之后就必须使用free 函数进行手动释放:
void free(void * ptr);
原本指向被free 函数释放的内存空间的指针就变成了悬空指针,它们不会指向任何地方。在<string.h> 头文件还有一些有关内存操作的函数:
void * memcpy(void * restrict dest,const void * restrict src,size_t n);
void * memmove(void * dest,const void * src,size_t n);
int memcmp(const void * s1,const void * s2,size_t n);
void * memchr(const void * s,int c,size_t n);
void *memset(void *s,int c,size_t n);
指针与数组
当指针指向数组的元素时,就可以对指针进行算数和逻辑运算。
- 指针加上整数:如果指针加上一个整数
i ,那么指针将指向当前元素后i 维的元素。 - 指针减去整数:如果指针减去一个整数
i ,那么指针将指向当前元素前i 维的元素。 - 两个指针相减:结果为两个指针之间的距离。
- 间接寻址运算符与自增自减运算符:当这两种运算符相遇时,首先区别它们的操作数是谁,其次自增自减运算符的优先级高于间接寻址运算符。
表达式 | 说明 |
---|
*p++ *(p++) | 先*p后p自增 | (*p)++ | 先*p后*p自增 | *++p *(++p) | p先自增后*p | ++*p ++(*p) | p先自增后p |
指针与结构
当一个指针指向结构变量时,可以使用右箭头选择操作符访问结构变量中的成员:
struct test{
int a;
};
struct test var={1},*ptr;
ptr=&var;
那么下面两个表达式是一样的:
(*ptr).a
ptr->a
结构变量在嵌套自身时只能定义一个指针类型数据,因为只有指针变量的大小编译器是已知的,始终为计算机的字长,并且此结构在声明时必须使用结构标记的方式。
指针与函数
每一个函数都有一个地址,那么把函数的地址称为指向函数的指针,存储指向函数的指针的指针变量的定义方式如下:
函数返回值类型 (*函数指针标识符)(函数参数列表);
通过以下方式给指针变量赋值,这是因为当函数名后面没有括号时,编译器会产生一个指向该函数的指针,而不会产生函数调用的代码。
函数指针标识符=函数名;
在使用指针变量调用函数时,可以通过以下两种方式:
(*函数指针标识符)(实参列表);
函数指针标识符(实参列表);
输入输出<stdio.h>
流
在C语言中术语流表示任意输入的源或任意输出的目的地。流可以是键盘、鼠标、屏幕、硬盘以及网卡等。在Linux中万物皆文件,所以也可以说流就是文件,因此在C语言中对流的访问都是通过文件指针FILE * 实现的。<stdio.h> 头文件定义了三个标准流,这些流可以直接使用,不需要对其进行声明、打开或关闭。
typedef struct xxx FILE;
#define EOF (-1)
#define _IOFBF 0x0000
#define _IOLBF 0x0040
#define _IONBF 0x0004
#define SEEK_CUR 1
#define SEEK_END 2
#define SEEK_SET 0
FILE* __acrt_iob_func(unsigned _Ix);
#define stdin (__acrt_iob_func(0))
#define stdout (__acrt_iob_func(1))
#define stderr (__acrt_iob_func(2))
文件操作
<stdio.h> 头文件支持操作字符文件和二进制文件,本质上说所有文件都是二进制文件,只是字符文件中字节表示一个字符而二进制文件中单个字节没有什么意义。文件都存放在磁盘中,每次读写文件都直接访问磁盘是非常消耗系统资源的,因此就需要使用内存充当缓冲区来进行优化。以写文件为例:把输入流的数据写入到缓冲区,当缓冲区满了或关闭流时缓冲区就会自动将数据刷新到磁盘中。每个流都有与之相关的错误指示器和文件末尾指示器,当打开流时就会重置这些指示器,当遇见错误或文件末尾时就会设置这些指示器,一旦设置了指示器,它就会保持这种状态直到显式清除。每个流都有一个相关联的文件位置,当打开文件时,会将文件位置设置在文件开头或结尾(追加方式打开),在执行读写操作时文件位置会自动推进。
FILE* fopen(const char * restrict fileName,const char * restrict mode);
int fclose(FILE* stream);
FILE* freopen(const char * restrict fileName,const char * restrict mode,FILE* stream);
FILE* tmpfile();
char *tmpnam(char * s);
int fflush(FILE* stream);
void setbuf(FILE * restrict stream,char * restrict buffer);
int setvbuf(FILE * restrict stream,char * restrict buffer,int mode,size_t size);
int remove(const char * fileName);
int rename(const char * oldFileName,const char * newFileName);
int feof(FILE * stream);
int ferror(FILE * stream);
void clearerr(FILE * stream);
long ftell(FILE * stream);
void rewind(FILE * stream);
int fseek(FILE * stream,long offset,int origin);
typedef xxx fpos_t;
int fgetpos(FILE * stream,fpos_t * position);
int fsetpos(FILE * stream,fpos_t const * position);
格式化输入输出
int printf(const char * restrict format,...);
int scanf(const char * restrict format,...);
int fprintf(FILE * restrict stream,const char * restrict format,...);
int fscanf(FILE * restrict stream,const char * restrict format,...);
printf 函数的格式串可以包含普通字符和转换说明,普通字符会原样输出,转换说明描述了如何把剩余的实参转换为字符串格式显示出来。转换说明符由百分号开头和跟随其后的最多五个部分组成:
% 标志 最小字段宽度 精度 长度修饰符 转换说明符
标志 | 作用 |
---|
- | 在字段内左对齐,默认是右对齐 | + | 有符号转换得到的数以+ 或- 开头 | 空格 | 有符号转换得到的非负数前面加空格 | # | 以八进制数、十六进制非零数以及浮点数始终有小数点,不能删除由g或G转换输出的数的小数点 | 0 | 用前导0 在数的字段宽度内进行填充,如果转换是d 、i 、o 、u 、x ,并且指定了精度,那么可以忽略该标志 |
- 最小字段宽度:如果数据项太小无法达到这一宽度,那么会对字段进行填充,默认会在数据项左侧添加空格使其右对齐。字段宽度可以是整数也可以是字符
* ,如果是字符* 那么字段宽度由下一个参数决定。 - 精度:精度的含义依赖于转换说明符,如果转换说明符是
d 、i 、o 、u 、x 、,那么精度表示最小位数;如果是a 、e 、f 、那么精度表示小数点后的位数;如果是、g ,那么精度表示有效数字的个数,如果是s ,那么精度表示最大字节数。精度是由小数点后跟一个整数或者字符* 构成的,如果出现字符* 那么精度由下一个参数决定。 - 长度修饰符:长度修饰符表明待显示的数据项类型的长度大于或小于特定转换说明中的正常值。
长度修饰符 | 转换说明符 | 含义 |
---|
hh | dioux | signed char、unsigned char | | n | signed char * | h | dioux | short int、unsigned short int | | n | short int * | l | dioux | long int、unsigned long int | | n | long int * | | c | wint_t | | s | wchar_t | | aefg | 无作用 | ll | dioux | long long int、unsigned long long int | | n | long long int * | j | dioux | intmax_t、uintmax_t | | n | intmax_t * | z | dioux | size_t | | n | size_t* | t | dioux | ptrdiff_t | | n | ptrdiff_t | L | aefg | long double |
转换说明符 | 说明 |
---|
d、i | 把int 类型值转换为十进制形式 | o、u、x | 把无符号整数转换为八进制、十进制或十六进制形式 | f | 把double 类型值转换为十进制形式,默认保留小数点后六位 | e | 把double 类型值转换为科学计数法形式,默认保留小数点后六位 | g | 把double 类型值转换为f形式或e 形式,当数值的指数部分小于-4 或大于等于精度值时会选择e 形式显式,默认尾部的0 不显示,且小数点仅在后边跟有数字时才显示出来。 | c | 显式无符号字符的int 类型值 | s | 显式由实参指向的字符 | p | 把void * 显示为可打印形式 | n | 相应的实参必须是指向int 型的指针,在该实参中存储printf 函数已经输出的字符数量,本身不显示输出 | % | 写字符% |
scanf 函数的格式串表示的是scanf 函数在读取输入时试图匹配的模式,如果一旦发现输入与格式串不匹配,那么函数就会立即返回,不匹配的数据将会被放回等待下次读取。scanf 函数的格式串由以下三部分组成: `
- 转换说明:和
printf 函数的转换说明类似,大多数转换说明都会跳过输入项开始处的空白字符(%[ 、%c 和%n 除外)。 - 空白字符:
scanf 函数的格式串中的一个或多个连续的空白字符和输入流中的零个或多个空白字符匹配。 - 非空白字符:除了
% 之外的所有非空白字符都必须和输入流中的相同字符匹配。
scanf 函数的转换说明符组成部分如下:
% 字符* 最大字段宽度 长度修饰符
- 字符*:赋值屏蔽,读入此数据项但是不把它赋值给对象,也不包含在函数返回的计数中。
- 最大字段宽度:限制输入项中的字符数量,如果达到这个最大值,那么此数据项的转换将结束,转换开始处跳过的空白字符不进行统计。
- 长度修饰符:表明用于存储输入数据项的变量的类型与特定转换说明中的常见类型长度不一致。
长度修饰符 | 转换说明符 | 含义 |
---|
hh | diouxn | signed char *、unsigned char * | h | diouxn | short int *、unsigned short int * | l | diouxn | long int *、unsigned long int * | | aefg | double * | | cs[ ] | wchar_t * | ll | diouxn | long long int *、unsigned long long int * | j | diouxn | intmax_t *、uintmax_t * | z | diouxn | size_t * | t | diouxn | ptrdiff_t * | L | aefg | long double * |
转换说明符 | 说明 |
---|
d | 匹配十进制整数 | i | 匹配整数 | o、u、x | 匹配无符号八进制、十进制或十六进制整数 | a、e、f、g | 匹配单精度浮点数 | c | 匹配单个字符 | s | 匹配非空字符串 | [ ] | 匹配来自集合的非空字符序列,然后在末尾添加空字符,实参是指向字符数组的指针,可以使用^ 进行前置否定 | p | 以printf 函数的输出格式匹配指针值 | n | 相应的实参必须是指向int 型的指针,把目前为止读到的字符数量存储到该实参 | % | 匹配字符% |
字符的输入输出
int putchar(int character);
int getchar();
int putc(int character,FILE * stream);
int getc(FILE * stream);
int fputc(int character,FILE * stream);
int fgetc(FILE * stream);
int ungetc(int c,FILE * stream);
行的输入输出
int puts(char const * s);
char *gets(char * s);
int fputs(const char restrict * s,FILE * restrict stream);
char* fgets(char * restrict s,int maxCount,FILE * stream);
块的输入输出
size_t fread(void * restrict ptr,size_t size,size_t nmemb,FILE * restrict stream);
size_t fwrite(const void * restrict ptr,size_t size,size_t nmemb, FILE * restrict stream);
字符串的输入输出
int sprintf(char * restrict buffer,const char * restrict format,...);
int snprintf(char * restrict buffer,size_t n,const char * restrict format,...);
int sscanf(const char * restrict buffer,const char * restrict format,...);
错误处理
<assert.h>
每次执行assert 时,都会检查它的参数值是否为假。如果参数为假那么assert 就会向stderr 写一条消息并调用abort 函数终止程序。
#define assert(expression) xxx
assert 一般用于调试阶段,因此会在生产时期禁止它,禁止的方式很容易,只需要在包含<assert.h> 头文件之前定义宏NDEBUG 即可。
<signal.h>
<signaal.h> 头文件提供了信号处理的工具,信号有两种类型:运行时错误和发生在程序之外的事件,大多数信号是异步的。<signaal.h> 头文件定义了一系列的宏来表示不同的信号:
#define SIGINT 2
#define SIGILL 4
#define SIGFPE 8
#define SIGSEGV 11
#define SIGTERM 15
#define SIGBREAK 21
#define SIGABRT 22
#define SIGABRT_COMPAT 6
signal 函数第一个参数是信号编码,第二个参数是一个指向信号发生时处理这一信号的函数的指针。当一个信号产生并调用特定的处理函数时,信号的编码会作为参数传给处理函数。在信函处理函数内只能调用signal 函数和reise 函数,并且不能使用具有静态存储权限的变量。如果信号是由abort 函数或raise 函数引发的,那么信号处理函数可以调用库函数或使用具有静态存储权限的变量。但是不能调用raise 函数。一旦处理函数返回,程序会在信号发生点恢复并继续执行,但如果信号是SIGABRT ,程序会直接终止;如果信号是SIGFPE 、SIGILL 和SIGSEGV ,那么处理函数返回的结果是未定义的。signal 函数的返回值是一个指向前一个处理函数的指针。
void (*signal(int arg,void (*func)(int)))(int);
也可以使用一些预定义的处理函数:
typedef void (* _crt_signal_t)(int);
#define SIG_DFL ((_crt_signal_t)0)
#define SIG_IGN ((_crt_signal_t)1)
#define SIG_GET ((_crt_signal_t)2)
#define SIG_SGE ((_crt_signal_t)3)
#define SIG_ACK ((_crt_signal_t)4)
raise 函数可以模拟信号的产生:
int raise(int signal);
<seijmp.h>
通常情况下函数会返回到它被调用的位置,但是<setjmp.h> 头文件可以使一个函数直接跳转到另一个函数而不需要返回。
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);
setjmp 用来标记程序中的一个位置,它接收一个jmp_buf 类型的参数,setjmp 会将当前环境存储到该变量中。然后返回零。longjmp 可以跳转到setjmp 标记的位置。该函数的参数是使用setjmp函数时的同一个jmp_buf 变量,该函数首先根据jmp_buf 变量的内容恢复当前的环境,然后从setjmp 宏中返回,此时setjmp 宏返回的是val 。如果val 的值是0 那么setjmp 将返回1 。
其它标准库
截至C99版本,C语言中的标准库有以下24个,大多数编译器都会使用更大的库,但他们不属于便准库的范畴。
库名 | 说明 |
---|
<assert.h> | 诊断 | <ctype.h> | 字符处理 | <errno.h> | 错误 | <float.h> | 浮点类型的特性 | <limite.h> | 整数类型的大小 | <locale.h> | 本地化 | <math.h> | 数学计算 | <setjmp.h> | 非本地跳转 | <signal.h> | 非本地跳转 | <stdarg.h> | 可变参数 | <stddef.h> | 常用定义 | <stdio.h> | 输入输出 | <stdlib.h> | 常用实用程序 | <string.h> | 字符串处理 | <time.h> | 时间和日期 | <complex.h> | 复数算数 | <fenv.h> | 浮点环境 | <inttypes.h> | 整数类型格式转换 | <iso646.h> | 拼写转换 | <stdbool.h> | 布尔类型和值 | <stdint.h> | 整数类型 | <tgmath.h> | 泛型数学 | <wchar.h> | 扩展的宽字节和多字节实用工具 | <wctype.h> | 宽字符分类和映射实用工具 |
<stddef.h>
<stddef.h> 头文件提供了常用的类型和宏的定义:
#define NULL ((void *)0)
#define offsetof(s,m) ((size_t)&(((s*)0)->m))
typedef xxx ptrdiff_t
typedef unsigned xxx size_t
typedef unsigned short wchar_t
<stdbool.h>
<stdbool.h>头文件提供了布尔相关的宏:
#define bool _Bool
#define false 0
#define true 1
<ctype.h>
<ctype.h> 头文件提供了字符分类函数和字符大小写映射函数,这些函数都接收一个int 类型的参数,将一个char 类型的实数传入时会进行类型的隐式转换,由于char 类型的有无符号性有具体的实现决定,因此这个隐式转换的结果也是不确定的,所以在使用前应该先将实参转换为unsigned char 类型。
int isalnum(int c);
int isalpha(int c);
int isdigit(int c);
int isxdigit(int c);
int islower(int c);
int isupper(int c);
int isspace(int c);
int tolower(int c);
int toupper(int c);
<stdlib.h>
<stdlib.h> 头文件提供了一些通用的实用工具。
数值转换
数值转换函数用于将含有数值的字符串转换为整数形式。每个函数都会跳过字符串开始处的空白字符,在遇到第一个不属于数的字符处停止。如果不能转换那么函数返回零。
double atof(const char *nptr);
int atoi(const char *nptr);
long int atol(const char *nptr);
long long int atoll(const char *nptr);
double strtod(const char * restrict nptr,char ** restrict endptr);
float strtof(const char * restrict nptr,char ** restrict endptr);
long double strtold(const char * restrict nptr,char ** restrict endptr);
long int strtol(const char * restrict nptr,char ** restrict endptr,int base);
long long int strtoll(const char * restrict nptr,char ** restrict endptr,int base);
unsigned long int strtoul(const char * restrict nptr,char ** restrict endptr,int base);
unsigned long long int strtoull(const char * restrict nptr,char ** restrict endptr,int base);
伪随机数生成
这两个函数都可以返回一个0 到RAND_MAX 的伪随机数。
int rand();
void srand(unsigned int seed);
与环境通信
int atexit(void (*func)());
void abort();
void exit(int status);
void _Exit(int status);
char *getenv(const char *name);
int system(const char *string);
搜索和排序
bsearch 函数用于数组内搜索元素,qsort 函数用于排序数组。
void *bsearch(const void *key,const void * base,size_t nmemb,size_t size,int (*compar)(const void *,const void *));
void qsort(void *base,size_t nmemb, size_t size,int(*compar)(const void *,const void *));
|