🌟🌟作者主页:ephemerals__
🌟🌟所属专栏:C语言
目录
前言
为了促进大家深入理解C语言并提升学习效率,本博客作者将对C语言中常用运算符的功能及其使用方法进行全面梳理,同时整合归纳这些运算符的优先级、结合性以及表达式求值规则。
一、c语言运算符的分类
c语言中,运算符可以被分为以下几类:
分类 | 举例 |
算数运算符 | + 、- 、* 、/ 、% |
位运算符 | & 、| 、^ 、<< 、>> |
赋值运算符 | = 、+= 、-= 、*= 、/= 、%= 、&= 、|= 、^= 、<<= 、>>= |
单目运算符 | ! 、++ 、-- 、& 、* 、+ 、- 、~ 、sizeof() 、()(强制类型转换) |
关系运算符 | > 、< 、== 、>= 、<= 、!= |
逻辑运算符 | && 、|| 、! |
条件操作符(三目操作符) | ? : |
逗号操作符 | , |
下标引用 | [] |
函数调用 | () |
结构成员访问操作符 | . 、-> |
其中,赋值运算符和关系运算符作为编程中的基础元素,其概念和用法相对直观易懂,因此在这里我们就不多赘述。接下来,我们按照类别逐一介绍其他运算符的功能以及它们的使用方法。
二、各运算符的功能及使用
1. 算数运算符
+
+ 在c语言有两个操作数,操作数可以是变量或者常量。例如:
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("%d\n", a + b);
printf("%d\n", a + 1);
return 0;
}
程序的运行结果是8和4。这说明得到两个数相加的结果。
-
-也有两个操作数,运算得到两个数相减的结果。像这样有两个操作数的操作符,我们将其统称为双目操作符。
*
* 和 + 、- 一样,也是双目操作符,得出两个数的积。
/
/表示除法,也是双目操作符,但是它的计算方式略有不同。如果说两个操作数均为整形,则得到的结果也为整形(实际可能算出小数,结果向下取整)。
让我们写一个程序验证:
#include <stdio.h>
int main()
{
int a = 7;
int b = 2;
float c = 0;
c = a / b;
printf("%f", c);
return 0;
}
运行结果如下:
可以看出,即便将结果赋值给一个浮点型变量,其值也是3.0。其原因就是整除运算只能得到整数,会自动丢弃小数部分。
如果想要得到小数该怎么办?其实很简单,如果其中一个操作数是属于浮点数类型,计算结果就是一个小数:
#include <stdio.h>
int main()
{
float x = 7.0 / 2;
printf("%f\n", x);
return 0;
}
得到的结果为3.5。
注意:如果两个操作数为整形,想要让一个较小的数当作被除数,则运行结果是0。
%
%操作符表示求余运算,有两个操作数,结果是它们相除后得到的余数。注意:这个操作符的操作数只能是整形,不可使用浮点型。
示例:
#include <stdio.h>
int main()
{
int a = 4;
int b = 2;
int c = 5;
int d = 3;
printf("%d\n", a % b);
printf("%d\n", c % d);
return 0;
}
结果为:
如果求余运算的操作数为负数,则结果的符号与第一个操作数一致。
2. 位运算符
c语言中的位运算符有五个,它们分别是:&(按位与)、|(按位或)、^(按位异或)、<<(左移运算符)、>>(右移运算符)。由于这些运算符涉及二进制和进制转换,所以在介绍它们之前,我们先来铺垫一下二进制的知识。
二进制和进制转换
在计算机学习当中,我们经常会听到二进制、八进制、十进制、十六进制的概念。那么它们是什么意思呢?
拿十进制举例:
十进制就是我们生活中最常用的数制,鱼缸里有16条鱼、中国有56个民族......这些数都是十进制数。
十进制数有以下特点:
1. 满10进1
2. 十进制的数字,它的每一位都由0~9的数字组成。
那么二进制数的特点也是一样的:
1. 满2进1
2. 二进制的数字,它的每一位都由0~1的数字组成。
实际上,二进制、八进制、十进制、十六进制都是数值的不同表示形式。
既然它们是数值的不同表示形式,那么就可以相互转换:
二进制转十进制
对于十进制数字123,首先我们需要知道,为什么它是123。这是一个三位数,它的每一位都是有权重的。从个位开始,每一位的权重分别是10^0,10^1,10^2。 每一位数的(数值 * 权重)累加起来,就是该数的数值。
那么对于二进制数也是一样的。例如有一个二进制数1101:
这样,我们就将一个二进制数1101转换为十进制数13。对于小数而言,它小数点之后的每一位权重就是2^-1、2^-2...
十进制转二进制
十进制转二进制的方法是对该数反复进行除2运算,得出所得余数的逆序列。例如对于十进制数123:
二进制转八进制
由于八进制数的每一位都由0~7的数字组成,而即便是这其中最大的“7”,二进制形式是“111”,也只占了三个二进制位,所以二进制转八进制时,我们将二进制数从低位到高位进行划分,每三位划分成一部分,并将每一部分换算成一个八进制位即可,最后不够三位的直接换算。例如,对于二进制数1101011:
八进制数在计算机中表示时,前面要加上“0”,也就是0153。
二进制转十六进制
与八进制的原理相同,十六进制数的每一位都由0~f 的数字组成,其中最大的“f”(十进制表示为15),它的二进制形式是“1111”,占了四个二进制位,所以我们将每四位二进制数字划分为一部分,然后分别进行换算。例如二进制数1101011:
十六进制数在计算机中表示时,前面要加上“0x”,即0x6b。
补充:由于十六进制是满16进1,所以十六进制中的a~f分别表示十进制的10~15。
源码、反码和补码
关于源码、反码和补码以及数据存储方式的相关知识,博主在之前的文章中已经进行了详细的介绍:
整数的二进制表示方法有三种:原码,反码和补码。当表示有符号的整数时,这三种表示方法都有符号位和数值位两部分,符号位占一个二进制位(最高位),数值位占其余二进制位,当符号位为0时,表示这是一个正数,为1时表示这是一个负数。
这里需要注意以下两点:
1.正整数的源码,反码和补码相同。
2.对于负整数,三者均不相同:
原码:直接将数值翻译成二进制数。
反码:符号位不变,数值位按位取反。
补码:源码+1得到补码。
整数的存储方式:一律以补码的形式存储。
在掌握了这些基础知识之后,我们将正式深入探讨位运算符。
&
“&”叫做“按位与”,它有两个操作数(这两个操作数必须是整数), 其功能是将两数对应的二进制位进行“与”运算。运算规则是:一假则假
1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0
示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 3; //00000000 00000000 00000000 00000011
int b = 10; //00000000 00000000 00000000 00001010
int z = a & b;//00000000 00000000 00000000 00000010
printf("a=%d\nb=%d\nz=%d\n", a, b, z);
return 0;
}
运行结果:
|
“|”叫做“按位或”,它有两个操作数(这两个操作数必须是整数), 其功能是将两数对应的二进制位进行“或”运算。运算规则是:一真则真
1 & 1 = 1
1 & 0 = 1
0 & 1 = 1
0 & 0 = 0
示例代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 3; //00000000 00000000 00000000 00000011
int b = 10; //00000000 00000000 00000000 00001010
int z = a | b;//00000000 00000000 00000000 00001011
printf("a=%d\nb=%d\nz=%d\n", a, b, z);
return 0;
}
运行结果:
^
“^”叫做“按位异或”,它有两个操作数(这两个操作数必须是整数), 其功能是将两数对应的二进制位进行“异或”运算。运算规则是:相同则异,不同则和
1 & 1 = 0
1 & 0 = 1
0 & 1 = 1
0 & 0 = 0
示例代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 3; //00000000 00000000 00000000 00000011
int b = 5; //00000000 00000000 00000000 00000101
int z = a ^ b;//00000000 00000000 00000000 00000110
printf("a=%d\nb=%d\nz=%d\n", a, b, z);
return 0;
}
运行结果:
~
“~”叫做“按位取反” ,有一个操作数(必须是整数),功能是将其二进制位所有的“0”变为“1”,“1”变为“0”。示例代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 3; //00000000 00000000 00000000 00000011 补码
//11111111 11111111 11111111 11111100 补码
//10000000 00000000 00000000 00000011 源码
int b = ~a;//10000000 00000000 00000000 00000011
printf("%d\n", b);
}
运行结果:
这里要注意:由于整数是以补码的形式存储的,按位取反后符号位会变成“1”,也就是负数,此时要将补码转换为源码去看。
补码转源码:符号位不变,其他位按位取反,然后+1 。
<<
“<<”叫做左移操作符,它有两个操作数(这两个操作数必须是整数),它的功能是将左操作数的二进制形式 左移 右操作数指定的位数。
左移的规则是:左边舍去,右边补零。
示例:
注意:不要试图去移动负数位,否则将导致未定义结果。
a << -1;
>>
“>>”叫做右移操作符,它有两个操作数(这两个操作数必须是整数)。
右移操作分为两种:逻辑右移和算数右移:
逻辑右移:右边舍去,左边补零
算数右移:右边舍去,左边按照符号位来补
对于无符号整数,右移操作通常被实现为逻辑右移。
对于有符号整数,情况则较为复杂。大多数现代计算机和编程语言在有符号整数的右移操作中采用算数右移;但也有一些编程语言或特定情况下可能采用逻辑右移来处理有符号整数。
示例:
注意:与左移操作符相同,如果右操作数为负数也将导致未定义行为。
3. 单目运算符
所谓单目运算符,就是指只有一个操作数的运算符。
!
“!”叫做逻辑非运算符,它用于反转一个操作数的布尔值(0为假,非0为真)。 例如:
#include <stdio.h>
int main()
{
int flag = 0;
if (!flag)//flag为假,!flag为真
{
printf("hello world\n");
}
}
运行结果:
++ 和 --
++/--是一种实现自增1/自减1的运算符,与一个变量或表达式结合即可。不过++/--结合的位置有两种:前置和后置,这两种将导致它自增/自减的时机不同。
前置++/--
先看一段代码:
int a = 10;
int b = ++a;//++的操作数是a,是放在a的前⾯的,就是前置++
printf("a=%d b=%d\n",a,b);
这段代码的输出结果:
这就是前置++的效果:首先定义a的值是10,之后先将a自增1,a此时的值是11,之后将a的值赋值给b,b的值就是11。
前置--的效果也是一样的,先对a进行自减运算,然后进行其他操作。
这样我们就得出一个结论:前置++/--:先自增/自减,后使用。
后置++/--
int a = 10;
int b = a++;//++的操作数是a,是放在a的后⾯的,就是后置++
printf("a=%d b=%d\n",a,b);
运行结果:
程序将a先赋值给b,b的值就是10,之后a再进行自增操作,就变成了11。
后置--的时机也是这样的,先进行其他操作,再自减。
所以对于后置++/--:先使用,后自增/自减。
& 和 *
这里的“&”和“ * ”不是“按位与”和“乘”,而是“取地址”和“解引用”。它们都是与指针相关的运算符,叫做指针运算符。&用于获取一个变量的地址,而*用于通过指针获取指向的值。例如:
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;//将a的地址赋值给p
*p = 20;//通过p找到a的值,将之修改为20
printf("%d\n", a);
return 0;
}
运行结果:
注意:在 int* p = &a; 语句中,“ * ”并不是解引用操作符,而是表示p是一个指针变量。
+ 和 -
这里的“+”和“-”并不是表示加法和减法,而是单目运算符“正”和“负”。 它们写在一个数之前,用于表示该数的正负(“+”号通常省略)。
sizeof()
sizeof()是c语言中的一个单目运算符,它用于求数据类型或变量所占的内存大小,单位是字节。例如:
#include <stdio.h>
int main()
{
int a = 10;
printf("%zd字节\n", sizeof(a));//求变量a所占内存大小
printf("%zd字节\n", sizeof(char));//求char类型变量所占内存大小
printf("%zd字节\n", sizeof(int[10]));//求10个元素的整形数组所占内存大小
return 0;
}
运行结果:
注意:sizeof()括号中的表达式是不会进行计算的,只会求出表达式的值所占内存的大小。
()
该操作符括号中要写明数据类型,表示“强制类型转换”。它可以将操作数转换为指定的类型。代码举例:
#include <stdio.h>
int main()
{
printf("%f\n", (float)5);//将5强制转换为float类型并输出
printf("%d\n", (int)5.5);//将5.5强制转换为int类型并输出
return 0;
}
运行结果:
4. 逻辑运算符
逻辑运算符是编程中用于执行逻辑运算的符号,通常用于布尔表达式,即返回真(非0)或假(0)的表达式。逻辑非运算符(!)之前已经提到,我们介绍其余两种。
&& 和 ||
&& (逻辑与)就是并且的意思,是一个双目操作符,当其两侧的表达式都为真时,整个表达式为真。其两侧有一个表达式为假,整个表达式为假。
||(逻辑或)就是或者的意思,是一个双目操作符,当其两侧的表达式有一个为真时,整个表达式为真。其两侧的表达式都为假,整个表达式才为假。
当我们要连续使用多个关系运算符时,可能会出现连用的情况:
3 < x < 5;
注意:这种写法是错误的,往往会出现意料之外的结果。以上这个表达式的实际意义是:先判断3是否小于x,如果为真,则前半段的值为1,否则为0。再将1或者0与5相比较,整个表达式的值是恒为真的。这不是我们预期的效果。
对于这种逻辑判断,我们就需要使用逻辑运算符。例如:
x > 3 && x < 5;
当x大于3和x小于5的条件同时满足时,整个表达式的值就是1(真),这样就能达到预期效果。
逻辑运算符的短路特性
c语言的逻辑运算符有一个特性:它先计算左边表达式,再计算右边表达式。如果说左边的表达式已经满足逻辑运算符的条件,那么右边的表达式不再计算。这个情况称之为“短路”。
例如:
a>=3 && a<=5;
如果a<3,则左边的表达式为假,整个表达式肯定为假,右边的式子就不再计算。
a<=3 || a>=5;
如果a<=3,左边的表达式就为真,整个表达式肯定为真,右边的式子就不再计算。
让我们判断以下代码的输出结果:
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
运行结果如下:
以上代码当中,由于逻辑运算符的运算特性,先对a++进行运算。由于这是后置++,先操作再自增,所以就对a的值先进行判断。a的值为0,也就是假,整个表达式的值就是假,就不会再计算后边的两个式子。此时逻辑判断完成后,a才会进行自增运算,所以a就变成了1。而b,c,d的值都不变。
5. 条件操作符
条件操作符(? :)是一种三目操作符,有三个操作数,能够实现类似if--else语句的逻辑分支。它的语法是:
exp1 ? exp2 : exp3
它的计算逻辑是:先判断exp1的真假。如果exp1为真,则执行exp2语句,否则执行exp3语句。整个表达式的值是最终执行语句的值。例如:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d", &a);
b = a > 5 ? 3 : -3;
printf("%d\n", b);
return 0;
}
以上代码中,如果输入a的值大于5,b的值就是3,否则就是-3。
6. 逗号操作符
逗号操作符(,
)是一种双目运算符,用于按顺序评估两个或多个表达式,并返回最后一个表达式的值。它的主要作用是允许将多个表达式放在同一个语句中,这样在某些情况下就可以提高代码的可读性和简洁性。代码举例:
#include <stdio.h>
int main()
{
int a = (5, 10, 20);//a的值为20,因为逗号操作符返回最后一个表达式的值
int b = 0;
b = (printf("Hello, "), printf("World\n"));//输出"Hello, World",b的值为printf("World!\n")的返回值
return 0;
}
#include <stdio.h>
int main()
{
for (int i = 0, j = 10; i < 5; i++, j--)//在for循环初始化时直接定义两个变量;每次循环结束后,可以执行两个操作(i++和j--)
{
printf("i = %d, j = %d\n", i, j);
}
return 0;
}
注意不要过度使用逗号操作符,过度使用有可能会降低代码的可读性。
7. 下标引用操作符
下标引用操作符( [ ] )就是我们访问数组元素时常用的运算符。他有两个操作数,分别是数组名和下标(下标从0开始)。 这两个操作数的顺序是可以颠倒的。
代码举例:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };//创建十个整形元素的数组
arr[9] = 10;//访问最后一个元素,修改为10
0[arr] = 1;//顺序可以颠倒
return 0;
}
注意:表达式 arr[9] 等价于 *(arr + 9) 。
8. 函数调用操作符
我们在进行函数调用时,会将函数的参数写在括号当中,这个括号就是函数调用操作符。对于一个函数,仅有函数名则表示其入口地址,而带上函数调用操作符才表示调用这个函数。
代码举例:
#include <stdio.h>
int func()
{
return 1;
}
int main()
{
printf("%p\n", func);//函数名表示函数入口地址
printf("%d\n", func());//使用函数调用操作符(),调用函数
return 0;
}
运行结果:
9. 结构成员访问操作符
结构成员访问操作符用于访问结构体变量的成员。我们在访问结构体成员时,有两种访问方式,分别是直接访问和间接访问。直接访问使用“ . ”,而间接访问使用“ -> ”。
.
“ . ”操作符用于结构体成员的直接访问。它的使用方式是:
结构体变量名.成员名
代码举例:
#include <stdio.h>
struct stu
{
int a;
int b;
};
int main()
{
struct stu s = { 1,2 };
printf("%d,%d\n", s.a, s.b);//访问结构成员
}
运行结果:
->
“ -> ”操作符用于结构体成员的间接访问。它的使用方式是:
结构体指针名->成员名
代码举例:
#include <stdio.h>
struct stu
{
int a;
int b;
};
int main()
{
struct stu s = { 1,2 };
struct stu* ps = &s;
printf("%d,%d\n", ps->a, ps->b);//访问结构成员
}
运行结果:
上述代码当中,ps->a 等价于 *(ps).a 。
三、运算符的优先级和结合性
在介绍完了这么多运算符之后,我们来讲解运算符的优先级和结合性。运算符的优先级和结合性决定了表达式求值时的计算顺序。
1. 优先级
运算符的优先级指的是:当一个表达式中有多个运算符时,首先执行哪一个运算符的计算逻辑。例如:
1 + 2 * 3;
上述表达式中,既含有运算符“+”,也含有运算符“ * ”,我们都知道,乘法的优先级要高于加法,所以先算乘法,再算加法,所以表达式的值应为7。
如果想要让其先算加法,再算乘法,则需要将“1+2”使用圆括号括起来,圆括号可以改变运算符的优先级。
2. 结合性
如果两个运算符的优先级相同,没办法确定先计算哪个,此时就要看其结合性是左结合还是右结合,再决定执行顺序。大部分的运算符都是左结合(从左到右执行),而少数运算符是右结合(从右到左执行),比如赋值运算符“ = ”。
举例:
3 * 6 / 2;
上述表达式中,乘法和除法的优先级相同,结合性都是从左到右,所以表达式会从左到右计算,先算乘法,再算除法,表达式的最终结果是9。
3. 操作符优先级与结合性汇总
运算符优先级和结合性总表如下(优先级从上到下依次递减):
运算符 | 描述 | 优先级 | 结合性 |
++、-- | 后置自增和自减 | 最高 | 从左到右 |
() | 函数调用 | ||
[ ] | 下标引用 | ||
. | 结构体成员直接访问 | ||
-> | 结构体成员间接访问 | ||
++、-- | 前置自增和自减 | 较高 | 从右到左 |
+、- | 正和负 | ||
! 、~ | 逻辑非和按位取反 | ||
() | 强制类型转换 | ||
* | 解引用 | ||
& | 取地址 | ||
sizeof() | 取内存大小 | ||
*、/、% | 乘法、除法、求余 | 中等 | 从左到右 |
+、- | 加减法 | ||
<<、>> | 左移和右移 | ||
<、<= | 小于、小于等于 | ||
>、>= | 大于、大于等于 | ||
==、!= | 等于、不等于 | ||
& | 按位与 | ||
^ | 按位异或 | ||
| | 按位或 | ||
&& | 逻辑与 | ||
|| | 逻辑或 | ||
? : | 条件操作符 | 较低 | 从右到左 |
= | 赋值操作符 | ||
+=、-= | 和赋值和差赋值 | ||
*=、/=、%= | 积赋值、商赋值、求余赋值 | ||
<<=、>>= | 左移赋值和右移赋值 | ||
&=、^=、|= | 按位与赋值、按位异或赋值、按位或赋值 | ||
, | 逗号操作符 | 最低 | 从左到右 |
四、表达式求值规则--算数转换和整形提升
最后,博主将介绍在表达式求值过程中需要遵循的两个重要规则:算术转换和整型提升。
1. 算数转换
如果某个操作符的各个操作数属于不同类型,那么除非一个操作数的类型转换为与另一个操作数相同,否则操作就无法进行。
下面的表格列举了常见的算术转换规则体系:
类型(从上到下排名) |
long double |
double |
float |
unsigned long |
long |
unsigned |
int |
如果某个操作数的类型在该表中排名靠后,那么它就要自动转换为靠前的操作数的类型,然后再执行运算。
2. 整形提升
c语言当中,表达式的计算总是至少以整形类型的精度来进行的。 为了获得这个精度,表达式中的字符和短整型在使用之前会被转换为普通整形,这种转换就叫做整形提升。举例:
char a = 1, b = 2, c = 3;
a = b + c;
在 a = b + c 这个表达式进行计算的过程中,由于b和c都是字符型变量,它们首先会被提升为普通整形,然后执行加法运算,运算的结果将被截断为字符型,然后存储到a当中。
整形提升的规则:
1. 对于有符号数,按照符号位提升,二进制左边补至整形长度。
2. 对于无符号数,二进制左边补0,补至整形长度。
解释一下上图当中 unsigned char c = -1 语句: -1是整形,它的二进制补码表示是32个“1”。当将其赋值给unsigned char类型的变量c时,会发生截断,前24个“1”都被舍去,只留下8个“1”,所以其二进制值就是“11111111”,十进制值就是255。
知识补充:char 和 unsigned char 类型数据范围图解:
整形提升的原因
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度⼀般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为 int 或 unsigned int ,然后才能送入CPU去执行运算。
总结
今天博主根大家分享了c语言中各种操作符的功能、使用方法以及优先级和结合性,并且与大家深入探讨了表达式求值的两个重要规则--算数转换和整形提升。学习这些知识对我们的C语言和C++学习都有着极大的帮助。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤