智能程序设计(C语言)


数据类型深化

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • 类型修饰符与取值范围

  • 整型溢出与字符运算 (深化)

  • 浮点数的精度与近似存储

  • 赋值细节

  • 常量与符号常量

  • 案例: Circle 与 Sphere


类型修饰符

修饰符: 改变整数类型的取值范围, 或是否允许负号

修饰符 含义
short / long 短整型 / 长整型
signed 有符号 (默认, 可表示负数)
unsigned 无符号 (只表示 0 与正数, 范围更大)

常见组合: short int, long int, unsigned int, unsigned long

  • 位数: short ≤ int ≤ long


sizeof 运算符

用 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 小时钟

  • 钟面是"模 12"的数: 1~12 首尾相接; 12 ≡ 0, "12 + 1 = 1" 其实是 0 + 1

  • 绕圈是改名造成的: 计数照常, 只是把 0 叫成 12


4 位补码环: 溢出就是绕圈

  • 和左图的钟同一结构: 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 表示红色)


字符与 ASCII: char 其实是小整数

上一周知道 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 与 double 的精度

  • 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


浮点数0.75内存中二进制如何表示(IEEE 754)?

  1. 拆成 2 的幂: 0.75 = 0.5 + 0.25 = 1×2⁻¹ + 1×2⁻² = 0.11₂
  2. 规范化: 0.11₂ = 1.1 × 2⁻¹
  3. 三个字段:
    • 符号位: 0 (正数)
    • 指数位: -1 → 加 127 = 126 = 01111110
    • 尾数位: 小数部分 1, 后面补 0 到 23 位 (隐含的 1 不存)
  4. 存储 (32 位), 三个字段对号入座:
   符号位   指数位         尾数位 (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 附近密、越远越稀——下一页放大看为什么


为什么 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)   /* 安全: 带括号 */

演示: Circle

给定一个圆的半径 (如 \(10\)), 计算其周长和面积

\(L = 2\pi r \qquad S = \pi r^2\)

  • 每个结果各占一行

  • 小数点后保留两位

打开 CLion, 一起写 circle.c——下一页是参考代码


演示: Circle (参考代码)

/*
 * 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;
}

演示: Sphere

给定一个球的半径 (如 \(100\)), 计算其表面积和体积

\(A = 4 \pi r^2 \qquad V = \frac{4}{3} \pi r^3\)

  • 每个结果占 \(1\) 行, 小数点后保留 \(4\) 位

  • 每个结果至少占 \(15\) 字符, 左对齐

同样打开 CLion, 一起写 sphere.c——下一页是参考代码


演示: Sphere (参考代码)

/*
 * 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);
}

课堂小测

  1. unsigned int 与 int 的区别是什么?查看类型占用字节数用什么运算符?
  2. 用 #define 定义符号常量时行末要不要分号?const 呢?
  3. INT_MAX 在哪个头文件中定义?它代表什么?
  4. 计算圆的面积时为什么要用符号常量 PI 而不是直接写 3.14159?


周三见!


未完待续