首页 > 其他分享 >【C语言】操作符详解

【C语言】操作符详解

时间:2024-08-02 22:25:44浏览次数:15  
标签:0000 int C语言 1111 详解 操作符 执行 表达式

目录

一、操作符分类

二、移位操作符

(1)左移位操作符

(2)右移位操作符

三、位操作符

四、移位、位操作符的综合练习

(1)不用临时变量,交换两个整数

(2)求内存中整数的二进制中1的个数

(3)二进制位置0或置1

五、单目操作符

六、逗号表达式

七、结构体成员访问操作符

(1)结构体

(2)结构体的声明

(3)结构体变量的定义和初始化

(4)结构体成员访问操作符

① 结构体成员的直接访问

② 结构体成员的间接访问

八、操作符的属性:优先级和结合性

(1)优先级

(2)结合性

(3)优先级和结合性表

九、表达式求值

(1)整型提升

(2)算术转换

十、问题表达式的解析

(1)表达式1

(2)表达式2

(3)表达式3

(4)表达式4

(5)表达式5

(6)总结


一、操作符分类

算数操作符:【+、-、*、/、%】

移位操作符:<<、>>

位操作符:&、|、^、~

赋值操作符:【=、+=、-=、*=、/=、%=】、<<=、>>=、&=、|=、^=

单目操作符:【!、+、-、sizeof、(类型)、++、--】、~、&、*

关系操作符:【>、>=、<、<=、==、!=】

逻辑操作符:【&&、||】

条件操作符:【? :】

逗号表达式:,

下标引用:【[]】

函数调用:【()】

        【】里面的操作符在之前已经学习过,今天学习的是没有被【】括起来的操作符。

        【+、-、*、/、%】、【=、+=、-=、*=、/=、%=】、【!、+、-、sizeof、(类型)、++、--】请看文章:http://t.csdnimg.cn/msdld 操作符部分。

        【>、>=、<、<=、==、!=】、【&&、||】、【? :】请看文章:http://t.csdnimg.cn/drUIS 操作符部分。

        【[]】请看文章:http://t.csdnimg.cn/OF3yE 访问一维数组元素的部分。

        【()】请看文章:http://t.csdnimg.cn/3CtAH 函数调用操作符部分。

        下面涉及原码、反码、补码的知识,请看文章:http://t.csdnimg.cn/sn9lE

二、移位操作符

        注意:

  • 移位操作符的操作数只能是整数;它右边的操作数(移动几位)只能是非负整数
  • 移动的位数范围是0~31(int类型4个字节为32 bit)。超过这个范围是不符合标准的,会给出警告。

(1)左移位操作符

        规则:左抛弃,右补0。

        例子:-12<<2

         -12

         = [1000 0000 0000 0000 0000 0000 0000 1100]原

         = [1111 1111 1111 1111 1111 1111 1111 0011]反

         = [1111 1111 1111 1111 1111 1111 1111 0100]补

        左移两位:

        [1111 1111 1111 1111 1111 1111 1101 0000]补

        = [1111 1111 1111 1111 1111 1111 1100 1111]反

        = [1000 0000 0000 0000 0000 0000 0011 0000]原

        = -48

(2)右移位操作符

        规则:

        逻辑右移:右抛弃,左补0。

        算术右移:右抛弃,左补原数的符号。

        例子:-12>>2 

        -12

        = [1111 1111 1111 1111 1111 1111 1111 0100]补

        ① 逻辑右移两位:

        [0011 1111 1111 1111 1111 1111 1111 1101]补

        = 1073741821

        ② 算术右移两位:

        [1111 1111 1111 1111 1111 1111 1111 1101]补

        = [1111 1111 1111 1111 1111 1111 1111 1100]反

        = [1000 0000 0000 0000 0000 0000 0000 0011]原

        = -3

        执行逻辑右移还是算术右移取决于编译器,编译器通常是算术右移。

        代码验证:

        观察上面的例子,可以发现左移操作符有乘以2的幂次方的效果,算术右移操作符有除以2的幂次方的效果,移动几位,幂指数就是几。

三、位操作符

        下面按位的意思是按照二进制位(bit)计算,且它们的操作数必须为整数。

  • 按位与(&):两者都为1,取1;其它取0。
  • 按位或(|):两者都为0,取0;其它取1。
  • 按位异或(^):两者相同取0;相反取1。
  • 按位取反(~):每一位都取反。

        举例:

        5

        = [0000 0000 0000 0000 0000 0000 0000 0101]补

        -3

        = [1000 0000 0000 0000 0000 0000 0000 0011]原

        = [1111 1111 1111 1111 1111 1111 1111 1100]反

        = [1111 1111 1111 1111 1111 1111 1111 1101]补

        5 & -3

        = [0000 0000 0000 0000 0000 0000 0000 0101]补

        = 5

        5 | -3

        = [1111 1111 1111 1111 1111 1111 1111 1101]补

        = [1000 0000 0000 0000 0000 0000 0000 0010]取反

        = [1000 0000 0000 0000 0000 0000 0000 0011]原

        = -3

        5 ^ -3

        = [1111 1111 1111 1111 1111 1111 1111 1000]补

        = [1000 0000 0000 0000 0000 0000 0000 0111]取反

        = [1000 0000 0000 0000 0000 0000 0000 1000]原

        = -8

        0

        = [0000 0000 0000 0000 0000 0000 0000 0000]补

        ~0

        = [1111 1111 1111 1111 1111 1111 1111 1111]补

        = [1000 0000 0000 0000 0000 0000 0000 0000]取反

        = [1000 0000 0000 0000 0000 0000 0000 0001]原

        = -1

        代码验证:

四、移位、位操作符的综合练习

(1)不用临时变量,交换两个整数

        题目:不能创建临时变量(第三个变量),实现两个整数的交换。

        若不考虑“不能创建临时变量”这个条件的情况下,我们通常使用的方法是:       

        但这里不能使用临时变量,需要想想其它方法。

        方法1:(加减法实现)

        首先,我们要明白,第三个变量是用来保留数据,避免数据在交换过程中被覆盖掉的。现在,我们不能使用第三个变量,那么就需要换一种方式把a、b的数据都保留下来。

        第37行a被赋值为 原a + 原b,这样既保留了 原a 的数据,又保留了 原b 的数据。然后,第38行,b被赋值为 a - 原b 等于 原a + 原b - 原b 等于 原a,此时a还保留着 原a+原b,不用担心原b的数据被覆盖掉了,在这一过程中,完成了 原a 向 原b 赋值。第39行a被赋值为 a - b 等于 原a + 原b - 原a 等于 原 b ,在这一过程中,完成了 原b 向 原a 赋值。

        但是这种方法有很大的局限性,就是当 a 和 b 的值过大时,因为第一个式子有加法运算,所以会发生溢出,得到不正确的值。

        因此,我们有需要寻找其它更优的办法。

        方法2:(位异或操作符^实现)

        因为 ^ 就是两数相同则为0,两数不同则为1,所以自身位异或自身等于0,如a^a得0,b^b得0。因为0^0得0,1^0得1,所以一个数位异或0等于自身,如a^0得a,b^0得b。

        故第52行的b被赋值为:a ^ b 等于 原a ^ 原b ^ 原b 等于 原a ^ 0 等于 原a,这一过程完成了原a 向 原b 赋值;第53行a被赋值为:a^b 等于 原 a ^ 原b ^ 原a 等于 原b ^ 0 等于 原b,这一过程完成了 原b 向 原a 赋值。

(2)求内存中整数的二进制中1的个数

        题目:求一个整数存储在内存中的二进制中1的个数。

        如果想计数1的个数,我们会想到取出该整数的每一位数字。取出每一位数字有一个常用的方法,就是:

        那么对于二进制整数,我们可以换成 %2 和 /2 :

        1234

        = [0000 0000 0000 0000 0000 0100 1101 0010]补

        当二进制整数为整数时,计算结果正确。

        -1234

        = [1000 0000 0000 0000 0000 0100 1101 0010]原

        = [1111 1111 1111 1111 1111 1011 0010 1101]反

、    = [1111 1111 1111 1111 1111 1011 0010 1110]补

        当二进制整数为负数时,计算结果错误。分析原因:因为a是负数,所以不管是余数还是商都是带有负号的,对于余数来说只有0和-1两情况,所以 一次都没计数。因此,a % 2 == 1对负数并不适用。如果把 a % 2 == 1 改成 a % 2 == -1 呢?结果如下:

        对比负数 -1234 的原码会发现只计数了原码的数值位上的1,符号位上的1根本没被计入;并且题目要求的是,计数在内存中整数的二进制中1的个数,内存中是用的补码形式而不是原码形式。因此,这种取余、求商的方法,是不能解决负数的情况的。

        方法1:

        首先,要清楚位与的规则,全1才为1,存在0就为0。然后,举例说明:

        -2

        = [1000 0000 0000 0000 0000 0000 0000 0010]原

        = [1111 1111 1111 1111 1111 1111 1111 1101]反

        = [1111 1111 1111 1111 1111 1111 1111 1110]补

        [1111 1111 1111 1111 1111 1111 1111 1110] ^ [0000 0000 0000 0000 0000 0000 0000 0001]

        = 0 (结果为0,第一位为0,不计数)

        [1111 1111 1111 1111 1111 1111 1111 1110] ^ [0000 0000 0000 0000 0000 0000 0000 0010]

        = [0000 0000 0000 0000 0000 0000 0000 0010](结果非0,第二位为1,计数器加1)

        ……

        从上面的例子可以看到,用这种方法可以计数负数补码的每一位1。与-2作位与的数字中1的位置变化可以用左移位操作符实现。

        方法2:

        首先思考下面的问题:

        二进制:若n = 1100,则1100 - 0001 = 1011

        二进制:若n = 1000,则1000 - 0001 = 0111

        ……

        观察上面的结果,我们可以得到一个结论,n - 1 会将 n 的最低位1改为0,最低位1后面的0都改为1。原因:减1必向最低位1借一个1,被借1后自然变成了0;而最低位1后面的0,因为自身没有1,都会向前一位借1,来减当前这个1,又因为是二进制,向前一位借到的1其实是2,2-1 = 1,最终最低位1后面的0都变为了1。

        因此,原数最低位的1以及之后的0,在n-1后都被更改为相反数,相反数(0和1)作位与的结果会是0;而n的最低位1前面的数在n-1后都未改变,1 & 1 得1,0 & 0 得0,即自身和自身作位与的结果是自身。

        综上所述,n & (n - 1) 的结果是,n的最低位1之前的高位数不变,之后的低位数以及最低位1都变为0。得到的效果简而言之就是,会将n最低位的1改为0

        源代码的最终效果:n 的补码中有多少个1,就会进多少次 while 循环,每次进循环都会更改一个1并计数器加1,直到所有的1都被更改为0,n的值变为0结束循环。

        对比方法1:方法1必须要对补码的每一位作位与运算,对int类型来说,要循环固定的32次,才能统计出1的个数;而方法2则是有多少个1,就作几次循环,效率比方法1更优。

(3)二进制位置0或置1

        题目:编写代码将13的二进制序列的第5位修改为1,然后再改为0。

        13 = [0000 0000 0000 0000 0000 0000 0000 1101]补

        第5位置1:

        [0000 0000 0000 0000 0000 0000 0000 1101] | [0000 0000 0000 0000 0000 0000 0001 0000] 

        = [0000 0000 0000 0000 0000 0000 0001 1101]补 = 29

        第5位置回0:

        [0000 0000 0000 0000 0000 0000 0001 1101] & [1111 1111 1111 1111 1111 1111 1110 1111]

        = [0000 0000 0000 0000 0000 0000 0000 1101] = 13

        代码及运行结果:

五、单目操作符

        还剩&和*,在后面学习指针后再讲解。

六、逗号表达式

        语法形式,用逗号隔开多个表达式:

表达式1, 表达式2, 表达式3......

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

        应用场景:

a = get_val();
count_val(a);
while(a){
    ... // 业务处理

    a = get_val();
    count_val(a);
}


可用逗号表达式改写:
while(a = get_val(), count_val(a), a){
    ... // 业务处理
}

        改写后,可以减少冗余的代码。

七、结构体成员访问操作符

(1)结构体

        我们有了内置类型还不够,比如我想描述一个学生,他有姓名、年龄、性别、学号....,这时应该怎么定义这个学生呢?于是,自定义类型结构体解决了这个问题。

        结构体是一些变量的集合,这些变量叫做它的成员变量,每个成员可以是不同类型的变量:字符型、整型、浮点型,甚至其它的结构体类型。

(2)结构体的声明

        语法形式如下:

struct tag{
    成员列表
}变量列表;

举例:学生结构体类型的声明
struct student{
    char name[20]; // 姓名
    int age; // 年龄
    char sex[5]; // 性别
    char id[20]; // 学号
};

(3)结构体变量的定义和初始化

// 结构体类型变量的定义:
// 班级结构体类型声明
struct Class{
    int grade[20]; // 年级
    char college[20]; // 学院
    char major[20]; // 专业
    char class[20]; // 班级
};

struct Class p1; // 定义班级结构体类型的变量p1(第一种方式的定义)

// 学生结构体类型的声明
struct Student{
    char name[20]; // 姓名
    int age; // 年龄
    char sex[5]; // 性别
    char id[20]; // 学号
    struct Class class; // 班级
} stu1, stu2; // 声明结构体类型的同时,定义变量(第二种方式的定义)


// 结构体类型变量的初始化:
struct Student stu3 = {'zhangsan', 20, 'male', '12003990101', NULL}; // 默认顺序初始化
struct Student stu4 = {.age = 21, .name = 'xiaoming', .sex = 'famale', .id = '12003990102', .class = NULL}; // 指定顺序初始化

struct Point{
    int data;
    struct Point p;
} n1 = {1, {2, NULL}}; // 结构体嵌套初始化

struct Point p1 = {.p = {.p.p = NULL, .p.data = 2}, .data = 2}; // 结构体嵌套初始化

(4)结构体成员访问操作符

① 结构体成员的直接访问

        直接访问用操作符(.),如下例子:

#include<stdio.h>

struct Point{
    int x;
    int y;
} p = {1, 2};

int main(){
    printf("%d, %d", p.x, p.y); // 结构体变量.成员名
    return 0;
}

② 结构体成员的间接访问

    暂留,学习完指针后补充

八、操作符的属性:优先级和结合性

        操作符的优先级和结合性两个属性,决定了表达式的计算顺序。

(1)优先级

        对于相邻的两个操作符,优先级高的操作符先执行计算,优先级低的操作符后执行计算。如下例子:

5 + 3 * 4

        乘号 * 比加号 + 的优先级高,所以先执行3 * 4 得 12,再执行5 + 12。

(2)结合性

        对于相邻的两个操作符的优先级相同时,无法再用操作符的优先级判断它们执行顺序。这时,就根据操作符的结合性决定执行顺序。结合性分为左结合(从左向右执行)和右结合(从右向左执行)。如下例子:

2 * 6 / 4 

        因为乘号 * 和除号 / 的优先级相同,所以根据结合性决定执行顺序。* 和 / 都为左结合,故先执行2 * 6 得 12,再执行 12 / 4。

(3)优先级和结合性表

        参考: C 运算符优先级 - cppreference.com

        这些也不用全记住,需要的时查表就行,况且还有万能的()。不过我们记住一些常用的更好:

  • 圆括号(()):最高级,从左到右。
  • 后缀自增与自减(++、--):1级,从左到右。
  • 前缀自增与自减(++、--):2级,从右到左。
  • 单目运算符(+、-):2级,从右到左。
  • 乘除法(*、/):3级,从左到右。
  • 加减法(+、-):3级,从左到右。
  • 关系运算符(<、>、<=、>=):6级,从左到右。
  • 各种赋值运算符:倒数第2级,从右到左。

九、表达式求值

(1)整型提升

        整型提升就是,对于字符型短整型的操作数(长度小于 int 类型),当他们在表达式中发生运算时,会先提升为 int 类型,再进行计算。

        为什么要进行整型提升:整型运算要在CPU中的器件 ALU 里执行,里面的操作数字节长度一般是 int 类型字节长度,同时CPU的通用寄存器里的操作数也是这个长度。而通用CPU是难以实现8 bit 长度的直接相加运算(虽然机器指令中可能有 8 bit 长度的相加指令),因此,对于长度小于 int 类型的整型会先转换为标准的 int 或者 unsigned int,再送进CPU执行运算。

        整型提升规则:

  • 有符号整数:高位补符号位上的值。
  • 无符号整数:高位补0。 

        在整型提升后,送入CPU执行运算完毕,会将 int 类型长度的数据截断为原本定义的数据类型长度,最后存储到变量中。举例:

(2)算术转换

        若一个操作符的操作数的类型不同时,要先将它们的类型转换成同一类型,才能进行运算。下面的层次体系被成为寻常算术转换

        例如:

        a 为 double 类型8字节,b  为 int 类型 4字节,根据转换层次体系,b转换为 double 类型,得到计算结果也为 double类型 8字节。

十、问题表达式的解析

(1)表达式1

a * b + c * d + e * f

        对于这个表达式,我们可以确定的是,相邻的 * 比 + 的优先级高,故先执行 * 再执行 + 。但是我们确定的只是相邻的 * 和 + 的执行顺序,不相邻的 * 和 + 的执行顺序是确定不了的。因此,第一个 + 和第三个 * (不相邻)到底是哪个先执行,是存在歧义的

        执行顺序可以是这样:

        按照优先级,1和2比4先执行;2和3比5先执行。按照 + 的结合性(左结合),4比5先执行。在这个执行路线中,第一个 + 比第三个 * 执行。

        执行顺序也可以是这样:

        按照优先级,1和2比3先执行;2和4比5先执行。按照 + 的结合性(左结合),3比5先执行。在这个执行路线中,第一个 + 比第三个 * 执行。

(2)表达式2

c + --c

        对于这个表达式,我们可以确定的是: -- 比 + 的优先级高,故先执行 -- 再执行 + 。但是,到底是执行表达式前就把值放入了第一个c里,还是执行了--c后,才把值放入第一个c里,是存在歧义的。

        假如c的值初始化为1。若在执行表达式前就把值放入了第一个c里,那么 1 + 0 得 1;若执行了--c后,才把值放入第一个c里,那么 0 + 0 得 0。

(3)表达式3

int main()
{
    int i = 10;
    i = i-- - --i * ( i = -3 ) * i++ + ++i;
    printf("i = %d\n", i);
    return 0;
}

        此代码来自于《C和指针》这本书,作者将该代码在不同的编译器中执行,得到了截然不同的结果:

        可以得出,由于这些歧义,以及不同编译器的实现不同,而导致执行结果不同。这种有歧义的代码,可移植性是很差的。

(4)表达式4

#include <stdio.h>

int fun()
{
    static int count = 1;
    return ++count;
}

int main()
{
    int answer;
    answer = fun() - fun() * fun();
    printf( "%d\n", answer);
    return 0;
}

        在这个表达式中,我们可以知道相邻的 * 比 - 优先级高,先执行 * 再执行 - 。但是先调用哪个fun 函数是无从而知的,这也造成了歧义。
        如果执行顺序是:

        结果为:2 - 3 * 4 得 2 - 12 等于-10。

        如果执行顺序是: 

        结果为:3 - 2 * 4 得 3 - 8 等于 -5。

        当然还有其它的执行顺序,就不一一例举。

(5)表达式5

#include <stdio.h>
int main()
{
    int i = 1;
    int ret = (++i) + (++i) + (++i);
    printf("%d\n", ret);
    printf("%d\n", i);
    return 0;
}

        这个代码在gcc编译器上运行的结果:

        在VS2019上运行的结果:

        为什么运行得到的结果不同呢?其实还是跟表达式1类似的问题:我们只知道相邻 ++ 比 + 的优先级高,+ 为左结合,但是不相邻的第一个 + 和第三个 ++ 哪个先执行是不知道的。

        对于VS2019的编译器,是先执行第三个 ++,后执行第一个 +:执行第一个 ++i,i为2 >> 执行第二个 ++i,i为3 >> 执行第三个 ++i,i为4 >> 执行第一个 + ,4 + 4 为 8 >> 执行第二个 + ,8 + 4 为12。

        对于gcc编译器,是先执行第一个 +,后执行第三个 ++:执行第一个 ++i,i为2 >> 执行第二个 ++i,i为3 >> 执行第一个 + ,3 + 3 为 6 >> 执行第三个 ++i,i为4 >> 执行第二个 + ,6 + 4 为10。

        以上的过程,都可以通过调试中的反汇编窗口验证(以下是VS2019的):

(6)总结

        就算有了优先级和结合性,表达式仍然可能不能通过操作符的属性来确定唯一的路径。为了降低表达式存在的风险,我们写表达式时,建议将一个复杂表达式分解为简单的表达式

标签:0000,int,C语言,1111,详解,操作符,执行,表达式
From: https://blog.csdn.net/2401_86272648/article/details/140774954

相关文章

  • C语言——函数
    C语言——函数函数的语法函数的调用关系递归函数的主要思想是:函数其实是从上到下逐步求解的过程,把一个大的问题拆成多个小的子问题或者说把一个大的功能拆成小的功能模块,通过实现小的功能最终实现大的功能的过程。函数的语法类型标识符函数名(形式参数){函数体......
  • 【C语言】程序环境,预处理,编译,汇编,链接详细介绍,其中预处理阶段重点讲解
    目录程序环境翻译环境1.翻译环境的两个过程2.编译过程的三个阶段 执行环境 预处理(预编译) 1.预定义符号2.#define 2.1用#define定义标识符(符号)2.2用#define定义宏 2.3#define的替换规则 2.4#和##的用法2.5宏和函数2.6#undef3.命令......
  • C语言指针与数组
    在上一篇对指针介绍的文章当中,我们初次了解到了指针,并且知道了地址和内存间的关系,懂得了如何取地址和对指针的解引用,算是对指针有了一个初步的了解。而今天让我们对指针进行更深一步的了解吧~一、指针与数组名我们知道,指针变量是一个用来存放地址的变量,比如我们定义一个整形......
  • 嵌入式软件--C语言高级 DAY 8.5 相关函数
    递归函数在嵌入式中应用不常见,但对于学习C语言的我们,也要时刻记得它的作用和用法。此外还要记住sprintf尤其重要!还有时间戳!一、递归函数1.概念一个函数在函数体内又调用了本身。但必须满足两个条件:具有明显的结束条件;趋近于结束条件的趋势。2.递归原理#include<stdio.h>......
  • 嵌入式软件--C语言高级 DAY 7数组
    一、概念数组array:是多个相同类型数据按一定顺序排列的集合,并使用一个标识符命名。并通过编号(索引,亦称为下标或角标)的方式对这些数据进行统一管理。数组的长度=元素的个数标号角标是从0开始。二、define_array.c定义数组的三种形式:1.定义数组,可以先确定数组的元素个......
  • C语言数据在内存中的存储超详解
    文章目录1.整数在内存中的存储2.大小端字节序和字节序判断2.1什么是大小端?2.2为什么会有大小端?2.3练习3.浮点数在内存中的存储3.1一个代码3.2浮点数的存储3.2.1浮点数存的过程3.2.2浮点数取的过程3.3题目解析1.整数在内存中的存储在操作符......
  • C语言自定义类型结构体与位段超详解
    文章目录1.结构体类型的声明1.1结构体声明1.2结构体变量的创建和初始化1.3结构体的特殊声明1.3结构体的自引用2.结构体内存对齐2.1对齐规则2.2为什么存在内存对齐2.3修改默认对齐数3.结构体传参4.结构体实现位段4.1什么是位段4.2位段成员的内存......
  • Android开发 - RecyclerView 类详解
    什么是RecyclerViewRecyclerView是Android的一个控件,用来展示长列表或网格的内容,它比以前的ListView更加灵活和高效列表展示:想象你在手机上浏览一个长长的商品列表或图片网格。RecyclerView就是用来展示这样的内容的控件高效显示:如果你有一万件商品,RecyclerView不会一......
  • c语言中的地址与指针的概念,及变量的指针和指向变量的指针变量
    C语言中的地址、指针、以及变量的指针与指向变量的指针变量。1.地址(Address)在C语言中,每个变量在内存中都有一个唯一的内存地址。这个地址是变量存储的位置的标识符。可以通过& 运算符来获取一个变量的地址。#include<stdio.h>intmain(){  intx=10;  ......
  • c语言结构体的概述,定义结构体变量类型的方法,结构体变量的引用,结构体变量的初始化,结构
    1.C语言结构体的概述在C语言中,结构体(struct)是一种复合数据类型,用于将不同类型的数据组合在一起。它可以包含基本数据类型(如int、float、char等)以及其他结构体。结构体非常适合表示具有多种属性的复杂数据,如学生信息(包含姓名、年龄、成绩等)或坐标点(包含x和y坐标)。结构......