预备知识
先补一下上一篇没提到的编程前基础知识吧,这部分穿插我自己的理解以及AI-generated content
进制转化
除基取余:用带余除法,除以基数(二进制基数为2),商进行同样操作,最后得到的所有余数倒序读,即是二进制数
乘积取整:遇到小数的情况,字面意思,乘以基数(2)之后去整数部分,新的小数部分采取同样操作知道没有小数部分,最后得到的整数部分正序读(但是不是所有小数都能转尽,如0.1)
八二互转:1位八进制数可以转为3位二进制数
二进制表示方式:原码、反码与补码 | 浮点数 | 字符编码
原码
首位表示正负,规定0为正1为负,那么用8位来表示一个数字可以如下
+5 = 0000 0101
-5 = 1000 0101 ← 最高位变 1,其余不变
缺点:
1.有两个0,正负各一个
2.符号位不能直接参与运算,得单独判断符号、比较大小、决定做加法还是减法,电路复杂
反码
正数同原码,负数取其相反数的全反,如下
+5 = 0000 0101
-5 = 1111 1010
优点:减法可以用加法替代了
缺点:还是有两个0,且多了循环进位,如5+(-5)=1 1111 1111
补码:现代计算机所使用的码
正数同原码,负数为反码+1
+5 = 0000 0101
-5 = 1111 1011
优点
1.只有一个0
2.加减法完全统一,溢出位直接舍弃,如5+(-5)=0
0000 0101
- 1111 1011
1 0000 0000 ← 最高位溢出丢弃
浮点数
本质是科学计数法的二进制版,由符号,指数,尾数组成,如1.0101*10²,其中符号为正(0),指数为2,尾数为1.0101
| 1 位 | 8 位 | 23 位 | ←float(32精度)
| 符号 | 指数 | 尾数 |
S E M
| 1 位 | 11 位 | 52 位 | ←double(64双精度)
| 符号 | 指数 | 尾数 |
S E M
指数位 E
指数不能直接存,要加一个偏移量(bias):
float:偏移 127,实际指数 = E - 127
double:偏移 1023,实际指数 = E - 1023
为什么加偏移?为了让指数也能表示负数,且方便比较大小,避开全0和全1
尾数位 M
这里有个隐含的 1:
二进制科学计数法规范化后,尾数总是 1.xxxxx,所以开头的 1 不用存,只存小数点后的部分。
float 存 23 位,实际精度是 24 位(隐含的 1 + 23 位)。
double 存 52 位,实际精度是 53 位。
!!浮点数并不精确
很多十进制小数无法用二进制精确表示,如上面提到的0.1,0.1 的二进制是 0.0001100110011… 无限循环,只能截断存储,所以有误差
That’s why:
if(0.1+0.2==0.3)// ❌ 结果是 false!printf("相等\n");right way:
if(fabs(a-b)<1e-9)// 差小于误差范围就算相等printf("相等\n");编程中的实际影响
1.scanf 读 double 必须用 %lf
2.浮点数不能用 == 比较
3.大数 + 小数会"吃掉"小数,如double x = 1e20 ; x = x + 1.0;
x的值还是1e20
4.0.0有正负之分
1.0 / 0.0 // +∞
1.0 / -0.0 // -∞
字符编码
字符也是数字,用编码表把字符和数字对应起来,如’A’对应65
常用ASCⅡ编码表,char就是按ASCⅡ存的,需要记忆:
‘A’ = 65,‘a’ = 97,两者差 32
‘0’ = 48
缺点:
1.只有 128 个字符,只够英语用。
2.存不下中文、日文、阿拉伯文、emoji 等。
3.各国自己扩展(比如中文 GBK),但互相冲突,同一串字节在不同编码下意思不同,乱码由此而来。
另外的编码表还有Unicode(统一码、万国码)
关键点
1.Unicode 只是一张巨大的表,规定「哪个字符对应哪个编号」。
2.它不规定怎么存储!这是最容易搞混的地方。
3.编号范围:U+0000 到 U+10FFFF,能容纳 100 多万个字符。
4.兼容ASCⅡ,前128个码点与其相同
常见误区
1.「Unicode 是 2 字节」——错!
2.Unicode 只是编号表,不是存储格式。怎么存要看 UTF-8、UTF-16 等具体方案。常用的UTF-8 是 Unicode 的一种具体存储/编码方式
乱码的根源:用 A 编码存,用 B 编码读。比如 UTF-8 的中文用 GBK 打开,就成了「锟斤拷」之类
冯·诺伊曼系统
运算器,控制器,储存器,输入设备,输出设备
别的懒得写的,查AI吧
指令、程序与程序设计语言
指令
指令:CPU 能直接识别并执行的最小操作命令,通常包含操作码与操作数
指令在内存里也是二进制
一台 CPU 能执行的所有指令的集合,叫指令系统,也叫指令集架构(ISA)
指令系统是硬件和软件的接口。编译器生成代码时,必须按目标 CPU 的指令系统来生成。
常见指令系统:x86、x86-64、ARM、RISC-V、MIPS
程序
程序 = 指令 + 数据,按一定顺序排列,用来完成某个任务
程序在内存里:
地址 内容
1000 指令1
1004 指令2
1008 指令3
…
2000 数据1
2004 数据2
程序 = 数据结构 + 算法
这是经典说法:
数据结构:数据怎么组织(数组、链表、树)
算法:怎么处理数据(步骤)
程序设计语言
机器语言(第一代)
直接是二进制:10110000 00000001
优点:CPU 直接执行,最快。
缺点:人看不懂、写起来痛苦、不可移植。汇编语言(第二代)
用助记符代替二进制
优点:比机器语言好读。
缺点:还是和具体 CPU 绑定,可移植差。
需要汇编程序翻译成机器码。高级语言(第三代及以后)
接近自然语言和数学:
优点:好写、好读、可移植。
缺点:需要翻译成机器码。
例子:C、C++、Java、Python、Go、JavaScript。
按执行方式分类
编译型 整体翻译成机器码,运行快 C、C++、Go
解释型 逐行解释执行,灵活但慢 Python、JavaScript
混合型 先编译成中间码,再解释/JIT Java、C#
按范式分类
面向过程:C、Pascal
面向对象:C++、Java、Python
函数式:Haskell、Lisp
逻辑式:Prolog
从源程序到可执行程序
hello.c → [预处理] → hello.i
→ [编译] → hello.s
→ [汇编] → hello.o
→ [链接] → hello (可执行文件)
预处理(Preprocessing)
工具:预处理器
做什么:
展开 #include(把头文件内容贴进来)
展开 #define 宏
处理 #ifdef 等条件编译
去掉注释
输入:hello.c → 输出:hello.i(还是文本)编译(Compilation)
工具:编译器
做什么:
词法分析、语法分析、语义分析
优化
生成汇编代码
输入:hello.i → 输出:hello.s(汇编文本)汇编(Assembly)
工具:汇编器
做什么:把汇编代码翻译成机器码,生成目标文件。
输入:hello.s → 输出:hello.o(二进制目标文件)
注意:.o 文件已经是机器码了,但还不能运行,因为里面调用的 printf 等还没接上。链接(Linking)
工具:链接器
做什么:
把多个 .o 文件和库文件合并
解决函数、变量的地址引用
生成最终可执行文件
输入:hello.o + 库 → 输出:hello(可执行文件)
上课时遇到的一些题目
判断(以下均为正确)
1.在 C 程序运行过程中,其值不能被改变的量称为常量,其值可以改变的量称为变量
2.在 C 程序中,APH 和 aph 代表不同的变量
3.实型数据不可以进行求余数运算 %
4.任何表达式语句都是表达式加分号组成的
5.合法的用户自定义标识符不能与 C 语言关键字重名,但可以和库函数名重名(但是会覆盖库函数名,不推荐)
6.数据类型限定了变量的取值范围,超出范围会出现数据溢出问题(如int只有32位)
7.C 语言标识符只能由字母、数字、下划线构成,不能含有空格,且数字不能放在首位
8.赋值表达式a=5本身有值,表达式的值就是 5
9.C 语言中变量必须先定义、后使用,未经定义的变量无法直接参与运算和赋值(虽然也可以用,但是规范如此,不声明会warning)
10.C 语言中空白字符(空格、换行)不可以随意添加,有时会影响程序语法正确性,例如int a b;就是语法错误
11.算法可以没有输入,但是一定要有输出(输入可以在内部规定,但是没有输出算法就没有意义)
12.else 的个数不能多于 if 的个数,且else对应的是上面距离它最近的未与else配对的if
13.C程序由函数组成
14.‘D’ - ‘A’ == ‘d’ - 'a’的值为1
选择
题目:以下不属于 C 语言基本数据类型作用的是__D____。
A. 规定变量内存占用大小
B. 限定变量取值范围
C. 确定数据存储精度
D. 提高程序运行速度
作业:计算油费
现在90号汽油6.95元/升、93号汽油7.44元/升、97号汽油7.93元/升。为吸引顾客,某自动加油站推出了“自助服务”和“协助服务”两个服务等级,分别可得到5%和3%的折扣。
本题要求编写程序,根据输入顾客的加油量a,汽油品种b(90、93或97)和服务类型c(m - 自助,e - 协助),计算并输出应付款。
输入格式:
输入在一行中给出两个整数和一个字符,分别表示顾客的加油量a,汽油品种b(90、93或97)和服务类型c(m - 自助,e - 协助)。
输出格式:
在一行中输出应付款额,保留小数点后2位。
#include<stdio.h>intmain(){inta,b;charc;doubleprice,unit;scanf("%d %d %c",&a,&b,&c);switch(b){case90:unit=6.95;switch(c){case'm':price=a*0.95*unit;break;case'e':price=a*0.97*unit;}break;case93:unit=7.44;switch(c){case'm':price=a*0.95*unit;break;case'e':price=a*0.97*unit;}break;case97:unit=7.93;switch(c){case'm':price=a*0.95*unit;break;case'e':price=a*0.97*unit;}break;}printf("%.2f",price);return0;}知识点碎碎念
1.getchar与putchar
getchar(): 从标准输入(键盘)读入一个字符,返回它的 ASCII 码(int 类型,不是char),没有参数
getchar 是行缓冲的:你输入的内容会先存在缓冲区,按回车后才真正被读取,且会读取缓冲区最前面的字符(FIFO,先进先出的队列)
putchar(): 把一个字符写到标准输出(屏幕),括号里面可以填单字符如’A’,可以填int,转换为ASCⅡ所对应的字符
经典用法
1).回显输入
intc;while((c=getchar())!=EOF){putchar(c);}2).统计字符数
intc,count=0;while((c=getchar())!=EOF){count++;}printf("共 %d 个字符\n",count);3).清空缓冲区
while(getchar()!='\n');//空循环体常见错误:
1).getchar 返回值用 char 接 → 可能无法正确判断 EOF,用 int。
2).忘记 EOF 判断 → 读到结束还在处理。
3).scanf 后直接 getchar → 读到残留的 ‘\n’。
4).putchar 传字符串 → putchar(“A”) 是错的,要传字符 putchar(‘A’)。
5).getchar 一次只读一个字符 → 想读整行要循环
2.switch
形式:
switch (表达式) {
case 值1:
语句;
break;
case 值2:
语句;
break;
…
default:
语句;
break;
}
注意switch只能判断整型数,case后面只能是常量
执行逻辑:算出 表达式 的值,然后跳转到匹配的 case 开始执行,直到遇到 break 或 switch 结束
break 用来结束整个 switch。如果没有 break,会"穿透"到下一个 case 继续执行,易错:漏写break导致穿透。但是要可以故意不写break,以执行多重判断
default分支:default 处理所有 case 都不匹配的情况,相当于 else
可以不加default,但是推荐加,应对考虑外情况
与if的区别:
判断"等于某个具体值"且分支多 → 用 switch。
判断范围、复杂逻辑(> < && ||)→ 用 if。
3.%nd,n为某个数字
如果数字本身位数 < 指定宽度 2:左边补空格,凑够 2 位
printf(“%2d”,5); → 输出: 5(前面有 1 个空格)
如果数字本身位数 > 指定宽度 2:不受 2 限制,原样全部输出,不会截断!
printf(“%2d”,123); → 输出:123(3 位 > 2,直接输出)
只是最小宽度,数字长于宽度就原样输出!
区分:
%2d:最小宽度 2,不够左边补空格
%-2d:负号 = 左对齐,不够宽度右边补空格
printf(“%-2d”,5); → 5 (5 后面带空格)
4.测试用例:一组“输入数据 + 预期输出结果”的组合,用来测试程序是否符合期望