zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【维生素C语言】第五章 - 操作符

C语言 操作符 第五章
2023-09-14 09:15:58 时间

前言:

本章将对C语言操作符进行深度的讲解,将每种操作符都单独拿出来精讲。最后添加了些简单的练习题,并配有详细解析。


一、算术操作符

0x00 概览

📌 注意事项:

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

      ② 对于 / 操作符,如果两个操作数 都为整数执行整数除法

      ③ 对于 / 操作符,只要有浮点数出现 ,执行的就是浮点数除法;

      ④ 对于 % 操作符的两个数 必须为整数

0x01 整数除法

📚  定义:对于 / 操作数,如果两个操作数都为整数,执行整数除法;

❓   整数除法:即一个整数除以另一个整数结果为只保留整数;

💬 代码演示:

int main()
{
    int a = 5 / 2; // 5÷2 = 商2余1
    printf("a = %d\n", a); // 👈 输出的结果是什么?

    return 0;
}

🚩 运行结果: a = 2

0x02 浮点数除法

📚  定义:只要有浮点数出现,执行的就是浮点数除法;

❓   浮点数除法:结果会保留小数部分( 给定对应的%前提下 );

💬 代码演示:

int main()
{
    double a = 5 / 2.0; // 5÷2 = 2.5,有1个浮点数,条件就成立,执行浮点数除法
    printf("a = %lf\n", a); // 👈 输出的结果是什么?

    return 0;
} 

🚩 运行结果: a  = 2.500000

0x03 取模操作符

📚  定义:取模运算即 求两个数相除的余数 ,两个操作数必须为非0整数;

📌  注意事项:

      ① 两个操作数必须为整数;

      ② 两个操作数均不能为0(没有意义);

💬 代码演示:

int main()
{
    int a = 996 % 10; // 996 mod 10 = 6
    int b = 996 % 100; // 996 mod 100 = 96
    printf("%d\n", a);
    printf("%d\n", b);

    return 0;
}

🚩  运行结果:6   96

❌  错误演示:

int main()
{
    double a = 5 % 2.0; // ❌ 操作数必须为整数
    printf("a = %lf\n", a);

    return 0;
} 

🚩  运行结果:error: invalid operands to binary % (have 'int' and 'double')

int main()
{
    int a = 2 % 0; // ❌ 操作数不能为0
    printf("%d\n", a);

    return 0;
} 

🚩  运行结果:warning: division by zero [-Wdiv-by-zero]

0x04 整除和浮点除的区分

💬 代码演示:我们想得到 1.2

int main()
{
    int a = 6 / 5;
    printf("%d\n", a);
    
    return 0;
}

  🚩 运行结果:  1 ( 但是运行结果为1 )

❓  难道是因为我们用的是 %d 打印的原因吗?

int main()
{
    float a = 6 / 5;
    printf("%f\n", a);
    
    return 0;
}

🚩 运行结果:  1.000000  ( 仍然不是想要的1.2,运行结果为1.000000 )

(气急败坏,无能狂怒)

💡   解析:其实问题不在于存到a里能不能放的下小数的问题,而是 6 / 5 得到的结果已经是为1了(执行的是整除);

🔑  解决方案:把6改成6.0,或把5改成5.0,也可以都改,让它执行浮点数除法;

int main()
{
    float a = 6 / 5.0;
    printf("%f\n", a);
    
    return 0;
}

🚩  运行结果:  1.200000

❓ 虽然代码可以运行,但是编译器报了一个 warning,让我们来瞅瞅是咋回事:

🔑  解析:直接写出的这个数字(6.0或5.0),编译器会默认认为它是 double 类型

                 那么计算后a的结果也会是 double 类型(双精度浮点数);

                 如果双精度浮点数的值放到一个单精度浮点数里的话,可能会丢失精度,

                 好心的编译器就发出了这样的一个警告,这个是正常的;

💡  如果你不想看到这样的警告,你可以这么做:

int main()
{
    float a = 6.0f / 5.0f; // 👈 “钦定” 为float单精度浮点数
    printf("%f\n", a);
    
    return 0;
}
int main()
{
    double a = 6.0 / 5.0;  // 👈 改成double
    printf("%lf\n", a);
    
    return 0;
}

📚  关于精度丢失的现象:

      ① 有效数字位数超过7位的时候,将会四舍五入,会丢失较多精度;

      ② 在运行较大数值运算的时候,将有可能产生溢出,得到错误的结果;

二、移位操作符

0x00 概览

📚  概念: 移位操作符分为 "左移操作符" 和 "右移操作符" ;

📌  注意事项:

      ① 移位操作符的 操作数必须为整数

      ② 对于运算符,切勿移动负数位(这是标准为定义的行为);

      ③ 左移操作符有乘2的效果,右移操作符有除2的效果(左乘2,右除2);

0x01 左移操作符

📚  移位规则:左边丢弃,右边补0 ;(左边的数给👴爬,至于爬多远,还要看操作数是多少)

💬  代码演示:

int main()
{
    int a = 2;
    int b = a << 1; // 将a的二进制位向左移动1位;
    printf("b = %d\n", b); // 4 (左移操作符有乘2的效果)
    
    /*
           00000000000000000000000000000010
         0|000000000000000000000000000010+0  (左边丢弃,右边补0)
    */
        
    return (0);
}

🚩  运行结果: b = 4

🔑  图解左移操作符:

0x02 右移操作符

📚  移位规则:两种移位规则;

      ① 算术右移:右边丢弃,左边补原符号位(通常为算术右移);

      ② 逻辑右移:右边丢弃,左边补0

📌 注意事项:

      ① C编译器中默认为算术右移,如果是 signed 有符号类型时,需要注意;

      ② 使用 unsigned 无符号类型时,算术右移和逻辑右移的结果是一样的;

int main()
{
    int a = 10;
    int b = a >> 1; // 把a的二进制位向右移动一位
    printf("b = %d\n", b); // 5 (右移操作符有除2的效果)

    /*
           00000000000000000000000000001010
          0+0000000000000000000000000000101|0
    */

    return 0;
}

🚩  运行结果:  b = 5

🔑  解析: 为了搞懂什么是算术右移,什么是逻辑右移,我们不得不了解整数的二进制表示方式:

0x03 整数的二进制表示方式(初步了解)

📚  负数-1要存放在内存中,内存中存放的是二进制的补码;

📌  整数的二进制表示形式(原反补):

      ① 原码:直接根据数值写出的二进制序列,即为原码;

      ② 反码:原码的符号位不变,其他位置按位取反,即为反码(如果不知道什么是按位取反,后面会讲);

      ③ 补码:反码 + 1,即为补码; (内存中存放的是补码

📜  -1 的原码、反码、补码:

💬 此时回到上述问题,如果右移时采用逻辑右移:

int main()
{
    int a = -1;
    int b = a >> 1;
    printf("b = %d\n", b);

    return 0;
}

🚩  运行结果:  b = -1

🔑 图解逻辑右移与算数右移:

❌ 错误演示:操作数不能是负数!

int main()
{
    int num = 10;
    num >> -1; // ❌  a<<1 ??  垃圾代码

    return 0;
}

🚩  运行结果:  warning: right shift count is negative [-Wshift-count-negative]

三、位操作符

0x00 概览

📚   位操作符:按位与、按位或、按位异或;

📌 注意事项:位操作符的 操作数必须为整数

0x01 按位与 &

📚  定义:按2进制按位与,只有对应的两个二进位都为1时,结果位才为1;(必须都为真,结果才为真

💬  代码演示:按位与的用法

int main()
{
    int a = 3;
    int b = 5;
    int c = a & b;  // a和b都为真
    printf("%d", c);

    return 0;
}

🚩  运行结果:  1

0x02 按位或

📚 定义:只要对应的两个二进位有一个为1时,结果位就为1;(只要有一个为真,结果就为真

💬 代码演示:按位或的用法

int main()
{
    int a = 0;
    int b = 5;
    int c = a | b; // a和b有一个为真
    printf("%d\n", c);
    
    return 0;
}

🚩  运行结果:  5

int main()
{
    int a = 0;
    int b = 0;
    int c = a | b; // a和b都为假
    printf("%d\n", c);

    return 0;
}

🚩  运行结果:  0

0x03 按位异或 ^

📚  定义:相同为0,相异为1;(上下相同就为假,不同为真

💡  巧记:觉得按位异或不好记? 试着这么记 👇

                                      " 这对恋人是异性恋吗?是回1,不是回0 "       0 1 是, 1 0 是, 1 1 不是, 0 0 不是;

※ 异或:a⊕b = (¬a ∧ b) ∨ (a ∧¬b) 如果a、b两个值不相同,则异或结果为1,反之结果为0;

💬  代码演示:按位异或的用法

int main()
{   
    int a = 3;
    int b = 5;
    int c = a ^ b; // a和b不同
    printf("%d\n", c);

    return 0;
}

🚩 运行结果: 6

int main()
{   
    int a = 3;
    int b = 3;
    int c = a ^ b; // a和b相同
    
    printf("%d\n", c);

    return 0;
}

🚩  运行结果:  0

0x04 位操作符的应用

📃 面试题:交换两个 int 变量的值,不能使用第三个变量;

  (即a=3,b=5,交换之后a=5,b=3)

1. 临时变量法 - 该题禁止了此方法,但是在工作中建议使用该方法;

int main()
{
    int a = 3;
    int b = 5;
    printf("交换前: a = %d, b = %d\n", a, b);
    int tmp = a; // 创建一个临时变量,存放a
    a = b; // a变为b
    b = tmp; // b变为原来的a
    printf("交换后: a = %d, b = %d\n", a, b);

    return 0;
}

🚩  运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3

2. 加减交换法 - 存在缺陷:可能会溢出(超过整型的存储极限)

int main()
{
    int a = 3;
    int b = 5;
    printf("交换前: a = %d, b = %d\n", a, b)
    a = a + b;
    b = a - b;
    a = a - b;
    printf("交换后: a = %d, b = %d\n", a, b);

    return 0;
}

🚩  运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3

🔑  解析:第一步: 3 + 5 = 8,第二步: 8 - 5 = 3,第三步: 8 - 3 = 5,此时,a = 5, b = 3 ;

3. 异或交换法 - 缺点:可读性差,执行效率低下;

int main()
{
    int a = 3;
    int b = 5;
    printf("交换前: a = %d, b = %d\n", a, b);
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    printf("交换后: a = %d, b = %d\n", a, b);

    return 0;
}

🚩  运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3

🔑 解析:

💬 编写代码实现:求一个整数存储在内存中的二进制中1的个数

1. 一般解法 - 模除

int main()
{   
    int num = 0;
    int count = 0;
    scanf("%d", &num);
    
    /* 统计num的补码中有几个1 */
    while(num != 0) {
        if(num % 2 == 1) {
            count++;
        }
        num = num / 2;
    }
    printf("%d\n", count);

    return 0;
}

🚩  运行结果: (假设输入3)  2

🔑  解析:

 

2. 移位操作符 + 按位与 结合的方式解决

💭 思路:

      ① 利用 for 循环,循环32/64次;

      ② 每次 if 判断,将 num 右移 i 位的结果与 1 按位与,为真则说明为1count++

      ③ 如果为假,进入下一次循环,最后打印出 count 即可;

int main()
{
    int num = 0;
    int count = 0;
    scanf("%d", &num);

    int i = 0;
    /* 32位系统,至少循环32次 */
    for(i=0; i<32; i++) {
        if( ((num >> i) & 1) == 1 )  // 如果num右移i位的结果和1按位与,为真
            count++;
    }
    printf("%d\n", count);

    return 0;
}

🚩  运行结果: (假设输入3)  2

四、赋值操作符

0x00 概览

📚  用法:用来重新赋值一个变量的值;

0x01 一般赋值

📚  赋值方法:

💬 赋值操作符是个很棒的操作符,它可以让你得到一个你之前不满意的值:

int main()
{
    int weight = 120; // 体重120,不满意,我要变瘦点!
    weight = 89; // 不满意就赋值~

    double salary = 10000.0; // 我:老板!我要加薪!
    salary = 20000.0; // 老板:好的,没有问题!

    return 0;
}

0x02 连续赋值

📚  定义:连续赋值(continuous assignment),即一次性赋多个值;

📜  建议:建议不要使用连续赋值,会让代码可读性变差,而且还不容易调试;

💬  代码演示:连续赋值的使用方法;

int main()
{
    int a = 10;
    int x = 0;
    int y = 20;
    a = x = y+1;// 👈 连续赋值

    x = y+1;
    a = x;
    // 👆 这样写更加清晰爽朗而且易于调试

    return 0;
}

0x03 复合赋值符

📚  意义:复合赋值运算符是为了减少代码输入量而设计的;

📌  注意事项:

      ① x = x + 10 与 x += 10 的意义等价;

      ② 可以提高代码的整洁度,让代码更加整洁;

💬  代码演示:复合赋值符的使用方法

int main()
{
    int x = 10;

    x = x + 10; 
    x += 10; //复合赋值符的写法 (和上面是等价的)

    return 0;
}

五、单目操作符

0x00 概览

❓  什么是单目操作符?

💡  在运算中只有一个操作数的操作符,叫做单目操作符;

0x01 逻辑反操作

📚  作用:可以让真变为假,也可以让假变为真;

💬 逻辑反操作的用法:

int main()
{
    int a = 10;
    printf("%d\n", !a); // 将真变为假, 0

    int b = 0;
    printf("%d\n", !b); // 将假变为真, 1
    
    return 0;
}

🚩  运行结果:  0   1

💬 最常用的用法:

int main()
{
    int flag = 5;

    if ( flag ) //  flag != 0 -> hehe
        printf("hehe\n");  // flag为真,打印hehe
        
    if ( !flag ) // flag == 0 -> haha
        printf("haha\n"); // flag为假,打印haha

    return 0;
}

🚩  运行结果:  hehe

0x02 负值 -

📚  作用:把一个数置为负数;

💬 负值的用法:

int main()
{
    int a = 10;
    a = -a; // 在a前面放一个负号
    printf("%d", a);
    
    return 0;
}

🚩  运行结果:  -10

0x03 正值 +

📚  作用:一般都省略掉了,和数学里面一样;

💬 加号一般都不写的:

int main()
{
    int a = +5; // 一般都省略掉了,和数学里一样
    printf("%d", a);

    return 0;
}

🚩  运行结果: 5

0x04 取地址操作符 & 与 解引用操作符 *

📚  理解:

      ① 取地址操作符可以理解为取快递;

      ② 解引用操作符可以理解为拆快递;

      (指针章节会详解)

💬 用法演示:

int main()
{
    int a = 10;
    int* pa = &a; // 取地址操作符  ( 随后将地址存放在int* pa里 )
    *pa = 20; // 解引用操作符  通过p里存的值找到它所指向的对象;
    // *p就是a, 将*p赋值为20,a就会变为20;
    
    return 0;
}

🔑  解析:

      ① 首先 int* pa 是一个指针变量(如果不知道什么是指针,可以暂且理解为是一个快递包裹);

      ② 快递包裹里装的是内存地址,我们使用 取地址操作符& 取出 a 的地址,存放到这个包裹里(int* pa = &a);

      ③ 这时,我们想修改 a 的值,我们要打开包裹进行修改,可以通过 解引用操作符* a 修改为新的值(*pa = 20);

0x05 操作数的类型长度 sizeof( )

📚 作用:计算变量所占内存空间的大小,单位是字节;

📌 注意事项:

      ① sizeof 括号中的表达式不参与运算;

      ② sizeof 本质上不是函数,所以可以省略括号,但是 sizeof 后面是类型时不可以省略括号;

💬 sizeof 的用法:

int main()
{
    int a = 10;
    char c = 'a';
    char* pc = &c;
    int arr[10] = {0};

    /* sizeof 计算的变量所占内存空间的大小,单位是字节 */
    printf("%d\n", sizeof(a)); //4;
    printf("%d\n", sizeof(int)); //4;

    printf("%d\n", sizeof(c)); //1;
    printf("%d\n", sizeof(char)); //1;

    printf("%d\n", sizeof(pc)); //4;   32位系统中
    printf("%d\n", sizeof(char*)); //4;

    printf("%d\n", sizeof(arr)); //40; 4x10=40
    printf("%d\n", sizeof( int [10] )); //40;

    return 0;
}   

💬 下列代码的运行结果为什么?

int main()
{
    short s = 0;
    int a = 10;

    printf("%d\n", sizeof(s = a + 5));
    printf("%d\n", s);
}

🚩  运行结果: 2   0

❓ 为什么是 s 还是 0 呢? s = a + 5s 不应该是 15吗……

🔑  解析:15个🔨15,sizeof 括号中的表达式不参与运算!

💬 下列代码输出后 (1) (2) (3) (4) 分别是多少(32位)?

void test1(int arr[]) //传参传过来的是首元素
{
    printf("%d\n", sizeof(arr)); // (3)
}
void test2(char ch[])
{
    printf("%d\n", sizeof(ch));  // (4)
}
int main()
{
    int arr[10] = {0};
    char ch[10] = {0};
    printf("%d\n", sizeof(arr)); // (1)
    printf("%d\n", sizeof(ch));  // (2)
    test1(arr);
    test2(ch);

    return 0;
}

💡  答案:(1)40    (2)10   (3)4   (4)4

🔑  解析:

      ①  (1) 一个int型大小为4,数组大小为10,4x10 = 40,所以答案为40;

      ②  (3) 一个char型大小为1,数组大小为10,1x10 = 10,所以答案为10;

      ③  (3) (4) 数组名传参,传过去的虽然是是首元素地址,因为首元素的地址也是地址

           所以要拿一个指针来接收它。本质上,arr 和 ch 为指针,而指针的大小,

           是4个字节或者8个字节(具体是几个字节看操作系统),题目中为32位,所以答案为4;

❌ 错误示范:

int main()
{
    /* sizeof 后面是类型时不可以省略括号 */
    int a = 10;
    printf("%d\n", sizeof a );     // 可以省略 ✅
    printf("%d\n", sizeof int);    // error! 不可以省略 ❌

    return 0;
}

🚩  运行结果:  error: expected expression before 'int'    printf("%d\n", sizeof int);

0x06 按位取反 ~

📚  作用:对一个数按位取反,0 变 1, 1 变 0;

📌  注意事项:

      ① 按位取反,1~0互换,包括符号位;

      ② 按位取反后,是补码;

💬 巧用按位取反:将某一个数的二进制位从右到左数的第三个数改为1;

int main()
{
    int a = 11;
    a = a | (1<<2);

    //    00000000000000000000000000001011    11
    //  | 00000000000000000000000000000100    让他和“这个数字”按位或
    //-------------------------------------    
    //    00000000000000000000000000001111   此时这一位变成了1

    // 如何创造出“这个数字”呢?
    //    1<<2;
    //    00000000000000000000000000000001   1
    //    00000000000000000000000000000100   把他向左移动两位时1就到这了
    //    a|(1<<2)
    //    00000000000000000000000000001011
    //  | 00000000000000000000000000000100  
    //-------------------------------------
    //    00000000000000000000000000001111  
    printf("%d\n", a); //15

    a = a & ( ~ (1<<2) );
    // 如何再改回去?                    ↓ 让这一位改成0
    //    00000000000000000000000000001111    让他和0按位与
    //  | 11111111111111111111111111111011    给他按位与一个“这样的数字”
    //-------------------------------------
    //    00000000000000000000000000001011    把这一位又还原成0了

    //  1<<2,同上
    //    00000000000000000000000000000100  这个数字按位取反可以得到 ...1011
    //  ~
    //    11111111111111111111111111111011

    //  a& ~
    //    00000000000000000000000000001111   15
    //  & 11111111111111111111111111111011
    //-------------------------------------
    //    00000000000000000000000000001011   11
    printf("%d\n", a); //11
    
    return 0;
}

🚩  运行结果:  15    11

0x07 前置、后置++

📚 定义:

      ① 前置++:先加加,后使用;

      ② 后置++:先使用,再加加;

💬 代码演示:后置++的用法

int main()
{
    int a = 10;
    printf("%d\n", a++); // 后置++:先使用,再++
    printf("%d\n", a); // a此时已变为11

    return 0;
}

🚩  运行结果: 10  11

💬 代码演示:前置++的用法

int main()
{
    int a = 10;
    printf("%d\n", ++a); // 前置++:先++,再使用
    printf("%d\n", a);

    return 0;
}

🚩  运行结果: 11  11

0x08 前置、后置 --

📚 定义:

      ① 前置--:先减减,后使用;

      ② 后置++:先使用,再减减;

💬 代码演示:后置 - - 的用法

int main()
{
    int a = 10;
    printf("%d\n", a--);
    printf("%d\n", a);

    return 0;
}

🚩 运行结果:  10  9

💬 代码演示:后置 - - 的用法

int main()
{
    int a = 10;
    printf("%d\n", --a);
    printf("%d\n", a);

    return 0;
}

🚩  运行结果:  9  9

0x09 强制类型转换(type)

📚  作用:强制类型转换可以把变量从一种类型转换为另一种数据类型;

📌  注意事项:

💬 代码演示:强制类型转换的用法

int main()
{
    int a = (int)3.14;

    return 0;
}

六、关系操作符

📌  注意事项:在编程的过程中要小心 = 和 == 不小心写错,导致的错误;

💬 代码演示:一般用于条件语句中

int main()
{
    int a = 3;
    int b = 5;
    
    if(a < b) {
        ...
    }
    if(a == b) {
        ...
    }
    if(a <= b) {
        ...
    }
    if(a != b) {
        ...
    }
    
    return 0;
}

七、逻辑操作符

0x00 逻辑与 &&

📚  说明:逻辑与,a和b都为真时结果才为真;(都为真才为真

💬 代码演示:

1. a和b都为真时,结果就为真,c = 1;

int main()
{
    int a = 3;
    int b = 5;
    int c = a && b; // 逻辑与 “并且” a和b都为真时才返回真
    printf("%d\n", c);
    
    return 0;
}

🚩  运行结果: 1(真)

2. a和b只要有一个为假,结果就为假,c = 0;

int main()
{
    int a = 0;
    int b = 5;
    int c = a && b;
    printf("%d\n", c);

    return 0;
}

🚩  运行结构:  0(假)

0x01 逻辑或  ||

📚 说明:a和b有一个为真,结果就为真;(有真则为真

💬 代码演示:

1. a和b只要有一个为真,结果就为真;

int main()
{
    int a = 0;
    int b = 5;
    int c = a || b; //逻辑与 “并且” a和b都为真时才返回真
    printf("%d\n", c);

    return 0;
}

🚩  运行结果:  1 (真)

2. a和b同时为假的时候,结果才为假;

int main()
{
    int a = 0;
    int b = 0;
    int c = a || b; //逻辑与 “并且” a和b都为真时才返回真
    printf("%d\n", c); // 0

    return 0;
}

🚩  运行结果:   0 (假)

0x02 练习

📃 笔试题:(出自360)

❓  1. 程序输出的结果是什么

int main()
{
    int i = 0, a=0,b=2,c=3,d=4;
    i = a++ && ++b && d++;
    printf("a=%d\n b=%d\n c=%d\n d=%d\n", a, b, c, d);

    return 0;
}

🚩 运行结果:  a=1;b=2;c=3;d=4

🔑 解析:

首先i的初始值是0,执行i = a++ && ++b && d++ 时,先执行的是a++,a初始值为0,因为是后置++的原因,此时a仍然为0,逻辑与碰到0,就不会再往下继续执行了,所以后面的++b,d++都不算数。打印时,因为刚才a++,所以此时a=1,打印出来的结果自然是a=1,b=2,c=3,d=4;

❓  2. 程序的输出结果是什么

int main()
{
    int i = 0, a=0,b=2,c=3,d=4;
    i = a++ || ++b || d++;
    printf("a=%d\n b=%d\n c=%d\n d=%d\n", a, b, c, d);

    return 0;
}

🚩  运行结果:  a=1;b=3;c=3;d=5

🔑 解析:

i=0,执行 i = a++ || ++b || d++ 时,先执行a++,因为是后置++所以此时a还是为0,但是因为是逻辑或,会继续往下走,++b为前置++,此时b为3,为真,就不会往下继续执行了,d++不算数。打印时,因为刚才a++,d++,所以此时a=1,打印出来的结果为 a=1,b=3,c=3,d=4;

🔺 总结:

1. 逻辑与:碰到假就停;(只要左边为假,右边就不算了)

2. 逻辑或:碰到真就停;(只要左边为真,右边就不算了)

八、条件操作符

📚  定义:

      ① 表达式1的结果如果为真,计算表达式2;

      ② 如果表达式1的结果为假,计算表达式3;

📌 注意事项:三目操作符不要写的过于复杂,否则可读性会很差;

💬 代码演示:

1. if...else写法:

int main()
{
    int a = 3;
    int b = 0;
    
    if (a > 5)
        b = 1;
    else 
        b = -1;
        
    return 0;
}   

2. 将上面代码转换成条件表达式:

int main()
{
    int a = 3;
    int b = 0;
    
    b = a>5 ? 1 : -1; // 条件操作符
    
    return 0;
}

💬 使用条件表达式实现找两个数中的较大值:

int main()
{
    int a = 10;
    int b = 20;
    int max = 0;
    
    max = (a>b ? a : b );
    printf("max = %d", max);

    return 0;
}

九、逗号表达式

❓  什么是逗号表达式

💡  逗号表达式,顾名思义,用逗号隔开的多个表达式;

📚 定义:从左向右依次执行,整个表达式的结果是最后一个表达式的结果;

💬 代码演示:逗号表达式的用法

int main()
{
    int a = 1;
    int b = 2;
    int c = (a>b,  a=b+10,   a,    b = a+1);
//          无结果    12    无结果  12+1=13

    printf("%d\n", c);

    return 0;
}

🚩  运行结果: 13

💬 判断条件的逗号表达式

if(a = b + 1, c = a / 2, d > 0)  // 从左向右依次执行后,d>0则条件为真

💬 逗号表达式的应用:简化代码结构

十、下标引用、函数调用和结构成员

0x00 下标引用操作符 [ ]

💬 这个很简单,直接上代码:

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    //             0 1 2 3 4 5 6 7 8 9
    printf("%d\n", arr[4]); // 5
    //                 ↑ 这里的方块,正是下标引用操作符;
    // [] 的操作数是2个:arr,4

    return 0;
}

0x01 函数调用操作符 ( )

📚 作用:接受一个或者多个操作数;

      ① 第一个操作数是函数名;

      ② 剩余的操作数就是传递给函数的参数;

💬 代码演示:函数调用操作符

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    int a = 10;
    int b = 20;
    int Add(a, b); // 此时()为函数调用操作符;

    return 0;
}

0x02 结构成员访问操作符 - 点操作符 .

📚 作用:访问结构体成员;

😢 如果忘了什么是结构体,可以去回顾第一章(初识C语言)

     https://blog.csdn.net/weixin_50502862/article/details/115426860

💬 代码演示:点操作符的使用

struct Book {
    char name[20];
    char id[20];
    int price;
};

int main()
{
    struct Book b = {"C语言", "C20210509", 55};
    printf("书名:%s\n", b.name);
    printf("书号:%s\n", b.id);
    printf("定价:%d\n", b.price);
    
    return 0;
}

🚩 运行结果: 书名:C语言
                         书号:C20210509
                         定价:55

0x03 结构成员访问操作符 - 箭头操作符 ->

📚 作用:通过结构体指针访问成员;

💬 代码演示

1. 仍然可以用点操作符来写,但是略显冗琐;❎(可以但不推荐)

📌 注意事项:  (*p).name ✅   *p.name ❌    注意优先级问题!

struct Book {
    char name[20];
    char id[20];
    int price;
};

int main()
{
    struct Book b = {"C语言", "C20210509", 55};
    struct Book* pb = &b;
    printf("书名:%s\n", (*pb).name);
    printf("书号:%s\n", (*pb).id);
    printf("定价:%d\n", (*pb).price);
    
    return 0;
}

2. 使用箭头操作符,更加直观; ✅

struct Book {
    char name[20];
    char id[20];
    int price;
};

int main()
{
    struct Book b = {"C语言", "C20210509", 55};
    struct Book* pb = &b;
    printf("书名:%s\n", pb->name);
    printf("书号:%s\n", pb->id);
    printf("定价:%d\n", pb->price);

    return 0;
}

十一章、表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定。同样,有些表达式的操作数在求职过程中可能需要转换为其他类型。

0x00 隐式类型转换

❓ 什么是整型提升:

      ① C的整型算术运算至少以缺省整型的精度来进行的;

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

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

🔑  图解整型提升:

❓ 那么问题又来了,如何进行整型提升呢?

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

📚 整型提升讲解(请仔细看注释的步骤):

int main()
{
    // 我们发现 a 和 b 都是 char 类型,都没有达到一个 int 的大小
    // 🔺这里就会发生整型提升
    char a = 3;
    //      00000000000000000000000000000011
    //      00000011 - a  因为是char类型,所以只能放8个比特位(截断)🔪
    char b = 127;
    //      00000000000000000000000001111111
    //      01111111 - b  同上,截断,存储的是8个比特位 🔪

    char c = a + b;
    // 首先看a符号:char有符号,是正数,按照原来变量的符号位来提升
    // 然后看b符号:char有符号,是正数,提升的时候也是补0
    //      00000000000000000000000000000011 (高位补0,提升完结果还是这个)
    //   +  00000000000000000000000001111111
    // -------------------------------------
    //      00000000000000000000000010000010 (这个结果要存到c里,c里只能存8个比特位)
    // 所以进行截断 🔪
    // 10000010 - c   (C里存的)   

    
    /* 这时我们要打印它 */    
    printf("%d\n", c);
    // 🔺这时,c要发生整型提升:
    // 我们看c的符号,char有符号,是负数,高位进行整型提升,补1
    //      10000010 - c  // 然后进行整型提升
    //      11111111111111111111111110000010 (补完1 之后的结果)

    // 🔺注意:这里是负数,原反补是不相同的!
    // 打印出来的是原码,内存里的是补码,现在开始反推:
    //      11111111111111111111111110000010 (补码)
    //      11111111111111111111111110000001 - 反码(补码-1)
    //      00000000000000000000000001111110 - 原码
    //      ==  -126

    return 0;
}

🚩  运行结果:  -126

💬 整型提升的栗子1:下列代码运行的结果是什么(体会整型提升的存在)

int main()
{
    char a = 0xb6;
    short b = 0xb600;
    int c = 0xb600000;
    if(a == 0xb6)
        printf("a"); //无
    if(b == 0xb600)
        printf("b"); //无
    if(c == 0xb600000)
        printf("c"); //c
    return 0;
}

🚩 运行结果: c

❓ 为什么 a 和 b 不会被打印出来呢

🔑 解析:

      ① 因为表达式里的 a 是 char 类型,因为没有达到整型大小,所以需要进行整型提升;

      ② 提升后比较当然不会相等,所以不会打印a,short 同理,c也不会被打印;

      ③ 还有一种解释方式:char a 里面存不下,所以不是 0xb6 ,所以不打印;

💬 整型提升的栗子2:下列代码运行结果是什么(体会整型提升的存在)

int main()
{
    char c = 1;
    printf("%u\n", sizeof(c));  // 1
    printf("%u\n", sizeof(+c)); // 4  整型提升后等于计算一个整型的大小
    printf("%u\n", sizeof(-c)); // 1
    printf("%u\n", sizeof(!c)); // 4  gcc-4
    
    return 0;
}

🔑 解析:

      ① sizeof(c) ,c是char型,结果自然是1;

      ② sizeof(+c),+c参与运算了,就会发生整型提升,相当于计算了一个整型的大小,所以为4;

      ③ sizeof(-c),同上,一样的道理,所以为4;

      ③ sizeof( !c) ,这里值得一提的是,有些编辑器结果可能不是4,但是根据gcc为准,答案为4;

🔺 结论:

      ① 通过上面的例子ba,可以得到结论:到整型提升是确实存在的;

      ② 比 int 大的不需要整型提升,比 int 小的要进行整型提升;

0x02 算术转换

📚 定义:如果某个操作数的各个操作数属于不同的类型,

                那么除非其中一个操作数的转换为另一个操作数的类型,否则操作无法进行;

📚 寻常算数转换:如果某个操作数类型在下面的这个表里,排名较低,

                               那么首先要转换为另外一个操作数的类型,然后才能执行运算;

🌰 举个栗子:(如果 int 类型的变量和 float 类型的变量放在一起,这时要把 int 转换成 float)

📌 注意事项:算数转换要合理,要不然会产生潜在的问题;

💬 精度丢失问题:

int main()
{
    float f = 3.14;
    int num = f; // 隐式转换,会有精度丢失
    printf("%d\n", num); // 3

    return 0;
}

🚩   3

0x03 操作符的属性

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

      ① 操作符的优先级;

      ② 操作符的结合性;

      ③ 是否控制求值顺序;

❓  两个相邻的操作符先执行哪个?取决于他们的优先级,如果两者的优先级相同,

💬 代码演示:优先级决定了计算顺序

int main()
{
    int a = 3;
    int b = 5;
    int c = a + b * 7; // 优先级决定:先乘后加

    return 0;
}

💬 代码演示:优先级一样,此时优先级不起作用,结合性决定顺序

int main()
{
    int a = 3;
    int b = 5;
    int c = a + b + 7; // 先算左边,再算右边

    return 0;
}

📚 运算符优先级表:

操作符描述用法示例结合类型结合性是否控制求值顺序
( )聚组(表达式)与表达式相同N/A

( )函数调用rexp(rexp, ..., rexp)rexpL-R
[ ]下标引用rexp[rexp]lexpL-R

.访问结构成员lexp.member_namelexpL-R
->访问结构指针成员rexp->member_namelexpL-R

++后缀自增lexp++rexpL-R

--后缀自减lexp--rexpL-R
!逻辑反!rexprexpR-L
~按位取反~rexprexpR-L
+单目,表示正值+rexprexpR-L
-单目,表示负值-rexprexpR-L
++前缀自增++lexprexpR-L
--前缀自减--lexprexpR-L
*间接访问*rexplexpR-L
&取地址&lexprexpR-L
sizeof取其长度,以字节表示sizeof rexp szieof(类型)

rexp

R-L
(类型)类型转换(类型)rexprexpR-L
*乘法rexp*rexprexpL-R
/除法rexp/rexp

rexp

L-R
%整数取余rexp%rexprexpL-R
+加法rexp+rexprexpL-R
-减法rexp-rexprexpL-R
<<左移位rexp<<rexprexpL-R
>>右移位rexp>>rexprexpL-R
>大于rexp>rexprexpL-R
>=大于等于rexp>=rexprexpL-R
<小于rexp<rexprexpL-R
<=小于等于rexp<=rexprexpL-R
==等于rexp==rexprexpL-R
!=不等于

rexp!=rexp

rexpL-R
&位与rexp&rexprexpL-R
^位异或rexp^rexprexpL-R
|位或rexp|rexprexp

L-R

&&逻辑与rexp&&rexprexpL-R
||逻辑或rexp&&rexprexpL-R
?:条件操作符rexp?rexp:rexprexpL-R
=赋值lexp=rexprexpN/V

+=加等于lexp+=rexprexpR-L
-=减等于lexp-=rexprexpR-L
*=乘等于lexp*=rexprexpR-L
/=除等于lexp /= rexprexpR-L
%=以...取模lexp %= rexprexpR-L

<<=

以...左移lexp <<= rexprexpR-L
>>=以...右移lexp >>= rexprexpR-L
&=以...与lexp &= rexprexpR-L
^=以...异或lexp ^= rexprexpR-L
|=以...或lexp |= rexprexpR-L
,逗号rexp, rexprexpL-R

❌ 问题表达式:

❌ 非法表达式:( 出自《C和指针》)

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

    return 0;
}

🔑 解析: 堪比《茴香豆的一万种写法》,

                 这种代码,运行结果取决于环境,不要写出这种代码!


作业

0x00 选择题

❓  下面哪个是位操作符 (   );

      A. &      B. &&      C. ||      D. !

0x01 分析代码

❓  下列代码运行后的结果是什么

#include <stdio.h>

int main()
{
	int a, b, c;
	a = 5;
	c = ++a;
	b = ++c, c++, ++a, a++;
	b += a++ + c;
	printf("a = %d b = %d c = %d\n:", a, b, c);

	return 0;
}

0x02 交换两个变量(不创建临时变量)

💬 不允许创建临时变量,交换两个整数的内容;

0x03 统计二进制中1的个数

💬  输入一个整数,写一个函数返回该数32位二进制表示中1的个数,其中负数用补码表示。

    ( eg. 15    0000 1111    4个1 )

🐂  牛客网OJ链接:二进制中1的个数__牛客网

0x04 求两个数二进制中不同位的个数

💬  编程实现:两个 int(32位)整数的 m 和 n 的二进制表达中,有多少个位 (bit) 不同?

    ( eg.  输入 1999 2299   输出 7 )

🐂  牛客网OJ链接:两个整数二进制位不同个数__牛客网

0x05 打印整数二进制的奇数位和偶数位

💬  说明:获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列;


答案

0x00 第一题:选择题

💡 正确答案:A

🔑 解析:

0x01 第二题:下列代码运行结果

💡  正确答案:a = 9   b = 23   c = 8

🔑 解析:

0x02 第三题:不创建临时变量交换两个整数内容

💬 不允许创建临时变量,交换两个整数的内容;

💡 参考答案:

void Swap (
    int* pa,
    int* pb
    )
{
    *pa = *pa ^ *pb;
    *pb = *pa ^ *pb;
    *pa = *pa ^ *pb;
}

int main()
{
    int a = 10;
    int b = 20;
    printf("交换前:a = %d  b = %d\n", a, b);
    Swap(&a, &b);
    printf("交换后:a = %d  b = %d\n", a, b);

    return 0;
}

0x03 第四题:统计二进制中1的个数

💬  输入一个整数,写一个函数返回该数32位二进制表示中1的个数,其中负数用补码表示。

    ( eg. 15    0000 1111    4个1 )

💡 参考答案:

1. 模除法

int CountNum1(int n)
{
    int count = 0;
    while(n) {
        if(n % 2 == 1) {
            count++;
        }
        n /= 2;
    }
    return count;
}

int main()
{
    int num = 0;
    scanf("%d", &num);
    int ret = CountNum1(num);
    printf("%d\n", ret);

    return 0;
}

2. 移位操作符 + 按位与 结合的方式

int CountNum1(int n)
{
    int count = 0;
    int i = 0;
    for(i=0; i<32; i++) {
        if( ((n>>i) & 1) == 1) {
            count++;
        }
    }
    return count;
}

int main()
{
    int num = 0;
    scanf("%d", &num);
    int ret = CountNum1(num);
    printf("%d\n", ret);

    return 0;
}

3.  &=

int CountNum1(int n) 
{
    int count = 0;
    while(n) {
        n = n & (n - 1);
        count++;
    }
    return count;
}

int main()
{
    int num = 0;
    scanf("%d", &num);
    int ret = CountNum1(num);
    printf("%d\n", ret);

    return 0;
}

0x04 第五题:求两个数二进制中不同位的个数

💬  编程实现:两个 int(32位)整数的 m 和 n 的二进制表达中,有多少个位 (bit) 不同?

    ( eg.  输入 1999 2299   输出 7 )

1.   >> & 移位按位与

int main()
{
    int m = 0;
    int n = 0;
    scanf("%d %d", &m, &n);

    int count = 0;
    int i = 0;
    for(i=0; i<32; i++) {
        if( ((m >> i) & 1) != ((n >> i) & 1) ) {
            count++;
        } 
    }
    printf("%d\n", count);

    return 0;
}

2. 异或法,然后统计二进制中有几个1

int NumberOf1(int n) 
{
    int count = 0;
    while(n) {
        n = n & (n - 1);
        count++;
    }
    return count;
}

int main()
{
    int m = 0;
    int n = 0;
    scanf("%d%d", &m, &n);

    int count = 0;
    int ret = m ^ n; // 相同为0,相异为1
    // 统计一下ret的二进制中有几个1,就说明m和n的二进制位中有几个位置不同
    count = NumberOf1(ret);
    printf("%d\n", count);

    return 0;
}

0x05 第六题:打印整数二进制的奇数位和偶数位

💬  说明:获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列;

int main()
{
    int n = 0;
    scanf("%d", &n);
    // 获取n的2进制中的奇数位和偶数位
    int i = 0;
    // 打印偶数位
    for(i=31; i>=1; i -= 2) {
        printf("%d ", (n >> i) & 1);
    }
    printf("\n");
    // 打印奇数位
    for(i=30; i>=0; i-=2) {
        printf("%d ", (n >> i) & 1);
    }

    return 0;
}


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

比特科技. C语言基础[EB/OL]. 2021[2021.8.31]. .

📌 本文作者: Foxny

📃 更新记录: 2021.6.9

勘误记录:

📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

本章完。