椋鸟C语言笔记#14:操作符整理、优先级与结合性、整型提升与算数转换

萌新的学习笔记,写错了恳请斧正。


目录

操作符的分类

移位操作符

位操作符

一些有趣的题

整数互换

二进制1计数

二进制位置零或置一

单目操作符

逗号表达式

下标访问与函数调用

下标访问操作符

函数调用操作符

结构成员访问符

优先级与结合性

优先级

结合性

怎么知道优先级和结合性

表达式求值

整型提升

算数转换

风险表达式分析

综上


操作符的分类

  • 算术操作符:+-*/%
  • 移位操作符:<<>>
  • 位操作符:&|^~
  • 赋值操作符:=+=-=*=/=<<=>>=&=|=^=
  • 单目操作符:!++--&*+-~sizeof(类型)
  • 关系操作符:>>=<<===!=
  • 逻辑操作符:&&||
  • 条件操作符:?:
  • 逗号表达式:,
  • 下标引用符:[]
  • 函数调用符:()
  • 结构成员访问符:.->

本文部分内容需要读者掌握进制转换、码制转换、逻辑代数的知识

部分操作符前文已经讲解,不赘述

移位操作符

移位操作符,就是把一个整数对应的二进制数向左/右移一定的位数

具体操作就是:

1. 左移操作符:

  • 先将数字n转换为对应的二进制数(位模式宽度与类型长度相同
  • 比如n左移一位(n<<1)就把二进制左边舍弃一位,右边补一个零

2. 右移操作符

右移操作符分为两种:算数右移与逻辑右移

  • 逻辑右移:左边补0,右边舍弃
  • 算数右移:左边补符号位,右边舍弃(补码)
  • 究竟是逻辑右移还是算数右移取决于编译器,一般是算术右移

注意移位的位数不能是负数,如3 << -1

位操作符

位操作符有4个,操作数只能是整数:

  • &:按位与,左右两数补码对应位进行逻辑与运算
  • |:按位或,左右两数补码对应位进行逻辑或运算
  • ^:按位异或,左右两数补码对应位进行逻辑异或运算
  • ~:按位取反,右侧数补码每一位取反

一些有趣的题

整数互换

那么有一道难题:如何不创建临时变量实现两个整数的交换呢?

我们可以利用异或运算的自反性:

a = a ^ b;
b = a ^ b;
a = a ^ b;

这样b的最终结果为:a⊕b⊕b=a

而a的最终结果为:(a⊕b)⊕(a⊕b⊕b)=a⊕b⊕a=b

同样的,其实也可以巧妙利用加减法:

a = a + b;
b = a - b;
a = a - b;

这样b的最终结果为:(a+b)-b=a

而a的最终结果为:(a+b)-[(a+b)-b]=(a+b)-a=b

但是加减法求和也可能造成溢出的问题

二进制1计数

写一个程序统计一个数的二进制(补码)中有几个1

我们很容易就能写出:

#include <stdio.h>

int main()
{
    int num  = 114;
    int count= 0;//计数
    while(num)
    {
        if (num%2 == 1)
            count++;
        num /= 2;
    }
    printf("⼆进制中1的个数 = %d\n", count);
    return 0;
}

但是这段代码只使用于非负整数,如果代入负数会出错

如果想让上方代码适用于负数,可以将num进行强制类型转换:

#include <stdio.h>

int main()
{
    int num  = 114;
    unsigned rnum = (unsigned)num;
    int count= 0;//计数
    while(rnum)
    {
        if (rnum%2 == 1)
            count++;
        rnum /= 2;
    }
    printf("⼆进制中1的个数 = %d\n", count);
    return 0;
}

将一个数强制转换为无符号数时,其二进制位保持一致(原数补码当成真值看)

但是既然学了移位操作符与位操作符,我们也可以换个方式:

任何数&1的结果都只与其二进制最后一位是否为1有关,若最后一位是1则结果为1,若最后一位是0,则结果为零,利用这个性质解决问题

#include <stdio.h>

int main()
{
    int num = -1;
    int i = 0;
    int count = 0;//计数
 
    for(i=0; i<32; i++)
        if(num & (1 << i))//或者写成((num >> i) & 1)也一样
            count++;        
    printf("⼆进制中1的个数= %d\n",count);
    return 0;
}

然而我们甚至可以进一步简化,只要用到一条性质:

n&(n-1)的结果就是将n对应的二进制数从最右边开始数,把第一个1替换为0

比如11011进行一次该运算变为11010,然后变成11000,10000,00000.

这样,我们也可以写成:

#include <stdio.h>

int main()
{
	int num = -1;
	int i = 0;
	int count = 0;//计数
	while (num)
	{
		count++;
		num = num & (num - 1);
	}
	printf("二进制中1的个数= %d\n", count);
	return 0;
}

利用这条性质,我们还能很容易判断一个数是不是二的次方数:

把二进制位右边第一个一转为零后整体为零则原数字符合条件。

二进制位置零或置一

比如,如果要将a二进制序列的第x位修改为1,然后再改回0,应该怎么做?

只要将其与(1 << (x-1))做按位或运算即可置零

而将其与~(1 << (x-1))做按位与运算即可置一

#include <stdio.h>

int main()
{
    int a = 0, x = 0;
    scanf("%d%d", &a, &x);
    a |= (1<<(x-1));
    printf("a = %d\n", a);
    a &= ~(1<<(x-1));
    printf("a = %d\n", a);
    return 0;
}

单目操作符

单目操作符就是只有一个操作数的操作符

!++--&*+-~sizeof(类型)

其中只有*和&未涉及,这些内容将在指针部分介绍

注:sizeof是个操作符而不是函数哟~

逗号表达式

exp1, exp2, exp3, …expN

逗号表达式就是从左往右依次计算其中的表达式,而最终返回最后一个表达式的值

例子如下:

int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);

上方代码最终a被赋值为12,b被赋值为13,c被赋值为13

赋值表达式的返回值为赋值结果

if (b=a+1, c=2*b, c>0)

上方代码最终决定if的也只有c>0,但前面的代码可能对c产生影响

具体运用场景如下:

代码1:

a = getval();
cntval(a);
while (a > 0)
{
    ...//一顿操作
    a = getval();
    cntval(a);
}

代码2:

while (a = getval(), cntval(a), a > 0)
{
    ...//一顿操作
}

代码2利用逗号表达式明显更简洁。

逗号表达式拥有所有操作符中最低的优先级!要注意是否需要加括号!

下标访问与函数调用

下标访问操作符

就是方括号,前面是数组名,里面是索引值。数组名和索引值都是它的操作数。

具体的前面数组已经讲过,不赘述。

函数调用操作符

就是小括号,前面是函数名,里面是传参。函数名和传参都是其操作数。

(函数可以没有传参,但函数名是其必要操作数)

具体的前面函数讲过,不赘述。

结构成员访问符

结构成员访问符包括两个结构体访问操作符,这将在下一篇笔记介绍。

优先级与结合性

优先级

操作符有自己的优先级,这决定了操作符被计算的顺序

当一个表达式含有多个运算符时,优先级高的先被处理

比如1 + 2 * 3中乘号优先级高,先计算2*3,随后1+6=7

结合性

当几个操作符优先级一致时,表达式按照运算符的结合性进行运算

结合性就是指从左往右计算或者从右往左计算(同一优先级结合性必定一致

怎么知道优先级和结合性

这个链接→优先级与结合性

表达式求值

整型提升

C语言中整型算术运算总是至少以缺省整型(默认整型int)类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。

整型提升的原因:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU (general-purpose CPU) 是难以直接实现两个8比特字节直接相加运算 (虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

那么整型提升的具体过程是什么样的呢?

  • 有符号整数提升是按照变量的数据类型的符号位来提升的
  • 无符号整数提升,高位补0
char ch1 = -1;//负数
//变量ch1的二进制位(补码)中只有8个比特位:
//11111111
//若char为有符号的char(前面讲过char是否有符号取决于系统)
//提升之后的结果是:
//11111111 11111111 11111111 11111111
char ch2 = 1;//正数
//00000001
//若char为有符号的char
//提升之后的结果是:
//00000000 00000000 00000000 00000001
算数转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

  1. long double
  2. double
  3. float
  4. unsigned long
  5. long
  6. unsigned
  7. int

如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算

综合考虑整型提升与算数转换,我们会发现如果两个字符或短整型相加溢出原本的位数,将依旧可以计算。但如果将结果再赋值给短整型或字符,前面多余的部分会被截取丢弃。

风险表达式分析

一、
a * b + c * d + e * f;

这个表达式看起来很简单,但是当编译器处理时就会出现两种不同的处理方式

第一种:a * b→c * d→e * f→(a * b) + (c * d)→((a * b) + (c * d)) + (e * f)

第二种:a * b→c * d→(a * b) + (c * d)→e * f→((a * b) + (c * d)) + (e * f)

这两种看起来区别不大是不是?

那可就大错特错了。

如果abcdef不是值,而是子表达式呢?

如果这些子表达式直接相互影响呢?

那么在不同的编译器下就会出现不同的运算结果了

光看这个例子可能还觉得问题不大,那我们继续往后看。

二、
a + --a;

这看起来很简单也没什么问题是么?

但是我们只知道前驱自减运算符在加法运算符前面,但是我们并没有办法得知,+操
作符的左操作数的获取在右操作数之前还是之后求值
,这就直接产生了歧义。

有些编译器运算结果是2a-1,有一些则是2a-2

三、
int main()
{
    int i = 10;
    i = i-- - --i * ( i = -3 ) * i++ + ++i;
    printf("i = %d\n", i);
    return 0;
}

这段代码来自《C和指针》

它在不同编译器下结果天差地别:

四、
#include <sdtio.h>

int fun()
{
    static int count = 1;
    return ++count;
}

int main()
{
    int answer;
    answer = fun() - fun() * fun();
}

这段代码的结果可能是-2、-10甚至其他数,因为我们只知道乘法与加法计算的优先顺序,不确定编译器是计算了2-3*4还是4-2*3亦或是其他组合。在VS2022的x86环境下结果为-10.

五、
#include <stdio.h>

int main()
{
    int i = 1;
    int ret = (++i) + (++i) + (++i);
    printf("%d\n", ret);
    printf("%d\n", i);
    return 0;
}

这段代码linux gcc给出的ret为10,而在VS2022x86下给出12

因为这段代码中的第一个加号在执行的时候,第三个前驱自增是否执行是不确定的。

依靠操作符的优先级和结合性是无法决定第一个加号和第三个前驱自增的先后顺序。

综上

不要写太复杂的表达式,如果有,尽量用括号确定其运算逻辑以免产生歧义。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

椋鸟Starling

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值