本文适用于大学的期中期末考试、专升本(专接本、专插本)考试、408等考研预科。如有相关题目疑问或建议欢迎在评论区进行互动。
转载请标明出处。
本章我们将介绍运算符的相关知识。在C语言学习的初级阶段,我们仅需要掌握以下十二种运算符,即可在习题作业中表现得游刃有余,它们分别是:算数运算符、关系运算符、逻辑运算符、赋值运算符、条件运算符、逗号运算符、指针运算符、位操作运算符、自增自减运算符,以及特殊运算符、强制类型转换运算符、求字节数运算符。
我们以目录的形式,将这些运算符的优先级关系整理为表格,方便大家快速定位查阅。
C运算符的优先级(目录) 优先级 运算符 名称或含义 使用形式 结合方向 说明 1 [] 数组下标 数组名[常量表达式] 从左到右 () 圆括号 (表达式)/函数名(形参表) . 成员选择(对象) 对象.成员名 -> 成员选择(指针) 对象指针->成员名 2 - 负号 -表达式 从右到左 单目运算符 (类型) 强制类型转换 (数据类型)表达式 ++ 自增 ++变量名/变量名++ 单目运算符 -- 自减 --变量名/变量名-- 单目运算符 * 取值运算符 *指针变量 单目运算符 & 取地址运算符 &变量名 单目运算符 ! 逻辑非 !表达式 单目运算符 ~ 按位取反 ~表达式 单目运算符 sizeof 长度运算符 sizeof(表达式) 3 / 除 表达式/表达式 从左到右 双目运算符 * 乘 表达式*表达式 双目运算符 % 余数(取模) 整型表达式/整型表达式 双目运算符 4 + 加 表达式+表达式 从左到右 双目运算符 - 减 表达式-表达式 双目运算符 5 << 左移 变量<<表达式 从左到右 双目运算符 >> 右移 变量>>表达式 双目运算符 6 > 大于 表达式>表达式 从左到右 双目运算符 >= 大于等于 表达式>=表达式 双目运算符 < 小于 表达式<表达式 双目运算符 <= 小于等于 表达式<=表达式 双目运算符 7 == 等于 表达式==表达式 从左到右 双目运算符 != 不等于 表达式!=表达式 双目运算符 8 & 按位与 表达式&表达式 从左到右 双目运算符 9 ^ 按位异或 表达式^表达式 从左到右 双目运算符 10 | 按位或 表达式|表达式 从左到右 双目运算符 11 && 逻辑与 表达式&&表达式 从左到右 双目运算符 12 || 逻辑或 表达式||表达式 从左到右 双目运算符 13 ? : 条件运算符 表达式1?表达式2:表达式3 从右到左 三目运算符 14 = 赋值运算符 变量=表达式 从右到左 /= 除后赋值 变量/=表达式 *= 乘后赋值 变量*=表达式 %= 取模后赋值 变量%=表达式 += 加后赋值 变量+=表达式 -= 减后赋值 变量-=表达式 <<= 左移后赋值 变量<<=表达式 >>= 右移后赋值 变量>>=表达式 &= 按位与后赋值 变量&=表达式 ^= 按位异或后赋值 变量^=表达式 |= 按位或后赋值 变量|=表达式 15 , 逗号运算符 表达式,表达式,…… 从左到右 顺序运算 注[1]:同一优先级下,运算次序由结合方向所决定。
注[2]:带有小括号的标题跳转有误,请手动前往或点击左侧大纲目录。
1.1 数组下标 []
在C语言中,对于一个数组,我们常常这样来定义:
数据类型 数组名[常量表达式]; //常量表达式必须是整型
数组的下标从0开始。
举个例子:
int a[10]; //定义一个整型数组a,该数组从a[0]开始,到a[9]结束,共十个元素
对于数组的详细解读我们后面会单写一个章节,在这里我们仅需要了解它作为“第一优先级”的成员,其优先级是凌驾于其他符号之上的。
1.2 圆括号 ()
圆括号在C语言中常常有以下三种用法:
函数调用:使用圆括号来调用函数,并传入参数。
在解决复杂问题的时候,我们往往不能只用一个main函数就编写完整个C语言代码,这时候就需要定义多个类型的函数,并对它们进行来回的调用以达到目的。同样的,我们会在后面的章节单独学习函数的相关知识,这里我们仅仅举一个在函数调用中使用圆括号的例子。
int secondary(int b) //secondary为被调函数
{
return b; //返回b的值
}
int main() //main为主调函数
{
int a = 5;
}
控制运算符优先级:使用圆括号可以强制控制或改变运算符的优先级和结合性。
举个例子:
int x = 10, y = 20;
int sum = x + y * 2; //默认乘法优先
int sum_with_parentheses = (x + y) * 2; //指定加法优先
控制运行流程:用于条件语句如if、while、for等,以及在函数中定义作用域。
举个例子:
int i;
scanf(" %d ", &i );
if( ( i > 0 ) && ( i < 10 ) ) //如果输入的i的值大于0且小于10,则执行花括号内的语句
{
printf( "这是一个位于0到10之间的数字\n" );
}
1.3 成员选择(对象) .
成员运算符在结构体中常常使用,如:
struct manager mgr1, *p;
p = &mgr1; //p是指向结构体变量的指针,此时的*p即等价于stu1
mgr1.id = 202477; //管理员1的id为202477,格式为:结构体变量名.成员名;
等价于:
(*p).id = 202477; //管理员1的id为202477,格式为:(*指针名).成员名;
等价于:
p->id = 202477; //管理员1的id为202477,格式为:指针名->成员名;
1.4 成员选择(指针) ->
指向运算符的相关举例已举例于1.3末。
2.1 负号 -
即表示相关数据的负值,通常有以下两种用法:
放在变量前:
int num = 5;
int negNum = -num;
放在常量前:
int num;
num = -5;
2.2 强制类型转换 (类型)
使用圆括号将需要转换的数据类型括起来,作为强制类型转换的运算符。例如:
(int) 2.71; //将浮点数2.71强制转换为整数类型
(int) 2.71 + 3.14; //将浮点数2.71强制转换为整数类型,再加上浮点数3.14,结果为浮点数5.14
(int) (2.71 + 3.14); //将浮点数2.71与浮点数3.14相加,得到结果为浮点数5.85后,再进行强制转换,最终结果为5
其是将小数部分进行截断处理,即结果为2,而非四舍五入的3。
而在C语言中,除了强制类型转换外,还存在一种自动转换机制,其规则如图所示:
2.3 自增 ++
使单个变量的值加一,一般用法有前缀、后缀之分:
++变量名 //前缀
变量名++ //后缀
这里是初学者经常犯错的地方之一,也是各大类型的基础考卷所需要考察的“基本功”之一,注意辨别并体会以下这个例子:
int m = 2,n;
m++; //即 m = m + 1,m的值为3
++m; //即 m = m + 1,m的值为3
n = m++; //即 n = m; m = m + 1,先操作再自增,n的值为2,m的值为3
n = ++m; //即 m = m + 1; n = m,先自增再操作,n的值为3,m的值为3
我们可以记忆这样一个口诀:变量相连即操作,变量不连自增/减。
其中是否相连可以以等号两边为变量还是自增/减运算符作为判断。
2.4 自减 --
类似的,自减也同样有前缀、后缀之分:
--变量名 //前缀
变量名-- //后缀
同样适用于2.3所提到的例子:
int m = 2,n;
m--; //即 m = m - 1,m的值为1
--m; //即 m = m - 1,m的值为1
n = m--; //即 n = m; m = m - 1,先操作再自增,n的值为2,m的值为1
n = --m; //即 m = m - 1; n = m,先自增再操作,n的值为1,m的值为1
2.5 取值运算符 *
取值运算符用于取得指针指向的变量的值,例如:
int x = 10;
int *p;
p = &x; //使用取值运算符获取x的值
int y = *p; //*p 将获取指针 p 指向的内存地址中的值,也就是变量 x 的值
printf("x的值是: %d\n", x); //程序将打印出 x 的值是 10
printf("y的值是: %d\n", y); //程序将打印出 y 的值是 10
2.6 取地址运算符 &
取址运算符用于获取操作数的内存地址。
基本的取址操作例如:
int x = 10; //定义一个整型变量x并初始化为10
int *p; //定义一个指向整型的指针p
p = &x; //使用取址运算符 & 来获取 x 的内存地址并将其存储在 p 中
printf("Address of x: %p", &x); //使用 printf函数 打印出 x 的地址
或者我们可以用其取得数组的地址:
int arr[5] = {1, 2, 3, 4, 5}; //定义一个有5个元素的数组arr并初始化
int (*p)[5]; //定义一个指向有5个整型元素的数组的指针p
p = &arr; //使用取址运算符 & 来获取 arr 的内存地址并将其存储在 p中
printf("Address of arr: %p", &arr); //使用 printf函数 打印出 arr 的地址
类似的,我们也可以这样来取得指针的地址:
int x = 10; //定义一个整型变量x并初始化为10
int *p = &x; //定义一个指向整型的指针 p 并使用取址运算符 & 来获取 x 的内存地址
int **pp; //定义一个指向整型指针的指针pp
pp = &p; //使用取址运算符 & 来获取 p 的内存地址并将其存储在 pp 中
printf("Address of p: %p", &p); //使用 printf函数 打印出 p 的地址
2.7 逻辑非 !
其作用是对其操作数的逻辑状态进行反转,如果操作数为真,那么逻辑非运算符的结果就为假。如果操作数为假,那么逻辑非运算符的结果就为真。且它只能对一个操作数进行操作。
使用逻辑非运算符可以对变量进行操作:
int a = 10;
if(!a) printf("a is false\n");
else printf("a is true\n");
/*变量a是一个非零值,因此,其逻辑值为真。运用逻辑非运算符 ! 后,结果为假。*/
使用逻辑非运算符也可以对常量进行操作:
int a = 0;
if(!0) printf("0 is false\n");
else printf("0 is true\n");
/*常量0本身的逻辑值就是假。运用逻辑非运算符 ! 后,结果仍然为假。*/
我们甚至可以使用逻辑非运算符对函数返回值进行操作:
int isEven(int num)
{
return (num % 2 == 0);
}
int main()
{
int num = 4;
if(!isEven(num)) printf("%d is odd\n", num);
else printf("%d is even\n", num);
return 0;
}
/*函数isEven()会判断一个数是否为偶数,如果是偶数,则返回真,否则返回假。逻辑非运算符 ! 会将这个返回值反转,从而得出结果。*/
2.8 按位取反 ~
按位取反运算符可以用来对一个二进制数按位取反,即将二进制数的每一位(0变为1,1变为0)。
值得注意的是:
按位取反运算符是单目运算符,只需要一个操作数。
对无符号整数进行按位取反操作,结果会保留在无符号整数的范围内。
对有符号整数进行按位取反操作,可能会因为符号位的变化而导致结果不是预期的
例如:
直接使用按位取反运算符:
unsigned int num = 10;
unsigned int result = ~num;
printf("%u\n", result);
使用按位取反运算符进行一系列操作
unsigned int num = 10;
unsigned int result;
result = ~num;
result &= 0x0F; //取反后,与0x0F进行与操作,实际上是取反后保留后四位
printf("%u\n", result);
使用按位取反运算符和循环实现取反:
unsigned int num = 10;
unsigned int result = 0;
int i;
for (i = 0; i < 32; i++)
{
if ((num & (1 << i)) == 0)
{
result |= (1 << i);
}
}
printf("%u\n", result);
2.9 长度运算符 sizeof
长度运算符用于获取数组、字符串、指针的长度,其返回的是指定类型或对象的长度(大小),单位是字节。
使用sizeof运算符获取数组长度:
int arr[] = {1, 2, 3, 4, 5, 6};
printf("The length of the array is: %lu\n", sizeof(arr)/sizeof(arr[0]));
/*使用sizeof运算符来获取整个数组的大小和一个元素的大小,通过除法将其转换为元素的数量,从而得到数组的长度。*/
使用sizeof运算符获取字符串长度:
char str[] = "Hello, World!";
printf("The length of the string is: %lu\n", sizeof(str)/sizeof(str[0]) - 1);
值得注意的是,在字符串的长度计算中,我们需要减去1,因为 \0 是字符串结束的标志。
使用sizeof运算符获取指针长度:
int *ptr;
printf("The length of the pointer is: %lu\n", sizeof(ptr));
/*使用sizeof运算符来获取指针的大小,在32位机器上,指针的长度通常是4个字节,在64位机器上,指针的长度通常是8个字节。*/
但sizeof运算符有一个弊端,即不能用于求得动态分配的内存的大小,例如使用malloc或calloc函数分配的内存。
3.1 除 /
区别于数学上的除法,若双目(两个运算对象)均为int,则除法为整除(进行截断处理),例如:
1/2 = 0;
1.0/2 = 0.5;
1/2.0 = 0.5; //若两边有任意一个浮点数,则结果也是浮点数,即不整除
3.2 乘 *
类似于数学上的乘法,但注意乘法符号不能用数学中的 × 或 · 代替,且不能省略。
3.3 余数(取模) %
取模运算符是用来获取两个整数相除之后的余数。例如:
1 % 2 = 1;
-10 % 3 = -1;
10 % -3 = 1;
'a' % 'A' = 32;
注意:求余(取模)时双目必须是整型,且结果的符号于被求余数的符号一致。
且0 % 任何数都为0,小 % 大 = 小本身。
4.1 加 +
类似于数学上的加法
4.2 减 -
类似于数学上的减法
5.1 左移 <<
用于将一个表达式的值向左移动指定的位数。其中左移一位相当于乘以2,左移两位相当于乘以4,以此类推。
左移正数:
int a = 1;
printf("%d\n", (a << 2)); //输出 8
左移负数:
int a = -1;
printf("%d\n", (a << 2)); //输出 -8
/*将变量a(一个负数)的值左移两位,在二进制表示中,负数以补码形式存储,即左移一个负数实际上是在移动其绝对值的二进制表示,并在高位插入0。所以输出结果为-8。*/
左移超过int位数的值:
int main() {
int a = 1;
printf("%d\n", (a << 33)); //输出 0
/*将变量a的值左移33位。在32位系统中,int类型的值只能存储在32位中,所以,左移超过这个数的位数实际上是没有意义的,输出结果为0。*/
左移时溢出:
unsigned int a = 1073741823; //二进制表示为 01111111111111111111111111111111
printf("%u\n", (a << 1)); //输出 2147483644
/*将一个非常大的数左移一位。由于是无符号整数,左移一位会在低位插入0,并且自动舍弃最高位的1。所以,输出结果为2147483644。*/
逻辑左移是将一个数的二进制表示形式向左移动指定的位数,在右边补零。逻辑左移不会改变数字的符号位。例如:
unsigned int num = 1; //二进制表示为 00000001
int shift = 2; //左移2位// 逻辑左移2位
num = num << shift; //输出结果,二进制表示为 00000100
printf("%u\n", num);
/*定义一个无符号整数num,初始值为1(二进制表示为00000001)。定义另一个整数shift,表示要左移的位数,这里是2。然后使用<<运算符对num进行逻辑左移操作,最后输出移位后的结果。*/
算数左移和逻辑左移在效果上是一样的:它们都将数值的二进制表示向左移动指定的位数,并且在右侧用零来填充。例如:
int num = 4; //二进制表示为 0000 0100 (对于大多数系统,int是32位或64位,但这里只显示最低有效位)
int shift = 2; //算数左移2位
num = num << shift; //输出结果,二进制表示为 0001 0000 (十进制为16)
printf("%d\n", num);
/*定义一个有符号整数num,初始值为4(二进制表示为0000 0100),将它左移2位,得到的结果是16(二进制表示为0001 0000)。*/
5.2 右移 >>
与左移相对,同样存在右移的操作。
逻辑右移:符号位不变,空出的位以0填充。
算术右移:符号位不变,空出的位以符号位的值填充。
int a = -1; //二进制表示为 1111 1111
int b = a >> 1; //逻辑右移1位,结果为 011111111 (十进制为 127)
int c = a >> 16; //逻辑右移16位,结果为 11111111111111111111111111111111 (十进制为 2147483647)
int d = -2; //二进制表示为 1111 1110
int e = d >> 1; //算术右移1位,结果为 101111111 (十进制为 126),符号位保持不变
printf("b = %d, c = %d, e = %d\n", b, c, e);
/*定义两个有符号整数a和d,它们的二进制表示中都包含一个1。使用逻辑右移运算符(>>)对a进行操作,使用算术右移运算符(>>)对d进行操作。可以看到当逻辑右移时,空出的位以0填充;当算术右移时,空出的位依然保持原有的符号位的值。*/
6.1 大于 >
比较两个值的大小,如果左侧的值大于右侧的值,结果为真(!0),否则结果为假(0)。
6.2 大于等于 >=
比较两个值的大小,如果左侧的值大于或等于右侧的值,结果为真(!0),否则结果为假(0)。
6.3 小于 <
比较两个值的大小,如果左侧的值小于右侧的值,结果为真(!0),否则结果为假(0)。
6.4 小于等于 <=
比较两个值的大小,如果左侧的值小于或等于右侧的值,结果为真(!0),否则结果为假(0)。
7.1 等于 ==
用于比较两个操作数是否相等,如果相等则返回1(真),如果不相等则返回0(假)。
注意:
在C语言中 = 是赋值, == 是判断左右两边是否相等。
7.2 不等于 !=
用于比较两个操作数是否不相等,如果不相等则返回1(真),如果相等则返回0(假)。
在C语言中,真一般表示为1或!0,假一般表示为0。
8.1 按位与 &
用于对两个整数执行按位与操作,即比较两个整数的二进制表示,并对每一位执行逻辑与操作。如果两个整数在某个位置上的二进制位都是1,则结果在该位置上的二进制位也是1;否则,结果是0。
9.1 按位异或 ^
用于比较两个二进制数值的不同位。当两个对应的二进制位相异时结果为1,相同时结果为0。
10.1 按位或 |
用于两个整数对应的二进制位执行或操作。如果其中一个数的某一位为1,或者两个数的相应位都为1,那么结果的对应位就为1。否则,结果的对应位就为0。
11.1 逻辑与 &&
当两个操作数中有一个为假(0)时,其结果为假。如果两个操作数都为真(!0),则结果为真。
即:
真 && 真 结果为真
假 && 假 结果为假
真 && 假 结果为假
假 && 真 结果为假
12.1 逻辑或 ||
当两个操作数中有一个为真(!0)时,其结果为真。如果两个操作数都为假(0),则结果为假。
即:
真 || 真 结果为真
假 || 假 结果为假
真 || 假 结果为真
假 || 真 结果为真
特别注意的是,逻辑与和逻辑或存在短路现象!且短路机制凌驾于所有优先级之上!
在 x && y 运算中,当 x 为假时, y 将不再运算。
在 x || y 运算中,当 x 为真时, y 将不再运算。
例如:
int a = 2,b = 1,c = 3,d = 4,t = 9;
a < b && (t = c > d); //假。后面短路不再计算,t值为9
(a > b++) || (t = c++ > --d); //真。结合性自左向右,前面结果为真,后面短路不再计算,b值为2.其他值不变
(a > b++) && (t = c++ > --d); //假。后面结果为真假,c值为2,d值为3,t值为0
13.1 条件运算符 ? :
条件运算符的一般形式为:表达式1?表达式2:表达式3
这里,表达式1是条件表达式,表达式2和表达式3是两个表达式。条件运算符的执行流程是:先计算条件表达式1的值,如果表达式1的值为真,则执行并返回表达式2的值,如果表达式1的值为假,则执行并返回表达式3的值。
14.1 赋值运算符 =
用于将右侧的表达式的值赋给左侧的操作数。
初学阶段需要掌握以下八类:
基本赋值:
int a = 10; //将10赋值给整型变量a
double b = 20.5; //将20.5赋值给双精度浮点型变量b
char c = 'c'; //将字符'c'赋值给字符变量c
将一个变量的值赋给另一个变量:
int x = 10;
int y = x; //将变量x的值赋给变量y
复合赋值运算符:
int a = 10;
a += 5; //等价于a = a + 5,a的值变为15
a -= 3; //等价于a = a - 3,a的值变为12
a *= 2; //等价于a = a * 2,a的值变为24
a /= 2; //等价于a = a / 2,a的值变为12
a %= 3; //等价于a = a % 3,a的值变为0
解引用赋值:
int array[5] = {1, 2, 3, 4, 5};
int *ptr = array;
*(ptr + 1) = 10; //将10赋值给ptr指向的地址的下一个元素,即array[1]
结构体赋值:
struct Student
{
char name[20];
int age;
};
struct Student stu1 = {"Tom", 18};
struct Student stu2;
stu2 = stu1; //将结构体stu1的值赋给结构体stu2
枚举类型赋值:
enum Day {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
enum Day today;
today = Mon; //将枚举类型中的Mon赋值给枚举变量today
指针赋值:
int x = 10;
int *ptr;
ptr = &x; //将变量x的地址赋值给指针ptr
返回值赋值:
int func(int a)
{
return a * 2;
}
int result;
result = func(5); //将函数func返回的值赋值给变量result
14.2 除后赋值 /=
复合赋值的一种,将左操作数除以右操作数,并将结果赋值给左操作数。
14.3 乘后赋值 *=
复合赋值的一种,将左操作数乘以右操作数,并将结果赋值给左操作数。
14.4 取模后赋值 %=
复合赋值的一种,将左操作数对右操作数除求余(取模)后,再将结果赋值给左操作数。
14.5 加后赋值 +=
复合赋值的一种,将左操作数加上右操作数,并将结果赋值给左操作数。
14.6 减后赋值 -=
复合赋值的一种,将左操作数减去右操作数,并将结果赋值给左操作数。
14.7 左移后赋值 <<=
复合赋值的一种,变量值根据表达式值所规定的位数进行左移,此变量的值不变。
14.8 右移后赋值 >>=
复合赋值的一种,变量值根据表达式值所规定的位数进行右移,此变量的值不变。
14.9 按位与后赋值 &=
复合赋值的一种,将其左操作数和右操作数按位与,然后将结果赋值给左操作数。
14.10 按位异或后赋值 ^=
复合赋值的一种,将其左操作数和右操作数按位异或,然后将结果赋值给左操作数。
14.11 按位或后赋值 |=
复合赋值的一种,将其左操作数和右操作数按位或,然后将结果赋值给左操作数。
15.1 逗号运算符 ,
将两个或多个表达式连接起来。使用逗号运算符时,从左到右计算每个表达式,但整个表达式的值取最右边的表达式的值。
《衡庐浅析·C语言程序设计·第二章·运算符及其优先级关系》部分到这里就结束了,下一个章节我们将会学习C语言中三种基本结构的相关知识,正式迈入C语言程序设计的大门,敬请期待,也欢迎大家在评论区进行互动!