变量与表达式
开篇故事
想象你走进一家酒店。前台接待员不会随便把客人塞进任何房间——她会先看客人是一个人还是带家属,然后安排对应的房型。单人房放单人床,家庭房放两张大床,套房配客厅和书房。如果硬把一套家具塞进单人房,要么放不下,要么把房间撑坏。
C 语言的变量就像这些房间。声明变量时,你告诉编译器「这个格子存什么类型的数据」。int 房间只能住整数,double 房间只能住浮点数。C 要求你在放东西之前先指定房型,不像 Python 或 JavaScript 那样可以随意更换内容。这种「提前声明」看似约束,其实是在编译阶段就帮你排除了大量错误——就像酒店提前分配好房间,入住时不会手忙脚乱。
变量是程序的记忆单元,表达式是程序的计算肌肉。二者结合,就是 C 语言最基础的能力。
本章适合谁
本章面向完全没有 C 语言经验的初学者。如果你用过 Python、JavaScript、Java 等高级语言,本章会帮助你理解 C 的变量系统与它们的差异。如果你是完全零基础的新手,也没关系——我会从最基础的概念讲起,每一步都有可运行的代码示例。
你会学到什么
- 如何声明和初始化不同类型的变量(
int、double、char、int64_t) const关键字的作用,以及它与#define宏的区别- 算术运算符的使用:
+、-、*、/、%,以及整数除法与浮点除法的陷阱 - 变量的作用域规则,包括块级作用域和变量遮蔽(shadowing)
- 前缀自增(
++x)与后缀自增(x++)的实际差异
前置要求
无需前置。这是 C 基础教程的第一章,我们从零开始。只需确保你的系统安装了 GCC 编译器(gcc --version 能输出版本号即可)。
第一个例子
下面是一段最简短的 C 变量演示。它声明了三个变量,打印它们的值,并做一次加法运算:
#include <stdio.h>
int main(void) {
int a = 10; /* 声明并初始化整型变量 a */
double b = 3.5; /* 声明并初始化双精度浮点数 b */
int sum = a + 5; /* 表达式:a + 5 的结果赋值给 sum */
printf("a = %d\n", a);
printf("b = %.1f\n", b);
printf("sum = a + 5 = %d\n", sum);
return 0;
}
编译并运行:
gcc -Wall -Wextra -std=c17 -o demo demo.c
./demo
输出:
a = 10
b = 3.5
sum = a + 5 = 15
这段代码做了三件事:
- 声明变量:告诉编译器
a是整数、b是浮点数 - 初始化:在声明的同时赋予初始值
- 表达式求值:
a + 5是一个表达式,编译器先计算再赋值
原理解析
变量声明与初始化
在 C 语言中,声明变量需要指定类型和名称:
int age; /* 仅声明,未初始化(值不确定) */
int count = 0; /* 声明并初始化 */
我强烈建议始终在声明时初始化。未初始化的局部变量包含的是内存中的随机值,使用它会导致不可预测的行为。
C 语言提供了多种整数和浮点类型,各有不同的取值范围:
| 类型 | 大小(典型) | 取值范围 | 适用场景 |
|---|---|---|---|
int | 4 字节 | ±2×10⁹ | 通用整数 |
int8_t | 1 字节 | -128 ~ 127 | 节省内存 |
uint32_t | 4 字节 | 0 ~ 4,294,967,295 | 无符号计数 |
int64_t | 8 字节 | ±9×10¹⁸ | 大数计算 |
float | 4 字节 | ~7 位有效数字 | 近似计算 |
double | 8 字节 | ~15 位有效数字 | 精确计算 |
int8_t、int64_t 等类型定义在 <stdint.h> 中,它们保证了跨平台的精确宽度——这比直接用 int、long 更可靠。
const 常量
const double PI = 3.14159265;
const int MAX_USERS = 100;
const 告诉编译器这个变量的值不能被修改。如果你尝试:
const int MAX = 10;
MAX = 20; /* ❌ 编译错误:只读变量 */
编译器会直接报错:
error: assignment of read-only variable 'MAX'
const vs #define
#define BUFFER_SIZE 256 /* 预处理宏:编译前直接文本替换 */
const int MAX_NAME = 64; /* const 变量:编译器知道它的类型 */
| 特性 | #define | const |
|---|---|---|
| 处理阶段 | 预处理(编译前) | 编译期 |
| 类型检查 | 无 | 有 |
| 调试器可见 | 否 | 是 |
| 可取地址 | 否 | 是 |
我的建议是:优先使用 const。它有类型安全检查,调试时能直接看到值。#define 只适合条件编译(如 #ifdef __linux__)或真正的编译时常量。
作用域基础
变量的作用域(Scope)决定了它在哪些地方可见:
int global = 100; /* 全局变量:整个文件可见 */
void func(void) {
int local = 50; /* 局部变量:仅在 func 内可见 */
{
int block = 20; /* 块级变量:仅在这个 { } 内可见 */
}
/* block 在这里已经不存在了 */
}
变量遮蔽(Shadowing):内层可以声明与外层同名的变量,但这只是「同名不同柜」:
int x = 1;
{
int x = 999; /* 遮蔽了外层的 x */
/* 这里的 x 是 999 */
}
/* 这里的 x 还是 1 */
算术运算符
C 语言提供了五种基本算术运算符:
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
+ | 加法 | 3 + 4 | 7 |
- | 减法 | 10 - 3 | 7 |
* | 乘法 | 5 * 6 | 30 |
/ | 除法 | 17 / 5 → 3(整数除法,截断小数) | |
% | 取余 | 17 % 5 → 2 |
整数除法是最常见的陷阱之一。当两个整数相除时,结果会被截断(不是四舍五入):
int result = 7 / 2; /* result = 3, 不是 3.5 */
double precise = 7.0 / 2; /* precise = 3.5 */
double cast = (double)7 / 2; /* cast = 3.5,通过强制转换 */
要获得精确的浮点除法,只需操作数中至少一个是浮点数。
常见错误
错误 1:使用未初始化的变量
/* ❌ 错误代码 */
int x;
printf("x = %d\n", x); /* x 的值是随机的! */
编译器警告(-Wextra 会提示,但不一定报错):
warning: 'x' is used uninitialized [-Wuninitialized]
/* ✅ 修复 */
int x = 0; /* 始终初始化 */
printf("x = %d\n", x);
错误 2:整数除法误当作浮点除法
/* ❌ 错误代码 */
double avg = 7 / 2;
printf("avg = %.1f\n", avg); /* 输出 3.0,不是 3.5 */
问题:7 / 2 是两个 int 相除,结果是 int 类型的 3,再转换为 double 变成 3.0。
/* ✅ 修复:让任一操作数为浮点数 */
double avg = 7.0 / 2; /* 方法1:字面量加 .0 */
double avg2 = (double)7 / 2; /* 方法2:强制类型转换 */
错误 3:溢出——超出变量范围
/* ❌ 错误代码 */
#include <stdint.h>
int32_t max = 2147483647;
int32_t next = max + 1; /* 溢出!回绕到 -2147483648 */
printf("%d\n", next); /* 输出: -2147483648 */
/* ✅ 修复:使用更大类型 */
#include <stdint.h>
int64_t max = 2147483647LL;
int64_t next = max + 1; /* 安全 */
printf("%lld\n", (long long)next); /* 输出: 2147483648 */
动手练习
🟢 入门:交换两个变量
写一段代码,交换 int a = 5 和 int b = 10 的值。提示:使用一个临时变量 temp。
点击查看答案
int a = 5, b = 10;
printf("交换前: a = %d, b = %d\n", a, b);
int temp = a;
a = b;
b = temp;
printf("交换后: a = %d, b = %d\n", a, b);
/* 输出: a = 10, b = 5 */
🟡 中级:摄氏度转华氏度
公式:F = C × 9/5 + 32。注意 9/5 在 C 中是整数除法!写一段代码,将 25.0 摄氏度转换为华氏度。
点击查看答案
double celsius = 25.0;
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
printf("%.1f°C = %.1f°F\n", celsius, fahrenheit);
/* 输出: 25.0°C = 77.0°F */
关键点:使用 9.0 或 9.0 / 5.0 确保浮点除法。
🔴 挑战:不用临时变量交换两个整数
你能不使用第三个变量,仅通过算术运算交换 a 和 b 吗?提示:用加法和减法。
点击查看答案
int a = 5, b = 10;
printf("交换前: a = %d, b = %d\n", a, b);
a = a + b; /* a = 15 */
b = a - b; /* b = 15 - 10 = 5 */
a = a - b; /* a = 15 - 5 = 10 */
printf("交换后: a = %d, b = %d\n", a, b);
/* 输出: a = 10, b = 5 */
注意:这种方法在 a + b 可能溢出时不安全。在实际代码中,应使用临时变量。这只是算法思维的练习。
故障排查 (FAQ)
Q:为什么 C 语言要求声明变量类型,而 Python 不需要?
A:C 是静态类型(Static Typing)语言,编译器在编译期就需要知道每个变量的内存布局。Python 是动态类型,类型检查推迟到运行时。静态类型的好处是:编译期就能发现类型错误,运行时更高效。
Q:int 到底是多大?4 字节?
A:C 标准只保证 int 至少 16 位。在现代桌面系统(macOS、Linux、Windows)上,int 通常是 32 位(4 字节)。如果需要精确宽度,请使用 <stdint.h> 中的 int32_t。
Q:% 运算符能用于浮点数吗?
A:不能。% 只适用于整数。如果需要浮点数取余,请使用 <math.h> 中的 fmod() 函数。
Q:++i 和 i++ 到底有什么区别?
A:两者都会让 i 加 1,但返回值不同:
++i(前缀):先加,返回新值i++(后缀):先返回旧值,再加
当单独一行使用时效果相同。区别在于 j = ++i 和 j = i++ 时。
知识扩展 (选学)
整数的二进制表示
计算机内部用二进制(0 和 1)存储整数。以 8 位有符号整数(int8_t)为例:
| 十进制 | 二进制(补码) | 说明 |
|---|---|---|
| 5 | 00000101 | 正数:直接表示 |
| -5 | 11111011 | 负数:补码(按位取反 + 1) |
| 127 | 01111111 | 有符号 8 位最大值 |
| -128 | 10000000 | 有符号 8 位最小值 |
最高位是符号位(0 = 正,1 = 负)。这也是 int8_t 范围是 -128 ~ 127 的原因:2⁸ = 256 种状态,一半给正数(含 0),一半给负数。
复合赋值运算符
你可以用复合赋值运算符简化代码:
x += 5; /* 等价于 x = x + 5 */
x -= 3; /* 等价于 x = x - 3 */
x *= 2; /* 等价于 x = x * 2 */
x /= 4; /* 等价于 x = x / 4 */
x %= 7; /* 等价于 x = x % 7 */
这些运算符不仅简洁,而且在某些情况下编译器能生成更高效的机器码。
小结
本章我们学习了 C 语言变量的核心概念:
- 变量声明:
类型 名称 = 值;— 始终初始化你的变量 - 数据类型:
int、double、char、int64_t等,用<stdint.h>获得精确宽度 - const 常量:比
#define更安全,有类型检查 - 算术运算:
+、-、*、/、%,注意整数除法会截断 - 作用域:变量只在声明它的
{}块内可见,内层可以遮蔽外层 - 类型转换:
(double)7 / 2获得浮点除法
核心术语:
- Variable / 变量
- Data Type / 数据类型
- Declaration / 声明
- Initialization / 初始化
- Scope / 作用域
- Const Correctness / 常量正确性
- Integer Division / 整数除法
- Type Casting / 类型转换
- Shadowing / 遮蔽
术语表
| 英文 | 中文 |
|---|---|
| Variable | 变量 |
| Data Type | 数据类型 |
| Declaration | 声明 |
| Initialization | 初始化 |
Constant (const) | 常量 |
| Integer | 整数 |
| Floating Point | 浮点数 |
| Arithmetic | 算术 |
| Expression | 表达式 |
| Scope | 作用域 |
| Lifetime | 生命周期 |
| Shadowing | 遮蔽 |
| Type Casting | 类型转换 |
| Overflow | 溢出 |
| Truncation | 截断 |
| Preprocessor | 预处理器 |
延伸阅读
- C17 标准(ISO/IEC 9899:2018) — C 语言的官方标准
- cppreference - C 语言 — 权威在线参考
- Kernighan & Ritchie《The C Programming Language》 — C 语言的经典教材
选择建议:初学者推荐先阅读cppreference 的相关章节加深理解;有一定基础后可以直接参考 C17 标准原文。
继续学习
本章你已经掌握了 C 语言最基本的变量和表达式。下一步,我们将学习 C 语言丰富的数据类型——包括结构体、枚举和联合体。