课程内容#
if 语句、switch 语句#
- (C 语言关系运算符)
- 0 → false;1 → true
- false 👉 0、NULL(null)、'\0'
- <= → =<
- ! 取非 (关系运算符)
- 要注意区分:位取反运算符~(位运算符)
- ⭐!! 非非操作:逻辑上的归一化
- 将所有真值统一到 1,假值还是 0
- 顺序结构
- 略
- 分支结构
IF 语句#
-
-
(表达式)
- 任何表达式都有逻辑返回值
- 对于赋值表达式 a = 123;
- 返回值为 a 变量的值 123
-
{代码段;}
- 了解什么是一条语句?
- 单语句:“;” 结尾的语句
- int a = 1, b = 234; // 是一条语句,“,” 前后分为为一个表达式
- 空语句:只有一个 “;”,没有作用
- 复合语句
- 用花括号
- 即代码段👆
- 单语句:“;” 结尾的语句
- 了解什么是一条语句?
-
if else 语句
- 可以用问号表达式(三目运算符)来代替
- ...? ...: ...;
- 可以用问号表达式(三目运算符)来代替
SWITCH 语句#
-
-
变量 a 要有一个明确的唯一的整型值作为映射!
- 字母也有这样的映射:ASCII 码
-
进入 case 后,依次执行后续代码
- 直到遇到break、continue 或结构末尾~
- 而不需要再判断其后的 case 是否满足
-
default 分支
- 当 case都不满足时,会进入,相当于 if else 语句的 else
CPU 的分支预测#
从力扣题 -回文整数引出
船长超高时间效率代码如下:
-
-
红框处实现的就是 x<0
- 用来筛选小于 0 的整数,负数肯定不是回文整数
-
这段语句在操作系统内核里出现:
-
-
!!(x) 逻辑归一化,结果只会是 1 或 0
- 经常成立与否的情况会告诉正在处理程序的 CPU
⭐CPU 执行程序过程解析
- C 语言程序执行过程:编译后生成可执行文件→加载到内存里→CPU 执行
- cash 缓存
- 一级缓存:速度最快,而容量最小
- CPU 指令执行方式对比
- 早期串行(取址、指令预解析、写数据、执行、写回内存)
- 当今并行
- 示意图如下左右:
-
-
对于 5 条指令
- 串行方式需要 5 * 5 = 25 个时钟周期
- 并行方式只需要 5 + 4 = 9 个时钟周期
-
当指令非常多时,提高效率将近 5 倍
-
- 所以!CPU喜欢顺序结构
- 讨厌分支结构(要少用 if else 语句)
- 分支预测问题
- 对于并行的方式,CPU 不知道分支结构下一步是谁,该提前加载哪一步?
- CPU 实际是随机加载下一步
- 所以,如果预测错了,后面就白做了,此时又要返回重新处理
- CPU 实际是随机加载下一步
- 而__builtin_expect () 做的就是告诉CPU 哪种分支情况更易发生!!!
循环结构#
- WHILE 语句
- while () {代码块;}
- 先判断,再执行
- do {代码块;} while (表达式); ←分号要注意!
- 至少执行一次
- while () {代码块;}
- FOR 语句
- for (初始化;循环条件;执行后操作) {代码块;}
- 让循环变得很秀,由三部分组成
- 这三部分可以都不写
- 例如:for (; ;);,那就相当于 while (1);
- 上面是一个死循环,啥都没有
随堂练习#
-
-
记得条件判断冗余检查
-
输入非法时(如 "q")陷入死循环问题
- 解决方法及思考过程详见下文:思考点 1
-
代码
-
冗余版
-
-
❗这里 scanf 的 \n 是隐患!要去掉 \n,否则需要多读一个数
-
因为在输入下个数时才读到 \n
-
⭐参考博客:用 "% d\n" 要多读一个数? —— scanf () 函数的那些坑
-
-
-
精简版
-
-
-
①
-
②
- 熟悉 switch 语句结束的特性
- 代码
-
-
-
代码
-
对于 while 方式,如果首次判断成立,则与 do...while 方式没有区别
-
-
代码
-
亮点笔记#
- ⭐对条件判断语句进行冗余检查(剪裁),通常可以有更精简的代码
- 所有判断表达式是否为 0 的情况,可以直接使用 if (! 表达式)
- 对于 if (表达式) 单语句,可以利用逻辑与
- (表达式) && 单语句
- 比如 i && printf (" "); ,用来在除第一次循环中输出空格
- 单语句不能是 {代码块;}
- 对于 if (表达式) 单语句,可以利用逻辑与
- if...else 用三目运算符代替
- 所有判断表达式是否为 0 的情况,可以直接使用 if (! 表达式)
- 一般将循环变量 i 定义在初始化部分:int i = 0
- 不需要提前放在前面
- 更规范、简明:用变量之前再定义,不要离的太远
- 作用域问题
- 只在循环里用到该变量
- 不需要提前放在前面
- ++i 比 i++ 快?
- 能用 ++i 就用
- 从函数栈的角度:👇
- ++i 直接入栈 i+1 的值
- 而 i++ 的话,先入栈 i 再入栈 i+1
- 能用 ++i 就用
代码演示#
分支结构(6.struct_program.cpp)#
-
-
a - b 可以代替 a == b 判等
-
if else 可以用三目运算符代替
-
-
输出 1:false
- a++ 和 && 的运算优先级?
- ++ 优先级大于 &&
- a++ 外面的 () 为什么没有把 ++ 的运算优先级提高到比 && 先加呢?
- 是优先的
- 这里输出 false 与此无关~
- 应该对于 if 判断来说,先判断 a,再去 ++
- a++ 表达式的值是没有 ++ 的 a
- a++ 和 && 的运算优先级?
-
输出 2:a = 1, b = 0
- ⭐逻辑与 -- 短路规则
- 只要前面有假值,就不会往后走了【聪明人的做法】
- ⭐逻辑与 -- 短路规则
-
-
(接上 a = 1, b = 0)
-
输出 1:true
-
输出 2:a = 2, b = 0
- ⭐逻辑或 -- 长路规则
- 只要前面有一个真值,就不用了往后走了
- 要想执行后面的表达式,必须前面的表达式均为假值
- ⭐逻辑或 -- 长路规则
-
-
以空格为间隔(末尾无空格)的输出方式参考
- 方式一:如果不是第一次循环,输出前空格
- 首次更好得到:i == 0
- 可优化第 43 行:i && printf(" ");
- 方式二:如果不是最后一次循环,输出后空格
- 方式一:如果不是第一次循环,输出前空格
-
rand()
-
需引入 <stdlib.h>
-
输出的是随机的无符号数
- rand () % 100 可以随机输出 0-99 的数
-
⭐其实是固定的随机
-
-
通过 srand() 设定随机种子
-
srand(time(0))
- time(0)
- 获取当前的时间,需引入 <time.h>
- 精确到 s,1970.1.1 至今的秒数
- time(0)
-
可以看出值是变化的了,但计算机中没有真正意义上的随机
-
-
-
-
-
优化版
-
上述代码有三处可以简化!两个 if 都可以去掉
-
-
⭐~
- 位运算与取余的转换:% 2 等价于 & 1
- % (2 ^ n) 等价于 & (2 ^ n - 1)
- +1 与真值 1 的等价
- 逻辑与的短路规则
- 位运算与取余的转换:% 2 等价于 & 1
-
循环结构(7.cpp)#
判断回文整数(十进制)
-
-
记得对负数做特判!
-
如果想判断二进制下的回文整数呢?
-
将圆圈处都改成 2 即可
- 计算机底层其实都是以二进制存储数据的
- 不管什么进制下,都可以判断回文
-
base 进制下回文整数判断
-
-
计算整数位数(十进制)
- while 循环和 do...while 循环的细微区别比较
- 关键在于第一次循环的条件是否成立~
-
- 当输入非 0 数字时,digit 和 digit2 没有区别
-
- 当输入数字 0 时,有区别
-
-
所以有 0 的情况判断位数
* 用 do...while
* 或者特判
-
- 关键在于第一次循环的条件是否成立~
附加知识点#
-
switch 语句
- case 后面不允许声明变量
- default 的末尾也要加 break 吗
- 可有可无,加 break 是为了统一风格
- Python 不支持 switch 语句
-
C 语言中,如果 main 函数的末尾没有 return 语句将会有什么影响?
- C99 开始,基本没影响,标准要求等价于自动补上return 0;
- 而 C99 之前,如果控制抵达 main 函数结尾而没有 return,则为行为未定义。
-
浮点数判等
- 不直接用 ==,会不准确,利用差值是否小于某个极小值来判等
-
__builtin_expect () 还有 6 + 个兄弟
-
-
位权决定左移右移的倍数
- 十进制,左移一位,*10
- 二进制,左移一位,*2
思考点#
-
💡(解决)对于 5.if.cpp,即随堂练习 1。输入非法值,如字母‘q’,会陷入无限循环
-
与‘q’的 ASCII 码有关吗?
- 无关~
- 以 % d 形式输出‘q’,看起来是一个地址,单次运行是固定的,应该是初始化 n 时随机分配的一个值
- 如果输入正常值再输入错误值,此时该值就是之前的 n 值
- 因为其实没有读入值来改变 n
-
读不进‘q’值
- scanf 的返回值为 0
- 但是也不会移向下一个输入,类似第一讲里的 %[^\n] 遇到 \n 无限循环事件
-
⭐当然! 同样可以使用 getchar () 吞掉这个非法值
- 输入 qw 属于非法吗
- 是的,需要 getchar () 吞两次
- 输入 qw 属于非法吗
-
改后代码
-
-
getchar () 放在 if (!ret) 里更好,只有在读入非法字符时才吞字符
-
上图的代码:getchar会吞空白符和非法字符
-
下图的代码:只会吞非法字符
-
- 为了演示吞掉的字符没有空白符只有非法字符,这里加了 printf 函数
-
-
-
-
-
-
💡同样,对于 5.if.cpp(有循环,需手动停止),用 > output 将标准输出重定向时,会遇到输出不写入重定向的文件的问题
- 具体情况已在海贼 QA 提问,期待探讨:输出重定向在什么条件下才将输出写入文件呢?(stdout、./a.out > output、Ctrl+C/Ctrl+D)
- Ctrl+C 不写入,Ctrl+D 写入
Ctrl+C:Linux 下默认的中断键,当键入此键时,系统会发送一个中断信号给正在运行的程序和 shell。
Ctrl+D:Linux 下标准输入输出的EOF。在使用标准输入输出的设备中,遇到该符号,程序会认为读到了文件的末尾,因此结束输入或输出。
- 如果有非法输入导致有大量输出时,Ctrl+C 也可以写入
- 疑问:这是因为缓存不够,提前强制输出到 output 文件了吗?
- 是这样理解
- 疑问:那么这部分写入 output 文件的输出仅仅是缓存溢出的输出,还是 Ctrl+C 前的所有输出呢?
- 溢出的
- 疑问:这是因为缓存不够,提前强制输出到 output 文件了吗?
- 代码 5.switch.cpp:为啥输入字母之类的会无限循环?
- 同 5.if.cpp,见思考点 1
Tips#
- OJ 做题方式
- 在 vim 里编写代码
- cat 显示代码复制出来
- 提交
- 经典题:已知年月日判断是否合理
- 有没有必要用那么多的 if else?
- 可以用空间换时间:把每月的天数用数组存起来
- 参考工具书第 6 章 6.4 节