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

194 阅读18分钟

​​​「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。

前言:

本章将对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 按位与,为真则说明为1,count++;

③ 如果为假,进入下一次循环,最后打印出 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 + 5,s 不应该是 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;
}

参考资料:

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

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

📌 本文作者: 王亦优

📃 更新记录: 2021.6.9

勘误记录:

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

本章完。