C语言程序设计思维导图
初识C语言1
初识C语言2
初识C语言3
分支语句
循环语句
函数的基本语法
函数的调用
函数递归
一维数组
二维数组
操作符详解1
操作符详解2
VS环境调试技巧和演示
课程梳理
比特就业课-C语言知识图谱【记得给视频三连】
C语言初级 使用指南https://www.bilibili.com/video/BV18N411F7VX/
- 初识C语言-串讲C语言语法
什么是C语言 https://www.bilibili.com/video/BV1cq4y1U7sg?p=3&t=281.7(4:40处)
C语言是一门通用的计算机语言
C语言是有国际标准的,比如:C89,C90,C99,C11
当前使用最多的还是C89,C90
C语言是一门编译型语言,需要编译器,本课程使用的是VS2013/VS2019
编译器安装vs2022 https://www.bilibili.com/video/BV11R4y1s7jz/?s
第一个C语言程序 https://www.bilibili.com/video/BV1cq4y1U7sg?p=4
数据类型 https://www.bilibili.com/video/BV1cq4y1U7sg?p=6&t=85.7(1:25处)
常量和变量
https://www.bilibili.com/video/BV1U44y1y7xN?p=7(视频前半部分讲解变量,14:35开始讲如何解决scanf函数不安全的问题)
C语言中可以改变的量是变量
C语言中不可以改变的是量是常量
变量分类
全局变量
在代码块外部定义的变量
局部变量
在代码块内部定义的变量
变量的作用域和生命周期 https://www.bilibili.com/video/BV1cq4y1U7sg?p=9&t=623.2(10:23处)
作用域
作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
局部变量的作用域是变量所在的局部范围。
全局变量的作用域是整个工程。
生命周期
变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段
局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
全局变量的生命周期是:整个程序的生命周期。
常量分类 https://www.bilibili.com/video/BV1cq4y1U7sg?p=10
字面常量
const修饰的常变量
define定义的标识符常量
枚举常量
字符串+转义字符
字符串 https://www.bilibili.com/video/BV1cq4y1U7sg?p=11
由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),或者简称字符串。
字符串的结束标志是\0字符
转义字符 https://www.bilibili.com/video/BV1cq4y1U7sg?p=13&t=637.8(10:38处)
这里常见的笔试题就是:求字符串长度
注释 https://www.bilibili.com/video/BV1cq4y1U7sg?p=14&t=29.2(0:29处)
C语言注释风格:缺点(不能嵌套注释)
C++注释风格
选择语句 https://www.bilibili.com/video/BV1cq4y1U7sg?p=16&t=23.9(视频0:23处讲解if...else...分支语句,switch的用法见第二课详解)
这里只是初步认识
if
switch
循环语句 https://www.bilibili.com/video/BV1cq4y1U7sg?p=17(详见第二课循环语句部分)
这里只是初步认识
for语句
while语句-讲解
do.while
函数 https://www.bilibili.com/video/BV1cq4y1U7sg?p=18(0:16处)
函数的出现可以把单一功能的代码进行封装,提高了代码的复用
数组(视频17中10:40处)
数组的定义
数组的下标访问
数组的下标是从0开始的
数组可以通过下标来访问
操作符 https://www.bilibili.com/video/BV1cq4y1U7sg?p=19 https://www.bilibili.com/video/BV1cq4y1U7sg?p=20
算数操作符:+ - / % (视频18中1:46处)
移位操作符:>> << (视频18中4:48处)
位操作符:& ^ | (视频18中7:24处)
赋值操作符: = += -= = /= &= ^= |= >>= <<= (视频18中8:47处)
单目操作符:! - + & sizeof ~ ++ — (类型) (视频18中10:30处讲解前几个操作符,15:51处讲解sizeof以及计算数组元素个数;视频19中3:07处开始讲解后几个操作符;&和这里尚未具体讲到)
关系操作符:> >= < <= != == (视频19中26:05处)
逻辑操作符:&& || (视频19中26:53处)
条件操作符: exp1 ? exp2 : exp3 (视频19中30:49处)
逗号操作符:exp1, exp2, exp3, …expN (视频19中35:35处)
下标引用:[] (视频19中39:49处)
结构体成员访问操作符:. -> (尚未具体讲到)
函数调用操作符 () (视频19中41:50处)
常见关键字
auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while https://www.bilibili.com/video/BV1cq4y1U7sg?p=21
关键字是不能字符创建的
自定义的变量名不能和关键字同名
static https://www.bilibili.com/video/BV1cq4y1U7sg?p=23&t=207.8(视频22中3:28处)
修饰局部变量
修饰全局变量
修饰函数
typedef (视频22中0:12处)
类型重定义
define定义符号和宏
define 定义符号,符号没有参数 https://www.bilibili.com/video/BV1cq4y1U7sg?p=24&t=50.4(0:50处)
define 定义宏,宏有参数 (视频24中2:48处)
指针
内存 https://www.bilibili.com/video/BV1cq4y1U7sg?p=26&t=110.9(1:20处)
内存被划分成一个个的内存单元,一个内存单元的大小是1byte
每个内存单元都有一个编号,也就是地址
内存的地址如果要存放起来,就可以放在指针变量中
指针的大小(视频26中38:02处)
指针变量是用来存放地址的
指针变量在32bit的平台上,是4byte
指针变量在64bit的平台上,是8byte
结构体 https://www.bilibili.com/video/BV1cq4y1U7sg?p=27
结构体是用来表示复杂对象的
关键字struct是用来定义结构类型的
- 分支和循环语句
语句(什么是语句?https://www.bilibili.com/video/BV1cq4y1U7sg?p=30&t=237.1 中3:55处) - 表达式语句
- 函数调用语句
- 控制语句
- 复合语句
- 空语句
分支语句
if(视频29中4:45处)
悬空else
if语句形式的对比
switch https://www.bilibili.com/video/BV1cq4y1U7sg?p=31
break
跳出switch语句
case
switch表达式中的只是几,就从哪个case进去
case语句没有先后顺序的问题
default
case的值都不能匹配switch后表达式的值时,就走了default
default子句的位置可以任意
循环语句
while
https://www.bilibili.com/video/BV1cq4y1U7sg?p=32(本视频前部分介绍了while循环的用法,16:14处简单讲解了getchar()从输入缓冲区配合while循环获取字符的过程)
https://www.bilibili.com/video/BV1cq4y1U7sg?p=33(本视频进一步讲解了缓冲区及while循环的应用)
break
跳出while循环,终止整个循环
continue
跳过本次循环continue后边的代码,直接去while循环的判断部分
for
break
跳出for循环,终止整个循环
continue
跳过本次循环continue后边的代码,直接去for语句的表达式(调整部分)
变种(视频5中17:48处)
for(;;){};
循环变量可以是多个
do while
https://www.bilibili.com/video/BV1cq4y1U7sg?p=38(视频前边介绍了do while循环,后边进行循环语句的练习)
do while循环是先执行后判断的,循环体至少执行一次
break
跳出循环
continue
跳过本次循环中continue后边的代码
折半查找算法(视频35中37:00处)
在已经有序的数列中查找元素
https://www.bilibili.com/video/BV1cq4y1U7sg?p=40(本视频是循环语句练习讲解,25:30处做猜数字游戏)
goto 语句 初阶视频40(33:50处)
goto 语句不建议大量使用
goto语句适用于多层循环嵌套的情况
- 函数
函数是什么?C初阶视频42(3:04处)
是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
函数的分类
库函数(视频42中8:28处)
使用前只要知道功能和使用方法,并包含头文件
库函数学习的工具
www.cplusplus.com
https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5
MSDN
库函数的简单分类
IO函数
字符串函数
字符函数
内存操作函数
时间日期函数
数学函数
其他函数
自定义函数 https://www.bilibili.com/video/BV1cq4y1U7sg?p=46&t=160.8
自己确定函数名,参数,返回类型,并自己实现功能
函数的参数(视频43中48:40处)
实参
真实传给函数的参数,叫实参
实参可以是:常量、变量、表达式、函数等
形参
形式参数是指函数名后括号中的变量
因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形参
函数调用 https://www.bilibili.com/video/BV1cq4y1U7sg?p=48
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
本质上,传址调用也是传值调用,只不过传址能达到效果更多
函数嵌套调用 https://www.bilibili.com/video/BV1cq4y1U7sg?p=50&t=254.5(4:14处)
函数的链式访问 (视频47中7:05处)
函数的声明和定义(视频47中16:34处)
函数声明
函数声明一般放在头文件
函数声明要在使用之前,必须满足先声明后使用
函数定义
函数的具体实现
函数递归
什么是递归 https://www.bilibili.com/video/BV1cq4y1U7sg?p=51&t=154.2(2:34处)
程序调用自身的编程技巧称为递归
把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
递归的两个必要条件(视频48中31:08处)
存在某个限制条件,当条件满足时,递归不再继续
每次递归调用都向这个限制条件接近
递归练习 https://www.bilibili.com/video/BV1cq4y1U7sg?p=55
- 数组
一维数组的创建和初始化 https://www.bilibili.com/video/BV1cq4y1U7sg?p=60&t=184.3(3:04处讲什么是数组)
数组的创建(视频57中4:24处)
数组是一组相同类型元素的集合
数组创建时 [] 中要给一个常量(在C99标准之前)
数组的初始化(视频57中12:24处)
创建数组的同时给数组的一些内容合理的初始值
创建数组时可不指定数组的大小,元素个数根据初始化的内容来确定
一维数组的使用(视频57中27:00处)
数组是使用下标来访问的,下标是从0开始
数组arr的大小可以通过计算得到 sizeof(arr)/sizeof(arr[0])
一维数组在内存中的存储(视频57中33:05处)
数组在内存中是连续存放的
元素地址随着数组下标的增长而递增
二维数组的创建和初始化
二维数组的创建 https://www.bilibili.com/video/BV1cq4y1U7sg?p=61(15:30处)
二维数组的初始化 (视频58中9:10处)
二维数组如果有初始化,行可以省略,列不能省略
二维数组的使用(视频58中26:18处)
和一维数组一样,二维数组也是通过下标来访问的
二维数组在内存中的存储(视频58中30:44处)
和一维数组一样,二维数组在内存中也是连续存储的
数组越界
如果数组有n个元素,最后一个元素的下标就是n-1。下标如果小于0,或者大于n-1,超出了数组合法空间的访问,就是数组越界
C语言本身是不做数组下标的越界检查,编译器也不一定报错
二维数组的行和列也可能存在越界
数组作为函数参数 https://www.bilibili.com/video/BV1cq4y1U7sg?p=62
数组传参时只是传入了数组的首元素的地址
数组传参时需要将数组长度设为函数参数
两种情况之外,所有的数组名都表示数组首元素的地址(视频59中18:46处)
sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组
&数组名,取出的是数组的地址。&数组名,数组名表示整个数组
- C语言操作符详解
操作符分类 https://www.bilibili.com/video/BV1U44y1y7xN?p=69&t=108.7(1:48处)
算术操作符(视频69中3:01处)
% 操作符的两个操作数必须为整数,结果的是整除之后的余数
/ 操作符如果两个操作数都为整数,执行整数除法;而只要有浮点数执行的就是浮点数除法(隐式类型转换)
移位操作符(视频69中12:00处)
左移操作符
左边抛弃,右边补0
右移操作符
逻辑移位:左边用0填充,右边丢弃
算术移位:左边用原该值的符号位填充,右边丢弃
逻辑右移主要对无符号数进行的操作
移位操作符的操作数只能是整数
移位操作是运算,不影响操作数变量内的数据
位操作符(视频69中32:16处)
& //按位与
| //按位或
^ //按位异或
操作数必须是整数
赋值操作符 https://www.bilibili.com/video/BV1U44y1y7xN?p=71&from=from_parent_mindnote(6:58处)
连续赋值操作
符合赋值符
单目操作符(视频71中11:51处、视频73前边13分钟)
单目操作符介绍
sizeof和数组
关系操作符 https://www.bilibili.com/video/BV1cq4y1U7sg?p=76&t=813.0(13:33处)
在编程的过程中==和=不小心写错,导致的错误
逻辑操作符(视频73中16:07处)
逻辑与的特点
前一项为假,则不计算后一项表达式
逻辑或的特点
前一项为真,则不计算后一项表达式
条件操作符(视频73中34:00处)
表达式1为真,则表达式2为最终结果,表达式3不计算,否则表达式2不计算
逗号表达式(视频73中38:16处)
逗号表达式,就是用逗号隔开的多个表达式
逗号表达式从左向右依次执行
整个表达式的结果是最后一个表达式的结果
下标引用、函数调用和结构成员 https://www.bilibili.com/video/BV1U44y1y7xN?p=74&t=116.4(1:56处)
访问一个结构的成员
结构体.成员名
结构体指针->成员名
表达式求值(视频74中25:46处)
表达式求值的顺序一部分是由操作符的优先级和结合性决定
有些表达式的操作数在求值的过程中可能需要转换为其他类型
隐式类型转换
整型提升(视频74中27:40处)
有符号整数的整型提升
高位补充符号位
无符号整数的整型提升
高位补0
整型截断
算术转换 https://www.bilibili.com/video/BV1cq4y1U7sg?p=79&t=230.8(3:50处)
操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行
寻常算术转换
某个操作数的类型在上面这个列表中排名较低,要转换为另外一个排名较高的操作数
操作符的属性(视频76中6:20处)
操作符的优先级
操作符的结合性
是否控制求值顺序
两个相邻的操作符先执行哪个?
取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性
问题表达式
表达式如果不能通过操作符的属性确定唯一的计算路径,表达式就是存在问题的
- 指针初阶
指针是什么?https://www.bilibili.com/video/BV1cq4y1U7sg?p=82&t=520.6(8:40处)
指针是内存中一个最小单元的编号
通常指的是用来存放内存地址的指针变量
地址是唯一标示一块地址空间的
指针的大小在32位平台是4个字节,在64位平台是8个字节
指针类型(视频79中17:56处)
指针+-整数
指针的类型决定了指针向前或者向后走一步有多大
指针的解引用
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)
指针的类型决定了,目标数据将作为什么类型进行解读
野指针 https://www.bilibili.com/video/BV1U44y1y7xN?p=80&t=150.7(2:30处)
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
指针未初始化
指针越界访问
指针指向的空间释放
如何规避野指针
指针初始化
小心指针越界
指针指向空间释放即使置NULL
避免返回局部变量的地址
指针使用之前检查有效性
指针运算(视频80中31:10处)
指针+- 整数
指针-指针
指针的关系运算(视频82中2:10处)
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
指针和数组 https://www.bilibili.com/video/BV1cq4y1U7sg?p=85&t=675.6(11:15处)
数组名表示的是数组首元素的地址
可以直接通过指针来访问数组
二级指针(视频82中27:10处)
指针变量也是变量
保存指针变量地址的就是二级指针
指针数组(视频82中36:16处)
存放指针的数组
- 结构体初识 https://www.bilibili.com/video/BV1cq4y1U7sg?p=87
结构是一些值的集合,这些值称为成员变量
结构的每个成员可以是不同类型的变量
结构体的声明
结构体变量的定义和初始化
结构体成员的访问(视频84中15:46处)
结构体.成员名
结构体指针->成员名
结构体传参(视频84中20:30处)
结构体传参的时候,要传结构体的地址
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降(视频84中32:30处)
- 实用调试技巧
什么是bug https://www.bilibili.com/video/BV1cq4y1U7sg?p=88&t=124.8(2:04处)
第一次被发现导致计算机错误的是一只飞蛾
https://www.bilibili.com/video/BV1cq4y1U7sg?p=90&t=1022.9(调试案例:17:02处)
调试的重要性
一名优秀的程序员是一名出色的侦探
初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写程序,但是80%的时间在调试
调试是什么?
是发现和减少计算机程序或电子仪器设备中程序错误的一个过程
调试的基本步骤
发现程序错误的存在
以隔离、消除等方式对错误进行定位
确定错误产生的原因
提出纠正错误的解决办法
对程序错误予以改正,重新测试
Debug和Release(视频85中15:04处)
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用(进阶视频96前15分举例说明了Release版本对程序的优化)
Windows环境调试(视频85中23:15处)
调试环境的准备
环境中选择 debug 选项,才能使代码正常调试
调试快捷键
F5 - 启动调试,经常用来直接跳到下一个断点处
F9 - 创建断点和取消断点
可以在程序的任意位置设置断点,使得程序在想要的位置随意停止执行
F10 - 逐过程,通常用来处理一个过程
一个过程可以是一次函数调用,或者是一条语句
F11 - 逐语句,就是每次都执行一条语句
可以使执行逻辑进入函数内部
CTRL + F5 - 开始执行不调试
调试时查看程序当前信息(视频85中40:12处)
查看临时变量的值
查看内存信息
查看调用堆栈(视频87中5:52处)
查看汇编信息
查看寄存器信息
如何写出好(易于调试)的代码 https://www.bilibili.com/video/BV1cq4y1U7sg?p=92
视频89模拟实现了strcpy函数,视频90中使用assert和const对函数进行了优化
使用assert(视频90中16:06处)
尽量使用const
const修饰指针变量(视频90中30:39处)
const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变
const如果放在的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变
养成良好的编码风格
添加必要的注释
避免编码的陷阱
编程常见的错误 https://www.bilibili.com/video/BV1cq4y1U7sg?p=95&t=3095.7(51:35处)
编译型错误
链接型错误
一般是标识符名不存在或者拼写错误
运行时错误
- windows版本的git实用方法
https://www.bilibili.com/video/BV1hf4y1W7yT?from=from_parent_mindnote(0:40处gitee创建项目,5:04处git下载安装,9:58处git操作三板斧)
背景
git是一个版本控制工具. 主要解决三个问题
代码被喵星人吃掉了
产品经理反复修改需求, 需要同时维护多个版本代码
多人协同开发
下载安装
安装 git for windows
https://git-scm.com/downloads
安装 tortoise git
https://tortoisegit.org/download/
注意事项
先安装 git for windows, 再安装 tortoise git
安装 git for windows 一路 next 即可
安装 tortoise git 中需要配置 git.exe, 这个是 git for windows 包含的部分. 如果 git for windows 安装成功, 这一步使用默认结果即可
安装 tortoise git 还需要配置姓名和邮箱, 这个尽量和 Github 的邮箱填成一致
两个工具安装完毕后, 需要重启电脑才能正确使用
使用 Github 创建项目
注册账号
创建项目
登陆成功后, 进入个人主页, 点击左下方的 New repository 按钮新建项目
然后跳转到的新页面中输入项目名称(注意, 名称不能重复, 系统会自动校验. 校验过程可能会花费几秒钟). 校验完毕后, 点击下方的 Create repository 按钮确认创建
在创建好的项目页面中复制项目的链接, 以备接下来进行下载
下载项目到本地
复制刚才创建好的项目的链接
打开指定的需要放置项目的目录
右击目录, 点击 Git Clone
在弹出的对话框中输入刚才复制的项目链接即可
下载成功, 会出现 绿色 图标
Git 操作的三板斧
放入代码
使用 VS 创建工程, 并把工程放在刚才下载到本地的项目路径中
或者将曾经写过的代码的工程目录直接拷贝到项目目录中
三板斧第一招: git add
告知 git 工具哪些文件需要进行版本管理
右击标记为 蓝色 ? (表示该文件未使用 git 管理) 的目录, 选择 add
弹出的对话框中勾选具体需要管理的文件. 勾选完毕点击 ok 即可
此时图标变成红色感叹号(表示该文件被git管理, 但是未提交内容)
三板斧第二招: git commit
将修改内容提交到本地
每提交一次, 就是一个版本. 比如开发完某个功能模块, 就可以提交一次了. 后续进行版本回退都是以提交为准
此时只是提交到本地, Github 上还看不到代码变更
右键选择 红色感叹号 目录, 选择 Git commit -> master
此时弹出了一个对话框. 可以在此处看到都需要提交哪些文件, 以及每个文件的具体改动情况. 并且需要输入提交日志. 描述这次提交的具体改动原因是什么. 这个日志是后续进行版本回退的重要参考依据
点击下方的 Commit 按钮完成提交
三板斧第三招: git push
提交的内容需要同步到服务器上, 才能让其他人看到改动. 使用 push 即可
右键需要 push 的目录, 点击 push
弹出的对话框确认 push. 不需要修改, 直接确认即可.然后会弹出对话框提示输入 Github 的账户和密码
用户名密码输入正确, 点击 Login , 即可完成 push. 此时刷新 Github 的界面, 就能看到新版本的代码了
C语言进阶
数据的存储
数据的类型 https://www.bilibili.com/video/BV1cq4y1U7sg?p=99
类型的意义(视频96中17:30处)
使用这个类型开辟内存空间的大小(大小决定了使用范围)
如何看待内存空间的视角
类型的基本归类(视频96中18:20处)
整形家族
char short int long
浮点型家族
float double
构造类型
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
指针类型
空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
整形在内存中的存储(视频96中25:55处)
相关练习 https://www.bilibili.com/video/BV1cq4y1U7sg?p=101
https://www.bilibili.com/video/BV1cq4y1U7sg?p=103
原码、反码、补码
计算机中的整数有三种表示方法,即原码、反码和补码
三种表示方法均有符号位和数值位两部分
符号位都是用0表示“正”,用1表示“负”
数值位负整数的三种表示方法各不相同
原码
直接将二进制按照正负数的形式翻译成二进制就可以
反码
将原码的符号位不变,其他位依次按位取反就可以得到了
补码
反码+1就得到补码
正数的原、反、补码都相同
对于整形来说:数据存放在内存中的是补码
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理
加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
大小端介绍(视频96中46:30处)
大端(存储)模式
是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
小端(存储)模式
是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中
是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中
浮点型在内存中的存储 https://www.bilibili.com/video/BV1cq4y1U7sg?p=104&t=146.8(2:26处)
国际标准IEEE(电气和电子工程协会) 754规定的二进制浮点数形式
(-1)^S M 2^E
(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数
M表示有效数字,大于等于1,小于2
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
对于11位的E,这个中间数是1023
指数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)
- 指针的进阶 https://www.bilibili.com/video/BV1cq4y1U7sg?p=104&t=146.8(5:08处)
字符指针(视频103中8:08处)
存放字符地址的
指向字符串时,是首字节地址
字符数组首元素地址是字符指针
指针数组(视频103中28:10处)
是存放指针的数组
数组指针 https://www.bilibili.com/video/BV1cq4y1U7sg?p=108&t=292.1(4:52处)
指向数组的指针
整型数组指针:int (p)[10];
[]的优先级要高于号的,所以必须加上()来保证p先和结合
&数组名VS数组名
&arr和arr值是一样的
&arr 的类型是:int()[10]; ,是一种数组指针类型
数组指针的使用(视频105中26:50处)
数组参数、指针参数(视频105中53:22处)
一维数组传参
二维数组传参
二维数组传参,函数形参的设计只能省略第一个[]的数字
一级指针传参(视频112中1:55处)
当函数的参数为一级指针的时候,可以接收的参数
相应类型变量的地址
相应类型数组的数组名
二级指针传参(视频112中9:48处)
当函数的参数为二级指针的时候,可以接收的参数
一级指针的地址
二级指针
指针数组的数组名
函数指针 https://www.bilibili.com/video/BV1cq4y1U7sg?p=115&t=171.1(22:50处)
void (pfun1)();
pfun1先和结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
两段有趣的代码
((void ()())0)();
代码2void (signal(int , void()(int)))(int);
函数指针数组 https://www.bilibili.com/video/BV1cq4y1U7sg?p=117&t=2176.7(36:16处)
int (parr1[10])();
parr1 先和 [] 结合,说明 parr1是数组,数组的元素是 int ()() 类型的函数指针
函数指针数组的用途:转移表(视频116中3:30处)
指向函数指针数组的指针 https://www.bilibili.com/video/BV1cq4y1U7sg?p=119&t=915.6(15:15处)
回调函数(视频116中28:32处)
通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应
练习:https://www.bilibili.com/video/BV1cq4y1U7sg?p=120&t=1082.7(18:02处讲解qsort函数的原理)、https://www.bilibili.com/video/BV1cq4y1U7sg?p=122(模仿qsort函数的通用冒泡排序)
指针相关练习讲解 https://www.bilibili.com/video/BV1cq4y1U7sg?p=122
- 字符串和内存函数的介绍
求字符串长度 https://www.bilibili.com/video/BV1cq4y1U7sg?p=135&t=190.6(3:10处)
strlen
size_t strlen ( const char str );
参数指向的字符串必须要以 ‘\0’ 结束
strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )
注意函数的返回值为size_t,是无符号的
长度不受限制的字符串函数
strcpy(视频132中22:20处)
char strcpy(char destination, const char source );
源字符串必须以 ‘\0’ 结束
会将源字符串中的 ‘\0’ 拷贝到目标空间
目标空间必须足够大,以确保能存放源字符串
目标空间必须可变
strcat(视频132中37:40处)
char strcat ( char destination, const char source );
源字符串必须以 ‘\0’ 结束
目标空间必须有足够的大,能容纳下源字符串的内容
目标空间必须可修改
strcmp(视频134中8:34处)
int strcmp ( const char str1, const char str2 );
标准规定
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
长度受限制的字符串函数介绍 https://www.bilibili.com/video/BV1cq4y1U7sg?p=137&t=2204.4(36:44处)
strncpy(视频134中38:31处)
char strncpy ( char destination, const char source, size_t num );
拷贝num个字符从源字符串到目标空间
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个
strncat(视频134中49:45处)
char strncat ( char destination, const char source, size_t num );
Appends the first num characters of source to destination, plus a terminating null-character.
If the length of the C string in source is less than num, only the content up to the terminatingnull-character is copied
strncmp
int strncmp ( const char str1, const char str2, size_t num );
比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完
字符串查找 https://www.bilibili.com/video/BV1cq4y1U7sg?p=139
strstr(视频136中4:36处)
char strstr ( const char str1, const char str2);
Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part ofstr1
strtok(视频136中35:54处)
char strtok ( char str, const char sep );
sep参数是个字符串,定义了用作分隔符的字符集合
第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记
如果字符串中不存在更多的标记,则返回 NULL 指针
错误信息报告
strerror(视频136中58:46处,视频137前14分补充讲解)
char strerror ( int errnum );
返回错误码,所对应的错误信息
字符函数 https://www.bilibili.com/video/BV1cq4y1U7sg?p=140&t=841.3(14:01处)
字符操作内存操作函数
memcpy(视频137中32:06处,视频139前10分有补充)
void memcpy ( void destination, const void * source, size_t num );
函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置
函数在遇到 ‘\0’ 的时候并不会停下来
如果source和destination有任何的重叠,复制的结果都是未定义的
memmove https://www.bilibili.com/video/BV1cq4y1U7sg?p=142&t=648.7(10:48处)
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的
如果源空间和目标空间出现重叠,就得使用memmove函数处理。
memset(视频139中52:21处)
memcmp(视频139中46:21处)
- 自定义类型详解(结构体&枚举&联合)
结构体 https://www.bilibili.com/video/BV1cq4y1U7sg?p=144&t=262.7(4:22处)
结构体类型的声明(视频141中5:32处)
结构是一些值的集合,这些值称为成员变量
结构的每个成员可以是不同类型的变量
匿名结构体类型
在声明结构的时候,可以不完全的声明,省略掉了结构体标签
两个成员完全相同的匿名结构类型,不是同一个类型
结构的自引用(视频141中17:58处)
结构体变量的定义和初始化(视频141中31:58处)
结构体内存对齐 https://www.bilibili.com/video/BV1cq4y1U7sg?p=145&t=117.6(1:57处)
内存对齐的规则(视频142中7:03处)
第一个成员在与结构体变量偏移量为0的地址处
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的值为8
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
内存对齐的意义(视频142中52:56处)
结构体的内存对齐是拿空间来换取时间的做法
节省空间的技巧(视频144中3:33处)
让占用空间小的成员尽量集中在一起
修改默认对齐数(视频144中5:16处)pragma pack(1)//设置默认对齐数为1
pragma pack()//取消设置的默认对齐数,还原为默认
结构体传参 https://www.bilibili.com/video/BV1cq4y1U7sg?p=144&t=1197.7(19:58处)
结构体传参的时候,要传结构体的地址
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
位段(视频144中27:53处)
位段的声明和结构是类似的,有两个不同(视频144中28:50处)
位段的成员必须是int、unsigned int 或signed int
位段的成员名后边有一个冒号和一个数字
位段的内存分配(视频144中35:15处)
位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
位段的跨平台问题(视频144中41:52处)
int 位段被当成有符号数还是无符号数是不确定的
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
枚举 https://www.bilibili.com/video/BV1cq4y1U7sg?p=149&t=56.8(0:56处)
枚举类型的定义(视频146中2:12处)
可能取值都是有值的,默认从0开始,依次递增1
在定义的时候也可以赋初值
枚举的优点(视频146中10:40处)
增加代码的可读性和可维护性
和 #define 定义的标识符比较枚举有类型检查更加严谨
防止了命名污染(封装)
便于调试
使用方便,一次可以定义多个常量
枚举的使用
只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
联合 https://www.bilibili.com/video/BV1cq4y1U7sg?p=150
联合类型的定义(视频147中1:43处)
联合的特点(视频147中11:00处)
类型定义的变量也包含一系列的成员
特征是这些成员公用同一块空间(所以联合也叫共用体)
联合变量的大小,至少是最大成员的大小
联合大小的计算 https://www.bilibili.com/video/BV1cq4y1U7sg?p=151&t=23.3(0:23处)
联合的大小至少是最大成员的大小
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
- 动态内存管理 https://www.bilibili.com/video/BV1cq4y1U7sg?p=156&t=242.7(4:20处)
为什么存在动态内存分配(视频153中4:46处)
有时候我们需要的空间大小在程序运行的时候才能知道,这时候就只能试试动态内存开辟了
动态内存函数(视频153中8:51处)
声明在 stdlib.h 头文件中
malloc(视频153中9:50处)
void malloc (size_t size);
函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
如果开辟成功,则返回一个指向开辟好空间的指针
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
返回值的类型是 void ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
free(视频153中19:36处)
void free (void ptr);
free函数用来释放动态开辟的内存
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
如果参数 ptr 是NULL指针,则函数什么事都不做
calloc(视频153中31:48处)
void calloc (size_t num, size_t size);
函数的功能是为 num 个大小为 size 的元素开辟一块空间
同时把所开辟空间的每个字节初始化为0
realloc(视频153中37:22处)
函数就可以做到对动态开辟内存大小的调整
void realloc (void ptr, size_t size);
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc在调整内存空间的是存在两种情况
情况1:原有空间之后有足够大的空间
直接在原有内存之后直接追加空间,原来空间的数据不发生变化
情况2:原有空间之后没有足够大的空间
在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址
常见的动态内存错误 https://www.bilibili.com/video/BV1cq4y1U7sg?p=158&t=106.4(1:46处)
对NULL指针的解引用操作(视频155中1:50处)
对动态开辟空间的越界访问(视频155中4:46处)
对非动态开辟内存使用free释放(视频155中8:26处)
使用free释放一块动态开辟内存的一部分(视频155中11:43处)
对同一块动态内存多次释放(视频155中16:35处)
动态开辟内存忘记释放(内存泄漏)(视频155中19:42处)
C/C++程序的内存开辟 https://www.bilibili.com/video/BV1cq4y1U7sg?p=160&t=2655.3(44:15处,视频前40分钟为动态内存相关练习)
栈区(stack)
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放
堆区(heap)
一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收
数据段(静态区)(static)
存放全局变量、静态数据
程序结束后由系统释放
代码段
存放函数体(类成员函数和全局函数)的二进制代码
柔性数组 https://www.bilibili.com/video/BV1cq4y1U7sg?p=162&t=238.9(3:58处)
柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员
sizeof 返回的这种结构大小不包括柔性数组的内存
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
柔性数组的使用
柔性数组的优势
- 文件操作
为什么使用文件 https://www.bilibili.com/video/BV1cq4y1U7sg?p=167&t=69.5(1:08处)
什么是文件(视频164中2:57处)
指的是磁盘上的文件
程序文件
源程序文件(后缀为.c)
目标文件(windows环境后缀为.obj)
可执行程序(windows环境后缀为.exe)
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用,文件标识常被称为文件名
文件的打开和关闭
文件指针(视频164中10:00处)
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息
文件的名字,文件状态及文件当前的位置等
这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节
一般都是通过一个FILE的指针来维护这个FILE结构的变量
通过文件指针变量能够找到与它关联的文件
指针指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件
文件的打开和关闭(视频164中18:38处)
在编写程序的时候,在打开文件的同时,都会返回一个FILE的指针变量指向该文件,也相当于建立了指针和文件的关系
文件在读写之前应该先打开文件
fopen函数来打开文件
FILE fopen ( const char filename, const char mode );
在使用结束之后应该关闭文件
fclose来关闭文件
int fclose ( FILE stream );
打开方式
“r”(只读) 为了输入数据,打开一个已经存在的文本文件,指定文件不存在则:出错
“w”(只写) 为了输出数据,打开一个文本文件,指定文件不存在则:建立一个新的文件
“a”(追加) 向文本文件尾添加数据,指定文件不存在则:建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件,指定文件不存在则:出错
“wb”(只写) 为了输出数据,打开一个二进制文件,指定文件不存在则:建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据,指定文件不存在则:出错
“r+”(读写) 为了读和写,打开一个文本文件,指定文件不存在则:出错
“w+”(读写) 为了读和写,建议一个新的文件,指定文件不存在则:建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写,指定文件不存在则:建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件,指定文件不存在则:出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件,指定文件不存在则:建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写,指定文件不存在则:建立一个新的文件
文件的顺序读写(视频164中31:24处)
字符输入函数 fgetc 适用于所有输入流(视频164中46:03处)
字符输出函数 fputc 适用于所有输出流(视频164中35:26处)
文本行输入函数 fgets 适用于所有输入流(视频164中59:50处)
文本行输出函数 fputs 适用于所有输出流(视频164中56:32处)
格式化输入函数 fscanf 适用于所有输入流(视频166中6:52处)
格式化输出函数 fprintf 适用于所有输出流 https://www.bilibili.com/video/BV1cq4y1U7sg?p=169&t=190.8(3:10处)
二进制输入 fread 适用于文件(视频166中18:15处)
二进制输出 fwrite 适用于文件(视频166中12:26处)
文件的随机读写 https://www.bilibili.com/video/BV1cq4y1U7sg?p=170&t=1130.8(18:50处)
fseek(视频167中23:47处)
根据文件指针的位置和偏移量来定位文件指针
int fseek ( FILE stream, long int offset, int origin );
ftell(视频167中29:44处)
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE stream );
rewind(视频167中31:54处)
让文件指针的位置回到文件的起始位置
void rewind ( FILE stream );
文本文件和二进制文件(视频167中34:05处)
二进制文件
数据在内存中以二进制的形式存储,不加转换的输出到外存的文件
文本文件
以ASCII字符的形式存储的文件
需要在存储前转换
数据在内存中都是怎么存储的
字符一律以ASCII形式存储
数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储
整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节)而二进制形式输出,则在磁盘上只占4个字节
文件读取结束的判定(视频167中45:23处)
被错误使用的feof
应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束
文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
fgetc 判断是否为 EOF
fgets 判断返回值是否为 NULL
二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
fread判断返回值是否小于实际要读的个数
文件缓冲区(视频167中1:03:45处)
ANSIC 标准采用“缓冲文件系统”处理的数据文件的
缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上
从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
缓冲区的大小根据C编译系统决定的
C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件
- 程序的编译(预处理)+链接
在ANSI C的任何一种实现中,存在两个不同的环境 https://www.bilibili.com/video/BV1cq4y1U7sg?p=172&t=206.1(3:25处)
翻译环境
在这个环境中源代码被转换为可执行的机器指令(视频169中5:52处)
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
编译的几个阶段(视频169中14:31处,预处理、编译、汇编以及链接的过程,完整内容接续进阶视频171)
预处理
gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中
预处理详解 https://www.bilibili.com/video/BV1cq4y1U7sg?p=175
预定义符号(视频172中1:33处)
FILE //进行编译的源文件
LINE //文件当前的行号
DATE //文件被编译的日期
TIME //文件被编译的时间
STDC //如果编译器遵循ANSI C,其值为1,否则未定义define(视频172中14:50处)
define 定义标识符(视频172中15:02处)
在define定义标识符的时候,后边千万不要加非必要的分号define 定义宏(视频172中25:05处)
允许把参数替换到文本中,这种实现通常称为宏或定义宏define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用define 替换规则(视频172中37:23处)
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
注意事项
宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索和##(视频172中41:44处)
的使用(视频172中42:36处)
define PRINT(FORMAT, VALUE) printf(“the value of “ #VALUE “is “FORMAT “\n”, VALUE)
PRINT(“%d”, i+3); 代码输出结果:the value of i+3 is 13
使用 # ,可以把一个宏参数变成对应的字符串
写在一起的字符串是会自动连接的的作用(视频172中55:45处)
define ADD_TO_SUM(num, value) sum##num += value
ADD_TO_SUM(5, 10); // 代码作用是:给sum5增加10可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的
带副作用的宏参数 https://www.bilibili.com/video/BV1cq4y1U7sg?p=177&t=40.5(0:40处)
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果
例子:#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
宏参数x+1;//不带副作用
宏参数x++;//带有副作用
宏和函数对比(视频174中9:27处)
优势
所以宏比函数在程序的规模和速度方面更胜一筹
宏是类型无关的
宏的参数可以出现类型,但是函数做不到
略势
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
宏是没法调试的
宏由于类型无关,也就不够严谨
宏可能会带来运算符优先级的问题,导致程容易出现错
几项对比(视频174中27:19处)
代码长度
宏:每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长
函数:代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码
执行速度
宏:更快
函数:存在函数的调用和返回的额外开 销,所以相对慢一些
操作符优先级
宏:宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号
函数:参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测
带有副作用的参数
宏:参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果
函数:参数只在传参的时候求值一 次,结果更容易控制。
参数类型
宏:宏参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型
函数:参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的
调试
宏:不方便调试的
函数:可以逐语句调试的
递 归
宏:不能递归的
函数:可以递归的
命名约定(视频174中35:46处)
一般来讲函数的宏的使用语法很相似,语言本身没法帮我们区分二者
代码习惯
把宏名全部大写
函数名不要全部大写undef https://www.bilibili.com/video/BV1cq4y1U7sg?p=177&t=2260.6(37:40处)
指令用于移除一个宏定义undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
命令行定义(视频174中40:12处)
许多C 的编译器提供了一种能力,允许在命令行中定义符号
根据同一个源文件要编译出一个程序的不同版本
条件编译(视频174中52:43处)
在编译一个程序的时候将一条语句(一组语句)编译或者放弃
常见的条件编译指令
文件包含 https://www.bilibili.com/video/BV1cq4y1U7sg?p=179&t=36.2(0:35处)
include 指令可以使另外一个文件被编译
预处理器先删除这条指令,并用包含文件的内容替换
头文件被包含的方式(视频176中1:02处)
本地文件包含
include “filename”
查找策略
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件
如果找不到就提示编译错误
库文件包含
include
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
嵌套文件包含(视频176中13:31处)
程序中就会出现两份comm.h的内容,这样就造成了文件内容的重复
两个解决方案(避免头文件的重复引入)
每个头文件这样写
每个头文件开头写
pragma once
其他预处理指令(视频176中26:00处)
编译
gcc -S test.c
编译完成之后就停下来,结果保存在test.s中
汇编
gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中
执行环境,它用于实际执行代码 https://www.bilibili.com/video/BV1cq4y1U7sg?p=174&t=1756.8(29:16处)
程序执行的过程
程序载入内存
在有操作系统的环境中:一般这个由操作系统完成
在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
调用main函数
执行程序代码
这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值
终止程序
正常终止main函数;也有可能是意外终止