1 运算符与表达式核心概念
1.1 什么是运算符
运算符是编程和数学中具有特定功能的符号,用于对数据进行运算、赋值、比较及逻辑处理等操作。它们能够改变、组合或比较操作数的值,进而生成新值或触发特定动作。
1.2 什么是表达式
表达式是编程和数学中用于表达计算或操作的结构,由运算数(如变量、常量)和运算符按特定规则组合而成。
表达式能够计算或代表一个确定的值,形式可简单(如单一变量或常量)也可复杂(包含多个运算符、运算数,甚至嵌套函数调用、条件表达式等)。
1.3 什么是操作数
在 C 语言的表达式中,操作数是指参与运算符运算的具体值或变量。操作数通常与运算符结合使用,以完成特定的计算或操作。根据运算符的类型,操作数可以分为左操作数和右操作数,尤其是在二元运算符(如 +、-、*、/ 等)及部分需要两个操作数的操作(如赋值)的上下文中。
- 左操作数:位于运算符左侧,在赋值操作中通常为变量,用于存储运算结果。
- 右操作数:位于运算符右侧,可以是变量、常量或表达式的结果。
例如,在表达式 a = b + 5; 中,a 为左操作数,b + 5 为右操作数(其中 b 和 5 分别为加法运算符 + 的左、右操作数)。
1.4 运算符分类
按操作数个数分类:
- 一元运算符(一目运算符):作用于单个操作数。
- 二元运算符(二目运算符):作用于两个操作数。
- 三元运算符(三目运算符):作用于三个操作数(如 C 语言中的条件运算符 ?:)。
按功能分类:
- 算术运算符:执行基本数学运算,如 +、-、*、/、%(取模)。
- 关系运算符:比较两个操作数的关系,返回布尔值,如 ==、!=、>、<、>=、<=。
- 逻辑运算符:对布尔值进行逻辑运算,如 &&(与)、||(或)、!(非)。
- 位运算符:对操作数的二进制位进行操作,如 &(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。
- 赋值运算符:用于给变量赋值,如 =、+=、-= 等。
1.5 如何掌握运算符
要全面掌握一个运算符,需关注以下几个方面:
- 含义:明确运算符的具体功能(如 & 是按位与,&& 是逻辑与)。
- 操作数个数与类型:区分一元(如 !)、二元(如 +)、三元(如 ?:),并注意类型要求(如 % 需整数)。
- 表达式值的计算规则:理解运算符优先级(如 * 高于 +)、结合性(如 a - b - c 从左到右)及隐式转换(如 int + float 转为 float)。
- 副作用与状态修改:判断运算是否直接修改操作数(如 ++、--、赋值运算符 = 会修改,而 +、== 不会)。
2 算术运算符
算术运算符用于对数值类型变量进行运算,在 C 程序中广泛使用。运算结果的类型和精度由操作数类型决定。
运算符 | 描述 | 操作数个数 | 组成的表达式的值 | 副作用 |
---|---|---|---|---|
+ | 正号 | 1 | 操作数本身 | 无 |
- | 负号 | 1 | 操作数符号取反 | 无 |
+ | 加法 | 2 | 两数之和 | 无 |
- | 减法 | 2 | 两数之差 | 无 |
* | 乘法 | 2 | 两数之积 | 无 |
/ | 除法 | 2 | 两数之商 | 无 |
% | 取模(取余) | 2 | 两整型数相除的余数 | 无 |
++ | 自增 | 1 | 自增前 / 后的值 | 有 |
-- | 自减 | 1 | 自减前 / 后的值 | 有 |
2.1 正、负号
正号(+)和负号(-)是一元运算符,用于直接修改操作数的符号。它们可以独立使用(如 +5、-3),也可在表达式中与其他运算符组合(如 -x + y)。
- 负号(-):将操作数的值取反(如 -12 变为 -12,-(-67) 变为 67)。
- 正号(+):通常不改变操作数的值,但可用于强调数值的正负性(如 +12 仍为 12)。
#include <stdio.h>int main()
{int x = 12;int x1 = -x, x2 = +x;printf("x=%d,x1=%d, x2=%d\n", x, x1, x2); // x=12,x1=-12, x2=12int y = -67;int y1 = -y, y2 = +y;printf("y=%d,y1=%d, y2=%d\n", y, y1, y2); // y=-67,y1=67, y2=-67return 0;
}
程序在 VS Code 中的运行结果如下所示:
2.2 加、减、乘、除
算术运算符 +(加)、-(减)、*(乘)、/(除)是二元运算符,用于执行基本的数学运算。
- 加法(+):将两个操作数相加(如 5 + 2.5 结果为 7.5,但整数类型会截断小数部分)。
- 减法(-):计算两数之差(如 7 - 2.5 结果为 4.5,整数类型截断后为 4)。
- 乘法(*):计算两数乘积(如 7 * 7 结果为 49)。
- 除法(/):
- 若操作数为整数,执行整数除法(如 6 / 4 结果为 1,丢弃小数部分)。
- 若至少一个操作数为浮点数,执行浮点除法(如 6.0 / 4 或 6 / 4.0 结果为 1.5)。
#include <stdio.h>int main()
{int a = 5 + 2.5;printf("a 的值为:%d\n", a); // 输出:7(7.5 被截断)printf("%d * %d = %d\n\n", a, a, a * a); // 输出:49a = a - 2.5;printf("a 的值为:%d\n", a); // 输出:4(4.5 被截断)printf("%d * %d = %d\n\n", a, a, a * a); // 输出:16double b = 6 / 4; // 整数除法:6/4=1,再转为 double → 1.00double c = 6.0 / 4; // 浮点除法:1.50double d = 6 / 4.0; // 浮点除法:1.50double e = (double)6 / 4; // 强制类型转换后除法:1.50double f = 6 / (double)4; // 强制类型转换后除法:1.50printf("b = %.2f c = %.2f d = %.2f e = %.2f f = %.2f\n", b, c, d, e, f);return 0;
}
程序在 VS Code 中的运行结果如下所示:
2.3 取模(取余)
取模运算符 % 用于计算两个整数相除后的余数,要求操作数必须为整数类型(如 int、long)。
- 规则:结果的符号与左操作数一致(如 -10 % 3 结果为 -1,10 % -3 结果为 1)。
- 浮点数限制:% 不可用于浮点数(如 5.0 % 2 会报错),因浮点除法不产生整数商和余数。
#include <stdio.h>int main()
{// 基础取模运算int res1 = 10 % 3;printf("10 %% 3 = %d\n", res1); // 输出: 1(商 3 余 1)// 负数取模:结果符号与左操作数一致int res2 = -10 % 3;printf("-10 %% 3 = %d\n", res2); // 输出: -1(商 -3 余 -1)int res3 = 10 % -3;printf("10 %% -3 = %d\n", res3); // 输出: 1(商 -3 余 1)int res4 = -10 % -3;printf("-10 %% -3 = %d\n", res4); // 输出: -1(商 3 余 -1)// 不可对浮点数进行取模运算,编译器会报错!!!// float res5 = 10.5 % 3.2;return 0;
}
程序在 VS Code 中的运行结果如下所示:
在 C 语言中,直接对浮点数使用取模运算符(%)会引发编译错误 ,如下所示:
2.4 自增与自减
自增(++)和自减(--)运算符 是 C 语言中用于对变量进行 +1 或 -1 操作的单目运算符,分为前缀形式和后缀形式。它们的核心区别在于表达式返回值和变量修改顺序:
- 前缀形式(++i、--i):
- 先修改值,再返回新值。
- 示例:i = ++a 会先执行 a = a + 1,然后将新值 a 赋给 i。
- 后缀形式(i++、i--):
- 先返回原值,再修改值。
- 示例:i = a++ 会先将 a 的当前值赋给 i,再执行 a = a + 1。
- 副作用一致性:
- 无论前缀或后缀,变量最终值均会被修改(如 a++ 和 ++a 最终都会使 a = a + 1)。
- 表达式值差异:
- 前缀返回新值(如 ++a 直接返回 a+1)。
- 后缀返回原值(如 a++ 返回修改前的 a)。
- 常见应用场景:
- 循环计数器(如 for (int i=0; i<10; i++))。
- 简化代码(如 while (--n > 0))。
#include <stdio.h>int main()
{// 基础用法:验证变量最终值(无论前缀/后缀,变量均被修改)int a = 8;a++; // 后缀:a 变为 9printf("a = %d\n", a);++a; // 前缀:a 变为 10printf("a = %d\n", a);a--; // 后缀:a 变为 9printf("a = %d\n", a);--a; // 前缀:a 变为 8printf("a = %d\n", a);// 前缀 vs 后缀对表达式的影响int i1 = 10, i2 = 20;// 后缀:先赋值原值,再自增int i = i1++; // i = 10(原值),i1 = 11printf("i=%d, i1=%d\n", i, i1);// 前缀:先自增,再赋值新值i = ++i1; // i1 = 12,i = 12printf("i=%d, i1=%d\n", i, i1);// 后缀:先赋值原值,再自减i = i2--; // i = 20(原值),i2 = 19printf("i=%d, i2=%d\n", i, i2);// 前缀:先自减,再赋值新值i = --i2; // i2 = 18,i = 18printf("i=%d, i2=%d\n", i, i2);return 0;
}
程序在 VS Code 中的运行结果如下所示:
在 C 语言中,自增(++)和自减(--)运算符只能用于可修改的左值(变量),不能用于常量或字面量。尝试这样做会导致编译错误:
复杂自增/减运算
在 C 语言编程中,理解运算符的求值顺序和避免未定义行为至关重要。某些看似简单的表达式,如涉及多个自增(++)和自减(--)运算符的复合表达式,其实际行为可能因编译器而异,导致不可预测的结果。下面通过一个具体示例,分析这类表达式的常见行为,并探讨如何避免此类问题以及如何利用编译器警告来辅助开发。
#include <stdio.h>int main()
{int num = 20;int sum = num++ - --num + ++num;printf("The sum is %d\n", sum); // 输出:The sum is 21, 不同编译器结果可能不一样printf("The num is %d\n", num); // 输出:The num is 21, 不同编译器结果可能不一样return 0;
}
程序在 VS Code 中的运行结果如下所示:
程序运算过程分析(以常见编译器行为【从左到右计算】为例):
- num++:先使用 num 的值 20 作为整个表达式(num++)的值,然后 num 自增为 21。
- --num:num 先从 21 自减为 20,然后整个表达式(--num)使用 20 作为其自身的值,这里 num 最后为 20。
- ++num:num 先从 20 自增为 21,然后整个表达式(++num)使用 21 作为其自身的值,这里 num 最后为 21。
- 最终计算: sum = 20 - 20 + 21 = 21,num 最终为 21。
未定义行为警告:
C 标准未明确规定上述这种复杂表达式的求值顺序,不同编译器可能产生不同的结果。这种代码依赖于编译器的具体实现,属于未定义行为(undefined behavior)。
编程建议:
- 避免编写此类难以理解和维护的复杂表达式。
- 建议拆分为多个简单语句以提高可读性和可预测性。
VS Code 警告配置
在之前的学习中,我们了解到可以在 VS Code 的当前工作区的 tasks.json 文件中修改编译指令。例如,我们之前添加了 -Wconversion 参数,现在我们加上 -Wall 和 -Wextra 这两个参数。
- -Wconversion:
- 启用对隐式类型转换的警告。
- 帮助开发者发现由于类型转换而可能导致的精度丢失或数据截断问题。
- -Wall:
- 启用 “所有” 警告信息。实际上,它并不是启用所有的警告,而是一个常用的警告集合。
- 包括一些常见的警告,比如未使用的变量、未初始化的变量、类型不匹配等。
- -Wextra:
- 启用一些额外的警告信息。
- 包含一些不在 -Wall中 的警告,比如函数返回类型不匹配、使用空指针解引用等。
加上这两个参数之后,如果我们再重新编译上面这个程序,编译器就会发出警告信息提示我们,如下所示:
3 关系运算符
在 C 语言中,关系运算符(也称比较运算符)用于比较两个值,并返回一个布尔结果。这些运算符在条件判断、循环控制等场景中非常常见。
关系运算符的操作数个数为 2,表达式的值为 0(假)或 1(真),且没有副作用。
运算符 | 描述 | 操作数个数 | 返回值(表达式的值) | 说明 | 副作用 |
---|---|---|---|---|---|
== | 相等 | 2 | 0 或 1 | 判断两个操作数是否相等 | 无 |
!= | 不等 | 2 | 0 或 1 | 判断两个操作数是否不相等 | 无 |
< | 小于 | 2 | 0 或 1 | 判断左操作数是否小于右操作数 | 无 |
> | 大于 | 2 | 0 或 1 | 判断左操作数是否大于右操作数 | 无 |
<= | 小于等于 | 2 | 0 或 1 | 判断左操作数是否小于或等于右操作数 | 无 |
>= | 大于等于 | 2 | 0 或 1 | 判断左操作数是否大于或等于右操作数 | 无 |
- 在 C 语言中,关系运算符的返回值为 0(表示假)或 1(表示真)。
- 任何非零值在布尔上下文中都被视为真,但在关系运算符的上下文中,返回值始终为 0 或 1。
#include <stdio.h>int main()
{int a = 8;int b = 7;// 使用关系运算符进行比较,并打印结果printf("a > b 的值:%d\n", a > b); // 输出 1(真),因为 8 > 7printf("a >= b 的值:%d\n", a >= b); // 输出 1(真),因为 8 >= 7printf("a < b 的值:%d\n", a < b); // 输出 0(假),因为 8 不小于 7printf("a <= b 的值:%d\n", a <= b); // 输出 0(假),因为 8 不小于或等于 7printf("a == b 的值:%d\n", a == b); // 输出 0(假),因为 8 不等于 7printf("a != b 的值:%d\n", a != b); // 输出 1(真),因为 8 不等于 7return 0;
}
程序在 VS Code 中的运行结果如下所示:
4 逻辑运算符
逻辑运算符用于对布尔值进行逻辑操作,通常用于条件判断和控制程序流程。
运算符 | 描述 | 操作数个数 | 返回值(表达式的值) | 副作用 |
---|---|---|---|---|
&& | 逻辑与 | 2 | 0 或 1 | 无 |
|| | 逻辑或 | 2 | 0 或 1 | 无 |
! | 逻辑非 | 1 | 0 或 1 | 无 |
4.1 逻辑与 &&
- 运算规则:当且仅当两个操作数均为真(非零)时,整个表达式结果为真;否则为假(遵循 “一假则假” 规则)。
- 短路现象:若第一个操作数为假,直接判定整个表达式为假,不再计算第二个操作数(避免不必要的执行)。例如,在逻辑与表达式 A && B 中,如果 A 为假(false),则无论 B 的值如何,整个表达式的结果都为假,因此 B 不会被计算。
#include <stdio.h>int main()
{double score = 70;// 使用逻辑与(&&)运算符检查 score 是否在 60 到 80(包括 60 和 80)之间if (score >= 60 && score <= 80){// 当 score 在 60 到 80 之间时,打印 "ok1"printf("ok1\n");}else{// 如果 score 不在这个范围内,则执行这个分支printf("ok2\n");}int a = 10, b = 99;// 展示逻辑与运算符的短路现象if (a < 2 && ++b > 99){// 这个分支不会执行,因为 a < 2 的结果为假,导致整个条件表达式为假printf("ok100");}printf("b=%d\n", b); // 由于短路现象,b 的值仍然是 99,没有因为 ++b 而改变return 0;
}
程序在 VS Code 中的运行结果如下所示:
注意:C 语言中的条件判断写法
在 C 语言中,逻辑与运算符 && 允许我们以一种直观的方式组合多个条件,例如 score >= 60 && score <= 80,这表示 score 必须在 60 和 80 之间(包括 60 和 80)。这种写法是合法的,并且易于理解。
然而,需要注意的是,C 语言中不能像数学中那样直接使用链式比较,例如 60 <= score <= 80。这种写法在数学中是合法的,但在 C 语言中会被解释为 (60 <= score) <= 80。这意味着 60 <= score 会被先计算为一个布尔值(0 或 1),然后再与 80 进行比较。这种解释方式通常不符合我们的意图,因此会导致逻辑错误。
因此,在 C 语言中,我们应该始终使用逻辑与运算符 && 来组合多个条件,以确保我们的逻辑判断是正确的。
4.2 逻辑或 ||
- 运算规则:若任意一个操作数为真(非零),整个表达式结果为真;仅当两个操作数均为假时,结果为假(遵循 “一真则真” 规则)。
- 短路现象:若第一个操作数为真,直接判定整个表达式为真,不再计算第二个操作数(避免不必要的执行)。例如,在逻辑或表达式 A || B 中,如果 A 为真(true),则无论 B 的值如何,整个表达式的结果都为真,因此 B 不会被计算。
#include <stdio.h>int main()
{double score = 70;// 使用逻辑或(||)运算符检查 score 是否满足大于等于 70 或小于等于 80 的条件if (score >= 70 || score <= 80){printf("ok1\n"); // 由于条件表达式总是为真,所以这里总是打印 "ok1"}else{printf("ok2\n"); // 这个分支永远不会执行,因为上面的条件表达式总是为真}int a = 10, b = 99;// 展示逻辑或运算符的短路现象if (a > 5 || ++b > 100){printf("ok100\n"); // 由于 a > 5 为真,所以这里会打印 "ok100"}printf("b=%d\n", b); // 打印 b 的值,由于短路现象,b 的值仍然是 99return 0;
}
程序在 VS Code 中的运行结果如下所示:
4.3 逻辑非 !
- 描述:对操作数的布尔值进行取反。
#include <stdio.h>int main()
{int score = 100;int res = score > 99;printf("res = %d\n", res); // 100 > 99,为真,输出:1printf("!res = %d\n", !res); // 取反,输出:0// 检查 res 的值,如果 res 为真(即 score > 99),则执行以下语句if (res){printf("yes, Thanks\n"); // 因为 score 确实大于 99,所以这里会打印 "yes, Thanks"}// 紧接着检查 !res 的值,即 res 的否定。由于 res 为真(1),!res 为假(0)if (!res){printf("no,Thanks \n"); // 这个语句不会执行,因为 !res为假}return 0;
}
程序在 VS Code 中的运行结果如下所示:
5 编程练习
5.1 购物折扣计算
你在超市购物,买了 3 盒牛奶(每盒 5 元)和 2 袋面包(每袋 8 元)。超市正在打折:如果购物总价满 30 元,可以减 5 元。编写一个程序,计算并输出未打折前的总价和打折后的实际支付金额。
#include <stdio.h>int main()
{// 牛奶面包的单价int milk_price = 5, bread_price = 8;// 牛奶和面包的购买数量int milk_count = 3, bread_count = 2;// 计算总价int total = milk_price * milk_count + bread_price * bread_count;// 打折后的总价int discounted_total = total;// 判断是否满足打折条件if (total >= 30){// 满足打折条件,减 5 元discounted_total = total - 5;}printf("未打折总价:%d 元\n", total);printf("实际支付:%d 元\n", discounted_total);return 0;
}
程序在 VS Code 中的运行结果如下所示:
5.2 儿童过山车资格
游乐园规定:身高超过 120 厘米或年龄超过 12 岁的儿童可以单独乘坐过山车。编写一个程序,输入儿童的身高(厘米)和年龄,判断是否可以单独乘坐过山车,并输出结果。
#include <stdio.h>int main()
{float height; // 儿童的身高int age; // 儿童的年龄printf("请输入儿童的身高(厘米):");scanf("%f", &height);printf("请输入儿童的年龄:");scanf("%d", &age);// 判断是否满足乘坐条件// 身高大于 120 厘米 或 年龄大于 12 岁if (height > 120 || age > 12){printf("可以乘坐过山车\n");}else{printf("不可以乘坐过山车\n");}return 0;
}
程序在 VS Code 中的运行结果如下所示:
5.3 周末公园计划
今天是周末,你计划去公园玩。如果天气晴朗且温度在 20°C 到 30°C 之间,就去公园;否则在家看书。编写一个程序,输入天气和当前温度,判断是否去公园,并输出结果。
#include <stdio.h>int main()
{int sunny; // 是否晴朗:1 表示是,0 表示否float temperature; // 当前温度printf("今天天气晴朗吗?(1 表示是,0 表示否):");scanf("%d", &sunny);printf("当前温度(°C)为:");scanf("%f", &temperature);// 判断是否去公园:晴朗且温度在 20 到 30 度之间if (sunny == 1 && temperature >= 20 && temperature <= 30)// 等价于 if (sunny && temperature >= 20 && temperature <= 30){printf("去公园\n");}else{printf("在家看书\n");}return 0;
}
程序在 VS Code 中的运行结果如下所示:
5.4 电影票价格
电影院对学生打折:学生票 20 元,成人票 35 元。如果观看的是 3D 电影,所有票价加 10 元。编写一个程序,输入是否是学生和是否是 3D 电影,计算并输出票价。
#include <stdio.h>int main()
{int is_student; // 是否是学生:1 表示是,0 表示否int is_3d; // 是否是 3D 电影:1 表示是,0 表示否int ticket_price; // 票价printf("是否是学生?(1 表示是,0 表示否):");scanf("%d", &is_student);printf("是否是 3D 电影?(1 表示是,0 表示否):");scanf("%d", &is_3d);// 1. 计算基础票价if (is_student == 1){ticket_price = 20; // 学生票价}else{ticket_price = 35; // 成人票价}// 2. 判断是否加收 3D 费用if (is_3d == 1){ticket_price += 10; // 3D 电影加收费用}printf("票价:%d 元\n", ticket_price);return 0;
}
上面这个程序可以进行简化操作,由于我们已经将 is_student 和 is_3d 定义为 int 类型,并且约定输入值为 1 或 0 来表示真假,我们可以直接在条件中使用这些变量,而不需要显式地与 1 进行比较。
#include <stdio.h>int main()
{int is_student; // 是否是学生:非零表示是,0 表示否int is_3d; // 是否是 3D 电影:非零表示是,0 表示否int ticket_price; // 票价printf("是否是学生?(1 表示是,0 表示否):");scanf("%d", &is_student);printf("是否是 3D 电影?(1 表示是,0 表示否):");scanf("%d", &is_3d);// 1. 计算基础票价if (is_student) // 直接判断是否非零{ticket_price = 20; // 学生票价}else{ticket_price = 35; // 成人票价}// 2. 判断是否加收 3D 费用if (is_3d) // 直接判断是否非零{ticket_price += 10; // 3D 电影加收费用}printf("票价:%d 元\n", ticket_price);return 0;
}
程序在 VS Code 中的运行结果如下所示:
5.5 健康饮食建议
根据每日摄入的卡路里和运动量,给出健康建议:
- 如果卡路里摄入 > 2000 且运动量 < 30 分钟,建议 "减少高热量食物"。
- 如果卡路里摄入 < 1500 且运动量 > 60 分钟,建议 "增加营养摄入"。
- 其他情况,建议 "保持现状"。
编写一个程序,输入卡路里摄入和运动量(分钟),输出健康建议。
#include <stdio.h>int main()
{int calories, exercise_minutes; // 今日卡路里摄入量和运动量(分钟)printf("请输入今日卡路里摄入量:");scanf("%d", &calories);printf("请输入今日运动量(分钟):");scanf("%d", &exercise_minutes);// 根据条件给出建议if (calories > 2000 && exercise_minutes < 30){printf("建议:减少高热量食物\n");}else if (calories < 1500 && exercise_minutes > 60){printf("建议:增加营养摄入\n");}else{printf("建议:保持现状\n");}return 0;
}
程序在 VS Code 中的运行结果如下所示:
提示:
在上述编程练习中,我们使用了 if 分支语句和 if-else if 多分支语句来实现逻辑判断。如果你现在还不太懂这些语句,没关系!从 if(如果)、else(否则)这些单词可以猜到它们的意思。后面我们会专门学条件判断,学完后再回来做这些练习,你会觉得轻松很多!