导图社区 C语言复习思维导图
《C生万物 · 包罗万象》 你值得拥有,C语言是一门通用的计算机语言,C语言是有国际标准的,比如: C89,C90,C99,C11,当前使用最多的还是C89,C90。
编辑于2023-07-19 09:16:37 浙江省C生万物 · 包罗万象
主题
字符串 + 内存函数
求字符串长度
strlen()
功能:可以求解字符串的长度
注意事项
① 参数指向的字符串必须要以 ‘\0’ 结束
② 注意函数的返回值为size_t,是无符号的( 易错 )
③ strlen()函数返回的是'\0'之前的字符个数
⭐模拟实现
1. 计数器实现
2. 递归实现
3. 指针相减
长度不受限制的字符串函数
strcpy()
功能:可以将一个字符串拷贝到另一个字符串中
注意事项
① 源字符串必须以 '\0' 结束,因为源字符串中的 '\0' 会被拷贝到目标空间
② 目标空间必须足够大,以确保能存放源字符串
③ 目标空间必须可变
⭐模拟实现
strcat()
功能:可以拼接两个字符串
注意事项
① 源字符串必须以 '\0' 结束
② 目标空间必须足够大,能容纳下源字符串的内容
③ 目标空间必须可变
④ 不可以给自己做追加【会产生覆盖的情况】
⭐模拟实现【前半部分即为strcpy()的逻辑】
strcmp()
功能:可以比较两个字符串
⭐模拟实现
长度受限制的字符串函数
strncpy()
功能:可以进行指定字符的拷贝
⭐模拟实现
strncat()
⭐模拟实现
功能:可以追加n个字符
strncmp()
⭐模拟实现
较为复杂,有兴趣可以学习一下。。。
功能:可以比较n个字符
字符串查找函数
strstr()
十大经典算法之KMP,也可以用做子串的匹配
strtok()
将字符串按照记号进行拆分
错误信息报告函数
strerror
功能:获取指向错误消息字符串的指针
可以给函数传递参数【errno】
#include <errno.h>
字符操作函数
需要使用到的时候去查文档即可
内存操作函数
memcpy()
功能:拷贝内存块
⭐memmove()
功能:移动内存块
memset()
功能:填充内存块
memcmp()
功能:比较两个内存块
自定义类型
结构体
结构体的声明
结构是一些值的集合,这些值称为成员变量
结构的每个成员可以是不同类型的变量
特殊的声明【匿名结构体】
特点
在声明结构的时候,可以不完全的声明,省略掉了结构体标签
两个成员完全相同的匿名结构类型,不是同一个类型,所以不可以形成引用
只能在声明的时候用一次,后面就不能再用了,这个语法形式和C++中的【匿名对象】很类似,声明周期只在当前行,进入下一行就会调用析构函数
结构体的自引用
结构体变量的定义和初始化
定义
直接在声明的时候定义
脱离结构体进行定义
初始化
结构体成员的访问
点操作符[.]
箭头操作符[->]
结构体内存对齐
结构体偏移量计算
offsetof
规则介绍
1. 第一个成员在与结构体变量偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
VS中默认的值为8、Linux环境默认不设对齐数(对齐数是结构体成员自身的大小)
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
为什么存在内存对齐?
① 平台原因(移植原因)
② 性能原因
总体来说 —— 结构体的内存对齐是拿空间来换取时间的做法
如何修改默认对齐数
#pragma pack(默认对齐数)
#pragma pack()
一道百度笔试题: offsetof 宏的实现
结构体传参
结构体传参的时候,要传结构体的地址
函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销
只传递这个结构体的地址过去的话,函数内部便可以使用结构体指针访问到所有的内容,以便节省开销
位段
位段的声明和结构是类似的,有两个不同:
位段的成员必须是 int、unsigned int 或signed int
位段的成员名后边有一个冒号和一个数字
位段的内存分配
位段的成员可以是 int、unsigned int、signed int 或者是 char (属于整型家族)类型
位段的空间上是按照需要以4个字节 [int] 或者1个字节 [char] 的方式来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
内存图分析位段分布
位段的跨平台问题
int 位段被当成有符号数还是无符号数是不确定的
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
位段的应用
枚举
枚举类型的定义
可能取值都是有值的,默认从0开始,依次递增1
在定义的时候也可以赋初值,后面的取值则从这个值开始往下递增
枚举的使用
只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
枚举的优点
1、增加代码的可读性和可维护性
2、和#define定义的标识符比较枚举有类型检查,更加严谨
3、防止了命名污染(封装)
4、便于调试(宏无法调试)
5、使用方便,一次可以定义多个常量
联合体
联合体类型的定义
联合体的特点
类型定义的变量也包含一系列的成员
联合体内部的成员都是共用一块地址空间(所以联合体也叫共用体)
联合体大小的计算
联合的大小至少是最大成员的大小
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
动态内存管理
为什么存在动态内存分配
静态管理的缺陷
1. 空间开辟大小是固定的
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配
动态管理
有时候我们需要的空间大小在程序运行的时候才能知道,这时候就只能试试动态内存开辟了
动态内存函数
声明在【stdlib.h】这个头文件中
malloc
特点
如果开辟成功,则返回一个指向开辟好空间的指针
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查【perror】
返回值的类型是void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定 —— 要做强转
如果参数size为0,malloc的行为是标准是未定义的,取决于编译器【开辟0个空间毫无意义】
free
特点
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的 —— 不可以去free除动态开辟以外的空间
如果参数 ptr 是NULL指针,则函数什么事都不做
注意事项:在free完之后一定要去及时释放,否则这个指针将会变为野指针
calloc
特点
为 num 个大小为 size 的元素开辟一块空间
并且把空间的每个字节初始化为0【与malloc的本质区别】
realloc
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置
realloc扩容机制
本地扩容
在本地就有足够的空间可以扩容,此时直接在后面续上新的空间即可
异地扩容
当后边没有足够的空间可以扩容,realloc函数会找一个满足空间大小的新的连续空间。把旧的空间的数据,拷贝到新空间的前面的位置,并且把旧的空间释放掉(无需手动释放),同时返回新的空间的地址
实际应用:数据结构之【顺序表】与【顺序栈】
常见的六种动态内存错误
1、对NULL指针的解引用操作
2、对动态开辟空间的越界访问
3、对非动态开辟内存进行free释放
4、使用free释放一块动态开辟内存的一部分
5、对同一块动态内存多次释放
6、动态开辟内存忘记释放(内存泄漏)
历年经典的笔试题分析【⭐】
题目一
题目二
题目三
题目四
C/C++程序的内存分布原理
C/C++内存分布
栈区
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区
一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。堆区主要进行动态内存分配,堆区的内存大小不固定,可以根据需要动态分配和释放
共享段库
① 文件映射【通过使用函数如mmap()或CreateFileMapping()】
② 动态库【包含可以被程序在运行时动态加载的代码和数据】
③ 匿名映射【将内存映射到进程地址空间的方式,而不是映射具体的文件】
静态区/数据段
存放全局变量、静态数据。程序结束后由系统释放。其在程序编译时就确定了变量的存储空间大小和内存地址,具有固定的大小和位置
代码段
存储程序指令(代码)的一块内存区域,也被称为文本段(Text Segment),代码段通常是只读的,因为程序指令一般不应该被修改,代码段中存放函数体(类成员函数和全局函数)的二进制代码
柔性数组
柔性数组的特点
1、sizeof 返回的这种结构大小不包括柔性数组的内存
2、结构中的柔性数组成员前面必须至少一个其他成员
3、包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
柔性数组的使用
柔性数组的优势
文件操作
为什么使用文件?
为了做到让数据持久化存储
什么是文件?
指的是磁盘上的文件
程序文件
包括源程序文件(后缀为.c)
目标文件
Windows环境后缀为.obj
Linux环境后缀为.o
可执行程序
windows环境后缀为.exe
Linux环境后缀为.out
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用
文件的打开和关闭
文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息
文件的名字,文件状态及文件当前的位置
这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名【FILE】
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节
一般都是通过一个FILE的指针来维护这个FILE结构的变量
通过文件指针变量能够找到与它关联的文件
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件
文件的打开和关闭【⭐】
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
打开方式
文件的顺序读写【重点掌握】
8个重要的库函数
单字符输入输出
fputc()
将一个字符写到输出流
fgetc()
从输入流读出内容进行返回
文本行输入输出
fputs()
将一个字符串写到输出流
fgets()
从输入流读取指定长度的字符串
格式化输入输出
fprintf()
将指定的格式化内容写到输出流中
fscanf()
从输入流中读取指定的格式化内容
二进制输入输出
fwrite()
将指针ptr所指向的内存块中count个大小为size个字节的数据写到文件输出流中
fread()
从文件输入流中读取指针ptr所指向的内存块中count个大小为size个字节的数据
注意在读和写切换的时候文件的打开形式也要随之切换
对比一组函数
scanf / fscanf / sscanf
printf / fprintf / sprintf
sprintf
sscanf
文件的随机读写
fseek
根据文件指针的位置和偏移量来定位文件指针
origin
SEEK_SET
Beginning of file
SEEK_CUR
Current position of the file pointer
SEEK_END
End of file *
ftell
返回文件指针相对于起始位置的偏移量
rewind
让文件指针的位置回到文件的起始位置
文本文件和二进制文件
二进制文件
数据在内存中以二进制的形式存储。不加转换地输出到外存
文本文件
以ASCII字符的形式存储的文件。在外存上以ASCII码的形式存储,则需要在存储前转换
数据在内存中一般都是怎么存储的
字符一律以ASCII形式存储
数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储
例如一个数值10000,以二进制的形式存放在内存中为10 27 00 00,占4个字节; 以ASCLL码的形式存放到内存中的,占5个字节1(49) 0(48) 0(48) 0(48) 0(48)
文件读取结束的判定
被错误使用的feof
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束
四个函数的结束判断解读
fgetc
如果读取正常,返回读取到的字符的ASCLL码值
如果读取失败,返回EOF
fgets
如果读取正常,返回读取到的数据的地址
如果读取失败,返回NULL
fscanf
如果读取正常,返回的是格式串中指定的数据个数
如果读取失败,返回的是小于格式串中指定的数据个数
fread
如果读取正常,返回的是等于要读取的数据个数
如果读取失败,返回的是小于要读取的数据个数
文件缓冲区
ANSIC 是标准采用【缓冲文件系统】处理的数据文件的
缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据【写】会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上
如果从磁盘向计算机【读】入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
缓冲区的大小根据C编译系统决定的
C语言在操作文件时,需要做刷新缓冲区或者在文件结束时关闭文件
程序环境和预处理
翻译环境
编译的几个阶段
在这个环境中源代码被转换为可执行的机器指令
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
预处理
gcc -E test.c -o test.i
预处理之后就停下来,预处理之后的结果都放在test.i这个文件中
会完成的事
头文件展开
宏替换
条件编译
去注释
⭐预处理详解
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
#define
#define定义标识符
注意:在#define定义标识符的时候,后面不要加“;”
#define定义宏
允许把参数替换到文本中,这种实现通常称为宏或定义宏
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
用于对数值表达式进行求值的宏定义都应该加上括号(),避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
#define替换规则
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
注意
① 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
② 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
# 和 双#
#的使用
#define a 10 PRINT("%d\n", a);
输出的结果为【the value of a is 10】
使用#,可以把一个宏参数变成对应的字符串
写在一起的字符串是会自动连接的
##的使用
int CentOS = 7; printf("%d\n", CAT(Cent, OS));
输出的结果为【7】
##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果
#define MAX(a, b) ((a) > (b) ? (a) : (b))
x+1; //不带副作用
x++; //带有副作用
宏和函数的对比
优势
宏比函数在程序的规模和速度方面更胜一筹。
更为重要的是函数的参数必须声明为特定的类型。宏则与类型无关的
宏有时候可以做函数做不到的事情
劣势
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。【宏可以使用反斜杠换到下一行继续写,可以像函数一样写很多】
宏是没法调试的。【这点是致命的】
宏由于类型无关,也就不够严谨。 【任何类型都可以传入】
宏可能会带来运算符优先级的问题,导致程容易出现错。【加括号太麻烦了!!!】
几项对比
命名规则
一般来讲函数的宏的使用语法很相似,语言本身没法帮我们区分二者
代码习惯
把宏名全部大写
函数名不要全部大写
#undef
功能:移除一个宏定义
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
如果在源文件中没有给出sz的初始化值,那可以在外部命令行的部分给出
条件编译
在编译一个程序的时候将一条语句(一组语句)编译或者放弃
文件包含
#include 指令可以使另外一个文件被编译
头文件被包含的方式
本地文件包含
查找策略
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件
如果还是找不到,就提示编译错误 —— 简单来说,会查找两次
库文件包含
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
总结
用 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索,只查找一次)
用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索,会查找两次)
嵌套文件包含
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复
解决方案
每个头文件这样写
每个头文件开头写
其他预处理指令
error
pragma
line
编译
gcc -S test.i -o test.s
编译完之后就停下来,结果保存在test.s中
会完成的事
将C语言的代码转换为汇编代码
语法分析
词法分析
语义分析
符号汇总
汇编
gcc -c test.s -o test.o
汇编完之后就停下来,结果保存在test.o中
会完成的事
将汇编指令转换为二进制指令(需要特定的文本阅读器)
形成符号表(没错,就这个功能)
链接
会完成的事
符号表的合并和重定位
兑现承诺,根据函数的声明寻找定义
如果有调用库函数的话,就需要通过动态链接去链接库里找到这个函数的定义
执行环境
调试技巧
什么是Bug?
“1949 年 9 月 9 日,我们晚上调试机器的时候,开着的窗户没有纱窗,机器闪烁的亮光几乎吸引来了世界上所有的虫子。果然机器故障了,我们发现了一只被继电器拍死的飞蛾,翅膀大约 4 英寸。”
调试是什么?有多重要?
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
一名优秀的程序员是一名出色的侦探
调试的基本步骤
1. 发现程序错误的存在
2. 以隔离、消除等方式对错误进行定位
3. 确定错误产生的原因
4. 提出纠正错误的解决办法
5. 对程序错误予以改正,重新测试
Debug和Release
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用
Windows环境下VS调试介绍
调试环境的准备
环境中选择 Debug 选项,才能使代码正常调试
调试快捷键
F5 - 启动调试,经常用来直接跳到下一个断点处
F9 - 创建断点和取消断点
熟知这个快捷键,我们便可以在程序的任意位置设置断点,便可以使得程序在想要的位置随意停止执行,继而一步步执行下去
F10 - 逐过程,通常用来处理一个过程
一个过程可以是一次函数调用,或者是一条语句。
F11 - 逐语句,就是每次都执行一条语句
可以使我们的执行逻辑进入函数内部
Ctrl + F5 - 开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用
调试的时候查看程序当前信息
查看临时变量的值
查看内存信息
查看调用堆栈
查看汇编信息
查看寄存器信息
Linux环境下GDB调试介绍
了解即可
经典案例分析
问题代码段1 —— 阶乘之和
问题代码段2 —— 越界的危害
Coding技巧 —— 如何写出优秀的代码
优秀代码的特征
代码运行正常bug很少
效率高
可读性高、可维护性高
注释清晰、文档齐全
常见的Coding技巧:
使用assert
尽量使用const
添加必要的注释
避免编码的陷阱
辨析常量指针和指针常量
对于【常量指针】而言,是将const放在[*]左边的,指针所指向的内容不能通过指针来修改,但指针变量本身可修改
对于【指针常量】而言,是将const放在[*]右边的,指针变量本身的指向不能修改,但是指针指向的内容可以通过指针来修改
模拟实现库函数strcpy之梅开n度
在写代码思考问题的时候要做到细致,考虑问题的时候要全面
编程常见的错误
编译型错误
通常指的是程序编译过程中出现的错误,这类错误往往是由于程序员在代码中违反了编程语言的语法或语义规则导致的
链接型错误
通常指的是程序在链接过程中出现的错误,链接是将编译产生的各个对象文件和所需的库文件合并,生成最终的可执行程序的过程
运行时错误
通常指的是程序在运行过程中出现的错误。这类错误不能在编译时发现,只有在程序实际运行时才会出现
数据的存储
数据的类型
类型的意义
使用这个类型开辟内存空间的大小(大小决定了使用范围)
如何看待内存空间的视角
类型的基本归类
整型家族
char、short、int、long【都可以分为signed和unsigned】
浮点数家族
float、double
构造类型
数组类型
结构体类型 struct
枚举 enum
联合 union
指针类型
int*
char*
float*
void*
空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
整型在内存中的存储
原码、反码、补码
计算机中的整数的表现形式有3种,即原码、反码、补码
三种表示方法均有符号位和数值位两部分
符号位都是用 "0" 表示正,用 "1" 表示负
正数原、反、补都相同
数值位负整数三种表示方法各不同
原码
直接将二进制转为十进制即可
反码
原码除符号位外其他位依次按位取反即可
补码
反码 + 1
原码与补码的转换形式
原码到补码:原码取反,+1得到补码
补码到原码
1. 补码 - 1,取反得到原码
2. 补码取反,+1得到原码
计算机内部的存储编码
对于整形来说:数据存放内存中其实存放的是补码。
原因
1. 原码的加法却异常复杂
2. 使用补码,可以将符号位和数值域统一处理
3. 加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
补码存储的顺序 —— 大小端
大端存储模式
数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端存储模式
数据的低位保存在内存的低地址中,而数据的高位, 保存在内存的高地址中;
⭐判断当前机器是大端还是小端
数据范围的介绍
char与signed char数据范围
-128 ~ +127
unsigned char数据范围
0 ~ 255
原码、反码、补码数据范围对比
原、反码:-127 ~ +127
补码:-128 ~ +127
七道经典大厂历年笔试题
浮点型在内存中的存储
国际标准IEEE(电气和电子工程协会) 754规定的二进制浮点数形式
(-1)^S * M * 2^E
(-1)^S 表示符号位,当S=0,V为正数;当S=1,V为负数
M表示有效数字,1<= M < 2(1.xxxxxx)
2^E 表示指数位
32位的浮点数
最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
64位的浮点数
最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
对有效数字M和指数E的特别规定
M舍去第一位的1,只保留小数部分;等到读取的时候,再把这一位加上即可
E为无符号整数
存入内存时E的真实值必须再加上一个中间数
对于8位的E,这个中间数是127(0 ~ 255)
对于11位的E,这个中间数是1023(0 ~ 2048)
指数E从内存中取出的情况
E不全为0或不全为1
指数E的计算值减去127(或1023),得到真实值
有效数字M前加上第一位的1
E全为0
浮点数的指数E等于1-127(或者1-1023)即为真实值
有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数 —— 这样做是为了表示±0,以及接近于0的很小的数字
E全为1
如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
操作符
算术操作符
/ 操作符叫做【整除】,若是两边都是整数,则执行执行 整数除法;只要有浮点数执行的就是浮点数除法
% 操作符叫做【取余】,两个操作数必须为整数。返回的是整除之后的余数
移位操作符
前言小知识 —— 原码、反码、补码
正数的原、反、补码都相同
负数的反码为原码除符号位外取反,补码为反码 + 1
左移操作符<<
【移位规则】:左边抛弃、右边补0
右移操作符>>
① 逻辑移位:左边用0填充,右边丢弃
② 算术移位:左边用原该值的符号位填充,右边丢弃
注意事项
逻辑右移主要对无符号数进行的操作
移位操作符的操作数只能是整数
移位操作是运算,不影响操作数变量内的数据
对于移位运算符,不要移动负数位,这个是标准未定义的
位操作符
& 按位与
【规则】:全1为1,有0为0
| 按位或
【规则】:有1为1,全0为0
^ 按位异或
【规则】:相同为0,相异为1
~ 按位取反
【规则】:1变0, 0变1
注:他们的操作数必须是整数
两道很变态的面试题
① 两数交换
② 进制定位
赋值操作符
连续赋值操作
a = b = c
复合运算符
+=
-=
*=
/=
%=
>>=
<<=
^=
单目操作符
分类细述
! 逻辑取反
[真变假,假变真]
- 负值
+正值
若是有两个操作数的话,则变为双目操作符,进行四则运算
& 取地址
任何变量在内存中都是有它们各自的地址的
* 解引用
一般只能对指针进行解引用的操作
指针处使用得多
sizeof() —— 是一个操作符,并不是函数
sizeof计算的值使用【%zu】来进行打印
sizeof后可省略() ——> 更加有力地证明它是一个操作符
sizeof()内部表达式不参与计算
sizeof(s = a + 2),计算的还是s在内存中所占的字节大小
sizeof与数组
sizeof(数组名)计算的是整个数组的大小
关系操作符
>
>=
<
<=
!=
==
要写if(10 == a),而不能写if(a == 10),可能会少加等号变成赋值
逻辑操作符
&& 按位与
表达式两边均为真才是真,前一项为假,则不计算后一项表达式
|| 按位或
表达式两边有一边为真即为真,前一项为真,则不计算后一项表达式
条件操作符
若表达式1为真,则表达式2为最终的结果,表达式3不计算,否则表达式2不计算
逗号表达式
exp1, exp2, exp3, ... expN
逗号表达式,就是用逗号隔开的多个表达式
从左向右依次执行。整个表达式的结果是最后一个表达式的结果
下标引用、函数调用和结构成员
1. [ ] 下标引用操作符
2. ( ) 函数调用操作符
3. 访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
表达式求值
注意点
表达式求值的顺序一部分是由操作符的优先级和结合性决定
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
隐式类型转换
整型提升
有符号数的整型提升 —— 高位补充符号位
无符号数的整型提升 —— 高位补充0
整型截断
【概念】:将一个较大范围的整数值赋给一个较小范围的整数类型时,超出目标类型范围的部分被截断丢弃的现象
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行
如果两个操作数中的一个是int类型,另一个操作数会被转换为int类型
如果两个操作数中的一个是short类型,另一个操作数会被转换为short类型
如果两个操作数中的一个是double类型,另一个操作数会被转换为double类型
寻常算数转换
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算
例如【int】和【unsigned int】一起进行算术运算的时候这个前者就要转换为后者的类型
例如【long int】和【long double】一起进行算术运算的时候这个前者就要转换为后者的类型
但是算术转换要合理,要不然会有一些潜在的问题
操作符的属性
操作符的优先级
操作符的结合性
是否控制求值顺序
两个相邻的操作符先执行哪个?—— 取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性
问题表达式
5个问题表达式
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的
指针
【指针初阶 · 入门篇】
指针是什么?
指针、地址、内存
在内存中,指针是内存中一个最小单元的编号
在日常中,指针通常指的是指针变量,是用来存放内存地址的变量
指针、内存、地址这三者其实是等价的
指针与变量
int* pa = &a;
一个指针变量可以保存一块相同数据类型变量的地址
解答:为何指针均为4个字节?
与当前程序所运行的平台有着很大的关系,指针的大小在32位平台是4个字节,在64位平台是8个字节
指针的进一步理解
指针和指针类型
指针的类型决定了指针向前或者向后走一步有多大
指针的解引用
⭐指针存在的意义1:访问字节的范围
指针类型 决定了指针在进行解引用操作的时候能访问几个字节【权限有多大】
char* 的指针,解引用访问1个字节
int* 的指针,解引用访问4个字节
double* 的指针,解引用访问8个字节
⭐指针存在的意义2:类型决定步长
指针类型 决定了指针的步长(向前 / 向后走一步都多大距离)
char* 的指针 + 1【跳过一个字符型,也就是向后走1个字节】
short* 的指针 + 1【跳过一个短整型,也就是向后走2个字节】
int* 的指针 + 1【跳过一个整型,也就是向后走4个字节】
double* 的指针 + 1【跳过一个浮点型,也就是向后走8个字节】
野指针
【概念】: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
① 指针未初始化
② 指针越界访问
③ 指针指向的空间释放
如何规避野指针
指针初始化
小心指针越界
指针指向空间释放,及时置NULL
避免返回局部变量的地址
指针使用之前检查有效性
指针运算
指针 +- 整数
指针 - 指针
指针的关系运算
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较 ——> 指针可以越界指向数组最后一个元素后面的那个位置,但是不可以指向第一个元素前面的那个位置
指针和数组
数组名表示的是数组首元素的地址。arr = &arr[0]
可以直接通过指针来访问数组。printf("%d ", *(p + i));
二级指针
指针变量也是变量,在内存中也有其自己的地址,也可以存放到某个指针中去
二级指针的理解
有关二级指针的运算
通过对二级指针进行一次解引用,可以获取到一级指针变量,重新 改变指向
通过对二级指针进行两次解引用,可以获取到一级指针所存放的变量,重新 改变值
【指针进阶 · 提升篇】
字符指针
指针存放单字符
指针存放字符串
指针保存的是所指向字符串的首字符地址
一道剑指offer的面试题
普通的两个字符串比较【==】对比的是它们在内存中的地址
如果其存放在栈区的话,二者的拥有两块独立的内存空间
如果其存放在常量区的话,则共享同一块地址空间
指针常量与常量指针
小知识
有const修饰的即为常量,一旦初始化了就不可修改
指针常量
【特性】:指针所指向的内容不能通过指针来修改,但指针变量本身可修改
常量指针
【特性】:指针变量本身的指向不能修改,但是指针指向的内容可以通过指针来修改
指针数组与数组指针
指针数组
对于指针数组来说,它是一个【数组】,数组里面存放的每一个元素都是指针,例:int* arr[5];
数组名与指针变量的万能置换规则:arr[i] == *(arr + i) == *(p + i) == p[i]
数组指针
对于数组指针来说,它是一个【指针】,这个指针所指向的是一个数组的地址,例:int (*p)[5]
数组传参与指针传参
一维数组传参
1. 形参可以是数组
2. 形参可以是指针
3. 形参可以是一个二级指针,指针数组的地址可以给到二级指针做接收,因为指针数组里面存放的都是一级指针
二维数组传参
1. 直接用二维数组做接收
! 小贴士 ! —— 形参部分的设计只能省略第一个[ ]内的数字,要给出当前的列数
2. 二维数组的数组名是首行的地址,是一个一维数组的地址,要使用数组指针来接收
一级指针传参
1. 可以直接是一个变量的地址
2. 可以是一级指针
3. 一维数组的数组名(二维数组a[5][5]第一行的数组名为a[0])
二级指针传参
1. 可以直接是一个一级指针的地址
2. 可以是二级指针
3. 指针数组的数组名(数组名是首元素地址,数组中的每一个元素都是一个一级指针)
指针函数与函数指针
指针函数
是一个函数,返回的是一个指针类型
函数指针
是一个指针,存放的是一个函数的地址
两道“有趣”的代码题O(∩_∩)O
函数指针数组
是一个数组,数组里的每一个元素都是函数指针
指向函数指针数组的指针
回调函数
通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数
使用回调函数,最大的一个目的,就是为了实现:解耦!
回调函数使用场景
场景一:模拟计算器的加减乘除
场景二:模拟qsort函数【⭐】
场景三:模拟任务下载进度
场景四:模拟文件下载模块
【指针进阶 · 炼狱篇】
再谈指针大小
指针和数组笔试题解析
sizeof() 是操作符,不是函数,它是用来计算对象或者类型创建的对象所占内存空间的大小
strlen() 是函数,它是用来求字符串长度的,计算的是字符串之前 ‘\0’ 出现的字符个数,如果没有看到 ‘\0’ 会继续往后找
指针相关历年笔试真题汇总
笔试题1
笔试题2
笔试题3
笔试题4
笔试题5
笔试题6
笔试题7
笔试题8
数组
一维数组的创建和初始化
数组的创建
数组是一组相同类型元素的集合
在C99标准之前,数组的[]内部要给出一个常量值,但是在C99之后,出现了【变长数组】的概念
数组的初始化
整型数组
不完全初始化:其余未初始化的数组内容默认为0
完全初始化
当数组没有指定大小的时候,初始化了几个数组的大小就是几个
字符数组
【有'\0'】:char ch1[] = "abc";
【无'\0'】:char ch2[] = { 'a', 'b', 'c' };
→ 拓展:数组作为局部变量不初始化内容默认为【随机值】;数组作为全局变量不初始化内容默认为【0】
一维数组的使用
数组是使用下标来访问的,下标是从0开始
数组arr的大小可以通过计算得到 sizeof(arr)/sizeof(arr[0])
一维数组在内存中的存储
数组在内存中是连续存放的
元素地址随着数组下标的增长而递增
二维数组的创建和初始化
二维数组的创建
二维数组的初始化
对于二维数组在初始化的时候可以省略行,但是不可以省略列!!
二维数组的使用
和一维数组一样,二维数组也是通过下标来访问的 —— a[2][2]
二维数组在内存中的存储
和一维数组一样,二维数组在内存中也是连续存储的
数组越界
【经典错误1】:边界值考虑不当导致越界访问
【经典错误2】:数组大小不足以承载输入的字符数
数组作为函数参数【⭐】
数组传参时只是传入了数组的首元素地址 —— 数组名即为首元素地址
数组传参时需要将数组长度设为函数参数,不可在函数内部进行计算,得到的并不是数组真正的长度
数组名意味着什么?
数组名代表首元素地址
两种特殊情况
sizeof(数组名) —— 求解的是整个数组的字节大小
&数组名 —— 为整个数组的地址
函数
【概念】:是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
分类
库函数
使用前只要知道功能和使用方法,并包含头文件
库函数学习的工具
cplusplus:https://legacy.cplusplus.com/
cppreference:
https://en.cppreference.com/w/【英文版】
https://zh.cppreference.com/w/【中文版】
MSDN:一个辅助离线工具
库函数的简单分类
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
自定义函数
自己确定函数名,参数,返回类型,并自己实现功能
函数的参数
实参
真实传给函数的参数,叫实参
对于实参而言,它可以是【常量】、【变量】、【表达式】、【函数】等
形参
形式参数是指函数名后面括号中的变量
因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形参
函数调用
传值调用
形参和实参分别占有不同的内存块,形参的改变不会影响实参
传址调用
把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
本质上,传址调用也是传值调用,只不过传址能达到效果更多
函数的嵌套调用和链式访问
嵌套调用
函数可以嵌套调用,但是不能嵌套定义
链式访问
printf("%d", printf("%d", printf("43")))
函数的声明和定义
函数声明
函数的声明一般要放在头文件中的
函数的声明一般出现在函数的使用之前。要满足先声明后使用
函数定义
函数的具体实现
函数递归
什么是函数递归?
程序调用自身的编程技巧称为递归( recursion)
递归的主要思考方式在于:把大事化小
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量
函数递归的两个必要条件
1. 存在限制条件,当满足这个限制条件的时候,递归便不再继续
2. 每次递归调用之后越来越接近这个限制条件
例题与练习
例题1:打印数字
例题2:求字符串长度
例题3:阶乘求解
练习1:斐波那契数列
练习2:青蛙跳台阶
练习3:汉诺塔问题⭐
函数栈帧的创建的销毁
文章链接:https://blog.csdn.net/Fire_Cloud_1/article/details/128588470
难点突破:
知道相关的寄存器以及相关的汇编指令
学会分析函数栈帧从创建到销毁的全过程
要清楚的五个问题
① 局部变量是如何创建的?
② 为什么局部变量不初始化内容是随机的?
③ 函数调用时参数时如何传递的?传参的顺序是怎样的?
④ 函数的形参和实参分别是怎样实例化的?
⑤ 函数调用是怎么做的?返回值是如何带会的?
分支和循环语句
1. 表达式语句
2. 函数调用语句
3. 控制语句⭐
条件判断语句(分支语句)
if语句
悬空else
如果出现if分支嵌套的情况,优先和内部的if进行匹配
if书写形式的对比
switch语句
case
switch表达式中是几,就从哪个case进去
case语句没有先后顺序的问题
default
case的值都不匹配switch后的表达式,就走了default子句
default子句的位置可以任意
break
可以跳出当前的switch子句
case子句都都要break,防止穿透!!!
循环执行语句
while语句
while语句中break与continue的使用
break - >>某种条件成立时整个循环不执行
continue - >>某种条件成立时跳过后面的代码
getchar()
【EOF】指的就是【End Of File】,当这个输入没有到达文件末尾时,getchar()就会从键盘上不断地去读取数据,直到文件末尾即要换行为止
getchar会去缓冲区里面读数据,每次只能读取一个,其返回类型为int
for语句(for循环)
while语句中break与continue的使用
break - >>某种条件成立时整个循环不执行
continue - >>某种条件成立时跳过后面的代码
变种for【循环变量可以省略、可以是多个....】
do while语句
do...while循环是先执行后判断,循环条件至少执行一次
折半查找算法 —— 在有序的序列中查找元素
转向语句
break语句
goto语句
不建议大量使用
适用于多层循环嵌套的情况
continue语句
return语句
4. 复合语句
5. 空语句
初识C语言 - 串讲C语言语法
什么是C语言
C语言是一门通用的计算机语言
C语言是有国际标准的,比如: C89,C90,C99,C11,当前使用最多的还是C89,C90
C语言是一门编译型语言,需要编译器,推荐使用的是VS2019
上层应用与下层应用到区别
第一个C语言程序
数据类型
变量与常量
先知
C语言中可以改变的量叫做变量,不可以改变的量叫做常量
变量的命名规则
只能由字母(包括大写和小写)、数字(1234567890)和下划线( _ )组成
不能以数字开头
长度不能超过63个字符
变量名中区分大小写的
变量名不能使用关键字
变量的分类
全局变量
在代码块外部定义的变量
局部变量
在代码块内部定义的变量
变量的作用域和生命周期
作用域
作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
局部变量的作用域是变量所在的局部范围,是一个范围的概念
全局变量的作用域是整个工程。
生命周期
变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段
局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
全局变量的生命周期是:整个程序的生命周期。
常量的种类
字面常量
const 修饰的常变量
#define定义的标识符常量
枚举常量
字符串 + 转义字符
字符串
由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),或者简称字符串。
字符串的结束标志是\0字符
有\0
无\0
转义字符
有关转义字符的笔试题
注释
作用
在程序执行的时候会直接跳过注释的代码,不会运行
可以为一些晦涩难懂的代码添加注释
!缺陷:不可嵌套注释
选择语句
初步认识
if
switch
循环语句
初步认识
for循环
while循环
do..while循环
函数
函数的出现可以把单一功能的代码进行封装,提高了代码的复用
原理
数组
数组的定义
数组的下标访问
操作符
算数运算符
+
-
*
/
%
移位操作符
<<
>>
位操作符
&
|
^
~
赋值操作符
【+=】、【-=】、【*=】、【/=】、【%=】、【>>=】、【<<=】、【^=】
单目操作符
关系操作符
【>】、【>=】、【<】、【<=】、【! =】、【==】
逻辑操作符
&&
| |
条件操作符
exp1 ? exp2 : exp3
逗号表达式
exp1, exp2, exp3, …expN
下标引用操作符 [ ]
函数调用操作符 ( )
结构成员调用操作符 . ->
常见关键字
注意点
关键字是不能字符创建的
自定义的变量名不能和关键字同名
分类
有关数据存储的底层原理
typedef关键字
static关键字
修饰局部变量
其生命周期会延长,但作用域不会发生变化
修饰全局变量
只在当前编译模块中(即当前文件内)生效,其他文件不可访问。因此其作用域发生了变化,但是生命周期没有变化(从定义到结束都不会被释放)
关键字修饰函数
如果一个函数前加了static关键字的话,它就只能在当前文件内访问到
修饰成员变量【C++的知识】
存放在静态区,而不在栈区,是不属于当前类的,因此需要在类外初始化
#define 定义常量和宏
#define定义符号,符号没有参数
#define定义宏,宏有参数
指针
内存的概念
内存被划分成一个个的内存单元,一个内存单元的大小是1byte
每个内存单元都有一个编号,也就是地址
内存的地址如果要存放起来,就可以放在指针变量中
指针变量的大小
指针变量是用来存放地址的
指针变量在32bit的平台上,是4byte
指针变量在64bit的平台上,是8byte
取决于运行平台
结构体
结构体是用来表示复杂对象的
关键字struct是用来定义结构类型的
变长数组不是数组的长度可以变化,而是数组的大小可以用变量来指定