【参考资料 II】C 运算符大全:算术、关系、赋值、逻辑、条件、指针、符号、成员、按位、混合运算符
目录
C 语言运算符大全
1 算术运算符
1.1 运算符说明
1.2 案例演示
2 关系运算符
2.1 运算符说明
2.2 关系表达式
2.3 案例演示
3 赋值运算符
3.1 运算符说明
3.1.1 基本赋值运算符
3.1.2 复合赋值运算符
3.2 案例演示
4 逻辑运算符
4.1 运算符说明
4.2 逻辑表达式
4.3 逻辑表达式的求值顺序
4.3.1 短路求值规则
4.3.2 示例分析
4.4 案例演示
5 条件运算符
5.1 运算符说明
5.2 案例演示
6 指针运算符
6.1 运算符说明
6.2 案例演示
7 符号运算符
7.1 运算符说明
7.2 案例演示
8 结构和联合运算符
8.1 成员运算符
8.2 间接成员运算符
8.3 等效写法说明
8.3.1 ptrst->code
8.3.2 item.code
8.3.3 (*ptrst).code
8.3.4 等效性分析
9 按位运算符
9.1 运算符说明
9.2 案例演示
9.3 左移和右移运算符特性
9.3.1 数学意义
9.3.2 注意事项与常见问题
10 混合运算符
10.1 大小运算符(sizeof)
10.2 对齐要求运算符(_Alignof)
10.3 强制类型转换运算符(type)
10.4 逗号运算符(,)
C 语言运算符大全
C 语言有大量的运算符。表 B.2.1 按优先级从高至低的顺序列出了 C 运算符,并给出了其结合性。除非特别指明,否则所有运算符都是二元运算符(需要两个运算对象)。
注意,一些二元运算符和一元运算符的表示符号相同,但是其优先级不同。例如,*(乘法运算符)和 *(间接运算符)。
提示:
C 语言运算符优先级不用刻意地去记忆,大致上:一元运算符 > 算术运算符 > 关系运算符 > 逻辑运算符 > 条件运算符 > 赋值运算符
常见的运算符优先级关系可简单记忆为: 逻辑非 ! > 算术运算符 > 关系运算符 > 逻辑与 && > 逻辑或 || > 条件运算符 > 赋值运算符
1 算术运算符
1.1 运算符说明
运算符 | 类型 | 描述 |
---|---|---|
+ | 二元运算符(加法) | 把右边的值加到左边的值上。 |
+ | 一元运算符(正号) | 生成一个大小和符号都与右边值相同的值。 |
- | 二元运算符(减法) | 从左边的值中减去右边的值。 |
- | 一元运算符(负号) | 生成一个与右边值大小相等但符号相反的值。 |
* | 乘法运算符 | 把左边的值乘以右边的值。 |
/ | 除法运算符 | 把左边的值除以右边的值;如果两个运算对象都是整数,则结果将被截断,只保留整数部分。 |
% | 求余运算符 | 计算左边值除以右边值时的余数。 |
++ | 前缀自增运算符 | 在使用变量之前,先将变量的值加 1。 |
++ | 后缀自增运算符 | 先使用变量的当前值,之后再将该变量的值加 1。 |
-- | 前缀自减运算符 | 在使用变量之前,先将变量的值减 1。 |
-- | 后缀自减运算符 | 先使用变量的当前值,之后再将该变量的值减 1。 |
1.2 案例演示
#include <stdio.h>
int main()
{
// 二元加法运算符:左结合属性,把右边的值加到左边的值上。
int a = 5 + 3;
printf("a (5 + 3) = %d\n", a); // 8
// 一元正号运算符:右结合属性,生成一个大小和符号都与右边值相同的值。
int b = +5;
printf("b (+5) = %d\n", b); // 5
b = +-5; // 根据正负号运算符的右结合属性,等价于 +(-5)
printf("b (+-5) = %d\n", b); // -5
b = +(-5); // 使用括号来强调运算优先级,使运算结构更加清晰
printf("b (+(-5)) = %d\n\n", b); // -5
// 二元减法运算符:左结合属性,从左边的值中减去右边的值。
int c = 5 - 3;
printf("c (5 - 3) = %d\n", c); // 2
// 一元负号运算符:右结合属性,生成一个与右边值大小相等但符号相反的值。
int d = -5;
printf("d (-5) = %d\n", d); // -5
// d = --5;
printf("d = --5 是错误的写法,--会被当成前缀自减运算符,前置递减运算符只能作用于左值。\n");
printf("\t左值是指可以出现在赋值表达式左侧的对象,通常是具有内存地址的变量。\n");
printf("\t数字字面量(如 5)是一个右值,它没有内存地址,因此不能作为前置递减运算符的操作数。\n");
// 正确的写法如下所示:
d = -(-5);
printf("d (-(-5)) = %d\n\n", d); // 5
// 乘法运算符:左结合属性,把左边的值乘以右边的值。
int e = 5 * 3;
printf("乘法:e (5 * 3) = %d\n", e); // 15
// 除法运算符:左结合属性,把左边的值除以右边的值。
// 如果两个运算对象都是整数,则结果将被截断为整数部分。
int f1 = 10 / 3;
printf("整型除法:f1 (10 / 3) = %d\n", f1); // 结果不是 3.333333,而是 3,小数部分会被截断
float f2 = 10.0 / 3.0;
printf("浮点型除法:f2 (10.0 / 3.0) = %f\n", f2); // 结果是 3.333333
// 求余运算符:左结合属性,计算左边值除以右边值时的余数。
int g = 10 % 3;
printf("求余:g (10 %% 3) = %d\n\n", g); // 1,注意这里需要使用转义字符 %% 来表示一个 %
// 前缀自增运算符:右结合属性,在使用变量之前,先将变量的值加 1。
int h = 5;
int i = ++h; // 先将变量 h 的值加 1(为 6),然后再将值(6)赋值给变量 i。
printf("i (++h) = %d, h = %d\n", i, h); // h 和 i 都将是 6
// 后缀自增运算符:左结合属性,
int j = 5;
int k = j++; // 先将变量 j 的值(为 5)赋值给变量 k,然后再将变量 j 的值加 1(为 6)。
printf("k (j++) = %d, j = %d\n", k, j); // k 将是 5, 之后 j 变为 6
// 前缀自减运算符:右结合属性,在使用变量之前,先将变量的值加 1。
int l = 5;
int m = --l; // 先将变量 l 的值减 1(为 4),然后再将值(4)赋值给变量 m。
printf("m (--l) = %d, l = %d\n", m, l); // l 和 m 都将是 4
// 后缀自减运算符:左结合属性,
int n = 5;
int o = n--; // 先将变量 n 的值(为 5)赋值给变量 o,然后再将变量 n 的值减 1(为 4)。
printf("o (n--) = %d, n = %d\n", o, n); // o 将是 5,之后 n 变为 4
return 0;
}
程序在 VS code 中的运行结果如下所示:
2 关系运算符
2.1 运算符说明
下面的每个运算符都把左边的值与右边的值相比较。
运算符 | 类型 | 描述 |
---|---|---|
< | 小于 | 左边的值小于右边的值时为真 (1) |
<= | 小于或等于 | 左边的值小于或等于右边的值时为真 (1) |
== | 等于 | 左边的值等于右边的值时为真 (1) |
> | 大于 | 左边的值大于右边的值时为真 (1) |
>= | 大于或等于 | 左边的值大于或等于右边的值时为真 (1) |
!= | 不等于 | 左边的值不等于右边的值时为真 (1) |
2.2 关系表达式
简单的关系表达式由关系运算符及其两侧的运算对象组成。
- 如果关系为真,则关系表达式的值为 1;
- 如果关系为假,则关系表达式的值为 0。
下面是两个例子:
- 5 > 2 关系为真,整个表达式的值为 1。
- (2 + a) == a 关系为假,整个表达式的值为 0。
2.3 案例演示
#include <stdio.h>
int main()
{
int a = 66;
int b = 99;
// 使用大于运算符 > 比较 a 和 b
// 如果 a 大于 b,则返回 1(真),否则返回 0(假)
printf("a > b 的值:%d\n", a > b); // 输出为 0,因为 66 不大于 99
// 使用大于等于运算符 >= 比较 a 和 b
// 如果 a 大于等于 b,则返回 1(真),否则返回 0(假)
printf("a >= b 的值:%d\n", a >= b); // 输出为 0,因为 66 不大于等于 99
// 使用小于运算符 < 比较 a 和 b
// 如果 a 小于 b,则返回 1(真),否则返回 0(假)
printf("a < b 的值:%d\n", a < b); // 输出为 1,因为 66 小于 99
// 使用小于等于运算符 <= 比较 a 和 b
// 如果 a 小于等于 b,则返回 1(真),否则返回 0(假)
printf("a <= b 的值:%d\n", a <= b); // 输出为 1,因为 66 小于等于 99
// 使用等于运算符 == 比较 a 和 b
// 如果 a 等于 b,则返回 1(真),否则返回 0(假)
printf("a == b 的值:%d\n", a == b); // 输出为 0,因为 66 不等于 99
// 使用不等于运算符 != 比较 a 和 b
// 如果 a 不等于 b,则返回 1(真),否则返回 0(假)
printf("a != b 的值:%d\n", a != b); // 输出为 1,因为 66 不等于 99
return 0;
}
程序在 VS code 中的运行结果如下所示:
3 赋值运算符
3.1 运算符说明
C 语言有一个基本赋值运算符,= 运算符是基本的形式。
3.1.1 基本赋值运算符
运算符 | 类型 | 描述 |
---|---|---|
= | 基本赋值 | 将右边表达式的值赋给左边的操作数(左值、变量)。 |
3.1.2 复合赋值运算符
C 语言不仅提供了一个基本的赋值运算符,还提供了多个复合赋值运算符,它们可以简化某些操作。
每个复合赋值运算符都是基本赋值运算符(=)和另一个二元运算符的组合。
下面的每个赋值运算符都根据它右边的值更新其左边的左值。
运算符 | 类型 | 描述 |
---|---|---|
+= | 加法赋值 | 将右边的值加到左边变量上,并将结果存回左边变量。 |
-= | 减法赋值 | 从左边变量中减去右边的值,并将结果存回左边变量。 |
*= | 乘法赋值 | 将左边变量乘以右边的值,并将结果存回左边变量。 |
/= | 除法赋值 | 将左边变量除以右边的值,并将结果存回左边变量。 |
%= | 求余赋值 | 计算左边变量除以右边值的余数,并将结果存回左边变量。 |
&= | 按位与赋值 | 对左边变量和右边值执行按位与操作,并将结果存回左边变量。 |
|= | 按位或赋值 | 对左边变量和右边值执行按位或操作,并将结果存回左边变量。 |
^= | 按位异或赋值 | 对左边变量和右边值执行按位异或操作,并将结果存回左边变量。 |
>>= | 右移赋值 | 将左边变量右移指定的位数(由右边值决定),并将结果存回左边变量。 |
<<= | 左移赋值 | 将左边变量左移指定的位数(由右边值决定),并将结果存回左边变量。 |
使用复合赋值运算符可以减少代码量并提高可读性。例如:
- rabbits *= 1.6; 与 rabbits = rabbits * 1.6 效果相同。
3.2 案例演示
#include <stdio.h>
int main()
{
// 基本赋值运算符
int a = 10;
int b = 3;
// 展示初始值
printf("初始值: a = %d, b = %d\n", a, b);
// 加法赋值
a += b; // 等同于 a = a + b;
printf("a += b 后,左值 a = %d\n", a); // 10 + 3 = 13
// 减法赋值
a -= b; // 等同于 a = a - b;
printf("a -= b 后,左值 a = %d\n", a); // 13 - 3 = 10
// 乘法赋值
a *= b; // 等同于 a = a * b;
printf("a *= b 后,左值 a = %d\n", a); // 10 * 3 = 30
// 除法赋值
a /= b; // 等同于 a = a / b;
printf("a /= b 后,左值 a = %d\n", a); // 30 / 3 = 10
// 求余赋值
a %= b; // 等同于 a = a % b;
printf("a %%= b 后,左值 a = %d\n\n", a); // 10 % 3 = 1
// 按位与(有 0 出 0)赋值
int c = 5; // 5 的二进制为 0101
c &= 3; // 3 的二进制为 0011, 按位与结果是 0001 (1)
printf("c &= 3 后: c = %d\n", c); // 1
// 按位或(有 1 出 1)赋值
c |= 2; // 2 的二进制为 0010, 1 的二进制 0001, 按位或结果是 0011 (3)
printf("c |= 2 后: c = %d\n", c); // 3
// 按位异或(相同为 0,不同为 1)赋值
c ^= 6; // 6 的二进制为 0110, 3 的二进制 0011, 结果是 0101 (5)
printf("c ^= 6 后: c = %d\n", c); // 5
// 右移赋值
int d = 8; // 8 的二进制为 0000 1000
d >>= 1; // 右移 1 位(相当于除 2 的 1 次方),结果是 0000 0100 (4)
printf("d >>= 1 后: d = %d\n", d); // 4
// 左移赋值
d <<= 2; // 4 的二进制为 0000 0100, 左移 2 位(相当于乘 2 的 2 次方)结果是 0001 0000 (16)
printf("d <<= 2 后: d = %d\n", d); // 16
return 0;
}
程序在 VS code 中的运行结果如下所示:
4 逻辑运算符
4.1 运算符说明
逻辑运算符通常以关系表达式作为运算对象。! 运算符只需要一个运算对象,其他运算符需要两个运算对象,运算符左边一个,右边一个。
运算符 | 类型 | 描述 |
---|---|---|
! | 逻辑非 | 对操作数逻辑取反。 如果操作数为真(非零),结果为假(0); 如果操作数为假(0),结果为真(1)。 |
&& | 逻辑与 | 如果两个操作数都为真(非零),结果为真(1);否则结果为假(0)。【一假则假】 短路求值:如果第一个操作数为假,则不计算第二个操作数。 |
|| | 逻辑或 | 如果至少一个操作数为真(非零),结果为真(1);【一真则真】 只有当两个操作数都为假(0)时,结果为假(0)。 短路求值:如果第一个操作数为真,则不计算第二个操作数。 |
4.2 逻辑表达式
逻辑表达式是由操作数和逻辑运算符(如 &&、|| 和 !)组成的表达式,用于执行布尔逻辑运算并返回真(1)或假(0)的结果。
逻辑非 (!)
- 如果操作数的值为假(0),则 !操作数 的值为真(1);
- 如果操作数的值为真(非零),则 !操作数 的值为假(0)。
- 示例:!0 的值为 1,而 !5 的值为 0。
逻辑与 (&&)
- 当且仅当两个操作数都为真(非零)时,操作数 1 && 操作数 2 的值才为真(1)。否则,结果为假(0)。【一假则假】
- 示例:5 && 0 的值为 0,因为第二个操作数为假。
逻辑或 (||)
- 当两个操作数中至少有一个为真(非零)时,操作数 1 || 操作数 2 的值为真(1)。【一真则真】
- 只有当两个操作数都为假(0)时,结果为假(0)。
- 示例:5 || 0 的值为 1,因为第一个操作数为真。
4.3 逻辑表达式的求值顺序
逻辑表达式的求值顺序是从左到右依次进行。
在某些情况下,编译器会根据逻辑运算符的特性进行短路求值(Short-Circuit Evaluation)。也就是说,当已经能够确定整个表达式的结果时,后续的操作数将不再被计算。
4.3.1 短路求值规则
逻辑与 (&&):
如果左侧操作数为假(0),则整个表达式必定为假,右侧操作数不会被计算。
逻辑或 (||):
如果左侧操作数为真(非零),则整个表达式必定为真,右侧操作数不会被计算。
4.3.2 示例分析
示例 1:
6 > 2 && 3 == 3
- 求值过程:
- 首先计算 6 > 2,结果为真(1)。
- 然后计算 3 == 3,结果也为真(1)。
- 最终结果:真(1)。
示例 2:
! (6 > 2 && 3 == 3)
- 求值过程:
- 先计算括号内的 6 > 2 && 3 == 3,结果为真(1)。
- 然后对结果取反,!1 的值为假(0)。
- 最终结果:假(0)。
示例 3:
x != 0 && 20 / x < 5
- 求值过程:
- 首先计算 x != 0。如果 x 为 0,则整个表达式为假,右侧的 20 / x < 5 不会被计算,从而避免了除以零的错误。
- 如果 x 非零,则继续计算 20 / x < 5。
- 最终结果:取决于 x 的具体值。
4.4 案例演示
#include <stdio.h>
int main()
{
int x = 0, y = 10;
// 示例 1:逻辑与 (&&) 的短路性质
// 如果第一个条件为假,则不会计算第二个条件
printf("示例 1: 逻辑与 (&&) 短路性质\n");
if (x != 0 && y / x > 5)
{ // 第一个条件 x != 0 为假,因此不会计算 y / x > 5
printf("条件成立: x != 0 && y / x > 5\n");
}
else
{
printf("条件不成立,短路发生。\n"); // 输出此行,避免了除以零错误
}
// 示例 2:逻辑或 (||) 的短路性质
// 如果第一个条件为真,则不会计算第二个条件
printf("\n示例 2: 逻辑或 (||) 短路性质\n");
if (y > 5 || ++x > 0)
{ // 第一个条件 y > 5 为真,因此不会计算 ++x > 0
printf("条件成立: y > 5 || ++x > 0\n");
}
printf("x 的值:%d\n", x); // 输出 x 的值仍为 0,因为 ++x 没有被执行
// 示例 3:逻辑非 (!) 的使用
// 对条件取反,判断 x 是否为 0
printf("\n示例 3: 逻辑非 (!) 的使用\n");
if (!x)
{ // !x 为真,因为 x == 0
printf("x 的值为 0\n");
}
else
{
printf("x 的值不为 0 \n");
}
// 示例 4:结合逻辑运算符的复杂条件判断
// 使用逻辑与和逻辑或组合,体现优先级和短路特性
printf("\n示例 4: 结合逻辑运算符的复杂条件判断\n");
printf("此时的 x = %d, y = %d\n", x, y); // x = 0, y = 10
if ((x == 0 || y > 20) && !(y == 10))
// x == 0 为真,根据逻辑或的短路性质,右侧的 y > 20 不会被计算,结果:(x == 0 || y > 20) 的值为真(1)
// y == 10 的值为真,结果: !(y == 10) 的值为假(0)
// 最终:(真 && 假) 结果为假(0)
{
printf("复杂条件成立。\n");
}
else
{
printf("复杂条件不成立。\n");
}
return 0;
}
程序在 VS code 中的运行结果如下所示:
5 条件运算符
5.1 运算符说明
运算符 | 类型 | 描述 |
---|---|---|
?: | 条件运算符 | ?: 有三个运算对象,每个运算对象都是一个表达式。 根据条件表达式的结果选择两个表达式中的一个。 语法为:expression1 ? expression2 : expression3。 如果 expression1 为真(非零),则返回 expression2 的值; 否则返回 expression3 的值。 |
5.2 案例演示
#include <stdio.h>
int main()
{
// 示例 1:基本用法
int result1 = (5 > 3) ? 1 : 2; // 如果 5 > 3 为真,则返回 1;否则返回 2
printf("(5 > 3) ? 1 : 2 的值为:%d\n", result1); // 输出:1
int result2 = (3 > 5) ? 1 : 2; // 如果 3 > 5 为假,则返回 2
printf("(3 > 5) ? 1 : 2 的值为:%d\n", result2); // 输出:2
// 示例 2:比较两个数的大小
int a = 10, b = 20;
int max = (a > b) ? a : b; // 如果 a > b 为真,则返回 a;否则返回 b
printf("a 和 b 中较大的值是:%d\n", max); // 输出:20
// 示例 3:嵌套使用条件运算符
int x = 5, y = 10, z = 15;
int largest = (x > y) ? ((x > z) ? x : z) : ((y > z) ? y : z);
// 解释:先比较 x 和 y,取较大者;再与 z 比较,最终得到最大值
printf("x, y, z 中最大的值是:%d\n", largest); // 输出:15
return 0;
}
程序在 VS code 中的运行结果如下所示:
6 指针运算符
6.1 运算符说明
运算符 | 类型 | 描述 |
---|---|---|
& | 地址运算符 | 当它作用于一个变量时,返回该变量的内存地址。 语法:&variable。 |
* | 间接或解引用运算符 | 当它作用于一个指针时,返回指针所指向内存地址中存储的值。 语法:*pointer。 |
6.2 案例演示
#include <stdio.h>
int main()
{
int nurse = 22; // 定义一个整型变量 nurse,并赋值为 22
int *ptr; // 定义一个指针变量 ptr,用于存储地址
int val; // 定义一个变量 val,用于存储解引用后的值
// 使用地址运算符 & 获取 nurse 的地址
ptr = &nurse; // 将 nurse 的地址赋值给指针 ptr
printf("nurse 的地址是:%p\n", (void *)ptr); // 输出 nurse 的地址
// 使用解引用运算符 * 获取指针指向的值
val = *ptr; // 解引用 ptr,获取 nurse 的值并赋值给 val
printf("ptr 指向的值是:%d\n", val); // 输出 ptr 指向的值
// 修改指针指向的值
*ptr = 33; // 通过解引用修改 nurse 的值
printf("修改后,nurse 的值是:%d\n", nurse); // 输出修改后的 nurse 值
return 0;
}
程序在 VS code 中的运行结果如下所示:
7 符号运算符
7.1 运算符说明
运算符 | 类型 | 描述 |
---|---|---|
- | 负号运算符 | 反转运算对象的符号,将其从正数变为负数,或从负数变为正数。 语法:-expression。 |
+ | 正号运算符 | 不改变运算对象的符号,通常用于显式表示正值。 语法:+expression。 |
7.2 案例演示
#include <stdio.h>
int main()
{
int a = 10;
int b = -5;
// 使用负号运算符
int neg_a = -a; // 将 a 的符号取反,结果为 -10
int neg_b = -b; // 将 b 的符号取反,结果为 5
printf("负号运算符:\n");
printf("-a = %d\n", neg_a); // 输出:-10
printf("-b = %d\n", neg_b); // 输出:5
// 使用正号运算符
int pos_a = +a; // 不改变 a 的符号,结果仍为 10
int pos_b = +b; // 不改变 b 的符号,结果仍为 -5
printf("\n正号运算符:\n");
printf("+a = %d\n", pos_a); // 输出:10
printf("+b = %d\n", pos_b); // 输出:-5
return 0;
}
程序在 VS code 中的运行结果如下所示:
8 结构和联合运算符
结构和联合使用一些运算符标识成员。
- 成员运算符与结构和联合一起使用;
- 间接成员运算符与指向结构或联合的指针一起使用。
8.1 成员运算符
运算符 | 类型 | 描述 |
---|---|---|
. | 成员运算符 | 与结构名或联合名一起使用,用于访问结构或联合中的成员。 语法:struct_or_union_name.member_name。 |
成员运算符 (.
) 主要用于直接访问结构体或联合体中定义的成员变量。当你有一个结构体或联合体的实例时,可以通过这个运算符来访问其内部的成员。 如下代码所示:
#include <stdio.h>
int main()
{
// 定义一个结构体模板
struct Product
{
int code; // 商品编号
float cost; // 商品价格
};
// 定义结构体变量
struct Product item;
// 使用成员运算符 (.)访问并修改结构体 item 中的成员
item.code = 1265; // 将值 1265 赋给结构体变量 item 的成员 code
item.cost = 99.99; // 将值 99.99 赋给结构体变量 item 的成员 cost
printf("商品编号: %d, 商品价格: %.2f\n", item.code, item.cost);
return 0;
}
程序在 VS code 中的运行结果如下所示:
8.2 间接成员运算符
运算符 | 类型 | 描述 |
---|---|---|
-> | 间接成员运算符 或 结构指针运算符 | 与一个指向结构或联合的指针一起使用,用于通过指向结构或联合的指针访问成员。 语法:pointer_to_struct_or_union->member_name。 |
间接成员运算符 (->) 用于通过指向结构体或联合体的指针访问其成员变量。当你有一个指向结构体或联合体的指针时,使用这个运算符可以方便地访问该结构体或联合体中的成员。如下代码所示:
#include <stdio.h>
int main()
{
// 定义一个结构体模板
struct Product
{
int code; // 商品编号
float cost; // 商品价格
};
// 定义结构体变量和指针
struct Product item;
struct Product *ptrst;
// 使用间接成员运算符 (->)
ptrst = &item; // 将指针指向结构体变量 item
ptrst->code = 3451; // 使用间接成员运算符访问结构体成员 code
ptrst->cost = 199.99; // 使用间接成员运算符访问结构体成员 cost
printf("商品编号: %d, 商品价格: %.2f\n", ptrst->code, ptrst->cost);
// 等效写法验证
(*ptrst).code = 5678; // 使用解引用运算符和成员运算符访问并修改成员 code
(*ptrst).cost = 299.99; // 使用解引用运算符和成员运算符访问并修改成员 cost
printf("\n等效写法验证:\n");
printf("商品编号: %d, 商品价格: %.2f\n", item.code, item.cost);
return 0;
}
程序在 VS code 中的运行结果如下所示:
8.3 等效写法说明
在 C 语言中,结构体成员可以通过不同的方式访问,具体取决于你是直接操作结构体变量还是通过指针操作。如下代码所示:
#include <stdio.h>
int main()
{
// 定义结构体模板
struct Product
{
int code;
float cost;
};
// 定义结构体变量和指针
struct Product item;
struct Product *ptrst;
// 将指针指向结构体变量
ptrst = &item;
// 方法 1: 使用间接成员运算符 ->
ptrst->code = 3451;
printf("方法 1 (ptrst->code): %d\n", ptrst->code);
// 方法 2: 使用成员运算符 .
item.code = 5678;
printf("方法 2 (item.code): %d\n", item.code);
// 方法 3: 使用解引用运算符和成员运算符
(*ptrst).code = 91011;
printf("方法 3 ((*ptrst).code): %d\n", (*ptrst).code);
return 0;
}
以上三种写法是等效的,最终效果都是将值赋给结构体 item 的成员 code。
程序在 VS code 中的运行结果如下所示:
8.3.1 ptrst->code
- 使用间接成员运算符 -> 访问指针所指向的结构体成员。
- 这是最常见的方式,尤其当使用指针时非常直观。
ptrst->code = 3451;
// ptrst 是一个指向结构体 item 的指针
// ptrst->code 表示通过指针访问 item 的成员 code
8.3.2 item.code
- 使用成员运算符 . 直接访问结构体变量的成员。
- 这种方式适用于你已经有一个结构体实例(而不是指针)的情况。
item.code = 3451;
// item 是一个结构体变量,item.code 表示直接访问 item 的成员 code
8.3.3 (*ptrst).code
- 使用解引用运算符 * 和成员运算符 . 组合访问结构体成员。
- 这种方式显式地解引用指针,然后访问结构体成员。
(*ptrst).code = 3451;
// *ptrst 表示解引用指针 ptrst,得到它指向的结构体变量(即 item)
// 然后通过成员运算符 . 访问成员 code
8.3.4 等效性分析
这三种写法之所以等效,是因为它们最终都访问了同一个内存区域——即结构体 item 中的 code 成员。让我们逐步分析它们的关系:
-
ptrst->code 和 (*ptrst).code 的等效性
- 在 C 语言中,ptrst->code 实际上是 (*ptrst).code 的语法糖。
- 编译器会将 ptrst->code 自动转换为 (*ptrst).code。
- 因此,这两种写法完全等效,只是 -> 更简洁、更常用。
-
(*ptrst).code 和 item.code 的等效性
- 根据代码 ptrst = &item;,我们知道 ptrst 指向结构体变量 item。
- 解引用指针 *ptrst 得到的就是 item 本身。
- 因此,(*ptrst).code 等价于 item.code。
-
总结等效关系
- ptrst->code ⇔ (*ptrst).code ⇔ item.code
- 它们最终都修改了 item 结构体中的 code 成员。
扩展:
语法糖(Syntactic Sugar) 是编程语言中的一种术语,指的是某些语法设计上的简化或便利写法,目的是让代码更易读、更简洁,而不会改变程序的功能或语义。换句话说,语法糖是一种对已有功能的 “美化” 或 “简化”,但其底层实现和效果是完全等价的。
总结:
- ptrst->code:最简洁的方式,推荐用于指针操作。
- item.code:直接访问结构体成员,适用于非指针场景。
- (*ptrst).code:显式解引用指针后访问成员,本质上与 ptrst->code 等效。
9 按位运算符
9.1 运算符说明
运算符 | 类型 | 描述 |
---|---|---|
~ | 按位取反 | 翻转运算对象的每一位(0 变 1,1 变 0)得到一个值。 语法:~expression。 |
& | 按位与 | 对两个运算对象的对应位进行逻辑与操作,只有当两个位都为 1 时,结果位才为 1(有 0 为 0)。 语法:expression1 & expression2。 |
| | 按位或 | 对两个运算对象的对应位进行逻辑或操作,只要有一个位为 1,结果位就为 1(有 1 为 1)。 语法:expression1 | expression2。 |
^ | 按位异或 | 对两个运算对象的对应位进行异或操作,只有当两个位中只有一个为 1 时,结果位才为 1(相同位为 0,不同位为 1)。 语法:expression1 ^ expression2。 |
<< | 左移运算符 | 将运算对象的二进制位向左移动指定的位数,右侧空出的位用 0 填充。 数学意义:左移 n 位相当于将数值乘以 2^n。 语法:expression << n。 注意事项: 左移可能会导致溢出(超出数据类型的最大范围)。 |
>> | 右移运算符 | 将运算对象的二进制位向右移动指定的位数,左侧空出的位用 0 填充(对于无符号整数或正整数)。 数学意义:右移 n 位相当于将数值除以 2^n 并向下取整。 语法:expression >> n。 注意事项: 对于负数,右移的结果可能依赖于具体实现(算术右移或逻辑右移)。 算术右移:算术右移保留符号位(最高位),即用符号位填充高位,用于有符号整数。 |
9.2 案例演示
#include <stdio.h>
int main()
{
int x = 2; // 二进制表示为 0000 0010
int y = 3; // 二进制表示为 0000 0011
// 按位取反 (~):0 变 1,1 变 0
int not_x = ~x;
// 二进制位:0000 0010
// 操作:~
// 结果: 1111 1101(此补码表示一个负数)
// 我们对这个补码 1111 1101 进行按位取反,末尾加一
// 得到 0000 0011,即为该负数对应的绝对值数:3
// 所以,原补码表示的数据就是 -3
printf("~x 的值为:%d\n", not_x); // 输出:-3(二进制补码表示:1111 1101)
// 按位与 (&):有 0 为 0
int and_result = x & y;
// 0000 0010
// &
// 0000 0011
// 0000 0010,这是 2 的二进制表示
printf("x & y 的值为:%d\n", and_result); // 输出:2(二进制:0000 0010)
// 按位或 (|)
int or_result = x | y;
// 0000 0010
// |
// 0000 0011
// 0000 0011,这是 3 的二进制表示
printf("x | y 的值为:%d\n", or_result); // 输出:3(二进制:0000 0011)
// 按位异或 (^)
int xor_result = x ^ y;
// 0000 0010
// ^
// 0000 0011
// 0000 0001,这是 1 的二进制表示
printf("x ^ y 的值为:%d\n", xor_result); // 输出:1(二进制:0000 0001)
// 左移运算符 (<<)
int left_shift_result = y << x;
// 0000 0011
// << 2
// 0000 1100,这是 12 的二进制表示
printf("y << x 的值为:%d\n", left_shift_result); // 输出:12(二进制:0000 1100)
// 右移运算符 (>>)
int right_shift_result = y >> x;
// 0000 0011
// >> 2
// 0000 0000,这是 0 的二进制表示
printf("y >> x 的值为:%d\n", right_shift_result); // 输出:0(二进制:0000 0000)
return 0;
}
程序在 VS code 中的运行结果如下所示:
9.3 左移和右移运算符特性
在 C 语言中,左移运算符 (<<) 和 右移运算符 (>>) 是按位运算符,它们通过对二进制位的移动来实现高效的数据操作。
9.3.1 数学意义
左移 n 位相当于对数值乘以 2^n。
#include <stdio.h>
int main()
{
unsigned int num = 5; // 5 的 32 位二进制表示为 0000 0000 0000 0000 0000 0000 0000 0101
int shift = 2; // 需要左移的位数
// 左移操作
unsigned int result = num << shift; // 将 num 的二进制位向左移动 2 位
printf("原始值: %u (二进制: ", num);
for (int i = 31; i >= 0; i--)
{
// 将 num 向右移动 i 位,使第 i 位移到最低位
// 对结果进行按位与操作,提取最低位(0 或 1)
printf("%d", (num >> i) & 1); // 打印每一位
}
printf(")\n");
printf("左移 %d 位后: %u (二进制: ", shift, result);
for (int i = 31; i >= 0; i--)
{
printf("%d", (result >> i) & 1); // 打印每一位
}
printf(")\n");
// 数学验证
// 左移 shift 位等价于乘以 2^shift
printf("数学验证: %u * 2^%d = %u\n", num, shift, num * (1 << shift));
return 0;
}
程序在 VS code 中的运行结果如下所示:
右移 n 位相当于对数值除以 2^n 并向下取整(对于无符号整数或正整数)。
#include <stdio.h>
int main()
{
unsigned int num = 20; // 20 的 32 位二进制表示为 0000 0000 0000 0000 0000 0000 0001 0100
int shift = 2; // 需要右移的位数
// 右移操作
unsigned int result = num >> shift; // 将 num 的二进制位向右移动 2 位
printf("原始值: %u (二进制: ", num);
for (int i = 31; i >= 0; i--)
{
// 将 num 向右移动 i 位,使第 i 位移到最低位
// 对结果进行按位与操作,提取最低位(0 或 1)
printf("%d", (num >> i) & 1); // 打印每一位
}
printf(")\n");
printf("右移 %d 位后: %u (二进制: ", shift, result);
for (int i = 31; i >= 0; i--)
{
printf("%d", (result >> i) & 1); // 打印每一位
}
printf(")\n");
// 数学验证
// 左移 shift 位等价于乘以 2^shift
printf("数学验证: %u / 2^%d = %u\n", num, shift, num / (1 << shift));
return 0;
}
程序在 VS code 中的运行结果如下所示:
9.3.2 注意事项与常见问题
-
溢出问题:
- 左移可能导致数值超出数据类型的范围。例如,对于 unsigned int 类型,左移过多位会导致高位丢失。
-
负数的右移:
- 对于负数,右移的行为可能因编译器不同而异:
- 算术右移:算术右移保留符号位(最高位),即用符号位填充高位,用于有符号整数。
- 如果整数是正数,高位补 0,因为符号位是 0。
- 如果整数是负数,高位补 1,因为符号位是 1。
-
int num = -8; // 二进制: 1111 1111 1111 1111 1111 1111 1111 1000 (假设 32 位) int result = num >> 2; // 右移 2位: 1111 1111 1111 1111 1111 1111 1111 1110 // result 为 -2,因为高位补 1,保持符号。
- 逻辑右移:逻辑右移不保留符号位,高位总是补 0,用于无符号整数。在某些编译器和硬件平台上,有符号整数也可能执行逻辑右移。
- 无论整数是正数还是负数,高位总是补 0。
-
unsigned int num = 8; // 二进制: 0000 0000 0000 0000 0000 0000 0000 1000 unsigned int result = num >> 2; // 右移 2 位: 0000 0000 0000 0000 0000 0000 0000 0010 // result 为 2,因为高位补 0。
- 算术右移:算术右移保留符号位(最高位),即用符号位填充高位,用于有符号整数。
- 对于负数,右移的行为可能因编译器不同而异:
-
适用于无符号整数:
- 左移和右移运算符在处理无符号整数时最为直观和可靠。
总结:
- 左移 (<<) 相当于对数值乘以 2^n,是一种高效的乘法操作。
- 右移 (>>) 相当于对数值除以 2^n 并向下取整,是一种高效的除法操作。
- 在 C 语言标准中,右移操作的行为对于有符号整数是实现定义的,这意味着不同的编译器和硬件平台可能会有不同的实现方式:
- 一些平台对有符号整数执行算术右移。
- 一些平台对有符号整数执行逻辑右移。
- 对于无符号整数,C 语言标准要求执行逻辑右移。
- 在实际编程中,左移和右移常用于优化性能,尤其是在嵌入式开发和底层编程中。
10 混合运算符
10.1 大小运算符(sizeof)
- 用途:sizeof 是一个编译时运算符,用于获取数据类型或变量在内存中所占的字节数。单位是 char 的大小,通常,char 类型的大小是 1 字节。
- 语法:
- sizeof(type):返回指定类型的大小。
- sizeof(expression) :返回表达式的大小。
- sizeof 字面量/变量名/数组名 :返回字面量/变量名/数组名的大小。
- 括号的可选性:
-
计算基本数据类型的大小,必须使用括号将数据类型关键字包裹起来。
-
对于字面量和变量,sizeof 运算符可以直接作用于它们,括号是可选的,可以省略括号。
-
一般建议在使用 sizeof 运算符时主动加上后面的小括号,这样能使代码更加清晰明了。
-
- 返回值类型:size_t,这是一个无符号整数类型,格式占位符可用 %zu。
#include <stdio.h>
int main()
{
// 计算基本数据类型的大小,必须使用括号将数据类型关键字包裹起来
printf("char:%zu \n", sizeof(char)); // char:1,表示 char 类型占用 1 个字节
printf("short:%zu \n", sizeof(short)); // short:2 ,表示 short 类型占用 2 个字节
printf("int:%zu \n", sizeof(int)); // int:4 ,但这也可能因编译器和平台而异
printf("long:%zu \n", sizeof(long)); // long:4,但这也可能因编译器和平台而异
printf("long long:%zu \n", sizeof(long long)); // long long:8,表示 long long 类型占用 8 个字节
printf("float:%zu \n", sizeof(float)); // float:4 ,表示 float 类型占用 4 个字节
printf("double:%zu \n", sizeof(double)); // double:8 ,表示 double 类型占用 8 个字节
printf("long double:%zu \n", sizeof(long double)); // long double:16 ,但这也可能因编译器和平台而异
printf("\n");
// 计算字面量数据的大小
// 对于字面量,sizeof 运算符可以直接作用于它们,括号是可选的,可以省略括号
// 字符字面量(如'a')在大多数情况下被视为 int 类型(但 sizeof 会返回其实际存储所需的大小)
// 整数和浮点数字面量的大小取决于它们被解释为的类型
printf("%zu \n", sizeof('a')); // 输出为 4,因为字符字面量被隐式提升为 int
printf("%zu \n", sizeof(431)); // 输出为 4,因为 431 是 int 类型的字面量
printf("%zu \n", sizeof 4.31); // 输出为 8,因为 4.31 默认为 double 类型的字面量
printf("\n");
// 计算变量的大小
// 对于变量,sizeof 运算符同样可以直接作用于它们,括号是可选的,可以省略括号
char a = 'A';
int b = 90;
long long c = 100;
double d = 10.8;
printf("a: %zu \n", sizeof(a)); // 输出为 1,表示 char 变量 a 占用 1 个字节
printf("b: %zu \n", sizeof b); // 输出为 4,表示 int 变量 b 占用 4 个字节
printf("c: %zu \n", sizeof c); // 输出为 8,表示 long long 变量 c 占用 8 个字节
printf("d: %zu \n", sizeof(d)); // 输出为 8,表示 double 变量 d 占用 8 个字节
printf("\n");
// 获取数组的大小
int arr[5] = {1, 2, 3, 4, 5};
printf("sizeof(arr): %zu 字节\n", sizeof(arr)); // 输出数组 arr 的总大小
printf("sizeof(arr[0]): %zu 字节\n", sizeof(arr[0])); // 输出数组第一个元素的大小
return 0;
}
程序在 VS code 中的运行结果如下所示:
10.2 对齐要求运算符(_Alignof)
- 用途:_Alignof 是 C11 标准引入的一个运算符,用于获取数据类型的对齐要求。对齐要求是指该类型的数据需要存储在内存地址的某个倍数上。(一些系统要求以特定值的倍数在地址上储存特定类型,如 4 的倍数。这个整数就是对齐要求。)
- 语法:
- _Alignof(type):返回指定类型的对齐要求。
- 返回值类型:size_t,这是一个无符号整数类型,格式占位符可用 %zu。
#include <stdio.h>
int main()
{
// 获取基本数据类型的对齐要求
printf("_Alignof(char): %zu 字节\n", _Alignof(char)); // 输出 char 类型的对齐要求
printf("_Alignof(int): %zu 字节\n", _Alignof(int)); // 输出 int 类型的对齐要求
printf("_Alignof(float): %zu 字节\n", _Alignof(float)); // 输出 float 类型的对齐要求
printf("_Alignof(double): %zu 字节\n", _Alignof(double)); // 输出 double 类型的对齐要求
return 0;
}
程序在 VS code 中的运行结果如下所示:
10.3 强制类型转换运算符(type)
- 用途:强制类型转换运算符 (type) 用于将一种类型的值显式地转换为另一种类型。需要注意的是,这种转换可能会导致精度损失或溢出。
- 语法:
- (type)expression:将表达式转换为圆括号中关键字指定的类型。
#include <stdio.h>
int main()
{
// 整数到浮点数的转换
int num = 42;
float float_num = (float)num; // 将整数 42 转换为浮点数 42.0
printf("强制类型转换: %d -> %.1f\n", num, float_num);
// 浮点数到整数的转换
float f = 3.14f;
int int_f = (int)f; // 将浮点数 3.14 转换为整数 3
printf("强制类型转换: %.2f -> %d\n", f, int_f);
// 指针转换(不推荐)
int *ptr = #
void *void_ptr = (void *)ptr; // 将指针转换为 void* 类型
int *back_to_int_ptr = (int *)void_ptr; // 将 void* 指针转换回 int* 类型
printf("指针转换: ptr = %p, back_to_int_ptr = %p\n", (void *)ptr, (void *)back_to_int_ptr);
return 0;
}
程序在 VS code 中的运行结果如下所示:
10.4 逗号运算符(,)
- 用途:逗号运算符
,
它把两个表达式链接成一个表达式,并保证先对最左端的表达式求值。整个表达式的值是最右边表达式的值。该运算符通常在 for 循环头中用于包含更多的信息。 - 语法:
- expression1, expression2:先计算 expression1,然后计算 expression2,整个表达式的值为 expression2 的值。
#include <stdio.h>
int main()
{
// 简单的逗号运算符示例
int a = (1, 2); // 计算 1 和 2,结果为 2
printf("a = %d\n", a);
// 在 for 循环中使用逗号运算符
int step, fargo;
for (step = 2, fargo = 0; fargo < 1000; step *= 2)
{
fargo += step;
printf("循环过程中: step = %d, fargo = %d\n", step, fargo);
}
printf("循环结束: step = %d, fargo = %d\n", step, fargo);
return 0;
}
程序在 VS code 中的运行结果如下所示: