
C语言里几乎每个程序都离不开这个符号。很多零基础同学第一次看到int a 10;会下意识把读成“等于”。但从 C 语言的角度看它更准确的名字是赋值运算符把右边的值放进左边变量对应的内存单元里。这个区别如果一开始不搞清楚后面会踩两个坑一是看代码时理解偏差二是写出if (a 1)这种永远不会按预期工作的 bug。本次教程就专注讲透赋值运算符这一件事服务人群是真正的零基础读者。我们会从“什么是变量”开始讲清楚、、-、*、/、%这些运算符的作用和区别再讲最容易混淆的与最后用两个完整可运行的 C 语言程序做实战验证。读完你不仅能看懂赋值相关的代码还能自己动手编译运行排查典型的编译错误。先给一个判断这段内容需要你有一台可以写代码的电脑装好任意一个 C 语言编译环境。Windows 用户可以用 Dev-C 或 VS CodeLinux / macOS 用户直接使用gcc。如果你还没有环境可以直接跳到第 8 章那里有一份简化版环境准备流程大概 10 分钟就能跑起第一个程序。1. 本章知识点速览知识点说明赋值运算符把右侧表达式的值存入左侧变量复合赋值运算符、-、*、/、%、、、、赋值表达式a 10本身是一个表达式它的值就是赋值后变量的值右结合性a b c 10从右往左依次赋值优先级赋值运算符优先级极低仅高于逗号运算符易错点是赋值是相等比较两者含义完全不同经典考点赋值表达式可以出现在条件判断中如if (n 0)推荐练习平台洛谷、PTA、GESP 等在线评测系统2. 赋值运算符的本质赋值运算符在 C 语言里的作用是向变量写入数据。int a 10;这个语句读法可以是“声明一个 int 类型的变量 a并把 10 赋值给它”。这里的不是一个数学等号而是一个操作指令。数学里的表示左右两侧相等信息是对等的C 语言里的表示数据流动方向信息从右边流到左边。所以a 10合法10 a完全不合法。理解这一点需要先理解 C 语言中的“变量”。变量可以理解成一个带名字的容器这个容器有固定的大小也有固定的类型。int类型通常占 4 字节能保存一个整数。赋值运算符做的事情就是把一个值放进这个容器中int age; // 声明变量 age此时里面是未初始化的垃圾值 age 18; // 通过赋值运算符把 18 放入变量 age第一行是“声明”第二行是“赋值”。很多教程会把这两步合并成int age 18;但你要清楚这里其实发生了两件事先声明变量再在声明时对它进行初始化赋值。初学者常忽略“未初始化变量里可能是垃圾值”这一点直接使用age去参与运算结果会输出一串莫名其妙的数字。变量被赋值之后可以再次赋值同一时刻变量里只保存最后一次写入的值int a 1; a 2; a 3; // 此时 a 的值是 3之前写入的 1 和 2 已经被覆盖赋值运算的目标位置必须是一个可以修改的内存单元在 C 语言术语里叫“左值”。变量是左值数组元素arr[0]是左值。常量、表达式结果、函数返回值不是左值所以不能写在左边。这些概念现在不用全记先记住一条左边必须是变量右边可以是常量、变量或表达式。3. 基本赋值运算符基本赋值运算符就是单独的。它的完整工作方式是先计算右边表达式的值然后把结果存入左侧变量。看这个程序#include stdio.h int main(void) { int a; int b 5; a 10; printf(第一次赋值: a %d\n, a); a b; printf(第二次赋值: a %d, b %d\n, a, b); a 1 2; printf(第三次赋值: a %d\n, a); return 0; }运行输出第一次赋值: a 10 第二次赋值: a 5, b 5 第三次赋值: a 3三段赋值分别演示了直接赋常量、把另一个变量的值复制过来、赋一个表达式的结果。其中a b表示把 b 里存的 5 复制一份给 aa 和 b 是两个独立变量以后修改 a 不会影响 b反之亦然。如果搞反了方向把变量写在右边、常量写在左边编译器会直接报错。例如int a 1; 10 a; // 编译错误左值必须是可修改的变量这类错误的提示通常是lvalue required as left operand of assignment翻译过来就是赋值运算符左边需要一个左值而10不是左值。看到这条报错第一反应就应该检查左边的对象是否写错。另一个常见误用是把初始化理解成赋值。int x 5;是初始化发生在变量创建时x 5;是赋值发生在变量创建之后。两者语法上有时看起来一样但语义不同。考试和面试中常出现“以下哪些说法正确”的题目重点考察的就是初始化与赋值的区分。4. 赋值表达式的值与右结合性C 语言里有一个与其他高级语言不太一样的特性赋值操作本身也是一个表达式有值。a 10这个表达式在执行完赋值后整个表达式的值就是 10。因此你可以把它放在任何需要值的位置甚至放在条件判断里。#include stdio.h int main(void) { int a, b, c; a b c 10; printf(a %d, b %d, c %d\n, a, b, c); printf(赋值表达式的值: %d\n, a 20); return 0; }运行输出a 10, b 10, c 10 赋值表达式的值: 20这里涉及赋值运算符的右结合性。大多数运算符是左结合的比如1 - 2 - 3等价于(1 - 2) - 3从左向右算。但赋值运算符不同a b c 10要从右往左解析等价于c 10; b c; a b;为什么要设计成右结合因为连续赋值的语义是先给最右侧变量赋初始值再把同一个值依次传递到左边。如果从左往右算a b c 10会变成先给 a 赋值但那时 b 和 c 还没赋值逻辑就乱了。理解了“赋值表达式有值”的特性你就能看懂很多 C 语言经典代码。比如while ((ch getchar()) ! EOF) { putchar(ch); }这段代码的含义是调用getchar()读取一个字符把返回值赋给变量ch然后判断这个赋值表达式的值是否不等于EOF。如果不等就继续循环。这里同时用到了赋值表达式的值和!比较运算符。需要注意的优先级比!低所以必须加外层的圆括号。如果不加括号会先比较getchar() ! EOF再把比较结果赋值给ch逻辑完全改变。这个特性也是后面大量 bug 的源头。因为赋值表达式有值所以if (a 1)是合法的 C 语言代码它先把 1 赋给 a然后判断表达式的值也就是 1在 C 语言中非 0 即真条件永远成立。这不是编译器 bug而是语言本身的设计。初学者最需要练就的本能就是区分什么时候该写什么时候该写。5. 复合赋值运算符复合赋值运算符是一类简写形式。a b等价于a a b。类似地a - b等价于a a - ba * b等价于a a * ba / b等价于a a / ba % b等价于a a % b。运算符示例等价写法a ba a b-a - ba a - b*a * ba a * b/a / ba a / b%a % ba a % ba ba a ba ba a ba ba a ba | b^a ^ ba a ^ b位运算相关的复合赋值、、、|、^在嵌入式开发中非常常见单片机寄存器配置几乎天天用到。零基础阶段可以先知道它们的存在等学到位运算后再深入。看一个完整示例#include stdio.h int main(void) { int x 10; x 5; printf(x 5 之后: x %d\n, x); x - 3; printf(x - 3 之后: x %d\n, x); x * 2; printf(x * 2 之后: x %d\n, x); x / 4; printf(x / 4 之后: x %d\n, x); x % 3; printf(x %% 3 之后: x %d\n, x); return 0; }运行输出x 5 之后: x 15 x - 3 之后: x 12 x * 2 之后: x 24 x / 4 之后: x 6 x % 3 之后: x 0很多初学者认为复合赋值只是简写没有其他区别。但从语义层面看两者有一个微妙差异a b中左侧变量a只被求值一次而a a b中左侧可能被求值多次。当左侧是复杂表达式时这种差异会体现出来。举个例子arr[i] 1中i只会自增一次而arr[i] arr[i] 1中i自增了两次并且这种写法在同一个表达式中对i执行了多次自增属于未定义行为不同编译器可能给出不同结果。所以实际工程中不推荐把自增操作混在赋值表达式里。考试如果遇到这类题要能看出只求值一次这个特性。6. 赋值运算符的优先级C 语言运算符很多从高到低大致可以分成这些等级优先级分类运算符最高括号、数组下标、成员访问()[]-.高单目运算!~---*(类型)sizeof中算术运算*/%中算术运算-中移位运算中关系运算中相等比较!中位与运算中异或运算^中位或运算中逻辑与中逻辑或||低条件运算?:很低赋值运算-*/%最低逗号运算,赋值运算符的优先级非常低只比逗号运算符高。这意味着在一行复杂代码中赋值操作往往是在最后执行的。例如a b * c d;先计算b * c再计算b * c d最后把结果赋值给a。所以这行代码不需要加括号。但下面这段就有问题a b 2 1;很多人认为它是(b 2) 1实际不是。因为的优先级高于它等价于b (2 1)即对b左移 3 位。初学者在写位运算和算术运算混排的代码时建议加上括号即使优先级你已经背熟括号也能让阅读者少猜一次。优先级与结合性是两个维度。赋值运算符本身优先级低同时又具有右结合性。a b c 10先执行最右边的赋值再逐步向左传递。这个特性前面已经演示过这里再强调当赋值运算符与比较运算符混用时必须留意优先级带来的执行顺序变化。7. 最容易混淆的与如果是零基础自学者这个点大概率会踩坑。是赋值运算符负责把右侧的值写入左侧变量是比较运算符负责判断左右两侧的值是否相等如果相等表达式结果为真非 0否则为假0。先看一个典型的逻辑错误#include stdio.h int main(void) { int n 5; if (n 0) { printf(条件成立\n); } else { printf(条件不成立\n); } printf(n 现在的值: %d\n, n); return 0; }这段代码的逻辑意图可能是“如果 n 等于 0”但写成了n 0。执行过程是先把 0 赋给 n此时 n 变为 0然后判断赋值表达式的值也就是 0C 语言中 0 为假所以输出“条件不成立”并且 n 被意外修改成了 0。如果写if (n 1)则条件永远成立因为赋值表达式的值是 1非 0 为真。这种 bug 很难排查因为程序能编译、能运行只是结果不符合预期。很多项目里的严重故障源头就是少写了一个。解决这个问题有两种常见手段。第一种写代码时把常量放在比较运算符左边if (0 n)这样如果漏写一个变成if (0 n)编译器会直接报错因为0不是左值不能接收赋值。这种写法在部分团队中流行但现代编译器对if (n 0)这种写法通常也会给出警告所以是否采用取决于团队规范。第二种学会看编译器警告。用gcc编译时加上-Wall选项gcc -Wall test.c -o test编译器可能会提示suggest parentheses around assignment used as truth value意思是“建议在作为真值使用的赋值表达式周围加括号”。看到这个警告就要检查是不是把写成了。与的混淆不只出现在if中还会出现在while、for的条件部分。典型正确用法是读取输入直到文件结束#include stdio.h int main(void) { int ch; printf(请输入若干字符按 CtrlDWindows 下 CtrlZ结束\n); while ((ch getchar()) ! EOF) { putchar(ch); } return 0; }这里的关键是(ch getchar())外层的括号。getchar()的返回值赋给ch之后整个表达式的值是读到的字符然后与EOF比较。如果漏掉外层括号由于赋值运算符优先级低于!实际会变成ch (getchar() ! EOF)也就是把比较结果的真假值赋给ch程序逻辑完全错误。8. 开发环境准备与编译运行学 C 语言只需要一个文本编辑器加一个编译器。推荐组合如下平台推荐方式说明WindowsDev-C 或 VS Code MinGWDev-C 开箱即用适合零基础Linuxgcc系统通常自带或通过包管理器安装macOSclang 或 gcc安装 Xcode Command Line Tools 即可在线编译ideone、洛谷在线 IDE不需要安装本地环境适合快速测试如果不想折腾本地环境可以直接在浏览器里打开洛谷的在线 IDE 或 ideone.com粘贴代码选择 C 语言点击运行。这种方式对零基础最友好完全绕开安装配置问题。如果你选择 Linux 下的本地编译常见流程是# 创建一个 C 源文件 vim hello.c # 在 hello.c 中写好代码后编译生成可执行文件 hello gcc hello.c -o hello # 运行 ./helloWindows 下用 Dev-C 的同学只需要新建源文件写代码然后点击“编译运行”按钮。程序会在弹出的控制台窗口中显示输出。下面是一个带赋值运算的完整测试程序你可以直接复制到你的环境里运行#include stdio.h int main(void) { int a 3; int b 4; int c; c a b; printf(c a b %d\n, c); c 10; printf(c 10 之后: %d\n, c); c - 2; printf(c - 2 之后: %d\n, c); c * 3; printf(c * 3 之后: %d\n, c); c / 5; printf(c / 5 之后: %d\n, c); c % 7; printf(c %% 7 之后: %d\n, c); return 0; }预期输出c a b 7 c 10 之后: 17 c - 2 之后: 15 c * 3 之后: 45 c / 5 之后: 9 c % 7 之后: 2如果编译过程中提示找不到stdio.h或gcc: command not found说明编译器没有正确安装。这时候不要继续往下写代码先解决环境问题。Windows 用户直接安装 Dev-CLinux 用户执行sudo apt install gccDebian/Ubuntu 系或sudo yum install gccCentOS/RHEL 系。macOS 用户执行xcode-select --install。9. 实战计算日期是当年的第几天这里用一个经典练习题来综合验证赋值运算符的使用题目是输入年月日计算并输出这一天是该年的第几天。例如输入2025 7 20输出应该是 2025 年的第 201 天。先分析思路。1 月到 6 月一共多少天需要累加再加上 7 月的 20 天。注意闰年判断如果月份大于 2且年份是闰年要在总天数基础上加 1。闰年条件为能被 4 整除但不能被 100 整除或者能被 400 整除。第一种写法用数组存储每个月天数#include stdio.h int main(void) { int year, month, day; int days 0; int daysPerMonth[12] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; printf(请输入年月日例如 2025 7 20); scanf(%d%d%d, year, month, day); for (int i 0; i month - 1; i) { days daysPerMonth[i]; } if (month 2 ((year % 4 0 year % 100 ! 0) || year % 400 0)) { days 1; } days day; printf(%d年%d月%d日是这一年的第 %d 天\n, year, month, day, days); return 0; }这里用到了多个赋值相关知识点int days 0;初始化计数器。days daysPerMonth[i];循环累加前几个月的天数。days 1;闰年修正。days day;加上当前月份的日。在计数器累加场景中非常常用。如果写成days days daysPerMonth[i]功能一样但更简洁也能避免左侧写重复。第二种写法不用数组用switch的穿透特性#include stdio.h int main(void) { int year, month, day; int days 0; printf(请输入年月日例如 2025 7 20); scanf(%d%d%d, year, month, day); switch (month - 1) { case 11: days 30; case 10: days 31; case 9: days 30; case 8: days 31; case 7: days 31; case 6: days 30; case 5: days 31; case 4: days 30; case 3: days 31; case 2: days 28; case 1: days 31; default: break; } if (month 2 ((year % 4 0 year % 100 ! 0) || year % 400 0)) { days 1; } days day; printf(%d年%d月%d日是这一年的第 %d 天\n, year, month, day, days); return 0; }两种代码运行结果相同。第二种方法利用switch的 case 穿透从比当前月份小 1 的 case 开始一路向下累加。这里的days 31写了很多遍你可以感受到复合赋值在重复计数场景的便利。缺点是代码可读性略差不熟悉穿透特性的读者可能看不懂。学习阶段建议先掌握数组方法它能帮你巩固循环和数组两个重要知识点。验证几个输入2025 1 1输出第 1 天。2025 3 1输出第 60 天31 28 1。2024 3 1输出第 61 天2024 是闰年2 月有 29 天。10. 常见错误与调试排查编译和运行阶段最常见的几类问题可以对照下面这张表来排查问题现象可能原因排查方式解决方案编译报错lvalue required左边写了常量或表达式检查赋值方向把常量放到右边变量放到左边编译报错undeclared identifier变量未声明就使用检查变量名拼写和声明位置在使用前先声明变量输出是随机大数字变量未初始化就参与运算检查变量声明后是否有赋值声明时初始化如int a 0;if条件永远成立或永远不成立误写成用-Wall编译看警告把条件修正为if (n 0)输入后程序没有任何输出scanf格式占位符与输入类型不匹配检查%d是否写错确保scanf(%d, n)格式正确除法结果总是 0两个整数相除如1 / 2检查操作数类型使用浮点类型或强制转换运算结果与预期不符运算符优先级理解错误拆开表达式逐步验证加括号明确优先级一个比较实用的调试技巧在关键位置用printf打印变量当前值。比如怀疑赋值没有生效就加一行printf(debug: a %d\n, a);观察输出是否在预期位置发生了变化。对于初学者来说printf是最简单的断点工具。11. 最佳实践与下一步学习建议C 语言赋值运算符本身不难但把它融入工程习惯需要建立几条规范。第一变量声明时尽量初始化。int count 0;永远好过先写int count;后面再找机会赋值。未初始化的局部变量值不确定使用它是未定义行为程序可能在你的电脑上正常换一个编译环境就出问题。第二条件判断中用不要用。写出if (n 1)这种代码编译器不一定报错但逻辑一定不对。看到编译警告时不要无视尤其是涉及赋值作为条件判断的警告。第三累加、累乘、位操作场景优先使用复合赋值运算符。total price;比total total price;更简洁也更容易体现“更新原变量”的意图。在嵌入式 C 语言中|用于置位、 ~用于清位是寄存器配置的固定操作。第四不要在一行代码里做太多事情。比如a b c 10虽然合法但可读性一般。初学阶段建议分开写等熟练之后再读连续赋值代码。第五碰到优先级不确定的表达式直接加括号。括号不会影响性能但能避免很多误读。学完赋值运算符下一步可以按这个顺序继续推进分支结构if、switch- 循环结构while、for- 数组 - 函数 - 指针 - 结构体 - 文件操作。赋值运算符会贯穿后面所有章节因为只要程序要保存计算结果就离不开赋值。练习方面建议在洛谷或 PTA 上从入门题开始。优先尝试与数学计算相关的题目比如计算两数之和、判断闰年、交换两个变量、分段函数求值。每道题都要做到能独立写出完整代码并编译通过不要只看别人的代码。你可以在练习中刻意使用较短的复合赋值写法但前提是自己能解释每一行的执行过程。如果希望跟着课程系统学习可以关注面向零基础的 C 语言网课比如翁恺老师的 C 语言课程。视频课适合建立整体框架但这篇文章的内容适合你反复看、动手敲。遇到编译错误不可怕每一个报错都是你理解 C 语言底层规则的机会。赋值运算符只是 C 语言运算符家族里的一个成员后面还有算术运算符、关系运算符、逻辑运算符、位运算符等着你去认识。把这个基础打牢后面的路会顺很多。如果这篇文章对你有帮助建议收藏备用。下一篇可以继续聊if分支或者运算符优先级总表看你的学习进度决定。