Wiki LogoWiki - The Power of Many

Week 02: 数据类型与运算符

深入理解 C 语言的数据类型、类型转换规则、算术运算符与位运算符.

1. 整数类型详解

1.1 整数的存储

计算机使用二进制补码表示有符号整数:

类型表示方式
正数原码 = 补码
负数补码 = 反码 + 1
// 以 8 位 (char) 为例
// 5 的补码:  00000101
// -5 的补码: 11111011

int8_t x = 5;    // 二进制: 00000101
int8_t y = -5;   // 二进制: 11111011

// 验证: 5 + (-5) = 0
// 00000101 + 11111011 = 100000000 (溢出舍弃最高位) = 00000000

1.2 整数溢出

#include <stdio.h>
#include <stdint.h>
#include <limits.h>

int main(void) {
    int8_t x = 127;
    printf("x = %d\n", x);      // 127
    
    x = x + 1;                   // 有符号溢出 (未定义行为)
    printf("x + 1 = %d\n", x);  // -128 (通常结果)
    
    uint8_t y = 255;
    y = y + 1;                   // 无符号溢出 (定义良好, 回绕)
    printf("y + 1 = %u\n", y);  // 0
    
    return 0;
}

有符号整数溢出是未定义行为 (UB), 编译器可能做出意外优化.

1.3 类型选择指南

场景推荐类型
计数、索引size_t (无符号)
一般整数int
需要具体位宽int32_t, uint64_t
字节操作uint8_t
布尔值bool (C99)
文件大小off_t, size_t

1.4 布尔类型 (C99)

C99 引入 _Bool 类型及 <stdbool.h> 头文件:

#include <stdbool.h>

int main(void) {
    bool flag = true;   // stdbool.h 定义 bool, true, false
    _Bool raw = 1;      // 原始类型名

    // _Bool 自动转换: 非零为 1, 零为 0
    _Bool b1 = 42;      // b1 = 1
    _Bool b2 = 0;       // b2 = 0
    _Bool b3 = -5;      // b3 = 1

    // sizeof
    printf("sizeof(bool) = %zu\n", sizeof(bool));  // 通常为 1

    // 在条件中使用
    if (flag) {
        printf("flag is true\n");
    }

    return 0;
}

C23 变化: bool, true, false 成为关键字, 不再需要 <stdbool.h>.

1.5 C23 类型新特性

1. constexpr 对象

C23 引入编译期常量, 类似 C++ 的 constexpr:

constexpr int SIZE = 100;
int arr[SIZE];  // SIZE 是编译期常量

constexpr double PI = 3.14159265358979;

// 必须用常量表达式初始化
// constexpr int x = rand();  // 错误!

2. typeoftypeof_unqual

获取表达式的类型:

int x = 10;
typeof(x) y = 20;       // y 是 int

const int cx = 10;
typeof(cx) cy = 20;     // cy 是 const int
typeof_unqual(cx) z = 30;  // z 是 int (去掉 const)

// 实用场景: 泛型宏
#define SWAP(a, b) do { \
    typeof(a) _tmp = (a); \
    (a) = (b); \
    (b) = _tmp; \
} while(0)

3. _BitInt(N) 任意位宽整数

// 精确位宽的整数类型
_BitInt(128) big = 12345678901234567890wb;
unsigned _BitInt(256) huge = 0;

// 获取最大位宽
#include <limits.h>
printf("Max BitInt width: %d\n", BITINT_MAXWIDTH);

// 字面量后缀: wb (有符号), uwb (无符号)
_BitInt(64) a = 100wb;
unsigned _BitInt(64) b = 100uwb;

4. nullptr 空指针常量

int *p = nullptr;  // 替代 NULL
// nullptr 有类型 nullptr_t, 比 NULL 更类型安全

#include <stddef.h>
nullptr_t np = nullptr;

2. 浮点类型详解

2.1 IEEE 754 表示

浮点数使用科学计数法存储:

(-1)^符号位 × 1.尾数 × 2^(指数-偏移)
类型符号指数尾数总位数
float182332
double1115264

2.2 浮点精度问题

#include <stdio.h>

int main(void) {
    float f = 0.1f;
    double d = 0.1;
    
    // 0.1 无法精确表示为二进制小数
    printf("0.1f = %.20f\n", f);  // 0.10000000149011611938
    printf("0.1  = %.20lf\n", d); // 0.10000000000000000555
    
    // 累积误差
    float sum = 0.0f;
    for (int i = 0; i < 10; i++) {
        sum += 0.1f;
    }
    printf("0.1 * 10 = %.20f\n", sum);  // 不精确等于 1.0
    
    // 比较浮点数
    if (sum == 1.0f) {
        printf("Equal\n");
    } else {
        printf("Not equal\n");  // 可能输出这个
    }
    
    return 0;
}

2.3 浮点比较

#include <math.h>
#include <float.h>

// 使用 epsilon 比较
int float_equal(float a, float b) {
    return fabs(a - b) < FLT_EPSILON;
}

// 相对误差比较
int float_near(float a, float b, float rel_epsilon) {
    return fabs(a - b) <= rel_epsilon * fmax(fabs(a), fabs(b));
}

2.4 特殊值

#include <math.h>

float inf = 1.0f / 0.0f;       // 正无穷
float neg_inf = -1.0f / 0.0f;  // 负无穷
float nan_val = 0.0f / 0.0f;   // NaN (Not a Number)

printf("inf: %f\n", inf);       // inf
printf("-inf: %f\n", neg_inf);  // -inf
printf("nan: %f\n", nan_val);   // nan

// 检测
isinf(inf);      // 非零
isnan(nan_val);  // 非零

// NaN 的特殊性质
nan_val == nan_val;  // 0 (false)! NaN 不等于任何值, 包括自己

3. 类型转换

3.1 隐式转换 (自动)

int i = 10;
double d = i;      // int → double (安全, 自动)

double x = 3.14;
int y = x;         // double → int (截断, 警告)

3.2 算术转换规则

当运算符两边类型不同时, 进行常规算术转换:

long double > double > float > unsigned long long > long long > 
unsigned long > long > unsigned int > int
int i = 5;
double d = 2.5;
double result = i + d;  // i 转换为 double

3.3 整数提升

小于 int 的类型在运算时提升为 int:

char a = 10;
char b = 20;
char c = a + b;  // a 和 b 先提升为 int, 计算后转回 char

// 潜在问题
uint8_t x = 200;
uint8_t y = 200;
uint16_t z = x + y;  // 正确: 400 (先提升为 int)

uint8_t w = x + y;   // 截断: 400 - 256 = 144

3.4 显式转换 (强制)

double d = 3.14;
int i = (int)d;      // 强制转换, 截断小数部分

// 指针转换
int *p = (int *)malloc(sizeof(int));

// 危险的转换
float f = 3.14f;
int *bad = (int *)&f;  // 类型双关, 可能违反严格别名规则

3.5 有符号与无符号转换

int i = -1;
unsigned int u = i;  // 二进制不变, 解释方式改变

printf("%d\n", i);   // -1
printf("%u\n", u);   // 4294967295 (2^32 - 1)

// 危险的比较
if (i < u) {
    printf("i < u\n");
} else {
    printf("i >= u\n");  // 输出这个! -1 被转换为很大的无符号数
}

4. 算术运算符

4.1 基本运算符

运算符描述示例
+加法5 + 38
-减法5 - 32
*乘法5 * 315
/除法5 / 31 (整除)
%取模5 % 32

4.2 整数除法

int a = 7, b = 3;
int q = a / b;   // 商: 2 (向零截断)
int r = a % b;   // 余数: 1

// C99 规定: a == (a/b)*b + a%b
// 7 == 2*3 + 1

// 负数情况
int c = -7, d = 3;
printf("-7 / 3 = %d\n", c / d);   // -2 (向零截断)
printf("-7 %% 3 = %d\n", c % d);  // -1

4.3 自增与自减

int x = 5;

// 前缀: 先增后用
int a = ++x;  // x = 6, a = 6

// 后缀: 先用后增
int b = x++;  // b = 6, x = 7

// 避免在同一表达式中多次修改
int y = 5;
// int z = y++ + ++y;  // 未定义行为!

4.4 复合赋值

int x = 10;
x += 5;   // 等价于 x = x + 5
x -= 3;   // 等价于 x = x - 3
x *= 2;   // 等价于 x = x * 2
x /= 4;   // 等价于 x = x / 4
x %= 3;   // 等价于 x = x % 3

5. 比较与逻辑运算符

5.1 比较运算符

运算符描述
==等于
!=不等于
<小于
>大于
<=小于等于
>=大于等于
int a = 5, b = 3;
int result = (a > b);  // 1 (true)

5.2 逻辑运算符

运算符描述
&&逻辑与
`
!逻辑非
int a = 5, b = 0;

if (a && b) { }     // false (0)
if (a || b) { }     // true (1)
if (!b) { }         // true (1)

5.3 短路求值

int a = 0;
int b = 5;

// && 短路: a 为 0, 不计算 b
if (a && (b = 10)) { }
printf("b = %d\n", b);  // 5 (b 未被修改)

// || 短路: a 为 0 (false), 需要计算 b
if (a || (b = 10)) { }
printf("b = %d\n", b);  // 10

// 常见用法: 避免空指针解引用
if (ptr != NULL && *ptr > 0) { }

6. 位运算符

6.1 按位运算符

运算符描述示例
&按位与5 & 31
``按位或
^按位异或5 ^ 36
~按位取反~5-6
<<左移5 << 110
>>右移5 >> 12

6.2 位运算示例

//   5 = 0101
//   3 = 0011
// --------
// & : 0001 = 1
// | : 0111 = 7
// ^ : 0110 = 6

uint8_t a = 5, b = 3;
printf("5 & 3 = %u\n", a & b);  // 1
printf("5 | 3 = %u\n", a | b);  // 7
printf("5 ^ 3 = %u\n", a ^ b);  // 6
printf("~5 = %d\n", ~a);        // -6 (0xFA 解释为有符号)

6.3 移位运算

uint8_t x = 5;  // 00000101

// 左移: 相当于乘以 2^n
printf("5 << 1 = %u\n", x << 1);  // 10 (00001010)
printf("5 << 2 = %u\n", x << 2);  // 20 (00010100)

// 右移: 相当于除以 2^n
printf("5 >> 1 = %u\n", x >> 1);  // 2 (00000010)

// 有符号右移: 符号扩展
int8_t y = -8;  // 11111000
printf("-8 >> 1 = %d\n", y >> 1);  // -4 (11111100)

6.4 位操作技巧

// 设置第 n 位
x |= (1 << n);

// 清除第 n 位
x &= ~(1 << n);

// 切换第 n 位
x ^= (1 << n);

// 检查第 n 位
if (x & (1 << n)) { }

// 获取最低位的 1
x & (-x);

// 清除最低位的 1
x & (x - 1);

// 判断是否为 2 的幂
(x != 0) && ((x & (x - 1)) == 0);

6.5 位域标志

#define FLAG_READ   (1 << 0)   // 0001
#define FLAG_WRITE  (1 << 1)   // 0010
#define FLAG_EXEC   (1 << 2)   // 0100

unsigned int flags = 0;

// 设置标志
flags |= FLAG_READ | FLAG_WRITE;

// 检查标志
if (flags & FLAG_READ) {
    printf("可读\n");
}

// 清除标志
flags &= ~FLAG_WRITE;

// 切换标志
flags ^= FLAG_EXEC;

7. 运算符优先级

优先级运算符结合性
1 (最高)() [] -> .左到右
2! ~ ++ -- (type) * & sizeof右到左
3* / %左到右
4+ -左到右
5<< >>左到右
6< <= > >=左到右
7== !=左到右
8&左到右
9^左到右
10``
11&&左到右
12`
13?:右到左
14= += -= ...右到左
15 (最低),左到右

建议: 使用括号明确优先级, 提高可读性.

// 不推荐
if (flags & FLAG_READ == FLAG_READ) { }  // 错误! == 优先级高于 &

// 推荐
if ((flags & FLAG_READ) == FLAG_READ) { }

8. 练习

8.1 位计数

编写函数计算一个整数的二进制表示中 1 的个数.

8.2 字节交换

编写函数交换一个 32 位整数的字节顺序 (大小端转换).

8.3 类型转换陷阱

分析以下代码的输出并解释原因:

unsigned int u = 1;
int i = -2;
printf("%d\n", u + i > 0);

9. 思考题

  1. 为什么有符号整数溢出是未定义行为?
  2. 浮点数为什么不能精确表示 0.1?
  3. 逻辑运算符和位运算符有什么区别?
  4. 右移有符号负数的结果是什么?
  5. 为什么位运算比乘除法快?

10. 本周小结

  • 整数存储: 二进制补码.
  • 浮点存储: IEEE 754.
  • 类型转换: 隐式转换, 显式转换, 整数提升.
  • 算术运算符: 加减乘除, 取模, 自增自减.
  • 位运算符: 与或非异或, 移位.
  • 运算符优先级: 使用括号明确.

理解数据在内存中的表示, 是掌握 C 语言的基础. 位运算是系统编程的必备技能.

On this page