操作符分类

原码、反码、补码

整数的2进制表示方法有三种,即原码、反码、补码。

有符号的三种表示方法均有符号位和数值位两部分组成。

首位被当作符号位,剩下的均为数值为。

符号位用0表示正,用1表示负。

正整数的原码、反码、补码都相同。

负整数的三种表示方法各不相同。

  • 原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
  • 反码:原码的数值位取反,符号位不变。(0变1,1变变0)
  • 补码:反码 + 1。

例如:
对于整型来说,其实存放在内存中的就是他的补码。

1
2
3
4
5
6
int a = -10;
// 原码就是 a 的二进制表示
// 一个整型数在内存中占 4 个字节(32 位),一个字节由8个 bit 位组成,所以 a 的二进制表示为:
// 10000000 00000000 00000000 00001010 就是原码
// 11111111 11111111 11111111 11110101 就是 a 的反码
// 11111111 11111111 11111111 11110110 就是 a 的补码

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将

符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法

器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路

移位操作符


全程操作的都是数据的补码。
##3.1 左移位操作符
移位规则:左边抛弃,右边补0

1
2
3
4
5
int a = 10;
// 00000000 00000000 00000000 00001010
int num = a << 1; // 左移一位,相当于乘以 2
// 00000000 00000000 00000000 00010100
printf("%d\n",num);

1
2
3
4
5
6
7
8
int a = -10;
// 10000000 00000000 00000000 00001010 就是 a 的原码
// 11111111 11111111 11111111 11110101 就是 a 的反码
// 11111111 11111111 11111111 11110110 就是 a 的补码
int num = a << 1;
// 11111111 11111111 11111111 11101100 就是 a 左移一位后的结果,这是补码
// 11111111 11111111 11111111 11101011 就是 a 左移一位后的反码
// 10000000 00000000 00000000 00010100 就是 a 左移一位后的原码

右移位操作符

右移位操作符分两种情况:逻辑右移和算术右移

逻辑移位规则:右边抛弃,左边补0

1
2
3
4
5
int a = 10;
// 00000000 00000000 00000000 00001010
int num = a >> 1; // 右移一位,相当于除以 2
// 00000000 00000000 00000000 00000101
printf("%d\n", num);

算术移位规则:右边抛弃,左边用概该数的符号位填充

1
2
3
4
5
6
7
8
9
int a = -10;
// 10000000 00000000 00000000 00001010 就是 a 的原码
// 11111111 11111111 11111111 11110101 就是 a 的反码
// 11111111 11111111 11111111 11110110 就是 a 的补码
int num = a >> 1;
// 11111111 11111111 11111111 11111011 就是 a 右移一位后的结果
// 11111111 11111111 11111111 11111010 就是 a 右移一位后的反码
// 10000000 00000000 00000000 00000101 就是 a 右移一位后的补码
printf("%d\n", num);


对于移位操作符,不能移动负数位,这个标准是未定义的。

位操作符

1
2
3
4
& // 按位与
| // 按位或
^ // 按位异或
! // 按位取反

注:他们操作的必须是整数。

& 是双目操作符,对两个数的补码进行操作,都为1是1,有0就是0

| 是双目操作符,对两个数的补码进行操作,有1就是1,都是0位0

^ 是双目操作符,对两个数的补码进行操作,位相同为0,不同为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
nt num1 = 10;
// 00000000 00000000 00000000 00001010 就是 num1 的二进制表示
int num2 = 20;
// 00000000 00000000 00000000 00010100 就是 num2 的二进制表示
printf("%d\n", num1 & num2);
// 都是1才是1,否则为0
// 00000000 00000000 00000000 00001010
// 00000000 00000000 00000000 00010100
// 00000000 00000000 00000000 00000000
printf("%d\n", num1 | num2);
// 有1为1,否则为0
// 00000000 00000000 00000000 00001010
// 00000000 00000000 00000000 00010100
// 00000000 00000000 00000000 00010110


printf("%d\n", num1 ^ num2);
// 相同为0,不同为1
// 00000000 00000000 00000000 00001010
// 00000000 00000000 00000000 00010100
// 00000000 00000000 00000000 00010110
printf("%d\n", ~ num1);
// 按位取反
// 1取0,0取1
// 00000000 00000000 00000000 00001010
// 11111111 11111111 11111111 11110101 就是 num1 的按位取反结果,这是补码
// 11111111 11111111 11111111 11110100 这是反码
// 10000000 00000000 00000000 00001011 这是原码

使用位操作符实现交换两变量的值

假设现在有一变量 x

那么 x 一定满足 x ^ x = 0, 0 ^ x = x

对于变量 a,b ,满足 (a ^ b) ^ c = a ^ (b ^ c)

即可推导出

1
2
3
4
5
6
7
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a = %d b = %d", a, b);

求一个整数存储在内存中的二进制中的 1 的个数

对于 1 来说,他与任意一个整数进行 & 运算只会出现 0 和 1 两种结果。

1
00000000 00000000 00000000 00000001 // 1在内存中的存储

那么就可以这么设计

1
2
3
4
5
6
7
8
9
10
11
12
13
// 计算一个整数的二进制表示中有多少个 1
int num = 0;
int count = 0;
scanf("%d", &num);
for(int i = 0;i < 32;i++)
{
// 1与任意一个整数进行按位与运算,如果该整数的二进制表示中对应位是 1,那么结果就是 1,否则就是 0
if (1 & num >> i)
{
count++; // 说明 num 的二进制表示中第 i 位是 1,那么就把 count 加 1
}
}
printf("有 %d 个 1\n", count);

还有一种效率更高的算法,不需要循环32次

1
2
3
4
5
6
7
8
int num = -1;
int count = 0;
while (num)
{
count++;
num = num & (num - 1); // 这个操作会把 num 的二进制表示中最后一个 1 变成 0
}
printf("有 %d 个 1\n", count);

这个算法利用的就是每次 num & num - 1 会将这个数放在末尾的1消掉。

直到这个数变成0为止,统计这个过程执行了多少次,就知道 num 的二进制表示中有多少个 1 了。


注:只有在补码存储下该公式成立

逗号表达式

就是指用逗号隔开的多个表达式。

从左向右依次执行,整个表达式的结果是最后一个表达式的结果。

1
2
3
4
int a = 3;
int b = 4;
int c = (a + b, a * b, a - b);
printf("%d\n", c);

这儿有一串代码

1
2
3
4
5
6
7
8
a = test1();
test2();
while (a > 0)
{
// 业务处理

test2();
}

那么根据逗号表达式的计算,还可以这么写

1
2
3
4
5
while (a = test1(), test2(), a > 0)
{
// 业务处理
test2();
}

标访问[]和函数调用()

这俩没啥好说的

1
2
3
4
5
6
7
8
9
int arr[10];//
创建数组

arr[9] = 10;//
使用下标引用操作符。

[ ]
的两个操作数是arr和9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void test1()
{
printf("hehe\n");
}

void test2(const char *str)
{
printf("%s\n", str);
}

int main()
{
test1();
//
这里的()就是作为函数调用操作符。

test2("hello world.");//
这里的()就是函数调用操作符。

return 0;
}

结构体访问操作符

这一块儿放到后面结构体一块儿说。

表达式求值

整型提升

C语言中整型算术运算总是至少以缺省整型类型的精度来进行的。

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整

型,这种转换称为整型提升。

1
2
3
4
char a = 3;
char b = 127;
char c = a + b;
printf("%d ", c);

这里 a 和 b 在进行相加之前,就会先提升为普通整型,再执行加法计算。

计算完成后,结果在被截断后存放进 c 中。

整型提升的规则是

  • 有符号整数提升是按照变量的数据类型的符号位来提升的

  • 无符号整数提升,高位补0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char c = a + b;
// a: 00000000 00000000 00000000 00000011
// b: 00000000 00000000 00000000 01111111
// a + b: 00000000 00000000 00000000 10000010
// 存到 c 中时,c 只有1个字节,所以只保留最后8位
// c: 10000010 这是补码


// 而 %d 输出的是有符号整数
// 所以C又会发生整型提升,c会被提升成int类型
// 适用有符号整数提升是按照变量的数据类型的符号位来提升的
// c: 11111111 11111111 11111111 10000010 这是补码
// 11111111 11111111 11111111 10000001 这是反码
// 10000000 00000000 00000000 01111110 这是原码

所以最后输出的是 -126

算术转换

如果某个操作符的各个操作数属于不同的类型,那么需将其中一个操作数的类型

转换为另一个操作数的类型。

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