• 8.0、C语言——操作符


    8.0、C语言——操作符

    算数操作符:

            +     -     *     /     %

            1、除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数

            2、对于 / 操作符如果两个操作数都为整数,执行整数除法,而只要有浮点数执行的就是浮点数除法

            3、% 操作符的两个操作数必须为整数,返回的是整除之后的余数

    移位操作符【只能作用于整数】:

            << 左移操作符

            >> 右移操作符

    右移位运算 分为以下两种:
    1、算术右移:
            右边丢弃,左边补原符号位【正数补0,负数补1】,当前的编译器基本上用的都是 算术移位

    2、逻辑右移:
            右边丢弃,左边补0【不挂正数还是负数都补0】

            其实总结一下:只有负数在算术右移运算时才会补 1 其他情况都是补 0;虽然有两种移位方式但是通常我们见到的都是算术右移;右移移位是除 2,左移一位是乘 2

    位操作符:

            &(与):都是 1 则为 1 ,有 0 则为 0      

            |(或):有 1 则为 1 ,全 0 才为 0

            ^(异或):相同为 0 ,不同为 1 

    来看几道面试题:

    1、不准使用中间变量,然后交换两个变量的值,int a = 5; int b = 3;  实现代码如下所示:

    方式一:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main() {
    4. int a = 5;
    5. int b = 3;
    6. a = a + b;
    7. b = a - b;
    8. a = a - b;
    9. printf("a = %d , b = %d",a,b);
    10. return 0;
    11. }

    该方法有一定的缺陷,如果我们的a ,b两个变量数值很大,相加后可能会溢出 

    方式二:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main() {
    4. int a = 5;
    5. int b = 3;
    6. a = a ^ b;
    7. b = a ^ b;
    8. a = a ^ b;
    9. printf("a = %d , b = %d",a,b);
    10. return 0;
    11. }

            这里我们可以将第一次 a^b 看成一个译码 x,经计算后会发现 x ^ a 会得到 b 原来的值,x ^ a 会得到 a 原来的值

    2、计算一个整数的二进制补码中有多少个 1

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main() {
    4. int input = 0;
    5. int count = 0;
    6. scanf("%d",&input);
    7. for (int i = 0;i < 32;i++) {
    8. if (1 == ((input >> i) & 1)) {
    9. count++;
    10. }
    11. }
    12. printf("count = %d",count);
    13. return 0;
    14. }

    举个例子:
            3 的二进制为:00000000 00000000 00000000 00000011

            1 的二进制为:00000000 00000000 00000000 00000001

            那么每次 右移 1 位 依次和 1 的二进制 进行 &(与) 运算,如果为 1 则说明是 1 则 count++,因为int类型为4字节32位,所以循环32次~

    单目操作符:

            !                        逻辑反操作,正变假,假变真
            -                          负值                        
            +                         正数     
            &                         取地址
            sizeof                  操作数的类型长度(一字节为单位)
            ~                          对一个数的二进制按位取反
            --                          前置、后置 --
            ++                        前置、后置 ++
            *                           间接访问操作符
            (类型)                   强制类型转换    

    !(取反),比如 int a = 10;  !a 等于 0【因为0为假,非0为真】,int a = 0;那么 !a 等于 1

    sizeof(计算占用内存大小) ->
            1、sizeof(a);计算变量可以省略括号
            2、sizeof(int);计算类型不可以省略括号
            3、sizeof()括号里的表达式,是不参与运算的,例如:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main() {
    4. int a = 10;
    5. short s = 0;
    6. printf("%d\n", sizeof(s = a + 5));
    7. printf("%d",s);
    8. return 0;
    9. }

            1、第一个输出 sizeof(s = a + 5) 等于 2,因为最终的结果是存放到 short 里所以占用的内存空间是 2 byte

            2、第二个输出的 s 仍然是 0,因为 sizeof() 括号里的表达式不参与运算,所以 s 的值不会发生变化

    知识扩展:当去掉数组名后剩下的就是数组的类型,比如 int arr[10]; 他的数组类型就是 int [10]

    逻辑运算符中:

    &&(与):如果 && 左边计算出来为 0(假) ,那么后面的将不再计算,直接得出 0(假)

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main() {
    4. int a = 0;
    5. int b = 2;
    6. int c = 3;
    7. int i = a++ && ++b && c;
    8. printf("%d%d%d%d", a, b, c, i);
    9. return 0;
    10. }

    直接输出1230

    || (或):如果 || 左边计算出来的结果为 1,那么将不再往后计算,直接得出结果 1(真)

    三目操作符:
            表达式1 ? 表达式2 :表达式3           (如果表达式1为真,则整个表达式的结果为表达式2,如果表示式为假则整个表达式的结果为表达式3)

    struct 结构体:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. //创建了一个结构体类型-struct Student
    4. struct Student
    5. {
    6. int age;
    7. int phone;
    8. char name[10];
    9. };
    10. int main() {
    11. //使用 struct Student 这个类型创建了一个学生变量叫做student1
    12. struct Student student1 = {18,123456789,"小明" };
    13. return 0;
    14. }

            可以把 struct Student 看成一个类型,就像是 int float char 类型一样都是一种数据类型,然后通过类型就可以定义变量 student1 就是由 struct Student 类型定义声明出来的变量,只不过赋值有多个属性值要用 { } 花括号括起来赋值。

            那么在我们声明一个struct类型的时候,不会占用内存的空间,但是一旦创建该类型的变量之后,就会向内存空间申请一块空间进行存放~

    如果想要访问该对象 student1 的属性可以通过下面三种方式去访问:
            1、通过 “ . ” 去访问,例如 student1.age
            2、通过 取地址 去访问,例如 :

    1. struct Student* ps = &student1;
    2. printf("age = %d", (*ps).age);

            3.通过  -> (这是结构体指针的操作符)去访问【当然这种方式本质上和第二种是一样的,但是会更加简洁直观一些】,例如:

    1. struct Student* ps = &student1;
    2. printf("age = %d", ps->age);

    表达式求值:

            表达式求值的顺序一部分是由操作符的优先级和结合性决定

            同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型

    隐式类型转换:

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

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

            整型提升的意义:

            表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是cpu的通用寄存器的长度

            因此,即使两个char类型的相加,在cpu执行时实际上也要先转换为cpu内整型操作数的标准长度

            通用cpu(genneral-purpose cpu)是难以直接实现两个8bit直接相加运算(虽然机器指令中可能有这种字节相加指令)所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入CPU去执行运算 

            

            如何进行整型提升呢?

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

     

            下面给大家举个例子:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main() {
    4. char a = 3;
    5. char b = 127;
    6. char c = a + b;
    7. printf("c = %d",c);
    8. return 0;
    9. }

            a、b、c 三个变量都是 char 类型【8bit】,但是赋值确实 int 类型【32bit】,
            这时候会将 int 类型从最低位强行阶截断出8位,赋给变;
            比如 3 -> 00000000 00000000 00000000 00000011 会强行截断成 00000011;
            最后在 a + b 相加的时候,由于计算机无法进行 8bit 与 8bit 数据的运算,所以会将整型提升至 32bit ,然后再进行运算;
            所以 a + b 计算得到的就是 00000000 00000000 00000000 10000010,但是存到c中的依然是被截断后的八位二进制即 10000010;
            最后执行 printf() 打印语句的时候,由于要打印出整型但是c又是char类型,所以这时候又要整形提升,变成 11111111 11111111 11111111 10000010【因为最高位是 1 所以前面补 1,有符号数在整型提升时 符号位是0就补0、符号位是1就补1,但是如果是无符号数就直接补0】
            但要知道这是补码,我们得将其 减一 再 取反 转化为十进制后得到的就是 -126,打印出来的就是 -126 了 

            

    算术转换:

            如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为 寻常算术转换

            long double

            double
            float
            unsigned long int
            long int 
            unsigned int
            int

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

            警告:但是算术转换要合理,要不然会有一些潜在的问题

    操作符的属性:

    复杂表达式的求值有三个影响的因素:

             1.操作符的优先级
             2.操作符的结合性
             3.是否控制求值顺序

    两个相邻的操作符先执行哪个?
            取决于他们的优先级。如果两者的优先级相同,取决于他们的操作符优先级【优先级高的先执行,比如说 * 比 + 优先级高,所以先算 *】 、结合性【当优先级相同时,从左向右执行】

    一些问题表达式:

            问题表达式一:a*b + c*d + e*f

            注释:代码1在计算的时候,由于比 + 的优先级高,只能保证计算是比 + 早,但是优先级并不能决定第三个 * 比第一个 + 早执行

    所以表达式的计算顺序可能是:

            a * b   ->   c * d   ->   a * b + c * d   ->   e * f   ->   a * b + c * d + e * f

            或者:

            a * b   ->   c * d   ->   e * f   ->   a * b + c * d +   e * f

            我们不要只是将他看成一个运算式子,要将他看成一个表达式,如果我们的表达式前面计算的结果会影响到后面的话,那么如果这个表达式的优先级执行顺序没法确定就会出现问题;
            所以说如果我们即使拥有优先级、结合性、求值顺序,表达式依然无法确定唯一的计算路径,那么这就是一个有问题的表达式。

    问题表达式二:c+ --c

            注释:同上,操作符的优先级只能决定自减 " -- " 的运算在 " + " 的运算前面,但是我们并没有办法得知 -> " + " 操作符的做操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的

     

    问题表达式三:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main() {
    4. int i = 10;
    5. i = i-- - --i * (i = -3) * i++ + ++i;
    6. printf("i = %d",i);
    7. return 0;
    8. }

    这个表达式每个编译器算出来的都不一样

    问题表达式四:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int fun() {
    4. static int count = 1;
    5. return ++count;
    6. }
    7. int main() {
    8. int answer;
    9. answer = fun() - fun() * fun();
    10. printf("%d\n",answer);
    11. return 0;
    12. }

            虽然说可以确定先计算 " * " 在计算" + ",但是无法确定 这三次调用 fun()函数谁先被调用执行,不同的调用顺序会导致最后计算的结果不同,所以这也是一个有问题的表达式

            总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的

  • 相关阅读:
    vue 中 style 标签中的 scoped 属性(作用域)和 lang 属性的介绍
    TensorFlow模型训练常见案例
    力扣225.用队列实现栈
    嵌入式开发:创建和使用可移植类型的7个技巧
    LeetCode使用JavaScript破解两数之和
    SQL关键字详解
    JAVA中解析package、import、class、this关键字
    期末测试——JavaScript方式练习题
    分布式搜索引擎es-3
    php服装商城网站毕业设计源码241505
  • 原文地址:https://blog.csdn.net/m0_52433668/article/details/126317452