变量与表达式

开篇故事

想象你走进一家酒店。前台接待员不会随便把客人塞进任何房间——她会先看客人是一个人还是带家属,然后安排对应的房型。单人房放单人床,家庭房放两张大床,套房配客厅和书房。如果硬把一套家具塞进单人房,要么放不下,要么把房间撑坏。

C 语言的变量就像这些房间。声明变量时,你告诉编译器「这个格子存什么类型的数据」。int 房间只能住整数,double 房间只能住浮点数。C 要求你在放东西之前先指定房型,不像 Python 或 JavaScript 那样可以随意更换内容。这种「提前声明」看似约束,其实是在编译阶段就帮你排除了大量错误——就像酒店提前分配好房间,入住时不会手忙脚乱。

变量是程序的记忆单元,表达式是程序的计算肌肉。二者结合,就是 C 语言最基础的能力。

本章适合谁

本章面向完全没有 C 语言经验的初学者。如果你用过 Python、JavaScript、Java 等高级语言,本章会帮助你理解 C 的变量系统与它们的差异。如果你是完全零基础的新手,也没关系——我会从最基础的概念讲起,每一步都有可运行的代码示例。

你会学到什么

  1. 如何声明和初始化不同类型的变量(intdoublecharint64_t
  2. const 关键字的作用,以及它与 #define 宏的区别
  3. 算术运算符的使用:+-*/%,以及整数除法与浮点除法的陷阱
  4. 变量的作用域规则,包括块级作用域和变量遮蔽(shadowing)
  5. 前缀自增(++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 语言提供了多种整数和浮点类型,各有不同的取值范围:

类型大小(典型)取值范围适用场景
int4 字节±2×10⁹通用整数
int8_t1 字节-128 ~ 127节省内存
uint32_t4 字节0 ~ 4,294,967,295无符号计数
int64_t8 字节±9×10¹⁸大数计算
float4 字节~7 位有效数字近似计算
double8 字节~15 位有效数字精确计算

int8_tint64_t 等类型定义在 <stdint.h> 中,它们保证了跨平台的精确宽度——这比直接用 intlong 更可靠。

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 变量:编译器知道它的类型 */
特性#defineconst
处理阶段预处理(编译前)编译期
类型检查
调试器可见
可取地址

我的建议是:优先使用 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 + 47
-减法10 - 37
*乘法5 * 630
/除法17 / 53(整数除法,截断小数)
%取余17 % 52

整数除法是最常见的陷阱之一。当两个整数相除时,结果会被截断(不是四舍五入):

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 = 5int 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.09.0 / 5.0 确保浮点除法。

🔴 挑战:不用临时变量交换两个整数

你能不使用第三个变量,仅通过算术运算交换 ab 吗?提示:用加法和减法。

点击查看答案
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:++ii++ 到底有什么区别?

A:两者都会让 i 加 1,但返回值不同:

  • ++i(前缀):先加,返回新值
  • i++(后缀):先返回旧值,再加

当单独一行使用时效果相同。区别在于 j = ++ij = i++ 时。

知识扩展 (选学)

整数的二进制表示

计算机内部用二进制(0 和 1)存储整数。以 8 位有符号整数(int8_t)为例:

十进制二进制(补码)说明
500000101正数:直接表示
-511111011负数:补码(按位取反 + 1)
12701111111有符号 8 位最大值
-12810000000有符号 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 语言变量的核心概念:

  • 变量声明类型 名称 = 值; — 始终初始化你的变量
  • 数据类型intdoublecharint64_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预处理器

延伸阅读

选择建议:初学者推荐先阅读cppreference 的相关章节加深理解;有一定基础后可以直接参考 C17 标准原文。

继续学习

本章你已经掌握了 C 语言最基本的变量和表达式。下一步,我们将学习 C 语言丰富的数据类型——包括结构体、枚举和联合体。