C语言运算符和表达式(一)

谨记:

上帝在缔造每个人的时候,给予每个人的一切都是相同的,然而,人与人却是存在如此大的差距,记着上帝为你关上了一扇门,那么他一定会为你打开另一扇窗户,人与人之间的差距是会不断的缩小的,如果你连走出的勇气都没有,那么为你开再多窗户和门都是毫无意义,因为,你永远是在原地。努力就有收获,付出就有回报!

引言

和其他程序设计语言一样,C语言中表示运算的符号称为运算符。运算符是告诉编译程序执行特定算术或逻辑操作的符号,运算的对象称为操作数。
对一个操作数进行运算的运算符称为单目运算符,对两个操作数进行运算的运算符称为双目运算符,三目运算符对三个操作数进行运算。用运算符和括号可以将操作数连接起来组成表达式。
C语言提供了40多个运算符,其中一部分跟其他高级语言相同(例如“+”、“−”、“*”等运算符),另外的与汇编语言类似,对计算机的底层硬件(如指定的物理地址)能进行访问。如下图所示:

这篇文章,大概讲解算术运算符和表达式、关系运算符和表达式、逻辑运算符和表达式以及位运算符和表达式,望读者认真体会和学习。

1、算术运算符和表达式

1.1 算术运算符
算术运算符包括我们熟悉的数学课上讲的加减乘除四则运算和求模运算以及正负运算符。如下图:

程序案例:

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10, b = 20 ,c;
        double x = 5, y = 15, z;
        c = a + b;//c赋值为30;
        c = a - b;//c赋值为 - 10;
        c = a * b;//c赋值为 200;
        c = a / b;//c赋值为 0;这里是求商
        c = a % b;//c赋值为10;
        printf("%d",c);
        z = x + y;//z赋值20;
        z = x - y;//z赋值-10;
        z = x * y;//z赋值75;
        z = x / y; //z赋值0.3333333
      //  z = x % y;报错是因为 % 运算符只针对整数。
    }
    return 0;
}

说明:
1、“+”、“−”、“”、“/”4种运算符的操作数,可以是任意基本数据类型,其中“+”、“−”、“”与一般算术运算规则相同。
2、除法运算符“/”包括了除和整除两种运算,当除数和被除数都是整型数时,结果只保留整数部分而自动舍弃小数部分,注意0不能作为除数。除数和被除数只要有一个浮点数,进行浮点数相除。
3、取模运算就是求余数,取模运算要求两个操作数只能是整数,不能是浮点数,如10.8%2或5%2.0都是不正确的。
4、运算符“−”除了用作减法运算符之外,还有另一种用法,即用作负号运算符。用作负号运算符时只要一个操作数,其运算结果是取操作数的负值。
5、字符型数会自动地转换成整型数, 因此字符型数也可以参加双目运算。
6、在进行 % 运算符操作时,如果被除数的绝对值小于除数的绝对值,那么结果就为被除数,的值,结果的符号取决于被除数,如果被除数是负数不管除数正负性,结果都为负,如果被除数为正,不管除数正负,结果都为正,例如:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
            int a = 10,b = 20;
            int c = a % b;
            printf("%d\n",c);
            int a1 = -10;
            int c1 = a1 % b;
            printf("%d\n",c1);
            int b1 = -20;
            int c11 = a % b1;
            printf("%d\n",c11);
            int c2 = a1 % b1;
            printf("%d\n",c2);
            int x = -40,y = 15,z;
            z = x % y;
            printf("%d\n",z);
    }
    return 0;
    }
    输出的结果为:
    10
    -10
    10
    -10
    -10
    Program ended with exit code: 0

1.2 算术表达式
算术表达式就是用算术运算符和括号可以将操作数连接起来组成算术表达式。
比如:a+2b-5、18/3(2.5+8)-'a'
在一个算术表达式中,我们允许出现不同类型的算术运算符,那么在这里,就有一个优先级的说法,在数学领域里,我们都知道一个表达式我们应该先算什么后算什么,在这里也是一样的。
1.2.1 优先级
C语言对每一种运算符都规定了优先级,混合运算中应按次序从高优先级的运算执行到低优先级的运算。算术运算符的优先级从高到低排列如下(自左向右)。
1.2.2 类型的转换
A、自动转换(隐式转换)自动转换是在源类型和目标类型兼容以及目标类型广于源类型时发生一个类型到另一类的转换。这种转换是系统自动进行的。其中,float型向double型的转换和char型向int型的转换是必定要进行的,即不管运算对象是否为不同的类型,这种转换都要进行。图4-1中纵向箭头表示当运算对象为不同类型时的转换方向。如int型与double型数据进行运算时,是先将int型转换为double型,再对double型数据进行运算,最后的运算结果也为double型。
B、强制转换 利用强制类型转换运算符可以将一个表达式的运算结果转换成所需要的类型。强制类型转换的一般形式是: (数据类型名)表达式
(int)a double(x + y);这里就不多做解释了。很简单的,读者可以自己尝试一哈。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    float a=2.34;
    printf ("(int) a=%d, a=%f\n",(int)a,a);
    return 0;
    }
}
运行结果:(int) a=2
          a=2.340000
          Program ended with exit code: 0

2、关系运算符和表达式

2.1 关系运算符
在程序中经常需要通过比较两个值的大小关系来决定程序下一步的工作。比较两个值的运算符称为关系运算符。关系运算符对两个表达式进行比较,返回一个真/假值。在C语言中的关系运算符,如下图:


关系运算符都是双目运算符,其结合性均为左结合。即从左往右,关系运算符的优先级低于算术运算符,高于赋值运算符。
在这6个关系运算符中,“<”、“<=”、“>”、“>=”的优先级相同,高于“= =”和“!=”,“= =”和“!=”的优先级相同。根据优先级的关系,以下表达式具有等价的关系。

    c>a+b       和           c>(a+b)
    a>b==c     和           (a>b)==c
    a=b>c       和           a=(b>c)

2.2 关系表达式
用关系运算符把表达式进行一个关联,则称为关系表达式。
一般格式: 表达式 关系运算符 表达式
关系表达式真值表: 真 和 假 程序中用 1 和 0代替。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        int a = 10, b = 15;
        if (a > b) {
            printf("true\n");
        }else{
            printf("false\n");
        }
        if (a % b > 0) {
            printf("true  %d \n ",(a % b));
        }else{
            printf("false\n");
        }
    }
    return 0;
}
输出结果:false
         true  10 
        Program ended with exit code: 0

在C99标准以前,C语言没有能表达true和false的类型,那个时候只能用“1”和“0”来代替,或者用宏定义另外,通过typedet来自定义bool类型,如下所示。

                typedef unsigned char bool;
                #define TRUE 1
                #define FALSE 0

然而在C99标准,C语言给出了_Bool这个类型,这样我们在编写程序的时候,我们就可以直接使用true和false。

     _Bool k = true;//C99标准
    if (k) {
        printf("true");
    }

3、逻辑运算符和表达式

3.1 逻辑算法
在我们C语言中啊,给我们提供了3种逻辑运算符,即:&&(逻辑运算与)、||(逻辑运算或)、!(逻辑运算非),其中与运算符(&&)和或运算符(||)均为双目运算符,具有左结合性;非运算符(!)为单目运算符,具有右结合性。下面具体介绍这三种运算符。

A、逻辑运算与(&&)
真值表: 
| 表达式1 | 表达式2 | &&值  |
|--------|--------|--------|
|   0    |   0    |   0    |
|   0    |   1    |   0    |
|   1    |   0    |   0    |
|   1    |   1    |   1    |

双目运算符,只有两个运算量都是1时,运算结果才为1。

B、逻辑运算或(||)

当两个运算量进行或运算时,只要有一个运算量为“1”,结果就为“1”,
    | 表达式1 | 表达式2 | ||值  |
    |--------|--------|--------|
    |   0    |   0    |   0    |
    |   0    |   1    |   1    |
    |   1    |   0    |   1    |
    |   1    |   1    |   1    |

C、逻辑非( !)
单目运算符,当运算量进行非运算,结果会取反。即 0 变为 1, 1 变为 0。

3.2 表达式
逻辑表达式的一般形式如下:
表达式 逻辑运算符 表达式
其中的表达式也可以是逻辑表达式,从而组成了嵌套的情形。如图,优先级。


通过优先级,我们可以得出:

a>b && c>d等价于(a>b) && (c>d)
!b==c||d<a等价于((!b)==c)||(d<a)
a+b>c && x+y<b等价于((a+b)>c) && ((x+y)<b)

逻辑表达式的值是式中各种逻辑运算的最后值,以“1”和“0”分别代表“真”和“假”。
在进行编程的时候,我们同样也可以用true和false进行一个运算。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10,b = 20, c = 30;
        if (a < b && b > c ) {
            printf("true\n");
        }else{
            printf("false\n");
        }
        if (a > b || b < c) {
            printf("true\n");
        }else{
            printf("false\n");
        }
        if (!(a == b)) {
            printf("true\n");
        }
    }
    return 0;
}
输出结果:
    false
    true
    true
    Program ended with exit code: 0

4、位运算符及表达式

4.1 位运算符
位运算符是指二进制位的运算,在C语言中,给我们提出了如下为运算符,

  • 位与(&)
  • 位或(|)
  • 异或(^)
  • 取反(~)
  • 左移(<<)
  • 右移(>>)

A、与运算符(&)
双目操作符,当两个位进行相与时,只有两者都为“1”时结果才为“1”,其他都为“0”。
如:0x8a & 0x45
0x8a 1000 1010
0x45 0100 0101
结果 0000 0000 转换为十六进制 0x00

0xff & 0xf3
0xff 1111 1111
0xf3 1111 0011
结果 1111 0011 转换为十六进制 0xf3

B、或运算符(|)
双目操作符,当两个位进行相或时,两者中只要有一方为“1”,结果就为“1”,其他都为0.
(1)注意:位或符号是一个|,两个||是逻辑或。
(2)真值表:1|0=1 1|1=1 0|0=0 0|1=1
(3)从真值表可以看出:位或操作的特点是:只有2个0相位或才能得到0,只要有1个1结果就一定是1.
(4)位或和逻辑或的区别:位或时两个操作数是按照二进制位彼次对应位相与的,逻辑或是两个操作数作为整体来相或的。
如: 0x2b | 0xf7
0x2b 0010 1011
0xf7 1111 0111
结果 1111 1111 转换为十六进制 0xff

C、位取反~
(1)注意:C语言中位取反是~,C语言中的逻辑取反是!
(2)按位取反是将操作数的二进制位逐个按位取反(1变成0,0变成1);而逻辑取反是真(在C语言中只要不是0的任何数都是真)变成假(在C语言中只有0表示假)、假变成真。

实验:任何非0的数被按逻辑取反再取反就会得到1;
任何非0的数被按位取反再取反就会得到他自己;

D、位异或^
(1)位异或真值表:1^1=0 0^0=0 1^0=1 0^1=1
(2)位异或的特点:2个数如果相等结果为0,不等结果为1。记忆方法:异或就是相异就或操作起来。
位与、位或、位异或的特点总结:
位与:(任何数,其实就是1或者0)与1位与无变化,与0位与变成0
位或:(任何数,其实就是1或者0)与1位或变成1,与0位或无变化
位异或:(任何数,其实就是1或者0)与1位异或会取反,与0位异或无变化

E、左移位<< 与右移位>>
C语言的移位要取决于数据类型。
对于无符号数,左移时右侧补0(相当于逻辑移位)
对于无符号数,右移时左侧补0(相当于逻辑移位)
对于有符号数,左移时右侧补0(叫算术移位,相当于逻辑移位)
对于有符号数,右移时左侧补符号位(如果正数就补0,负数就补1,叫算术移位)

嵌入式中研究的移位,以及使用的移位都是无符号数。

重点

特定位清零用&
(1)回顾上节讲的位与操作的特点:(任何数,其实就是1或者0)与1位与无变化,与0位与变成0
(2)如果希望将一个寄存器的某些特定位变成0而不影响其他位,可以构造一个合适的1和0组成的数和这个寄存器原来的值进行位与操作,就可以将特定位清零。
(3)举例:假设原来32位寄存器中的值为:0xAAAAAAAA,我们希望将bit8~bit15清零而其他位不变,可以将这个数与0xFFFF00FF进行位与即可。
4.2 表达式
4.2.1、特定位置1用|
(1)回顾上节讲的位或操作的特点:任何数,其实就是1或者0)与1位或变成1,与0位或无变化
(2)操作手法和刚才讲的位与是类似的。我们要构造这样一个数:要置1的特定位为1,其他位为0,然后将这个数与原来的数进行位或即可。

4.2.2、特定位取反用^
(1)回顾上节讲的位异或操作的特点:(任何数,其实就是1或者0)与1位异或会取反,与0位异或无变化
(2)操作手法和刚才讲的位与是类似的。我们要构造这样一个数:要取反的特定位为1,其他位为0,然后将这个数与原来的数进行位异或即可。

4.2.3.如何用位运算构建特定二进制数
4.2.3.1、寄存器位操作经常需要特定位给特定值
(1)从上节可知,对寄存器特定位进行置1或者清0或者取反,关键性的难点在于要事先构建一个特别的数,这个数和原来的值进行位与、位或、位异或等操作,即可达到我们对寄存器操作的要求。
(2)解法1:用工具软件或者计算器或者自己大脑计算,直接给出完整的32位特定数。
优势:可以完成工作,难度也不大,操作起来也不是太麻烦。
劣势:依赖工具,而且不直观,读程序的人不容易理解。
评价:凑活能用,但是不好用,应该被更好用的方法替代。
(2)解法2:自己写代码用位操作符号(主要是移位和位取反)来构建这个特定的二进制数

4.2.3.2、使用移位获取特定位为1的二进制数
(1)最简单的就是用移位来获取一个特定位为1的二进制数。譬如我们需要一个bit3~bit7为1(隐含意思就是其他位全部为0)的二进制数,可以这样:(0x1f<<3)
(2)更难一点的要求:获取bit3~bit7为1,同时bit23~bit25为1,其余位为0的数:((0x1f<<3) | (7<<23))

4.2.3.3、再结合位取反获取特定位为0的二进制数
(1)这次我们要获取bit4~bit10为0,其余位全部为1的数。怎么做?
(2)利用上面讲的方法就可以:(0xf<<0)|(0x1fffff<<11)
但是问题是:连续为1的位数太多了,这个数字本身就很难构造,所以这种方法的优势损失掉了。
(3)这种特定位(比较少)为0而其余位(大部分)为1的数,不适合用很多个连续1左移的方式来构造,适合左移加位取反的方式来构造。
(2)思路是:先试图构造出这个数的位相反数,再取反得到这个数。(譬如本例中要构造的数bit4~bit10为0其余位为1,那我们就先构造一个bit4~bit10为1,其余位为0的数,然后对这个数按位取反即可)

4.2.3.4、总结:位与、位或结合特定二进制数即可完成寄存器位操作需求
(1)如果你要的这个数比较少位为1,大部分位为0,则可以通过连续很多个1左移n位得到。
(2)如果你想要的数是比较少位为0,大部分位为1,则可以通过先构建其位反数,然后再位取反来得到。
(3)如果你想要的数中连续1(连续0)的部分不止1个,那么可以通过多段分别构造,然后再彼此位与即可。这时候因为参与位或运算的各个数为1的位是不重复的,所以这时候的位或其实相当于几个数的叠加。

4.2.4.位运算实战演练1
回顾:要置1用|,用清零用&,要取反用^,~和<< >>用来构建特定二进制数。
4.2.4.1、给定一个整型数a,设置a的bit3,保证其他位不变。
a = a | (1<<3) 或者 a |= (1<<3)
4.2.4.2、给定一个整形数a,设置a的bit3~bit7,保持其他位不变。
a = a | (0b11111<<3) 或者 a |= (0x1f<<3);
4.2.4.3、给定一个整型数a,清除a的bit15,保证其他位不变。
a = a & (~(1<<15)); 或者 a &= (~(1<<15));
4.2.4.4、给定一个整形数a,清除a的bit15~bit23,保持其他位不变。
a = a & (~(0x1ff<<15)); 或者 a &= (~(0x1ff<<15));
4.2.4.5、给定一个整形数a,取出a的bit3~bit8。
思路:
第一步:先将这个数bit3~bit8不变,其余位全部清零。
第二步,再将其右移3位得到结果。
第三步,想明白了上面的2步算法,再将其转为C语言实现即可。
a &= (0x3f<<3);
a >>= 3;

4.2.4.6、用C语言给一个寄存器的bit7~bit17赋值937(其余位不受影响)。
关键点:第一,不能影响其他位;第二,你并不知道原来bit7~bit17中装的值。
思路:第一步,先将bit7~bit17全部清零,当然不能影响其他位。
第二步,再将937写入bit7~bit17即可,当然不能影响其他位。
a &= ~(0x7ff<<7);
a |= (937<<7);

4.2.5.位运算实战演练2
4.2.4.7、用C语言将一个寄存器的bit7~bit17中的值加17(其余位不受影响)。
关键点:不知道原来的值是多少
思路:第一步,先读出原来bit7~bit17的值
第二步,给这个值加17
第三步,将bit7~bit17清零
第四步,将第二步算出来的值写入bit7~bit17

4.2.4.8、用C语言给一个寄存器的bit7~bit17赋值937,同时给bit21~bit25赋值17.
思路:4.2.4.6的升级版,两倍的4.2.4.6中的代码即可解决。
分析:这样做也可以,但是效果不够高,我们有更优的解法就是合两步为一步。

4.2.6.技术升级:用宏定义来完成位运算
4.2.6.1、直接用宏来置位、复位(最右边为第1位)。

#define SET_NTH_BIT(x, n)  (x | ((1U)<<(n-1)))
#define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))

4.2.6.2、截取变量的部分连续位。例如:变量0x88, 也就是10001000b,若截取第2~4位,则值为:100b = 4
#define GETBITS(x, n, m) ((x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1))
分析:这个题目相当于我们4.2.4.5中做的事情,只不过要用宏来实现。
这个题目相当于是要把x的bit(n-1)到bit(m-1)取出来
复杂宏怎么分析:

((x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1))
第一步,先分清楚这个复杂宏分为几部分:2部分
(x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1)
分析为什么要>>(n-1),相当于是我们4.2.4.5中的第二步

第二步,继续解析剩下的:又分为2部分
x & ((0U)<<(m-n+1))<<(n-1)
分析为什么要&,相当于我们4.2.4.5中的第一步

第三步,继续分析剩下的:
~ (~(0U)<<(m-n+1)) << (n-1)
这个分析时要搞清楚第2坨到底应该先左边取反再右边<<还是先右边<<再左边取反。
解法:第一,查C语言优先级表;第二,自己实际写个代码测试。
说明这个式子应该是 ((0U)<<(m-n+1)) << (n-1) ,这就又分为2部分了

10001000
00001110
00001000

推荐阅读更多精彩内容