《C语言进阶剖析》 3.运算符

本文介绍了C语言中的位运算符,包括左移和右移运算符的规则,并通过示例说明了位运算符的使用,如交换变量值的方法。同时,讨论了++和--运算符在混合运算中的不确定性,以及C标准对此的规定。此外,还探讨了三目运算符和逗号表达式的用法及特性。
摘要由CSDN通过智能技术生成

一.《第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编译器编译,程序执行结果如下:

  

可见两款编译器对于 的输出结果与我们的预期都不同,那为何会这样呢?我们从汇编代码中寻找原因。

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值