椋鸟C语言笔记#14

操作符整理、优先级与结合性、整型提升与算数转换

Featured image

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

操作符的分类

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

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

移位操作符

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

具体操作就是:

  1. 左移操作符:

左移操作符演示

  1. 右移操作符

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

注意移位的位数不能是负数,如 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 去执行运算。

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

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-34 还是 4-23 亦或是其他组合。在 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

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

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

综上

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