一.《第15课 - 逻辑运算符分析》
二.《第16课 - 位运算符分析》
1. C语言中的位运算符
C语言中的位运算符直接对 bit 位进行操作,其效率最高。
2. 左移和右移运算符的注意点
(1)左操作数必须为整型类型即char、short、int,其中char和short被隐式转换为int后进行移位操作。其它的数据类型,如float等不能进行移位操作。
(2)C标准规定右操作数的范围为[0,31],如果右操作数不在该范围内,其行为是未定义的,不同的编译器处理方式不同。
(3)左移运算符 << 将运算数的二进制位左移
规则:高位丢弃,低位补0
(4)右移运算符 >> 将运算数的二进制位右移
规则:高位补符号位(正数补0,负数补1),低位丢弃
(5)0x1 << 2 + 3 的值是什么?是1左移两位之后的值4 + 3 = 7吗? ==> 注意,+ 的优先级大于 << 和 >> 的优先级,即0x1 << 5 等于32
(6)左移n位相当于乘以2的n次方,但效率比数学运算符高;右移n位相当于除以2的n次方,但效率比数学运算符高
【位运算符初探】
#include <stdio.h>
int main()
{
printf("%d\n", 3 << 2); // 3 << 2 ==> 11 << 2 ==> 1100,即12
printf("%d\n", 3 >> 1); // 11 >> 1,即1
printf("%d\n", -1 >> 10); // -1 >> 10 ==> 11111111 11111111 11111111 11111111 >> 10 ==> 11111111 11111111 11111111 11111111 ,即0xffffffff,仍为-1
printf("%d\n", 0x01 << 2 + 3); // 相当于0x01 << (2+3),即32
printf("%d\n", 3 << -1); // oops! 右操作数取值范围为[0,31],-1不在该范围内,不同编译器处理方式不同
// 使用gcc编译,结果为1(将左移-1位当做右移1位,3右移1位结果为1); 使用VS2010编译,结果为0(不符合C标准直接输出0); 使用bcc32编译,结果为-2147483648(int类型的最小值,提醒程序员用法错误)
return 0;
}
使用gcc编译执行上述代码
※※ 避免位运算符、逻辑运算符和数学运算符同时出现在一个表达式中,如果确实需要同时参与运算,尽量使用括号( )来表达计算次序。
3. 交换两个整型变量的值
下面再介绍一下使用位运算符交换两个变量的值。
1 #include <stdio.h>
2
3 // 使用中间变量交换
4 #define SWAP1(a, b) \
5 { \
6 int t = a; \
7 a = b; \
8 b = t; \
9 }
10
11 // 使用部分和交换
12 #define SWAP2(a, b) \
13 { \
14 a = a + b; \ // a + b 的值不能溢出
15 b = a - b; \
16 a = a - b; \
17 }
18
19 //使用异或 ^ 交换
20 #define SWAP3(a, b) \
21 { \
22 a = a ^ b; \
23 b = a ^ b; \
24 a = a ^ b; \
25 }
26
27 int main()
28 {
29 int a = 1;
30 int b = 2;
31
32
33 printf("a = %d\n", a);
34 printf("b = %d\n", b);
35
36 SWAP3(a ,b);
37
38 printf("a = %d\n", a);
39 printf("b = %d\n", b);
40
41 return 0;
42 }
swj@ubuntu:~/c_course/ch_16$ ./a.out a = 1 b = 2 a = 2 b = 1
swj@ubuntu:~/c_course/ch_16$ ./a.out
a = 1
b = 2
a = 2
b = 1
4. 位运算与逻辑运算
(1)位运算没有短路规则,每个操作数都参与运算
(2)位运算的结果为整数,而不是 0 或 1
(3)位运算的优先级高于逻辑运算的优先级
【混淆概念的判断条件】
位运算
#include <stdio.h>
int main()
{
int i = 0;
int j = 0;
int k = 0;
// 位运算没有短路规则,所有操作数都参与运算
if( ++i | ++j & ++k )
{
printf("Run here...\n");
}
printf("i = %d\n", i); // 1
printf("j = %d\n", j); // 1
printf("k = %d\n", k); // 1
return 0;
}
逻辑运算
#include <stdio.h>
int main()
{
int i = 0;
int j = 0;
int k = 0;
// 短路规则, (true && ++i) || (++j && ++k)
if( ++i || ++j && ++k )
{
printf("Run here...\n");
}
printf("i = %d\n", i); // 1
printf("j = %d\n", j); // 0
printf("k = %d\n", k); // 0
return 0;
}
三.《第17课 - ++和--操作符分析》
1. ++和--操作符对应的两条汇编指令
(1)前置++/--对应的两条汇编指令:变量自增(减)1;然后取变量值
(2)后置++/--对应的两条汇编指令:先取变量值;然后变量自增(减)1
上面两条规则很简单,那 ++ 和 -- 操作符是不是就不需要研究了呢?我们使用VS2010和gcc编译执行下面的代码,观察输出结果是否与我们预期的一致。
#include <stdio.h>
int main()
{
int i = 0;
int r = 0;
r = (i++) + (i++) + (i++);
printf("i = %d\n", i);
printf("r = %d\n", r);
r = (++i) + (++i) + (++i);
printf("i = %d\n", i);
printf("r = %d\n", r);
return 0;
}
我们的预期是:
r = (i++) + (i++) + (i++); // r = 0 + 1 + 2 = 3 i自增三次后 i = 3
r = (++i) + (++i) + (++i); // r = 4 + 5 + 6 = 15 i又自增三次后 i = 6
使用gcc编译器编译,程序执行结果如下:
使用VS2010编译器编译,程序执行结果如下:
可见两款编译器对于 r 的输出结果与我们的预期都不同,那为何会这样呢?我们从汇编代码中寻找原因。
VS2010汇编代码
分析 r = (i++) + (i++) + (i++); 在VS2010中对应的汇编代码
再分析接下来的 r = (++1) + (++i) + (++i); 在VS2010中对应的汇编代码如下
gcc对应的汇编代码
使用 objdump -d -S 对代码进行反汇编 暂时分析不好下面的汇编指令。。。。。等掌握了再把结果补上。。。。。。
r = (i++) + (i++) + (i++);
400543: 8b 55 f8 mov -0x8(%rbp),%edx
400546: 8d 42 01 lea 0x1(%rdx),%eax
400549: 89 45 f8 mov %eax,-0x8(%rbp)
40054c: 8b 45 f8 mov -0x8(%rbp),%eax
40054f: 8d 48 01 lea 0x1(%rax),%ecx
400552: 89 4d f8 mov %ecx,-0x8(%rbp)
400555: 8d 0c 02 lea (%rdx,%rax,1),%ecx
400558: 8b 45 f8 mov -0x8(%rbp),%eax
40055b: 8d 50 01 lea 0x1(%rax),%edx
40055e: 89 55 f8 mov %edx,-0x8(%rbp)
400561: 01 c8 add %ecx,%eax
400563: 89 45 fc mov %eax,-0x4(%rbp)
r = (++i) + (++i) + (++i);
40058e: 83 45 f8 01 addl $0x1,-0x8(%rbp)
400592: 83 45 f8 01 addl $0x1,-0x8(%rbp)
400596: 8b 45 f8 mov -0x8(%rbp),%eax
400599: 8d 14 00 lea (%rax,%rax,1),%edx
40059c: 83 45 f8 01 addl $0x1,-0x8(%rbp)
4005a0: 8b 45 f8 mov -0x8(%rbp),%eax
4005a3: 01 d0 add %edx,%eax
4005a5: 89 45 fc mov %eax,-0x4(%rbp)
从上面的分析结果来看,两款编译器对于 ++ 和 -- 操作符的混合运算的处理并不相同,
2. C标准对++和--运算符的规定
(1)C语言只规定了 ++ 和 -- 对应指令的相对执行次序,并没有要求两条汇编指令的执行是连续的,所以两条汇编指令中间可能会被其它指令打断!
(2)在混合运算中,++ 和 -- 的汇编指令可能被打断执行,因此 ++ 和 -- 参与混合运算结果是不确定的,就出现了前面两款编译器的差别。
※ 这也告诉我们,在实际工程开发中不要将 ++ 和 -- 参与到混合运算中!!!
这里额外看下Java中对++ 和 -- 混合运算的操作
class Test {
public static void main(String[] args) {
int i = 0;
int r = 0;
r = (i++) + (i++) + (i++);
System.out.println("i = " + i);
System.out.println("i = " + i);
r = (++i) + (++i) + (++i);
System.out.println("i = " + i);
System.out.println("i = " + i);
}
}
编译执行,可见在Java中的结果和我们之前预期的是相同的。
3. 笔试面试中的奇葩题
要想知道编译器如何解释上面表达式中的++/--,首先要了解一下C编译器的贪心法。
贪心法:++/-- 表达式的阅读技巧
(1)编译器处理的每个符号应该尽可能多的包含字符
(2)编译器以从左向右的顺序一个一个尽可能多的读入字符
(3)当读入的字符不可能和已读入的字符组成合法符号为止
根据贪心法分析前面的问题:
// 读入第1个字符+,因为它可以与其它符号组合,于是继续读入第2个字符+,
// 编译器发现两个+形成一个自增运算符,会继续读入后面的变量i,得到++i。
// 但根据贪心法,还会再读入一个字符,即++i+,表示++i后面会加上一个数,这也是有意义的。
// 所以会再读入第5个字符,发现读入的字符不能变成一个有意义符号,所以停止读入字符
// 然后去计算 ++i++ 这个表达式,得到1++,显然语法上是错误的
++i+++i+++i;
a+++b; // 读到a++是有意义的,会再读入一个字符+,仍有意义,会再次读入b,即(a++) + b;
再想到之前我们讲解C注释时的一个例子:
y= x/*p;
根据贪心算法,编译器读取到 / 之后还会再读取 *,然后就把 /* 组合在一起,就是多行注释了,当时的解决办法是再 / 前后加上空格,即 y = x / *p; 为什么这样就可以了呢?
空格可以作为C语言中一个完整符号的休止符,编译器读入空格后立即对之前读入的符号进行处理。
四.《第18课 - 三目运算符和逗号表达式》
1. 三目运算符
(1)三目运算符(a ? b : c)可以作为逻辑运算的载体
(2)规则:当a的值为真时,返回b的值;否则返回c的值
三目运算符的本质就是 if...else... 语句,只不过三目运算符格式更加方便优雅。
1 if (a) 2 b; 3 else 4 c;
【三目运算符初探】
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = 0;
c = (a < b) ? a : b;
// ((a < b) ? a : b) = 3; // 编译报错 error: lvalue required as left operand of assignment
// 三目运算符最终返回的是一个值,而不是一个变量,不能作为左操作数
// 1 = 10; // error: lvalue required as left operand of assignment 相同的错误
// 要想使用三目运算符对某一变量赋值,可以使用变量的地址进行操作
*(a < b ? &a : &b) = 3;
printf("a = %d\n", a); // 3
printf("b = %d\n", b); // 2
printf("c = %d\n", c); // 1
return 0;
}
(3)三目运算符的返回类型
- 通过隐式类型转换规则,返回b或c中较高的类型
- 当b和c不能隐式转换到同一类型时将编译出错,类如指针与基本类型
#include <stdio.h>
int main()
{
char c = 0;
short s = 0;
int i = 0;
double d = 0;
char* p = "str";
printf("%zu\n", sizeof(c ? c : s)); // char和short转换为int, 4
printf("%zu\n", sizeof(i ? i : d)); // int转换为double,8
printf("%zu\n", sizeof(d ? d : p)); // error: type mismatch in conditional expression
// 指针和基本类型之间不允许隐式类型转换
return 0;
}
2. 逗号表达式
(1)逗号表达式是C语言中的"粘贴剂",多用于将多个子表达式连接为一个表达式
(2)逗号表达式的值为最后一个子表达式的值
(3)逗号表达式中的前N-1个子表达式可以没有返回值
(4)逗号表达式按照从左向右的顺序计算每个表达式的值
#include <stdio.h>
void hello()
{
printf("Hello!\n");
}
int main()
{
// 二维数组使用大括号初{ }始化,不能使用小括号
int a[3][3] = {
(0, 1, 2), // 注意这里是逗号表达式,最终结果为2
(3, 4, 5), // 注意这里是逗号表达式,最终结果为5
(6, 7, 8) // 注意这里是逗号表达式,最终结果为8
};
int i = 0;
int j = 0;
// 下面会出现死循环吗?
while (i < 5)
printf("i=%d\n", i), // 注意这里是逗号而不是分号
hello(),
i++;
for (i = 0; i < 3;i++){
for (j = 0; j < 3;j++){
printf("a[%d][%d] = %d\n", i, j, a[i][j]);
}
}
return 0;
}
逗号表达式的正确用法 ==> 一行代码实现strlen函数(使用递归实现)
#include <stdio.h>
#include <assert.h>
int my_strlen(const char *s)
{
// return *s ? my_strlen(s + 1) +1 : 0; // 没有考虑s为空指针
return assert(s), (*s ? my_strlen(s + 1) +1 : 0);
}
int main()
{
printf("str len = %d\n", my_strlen("2019-12-27 21:06:18"));
printf("str len = %d\n", my_strlen(NULL));
return 0;
}