类型修饰符与取值范围
整型溢出与字符运算 (深化)
浮点数的精度与近似存储
赋值细节
常量与符号常量
案例: Circle 与 Sphere
修饰符: 改变整数类型的取值范围, 或是否允许负号
| 修饰符 | 含义 |
|---|---|
short / long |
短整型 / 长整型 |
signed |
有符号 (默认, 可表示负数) |
unsigned |
无符号 (只表示 0 与正数, 范围更大) |
常见组合: short int, long int, unsigned int, unsigned long
位数: short ≤ int ≤ long
用 sizeof 运算符可查看类型占用的字节数
sizeof 用法: sizeof(int) 类型 / sizeof x 变量 (括号可省)
printf("%zu %zu %zu\n", sizeof(int), sizeof(double), sizeof(char));
打印 sizeof 的结果用 %zu (它不是 int)
int 通常是 32 位(4 字节), 最大整数 \(2^{31}-1\) = 2 147 483 647(约 21 亿)
INT_MAX / INT_MIN 定义在 limits.h 中
#include <stdio.h>
#include <limits.h>
int main(void) {
int x = INT_MAX;
printf("INT_MAX = %d\n", INT_MAX);
printf("INT_MAX + 1 = %d\n", x + 1); /* 溢出! */
return 0;
}
溢出不会报错, 会"绕回"成负数——C 不做检查; 用 sizeof 查看类型字节数 (呼应修饰符页)
补码 = 按位取反再加 1 (5 与 -5):
int x = 5; 00000000 00000000 00000000 00000101
int y = -5; 11111111 11111111 11111111 11111011
为什么 -1 是全 1 (0 减 1 借位到底):
00000000 00000000 00000000 00000000
- 00000000 00000000 00000000 00000001
---------------------------------------
11111111 11111111 11111111 11111111
最高位是符号位: 0 表示正, 1 表示负 (对 signed 而言)
上一页的"绕回"是怎么来的? 先看负数怎么存——整数用 二进制, 负数用 补码
为什么用补码? 让减法变成加法 (\(5 + (-5)\) 直接相加得 0), 电路更简单
同样 32 位, 解释方式不同, 能表示的范围就不同:
int (有符号): \(-2^{31}\) ~ \(2^{31}-1\) (约 ±21 亿)
unsigned int (无符号): \(0\) ~ \(2^{32}-1\) (约 42 亿)
本页了解即可: 知道"负数用补码、最高位是符号位"就够了
钟面是"模 12"的数: 1~12 首尾相接; 12 ≡ 0, "12 + 1 = 1" 其实是 0 + 1
绕圈是改名造成的: 计数照常, 只是把 0 叫成 12
和左图的钟同一结构: 0 起共 16 格, 加 1 顺时针走一格; 负数左半圈, 正数右半圈
溢出 = 跨过底部分界: 0111 + 1 = 1000 (7 → -8), 反向 -8 - 1 = 7; 顶部 -1 + 1 = 0 回原点; C 不检查溢出
同一个整数有三种写法:
int a = 17; /* 十进制 */
int b = 021; /* 八进制: 以 0 开头, 021 = 17 */
int c = 0x11; /* 十六进制: 以 0x 开头, 0x11 = 17 */
坑: 010 不是 10, 而是 8 (前导 0 表示八进制)
打印时用不同转换说明看同一数字的"不同面孔":
printf("%d %o %x\n", 17, 17, 17); /* 输出: 17 21 11 */
十六进制在位运算、内存地址、颜色码中很常见 (如 0xFF0000 表示红色)
上一周知道 char 存的是编码; 今天把它当整数用
'a'=97, 'A'=65, '0'=48, ' '=32——大写字母与小写字母差 32
同一个变量, %c 打字符 / %d 打编码
char ch = 'a';
printf("%c %d\n", ch, ch); /* a 97 */
printf("%c\n", ch - 32); /* A */
printf("%d\n", '9' - '0'); /* 9 */
常用技巧: toupper/tolower (ctype.h); '9'-'0' 把字符数字变成整数
Hex 列是两位 16 进制; 大写 + 32 = 小写 (如 0x41 + 0x20 = 0x61)
float: 约 6 位有效数字; double: 约 15 位有效数字
float f = 1.0f / 3.0f; /* 0.333333 */
double d = 1.0 / 3.0; /* 0.333333333333333 */
为什么默认用 double? 精度高, 且字面量 3.14 本来就是 double
float 字面量要加 f 后缀 (1.0f); 不加就是 double (赋给 float 会有转换警告)
float 一般只在内存紧张时用 (如嵌入式)
浮点运算有误差: 0.1 + 0.2 不等于 0.3——第 3 周细讲
浮点数按 IEEE 754 存储——"二进制的科学计数法": 符号、有效数字、指数三部分
| 类型 | 总位数 | 符号位 | 指数位 | 尾数位 |
|---|---|---|---|---|
| float | 32 | 1 | 8 | 23 |
| double | 64 | 1 | 11 | 52 |
规范化: 正常数都存成 \(1.b_1b_2\cdots b_{23} \times 2^e\) (每位是 0 或 1, 共 23 位)——开头的 1 是必然的, 隐含不存, 白赚一位精度
指数用移码存 (加 127): 让位模式顺序 == 数值顺序——比较浮点数可直接当整数比, 硬件更简单
位数决定精度: 二进制 \(n\) 位 ≈ \(n \times \log_{10}2\) ≈ \(n \times 0.301\) 个十进制位; float 23+1=24 位 → ≈7.2, 保守说 6-7 位; double 53 位 → ≈16, 说 15-16 位
下一页: 亲手验算一个 float
符号位 指数位 尾数位 (23 位)
↓ ↓ ↓
0 01111110 10000000000000000000000
↑
正 (0)
5. 反推验算:
指数: 01111110 = 126, 减偏移 127 → -1
尾数: 补回隐含的 1 → 1.10000000000000000000000₂
结果: 1.1₂ × 2⁻¹ = 0.11₂ = 0.75 ✓
32 位图案一共只有 2³² ≈ 42.9 亿个——而数轴上的数是无限的, 绝大多数数都表示不出来
比如 0.1: 只能存成离它最近的那个可表示数 (就近截断)
结论: 浮点是近似存储; 比较浮点数不要用 ==, 用 fabs(a - b) < 1e-9 (fabs 是 math.h 的绝对值函数, 第 3 周细讲)
0 附近密、越远越稀——下一页放大看为什么
1 到 2 之间有 \(2^{23}\) ≈ 840 万个 float; 2 到 4 之间也只有 840 万个——区间长度翻倍, 格子数不变
每个格子"管"的范围翻倍 → 离 0 越远越稀疏; 约一半的 float 都挤在 -1 到 1 之间
| 区间 | 形式 | 指数 e |
|---|---|---|
| [1/2, 1) | \(1.b_1\cdots b_{23} \times 2^{-1}\) | -1 |
| (1/4, 1/2), (1/8, 1/4), … | \(1.b_1\cdots b_{23} \times 2^{e}\), 一直对半切到 \(2^{-126}\) | -2, -3, …, -126 |
| (0, \(2^{-126}\)) | \(0.b_1\cdots b_{23} \times 2^{-126}\) (非规格化数) | 固定 -126 |
| [1, 2) | \(1.b_1\cdots b_{23} \times 2^{0}\) | 0 |
| [2, 4) | \(1.b_1\cdots b_{23} \times 2^{1}\) | +1 |
| [4, 8) | \(1.b_1\cdots b_{23} \times 2^{2}\) | +2 |
指数段全 0 与全 1 被保留; 浮点数的边界值也都落在它们旁边:
| 名字 | 值 | 指数段 | 尾数段 |
|---|---|---|---|
| 最大正规数 | ≈ \(3.4 \times 10^{38}\) | 11111110 | 全 1 |
| 最小正规数 (负) | ≈ \(-3.4 \times 10^{38}\) | 11111110 | 全 1 |
| 最小正正规数 | \(2^{-126}\) ≈ \(1.18 \times 10^{-38}\) | 00000001 | 全 0 |
| 最小正数 | \(2^{-149}\) ≈ \(1.4 \times 10^{-45}\) | 00000000 | 000…001 |
0 / -0.0 |
0 | 全 0 | 全 0 |
| 非规格化数 | \(0.b_1\cdots b_{23} \times 2^{-126}\) | 全 0 | 非 0 |
±∞ |
无穷 | 全 1 | 全 0 |
NaN |
非数 | 全 1 | 非 0 |
两个保留编码落在位模式的两个极端 (移码的第二个好处); 最大数接近 \(2 \times 2^{127}\) 而非 \(1 \times 2^{127}\), 最小正正规数是 \(2^{-126}\) 而非 \(2^{-127}\)
赋值: 通过赋值的方式获得值
height = 8;
length = 12;
width = 10;
gender = 'M';
浮点常量的小尾巴: float 加 f, double 不加
float profit = 2150.48f; /* float 加 f */
double pi = 3.14159; /* double 不加 f */
赋的值超出目标类型范围时, 结果无意义 (不会自动截到最大/最小值):
char c = 10000; /* 无意义 */
int i = 1.0e20; /* 无意义: 超出 int 范围 */
float f = 1.0e100; /* 无意义: 超出 float 范围 */
常量: 值已知且不能改变的量 (8, 12, 'M'); 符号常量: 给常量起名字, 提高可读性、便于修改
宏定义: 预处理命令, 结尾没有分号
#define PI 3.14159
#define GRAM_PER_MOL 32
const限定: 声明为只读变量
const double MOL = 6.02E23;
const int GRAM_PER_MOL = 32;
宏里如果含运算符, 整体要加括号, 否则替换后会因优先级出错:
#define SCALE_FACTOR (5.0f / 9.0f) /* 安全: 带括号 */
给定一个圆的半径 (如 \(10\)), 计算其周长和面积
\(L = 2\pi r \qquad S = \pi r^2\)
每个结果各占一行
小数点后保留两位
打开 CLion, 一起写 circle.c——下一页是参考代码
/*
* circle.c(计算周长和面积)
*/
#include <stdio.h>
int main(void) {
int radius = 10;
double circumference = 2 * 3.14159 * radius;
double area = 3.14159 * radius * radius;
// .2: precision 小数点后保留几位
printf("circumference = %.2f\narea = %.2f\n", circumference, area);
return 0;
}
给定一个球的半径 (如 \(100\)), 计算其表面积和体积
\(A = 4 \pi r^2 \qquad V = \frac{4}{3} \pi r^3\)
每个结果占 \(1\) 行, 小数点后保留 \(4\) 位
每个结果至少占 \(15\) 字符, 左对齐
同样打开 CLion, 一起写 sphere.c——下一页是参考代码
/*
* sphere.c(球的体积和面积)
*/
#include <stdio.h>
int main() {
int radius = 100;
double surface_area = 4 * 3.14159 * radius * radius;
double volume = 4.0 / 3 * 3.14159 * radius * radius * radius;
// 15: width
printf("%-15.4f : surface_area\n%-15.4f : volume\n",
surface_area, volume);
}
unsigned int 与 int 的区别是什么?查看类型占用字节数用什么运算符?
#define 定义符号常量时行末要不要分号?const 呢?
INT_MAX 在哪个头文件中定义?它代表什么?
PI 而不是直接写 3.14159?