查漏补缺|C 语言
本文最后更新于:3 years ago
C 语言的知识点不多,但是比较杂。本文系统地对 C 语言进行补充学习。
目录
PC端右侧有目录。
001.C语言的简洁
002.编译过程
1.分步编译的四步
2.编译命令
3.文件包含处理
4.条件编译
(1)测试存在
(2)测试不存在
(3)根据表达式定义
003.system函数
004.数据类型
1.作用
2.本质
3.补充
4.数组类型
5.sizeof
(1)sizeof是用来求数据类型的字节数的。
(2)数组名和指针的辨析
(3)内存大小和字符串长度的辨析
6.数据类型取值范围
(1)字节大小
(2)为何正数范围比负数范围少一个?
(3)数值越界
7.关键字:signed、unsigned
005.输出
1.格式
2.调试
3.补充:取余和取模的区别
006.各进制的储存方式
007.关键字:define、类型限定符
1.define
2.extern
3.const
(1)修饰只读变量
(2)易错
(3)好处
4.volatile
5.register
008.switch语句
009.随机数
010.getchar() 吞掉回车
011.字符数组和字符串
1.区别
2.字符串越界
3.字符串初始化原理
012.字符串处理函数
1.gets()
2.fgets()
3.fputs()
4.strlen()
5.strcpy
6.strncpy
7.strcmp
8.strncmp
9.strcat
10.strncat
11.sprintf
12.sscanf
(1)提取数字
(2)提取字符
13.strchr
14.strstr
(1)查询字符串
(2)模型|查找字符串个数
15.strtok
16.atoi
17.atof
18.atol
013.字符串常用模型
1.两头堵模型
2.字符串反转模型
(1)传统解法
(2)递归解法
014.分文件编程
1.文件功能
2.防止头文件重复包含
015.指针
1.野指针
2.空指针
3.指针大小
4.多级指针
5.*p 等价于 p[0]
6.指针步长
7.万能指针
8.const修饰指针
9.形参中的数组退化为指针
10.字符串常量地址相同
11.数组名
(1)数组名是常量
(2)数组名b
、&b
的数据类型不一样
(3)二维数组数组名
(4)二维数组的本质
(5)二维数组名的本质
(6)二维数组求行数、列数
12.指针数组
13.数组指针
(1)储存整个数组首地址
(2)形参中不能用 char **a
代替 char a[][10]
(3)改进办法1:形参中规定指针步长
(4)改进本法2:形参中用数组指针
14.指针函数
15.函数指针
16.函数指针数组
17.回调函数
18.指针易错
(1)指针值传递
(2)二级指针没有空间
016.main()函数参数
017.内存管理
1.普通局部变量
2.static局部变量
3.普通和static局部变量的区别
4.全局变量
5.static全局变量
6.内存分区
(1)内存四区
(2)内存加载顺序
(3)栈区堆区扩展方向
7.内存操作函数|memset
8.内存操作函数|memcpy
9.内存操作函数|memmove
10.内存操作函数|memcmp
11.内存释放
12.堆区空间越界
13.堆区数组
14.内存污染|返回栈区地址
(1)问题
(2)改进
15.内存泄漏|值传递
(1)问题
(2)改进|返回堆区空间
16.非法使用内存导致错误
17.如何避免非法使用内存
018.结构体
1.格式
2.不常用定义方法
3.结构体数组初始化
4.结构体嵌套
5.结构体的值传递和地址传递
6.结构体指针套一级指针
7.结构体指针数组套二级指针
8.结构体的深拷贝与浅拷贝
9.结构体内存
(1)结构体偏移量
(2)结构体对齐规则
(3)特殊情况|当结构体成员为数组时
(4)特殊情况|当有不完整类型时
10.typedef
11.比较结构体是否相等
019.共用体(联合体)、枚举
1.共用体
2.枚举
020.文件操作
1.缓冲区
(1)为什么要有缓冲区?
(2)缓冲区写入文件的条件
(3)刷新缓冲区|fflush()
2.文件指针
3.文件分类
(1)分类
(2)win 和 linux 文本文件的区别
4.文件操作流程
5.标准设备文件指针
6.文件打开和关闭
(1)fopen()
<1> 路径
<2> 权限
<3> 注意
(2)fclose()
7.写文件|fputc()、fputs()
8.读文件|fgetc()、fgets()
9.判断文件结尾|feof()
10.格式化|fprintf()
11.格式化|fscanf()
12.按块读写文件
(1)fwrite()
(2)fread()
(3)返回值与边界问题
13.光标移动|fseek()
14.获取光标位置|ftell()
15.光标回到开头|rewind()
16.应用:获取文件大小
021.链表
1.链表和数组的区别
2.分类
(1)动态链表和静态链表
(2)带头链表和不带头链表
(3)单向链表和双向链表
(4)循环链表
3.增删改查
4.交换节点
5.链表反转
022.常见错误
1. Segmentation Fault
001.C语言的简洁
32个关键字、9中控制语句、34个运算符。
short | int | float | double | char | unsigned |
---|---|---|---|---|---|
long | enum | void | auto | for | while |
do | if | else | switch | case | break |
continue | goto | return | sizeof | const | typedef |
static | struct | default | union | register | signed |
extern | volatile |
If-else | for | while |
---|---|---|
do-while | continue | break |
switch | goto | Return |
002.编译过程
1 |
|
1.分步编译的四步
**预处理:**宏定义展开、头文件展开、条件编译、删除注释、不会检查语法错误,生成
*.i 文件
。**编译:**词法分析、语法分析、语义分析、优化后生成相应的汇编代码。检查语法错误,将
*.i 文件
编译成汇编文件*.s 文件
。**汇编:**将
*.s 文件
生成 目标文件*.o 文件
(二进制文件)。**链接:**C语言程序需要依赖各种库,编译后需要把库链接到可执行文件
*.out
或者*.exe
中。【主要是动态库即DLL文件(Dynamic Link Library)】。
2.编译命令
1 |
|
选项 | 含义 |
---|---|
-E | 只进行预处理 |
-S | 只进行预处理、编译 |
-c | 只进行预处理、编译、汇编 |
-o | 指定输出文件的文件名 |
PS:助记,选项是ESC,后缀是iso。
3.文件包含处理
include <>
用于包含库函数的头文件。
include ""
用于包含自定义函数的头文件。
4.条件编译
条件编译在预处理阶段展开。
(1)测试存在
1 |
|
举个例子:
如果存在变量 a,则打印上面一句,如果不存在变量a,则打印下面一句。
1 |
|
(2)测试不存在
ifndef
比ifdef
多一个 n。多用于 防止头文件重复包含。
1 |
|
举个例子:
自定义头文件 test.h
。为防止头文件重复包含,可以使用以下条件编译:
1 |
|
也可以防止头文件重复包含。
1 |
|
(3)根据表达式定义
1 |
|
举个例子:
用来调试代码。
1 |
|
003.system函数
**用途:**在程序中,执行DOS命令 / Linux命令 /外部程序。
常见的有:
system(“PAUSE”)
暂停屏幕
system(“CLS”)
:清屏
system("shutdown -s -t 3")
:三秒后关机
1 |
|
PS:Windows的两个框架:QT / MFC,故可用QT / MFC调用system函数,执行 DOS命令 或者 外部程序。
004.数据类型

1.作用
编译器预算变量分配的内存空间大小。
2.本质
数据类型的本质:固定内存大小的别名
3.补充
变量命名不能以数字开头。
char 类型本质上是 1字节大小的整型,存储的是ASCII码。
4.数组类型
数组也是一种数据类型,由 元素个数 和 元素对应类型 决定。
int a[10];
,a 的数据类型是 int [10]
。
5.sizeof
(1)sizeof是用来求数据类型的字节数的。
如 int a;
那么无论 sizeof(int)
或者是 sizeof(a)
都是等于4,因为 sizeof(a)
其实就是 sizeof(type of a)
。
对于
char str[] = "ab";
就有sizeof(str) == 3
, 因为 str 的数据类型是char [3]
。对于
sizeof("ab") == 3
,因为"ab"
的数据类型是const char [3]
。对于
char *p = "ab";
就有sizeof(p) == 4
,因为 p 的数据类型是char*
。对于空字符串
sizeof("") == 1
,sizeof("\0") == 2
。只要是字符串,默认结尾加\0
(2)数组名和指针的辨析
1 |
|
(3)内存大小和字符串长度的辨析
sizeof 计算内存大小,测量字符串长度,使用 strlen()
函数。
1 |
|
6.数据类型取值范围
计算机储存数据,都是按照补码来储存的,所以取值范围即若干位补码的取值范围。
(1)字节大小
数据类型 | 占用空间 | 取值范围 |
---|---|---|
char | 1字节 | -128~127(-27 ~ 27 -1) |
short | 2字节 | -32768~32767(-215 ~ 215 -1) |
int | 4字节 | -2147483648~2147473647(-231 ~ 231 -1) |
long | 4字节 | -2147483648~2147473647(-231 ~ 231 -1) |
unsigned short | 2字节 | 0~65535(0 ~ 216 -1) |
unsigned int | 4字节 | 0~4294967295(0 ~ 232 -1) |
unsigned long | 4字节 | 0~4294967295(0 ~ 232 -1) |
long long | 8字节 | |
float | 4字节 | 精度:7位有效数字 |
double | 8字节 | 精度:16位有效数字 |
(2)为何正数范围比负数范围少一个?
以有符号的char为例。
原码范围 | ||
---|---|---|
正数 | 0000 0000 ~ 0111 111 | +0 ~ +127 |
负数 | 1000 0000 ~ 1111111 | -0 ~ -127 |
特殊 | 1000 0000 不等于-0,等于-128 | -128 |
**原因:**因为-127 + -1 = 1 1000 0000
即-128,舍去符号位1,将 1000 0000
当作 -128。
1 |
|
(3)数值越界
化为补码运算后,舍弃高位。
1 |
|
PS:以下不是数值越界情况,a + 2
是另外一块存储单元。
1 |
|
7.关键字:signed、unsigned
signed 表示 有符号,数据类型默认有符号。
1 |
|
unsigned 表示 无符号。
无符号打印用
%u
,打印的是补码,所以不能打印负数!。
1 |
|
005.输出
1.格式
类型 | 格式 |
---|---|
short | %hd |
long | %ld |
unsigned int | %u |
long | %ld |
long long | %lld |
unsigned long long | %ull |
以16进制打印指针 | %p |
打印% | %% |
转义字符 | 含义 |
---|---|
\r | 光标切换到句首,【删除本行】 用处:进度条,倒计时 |
\b | 退格,【删除一个字符】 |
\123 | 八进制转义字符 |
\x23 | 十六进制转义字符 |
“\0123” | 字符串中 “\0123” 表示 \n, |
1 |
|
double 和 float 输出默认六位小数,格式化输出除外。
2.调试
用于打印调试日志。
__FILE__
、__LINE__
是系统设置好的。⚠️注意:__FILE__
、__LINE__
是左右各两个下划线连在一起。
1 |
|
3.补充:取余和取模的区别
同号取余取模结果一致。
异号结果不一致
举例:
1 |
|
**本质:**取余和取模都是以下两步
1 |
|
取余和取模的区别就在于第一步求商这步:
取余:-7 / 4 = -1.75,取余会靠近 0 舍入,商 = -1。
取模:-7 / 4 = -1.75,取模会靠近 负无穷大 舍入,商 = -2。
第二步相同。
006.各进制的储存方式
十进制DEC | 直接写 | %d |
八进制OCT | 以0开头:int a =0123 | %o【默认以四个字节输出】 |
十六进制HEX | 以0x开头:int a = 0x123 | %x【默认以四个字节输出】 %X(即大写字母十六进制输出) |
二进制BIN | C语言不能直接表示 |
注意事项:
输入输出十进制,计算机默认输入输出的是 原码。
输入输出八进制 / 十六进制,计算机默认输入输出的是 补码。
正数原码补码一致,所以无影响。
举例:
1 |
|
1 |
|
PS:%0x
以四个字节输出,(32位)。
007.关键字:define、类型限定符
**类型限定符:**extern、const、volatile、register。
1.define
1 |
|
作用:定义一个宏定义标示符MAX,它代表1000。
#号开头的语句是预处理语句,(预处理时,MAX将自动替换为1000),无需分号结束。
2.extern
1 |
|
int a 是既声明,又定义。
extern int a 是声明,未定义(即未建立储存空间)。
3.const
constant 的缩写。
(1)修饰只读变量
1 |
|
(2)易错
必须初始化。
1 |
|
如何在另一.c源文件中引用const常量。
1 |
|
(3)好处
可以避免不必要的内存分配。(define 有若干个拷贝,const 只有一份拷贝,不会浪费内存。)
指针做函数参数,可以有效的提高代码可读性,减少bug。
4.volatile
防止编译器优化代码。
1 |
|
编译器优化代码:当编译器发现,第2行 和 第3行之间没有代码对 i 的值进行改变,自动把上次读的 i值 放在 b 中。
volatile
关键字声明 变量i 之后,告诉编译器 i 易变(受操作系统、硬件或者其它线程的影响), 编译器每次都需要在 i 的地址处读取 i 值。
5.register
register:寄存器
1 |
|
定义寄存器变量,如果CPU有空闲寄存器,将a存入寄存器,提高效率。
008.switch语句
case 数字或者字符
1 |
|
括号内的a只能是 整型 或者 字符型。
009.随机数
void srand(unsigned int seed);
设置rand()产生随机数时的随机种子seed。int rand(void)
rand()返回一个随机数。
1 |
|
010.getchar() 吞掉回车
读入字符的时候,一定要注意!利用getchar() 清空 stdin 的缓冲区。
【两种场景】
先读入一个字符串,再读入一个字符
1 |
|
先读入一个字符,再读入一个字符
1 |
|
【原因】
键盘输入的字符,会放到缓冲区,scanf
函数从缓冲区中读取。读取字符的时候,可以读 ‘\n’
。读字符串时,不会读入\n
,所以不需要 getchar()
。
以两个c为例,键盘上敲出,
1 |
|
缓冲区中显示如下。
1 |
|
scanf(“%c”, ch)
会读取一个 \n
。
011.字符数组和字符串
1.区别
在 C 语言中,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。
1 |
|
2.字符串越界
定义字符串时,初始化内容的长度大于定义的字符串,就会越界。
3.字符串初始化原理
"abcdef"
储存在文字常量区,a[]
内存空间在栈区,初始化时,将文字常量区的内容 "abcdef"
,拷贝到栈区 的 a[]
。
1 |
|
012.字符串处理函数
1.gets()
可以读取空格,不推荐使用。
1 |
|
2.fgets()
三个要点:
1.写入会覆盖字符串。
2.stdin 内容 < size,写入时自动增加一个换行符。
3.当 stdin 什么都没有时,由于stdin 内容 < size,写入时自动增加一个换行符。
读取回车、空格,推荐使用。
读取遇到换行符,结束本次读取。
1 |
|
3.fputs()
1 |
|
4.strlen()
计算字符串长度(不算结束符 “\0”)(“\n” 算一个字符)。
1 |
|
注意事项:
strlen()
:从首元素开始,到结束符为止的长度(不计算结束符)。
sizeof()
:计算数据类型的长度,不会因为结束符停止。
1 |
|
5.strcpy
拷贝字符串,自动加’\0’。
**重要:**遇到 ‘\0’ 停止拷贝。
1 |
|
6.strncpy
拷贝指定n个字符,不自动加’\0’。
**重要:**遇到 ‘\0’ 停止拷贝。
1 |
|
7.strcmp
判断字符串是否相同。
比较字符串大小(一个字母字母比较)。
注意:strcmp 传入参数时需保证参数不为 NULL/nullptr,因为 strcmp 实现中会直接取*,若是 NULL/nullptr,会报段错误。
1 |
|
附:字符串比较问题
1 |
|
8.strncmp
判断指定n个字母的大小。
注意:与 strcmp 一样,传入参数时需保证参数不为 NULL/nullptr,因为 strncmp 实现中会直接取*,若是 NULL/nullptr,会报段错误。
1 |
|
9.strcat
将 str2 追加在 str1 后面。
1 |
|
存疑:
1 |
|
但是若是定义a 的空间大一些,就不会错。
1 |
|
10.strncat
将 指定长度的 str2 追加在 str1 后面。
1 |
|
11.sprintf
格式化一个字符串,并输出到指定数组中。
1 |
|
12.sscanf
从字符串数组中按指定格提取内容(便于提取数字)。
(1)提取数字
1 |
|
(2)提取字符
1 |
|
13.strchr
查询字符。
1 |
|
14.strstr
(1)查询字符串
1 |
|
(2)模型|查找字符串个数
1 |
|
15.strtok
字符串分割,会更改原字符串,记得备份。
第一次调用,参数写原字符串地址。
第二次起调用,参数写NULL。
每次调用,返回值为切割的字符串地址。
1 |
|
具体样例:
1 |
|
16.atoi
将字符串转化为整型。
1 |
|
17.atof
将字符串转化为浮点型。
1 |
|
18.atol
将字符串转化为长整型。
1 |
|
013.字符串常用模型
1.两头堵模型
有一个字符串,开头或结尾含有n个空格, (" abcdefgdddd "),欲去掉前后空格,返回一个新字符串。
1 |
|
2.字符串反转模型
例如:“abcd” 变为 “dcba”。
(1)传统解法
1 |
|
(2)递归解法
非常巧妙,
strncat(buf, str, 1);
一定要放在inverse(str+1);
后面,这样才能先写入后面的数。
1 |
|
014.分文件编程
1.文件功能
main.c
调用自己的头文件。
1 |
|
my.h
放置函数声明,避免每调用一次,都要写无数条函数声明。
1 |
|
my_fun.c
写函数的具体实现。
1 |
|
2.防止头文件重复包含
1 |
|
015.指针
1.野指针
野指针:保存非法地址的指针。
非法地址:只有定义后的变量的地址才是合法地址。
1 |
|
2.空指针
1 |
|
3.指针大小
32位编辑器 用32位(4字节)存指针。
64位编辑器 用64位(8字节)存指针。
4.多级指针
1 |
|
5.*p 等价于 p[0]
1 |
|
变量a 大小为4个字节,由四个地址储存。p 是指向 a 的首地址,等价于 p[0]。
**PS:**p[1] 等价于 *(p + 1),即p加上4字节,野指针不能用。
1 |
|
6.指针步长
p+1,不是 p中所指向的地址
+ 1,而是 p中所指向的地址
+ sizeof(数据类型)
。
7.万能指针
void * 定义的指针。
1 |
|
8.const修饰指针
(1)const * 表示指针所指向的变量只读。
const 修饰 *
1 |
|
(2)* const p 表示指针变量只读。
const 修饰 p
1 |
|
(3)双const
指针和指针所指变量的值都不能修改。
1 |
|
9.形参中的数组退化为指针
形参中的 a[]
是指针,写 int *a
int a[100]
int a[]
是一样的。
1 |
|
PS:补充见 015.13.(2) 形参中不能用 char **a
代替 char a[][10]
10.字符串常量地址相同
字符串常量 放在 data 区,相同的常量,地址相同。
【每个字符串都是一个地址】,这个地址是字符串首地址。
1 |
|
11.数组名
(1)数组名是常量
数组只有定义的时候可以初始化!
1 |
|
(2)数组名b
、&b
的数据类型不一样
1 |
|
b
表示首元素的地址。b + 1
表示 首元素的地址加 4。
&b
表示整个数组的首地址。&b + 1
表示 整个数组的首地址加 4 * 10。
(3)二维数组数组名
1 |
|
a
、&a[0]
表示 第 0 行首地址。步长:sizeof(int [4])
a + i
、&a[i]
表示 第 i 行首地址。 步长:sizeof(int [4])
*(a + i)
、 a[i]
表示 第 i 行首元素地址。步长:sizeof(int)
*(a + i) + j
、 &a[i][j]
表示 第 i 行第 j 列元素的地址。步长:sizeof(int)
*(*(a + i) + j)
、 a[i][j]
表示 第 i 行第 j 列元素的值。
&a
表示 整个数组的首地址,&a + 1
表示 整个数组的首地址加 整个数组的大小。步长:sizeof(int [4] * 3)
【辨析】:
&a
:整个数组的首地址
a
、a + 0
:第 0 行首地址,&a[0]
、&(*a)
*a
、*(a + 0)
:第 0 行 第 0 个元素首地址,a[0]
**a
、*(*(a + 0) + 0)
:第 0 行 第 0 个元素的值,*a[0]
、a[0][0]
(4)二维数组的本质
二维数组的本质:一维数组;也可以看成若干个一维数组的组合。
将二维数组的每一行看作一个数组,有助于理解 数组指针。
另外,由于二维数组其实也是线性存储的,所以二维数组可以当一维数组输出。
1 |
|
(5)二维数组名的本质
二维数组名的本质:数组指针。
通过 + i
来指向若干个数组的首地址,a
表示第 0 行的首地址,a + 1
表示第 1 行的首地址。
(6)二维数组求行数、列数
1 |
|
12.指针数组
1 |
|
13.数组指针
(1)储存整个数组首地址
【一维数组】数组指针指向 一维数组 整个数组的首地址,&a
,步长为整个数组的大小。
【二维数组】每一行是一个数组,数组指针指向 一维数组 整个数组的首地址(即每一行的首地址),a
,表示第 0 行数组的首地址,步长为整个第 0 行数组的大小。
【注意】数组指针 p 的步长,是数据类型的大小,也就是整个数组的大小。
1 |
|
(2)形参中不能用 char **a
代替 char a[][10]
对于一维数组,函数形参中,写 char a[]
、char a[100]
、char *a
是一样的,均为 char *
类型的指针。
1 |
|
但是对于,二维数组,函数形参中不能用 char **a
代替 char a[][10]
。
因为 a[1]
表示 *(a + 1)
,而 a 的步长是 sizeof(char *) == 8
。我们想要的步长是 10
1 |
|
(3)改进办法1:形参中规定指针步长
void fun(char **a, int n);
改为 void fun(char a[3][10], int n);
、fun(char a[][10], int n);
。
数组做形参都会退化成指针,即 a 是一个指针,所以cahr a[3]
、 char a[]
都是 char *a
的意思,指针 a 指向 数组的首地址,(每一行就是一个数组),a + i
表示 第 i 行的首地址,即第 i 行数组的首地址。
(4)改进本法2:形参中用数组指针
void fun(char **a, int n);
改为 void fun(char (*a)[10], int n);
传入的是数组的首地址,(每一行就是一个数组),即传入的是 每一行的首地址。
1 |
|
14.指针函数
返回指针类型的函数。
1 |
|
15.函数指针
指向函数的指针。
(1)先定义函数类型,再定义指针(不常用)
1 |
|
(2)先定义函数指针类型,根据类型定义指针变量
1 |
|
(3)直接定义函数指针(常用)
1 |
|
16.函数指针数组
函数指针组成的数组。
int (*p_fun[5])() = {add, subtract, multiply, divide, exit};
大括号内写函数名。
1 |
|
没使用函数指针数组之前,制作菜单,根据命令调用函数。
1 |
|
使用函数指针之后。
1 |
|
17.回调函数
在函数中调用函数,通过传递不同的函数指针,实现同一个函数框架,调用不同的函数。
1 |
|
18.指针易错
(1)指针值传递
调用函数修改指针的值,需要传递的参数是 指针的地址,并用二级指针的来做形参,接收指针的地址。
1 |
|
(2)二级指针没有空间
一级指针没有指向内存空间,通过操作二级指针给一级指针所指内存拷贝内容。【内存污染】
1 |
|
016.main()函数参数
1 |
|
argc
储存的是 argv[]
的 数组大小
argv[]
存储的是命令 + 参数,举个例子:
1 |
|
017.内存管理
1.普通局部变量
(1)作用域:括号
(2)离开{},内存自动释放
(3)就近原则
1 |
|
2.static局部变量
(1)作用域:括号
(2)离开{},不会释放;程序结束,static局部变量才自动释放【存储在data区,程序不结束,data区数据不释放】。
1 |
|
(3)data区数据,在编译阶段已分配空间,程序还没执行,static 局部变量就存在。
(4)static 局部变量不初始,值默认 = 0。多次调用初始化语句,只会执行一次。
(5)static 局部变量只能用常量初始化。
1 |
|
3.普通和static局部变量的区别
(1)普通局部变量只有执行到定义变量的语句才分配空间;static局部变量编译时就分配空间。
(2)普通局部变量离开{},自动释放;static局部变量程序结束,自动释放。
(3)普通局部变量不初始化,值为随机数,static局部变量不初始化,值为0【初始化语句只执行一次】。
4.全局变量
(1)使用变量时,前面没有变量定义,需声明。
1 |
|
(2)分文件写,在main.c中定义,在头文件中声明。
为什么在头文件中声明? 避免函数需要进行多次声明。【见 014.分文件编程 】
**为什么在main.c中定义?**不能在头文件中定义,否则多次调用头文件时,会出现多次定义全局变量的问题。
5.static全局变量
static全局变量,只能在本文件中使用,不能在其他文件使用。
普通全局变量可以在所有文件中使用。
6.内存分区
(1)内存四区
堆区 heap
栈区 stack
- 局部变量
全局区
- 未初始化 bss
- 静态变量
- 全局变量
- 初始化 data
- 静态变量
- 全局变量
- 文字常量区
- 未初始化 bss
代码区 code
(2)内存加载顺序
当程序运行时,首先将以下几个确定的内存分区(code,data,bass)先加载:
- code(代码区):只读,函数。
- data:初始化的数据,全局变量,static 变量。
- 文字常量区(只读)。
- bss:没有初始化的数据,全局变量,static变量。
然后额外加载两个区:
- stack(栈区):普通局部变量,自动管理内存,先进后出。
- heap(堆区):手动申请空间(malloc),手动释放(free),整个程序结束,系统自动回收。
(3)栈区堆区扩展方向
栈区:从高地址向低地址扩展。
堆区:从低地址向高地址扩展
7.内存操作函数|memset
void 是万能指针,可以接收任何类型指针【见015.7.万能指针】,size_t 是无符号整型。
【特别注意】
memset(b, 0, sizeof(b))
是对 每个字节 都赋值为 0;以64位 int 为例,四个字节分别赋值为 0(最后该数字还是等于0)。
【进一步探究】
对于 char 而言,可以赋值任何字符,因为其只有一个字节。
对于 int 而言,除了 0 之外,还可赋值为 -1、0x3f3f3f。
当赋值为 -1,-1 的反码是
1111 1111 1111 1111 1111 1111 1111 1111
,存入 1 字节就是1111 1111
,每个字节都是1111 1111
,那四个字节就是1111 1111 1111 1111 1111 1111 1111 1111
,所以该 int 数字还是 -1。当赋值为 0x3f3f3f,其反码是 0x3f3f3f,存入 1 字节就是 0x3f,每个字节都是 0x3f,那四个字节就是 0x3f3f3f,所以该 int 数字还是 0x3f3f3f。
1 |
|
8.内存操作函数|memcpy
1 |
|
9.内存操作函数|memmove
**用途:**将 a[0], a[1], a[2] 所存内容移动到 a[2], a[3], a[4].(如果用 memcpy 拷贝,会发生内存重叠)。
1 |
|
10.内存操作函数|memcmp
与 strncmp 相似,**用途:**判断是否相等。
1 |
|
**补充:**不能使用 memcmp 来判断 strcut 结构体是否相等。
因为结构体存在内存对齐,内存对齐的那几个字节的值是随机的。
1 |
|
再补充:如果一定要用 memcmp 比较,需要先用 memset 将 A、B 置 0,再赋值,才能比较。因为 memset 将字节对齐的那部分也置于 0 了。
再再补充:结构体比较是否相等,用重载 ==。
11.内存释放
内存释放只能释放一次,释放多次会发生段错误。
内存泄漏: 动态分配空间,不释放空间。
内存污染: 非法使用内存(操作野指针所指向的内存、堆区越界)
1 |
|
补充:
第五行,null 代表着,该指针被释放;未被赋值为 null,代表着指针所指空间未被释放。
未被赋值为 null 的指针,容易造成二次释放。已释放的空间,二次释放,将出现错误。
12.堆区空间越界
malloc
分配的空间 < 操作的空间,会发生内存污染。
1 |
|
13.堆区数组
1 |
|
14.内存污染|返回栈区地址
(1)问题
函数执行完,自动释放栈区内存空间,(可以返回结果,不能返回空间地址)。
错误案例01:
1 |
|
错误案例02:
1 |
|
(2)改进
将函数体内局部变量定义为 static ,储存在 data 区,程序结束前,不会释放。
1 |
|
15.内存泄漏|值传递
(1)问题
传递的是指针的值(NULL),函数中申请堆区空间,tmp 指向堆区,但是 p 并没有指向堆区。
1 |
|
(2)改进|返回堆区空间
1 |
|
16.非法使用内存导致错误
非法使用指针,造成内存污染。例如:
1 |
|
17.如何避免非法使用内存
定义一个指针后,先指向一块内存再使用!
指向栈区内存(即指向普通变量)
1 |
|
指向堆区(malloc申请空间)
【堆区空间 记得 free()】
1 |
|
3.指向文字常量区
1 |
|
018.结构体
复合类型
1.格式
结构体类型定义,右括号有分号!
1 |
|
和数组一样:
- 结构体变量只有定义的时候可以初始化。
- 初始化使用大括号,而且有分号。
struct student s1 = {18, "Tom"};
【易错】没初始化,不能操作 字符数组名(line 4)
1 |
|
2.不常用定义方法
1 |
|
3.结构体数组初始化
1 |
|
4.结构体嵌套
1 |
|
5.结构体的值传递和地址传递
值传递,是直接内存拷贝,效率不高;地址传递效率更高。
1 |
|
6.结构体指针套一级指针
1 |
|
7.结构体指针数组套二级指针
用一个指针指向 导师结构体数组,表示有 teacher_num 个导师,一个导师有 student_num 个学生。用二级指针指向学生数组。
1 |
|
8.结构体的深拷贝与浅拷贝
浅拷贝(Shallow Copy):结构体中嵌套指针,而且动态分配空间,同类型结构体变量赋值,不同结构体成员指针变量指向同一块内存。
深拷贝(Deep Copy):人为申请空间,重新拷贝堆区内容。
1 |
|
9.结构体内存
由于结构体存在字节对齐,所以结构体变量的内存大小,大于其成员变量的内存大小之和。
**注意:**当结构体变量更改顺序时,所占内存大小不一样。
1 |
|
(1)结构体偏移量
结构体类型定义下来,内部的成员变量的内存布局已经确定下来。
(2)结构体对齐规则
为什么要对齐?
便于查找,提高存取数据的速度,利用空间换时间。
比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需1个读取周期(以32位系统为例,CPU读取内存时,一次读取32位,4个字节)即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。
结构体的对齐参数
以结构体中最长字节的变量对齐。利用偏移量来对齐,偏移量按照类型大小成倍增加。
举个例子
1 |
|
系统默认以最长字节的类型的大小来对齐:8 个字节。
变量 | 偏移量 sizeof(类型) * 倍数 | 所占位置 | 首地址 |
---|---|---|---|
char c | 1 * 0 = 0 | 1 | 1 |
double d | 8 * 1 = 8 | 9 - 16 | 9 |
short s | 2 * 8 = 16 | 17 - 18 | 17 |
int i | 4 * 5 = 20 | 21 - 24 | 21 |

字节对齐可以程序控制,采用指令。但对齐参数不能大于最长字节的类型大小。
1 |
|
当系统以 4 个字节对齐。
当最长类型大小 > 对齐参数,按照对齐参数计算偏移量。
变量 | 偏移量 sizeof(类型) * 倍数 | 所占位置 | 首地址 |
---|---|---|---|
char c | 1 * 0 = 0 | 1 | 1 |
double d | 4 * 1 = 4【最长类型大小 > 对齐参数】 | 5 - 12 | 5 |
short s | 2 * 7 = 14 | 15 - 16 | 15 |
int i | 4 * 4 = 16 | 17 - 20 | 17 |

当系统以 1 个字节对齐。
(3)特殊情况|当结构体成员为数组时
当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是 将数组的每个元素当一个成员来分配,其他分配规则不变。比如 int a[5]
,当作 5 个 int
类型来处理。
(4)特殊情况|当有不完整类型时
1 |
|
【要点】:
对于位段成员,依旧按类型分配空间。
同类型的、相邻的位段成员,可当作一个类型变量来处理。比如
a1:5
a2:9
可以当作一个int
变量处理。
系统默认以 4 个字节来对齐:
a1:5
表示 占 5bit,a2:9
表示 占 9bit,加起来一共 16bit,未超过 int
的 4 个字节。所以将 a1:5
a2:9
放在一个 int
空间。由于 b:4
和 s
的类型不一样,所以不能放在一起,b:4
依旧占 4 个字节。
10.typedef
1 |
|
11.比较结构体是否相等
不可使用 memcmp 判断,因为用于内存对齐的几个字节是垃圾值。
使用重载 == 操作符。
019.共用体(联合体)、枚举
1.共用体
1 |
|
2.枚举
1 |
|
020.文件操作
1.缓冲区
ANSI C标准采用“缓冲文件系统”处理数据文件。
(1)为什么要有缓冲区?
提高交互效率
fputs()
把内存的值写入文件:内存 -> 缓冲区 -> 缓冲区(缓冲区满了,或者程序结束) -> 屏幕(标准输出文件)。
fgets()
从文件读取数据:键盘(标准输入文件) -> 缓冲区 -> 缓冲区(缓冲区满了,或者程序结束)-> 内存。

(2)缓冲区写入文件的条件
【标准设备文件】
stdin
stdout
stderr
可以实时读写。【缓冲区满了】但是缓冲区不同系统大小不一样,缓冲区相当于一个固定大小的字符串。
【程序结束】程序正常关闭,缓冲区的内容,自动写入文件。
【关闭文件】
fclose(fp)
文件正常关闭,缓冲区的内容,自动写入文件。【手动刷新缓冲区】
fflush(fp)
仅限 Linux。文件不关闭,程序没结束,实时刷新缓冲区。
(3)刷新缓冲区|fflush()
1 |
|
【linux系统下,刷新缓冲区】:当缓冲区积累的内容没有足够多,或者程序未结束,缓冲区的内容还未写入文件,需要手动刷新一下缓区。
1 |
|
2.文件指针
1 |
|
fp指针 是结构体指针,结构体内部是,若干个数据成员,保存文件状态等各种信息(不必关心内部)。
fp指针,调用
fopen()
时 ,在堆区分配空间,地址返回给 fp,【故 fp指针 不指向文件,而是指向堆区的结构体,该结构体用来储存文件状态等信息】。通过文件库函数操作 fp指针,来修改该结构体内部数据成员。
1 |
|
3.文件分类
(1)分类
磁盘文件:硬盘中的文件
- 文本文件(遇到 -1或特殊字符会结束)
- 二进制文件
设备文件
- 键盘
- 屏幕
(2)win 和 linux 文本文件的区别
win 文本文件的换行符是 “\r\n”
linux 文本文件的换行符是 “\n”
ps:所以记事本打开会不换行。
win 下读写 会自动转换换行符。
在 win 下,读取文本文件时,系统将 “\r\n” 转化为 “\n”
写入文件时,系统将 “\n” 转化为 “\r\n”
4.文件操作流程
打开文件fopen()
读写文件
- 按 字符 读写 fgetc()、fputc()
- 按 字符串(行)读取文件 fgets()、fputs()
- 文件结尾判断 feof() 【end of file】
关闭文件 fclose()
5.标准设备文件指针
stdin:标准输入
stdout:标准输出
stderr:标准出错,
perror
函数输出错误信息
补充:
fclose() 关闭文件
perror() 打印库函数调用失败原因
- 失败:Bad file descriptor
- 成功:Sucess 或者 不显示信息
1 |
|
6.文件打开和关闭
(1)fopen()
fp指针,调用 fopen()
时 ,在堆区分配空间,地址返回给 fp。失败返回 NULL。
<1> 路径
1 |
|
**Tip:**如果路径字符串过长,可以用续行符。
1 |
|
<2> 权限
w
:【写入】如果文件不存在,新建;如果文件存在,清空内容再打开。
r
:【只读】如果文件不存在,打开失败。
a
:【追加】如果文件不存在,新建;如果文件存在,光标自动放在文件最后末尾。
r+
:【读写】允许读写,文件必须存在
w+
:【读写】允许读写,文件不存在,则创建,文件存在,则清空
rb:window 平台下,r或者rt 读文本文件,rb 表示二进制文件【linux 写 rb 不影响】。
wb:window 平台下,w或者wt 写文本文件,wb 表示二进制文件【linux 写 wb 不影响】。
<3> 注意
编译的同时运行程序,相对路径不一定相对于源代码路径。
直接运行程序,相对路径是相对于可执行程序。
(2)fclose()
1 |
|
7.写文件|fputc()、fputs()
1 |
|
将 ch 转换为 unsigned char 后,写入 stream 指定文件中。
ch:需要写入文件的字符
stream:文件指针
返回值:**
成功:返回读到的字符
失败:-1
流程:写入缓冲区 -> 写入硬盘中的文件。
1 |
|
PS:写文件|fputs() 见 012.3 fputs()
8.读文件|fgetc()、fgets()
1 |
|
从 stream 指定的文件中读取一个字符。
返回值:
成功:返回读到的字符
失败:-1
1 |
|
PS:读文件|fgets() 见 012.2 fgets()
当 fgets() 读到文件结尾,读取失败,保存上一次读取的值(按行读取,也就是上一行的值)。
9.判断文件结尾|feof()
feof() 任何文件都能判断。
文本文件,结尾有一个隐藏的 -1,因为ascii码中没有 -1 ,EOF(end of)的宏定义为 -1。
1 |
|
二进制文件不能用 -1 判断结尾。
任何文件都能判断是否结尾,如果到文件结尾,feof() 返回真。
1 |
|
原理: 判断的是,光标前一个字符是否是最后一个字符。
对于空文件,应该先读取,让光标后移,才能判断是否结尾。
对于文本文件,读取到 隐藏字符 -1,feof() 才能判断结尾。
1 |
|
10.格式化|fprintf()
1 |
|
11.格式化|fscanf()
1 |
|
读取失败时(读到末尾,边界值),自动保存上一次读取的值。
1 |
|
12.按块读写文件
(1)fwrite()
1 |
|
返回值:
成功:返回写入内容的块数目 number,(不是文件总大小)。
失败:返回 0。
参数:
ptr:准备写入文件数据的地址
size:往文件写入内容的块大小,数据类型的大小
number:往文件写入内容的块数目
stream:操作的文件指针
总大小:
size * number
是总大小,所以 size 和 number 的参数位置调换,没有影响。
1 |
|
(2)fread()
**。返回值:**同 fwrite()
**参数:**同 fwrite()
注意:
文件内容 > 用户指定总大小
size * number
,返回值为用户指定大小的块数目。文件内容 < 用户指定总大小
size * number
,返回值为实际读取的块数目,或者为 0(比如,number = 0.2时,返回为 0 )。
1 |
|
(3)返回值与边界问题
对于二进制文件,所读取的字符串,不要用
strlen()
判断长度。
读文件时,应该用
ret == 0
做是否读到的判断,不应该用strlen() == 0
做是否读到的判断,因为有些特殊字符会导致出现‘/0’
。
1 |
|
对读到的字符串进行处理,应该用
i < ret
做循环,不应该用i < strlen()
做循环,因为有些特殊字符会导致出现‘/0’
。
1 |
|
写文件时,应该读多少写多少,避免越界。
1 |
|
13.光标移动|fseek()
**功能:**移动文件光标
1 |
|
参数:
offset:偏移量
whence:
- SEEK_SET:从文件开头移动 offset 个字节。
- SEEK_CUR:从当前位置移动 offset 个字节。
- SEEK_END:从文件末尾移动 offset 个字节。
1 |
|
14.获取光标位置|ftell()
返回的长整型为,光标距离开头的位置。
1 |
|
15.光标回到开头|rewind()
1 |
|
16.应用:获取文件大小
读方式打开文件。
光标移到文件结尾。
返回文件位置。
关闭文件 或者 恢复光标
1 |
|
021.链表
1.链表和数组的区别
链表:
不需要一块连续的储存区域
删除和插入元素高效
数组:
随机访问元素效率高
2.分类
(1)动态链表和静态链表
类似于,动态数组和静态数组,储存结构的两种不同表示方式。
1 |
|
**静态链表:**所有结点程序定义,不是临时申请的,无需释放。
**动态链表:**一个个临时申请的结点和输出的数据。建立的前后相连的关系。
1 |
|
(2)带头链表和不带头链表
**带头链表:**头结点固定,储存的并不是有效数据,可能结点的个数。有效结点从第二个开始,若需在头部插入新结点,插在头结点和第二个结点之间即可。
**不带头结点:**头结点不固定,若需在头部插入新结点,新结点指针指向头结点即可。
(3)单向链表和双向链表
(4)循环链表
3.增删改查
1 |
|
4.交换节点
交换节点 = 交换结构体储存内容 + 交换指针指向。
1 |
|
PS:如果数据域只有一个,直接交换数据域。
1 |
|
PSS:如果数据域有多个,可以封装成一个结构体 data,在交换 data。
1 |
|
5.链表反转
先用 p_next 保存第 3 个节点地址。
在将第 2 个节点(p_cur)的指针域指向第 1 个节点(p_pre)。
p_pre、p_cur 整体后移
1 |
|
022.常见错误
1. Segmentation Fault
段错误,主要原因是 操纵了非法内存。
常见情景有:
数组越界
1 |
|
链表越界
1 |
|
指针操纵非法内存
1 |
|