C 的优缺点

  • 底层语言
  • 小型语言
  • 包容性语言

优点

  • 高效
  • 可移植
  • 功能强大
  • 灵活
  • 标准库
  • 与 UNIX 系统的集成

缺点

  • 容易隐藏错误
  • 可能会难以理解
  • 可能会难以修改

高效使用 C

  • 规避 C 的缺陷
  • 使用软件工具使其更可靠。 如 lint , splint
  • 利用现有的代码库
  • 切合实际的编码规范
  • 避免投机取巧和极度复杂的代码
  • 紧贴标准

基本概念

编译和链接

  • 预处理(preprocessor) : 执行以 # 开头的命令(通常指为指令
  • 编译(compiler) : 翻译成机器指令(即目标代码)
  • 链接(link) : 将目标代码和其他代码整合在一起产生可执行的程序

注释

/* ... */ : 这种不可以嵌套

C99 提供了另一个 : // , 这种可以嵌套在 /* ... */

依据 C 标准, 编译器必须用一个空格字符来替换每条注释语句

声明

使用变量之前, 必须对其进行声明。指定类型和变量名

C99 : 声明可以不在语句之前

指令

即以 # 开头的代码, 它由预处理器执行。

每条指定都要求独立成行

函数

一系列组合在一起并且赋予了名字的语句

语句

程序运行时执行的命令

变量

即数据的存储单元

赋值

赋值的右侧可以是一个含有常量, 变量和运算符的公式

初始化

没有默认值并且尚未在程序中被赋值的变量是未初始化的

表达式

在任何需要数值的地方, 都可以用具有相同类型的表达式

表达式至少会产生一个 返回值。 而语句则不会。

记号 token

即许多在不改变意思的基础上无法再分割的字符组。

  • 标识符
  • 关键字
  • 运算符
  • 标点符号(逗号,分号,括号等)
  • 字符串字面量

定义常量/宏定义

#define PI 3.14

#define PIF (1.0f/3.14f)

宏包含运算符时, 必须用括号

标识符

变量, 函数, 宏和其他实体进行命名, 这些名字就是标识符 identifier

可包含字母,数字和下划线。 但必须以字母或者下划线开头。

C 的标识符是区分大小写的

C 对标识符的最大长度没有限制。 但标准不同

  • C89, 声称可任意长, 但只要求编译器记住前 31 个字符
  • C99 , 只要求编译器记住前 63 个字符

程序退出

在 main 函数中 return 0exit(0) 是样的 . 如果没有 return :

  • C89 中,退出时返回给 OS 的值是未定义的

  • C99 中, 如果main 中声明为 int, 则会向OS 返回 0; 否则是未定义

关键字 keyword

auto enum restrict (c99) unsigned
break enum return void
case float short volatile
char for signed while
const goto sizeof _Bool (c99)
continue if static _Complex (c99)
default inline (c99) struct _Imaginary (c99)
do int switch
double long typedef
else register union

C99 新增了 5 个 。 注意它们是区分大小写的

常用的编译选项

gcc –O –Wall –W –pedantic –ansi –std=c99 –o pun pun.c

在 Mac 上使用

alias gcc-all='gcc-9 -Wall -O -Wextra -pedantic -std=c99 '

gcc-9 替换为正确的 GNU GCC 版本

格式化输入输出

% 后边的信息指定了把数值从内部形式(二进制)转换成打印形式(字符)

printf 函数

它必须服从格式串。例如 %f %d , 表示从后面的参数中, 获取第一个float 类型和第一个整型。 即使它们的位置没对应!

例如

int i = 10;
float f = 43.22f;

printf("%f %d\n", i, f);

//打印出的是 
//43.220001 10

格式串

%m.pX%mX%-m.pX%-mX

  • m : 指定了要显示的最少字符数量。如果要显示的数值所需的字符少于 m, 则是右对齐. 而 -m 表示左对齐。
  • p : precision, 精度。
  • X : 常见的有
    • d : 十进制整数
    • e : 指数(科学记数法)。 如果 p 为 0, 则不显示小数点. 默认为 6.
    • f : 浮点数, 没有指数。 p 与 e 同
    • g : 指数或定点进制形式的浮点数, 自动根据数的大小来决定. p 表示小数点后的数字个数

转义序列

  • \t
  • \n
  • \b
  • \a
  • \"

scanf 函数

int a;
scanf("%d\n", &a)

scanf 从输入的数据中定位适当类型的项, 并在必要时跳过空白字符(空格, 水平和垂直制表符, 换页符, 换行符)

遇到不可能属于此项的字符时, 停止.

如果读入数据项成功, 则继续处理 ; 如果某一项不能成功读入, 则不再处理剩余部分, 立即返回.

当 scanf 函数遇到一个不可能属于当前项的字符时, 它会把此字符放回原处

格式串

格式串中的普通字符. 如果为

  • 空白字符 : 格式串中的一个或多个空白字符, scanf 会从输入中重复读空白字符, 直到遇到一个非空白字符为止. 格式串中的空白字符, 可以匹配 0 个或多个空白字符
  • 其他字符 : scanf 会将它与下一个输入字符进行比较. 如果匹配, 则放弃输入字符而继续处理. 如果不匹配, 则会把不匹配的字符放回输入, 然后异常退出.

杂项

%i

在 printf 函数中, 与 %d 没区别

在 scanf 函数中, %d 只能与十进制整数匹配, 而 %i 可以匹配八进制(0开头), 十六进制(0x 开头), 十进制表示的整数

显示 %

printf("%%")

表达式

C 语言的一特点就是它更多地强调表达式而不是语句

算术运算符

  • / : 当两个操作数都是整数时, 运算符 / 会丢掉分数部分来截取结果
  • % : 要求两个操作数都是整数. 否则编译不通过
  • /% 中, 0 作为右操作数会导致未定义的行为
  • /% 中用于负操作数时, 结果难以确定.
    • C89 中, 结果既可向上取整, 也可向下取整.
    • C99 中, 除法结果总是向零截取.

结合性

  • 左结合 : 二元运算符都是左结合
  • 右结合 : 一元运算符

赋值运算符

它不同于其他编程语言, 在 C 中, 赋值就像 + 那样是运算符. 即它会产生结果

所以, 它可以连写: i = j = k = 0

  • 简单赋值: =
  • 复合赋值: +=, -= 等等

左值 lvalue

表示存储在计算机内存中的对象, 而不是常量或计算的结果.

赋值运算符要求左操作数是左值.

右值

即是表达式.

可以为变量, 常量或更复杂的表达式

自增自减

前缀

++i : 它的结果是 i+1 , 副作用的效果是自增 i . 即: 立即自增 i

它和赋值运算符一样, 也有副作用.

后缀

i++ : 它的结果是 i , 副作用是自增 i . 即: 先使用 i 的原始值, 稍后再自增 i .

表达式求值

注意, C 语言并没有定义子表达式的求值顺序. 除了

  • 含有 逻辑与 以及 逻辑或 运算符
  • 条件运算符
  • 逗号运算符

同时访问及修改变量的值是不可取的. 例如

a = 5

c = (b = a + 2) - (a = 1)

表达式语句

任何表达式都可以用作语句. 即, 不论表达式是什么类型, 计算什么结果, 都可以在后面添加分号将其转换成语句.例如表达式 ++i 转换成语句则为 ++i;

注意

如果 v 有副作用, 则 v += e 不等价于 v = v + e

因为 v += e 只会求一次 v 的值. 而 v = v + e 会求两次 v 的值.

自增或自减, 对于现代编译器而言不会使程序更小或更快, 继续使用这些运算符主要是由于它们的简洁和便利.

自增和自减也可以处理 float 变量

选择语句

逻辑表达式

在 C 语言中, 比较运算会产生整数: 0(假) 或 1(真)

并且 C 中, 非零值都解释为 真

布尔逻辑会对操作数进行 短路 计算 : 即先计算 左操作数的值, 然后计算右操作数. 如果表达式的值可以仅由左操作数推导, 则不计算右操作数.

if (表达式) {
}

if (表达式) {
} else if (表达式) {
} else if (表达式) {
} else {
}

if (表达式) {
} else {
}

布尔值

C89 中没定义布尔类型. 所以 C89 风格通常使用

#define TRUE 1
#define FALSE 0

来标识

C99 中提供了 _Bool 型, 它是无符号整型. 但和一般的整型不同, 它只能赋值 0 或 1. (因为 C89 标准指出, 以下划线后跟一个大写字母的名字是保留字, 程序员不应该使用)

C99 也提供了一个新的 header : <stdbool.h> , 这时就可以这样子用

bool flag;

flag = false;
flag = true;

switch

它往往比 if 语句执行速度快且更易阅读.

C 不允许有重复的分支标号. 但对顺序没要求, 特别是 default 分支不一定要放在最后.

循环

  • while
  • do
  • for

在 C99 中, for 语句的第一个表达式可以替换为一个声明. 即

for (int i = 0; i < n; i++ {
}

逗号表达式

ex1, ex2 : 这里要通过两步来实现.

  1. 计算 ex1 并且扔掉计算出的值
  2. 计算 ex2 , 把这个值作为整个表达式的值

左操作数在右操作数之前求值

退出循环

  • break 语句
  • continue 语句
  • goto 语句

break, continue 和 return 语句本质上都是受限制的 goto 语句.

空语句

;

基本类型

整数

有符号和无符号 . 各种组合

// 可缩写为 short 或 unsigned short
short int 
unsigned short int

int 
unsigned int

// 可缩写为 long 或 unsigned long
long int 
unsigned long int

取值范围, 可以可看 <limits.h> 定义的宏.

C99 添加了额外两个标准类型: long long intunsigned long long int

long long 类型要求至少是 64 位宽.

整数溢出

对于有符号, 行为是未定义的.

对于无符号, 结果定义为: 对 2^n 取模. n 是用于存储结果的位数.

读写整数的格式说明

  • 无符号
    • u : 十进制
    • o : 八进制
    • x : 十六进制
  • 短整数 : 在 d , o, ux 前面加上字母 h
  • 长整数 : 在 d, o, u, x 前加上字母 l
  • C99 中的 long long int 时 : 在 d, o, u, x 前加上字母 ll

浮点

  • float
  • double
  • long double

C99 添加了

  • float _Complex
  • double _Complex
  • long double _Complex

读写

单精度 float

  • %e
  • %f
  • %g

双精度 double

  • %le
  • %lf
  • %lg

long long double

  • %Le
  • %Lf
  • %Lg

printf 中 e, f, g 可以输出 float 或 double

long long double, 则为 %Le, %Lf, %Lg

字符

用单引号括起来, 而不是双引号

在 C 中, 把字符当作小整数进行处理.

字符常量, 事实上是 int 类型而不是 char 类型.

C 标准并没有说明 cahr 是有符号还是无符号的. 如果要确实区分, 要明确写成

signed charunsigned char

C99 的整数类型含义包含了字符类型, 枚举类型以为 _Bool 类型(无符号整数)

转义

  • 字符转义序列
  • 数字转义序列
    • 八进制: \033\33 , 注意, 不一定要用 0 开头
    • 十六进制: \xFF . x 必须小写. 十六进制的数字则不限大小写

读写

  • scanf 和 printf 函数. scanf 函数不会跳过空白字符, 并且它会遗留下它扫视过, 但未读取的字符(包括换行符)
  • getchar 和 putchar : 比 scanf 和 printf 更快. 并且通常是作为宏实现.
    • getchar 返回的是一个 int 类型而不是 char 类型. 它也不会跳过空白

类型转换

隐式转换

  • 算术表达式或逻辑表达式的操作数类型不相同时
  • 赋值运算符右侧表达式的类型和左侧变量的类型不匹配时 : 将右边的转换成左边的类型
  • 函数调用中的实参类型与形参类型不匹配时
  • 当 return 语句中表达式的类型和函数返回值的类型不匹配时

    float -> double -> long double
    
    
    int -> unsigned int -> long int -> unsigned long int
    

C99 标准下, 转换等级从高到低为

  • long long int, unsigned long long int
  • long int, unsigned long int
  • int, unsigned int
  • short int, unsigned short int
  • char, unsigned char, signed char
  • _Bool

强制转换

(类型名)表达式

C 语言把 (类型名) 视为一元运算符. 它的优先级高于二元运算符. 所以 (float)dividend / divisor 解释为

((float) dividend) / divisor

有时候需要使用强制转换来避免溢出. 例如

long i;
int j = 1000;
i = j * j ;

在某些机器上 , j * j 的结果太大, 无法表示成 int 从而导致溢出. 这时可用强制转换来避免

i = (long) j * j
// 注意不是 (long) (j * j )

但要注意的是 i = (long)(j * j) 是不对的, 因为溢出在强制类型转换之前就已经发生了.

类型定义

#define BOOL int

//或

typedef int Bool
  • 易于理解
  • 易于修改

C 语言库自身使用 typedef 为那些可能依据 C 实现不同而不同的类型创建类型名, 这些类型名经常以 _t 结尾.

类型定义比宏定义功能更强大. 特别是, 数组和指数类型不能定义为宏的.

其次, typedef 命名的对象具有和变量相同的作用域规则;

定义在函数体内的 typedef 名字在函数外是无法识别的

而宏的名字, 在预处理时会在任何出现的地方被替换

sizeof

sizeof (类型名) , 它返回值是一个无符号整数, 代表存储类型名的值所需要的字节数. 它是一种特殊的运算符, 因为编译器本身通常就能确定 sizeof 表达式的值(C89 中编译器总是可以确定. 但 C99 有一个例外, 编译器不能确定变长数组的大小, 因为它在运行期间是可变的). sizeof(char) 的值始终为 1.

它可用于

  • 常量
  • 变量
  • 表达式

注意, 它是一元运算符

C99 中在printf 中可以 %zu 转出其 sizeof 的结果

数组

声明数组, 需要指明元素的类型和数量

int a[10];

数组的下标(索引), 始终是从 0 开始. 所以长度为 n 的数组, 索引是范围是 0 ~ n-1

下标(索引)可以是任何整数的表达式

初始化

int a[10] = {1,2,3}

int a[10] = {0}

如果比数组短, 则其余赋值为 0.

指定初始化

// C99 特性. 没有特定的则为 0
int a[15] = {[2]=29, [9]=7}

// 如果不指定长度, 则根据指定的最大的下标来推导. 下面的数组大小为 31.
int a[] = {[2]=19, [30]=3}

sizeof

它返回的是数组的大小. 即

int a[10];
sizeof(a) //一般为 40

多维数组

int m[5][9];

//初始化
int a[2][3] = {{1,2,3}, {4,5,6}}

//或, 以下表示元素全初始化为 0
int a[2][3] = {0}

形式为 m[i][j]

  • i : 数组 m 的第 i 行(从 0 开始)
  • j : 数组 m 的第 i 行的第 j 列(从 0 开始)

C 语言是按行主序存储数组的. 即先存储完第 0 行, 然后第 1 行,等等.

多维数组也可以指定初始化(C99 标准), 然后其余元素为 0.

常量数组

使用 const 来修饰.

变长数组(C99)

int n;
scanf("%d", &n);

int a[n];

它们没有静态存储期限

以及没有初始化式

注意, C99 不允许 goto 语句绕过变长数组的声明.

函数

定义

返回类型 函数名(形式参数) {
  声明
  语句
}

声明

返回类型 函数名 (形式参数);

编译器检查时, 如果没有”看见”函数的声明, 则会假设它的返回值类型为 int , 参数类型也是 int . 如果编译器在后面遇到函数的定义时, 如果不一致, 则会报错.(即隐式声明函数)

比如这样子

#include <stdio.h>

int main(void) {
    int sum = add(1, 2);
    printf("%d\n", sum);
    return 0;
}

int add(int a, int b) {
    return a + b;
}

编译器可以通过并正确运行.

但如果函数为

#include <stdio.h>

int main(void) {
    double sum = add(1, 2);
    printf("%f\n", sum);
    return 0;
}

double add(double a, double b) {
    return a + b;
}

则会报错如下

hello.c:6:18: warning: implicit declaration of function 'add' is invalid in C99 [-Wimplicit-function-declaration]
    double sum = add(1, 2);
                 ^
hello.c:11:8: error: conflicting types for 'add'
double add(double a, double b) {
       ^
hello.c:6:18: note: previous implicit declaration is here
    double sum = add(1, 2);
                 ^
1 warning and 1 error generated.

但如果先声明, 则可以正确运行:

#include <stdio.h>

double add(double a, double b);

int main(void) {
    double sum = add(1, 2);
    printf("%f\n", sum);
    return 0;
}

double add(double a, double b) {
    return a + b;
}

不过, C99 标准的话, 则必须先声明或定义, 再调用, 否则报错.

实际参数

C 语言中, 实际参数是 值传递 的.

参数转换

  • 编译器在调用前遇到原型。就像使用赋值一样,每个实际参数的值被隐式地转换成相应 形式参数的类型
  • 编译器在调用前没有遇到原型。编译器执行默认的实际参数提升:(1)把float类型的 实际参数转换成double类型,(2)执行整值提升,即把char类型和short类型的实际参数转换成int类型

实际参数不能是任意表达式, 而必须是赋值表达式. 在赋值表达式中, 不能用逗号作为运算符, 除非逗号是在圆括号中.

数组型参数

int sum_array(int a[], int n) {
}

int m[10];
int sum = sum_array(m, 10);

注意, 传递数组时, 在函数内的修改, 会体现在函数外部

变长数组参数

int sum_array(int n, int a[n]) {
}

注意, 参数的顺序很重要.

数组参数声明使用 static

int sum_array(int a[static 3], int n) {
}

这里的 static 只是一个提示, C 编译器可以据此生成更快的指令来访问数组.

如果数组是多维的, static 仅可用于第一维(例如, 二维数组的话, 则是行数)

复合字面量

通过指定其包含的元素而创建没有名字的数组. 例如

total = sum_array((int []){3,2,3,4}, 5);

它的长度是由元素个数决定.

函数内部创建的复合字面量可以包含任意的表达式, 不限于常量. 例如

total = sum_array((int []){2 * i, i + j, j * k}, 3);

默认情况下, 复全字面量为左值, 的以元素可变.

如果要求为只读, 则可以加上 const . 例如 (const int []){5, 4}

程序终止

以前C 程序常常省略main 的返回类型, 这是利用了返回类型默认为 int 的传统.

但在 C99 中, 省略函数的返回类型是不合法的.

  • return
  • exit 函数. 它是 <stdlib.h> header 文件中

main 函数中的语句 return 表达式; 等同于 exit(表达式);

它们的区别是, exit 函数, 不管哪个函数调用, 都会导致程序终止. 而 return 则只有当 main 函数调用时才会导致程序终止.

递归

为了防止 无限递归,所有递归函数都需要某些类型的终止条件

int fact(int n) {
	if (n <= 1) return 1;
	else
	return n * fact(n-1);
}

它的递归过程

fact(3)发现3不是小于或等于1的,所以fact(3)调用 
	fact(2),此函数发现2不是小于或等于1的,所以fact(2)调用
		fact(1),此函数发现1是小于或等于1的,所以fact(1)返回1,从而导致 
	fact(2)返回2×1=2,从而导致
fact(3)返回3×2=6

快速排序算法

  1. 选择数组元素e(作为“分割元素”),然后重新排列数组使得元素从1一直到i1都是小 于或等于e的,元素i包含e,而元素从i+1一直到n都是大于或等于e的。
  2. 通过递归地采用快速排序方法,对从1到i1的元素进行排序。
  3. 通过递归地采用快速排序方法,对从i+1到n的元素进行排序。

程序结构

局部变量

  • 自动存储期限
  • 块作用域

C99 不要求函数在一开始就进行变量声明.

静态局部变量

它具有静态存储期限. 拥有永久的存储单元, 在整个程序执行期间都会保留变量的值.

但它始终是块作用域, 所以对其他函数是不可见的.

外部变量(全局变量)

  • 静态存储期限
  • 文件作用域

作用域

当程序块内的声明命名一个标识符时,如果此标识符已经是 可见的(因为此标识符拥有文件作用域,或者因为它已在某个程序块内声明),新的声明临时

“隐藏”了旧的声明,标识符获得了新的含义。在程序块的末尾,标识符重新获得旧的含义

构建 C 程序

#include指令;
#define指令;
类型定义;
外部变量的声明;
除main函数之外的函数的原型; 
main函数的定义;
其他函数的定义

指针

指针就是地址, 而指针变量就是存储地址的变量

C语言要求每个指针变量只能指向一种特定类型(引用类型)的对象

指针运算符

  • &(取 地址)运算符。如果x是变量,那么&x就是x在内存中的地址。
  • 为了获得对指针所指向对象的访 问,可以使用*(间接寻址)运算符。如果p是指针,那么*p表示p当前指向的对象

指针赋值

int i, j, *p, *q;
p = &i;
q = p;

指针作为参数

向函数传递需要的指针却失败了可能会产生严重的后果

void decompose(double x, long *int_part, double *frac_part) {
}

const 保护

void f(const int *p) {
}

表明函数不会改变指针参数所指向的对象

void f(int * const p) {
}

保护p本身

指针作为返回值

int *max(int *a, int *b) {
}

永远不要返回指向自动局部变量的指针

打印指针

#include <stdio.h>

int main(void) {
    int a = 100;
    int *p = &a;
    printf("%p\n", p);
    return 0;
}

指针和数组

用指针处理数组的主要原因是效率,但是这里的效率提升已经不再像当 初那么重要了,这主要归功于编译器的改进

指针的算术运算

  • 指针加上整数 : 指针p加上整数j产生指向特定元素的指针,这个特定元素是p原先指向的元素后的j个位 置
  • 指针减去整数 : 如果p指向数组元素a[i],那么p - j指向a[i - j]
  • 两个指针相减 : 当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量)。因此,如果p指 259 向a[i]且q指向a[j],那么p-q就等于i-j

指针比较

可以用关系运算符(<、<=、>和>=)和判等运算符(==和!=)进行指针比较。只有在两个 指针指向同一数组时,用关系运算符进行的指针比较才有意义。比较的结果依赖于数组中两个 元素的相对位置

指向复合常量的指针(C99)

int *p = (int []){3, 0, 3, 4, 1};

使用复合字面量可 以减少一些麻烦,我们不再需要先声明一个数组变量,然后用指针p指向数组的第一个元素

指针用于数组处理

指针的算术运算允许通过对指针变量进行重复自增来访问数组的元素

表达式 含义
*p++或*(p++) 自增前表达式的值是*p,以后再自增p
(*p)++ 自增前表达式的值是*p,以后再自增*p
*++p或*(++p) 先自增p,自增后表达式的值是*p
++*p或++(*p) 先自增*p,自增后表达式的值是*p

用数组名作为指针

可以用数组的名字作为指向数组第一个元素的指针

用指针作为数组名

可以把指针看作数组名进行取下标操作

处理多维数组的元素

如果使指针p指向二维数组中的第一个元素(即0行0 列的元素),就可以通过重复自增p的方法访问数组中的每一个元素

处理多维数组的行

p = &a[i][0];
//简写为
p = a[i];

处理多维数组的列

int a[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS], i;

for (p = &a[0]; p < &a[NUM_ROWS]; p++)
	(*p)[i] = 0;

用多维数组名作为指针

int a[NUM_ROWS][NUM_COLS];

这时, a不是指向a[0][0]的指针,而是指向a[0]的指针.

用作指针时, a的类型是int (*)[NUM_COLS](指向长度为NUM_COLS的整型数组的指针)

指针和变长数组(C99)

如果变长数组是多维的,指针的类型取决于除第一维外每一维的长度.

void f(int n) {
	int a[n], *p; 
  p = a;
	...
}

字符串

空字符的整数值就是0

字符串字面量

用一对双引号括起来的字符序列

延续字符串字面量

把第一行用字符\结尾,那么 C语言就允许在下一行延续字符串字面量.

当两条或更多条字符串 字面量相邻时(仅用空白字符分割),编译器会把它们合并成一条字符串

#include <stdio.h>

int main(void) {
    printf("When you come to a fork in the road, take it. " 
            "--Yogi Berra");
    return 0;
}

如何存储字符串字面量

从本质而言,C语言把字符串字面量作为字符数组来处理

当C语言编译器在程序中遇到长 度为n的字符串字面量时,它会为字符串字面量分配长度为n+1的内存空间。这块内存空间将用 来存储字符串字面量中的字符,以及一个用来标志字符串末尾的额外字符(空字符)。空字符是 一个所有位都为0的字节,因此用转义序列\0来表示

既然字符串字面量是作为数组来存储的,那么编译器会把它看作是char *类型的指针

字符串字面量的操作

对字符串字面量取下标

char ch;
ch = "abc"[1];

试图改变字符串字面量会导致未定义的行为

字符串字面量与字符常量

只包含一个字符的字符串字面量不同于字符常量。字符串字面量”a”是用指针来表示的, 这个指针指向存放字符”a”(后面紧跟空字符)的内存单元。字符常量’a’是用整数(字符集的 数值码)来表示的

字符串字面量可以有多长

按照C89标准,编译器必须最少支持509个字符长的字符串字面量。

C99把最小长度增加到了4095个字符

为什么不把字符串字面量称为“字符串常量”

因为它们并不一定是常量。由于字符串字面量是通过指针访问的,所以没有办法避免程序修改字符 串字面量中的字符,

字符串变量

当声明用于存放字符串的字符数组时,要始终保证数组的长度比字符串的长度 多一个字符。这是因为C语言规定每个字符串都要以空字符结尾。如果没有给空字符 预留位置,可能会导致程序运行时出现不可预知的结果,因为C函数库中的函数假设 字符串都是以空字符结束的

初始化字符串变量

char date1[8] = "June 14";

字符串变量的声明中可以省略它的长度。这种情况下,编译器会自动计算长度

字符数组与字符指针

  • 在声明为数组时,就像任意数组元素一样,可以修改存储在数组中的字符.

  • 在声明为指针时,date指向字符串字面量,而字符串字面量是不可以修改的.

  • 在声明为数组时,date是数组名。在声明为指针时,date是变量,这个变量可以在程序

执行期间指向其他字符串

字符串的读和写

printf 函数和 puts 函数写字符串

转换说明%s允许printf函数写字符串

printf函数会逐个写字符串中的字符,直到遇到空字符才停止。(如果空字符丢失,printf函 数会越过字符串的末尾继续写,直到最终在内存的某个地方找到空字符为止。)

如果只想显示字符串的一部分,可以使用转换说明%.ps,这里p是要显示的字符数量.

转换说明%ms会在大小为m的字段内显示字符串。 (对于超过m个字符的字符串,printf函数会显示出整个字符串,而不会截断。)如果字符串少 于m个字符,则会在字段内右对齐输出。如果要强制左对齐,可以在m前加一个减号。m值和p 值可以组合使用:转换说明%m.ps会使字符串的前p个字符在大小为m的字段内显示.

puts函数只有一个参数,即需要显示的字符串。在写完字符串后,puts函数总会添加一个额外

的换行符,从而前进到下一个输出行的开始处

scanf 函数和 gets 函数读字符串

scanf函数会跳过空白字符,然后读入字符并存储到str中,直到遇到空白字符为止。scanf函数始终会在字符串末尾存储一个空字符。用scanf函数读入字符串永远不会包含空白字符.

而 gets 函数则不太一样

  • gets函数不会在开始读字符串之前跳过空白字符(scanf函数会跳过)

  • gets函数会持续读入直到找到换行符才停止(scanf函数会在任意空白字符处停止)。此

外,gets函数会忽略掉换行符,不会把它存储到数组中,用空字符代替换行符

scanf函数和gets函数都无法检测数组何时被填满.

fgets函数相比则更安全

使用 C 语言的字符串库

#include <string.h>

strcpy函数

char *strcpy(char *s1, const char *s2);

也就是说,strcpy函数把s2中的字符复制到s1中直到遇到s2 中的第一个空字符为止(该空字符也需要复制). strcpy函数返回s1(即指向目标字符串的指 针)。这一过程不会改变s2指向的字符串,因此将其声明为const

#include <stdio.h>
#include <string.h>

int main(void) {
   char str1[10], str2[10];
   strcpy(str2, "abcd");
   printf("%s\n", str2);
    return 0;
}

尽管执行会慢一点,但是调用strncpy函数是一种更安全的复制字符串的方 法。strncpy类似于strcpy,但它还有第三个参数可以用于限制所复制的字符数.

更安全的用法:

strncpy(str1, str2, sizeof(str1) - 1); 
str1[sizeof(str1)-1] = '\0';

strlen函数

strlen函数返回字符串s的长度:s中第一个空字符之前的字符个数(不包括空字符)

strcat函数

char *strcat(char *s1, const char *s2);

strcat函数把字符串s2的内容追加到字符串s1的末尾,并且返回字符串s1(指向结果字符串的 指针)

strncat函数比strcat更安全,但速度也慢一些

strcmp函数

int strcmp(const char *s1, const char *s2)

strcmp函数比较字符串s1和字符串s2,然后根据s1是小于、等于或大于s2

strcmp函数利用字典顺序进行字符串比较

字符串数组

创建二维的字符数组,然后按照每行一个字符串的方式把字符串存储到数组中.

char planets[][8] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto"};

image-20200516234829449

但这容易导致空间浪费. 一种改进是使用参差不齐的数组(ragged array):

char *planets[] = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto"};

image-20200516234842766

命令行参数

int main(int argc, char *argv[]) {
}
  • argc(“参数计数”)是命令行参数的数量(包括程序名本身)
  • argv(“参数向量”)是指向命 令行参数的指针数组,这些命令行参数以字符串的形式存储

argv[0]指向程序名,而从argv[1]argv[argc-1]则指向余下的命令行参数

argv有一个附加元素,即argv[argc],这个元素始终是一个空指针

预处理器

工作原理

预处理器的行为是由预处理指令(由#字符开头的一些命令)控制的

  • #define 指令定义了一个宏——用来代表其他东西的一个名字,例如常量或常用的表达式.预处理器会通过将宏的名字和它的定义存储在一起来响应#define指令。 C程序 当这个宏在后面的程序中使用到时,预处理器“扩展”宏,将宏替换为其定义值
  • #include指令告诉预处理器打开一个特定的文件,将它的内容作为 正在编译的文件的一部分“包含”进来

预处理器的输入是一个C 语言程序,程序可能包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。预 处理器的输出是另一个C程序:原程序编辑后的版本,不再包含指令.

预处理器不仅仅是执行了指令,还做了一些其他的事情。特 别值得注意的是,它将每一处注释都替换为一个空格字符。有一些预处理器还会进一步删除不 必要的空白字符,包括每一行开始用于缩进的空格符和制表符

要想查看预处理后的文件, 在 GCC 中可以使用 -E 选项. 例如

源文件hello.c 内容为

#define HELLO 10

int main(void) {
    printf("%d\n", HELLO);
    return 0;
}

通过 gcc-9 -E hello.c (注意 Mac 上gcc 默认的是 clang, 要想使用 GNU GCC , 则要指定名, 在我机器上是 gcc-9

$ gcc-all -E hello.c
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "hello.c"


int main(void) {
    printf("%d\n", 10);
    return 0;
}

预处理指令

  • 宏定义
  • 文件包含
  • 条件编译

指令规则

  • 指令都以#开始
  • 在指令的符号之间可以插入任意数量的空格或水平制表符
  • 指令总是在第一个换行符处结束,除非明确地指明要延续. 如果想在下一行延续指令, 我们必须在当前行的末尾使用\字符
  • 指令可以出现在程序中的任何地方
  • 注释可以与指令放在同一行

宏定义

简单的宏

#define 标识符 替换列表

宏定义中的替换列表为空是合法的

带参数的宏

#define 标识符(x1, x2,..., xn)	替换列表

//例如
#defineMAX(x,y) ((x)>(y)?(x):(y))

宏的名字和左括号之间必须没有空格

使用带参数的宏替代真正的函数有两个优点

  1. 程序可能会稍微快些。程序执行时调用函数通常会有些额外开销——存储上下文信息、

复制参数的值等,而调用宏则没有这些运行开销。(注意,C99的内联函数为我们提供了一种不使用宏而避免这一开销的办法。)

  1. 宏更“通用”。与函数的参数不同,宏的参数没有类型.

缺点

  1. 编译
  2. 后的代码通常会变大
  3. 宏参数没有类型检查
  4. 无法用一个指针来指向一个宏
  5. 宏可能会不止一次地计算它的参数 . 函数对它的参数只会计算一次,而宏可能会计算两 次甚至更多次。如果参数有副作用,多次计算参数的值可能会产生不可预知的结果. 为了自我保护,最好避免使用带有副作用的参数

#运算符

宏定义可以包含两个专用的运算符:###。编译器不会识别这两种运算符,它们会在预处 理时被执行

  • #运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中
  • ##运算符可以将两个记号(如标识符)“粘合”在一起,成为一个记号

宏的通用属性

  • 宏的替换列表可以包含对其他宏的调用
  • 预处理器只会替换完整的记号,而不会替换记号的片断
  • 宏定义的作用范围通常到出现这个宏的文件末尾
  • 宏不可以被定义两遍,除非新的定义与旧的定义是一样的
  • 宏可以使用#undef指令“取消定义”

宏定义中的圆括号

  • 如果宏的替换列表中有运 算符,那么始终要将替换列表放在括号中
  • 如果宏有参数,每个参数每次在替换列表中出现时都要放在圆括号中

创建较长的宏

#define ECHO(s) (gets(s), puts(s))

预定义宏

含义
__LINE__ 被编译的文件中的行号
__FILE__ 被编译的文件名
__DATE__ 编译的日期(格式”mm dd yyyy”)
__TIME__ 编译的时间(格式”hh:mm:ss”)
__STDC__ 如果编译器符合C标准(C89或C99),那么值为1

C99 中新增的预定义宏

含义
__STDC_HOSTED__ 如果是托管式实现,值为1;如果是独立式实现,值为0
__STDC_VERSION__ 支持的C标准版本
__STDC_IEC_559__ 如果支持IEC 60559浮点算术运算,则值为1
__STDC_IEC_559_COMPLEX__ 如果支持IEC 60559复数算术运算,则值为1
__STDC_ISO_10646__ 如果wchar_t的值与指定年月的ISO 10646标准相匹配,则值为yyyymmL
  • 托管式实现(hosted implementation)能够接受任何符合C99标准的程序
  • 独 立式实现(freestanding implementation)除了几个最基本的以外,不一定要能够编译使用复数 类型或标准头的程序

空的宏参数(C99)

C99允许宏调用中的任意或所有参数为空。当然这样的调用需要有和一般调用一样多的逗 号(这样容易看出哪些参数被省略了)

参数个数可变的宏(C99)

#define TEST(condition, ...) ((condition)? \ 
	printf("Passed test: %s\n", #condition): \ 
	printf(__VA_ARGS__))

__VA_ARGS__是一个专用的标 识符,只能出现在具有可变参数个数的宏的替换列表中,代表所有与省略号相对应的参数

__func__标识符

每一个函数都可以访问__func__标识符,它的行为很像一个存储当前正在执行的函数的名 字的字符串变量。其作用相当于在函数体的一开始包含如下声明:

static const char __func__[] = "function-name";

条件编译

if 和 endif

#define DEBUG 1

#if DEBUG
printf("Value of i: %d\n", i); printf("Value of j: %d\n", j); 
#endif

defined运算符

当defined 应用于标识符时,如果标识符是一个定义过的宏则返回1,否则返回0

#if defined(DEBUG) 
...
#endif

#ifdef 指令和 #ifndef 指令

#ifdef 标识符 
当标识符被定义为宏时需要包含的代码 
#endif

#ifdef 标识符 等价于 #if defined(标识符)

#elif指令和#else指令

#if 表达式1 
当表达式1非0时需要包含的代码
#elif 表达式2 
当表达式1为0但表达式2非0时需要包含的代码 
#else
其他情况下需要包含的代码
#endif

其他指令

  • #error 消息 . 例如 #error No operating system specified
  • #line n : 改变程序行编号方式
  • #line n "文件"
  • #pragma 记号
  • _Pragma (字符串字面量) : C99. 遇到该表达式时,预处理器通过移除字符串两端的双引号并分别用字符”和\代替转义序列\"\\ 来实现对字符串字面量(C99标准中的术语)的“去字符串化”. 表达式的结果是一系列的记号, 这些记号被视为出现在pragma指令中.

编写大型程序

源文件

每个源文件包含程序的部分内容,主要是函数 和变量的定义。其中一个源文件必须包含一个名为main的函数,此函数作为程序的起始点

头文件

头文件的扩展名为.h

然后用 #include 指令包含进来

  • 共享宏定义和类型定义
  • 共享变量声明. extern int i : extern告诉编译器,变量i是在程序中的其他位置定义的(很可能是在不同的源文件中),因此 不需要为i分配空间
  • 共享函数原型

保护头文件

防多次处理

#ifndef BOOLEAN_H 
#define BOOLEAN_H
#define TRUE 1 
#define FALSE 0 
typedef int Bool;
#endif

include 格式

#include <文件名> : 搜寻系统头文件所在的目录(或多个目录)

#include "文件名" : 先搜寻当前目录,然后搜寻系统头文件所在的目录(或多个目录)。 通常可以改变搜寻头文件的位置,这种改变经常利用诸如-I路径这样的命令行选项来实现.

#include 记号 : 预处理器会扫描这些记号,并替换遇到的宏。宏替换完成以 后,#include指令的格式一定与前面两种之一相匹配

构建

gcc –o justify justify.c line.c word.c

makefile

justify: justify.o word.o line.o
	gcc -o justify justify.o word.o line.o
	
justify.o: justify.c word.h line.h 
	gcc -c justify.c
	
word.o: word.c word.h 
	gcc -c word.c
	
line.o: line.c line.h 
	gcc -c line.c

这里有4组代码行,每组称为一条规则。每条规则的第一行给出了目标文件,跟在后边的是它所 依赖的文件。第二行是待执行的命令

  • makefile中的每个命令前面都必须有一个制表符,不是一串空格

  • makefile通常存储在一个名为Makefile(或makefile)的文件中。使用make实用程序时,

它会自动在当前目录下搜索具有这些名字的文件

  • 用下面的命令调用make: make 目标

  • 如果在调用make时没有指定目标文件,将构建第一条规则中的目标文件

在程序外定义宏

大多数编译器(包括GCC)支持-D选项,此选项允许用命令行来指定宏的值: gcc –DDEBUG=1 foo.c

结构、联合和枚举

结构变量

struct {
	int number;
	char name[NAME_LEN+1]; 
  int on_hand;
} part1, part2;

结构的成员在内存中是按照声明的顺序存储的. 成员之间没有间隙

初始化

struct {
int number;
char name[NAME_LEN+1]; int on_hand;
} part1 = {528, "Disk drive", 10}, 
part2 = {914, "Printer cable", 5};

C99 中, 初始化式中的成员数可以少于它所初始化的结构,就像数组那样,任何“剩余的” 成员都用0作为它的初始值。特别地,剩余的字符数组中的字节数为0,表示空字符串

指定初始化(C99)

{.number = 528, .name = "Disk drive", .on_hand = 10}

对结构的操作

  • 访问结构内的成员,首先写出结构的名字,然后写一个句点,再写出成员的名字
  • 结构的成员是左值. 所以它们可以出现在赋值运算的左侧,也可以作为自增或自 减表达式的操作数
  • 结构赋值运算. 对结构进行复制时,嵌在结构内的数组也得到了复制.

    #include <stdio.h>
    
    int main(void) {
    struct {
        int a;
        int b;
        int c[3];
    } h1 = {10, 20, {1,2,3}}, h2;
    
    printf("%p\n", h1.c);
    
    h2 = h1;
    printf("%p\n", h2.c);
    
    return 0;
    }
    

句点实际上就是一个C语言的运算符.

它 的运算优先级与后缀++和后缀–运算符一样,所以句点运算符的优先级几乎高于所有其他运算符

结构标记的声明

struct part {
	int number;
	char name[NAME_LEN+1]; 
  int on_hand;
};

右花括号后的分号是必不可少的,它表示声明结束

结构标记的声明可以和结构变量的声明合并在一起

这样子要声明相应的变量的话, 可以用

struct part part1, part2;

结构类型的定义

typedef struct {
	int number;
	char name[NAME_LEN+1]; 
  int on_hand;
} Part;

这样子, 可以像内置类型那样使用Part.

Part part1, part2;

结构用于链表时,强制使用声明结构标记

结构作为参数和返回值

给函数传递结构和从函数返回结构都要求生成结构中所有成员的副本。这样的结果是,这 些操作对程序强加了一定数量的系统开销,特别是结构很大的时候。为了避免这类系统开销, 有时用传递指向结构的指针来代替传递结构本身是很明智的做法

复合字面量(c99)

(struct part) {528, "Disk drive", 10}

一个复合 字面量可以包括指示符,就像指定初始化式一样:

print_part((struct part) {
  .on_hand = 10,
	.name = "Disk drive",
	.number = 528});

嵌套的结构

struct person_name {
	char first[FIRST_NAME_LEN+1]; 
  char middle_initial;
	char last[LAST_NAME_LEN+1];
};

struct student {
	struct person_name name; 
  int id, age;
	char sex;
} student1, student2;

结构数组

struct part inventory[100];

由于结构数组(以及包含数组的结构)很常见,因此C99的指定初始化式允许每一项 具有多个指示符.

假定我们想初始化inventory数组使其只包含一个零件,零件编号为528,现

货数量为10,名字暂时为空:

struct part inventory[100] = {
  [0].number = 528, 
  [0].on_hand = 10, 
  [0].name[0] = '\0'
};

联合

像结构一样,联合(union)也是由一个或多个成员构成的,而且这些成员可能具有不同的 类型。但是,编译器只为联合中最大的成员分配足够的内存空间。联合的成员在这个空间内彼 此覆盖

联合的性质和结构的性质几乎一样,所以可以用声明结构标记和类型的方法来声明联合的 标记和类型。像结构一样,联合可以使用运算符=进行复制,也可以传递给函数,还可以由函数 返回

指定初始化式:

union { 
  int i;
	double d;
} u = {.d = 10.0};

把值存储在联合的一个成员中,然后通 过另一个名字来访问该数据通常不太可取,因为给联合的一个成员赋值会导致其他成员的值不 确定。然而,C标准提到了一种特殊情况:联合的两个或多个成员是结构,而这些结构最初的 一个或多个成员是相匹配的。(这些成员的顺序应该相同,类型也要兼容,但名字可以不一样。) 如果当前某个结构有效,则其他结构中的匹配成员也有效

用联合来构造混合的数据结构

typedef union { 
  int i;
	double d; 
} Number;

Number number_array[1000];

number_array[0].i = 5; 
number_array[1].d = 8.395;

为联合添加“标记字段”

#define INT_KIND 0
#define DOUBLE_KIND 1

typedef struct {
  int kind;
  union{
		int i;
		double d; 
  } u;
} Number;

枚举

enum {CLUBS, DIAMONDS, HEARTS, SPADES} s1, s2;

枚举标记和类型名

标记

enum suit {CLUBS, DIAMONDS, HEARTS, SPADES};

//然后声明

enum suit s1, s2;

定义类型名

typedef enum {CLUBS, DIAMONDS, HEARTS, SPADES} Suit;
Suit s1, s2;

枚举作为整数

在系统内部,C语言会把枚举变量和常量作为整数来处理。默认情况下,编译器会把整数 0, 1, 2,等等赋给特定枚举中的常量.

我们可以为枚举常量自由选择不同的值:

enum suit {CLUBS = 1, DIAMONDS = 2, HEARTS = 3, SPADES = 4};
  • 枚举常量的值可以是任意整数,列出也可以不用按照特定的顺序.
  • 两个或多个枚举常量具有相同的值甚至也是合法的
  • 当没有为枚举常量指定值时,它的值比前一个常量的值大1

枚举的值只不过是一些稀疏分布的整数,所以C语言允许把它们与普通整数进行混合

用枚举声明“标记字段”

typedef struct {
	enum {INT_KIND, DOUBLE_KIND} kind; 
  union {
		int i;
		double d; 
  } u;
} Number;

例子

#include <stdio.h>
#include <string.h>

enum {INT_KIND, DOUBLE_KIND} kind;

int main(void) {
    kind = INT_KIND;
    printf("%d\n", kind);
    return 0;
}

指针的高级应用

动态存储分配

内存分配函数

在头文件 <stdlib.h>

  • malloc函数——分配内存块,但是不对内存块进行初始化。
  • calloc函数——分配内存块,并且对内存块进行清零。

  • realloc函数——调整先前分配的内存块大小

因为malloc函数不需要对分配的内存块进 行清零,所以它比calloc函数更高效

这些函数会返 回void *类型的值。void *类型的值是“通用”指针,本质上它只是内存地址.

空指针

当调用内存分配函数时,总存在这样的可能性:找不到满足我们需要的足够大的内存块。 如果真的发生了这类问题,函数会返回空指针(null pointer)

空指针用名为NULL的宏来表示

p = malloc(10000); 
if (p == NULL) {
	/* allocation failed; take appropriate action */ 
}

名为 NULL 的宏在 6 个头文件都有定义

  • <locale.h>
  • <stddef.h>
  • <stdio.h>
  • <stdlib.h>
  • <string.h>
  • <time.h>

C99的<wchar.h>也定义了NULL

在C语言中,指针测试真假的方法和数的测试一样。所有非空指针都为真,而只有空指针为假. 所以可以如下这样子写

if (p == NULL) ...
// 可写为
if (!p) ...
  
if (p != NULL) ...
// 可写为
if (p) ...

动态分配字符串

// 这种分配内存是未初始化的
void *malloc(size_t size);

通常情 况下,可以把void*类型值赋给任何指针类型的变量,反之亦然

一旦使用 malloc 函数成功成功, 则指针可以当作数组名来使用.

calloc 函数

//第一个参数是 N 个元素, 第二个参数是每个元素的大小
void *calloc(size_t nmemb, size_t size);

这个函数会将内存清 0

struct point { int x, y; } *p;
p = calloc(1, sizeof(struct point));

realloc 函数

void *realloc(void *ptr, size_t size);

当调用realloc函数时,ptr必须指向先前通过malloccallocrealloc的调用获得的内存 块。size表示内存块的新尺寸,新尺寸可能会大于或小于原有尺寸.

规则

  • 当扩展内存块时,realloc 函数不会对添加进内存块的字节进行初始化
  • 如果realloc函数不能按要求扩大内存块,那么它会返回空指针,并且在原有的内存块 中的数据不会发生改变.
  • 如果realloc函数被调用时以空指针作为第一个实际参数,那么它的行为就将像malloc 函数一样
  • 如果realloc函数被调用时以0作为第二个实际参数,那么它会释放掉内存块

一旦realloc函数返回,请一定要对指向内存块的所有指针进行更新,因为 realloc函数可能会使内存块移动到了其他地方

释放存储空间

对程序而言,不可再访问到的内存块被称为是垃圾(garbage)。留有垃圾的程序存在内存 泄漏(memroy leak)现象

void free(void *ptr);

free函数的实际参数必须是先前由内存分配函数返回的指针。(参数也可以是 空指针,此时free调用不起作用。)如果参数是指向其他对象(比如变量或数组元素) 的指针,可能会导致未定义的行为

悬空指针

调用free(p)函数会释放p指向的内存块,但是不会改变p本身

悬空指针是很难发现的,因为几个指针可能指向相同的内存块。在释放内存块后,全部的 指针都悬空了

链表

struct node {
	int value;   /* data stored in the node */
	struct node *next; /* pointer to the next node */
};

struct node *first = NULL;

// 创建新节点. 注意不是 new_node = malloc(sizeof(new_node)); , 这个是指针大小, 而不是数据大小
struct node *new_node;
new_node = malloc(sizeof(struct node));

//新节点赋值. 注意括号.
(*new_node).value = 10;

// 不过可以写成
new_node->value = 10;

在结构有一个指向相同结构类型的指针成员时(就像node中那样),要求使用结构标记.

->运算符

运算符->是运算符*和运算符.的组合

搜索链表

for (p = first; p != NULL; p = p->next) {
}

从链表中删除结点

struct node *delete_from_list(struct node *list, int n) {
    struct node *cur, *prev;
    for (cur = list, prev = NULL;
        cur != NULL && cur->value != n; prev = cur, cur = cur->next)
        ;
    if (cur == NULL) 
        return list;

    if (prev == NULL) 
        list = list->next;
    else
        prev->next = cur->next;
    free (cur);
    return list; 
}

指向指针的指针

当函数的实际参数是指针变量时,有时候会希望函数能通过指针指向别处的方 式改变此变量。做这项工作就需要用到指向指针的指针

指向函数的指针

double integrate(double (*f)(double), double a, double b);

// 在函数体内调用
y = (*f)(x);


// 调用. 注意, sin 是没有 & 的
result = integrate(sin, 0.0, PI / 2);

qsort函数

void qsort(void *base, size_t nmemb, size_t size, int (*compar) (const void *, const void *));

base必须指向数组中的第一个元素。(如果只是对数组的一段区域进行排序,那么要使base指 向这段区域的第一个元素。)

在一般情况下,base就是数组的名字。nmemb是要排序元素的数量

(不一定是数组中元素的数量)。size是每个数组元素的大小,用字节来衡量。compar是指向比 较函数的指针。当调用函数qsort时,它会对数组进行升序排列,并且在任何需要比较数组元 素的时候调用比较函数

受限指针C99

int * restrict p;

灵活数组成员

当结构的最后一个成员是数组时,其长度可以省略.

struct vstring {
	int len;
	char chars[]; /* flexible array member – C99 only */
};

灵活数组成员不同寻常之处在于,它在结构内并不占空间.

  • 灵活数组成员必须出现在结构的最后, 而且结构必须至少还有一个其他成员。
  • 复制包含灵活数组成员的结构时,其他成员都会被复制 但不复制灵活数组本身

具有灵活数组成员的结构是不完整类型(incomplete type)。不完整类型缺少用于确定所需内 存大小的信息.

特别是,不完整类型(包括含有灵活数组成员的结构)不能作为其他结构的成员和数组的元素, 但是数组可以包含指向具有灵活数组成员的结构的指针

NULL宏表示什么

NULL实际表示0。当在要求指针的地方使用0时,C语言编译器会把它看成是空指针而不是整数0。提

供宏NULL只是为了避免混淆.

声明

声明为编译器提供有关标识符含义的信息.

声明说明符 声明符;
//存储类型 [类型限定 类型说明] 或
//存储类型 [类型说明 类型限定]
  • 声明说明符(declaration specifier)描述声明的变量或函数的性质
    • 存储类型. 4 种. 最多出现一种
    • auto
    • static
    • extern
    • register : register变量使用取地址运算符&是非法的
    • 类型限定符. 可零或多个
    • const (c89) : 只读变量.
    • volatile (c89)
    • restrict (c99)
    • 类型说明符. 比如 short, int , long 等
    • 函数说明符: inline
  • 声明符
    • * : 表示指针
    • [] : 结尾表示数组
    • () : 表示函数

复杂声明的理解

  • 从内往外看(从标识符开始看)
  • 作选择时, 始终使 []() 优先级高于 *

函数的存储类型

  • extern : 允许其他文件调用此函数 (默认)
  • static : 只能在定义函数的文件内部调用此函数

初始化式

  • 具有静态存储期限的变量的初始化式必须是常量
  • 如果变量具有自动存储期限,那么它的初始化式不需要是常量
  • 包含在花括号中的数组、结构或联合的初始化式必须只包含常量表达式,不允许有变量 或函数调用
  • 自动类型的结构或联合的初始化式可以是另外一个结构或联合

未初始化的变量

  • 具有自动存储期限的变量没有默认的初始值
  • 具有静态存储期限的变量默认情况下的值为零. 用calloc分配的内存是简单的给字节的 位置零,而静态变量不同于此,它是基于类型的正确初始化,即整型变量初始化为0, 浮点变量初始化为0.0,而指针则初始化为空指针

内联函数

调用函数和从函数返回所需的工作量称为“额外开销”,因为我们并没有要求函数执行这些工作

在C89中,避免函数额外开销的唯一方式是使用带参数的宏. C99提供了一种更好的解决方案:创建内联函数(inline function)。“内联”表明编 译器把函数的每一次调用都用函数的机器指令来代替。这种方法虽然会使被编译程序的大小增 加一些,但是可以避免函数调用的常见额外开销

不过,把函数声明为inline并不是强制编译器将代码内联编译,只是建议编译器应该使函 数调用尽可能地快,也许在函数调用时才执行内联展开

C99中的一般法则是,如果特定文件中某个函数的所有顶层声明中都有inline但没有 extern,则该函数定义在该文件中是内联的

内联函数的限制

  • 函数中不能定义可改变的static变量
  • 函数中不能引用具有内部链接的变量

GCC最后需要注意的是:仅当通过-O命令行选项请求进行优化时,才会对函数进行“内联”

程序设计

模块

它是一组服务的集合. 它被其他模块使用. 每个模块都有一个接口来描述所提供的服务. 模块的细节都包含在模块的实现中.

服务: 就是函数 接口: 就是头文件 实现: 就是函数定义的源文件

内聚性与耦合性

模块应该具有下面两个性质

  • 高内聚性
  • 低耦合性

模块的类型

  • 数据池 : 一些相关的变量或常量的集合. 这类模块通常只是一个头文件. 例如 <limits.h>, <float.h>
  • 库 : 是一个相关函数的集合. 例如 <string.h>
  • 抽象对象 : 对于隐藏的数据结构进行操作的函数的集合
  • 抽象数据类型 ADT : 将具体数据实现方式隐藏起来的数据类型称为抽象数据类型

信息隐藏

  • 安全性
  • 灵活性

强制信息隐藏的主要工具是static存储类型。将具有文件作用域 的变量声明成static可以使其具有内部链接,从而避免它被其他文件(包括模块的客户)访问。 (将函数声明成static也是有用的——函数只能被同一文件中的其他函数直接调用。

抽象数据类型

封装

C语言提供的唯一封装工具为不完整类型. C标准对不完整类型的描述是:描述了对象但缺少定义对象大小所需的 信息

例如,声明 struct t . 不完整类型的使用是受限的

  • 因为编译器不知道不完整类型的大小,所以不能用它来 声明变量
  • 但是完全可以定义一个指针类型引用不完整类型

底层程序设计

位运算符

  • << : 左移
  • >> : 右移
  • ~ : 按位取反
  • & : 按位与
  • ^ : 按位异或. 与或类似, 但两个操作数的位都是1时结果为 0
  • | : 按位或

如果是无符号数或非负值,则需要在左端补0。如果是负值,其结果是由实现定义的:一些实现会在左端补0,其他一些实现会保留 符号位而补1

为了可移植性,最好仅对无符号数进行移位运算

用位运算符访问位

  • 位的设置 : 用或运算. i|=1<<j; /*setsbitj*/
  • 位的清除 : 用与运算. i &= ~(1 << j); /* clears bit j */
  • 位的测试 : 用与运算. if(i&1<<j)... /*testsbitj*/

    #define BLUE 1 
    #define GREEN 2 
    #define RED 4
    
    i|=BLUE; /*setsBLUEbit */ 
    i &= ~BLUE; /* clears BLUE bit */ 
    if (i & BLUE)... /* tests BLUE bit */
    
    
    
    i |= BLUE | GREEN; /* sets BLUE and GREEN bits */
    i &= ~(BLUE | GREEN);  /* clears BLUE and GREEN bits */
    if (i & (BLUE | GREEN)) ...  /* tests BLUE and GREEN bits */
    

用位运算符访问位域

  • 修改位域 : i = i & ~0x0070 | 0x0050; /* stores 101 in bits 4-6 */
  • 获取位域 : j = i & 0x0007; /* retrieves bits 0-2 */

结构中的位域

struct file_date { 
	unsigned int day: 5; 
	unsigned int month: 4; 
	unsigned int year: 7;
};

每个成员后面的数指定了它所占用位的长度

可移植性技巧 将所有的位域声明为unsigned int或signed int 在C99中,位域也可以具有类型_Bool。C99编译器还允许额外的位域类型

由于通常意义上讲位域没有地 址,C语言不允许将&运算符用于位域.

位域是如何存储的

当编译器处理结构的声明时,会将位域逐个放入存储单元,位域之间 没有间隙,直到剩下的空间不够用来放下一个位域了

C语言允许省略位域的名字。未命名的位域经常用来作为字段间的“填充”,以保证其他位 域存储在适当的位置

struct s { 
	unsigned int a: 4;
	unsigned int : 0;
	unsigned int b: 8;
};

长度为0的位域是给编译器的一个信号,告诉编译器将下一个位域在一个存储单元的起始位置对 齐。假设存储单元是8位长的,编译器会给成员a分配4位,接着跳过余下的4位到下一个存储单 元,然后给成员b分配8位。如果存储单元是16位,编译器会给a分配4位,接着跳过12位,然后 给成员b分配8位

将指针作为地址使用

地址所包含的位的个数与整数(或长整数)一致。构造一个指针来表示某个特定的地址是 十分方便的:只需要将整数强制转换成指针就行. 例如

BYTE *p;
p = (BYTE *) 0x1000; /* p contains address 0x1000 */

x86处理器按小端方式存储数据

volatile类型限定符

volatile 类型限定符使我们可以通知编译器,程序中的某些数据是“易变”的

volatile 限定符会通知编译器每一次都必 须从内存中重新取

标准库

C89标准库总共划分成15个部分. C99新增了9个头,总共有24个.

  1. <assert.h> : 仅包含assert宏,它允许我们在程序中插入自我检查。一旦任 何检查失败,程序会被终止
  2. <complex.h> C99. 定义了 complexI 宏,这两个宏对于复数运算来说非常有用该头还提供了对复数进行数学运算的函数
  3. <ctype.h> : 提供用于字符分类及大小写转换的函数
  4. <errno.h> : errno是一个左值(lvalue),可以在调用特定库函数后进行检测,来判断调用过程中是否有错误发生
  5. <fenv.h> C99. 提供了对浮点状态标志和控制模式的访问。例如,程序可以测试标志来判断浮点数运算过程中是否发生了溢出,或者设置控制模式来指定如何进行取整
  6. <float.h> : 提供了用于描述浮点类型特性的宏,包括值的范围及精度
  7. <iso646.h> C99. 定义了可代表特定运算符(包含字符&|~!^的运算符)的宏。当编程环境的本地字符集没有这些字符时,这些宏非常有用
  8. <inttypes.h> C99. 定义了可用于 <stdint.h> 中声明的整数类型的输入/输出的格式化字符串的宏,还提供了处理最大宽度整数的函数
  9. <limits.h> : 提供了用于描述整数类型(包括字符类型)特性的宏,包括它们的最大值和最小值
  10. <locale.h> : 提供一些函数来帮助程序适应针对某个国家或地区的特定行为方式。这些与本地化相关的行为包括显示数的方式(如用作小数点的字符)、货币的格式(如货 币符号)、字符集以及日期和时间的表示形式
  11. <math.h> : 提供了常见的数学函数,包括三角函数、双曲函数、指数函数、对数函数、幂函数、邻近取整函数、绝对值运算函数以及取余函数
  12. <setjmp.h> : 提供了setjmp函数和longjmp函数。setjmp函数会“标记”程序中的一个位置,随后可以用longjmp返回被标记的位置。这些函数可以用来从一个函数跳转 到另一个(仍然活动中的)函数中,而绕过正常的函数返回机制。setjmp函数和longjmp函数 主要用来处理程序执行过程中出现的严重问题
  13. <signal.h> : 提供了用于处理异常情况(信号)的函数,包括中断和运行时错误。signal函数可以设置一个函数,使系统会在给定信号发生后自动调用该函数;raise函 数用来产生信号。
  14. <stdarg.h> : 提供了一些工具用于编写参数个数可变的函数,就像printf和scanf函数一样。
  15. <stdbool.h> C99. 定义了bool、true和false宏,同时还定义了一个可以用于测 试这些宏是否已被定义的宏。
  16. <stddef.h> : 提供了经常使用的类型和宏的定义
  17. <stdint.h> : C99. 声明了指定宽度的整数类型并定义了相关的宏(例如指定每种类型的最大和最小值的宏)。同时定义了用于构建具体类型的整数常量的带参数的宏
  18. <stdio.h> : 提供了大量的输入/输出函数,包括对顺序访问和随机访 问文件的操作
  19. <stdlib.h> : 包含了大量无法划归其他头的函数。包含在<stdlib.h>中的函数可以将字符串转换成数,产生伪随机数,执行内存管理任务,与操作系统通信,执行搜索与 排序,以及在多字节字符与宽字符之间进行转换
  20. <string.h> : 提供了用于进行字符串操作(包括复制、拼接、比较及搜索)的函数以及对任意内存块进行操作的函数
  21. <tgmath.h> C99. 在C99中,<math.h><complex.h>头中的许多数学函数都有多个版本。<tgmath.h> 头中的泛型宏可以检测传递给它们的参数的类型,并替代为相应<math.h><complex.h>中函数的调用
  22. <time.h> : 提供相应的函数来获取时间(和日期),操纵时间,以及格式化时间的显示
  23. <wchar.h> : C99. 提供了宽字符输入/输出和宽字符串操作的函数。
  24. <wctype.h> : C99. 是<ctype.h>的宽字符版本,提供了对宽字符进行分类和修改的函数

对标准库中所用名字的限制

任何包含了标准头的文件都必须遵守两条规则

  • 该文件不能将头中定义过的宏的名 字用于其他目的
  • 具有文件作用域的库名(尤其是typedef 名)也不可以在文件层次重定义

其他的规则

  • 由一个下划线和一个大写字母开头或由两个下划线开头的标识符是为标准库保留的标识符。程序不允许为任何目的使用这种形式的标识符。
  • 由一个下划线开头的标识符被保留用作具有文件作用域的标识符和标记
  • 在标准库中所有具有外部链接的标识符被保留用作具有外部链接的标识符。特别是所有标准库函数的名字都被保留

使用宏隐藏的函数

C程序员经常会用带参数的宏来替代小的函数,这在标准库中同样很常见.

因此,对于 库的头,声明一个函数并同时定义一个有相同名字的宏的情况并不少见.

在大多数情况下,我们喜欢使用宏来替代实际的函数,因为这样可能会提高程序的运行速 度。然而在某些情况下,我们可能需要的是一个真实的函数,可能是为了尽量缩小可执行代码 的大小。

如果确实存在这种需求,我们可以使用#undef指令来删除宏定义

此外,我们也可以通过给名字加圆括号来禁用个别宏调用:

ch = (getchar)(); /* instead of ch = getchar(); */

输入输出

<stdio.h>中用于读或写数据的函数称 为字节输入/输出函数, 而<wchar.h>中的类似函数则称为宽字符输入/输出函数

在C语言中,术语流(stream)表示任意输入的源或任意输出的目的地.

<stdio.h>中的许多函数可以处理各种形式的流,而不仅仅可以处理表示文件的流

文件指针

FILE *

<stdio.h> 提供了3个标准流

  • stdin : 标准输入, 默认为键盘
  • stdout : 标准输出, 默认为屏幕
  • stderr : 标准错误, 默认为屏幕

**重定向 **

  • 输入重定向: demo <in.dat
  • 输出重定向: demo >out.dat

字符<>不需要与文件名相邻,重定向文件的顺序也是无关紧要的.

文本文件与二进制文件

文本文件分为若干行. 文本文件可以包含一个特殊的“文件末尾”标记. 在Windows中,标记为 \x1a (Ctrl+Z)。Ctrl+Z不是必需的,但如果存在,它就标志着文件的结束,其后的所有字节都会被忽略. 大多数其他操作系统(包括UNIX)没有专门的文件末尾字符

二进制文件不分行,也没有行末标记和文件末尾标记,所有字节都是平等对待的.

文件操作

打开文件

FILE *fopen(const char * restrict filename, const char * restrict mode);

restrict 是C99关键字,表明filename和mode所指向的字符串的内存单元不共享

Windows会把/接受为目录分隔符

当无法打开文件时,fopen函数会返回空指针。这可能是因为文件不存在,也可能是因为文件的位置不对,还可能是因为我们没有打开文件的权限

模式

文本文件

  • r : 打开文件用于读
  • w : 打开文件用于写(文件不需要存在)
  • a : 打开文件用于追加(文件不需要存在)
  • r+ : 打开文件用于读和写,从文件头开始
  • w+ : 打开文件用于读和写(如果文件存在就截去)
  • a+ : 打开文件用于读和写(如果文件存在就追加)

当使用fopen打开二进制文件时需要在模式字符串中包含字母b.

  • rb : 打开文件用于读
  • wb : 打开文件用于写(文件不需要存在)
  • ab : 打开文件用于追加(文件不需要存在)
  • r+b : 打开文件用于读和写,从文件头开始
  • w+b : 打开文件用于读和写(如果文件存在就截去)
  • a+b : 打开文件用于读和写(如果文件存在就追加)

关闭文件

int fclose(FILE *stream);

如果成功关闭了文件,fclose 函数会返回零;否则,它将会返回错误代码EOF(在<stdio.h>中定义的宏)

为打开的流附加文件

FILE *freopen(const char * restrict filename, const char * restrict mode, FILE * restrict stream);

freopen函数为已经打开的流附加上一个不同的文件。最常见的用法是把文件和一个标准 流(stdin、stdout或stderr)相关联 freopen函数的返回值通常是它的第三个参数(一个文件指针)

从命令行获取文件名

int main(int argc, char *argv[]) {

}

argv[0]指向程序的名字

临时文件

FILE *tmpfile(void); 
char *tmpnam(char *s);

tmpfile函数创建一个临时文件(用 wb+ 模式打开),该临时文件将一直存在,除非关闭 它或程序终止.

tmpnam函数为临时文件产生名字。如果它的实际参数是空指针,那么tmpnam函数会把文件 名存储到一个静态变量中,并且返回指向此变量的指针.

文件缓冲

int fflush(FILE *stream);
void setbuf(FILE * restrict stream, char * restrict buf);
int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);

通过调用fflush函数,程序可以按我们所希望的频率来清洗文件的缓冲区.

fflush(fp); : 为和fp相关联的文件清洗了缓冲区

fflush(NULL); : 清洗了全部输出流

如果调用成功,fflush函数会返回零;如果发生错误,则返回EOF

setvbuf函数允许改变缓冲流的方法,并且允许控制缓冲区的大小和位置。函数的第三个 实际参数指明了期望的缓冲类型,该参数应为以下三个宏之一

  • _IOFBF(满缓冲)。当缓冲区为空时,从流读入数据;当缓冲区满时,向流写入数据
  • _IOLBF(行缓冲)。每次从流读入一行数据或者向流写入一行数据
  • _IONBF(无缓冲)。直接从流读入数据或者直接向流写入数据,而没有缓冲区

setvbuf函数的第二个参数(如果它不是空指针的话)是期望缓冲区的地址。缓冲区可以 有静态存储期限、自动存储期限,甚至可以是动态分配的.

用空指针作为第二个参数来调用setvbuf也是合法的,这样做就要求setvbuf创建一个指定 大小的缓冲区。如果调用成功,setvbuf函数返回零。如果mode参数无效或者要求无法满足, 那么setvbuf函数会返回非零值

setvbuf函数 的最后一个参数是缓冲区内字节的数量。较大的缓冲区可以提供更好的性能,而较小的缓冲区 可以节省空间

setbuf函数是一个较早期的函数,它设定了缓冲模式和缓冲区大小的默认值。如果buf是 空指针,那么setbuf(stream, buf)调用就等价于

(void) setvbuf(stream, NULL, _IONBF, 0);

否则的话,它就等价于

(void) setvbuf(stream, buf, _IOFBF, BUFSIZ);

其他文件操作

int remove(const char *filename);
int rename(const char *old, const char *new);

如果调用成功,这两个函数 都返回零;否则,都返回非零值

格式化的输入/输出

...printf 函数

int fprintf(FILE * restrict stream, const char * restrict format, ...); 
int printf(const char * restrict format, ...);

返回值是写入的字符数,若出错则返回一个负值

fprintf函数和printf函数唯一的不同就是printf函数始终向stdout(标准输出流)写入 内容,而fprintf函数则向它自己的第一个实际参数指定的流中写入内容

...scanf 函数

int fscanf(FILE * restrict stream, const char * restrict format, ...); 
int scanf(const char * restrict format, ...);

返回读入并且赋值给对象的数据项的数量. 如果在读取任何数据项之前发生输入失败,那么 会返回EOF

检测文件末尾和错误条件

int feof(FILE *stream); 
int ferror(FILE *stream);

如果为与fp相关的流设置了文件末尾指示器,那么feof(fp)函数调用就会返 回非零值

如果设置了错误指示器,那么ferror(fp)函数的调用也会返回非零值。

而其他情况 下,这两个函数都会返回零

字符的输入/输出

输出

int fputc(int c, FILE *stream); 
int putc(int c, FILE *stream); 
int putchar(int c);

putchar函数向标准输出流stdout写一个字符.

虽然putc函数和fputc函数做的工作相同,但是putc通常作为来实现(也有函数实现), 而fputc函数则只作为函数实现。putchar本身通常也定义为.

始终要把fgetc、getc或getchar函数的返回值存储在int型的变量中,而不是 char类型的变量中

输入

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
int ungetc(int c, FILE *stream);
  • getchar 函数从标准输入流stdin中读入一个字符
  • fgetc函数和getc函数从任意流中读入一个字符

getc和fgetc之间的关系类似于putc和fputc之间的关系。getc通常作为宏来实现(也有 函数实现),而fgetc则只作为函数实现。getchar本身通常也定义为宏

ungetc 函数。此函数把从流中读入的字符“放回”并清除 流的文件末尾指示器.

行的输入/输出

输出

int fputs(const char * restrict s, FILE * restrict stream); 
int puts(const char *s);
  • puts函数总会添加一个换行符.
  • fputs函数不会自己写入换行符,除非字符串中本身含有换行符

当出现写错误时,上面这两种函数都会返回EOF。否则,它们都会返回一个非负的数

输入

char *fgets(char * restrict s, int n, FILE * restrict stream); 
char *gets(char *s)
  • gets函数逐个读取字符,并且把它们存储在str所指向的数组中,直到它读到换行符时停止(丢 弃换行符
  • fgets函数是gets函数的更通用版本,它可以从任意流中读取信息。fgets函数也比gets 函数更安全,因为它会限制将要存储的字符的数量. fgets函数逐个读入字符,直到遇到首个换行符时或者已经读入了 sizeof(str)-1个字符时结束操作. 如果fgets函数读入了换 行符,那么它会把换行符和其他字符一起存储.

块的输入/输出

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 fgetpos(FILE * restrict stream, fpos_t * restrict pos); 
int fseek(FILE *stream, long int offset, int whence);
int fsetpos(FILE *stream, const fpos_t *pos);
long int ftell(FILE *stream);
void rewind(FILE *stream);

fseek函数改变与第一个参数(即文件指针)相关的文件位置. 第三个参数说明新位置是根据文件的起始处、当前位置还是文件末尾来计算.

  • SEEK_SET : 文件的起始处
  • SEEK_CUR : 文件当前处
  • SEEK_END : 文件末尾处

第二个参数是个(可能为负的)字节计数. 通常情况下,fseek函数返回零。如果产生错误(例如,要求的位置不存在),那么fseek 函数就会返回非零值。

ftell 函数以长整数返回当前文件位置.

rewind 函数会把文件位置设置在起始处.

为了用于非常大的文件,C语言提供了另外两个函数:fgetpos函数fsetpos函数。 这两个函数可以用于处理大型文件,因为它们用fpos_t类型的值来表示文件位置

字符串的输入/输出

输出

int sprintf(char * restrict s, const char * restrict format, ...);
int snprintf(char *restrict s, size_t n, const char * restrict format, ...);

sprintf函数类似于printf函数和fprintf函数,唯一的不同就是sprintf函数把输出写 入(第一个实参指向的)字符数组而不是流中。sprintf函数的第二个参数是格式串,这与printf 函数和fprintf函数所用的一样

snprintf函数与sprintf一样,但多了一个参数n。写入字符串的字符不会超过n1,结尾 的空字符不算;只要n不是零,都会有空字符。(我们也可以这样说:snprintf最多向字符串中 写入n个字符,最后一个是空字符。)

输入

int sscanf(const char * restrict s, const char * restrict format, ... );

库对数值和字符数据的支持

就近取整函数

ceil“向上舍入”到最近的整数

floor“向下舍入”到最近的整数

查看

  • <float.h>
  • <limits.h>
  • <math.h>
  • <ctype.h>
  • <string.h>

错误处理

诊断

void assert(scalar expression);

如果参数的值不为0,assert什么也不做;如果参数的值为0,assert 会向stderr(标准错误流)写一条消息,并调用abort函数终止程序执行

C89标准指出,assert的参数必须是int类型的。C99 放宽了要求,允许参数为任意标量类型

禁止 assert, 只需要在包含<assert.h>之前定义宏NDEBUG即可:

#define NDEBUG 
#include <assert.h>

错误

<errno.h>

标准库中的一些函数通过向<errno.h>中声明的int类型errno变量存储一个错误码(正整 数)来表示有错误发生

perror函数(在<stdio.h>中声明) : perror函数会输出到stderr流而不是标准输出

strerror函数属于<string.h>。当以错误码为参数调用strerror时,函数会返回一个指 针,它指向一个描述这个错误的字符串

信号处理

<signal.h> 提供了两个函数:raisesignal

signal 函数

signal函数,它会安装 一个信号处理函数,以便将来给定的信号发生时使用. 每个信号处理函数都必须有一个int类型的参数,且返回类型为void

预定义的信号处理函数(都用宏表示)

  • SIG_DFL : 按“默认”方式处理信号
  • SIG_IGN : 忽略该信号 signal(SIGINT, SIG_IGN);

如果一个signal调用失败(即不能对所指 定的信号安装处理函数),就会返回SIG_ERR并在errno中存入一个正值

raise 函数

raise函数.

int raise(int sig);

返回0代表成功,非0则代表失败

非局部跳转

int setjmp(jmp_buf env);

void longjmp(jmp_buf env, int val);

setjmp宏“标记”程序中 的一个位置;随后可以使用longjmp跳转到该位置。虽然这一强大的机制可以有多种潜在的用 途,但它主要被用于错误处理

总而言之,setjmp会在第一次调用时返回0;随后,longjmp将控制权重新转给最初的 setjmp宏调用,而setjmp在这次调用时会返回一个非零值.

国际化特性

本地化

<locale.h>

类别

通过不同的宏来指定类别

  • LC_COLLATE : 影响两个字符串比较函数的行为
  • LC_CTYPE : 影响<ctype.h>中的函数(isdigitisxdigit除外)的行为
  • LC_MONETARY : 影响由localeconv函数返回的货币格式信息
  • LC_NUMERIC : 影响格式化输入/输出函数(如printf和scanf)使用的小数点字符以及 <stdlib.h> 中的数值转换函数
  • LC_TIME : 影响strftime函数(在<time.h>中声明)的行为,该函数将时间转换成字符串
  • LC_ALL : 所有类别

设置: 第一个参数是以上类别之一

char *setlocale(int category, const char *locale);

C标 准对第二个参数仅定义了两种可能值:“C”和”“。如果有其他地区,由具体的实现自行处理

GNU的C库(称为 glibc)提供了”POSIX”地区,该地区与” “一样。glibc用于Linux,允许在需要的时候增加额 外的地区. 地区格式为

语言[_地域][.码集][@修饰符]
  • 语言的可能值列在ISO 639标准中
  • 地域”来自另一个标准 (ISO 3166)

localeenv 函数

struct lconv *localeconv(void);

关于当前地区的很具体的信息.

请记住C语言的库函数不能自动格式化货币量,需要由程序员使用lconv结构中的信息来完 成格式化

多字节字符和宽字符

多字节字符

在多字节字符编码中,用一个或多个字节表示一个扩展字符。根据字符的不同,字节的数 量可能发生变化。C语言要求任何扩展字符集必须包含特定的基本字符(即字母、数字、运算 符、标点符号和空白字符)。这些字符都必须是单字节的。其他字节可以解释为多字节字符的 开始

宽字符

另外一种对扩展字符集进行编码的方法是使用宽字符(wide character)。宽字符是一种整数,其值代表字符。不同于长度可变的多字节字符,特定实现中所支持的所有宽字符有着相 同的字节数。宽字符串是指由宽字符组成的字符串,其末尾有一个空的宽字符(数值为零的 宽字符)

宽字符具有wchar_t类型(在<stddef.h>和其他一些头中声明)

统一码和通用字符集

统一码为每一个字符分配一个唯一的数(称为码点). 可以有多种方式使用字节来表示这些 码点。我将介绍两种简单的方法,其中一种使用宽字符,另一种使用多字节字符.

UCS-2是一种宽字符编码方案,它把每一个统一码码点存储为两个字节.

UTF-8,该方案使用多字节字符. UTF-8的一个有用的性质就是ASCII的字符在UTF-8中保持 不变:每个字符都是一个字节且使用同样的二进制编码. 它的性质有

  • 128个ASCII字符中的每一个字符都可以用一个字节表示。仅由ASCII字符组成的字符串在UTF-8中保持不变
  • 对于UTF-8字符串中的任意字节,如果其最左边的位是0,那么它一定是ASCII字符,因为其他所有字节都以1开始
  • 多字节字符的第一个字节指明了该字符的长度。如果字节开头1的个数为2,那么这个字符的长度为2个字节。如果字节开头1的个数为3或4,那么这个字符的长度分别为3个字节或4个字节
  • 在多字节序列中,每隔一个字节就以10作为最左边的位

多字节/宽字符转换函数

来自<stdlib.h>

int mblen(const char *s, size_t n);
int mbtowc(wchar_t * restrict pwc, const char * restrict s, size_t n);
int wctomb(char *s, wchar_t wc);
  • mblen函数检测第一个参数是否指向形成有效多字节字符的字节序列
  • mbtowc函数把(第二个参数指向的)多字节字符转换为宽字符。第一个参数指向函数用于 存储结果的wchar_t类型变量,第三个参数限制了mbtowc函数将检测的字节的数量
  • wctomb函数把宽字符(第二个参数)转换为多字节字符,并把该多字节字符存储到第一个参数指向的数组中

多字节/宽字符串转换函数

size_t mbstowcs(wchar_t * restrict pwcs, const char * restrict s, size_t n);
size_t wcstombs(char * restrict s, const wchar_t * restrict pwcs, size_t n);
  • mbstowcs函数把多字节字符序列转换为宽字符
  • wcstombs函数和mbstowcs函数正好相反:它把宽字符序列转换为多字节字符

拼写替换

<iso646.h>头相当简单。它只定义了11个宏,除此之外什么都没有.

and &&
and_eq &=
bitand &
bitor `
compl ~
not !
not_eq !=
or `
or_eq `
xor ^
xor_eq ^=

通用字符名

通用字符名类似于转义序列。但是,普通的转义序列只能出现于字符常量和字符串字面量 中,而通用字符名还可以用于标识符。这个特性允许程序员在为变量、函数等命名时使用他们 的本国语言

可以用两种方式书写通用字符名(\udddd\Udddddddd),每个d都是一个十六进制的数字。 在格式\Udddddddd中,8个d组成一个8位的十六进制数用于标识目标字符的UCS码点。格式 \udddd可以用于码点的十六进制值为FFFF或更小的字符,包括基本多语种平面上的所有字符.

UCS码点的值可以 在 www.unicode.org/charts/ 找到

扩展的多字节和宽字符实用工具

<wchar.h> 头提供了宽字符输入/输出和宽字符串处理的函数.

注意,<wchar.h>为宽字符提供了函数但没有为多字节字符提供函数。这是因为C的普通 库函数能够处理多字节字符,所以不需要专门的函数.

流倾向

每个流要么是面向字节的(传统方式),要么是面向宽字符的(把数据当成宽字符写入流 中). 第一次打开流时,它没有倾向。

使用字节输入/输出函数在流上执行操作会使流成为面向 字节的,使用宽字符输入/输出函数执行操作会使流成为面向宽字符的。流的倾向可以调用fwide 函数进行选择。流只要保持打开状态,就能保持其倾向。调用freopen函数重新打开流会删除其倾向.

宽字符分类和映射实用工具

<wctype.h>头是<ctype.h>头的宽字符版本

其他库函数

可变参数

<stdarg.h>

类型 va_arg(va_list ap, 类型);

void va_copy(va_list dest, va_list src); 
void va_end(va_list ap);
void va_start(va_list ap, parmN);

通用的实用工具

<stdlib.h>

  • 数值转换函数;
  • 伪随机序列生成函数;
  • 内存管理函数;
  • 与外部环境的通信;
  • 搜索和排序工具;
  • 整数算术运算函数;
  • 多字节/宽字符转换函数;
  • 多字节/宽字符串转换函数

日期和时间

<time.h>

它提供了三种存储类型

  • clock_t:按照“时钟嘀嗒”进行度量的时间值
  • time_t :紧凑的时间和日期编码(日历时间)
  • struct tm:把时间分解成秒、分、时等

C99 对数学计算的新增支持

<stdint.h> 中声明的类型可分为以下5组

  • 精确宽度整数类型. intN_t
  • 最小宽度整数类型. int_leastN_t
  • 最快的最小宽度整数类型. int_fastN_t
  • 可以保存对象指针的整数类型. intptr_t
  • 最大宽度整数类型. intmax_t , uintmax_t