C语言系统学习
C 语言系统学习
C 语言编译过程
预处理
#include:头文件包含(把头文件中的内容复制到指令所在的位置)#define N 5:宏定义(简单的文本替换)#define FOO(x) (1 + (x) * (x)):带参数的宏(宏函数)(文本替换:用“实参”替换“形参”)- 宏函数注意事项:

- 为什么要用宏函数?
- 避免函数调用的开销(频繁调用且简短的函数可以用宏函数);
- 提供了一定的“宏编程”能力。
编译
- 经过预处理器处理的文件会交给编译器进行编译;
- 编译器会把程序翻译成对应平台的汇编代码。
汇编
- 汇编器会把生成的汇编代码翻译成对应平台的机器代码(目标文件);
- 此时程序还不能运行,还需要经过最后一个步骤——链接。
链接
- 在链接阶段,链接器会把由汇编器生成的目标代码和程序需要的其它附加代码整合在一起,生成最终可执行的程序;
- 这些附加代码包括程序用到的库函数(如
printf函数)。
汇总
- 整个过程如下图所示:

虚拟内存空间
- 程序经过预处理、编译和链接,最终生成可执行文件;
- 可执行文件被操作系统加载到内存,程序得以运行,运行中的程序称为进程(
process); - 每个进程都有自己的虚拟内存空间,如下图所示:

变量
- 变量要绑定一个值:

输入输出模型
- 输入输出模型是为了平衡内存和外部设备的速度差异。

printf
printf = print + format打印+格式化;- 作用:打印格式串中的内容,并用后面表达式的值替换转换说明(即
%d、%f等占位符)。

- 格式串:

scanf
- 原理:从左到右,依次匹配格式串中的每一项;

scanf返回匹配成功的转换说明的个数;- 能够匹配任意多个空白字符;
- 如果有一项匹配不成功,
scanf会立刻返回。

整数的编码
有/无符号整数
- 无符号整数:

- 有符号整数:

- 两个性质:

- 原码、补码、反码
-
正数的原码、反码、补码均相同。
-
原码:用最高位表示符号位,其余位表示数值位的编码称为原码。其中,正数的符号位为 0,负数的符号位为 1。
-
负数的反码:把原码的符号位保持不变,数值位逐位取反,即可得原码的反码。
-
负数的补码:在反码的基础上加 1 即得该原码的补码。
-
例如:
-
+11 的原码为: 0000 1011
-
+11 的反码为: 0000 1011
-
+11 的补码为: 0000 1011
-
-7 的原码为:1000 0111
-
-7 的反码为:1111 1000
-
-7 的补码为:1111 1001
-
-
注意:
- 对补码再求一次补码操作就可得该补码对应的原码。
- 在计算机系统中,数值一律用补码来表示(存储)。
- 使用补码,可以将符号位和其他位统一处理;
-
浮点数
- 浮点数是不精确的

字符
- 字符的编码为 ASCII:

- 类型通用的两个特征:

- 表示值:

- 支持哪些操作:

- 读/写(和用户操作)

- 惯用法:

类型转换
- 隐式转换:不要将有符号整数和无符号整数混合运算

- 强制转换

定义别名
- 定义别名的作用

sizeof 运算符
sizeof()运算符

void 类型
- void 是一个类型
- 空类型
- 没有值
表达式
- 表达式:计算某个值的公式
- 最简单的表达式:变量和常量
- 运算符:连接表达式,创建更复杂的表达式
- 属性:优先级、结合性
位运算符
i<<n:如果没有溢出,左移 位相当于乘以 ;i>>n: 右移 位,相当于除以 (向下取整);- 如果
i是无符号值或者非负值,则在左边补0; - 如果
i是负值,其结果是由实现定义的,有可能在左边补0,也有可能在左边补1。
- 如果
- 为了可移植性,最好仅对无符号数进行移位运算。

问题一
- 判断一个数是否奇数:
- 需要注意
-1 % 2 == -1,因此不能使用n % 2 == 1来判断。
- 需要注意
1 | |
- 采用二进制的形式:
1 | |
问题二
- 如何判断一个非
0整数是否为2的幂(1,2,4,8,16,......)?- 从二进制中能够看到只有一位为
1,其余都是0。
- 从二进制中能够看到只有一位为

- 代码:
1 | |
问题三
- 给定一个值不为
0的整数,请找出值为1的最低有效位(last set bit)。

- 代码:
1 | |
- 原理:
n的原码(补码):0001 1000-n的补码:0110 1000n & -n的码:0000 1000- 在计算机系统中,数值一律用补码来表示(存储)。
问题四
- 给定两个不同的整数 a 和 b,请交换它们两个的值(要求不使用中间临时变量)。
- 关键是找到一对逆运算,如
a=a+b-b;
- 关键是找到一对逆运算,如
- 代码:
1 | |
- 但是使用加法容易造成溢出的情况,可以使用异或这个逆运算。
1 | |
问题五
- 一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数?
- 关键部分:
1 | |
- 代码:
1 | |
- 证明:
- 1、具有
a^a=0和a^0=a的性质; - 2、偶数个数据累积异或后是
0,奇数个数据累积异或后就剩下它自身。
- 1、具有
问题六
- 一个数组中有两个不相等的数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两个数?
- 关键代码:
1 | |
- 代码:
1 | |
语句
- 语句的分类:

数组
数组的内存模型
- 连续的一片内存空间,并且会划分成大小相等的小空间。
- 使用同一类型使得能够随机访问元素。

- 数组的索引一般是从
0开始的:

- 数组效率一般高于链表效率:
- 空间利用率更高;
- 空间局部性(数组是连续的)。
数组的定义
- 数组的定义:
- 数组的操作只有取下标。


- 求数组的大小的宏函数:
1 | |
- 类型的判断:
- 先看标识符;
- 从标识符表示往右看;
- 再从标识符往左看得到完整的类型信息,例:

多维数组
- 注意:C 语言只有一维数组,多维数组本质上也是一维数组。

常量数组
- 常量数组:元素不能够修改。
- 安全,用于存储静态数组;
- 效率高,编译器能够对常量数组做一些优化。

函数
函数的特征
- 数学上的函数:有返回值,没有副作用;
- C 语言的函数:可以没有返回值,可以有副作用。
函数的准则
- 函数的功能应该越单一越好(能够复用),函数的实现越高效越好;
- 函数是 C 语言的“基本构建组件”,C 语言的本质就是函数之间的调用。
函数相关概念
- 函数定义(隐含了声明):
void foo(int a) {...} - 函数声明:
void foo(int a); - 函数调用:
foo(3); - 函数指针:
foo或&foo
参数传递
- 参数传递的内存调用示例:

- 特例:数组作为参数传递时,会退化成指向第一个元素的指针!

局部变量和外部变量
- 作用域:编译时变量名可以被引用的文本范围;
- 存储期限:运行时变量绑定的值可以被引用的时间长度:

- 局部变量:定义在函数里面的变量;
- 作用域:块作用域,从定义开始到块的末尾(块:大括号
{})。 - 存储期限:
- 默认——自动存储期限-栈
static——静态存储期限-数据段
- 作用域:块作用域,从定义开始到块的末尾(块:大括号
- 默认存储期限示例:

- 静态存储期限示例:

- 外部变量(全局变量):定义在函数外面的变量。
- 作用域:文件作用域,从定义开始到文件末尾。
- 存储期限:静态存储期限。
相关问题
- 静态存储期限的局部变量和外部变量有什么区别?
- 答:作用域不同!
- 静态储存期限的局部变量有什么作用?(使用场景)
- 答:能够存储上一次函数调用的状态!

递归
递归的含义
- 递归的含义:

- 具有递归结构的问题,不一定要用递归求解。
- 递归可能存在大量重复计算的问题。
- 可以采用动态规划来避免重复计算的问题:顺序求解子问题,并将子问题的解保存起来,从而避免重复计算,最终求解到大问题。
汉诺塔问题
- 汉诺塔问题:


约瑟夫环问题
- 约瑟夫环问题:


- 更一般的约瑟夫环问题:

递归三问
- 递归三问:什么时候使用递归?

指针基础
内存模型
- 指针的内存模型:
- 计算机的最小寻址单位——字节
- 变量的地址——变量首字节的地址
- 指针——地址
- 指针变量——存储地址值的变量

指针的操作
- 指针的定义:
- 变量名:
p - 类型:
int* - 注意事项:声明指针变量时,需要指定它指向对象的类型
- 变量名:

- 指针的两个基本操作:
- 取地址运算符:
&——&i - 解引用运算符:
*——*p- 指针变量
p指向对象的别名 - 间接引用——访问内存两次
- 指针变量
- 取地址运算符:

野指针
- 野指针:不知道指向哪块数据。
- 对野指针进行解引用操作,是未定义行为。

-
如何给指针变量赋值?
int *p = &i;int *q = p;—— 指针变量p,q指向同一个对象。p = NULL;:空指针,不指向任何对象指针。
-
指针赋值示例:


指针的应用
- 作为参数——在被调函数中修改主调函数的值。

- 指针作为返回值。
- 教训:不要返回指向当前栈帧的指针!

指针常量和常量指针
- 指针变量
p对内存1,内存2都有写权限:

- 常量指针,对内存
1有写权限:

- 指针常量,对内存
2有写权限:

- 两边加
const修饰符,对内存1,内存2都没有写权限:

- 本质:限制变量的写权限!
传入参数和传出参数
- 传入参数:在函数里面,不会(不能够)通过指针变量修改它指向的对象:

- 传出参数:在函数里面,可以通过指针变量修改它指向的对象!

指针的算术运算
- 画图表现数组和指针:

- 指针加上一个整数,结果还是一个指针:
- 指针加上一个整数
n,其含义在于指针向右偏移了n个单位; - 单位表示的是对象类型的大小。
- 指针加上一个整数

- 指针减去一个整数
n,其含义在于指针向左偏移了n个单位:

- 两个指针相减,结果是一个整数:

- 根据指针减法,可以定义指针的比较运算:

指针和数组的关系
- 采用指针处理数组:

*和++的组合,--同理:

- 指针运算的组合的示例:

- 在必要的时候,数组可以退化成指向它索引为
0元素的指针:

-
数组退化的几种情况总结
- 数组作为参数:
fun(arr); - 数组在赋值表达式的右边:
int* p = arr; - 数组参与算术运算:
arr+3;
- 数组作为参数:
-
指针也支持取下标运算:

- 下标可以放到中括号
[]前面:

字符串
字符串字面值
- 三种书写方式:
- 如第三种方式,如果两个字符串字面值之间仅有空白字符,可以认为这两个字符串字面值是相邻的,相邻的字符串字面值在编译的时候就会拼接成一个字符串字面值。

- 内存模型:
- 字符串字面值在代码段——不可被修改的。
- 可以把字符串字面值看作常量数组。

- 字符串字面值支持的操作:
- 常量数组能支持的操作,字符串字面值都支持,例:
- 十进制转换为十六进制的函数:

字符串变量
- 总纲:
- C 语言中没有字符串类型——
string、String - C 语言中的字符串依赖字符数组存在!
- C 语言字符串是一种“逻辑类型”。
- C 语言中没有字符串类型——

- 注意事项:
- 字符串必须以
'\0'结尾! - 在 C 语言中求字符串的长度不是 的时间复杂度,不能写成下面的例子:
- 字符串必须以

声明和初始化
- 注意数组的初始化和字符串的字面值。

读和写(和用户交互)
printf()+%s输出字符串

scanf()+%s读字符串%s的匹配规则:- 忽略前置空白的字符,读取字符填入字符数组;
- 遇到空白字符结束。
- 缺点:
- 不能够存储空白字符;
- 不会检查数组越界。

puts()函数:puts(str);等价于printf("%s\n",str);

gets()函数- 从
stdin(标准输入流) 中读取一行数据,传入字符数组,并将'\n'替换为'\0'; - 缺陷:不会检查数组越界。
- 从

fgets()函数- 和
gets()函数的区别:- 会检查是否越界;
- 会存储
'\n'符,并在后面添加'\0'。
- 和

字符串库函数
- 需要引入
#include <string.h>库。
strlen
strlen()的实现和遍历字符串的惯用法:

strlen()不会计算'\0':

strcpy() 和 strncpy() 函数
strcpy()函数不检查数组越界,strncpy()函数要设定最大拷贝的数量。

strcpy()函数的实现和惯用法:- 将赋值运算符右侧的"表达式”的值赋给左侧的变量,赋值表达式的值就是被赋值的变量的值。
- 例如:
a=(b=5)相当于b=5和a=b两个赋值表达式,因此a的值等于5,整个赋值表达式的值也等于5。

strncpy()函数的实现:

strcat() 和 strncat() 函数
strcat()函数不检查数组越界,strncat()函数要设定最大拷贝的数量。

strcat()函数的实现:

strncat()函数的实现:

strcmp()函数
- 字符串的比较:
strcmp()函数 - 比较规则:字典序(
ASCII)

strcmp()函数的实现:

字符串数组
字符串数组
- 字符数组的数组——二维字符数组
- 缺陷:
- 浪费内存空间;
- 交换字符串不方便。
- 初始化的时候是数组初始化式。
- 缺陷:

字符指针数组
- 储存的是字符指针,比较灵活,不会造成内存空间的浪费;
- 直接初始化的时候是字面值,字符串存储在代码段,无法修改;
- 需要能够修改的字符串要先定义字符串变量再合并到字符指针数组。

命令行参数
- 命令行参数可以认为是操作系有调用
main()函数时传递的参数。 - 命令行参数和从
stdin(标准输入)读取数据有什么区别?- 命令行参数:程序还未执行;
- 从
stdin读取数据:程序已经运行。
- 命令行参数有什么作用?
cp src dst:写一些非常通用的程序;ls -l:改变程序的行为,参数不同,行为不同。

argc:argument count,命令行参数的个数;argv:argument vector,命令行参数,命令行参数都是字符串。argv[0]:表示可执行程序的路径。

- 命令行参数的内存模型:

- 命令行参数的类型转换(
sscanf()函数):

- 不同的指定格式(类型)读取函数,从不同的输入中读取数据:

结构体
结构体的含义
- 对象的含义:
- 属性:静态数据;
- 方法:行为。
- C 语言的结构体类似其它语言的“类”,不一样的是,结构体只有属性没有方法。
结构体的语法
- 结构体的语法:

- 一般来说,使用
typedef给struct类型取别名Student,这样就可以直接使用Student a;来定义结构体。

- 匿名结构体,只能被使用一次

结构体的内存模型
- 一片连续的内存空间;
- 会按声明的顺序存放每一个成员;
- 在结构体变量的中间或后面,可能会有填充(由于以前的数据总线只有
32 bit,因此填充是为了对齐,对齐是为了更快地访问数据)。

结构体变量的操作
- 获取成员和赋值(结构体变量的复制),但是由于结构体的参数量可能很大,所以往往更习惯传递一个指向结构体变量的指针。

- 结构体指针操作的语法糖:

枚举
- 枚举的作用:表示一些离散值(类别或者状态)
- 枚举的定义和用法:

动态内存分配
- 为什么要在堆上分配空间?
- 因为栈空间是受限的!
- 栈帧的大小是在编译期间确定的,而且栈不能存放动态大小的数据;
- 栈空间比较小:
- 主线程:8M;
- 其他线程:2M;
- 因此栈上不能存放很大的数据。
- 每个线程都有自己的栈,所以栈空间最好不要放多线程共享的数据。
- 因为栈空间是受限的!
堆空间
- 堆空间的内存模型:

- 如何申请堆空间?

- 在堆上成功创建连续的内存空间后会返回指向新内存块的指针,创建失败则返回空指针。

calloc()函数的示例代码:
1 | |
- 其中
void*是通用指针,作用是可以和其它类型指针相互转换。- 如果
void*指向对象的类型还不确定,不能够直接操作通用指针(例如对指针进行解引用等操作)。
- 如果
动态数组
- 自定义实现动态数组的模型:

- 依赖关系如下:
- 依赖接口,不需要依赖具体的实现!

realloc()函数的缩容和扩容原理

- 扩容代码:
1 | |
- 大端法和小端法

free()函数的使用问题:free()函数使用之后,其原本指针属于悬空指针(野指针的一种),因此- 要避免
free()函数使用多次的问题(double free); free()函数使用之后继续使用原本的指针(use after free);
- 要避免
- 当堆上的数据不在使用时,应该有且只释放一次!

- 垃圾回收器的优缺点:

main.c代码:
1 | |
Vector.h代码:
1 | |
Vector.c代码:
1 | |
二级指针
- 二级指针,即指向指针的指针。

- C 语言函数是的参数传递属于传值,因此下面例子中只是把指针
head的值传入了addNode()函数的栈帧中,并没有实质性地修改指针head的指向。

- 传二级指针即可解决上述问题:

- 总结:怎么确定传一级指针还是二级指针?
- 想要修改哪个变量,就传那个变量的地址;
- 想修改指针指向的对象,传一级指针;
- 想修改指针的值(指向),传二级指针。
函数指针
- 函数指针,即指向函数的指针,它保存了函数的地址,可以通过它调用指向的函数。

- 函数指针的语法和调用:


- 函数指针的作用:
- 函数式编程(传递函数,返回函数),通过函数指针支持函数式编程。
- 分解任务,解耦合。
- 编写非常通用的函数(功能非常强大的函数)。
- 函数式编程(传递函数,返回函数),通过函数指针支持函数式编程。
qsort()函数示例:

qsort()函数利用函数指针(钩子函数)分解任务,解耦合:

数据结构
链表
单向链表
- 增:在某个结点后面添加结点。

- 删:在某个结点后面删除。

- 查:
- 根据索引查找结点;
- 查找与特定值相等的结点。

- 遍历:正向遍历。

双向链表
- 双向链表的模型:

- 基本操作:
- 增:能够在某个结点的前后添加;
- 删:删除当前结点;
- 查:
- 根据索引查找值:
- 单链表:,平均遍历 ;
- 双链表:,平均遍历 ;
- 查找与特定值相等的结点:
- 无序:;
- 有序:双链表比单链表效果更好(在多次查找的时候,通过记录上一次查找的结点来优化);
- 查找前驱结点:;
- 根据索引查找值:
- 遍历:
- 正向;
- 逆向。
- 空间和时间:
- 空间换时间:缓存、缓冲;
- 时间换空间:压缩、交换区(
swap area);
栈
- 栈的模型:栈是操作受限的线性表(数组、链表),在一端添加,在同一端删除元素。
- 特性:先进后出
- 为什么需要栈这种数据结构?
- 安全;
- 可读性强;
- 和现实生活中的场景是对应的。

- 栈的基本操作:
- 添加:
push(); - 删除:
pop(); - 查找:
peek(); - 判空:
empty()->遍历来实现;
- 添加:
- 栈的实现:

栈的应用
函数调用栈
- 函数调用栈:

符号匹配问题
- 符号匹配问题:
- 遍历字符串
- 遇到左括号,将对应的右括号入栈;
- 遇到右括号,出栈,判断是否和遇到的右括号一致:
- 否:不匹配
- 是:继续
- 遍历完字符串后,判断栈是否为空:
- 否:不匹配
- 是:匹配
- 遍历字符串

栈表示优先级
- 栈表示优先级:
- 单调栈:从栈顶到栈底是单调递增或者单调递减的。
- 中缀表达式和后缀表达式:

- 如何计算后缀表达式:

- 如何将中缀表达式转换成后缀表达式:

用栈记录轨迹
- 浏览器的前进/后退功能:
http:无状态协议,每一次请求是独立的。

- 深度优先搜索
- 回溯问题
队列
队列模型
- 队列模型:受限的线性表,一端添加元素,另一端删除元素。

队列基本操作
- 基本操作:
- 入队列:
push(); - 出队列:
pop(); - 查看队头:
peek(); - 判空:
empty(); - 判满:
full();
- 入队列:
队列的实现(动态数组)
实现一
- 队头为
0位置,用rear标识队尾:

实现二
- 用
front标识队头,用rear标识队尾:

实现三
- 采用循环数组:

- 入队列(需要扩容的设计):

- 其它操作的实现:

队列的应用
- 缓冲(先进先出),具有公平性,一般采用有界队列。
- 消息队列(中间件),避免集群之间耦合导致故障扩散。

- 广度优先搜索(三度好友、队列)。

哈希表
引入
- 为什么需要哈希表?
- 问题:统计一个文件中,字母出现的次数(不区分大小写)
- 可以采用数组来存储,数组下标表示字母,数组的值表示次数。这样的对应关系称为键值对(
key-value)。 - 具有以下两点限制:
- 键的取值范围很小;
- 键可以很容易地转换成数组的下标。
- 核心问题在于:如果不满足上述的两个限制条件,该如何表示键值对(
key-value)?- 采用哈希表!
- 可以采用数组来存储,数组下标表示字母,数组的值表示次数。这样的对应关系称为键值对(

模型
- 同一个哈希表,哈希桶的数据结构可以不一样,但是
key是唯一的!

基本操作
- 增:
put(key, val); - 删:
delete(key); - 查:
val = get(key); - 遍历:依次遍历每一个哈希桶。
哈希表的实现
- 哈希函数(数据的指纹)

- 性能高的哈希函数(等概率随机映射)

- 哈希桶(用于解决哈希冲突)
- 拉链法
- 开放地址法


main.c代码:
1 | |
hashmap.h代码:
1 | |
hashmap.c代码:
1 | |
哈希表的扩容
- 安全性:在扩容时,更改
HashSeed。

- 扩容方案:

哈希表的性能
- 哈希表的性能与所有哈希桶中链表的平均长度有关
- 一般设定负载因子在 0.75

哈希表的应用
- 存储键值对数据;
- Redis(C 语言)—— 内存数据库(键值对数据库)—— 缓存;
- Redis 内部大量使用哈希表。
位图
位图的模型
- 位(
bit)的数组,内存紧凑的数据结构,存储两种状态。 - 为什么需要构建一个专门的数据结构来表示位的数组?
- 计算机最小的寻址单位是字节(
Byte),而不是位(bit)。
- 计算机最小的寻址单位是字节(

位图的基本操作
- 增(
set):将某一位设置为 1; - 删(
uset):将某一位设置为 0; - 查(
isset):判断某一位是否为 1; - 遍历。
位图的实现
- 要求:
- 尽可能少地申请内存空间;
- 运算速度要快(采用位运算)。

- 不同的运算对应的位运算和位图使用示例(注意是小端法):

位图的应用
- 假如有 10 亿的 QQ 用户,要存储每一个用户的在线状态(内存小于 1 G),需要使用什么数据结构?

- 如何实现对数组排序并去重?
- 遍历原始数组,在对应的数字的位图上置为 1,最后遍历位图即可得到排序并去重的数组。

二叉树
二叉树的模型
- 二叉树的每个结点最多有两棵子树,每个结点的度
degree ≤ 2:- 二叉树的子树又分为左子树和右子树。

二叉树的特殊形态
- 二叉树有两种特殊的形态:完全二叉树和满二叉树。
- 完全二叉树:若二叉树的深度为
h,除了第h层外,其它各层(1 ~ h-1)层的结点数目都达到最大值,第h层的结点都连续排列在最左边,这样的二叉树就是完全二叉树; - 满二叉树:每一层的结点数目都达到最大值(包括最下面一层);
- 满二叉树包含完全二叉树。
- 完全二叉树:若二叉树的深度为

二叉搜索树(Binary Search Tree,BST)
- 二叉搜索树又叫二叉排序树,要求树中的结点可以按照某个规则进行比较,其定义如下:
- 左子树中所有结点的
key值都比根节点的key值小,并且其左子树也是二叉搜索树; - 右子树中所有结点的
key值都比根节点的key值大,并且其右子树也是二叉搜索树。
- 左子树中所有结点的

二叉搜索树的实现
结构
- 二叉搜索树的结构(
bst.h):

添加节点
- 添加节点:

- 代码:
1 | |
查找节点
- 查找结点与添加节点逻辑类似,代码如下:
1 | |
遍历节点
广度优先搜索遍历
- 广度优先搜索步骤(层次遍历):

- 代码:
1 | |
深度优先搜索遍历
- 深度优先搜索步骤(前序、中序(二叉排序树)、后序):

- 代码:
1 | |
删除二叉搜索树节点
- 删除度为 0 的情况:

- 删除度为 1 的情况:

- 删除度为 2 的情况:
- 退化成度为 0 或者 1 的情况来处理;
- 采用要删除节点的右子树的最小节点来替代当前节点来实现退化!

- 特例:右子树最小节点就是右子树根节点

- 代码:
1 | |
二叉搜索树的性能分析
- 缺陷:不能保证 时间复杂度的插入,删除和查找。

平衡二叉搜索树
- AVL树和红黑树的对比:

红黑树
- 红黑树的基础模型是 2-3-4树,基于此在二叉搜索树的基础上实现红黑树。
- 问题 1:如何表示2-3-4树中的
3-node和4-node?

- 问题 2:“边”是一种逻辑结构,是不存在的,如何表示“边”的颜色?
- 把颜色放入孩子节点中!

- 红黑树的性质:
- 2-3-4树的高度 ≤ 红黑树高度 ≤ 2×(2-3-4树的高度)
- 因为黑高平衡的原因!

文件流
流的模型
- 流的模型:

-
优点:
- 程序员读写文件时,不需要关心文件的位置;
- 数据源(
data source)和数据汇(data sink)是解耦的。
-
程序员视角的文件:

前置知识
缓冲区类型
- 缓冲区是以先进先出的方式管理数据的,缓冲区分为三种类型:
- 满缓冲:当缓冲区为空时,从输入流中读取数据;当缓冲区满时,向输出流写入数据。
- 行缓冲:每次从输入流中读取一行数据;每次向输出流中写入一行数据(
stdin、stdout)。 - 无缓冲:顾名思义,就是没有缓冲区(
stderr,输出错误信息)。

标准流
- 标准流不需要手动创建,也不需要手动关闭(即其它文件流都是需要手动操作的)。

二进制文件和文本文件
- 二进制文件:字节

- 文本文件:字符

- 二进制文件和文本文件的区别举例:

文件流接口(API)
文件流操作步骤
- 文件流操作步骤如下:
- 打开文件流:
fopen(); - 读/写文件:统计、转换、加密、解密…
- 关闭文件流:
fclose()。
- 打开文件流:
打开文件流
- 打开文件流的函数:

- 文件的路径(
filename)- 绝对路径和相对路径

- 打开文件的模式
- 文件的类型
- 对文件的操作(
r,w) - 写模式和追加模式是不一样的,如果文件存在,写模式会清空原有数据,而追加模式会在原有数据的后面写入新的内容。
- 以文本形式打开文件的模式:

- 以二进制形式打开文件的模式:

关闭文件流
- 关闭文件流的函数:

打开和关闭文件流的程序
- 打开和关闭文件流的程序示例:
1 | |
读写文本文件
一个字符一个字符地读写
- 一个字符一个字符地读写:

- 代码:
1 | |
一行一行地读写
- 一行一行地读写:

- 代码:
1 | |
格式化地读写
- 格式化地读写:

读写二进制文件
- 读二进制文件:

- 写二进制文件:

- 代码示例:
1 | |
移动文件位置
fseek()函数:可以改变与 stream 相关联的文件位置。

- 使用示例:

ftell()函数:

rewind()函数:

- 代码示例:

1 | |
错误处理
- 需要引入
#include <errno.h>和#include <string.h>两个头文件。 - 错误处理示例:

C语言系统学习
http://example.com/2025/03/26/C_system/