操作符详解
在C语言中,我们会用到许多操作符,例如常见的 + 、 - 、 * 、/ , 等等,当然这些只是冰山一角,那么再C语言中还有哪些操作符呢?我们来探讨一下。
一、操作符的分类
首先我们来看一看C语言中都有哪些操作符,并把他们分类。
• 算术操作符: + 、- 、* 、/ 、%
• 移位操作符: << >>
• 位操作符: & | ^ ~
• 赋值操作符: = 、+= 、 -= 、 = 、 /= 、%= 、<<= 、>>= 、&= 、|= 、^=
• 单目操作符: !、++、–、&、、+、-、~ 、sizeof、(类型)
• 关系操作符: > 、>= 、< 、<= 、 == 、 !=
• 逻辑操作符: && 、||
• 条件操作符: ? :
• 逗号表达式: ,
• 下标引⽤: []
• 函数调⽤: ()
二、原码、反码、补码
我们在学习操作符之前首先要学习整数在内存中的存储,有三种形式:原码、反码、补码。具体可以通过下面这篇博客学习:
三、移位操作符
首先我们来学习一下移位操作符:<< 、 >>。
左移操作符:<<
右移操作符:>>
注意:移位操作符的操作数只能是整数。
3.1 左移操作符:<<
规则:左边舍去,右边补0。
举个例子:
int main()
{
int a = 10;
int b = a << 1;
printf("a = %d, b = %d", a, b);
return 0;
}
把10赋值给a,
a的二进制补码就是原码:
00000000 00000000 00000000 00001010
b等于a左移一位的值:左边的0舍去,在右边补上0,即:
00000000 00000000 00000000 00010100
所以b就是20。
结果:
从中不难发现,左移一位后,二进制补码中的每一位都相当于扩大了2倍,结果最后也扩大了2倍,所以可以得出结论:
数字左移几位赋值,就扩大2的几次方倍。
3.2 右移操作符:>>
右移操作符分为两种:逻辑右移和算术右移。
1.逻辑右移:左边补0,右边舍去
2.算术右移:左边补符号位,右边舍去
在写代码时,具体是那种右移具体取决于编译器,在VS上是算术右移。
int main()
{
int a = 10;
int b = a >> 1;
printf("a = %d, b = %d", a, b);
return 0;
}
整数原码补码相同,补符号位0。
结果:
当是负数时:
int main()
{
int a = -10;
int b = a >> 1;
printf("a = %d, b = %d", a, b);
return 0;
}
10000000 00000000 00000000 00001010//a的原码
11111111 11111111 11111111 11110110//a的补码
11111111 11111111 11111111 111111011//对a右移一位(此时是补码)
10000000 00000000 00000000 000000101//原码 -5
结果:
我们也可以得出结论:对于算数右移,数字右移几位赋值,就是除以2的几次方。
注意:对于移位操作符不能移动负数位。
int n = 10;
n >> -1
这种写法是未定义的,是错误的。
四、位操作符
位操作符有四个:
& //按位与
| //按位或
^ //按位异或
~ //按位取反
注意:
1.位操作符的操作数也必须是整数。
2.位操作符操作的是整数的二进制的补码。
4.1 按位与:&
按位与是对两个整数进行操作,例如:6 & 8。
计算规则:两个二进制数都是1才为1,有一个是0就是0。
简记:(全1为1,有0为0)
int main()
{
int a = 4;
int b = -7;
int c = a & b;
printf("%d", c);
return 0;
}
00000000 00000000 00000000 00000100//4的补码
11111111 11111111 11111111 11111001//-7的补码
//根据&的计算规则:有0为0
00000000 00000000 00000000 00000000//4 & -7的补码 -- 0
0的补码与原码相同,所以打印结果为0。
结果:
4.2按位或:|
按位或也是对两个整数计算,例如:6 | 8。与按位与相反:
计算规则:两个二进制数都是0才为0,有一个是1就是1。
简记:(全0为0,有1为1)
int main()
{
int a = 4;
int b = -7;
int c = a | b;
printf("%d", c);
return 0;
}
00000000 00000000 00000000 00000100//4的补码
11111111 11111111 11111111 11111001//-7的补码
//根据|的计算规则:有1为1
11111111 11111111 11111111 11111101//4 | -7的补码
10000000 00000000 00000000 00000011//4 | -7的原码 -- -3
结果:
4.3按位异或:^
按位异或的操作数也是两个,例如:6 ^ 8。
计算规则:两个二进制数相同则为0,不同则为1。
简记:(同0异1)
int main()
{
int a = 4;
int b = -7;
int c = a ^ b;
printf("%d", c);
return 0;
}
00000000 00000000 00000000 00000100//4的补码
11111111 11111111 11111111 11111001//-7的补码
//根据^的计算规则:同0异1
11111111 11111111 11111111 11111101//4 | -7的补码
10000000 00000000 00000000 00000011//4 | -7的原码 -- -3
结果:
4.4按位取反:~
按位取反的操作数只有一个整数,例如:~0。
计算规则:将数字的二进制补码中的0改为1,1改为0。
int main()
{
int a = 0;
printf("%d", ~a);
return 0;
}
0的补码与原码相同
00000000 00000000 00000000 00000000//0的补码
//根据~的计算规则,0改为1,1改为0
11111111 11111111 11111111 11111111//~0的补码
10000000 00000000 00000000 00000001//~0的原码 -- -1
结果:
练习
那么这些操作符有什么用呢?下面我们就通过一些练习来看一下。
练习1:不能创建临时变量(第三个变量),实现两个整数的交换
通常我们交换两个数所用的方法就是创建一个中间变量,对两个变量进行互换:
int temp = 0;
temp = a;
a = b;
b = temp;
如果不能用临时变量如何交换呢?
方法一:
int main()
{
int a = 6;
int b = 8;
printf("交换前:a = %d, b = %d\n", a, b);
a = a + b;//把a+b赋值给a,此时的a的值是6+8=14
b = a - b;//b = a - b = a + b - b = a,b=14-8=6,把原本的a赋值给b
a = a - b;//此时的a还是14,a=14-6=8,把原本的b赋值给a,完成交换
printf("交换后:a = %d, b = %d", a, b);
return 0;
}
但是这种方法可能会有缺点:当a和b都是一个非常大的数字的时候,两者再相加的数字可能比int类型的a所能表示的最大值还要大,就会造成溢出的情况。
方法二:
既然我们学习了操作符,我们就用操作符的方法来试着解决。
对于按位异或^,同0异1。我们可以知道一个数与自己异或就是0,任何数与0异或还是它本身。
a ^ a = 0
a ^ 0 = 0
而且异或^ 是支持交换律的,例如,3 ^ 3 ^ 5 = 3 ^ 5 ^ 3 = 5。
3 ^ 3 = 0, 0 ^ 5 = 5
00000011 – 3的补码
00000101 – 5的补码
00000110 – 3 ^ 5的补码
00000110 – 3 ^ 5的补码
00000011 – 3的补码
00000101 – 3 ^ 5 ^ 3的补码
那么我们根据一个数与自己异或就是0,任何数与0异或还是它本身这个条件来思考,当b ^ b = 0,0 ^ a = a。所以b ^ b ^ a = a,同理a ^ a ^ b = b。
int main()
{
int a = 6;
int b = 8;
printf("交换前:a = %d, b = %d\n", a, b);
a = a ^ b;
b = a ^ b;//b = a ^ b ^ b = a, 把a赋值给了b
a = a ^ b;//a = a ^ b ^ a = a ^ a ^ b = b, 完成交换
printf("交换后:a = %d, b = %d", a, b);
return 0;
}
结果:
练习2:编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数
在求二进制中1的个数之前我们思考一下,如果给一个十进制的数如何求它中1的个数:
我们可以通过多次%10 和 /10的方法来得到一个整数的每一位,对得到的数字进行判断就能求出1的个数
例如12211:
如此一来,我们就可以照猫画虎,十进制我们 %10 和 /10,所以在二进制中我们可以%2 和 / 2:
int count_one_of_bit(int n)
{
int count = 0;
while (n)
{
if (n % 2 == 1)
count++;
n /= 2;
}
return count;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = count_one_of_bit(n);
printf("%d", ret);
return 0;
}
当我们输入一个整数,例如11,二进制是0000 1011,有三个1:
但是当输入一个负数的时候,就会发现结果是错误的:
因为-1进入循环后,-1 / 2就是0,就结束了循环。
我们知道负数在内存中是以补码的形式存放的,所以如果我们让计算机认为他是原码不就行了,所以我们想到了unsigned(无符号),用这个符号修饰不就可以让计算机认为传入的数据放在内存中的都是原码咯(都看作整数):
方法一:
int count_one_of_bit(unsigned int n)
{
int count = 0;
while (n)
{
if (n % 2 == 1)
count++;
n /= 2;
}
return count;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = count_one_of_bit(n);
printf("%d", ret);
return 0;
}
-1的补码是11111111 11111111 11111111 11111111 总共32个1:
方法2:
今天我们学习了操作符,我们尝试一下用操作符的方法来解决一下:
我们可以用&(全1为1,有0为0)操作符,如果二进制中的一位数是1,那么与1&就是1,如果二进制中的一位数是0,那么与1&就是0;
所以我们可以让一个数的二进制中的每一位依次与1&,所得结果为1,则数量+1。
如何才能让1依次移动呢,于是我们想到了左移操作符<<,每完成一位的&,就让1左移一位,因为是整数类型,所以利用for循环移动32bit位。
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int count = 0;
for (i = 0; i < 32; i++)
{
if (n & (1 << i))
count++;
}
printf("%d", count);
return 0;
}
因为这种方法每一位都要循环,要循环32次,计算量有点大,所以有待优化。
方法三:
&是全1为1,有0为0;
那么如果一个数 & 这个数-1,n &(n-1)就会把这个数(n)二进制最后面的1给消除变为0,所以一直进行n = n &(n-1)的运算,就会把一个数变为0;
执行几次运算就说明数字的二级制中有几个1,所以用while循环;
例如15,二进制是1111(前面的0省略不写了)
int main()
{
int n = 0;
scanf("%d", &n);
int count = 0;
while (n)
{
n = n & (n - 1);
count++;
}
printf("%d", count);
return 0;
}
练习3:⼆进制位置0或者置1
编写代码将13⼆进制序列的第5位修改为1,然后再改回0
00000000 00000000 00000000 00001101//13的补码
00000000 00000000 00000000 00011101//修改为1
00000000 00000000 00000000 00001101//改回0
- 如果仅仅让13的二进制的第五位修改为1,所以可以在第五位与1进行按位或( | )(全0为0,有1为1)0 | 1 = 1;
- 剩下的二进制位不能变,所以剩下的位全是与0按位或(原本是0,与0 | 还是0;原本是1,与0 | 还是1不变)
- 但是1怎么跑到第五位呢?
所以就又用到了左移操作符<<:(1 << 4),4是由5 - 1得到的,5是题目给的。
00000000 00000000 00000000 00010000 // 1<< 4的补码
int n = 13;
n = n | (1 << 4);
printf("%d", n);
改成了1后如何在改回去呢?
- 我们知道1&0 = 0,所以我们考虑用按位与(&),让第五位与0进行按位与;
- 但是剩下的二进制也要不能变啊,也不能与0按位与,不然就全变为0了,所以剩下的二进制位与1进行按位与(原本是0,与0&还是0;原本是1,与1&还是1不变)
- 但是这个数又怎么得到呢?
11111111 11111111 11111111 11101111
我们发现这个数刚好与(1 << 4)的二进制补码是相反的,所以直接用 ~(按位取反),让n与 0 ~ (1 << 4)按位与(&)。
int main()
{
int n = 13;
n = n | (1 << 4);
printf("%d\n", n);
n = n & ~(1 << 4);
printf("%d", n);
return 0;
}
结果:
五、单目操作符
单目操作符包括:
!、++、–、&、* 、+、-、~ 、sizeof、(类型)
单目操作符只有一个操作数,所以叫做单目操作符,在单目操作符中有& 和 * 没有学过,这2个操作符与指针相关,所以我们会在学习指针的时候再学习。
其他的单目操作符相信大家都非常了解,就不在过多介绍。
六、条件操作符(三目操作符): ? :
条件操作符也叫三⽬操作符,需要三个操作数,形式如下:
exp1 ? exp2 : exp3
条件操作符的计算逻辑是:如果 exp1 为真, exp2 计算,计算的结果是整个表达式的结果;如果
exp1 为假, exp3 计算,计算的结果是整个表达式的结果。
例:
int main()
{
int a = 0;
int b = 0;
scanf("%d", &a);
b = a>5 ? 3:-3;
printf("%d\n", b);
return 0;
}
a > 5 在 ?的左边,是第一个表达式,
当输入的a的值小于等于5时,表达式结果为假;
当输入的a的值大于5时,表达式结果为真;
;
如果为假就执行 :右边的表达式(第三个表达式):- 3,所以b = -3;
如果为真就执行 :左边的表达式(第二个表达式): 3,所以b = 3;
结果:
七、逗号表达式: ,
逗号表达式就是有逗号隔开的多个表达式,形如:
exp1, exp2, exp3, …expN
其中每个表达式都会从左向右依次执行,但是整个表达式的结果是最后一个表达式的结果。
例如:
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("%d", c);
return 0;
}
第一个表达式:a > b 为假,就是0
第二个表达式:a = b + 10, a = 12,结果就是12
第三个表达式:a,结果就是12
第四个表达式:b = a + 1 = 13,结果就是13
第四个表达式是最后一个表达式,所以结果就是13,c = 13
结果:
另:
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
//...
a = get_val();
count_val(a);
}
当我们看到这样的一段代码时,会发现在业务处理部分会有些冗余,既然逗号表达式中的每一个表达式都会执行,那我们就可以把它放在while循环的判断条件里:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
八、下标引用操作符、函数调用操作符
8.1 下标引用操作符:[ ]
[ ] 就是下标引用操作符,这个我们通常在数组中见到:
int arr[10];//创建数组
arr[9] = 10;//实⽤下标引⽤操作符。
//[ ]的两个操作数是arr和9
8.2 函数调用操作符:( )
()就是函数调用操作符,操作数至少是一个,就是函数名。操作数是多个的就是函数名和函数的所有参数。
一个操作数:
void test()
{
printf("hello world");
}
int main()
{
test();//操作数只有test函数名
return 0;
}
多个操作数:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 6;
int b = 8;
int c = Add(6, 8);//操作数有函数名Add,参数6,8
printf("%d", c);
return 0;
}
九、结构体成员访问操作符
结构体成员访问操作符后面我会放在与结构体的知识一起讲解
十、操作符的属性:优先级、结合性
当我们使用多个操作符的时候,哪一步先算,哪一步后算就是一个值得我们关心的问题。
C语⾔的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
10.1 优先级
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。
注:相邻操作符优先级高的先执行,优先级低的后执行。
例如:
int a = 3 + 4 * 5
上面示例中,表达式 3 + 4 * 5 ⾥⾯既有加法运算符( + ),⼜有乘法运算符( * )。由于乘法
的优先级⾼于加法,所以会先计算 4 * 5 ,⽽不是先计算 3 + 4 。
运算符的优先级很多,我为大家总结了下面一些常见的运算符的优先级顺序(优先级从高到低)
• 圆括号( () )
• ⾃增运算符( ++ ),⾃减运算符( – )
• 单⽬运算符( + 和 - )
• 乘法( * ),除法( / )
• 加法( + ),减法( - )
• 关系运算符( < 、 > 等)
• 赋值运算符( = )
由于圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级。
10.2 结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符( = )
例如:
int a = 5 * 6 / 2
上⾯示例中, * 和 / 的优先级相同,它们都是左结合运算符,所以从左到右执⾏,先计算 5 * 6 ,
再计算 6 / 2 。
如果大家想了解更多操作符的优先级和结合性,可以参考C语言官网的操作符优先级和结合性:
https://zh.cppreference.com/w/c/language/operator_precedence
十一、表达式求值
11.1 截断
在C语言中我们会发现存在把一个类型的数据存放在存储空间小于这个数据的类型的变量中,这时就会发生截断。
截断规则:保留低位字节
例如:
char a = 5;
5是一个int类型的整数,占4个字节,放在了一个字节大小的char类型的变量a中,放不下,就会发生截断。
00000000 00000000 00000000 00000101 // 5的补码
保留低位一个字节放在a中:00000101
所以此时a的二进制补码就是00000101
11.2 整型提升
C语言中的整型算术运算总是以默认整型的精度来计算的。
为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型(int),这种转换称为整型提升
注:char 是字符类型,存储的是ASCII码值(是整数),所以也属于整型家族。
例如:
char a = 5;
char b = 125;
char c = a + b;
a和b都是char类型的,所以在计算a + b时要先把a和b转换成int类型的整型在进行计算。
那么为什么要整型提升呢?
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀
般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。
因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓
度。
通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中
可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为
int或unsigned int,然后才能送⼊CPU去执⾏运算
那么如何进行整型提升呢?
1.有符号整数,在前面高位补符号位(正数补0,负数补1)
2.无符号整数,直接在前面高位补0
int main()
{
char a = 5;
char b = 125;
char c = a + b;
printf("%d", c);
return 0;
}
5是整型,所以先发生截断放在char类型的a中,a的补码是00000101。
同理125是整型,发生截断,b的补码是01111101。
c = a + b
00000101//a的补码
01111101//b的补码
10000010//c的补码
是以%d(有符号整型)的形式打印,所以要对c整型提升,c的补码是10000010,补符号位1,就是:
11111111 11111111 11111111 10000010 // 补码
10000000 00000000 00000000 01111110 // 原码 -- -126
结果:
11.3 算数转换
如果某个操作符的各个操作数属于不同类型,那么就需要将其中一个操作数转换为另外一个操作数的类型。
下面就是转换优先规则(向上转换):
long double
double
float
unsigned long int
long int
unsigned int
int
例如:一个int类型的数据与一个float类型的数据求和,int类型的数据就会转换成float类型。
十二、问题表达式分析
我们学习了操作符的优先级和结合性后,难道就能给出一个表达式就能很明确的求出唯一的值了吗?我们根据以下例子来探讨一下:
例1:
a * b + c * d + e * f
在这个表达式计算时,* 的优先级比 + 高,所以就会有人认为先算a * b,c * d, e * f,然后在把三个计算结果相加。其实不一定
因为优先级是对于相邻两个操作符而言的,第三个 * 与第一个 + 并不相邻,所以无法判断这两个那个表达式哪个先算,那个后算。
又有人会有疑问,这最终结果不都是一样的吗?
如果我们把每个字母都看成一个表达式的话,并且每个表达式之间会互相影响,那么谁先执行谁后执行就会对最终结果产生影响,就会有争议。
例2:
c + - - c;
首先- -的优先级高,先算- -c,但是在计算 + 的时候左边的c是- -后的呢还是原来的c,所以无法确定,也会有争议存在。
例3:
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
这种代码就更复杂且无法确定了,在各个编译器中结果各不相同:
例4:
#include <stdio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
static 修饰了count变量,说明count变量在函数执行完后不销毁内存空间,也就是说它会保持函数执行后的值不变,比如第一次调用fun()函数,函数执行完count变成了2,第二次调用fun()的时候,count的初始值就是2,而不是1。
所以那个函数调用的顺序就会结果造成影响。所以像这样的代码也是存在问题的。
总结:
所以即使我们我们掌握了操作符的优先级和结合性,有的表达式依然可能无法得出一个确定唯一的结果,这种表达式就是有问题的,有风险的,所以这种特别复杂的表达式尽量不要写出来。
所以我们可以通过
1.使用大括号( )改变优先级确定运算顺序
2.把一个复杂的表达式拆解成多个简单的表达式
来解决。
结语:C语言操作符的讲解到这里就结束了,本文中有错误或者有待改进的地方万望大家批评和指导,感谢您的阅读,拜拜!