← 返回文章列表

C 语言(四)|操作符:运算规则、优先级与常见陷阱

详细介绍C语言操作符的分类、底层逻辑、优先级结合性和表达式求值规则。

版权声明:本文为作者原创,版权归作者所有。如需转载或引用,请联系作者获得授权,并在使用时注明出处。

一、核心定位

操作符是编程的“基础工具”,贯穿变量运算、逻辑判断、代码简化、内存访问的全过程,本次复习重点是理清分类、吃透用法、掌握底层逻辑、规避常踩坑点。

回顾编程知识点时,发现自身对不少操作符的用法仍有混淆,尤其是位操作、移位操作的底层逻辑掌握不扎实,还有操作符优先级、表达式求值经常出错,因此特意梳理这份复习笔记,吃透二进制及位操作相关底层知识,掌握操作符属性及表达式求值规则,为后续编程实战(表达式编写、bug排查、底层优化)筑牢基础,也方便后续快速回顾。

结合自身复习节奏,全文按基础铺垫→分类梳理→重点突破→底层逻辑→属性与求值→坑点避坑→实战复盘展开,覆盖11个核心要点,兼顾基础与难点,以复习视角记录梳理过程和易错点,适合需要系统复习操作符的小伙伴参考。

二、基础铺垫

2.1 二进制和进制转换

核心基础:计算机底层是以二进制(0、1)存储数据的,所有操作符对数据的运算,本质都是对二进制位的操作——这是理解移位、位操作符的前提,需重点牢记。

重点进制转换:主要梳理十进制与二进制(整数+小数简要梳理)、二进制与十六进制的转换方法;其中十六进制主要用于简化二进制书写,在后续位操作场景中高频使用,是重点记忆内容。

复习技巧:无需死记复杂推导,重点牢记十进制转二进制除2取余、二进制转十进制按位加权求和,结合1-2个简单示例辅助记忆,比如将十进制5转为二进制,反复练2遍即可熟练掌握。

二进制转化成十进制

二进制:1       0       1      1
十进制:1*2^3 + 0*2^2 + 1*2^1 + 1*2^0 = 11

十进制转化为二进制

十进制:15 → 除2取余得二进制:01111 → 简化为1111(去除高位无意义0)

二进制转八进制

二进制每三位对应一位八进制;原因是八进制基数8是二进制基数2的3次方(8=2³),3位二进制数的取值范围,能完整覆盖1位八进制数的所有可能,二者一一对应,转换时直接按位映射即可。

示例:二进制101 010 111 → 按三位分组直接转换为八进制。

二进制转十六进制

与转八进制逻辑完全相同;核心原因是十六进制基数16是二进制基数2的4次方(16=2⁴)。转换规则:二进制每四位对应一位十六进制,4位二进制数的所有取值,能完整覆盖1位十六进制数的全部可能,二者一一对应,转换时直接按位映射即可。

2.2 原码、反码、补码

核心用途:解决计算机中负数的表示与运算问题,移位、位操作符的运算均基于补码进行,属于底层关键知识点,必须吃透,否则位运算易出错。

三者定义(以整数为例):结合理解整理易懂定义,方便回顾:

  • 原码:符号位(0正1负)+ 数值的二进制绝对值,简单直观但无法直接用于负数运算,这是反码、补码出现的原因。
  • 反码:正数反码 = 原码;负数反码 = 符号位不变,数值位按位取反。
  • 补码:正数补码 = 原码;负数补码 = 反码 + 1(重点);计算机实际存储和运算的是补码,所有整数运算都要基于补码分析。

易错点:负数补码转原码易出错,总结两种通用方法,可交叉验证:

  1. 补码-1得反码,反码按位取反得原码;
  2. 补码直接按位取反+1得原码。

无符号数:无符号位,每一位都是数值位,原码、反码、补码的二进制表示完全相同。

三、操作符整体分类

核心原则:按功能用途分类,每个类别明确「核心作用 + 典型代表 + 使用场景」,拒绝孤立记忆,衔接底层基础知识点,确保知识串联而非零散记忆。

3.1 移位操作符(单独分类,重点突破薄弱点)

核心定义:操作二进制位,将数据的二进制补码位向左/向右移动指定位数;仅适用于整数,浮点数使用会报错,运算基于补码进行。

核心代表及用法:结合示例记忆,避免混淆:

左移<<

左边舍弃,右边补0;等价于整数乘以2的n次方,效率高于乘法,无符号/有符号整数通用该规则。

#include <stdio.h>
int main()
{
    int num = 4;
    int n = num << 1; // 仅返回移位结果,不修改num本身
    printf("n = %d\n", n);  // 输出8
    printf("num = %d\n", num); // 输出4,原值不变
    // 若要修改原值,写num = num << 1 或 num <<= 1
    return 0;
}

右移>>

分两种实现,主流编译器默认算术右移:

  • 算术右移(主流):左边补符号位,右边舍弃;
  • 逻辑右移:左边补0,右边舍弃。需注意编译器差异,避免结果异常。

易错点:

  • 移位位数不能为负数(C语言标准未定义,不同编译器行为不一致);
  • 有符号数右移的符号位补位规则需牢记,否则易出现运算结果异常。

建议:自行推导int a=4(二进制100)左移1位、int b=-4右移1位的补码运算过程,掌握移位底层逻辑。

3.2 位操作符(&、|、^、~)

注:操作数必须是整数,运算基于补码进行。

核心定义:直接操作数据的二进制补码位,仅适用于整数;常用于底层优化、进制转换、状态判断等场景。

核心代表及用法:

  • &(按位与):对应位均为1,结果为1;否则为0;常用用途:判断某一位是否为1、清零指定位。
  • |(按位或):对应位有一个为1,结果为1;否则为0;常用用途:置1指定位。
  • ^(按位异或):重点复习操作符;对应位不同为1,相同为0;核心特性:a^a=0、a^0=a;基于特性可实现交换两个数、翻转指定位。

无临时变量交换两个整数代码:

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    printf("a = %d b = %d\n", a, b); // 输出a=20 b=10
    return 0;
}
  • ~(按位非):对应位按位取反(0变1,1变0);注意:负数补码取反后符号位会变化,运算时需关注符号位。

关键区分:易混淆位操作符(&、|)与逻辑操作符(&&、||),前者操作二进制位,后者操作逻辑值;比如判断两个数是否都为非0,应使用&&而非&。

3.3 单目操作符

核心定义:只有一个操作数,用于对单个数据进行运算;易和双目操作符混淆,需逐一梳理。

核心代表及用法:

  • 算术类:++(前置/后置自增)、--(前置/后置自减)、+(正号,可省略)、-(负号,对补码取反+1);前置/后置自增自减为重点复习内容,后续单独深度拆解。
  • 逻辑类:!(逻辑非);规则:将非0值转为0,0转为1;示例:!5=0、!0=1。
  • 取值/取地址:&(取地址,获取变量内存地址)、*(取值,通过地址获取变量值);需结合指针基础理解,避免误用。
  • 强制转换:(类型)(将一个数据强制转为指定类型);注意:浮点数转整数会舍弃小数部分,存在精度丢失问题,需谨慎使用。
  • sizeof:计算变量/类型所占内存大小(单位:字节);易混点:与strlen区分——sizeof计算内存大小,strlen计算字符串长度(不包含结束符'\0')。

3.4 逗号表达式(单独分类,明确用法,记录易错点)

核心定义:用逗号,连接多个表达式,从左到右依次执行,整个表达式的结果是最后一个表达式的值。

核心特性:优先级最低,常用于简化代码(如for循环的初始化/更新部分)。

示例:

int a=1, b=2;
int c = (a++, b++, a+b); // 执行顺序:a=2→b=3→c=5,最终结果为5

易错点:易误将前面表达式的结果作为最终结果;前面的表达式仅执行,不影响最终结果,最终结果仅由最后一个表达式决定。

3.5 访问类操作符(下标 + 函数调用 + 结构成员,结合实战梳理)

3.5.1 下标访问[]

用于访问数组元素,格式:数组名[下标];核心知识点:本质等价于*(数组名+下标),结合指针基础理解数组访问逻辑。

易错点:下标从0开始,编译器不检查下标越界,运行时易出现异常,需注意判断下标范围。

3.5.2 函数调用()

用于调用函数,格式:函数名(参数列表);参数列表可为空(无参函数),但必须保留括号,遗漏会导致编译报错。

3.5.3 结构成员访问操作符(两种形式)

两种形式易混淆,结合示例记忆,核心用于访问自定义结构体的成员属性;先明确结构体的基础概念:

结构体基础

核心作用:把不同类型的变量打包成一个整体,解决普通变量只能单独存储单一类型数据的问题;比如描述学生(学号int、姓名char[]、成绩float),可将多个不同类型属性整合为“学生类型”,方便管理。

定义原因:实际编程中描述的事物均为多属性,普通变量会让属性相互独立,无法体现“属于同一事物”的关联,结构体可解决该问题。

核心定义:关键字struct,分两步:先声明结构体类型(告诉编译器自定义类型的结构),再定义该类型的变量(与int a定义格式一致)。

结构体完整示例代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h> // 字符串赋值需用到strcpy函数

// 声明书本结构体(用于嵌套)
struct Book
{
    // 书本属性
    char bname[20]; // 书名
    float price;    // 价格
}B1; // 声明类型同时定义全局变量B1

// 声明学生结构体(嵌套Book结构体)
struct MyStruct
{
    // 学生属性
    char name[20]; // 名字
    short age;     // 年龄
    char sex[10];  // 性别
    char id[20];   // 学号
    struct Book b; // 支持嵌套结构体
}A1; // 声明类型同时定义全局变量A1

int main()
{
    // 定义局部结构体变量并整体初始化(成员顺序需与声明一致)
    struct MyStruct A2 = { "zhangsan",19,"男","2025345",{"C语言",22.0}};
    printf("学生A2的信息:\n");
    printf("A2学生的书本名称:%s\n", A2.b.bname); // 嵌套结构体成员访问:变量.嵌套结构体.成员

    // 给全局结构体变量A1赋值并读取
    printf("请输入A1学生信息:\n");
    printf("姓名:");
    scanf("%s", A1.name); // 字符数组直接传名,无需&
    printf("年龄:");
    scanf("%hd", &A1.age); // short类型用%hd格式符
    printf("性别:");
    scanf("%s", A1.sex);
    printf("A1的性别:%s\n",A1.sex);
    
    return 0;
}

结构体使用关键注意点:

  • 字符串成员必须用字符数组(如char name[20]),新手阶段暂不使用char *name(涉及指针);
  • 字符数组赋值不能直接用=(如A1.name = "张三"是错误的),需用strcpy函数,且必须包含头文件#include <string.h>。

结构体成员访问操作符详解

  • .(点操作符):用于结构体变量访问成员;格式:结构体变量.成员名;示例:stu.name、A2.b.price。
  • ->(箭头操作符):用于结构体指针访问成员;格式:结构体指针->成员名;等价于:(结构体指针).成员名;示例:p->name 等价于 (p).name(p为结构体指针)。

3.6 其他常用操作符(补充完善,避免遗漏)

  • 赋值操作符:=(基础赋值)、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=(复合赋值);等价规则:复合赋值 = 运算 + 赋值,如a += b等价于a = a + b,简化代码书写。
  • 关系操作符:>、<、>=、<=、==(等于判断)、!=(不等于);作用:判断大小/相等关系,返回布尔值(0或非0),用于条件判断;易混点:==(判断相等)与=(赋值),后续重点梳理。
  • 逻辑操作符:&&(逻辑与)、||(逻辑或);用于组合条件判断,具有短路求值特性(可简化代码、避免无效运算),后续单独拆解。
  • 三元运算符:condition ? expr1 : expr2;用于简化简单的if-else语句;规则:condition为真,执行expr1;为假执行expr2;示例:求两数最大值max = a > b ? a : b。

四、操作符的核心属性(重点补充:优先级、结合性)

核心意义:优先级和结合性直接决定表达式的运算顺序,是表达式求值经常出错的核心原因;复习重点是牢记高频优先级 + 结合性规则,避免写错表达式。

4.1 优先级

定义:多个操作符共存于一个表达式时,优先级高的先运算;类比数学中的“先乘除后加减”。

高频优先级梳理(从高到低):

  • 最高优先级:()(括号,可强制改变运算顺序)、[]、->、.(访问类操作符);技巧:不确定运算顺序时,加括号即可,安全且清晰。
  • 核心顺序:单目操作符 > 算术操作符 > 关系操作符 > 逻辑操作符 > 赋值操作符 > 逗号表达式(最低)。

易错点:

  • 单目操作符优先级高于算术操作符(如!a + b,先算!a再算加法);
  • 赋值操作符优先级低于关系操作符(如a = b == c,先算b==c再将结果赋值给a)。

4.2 结合性

定义:多个优先级相同的操作符共存时,结合性决定运算方向(从左到右/从右到左);与优先级共同决定表达式运算顺序,缺一不可。

高频结合性规则:

  • 从左到右(左结合):算术、关系、逻辑、逗号、访问类、大部分双目操作符;示例:a + b + c、a > b > c,按从左到右运算。
  • 从右到左(右结合):单目操作符、赋值操作符、三元运算符;示例:a = b = c(先算b=c再算a=b)、+++a(等价于++(+a))。

实用技巧:不确定运算顺序时,直接加括号(()优先级最高),避免依赖优先级和结合性记忆出错,这是写代码的通用规范。

五、表达式求值

核心:基于操作符的优先级、结合性,结合底层逻辑(补码运算),分析表达式的执行顺序和结果;重点解决“求值异常”问题,避免逻辑错误。

5.1 求值的三大依据

  1. 操作符优先级;
  2. 操作符结合性;
  3. 操作数的求值顺序;重点:多数操作符未定义操作数求值顺序(如a++ + ++a),不同编译器运算结果不同,需避免写出此类代码。

5.2 重点梳理不同类型表达式求值

  • 含单目操作符的表达式:结合优先级、结合性分析执行顺序;示例:++a + b,先算++a再算加法。
  • 含位操作、移位操作的表达式:核心步骤——先将操作数转为补码,再按位/移位规则运算,最后转回原码得到结果;需结合补码知识分步推导。
  • 含逗号、赋值操作的表达式:明确逗号表达式“从左到右执行,结果为最后一个表达式的值”;牢记赋值操作的右结合特性;示例:a = (b++, c++, b+c),先执行逗号表达式,再将结果赋值给a。

5.3 关键易错点

避免写出“操作数求值顺序未定义”的表达式(如a = ++a + 1、a++ + ++a),此类代码可移植性差,不同编译器运行结果不同,极易出现bug。

六、重点操作符深度拆解

原则:聚焦移位、位、单目、访问类等薄弱、易出错的操作符,结合底层基础和操作符属性,搭配“用法 + 示例 + 易错点”,确保吃透核心逻辑。

6.1 移位操作符 & 位操作符深度拆解

  • 移位操作符:补充负数移位示例(如int a=-4,a>>1的补码运算过程),分步推导补码移位逻辑,明确算术右移的符号位补位规则,理解原理而非死记结果。
  • 位操作符:重点拆解^(按位异或),梳理核心应用场景:
  • 交换两个数(无临时变量);
  • 翻转指定位;
  • 实战延伸:位操作符的底层优化技巧,示例:判断奇数用a & 1(效率高于a % 2 == 1),因位运算是CPU直接支持的底层运算,速度更快。

6.2 单目操作符深度拆解(易混淆点突破)

  • 自增自减(++/--):核心区分——前置先运算再使用,后置先使用再运算;示例:int a=2; int b=a++ + ++a;,不同编译器因操作数求值顺序未定义,结果不同,避免依赖固定结果写代码。
  • **sizeof与*的区别**:示例:int a[5];,sizeof(a)为数组总大小(54=20字节,int占4字节),sizeof(a)为首元素大小(4字节),明确二者计算对象的差异。
  • 强制转换:演示风险点——精度丢失(int a=3.14;结果为3)、指针强制转换风险((int*)100;将数值转为指针),提醒自己谨慎使用。

6.3 结构成员访问操作符拆解(贴合实战,解决混淆)

核心示例:定义学生结构体,分别用结构体变量和结构体指针访问成员,验证等价关系:p->name ≡ (*p).name(p为结构体指针)。

核心规则:结构体变量用.,结构体指针用 ->,牢记无混淆。

易错点:结构体指针访问成员时误用.(如p.name),会直接编译报错,写代码时需反复检查。

七、高频坑点 & 易错点总结

核心:梳理写代码、复习时经常用错、混淆的点,每个坑点搭配“错误示例 + 正确用法”,结合底层逻辑和操作符属性加深记忆,减少bug。

坑点1:混淆位操作符(&、|)与逻辑操作符(&&、||)

  • 错误示例:if(a & b)(本意判断a、b均非0);
  • 正确用法:if(a && b);
  • 核心区别:前者操作二进制位,后者操作逻辑值(0/非0)。

坑点2:移位操作符的误用

  • 错误行为:移位位数为负数、忽略有符号数右移的符号位补位规则;
  • 后果:不同编译器行为不一致,运算结果异常;
  • 规避:移位位数为非负整数,有符号数右移牢记算术右移规则。

坑点3:原码、反码、补码混淆

  • 错误行为:用原码直接进行负数运算;
  • 正确用法:所有整数运算(移位、位操作、算术运算)均基于补码进行;
  • 规避:运算前先将操作数转为补码,运算后再转回原码得到结果。

坑点4:操作符优先级/结合性记忆错误

  • 错误示例:a = b + c * d误先算b+c;
  • 解决方案:不确定运算顺序时,直接加括号,安全且清晰。

坑点5:单目操作符与双目操作符混用

  • 错误示例:++a + b误先算a+b再自增;
  • 规避:牢记单目操作符优先级高于算术等双目操作符。

坑点6:结构成员访问操作符误用

  • 错误示例:结构体指针用.(p.name)、结构体变量用 ->(stu->name);
  • 核心规则:指针用 ->,变量用.。

坑点7:逗号表达式结果误用

  • 错误示例:int c = (a++, b++);误将a++结果作为c的值;
  • 核心规则:逗号表达式结果为最后一个表达式的值(此例为b++)。

坑点8:写出操作数求值顺序未定义的表达式

  • 错误示例:a++ + ++a、a = ++a + 1;
  • 后果:不同编译器结果不同,代码可移植性差;
  • 规避:坚决避开此类表达式,拆解为多条语句书写。

八、复习复盘 & 实战小练习

8.1 核心知识点复盘

  • 底层基础:二进制转换、原码/反码/补码是移位、位操作符的核心前提,补码运算规则为底层运算基础,需牢记。
  • 操作符分类:掌握移位、位、单目、访问类、逗号表达式的用法,能区分易混淆类别,快速选择合适的操作符。
  • 核心属性:优先级、结合性决定表达式运算顺序,记住高频规则,不确定时加括号。
  • 表达式求值:掌握求值方法,避开未定义求值顺序的陷阱,结合底层逻辑分步分析。

8.2 简单实战练习

练习1:进制转换 + 补码运算

将十进制-5转为二进制补码,计算补码左移1位、右移1位的结果,再转回十进制,检验补码和移位操作的掌握程度。

练习2:位操作符应用

用按位异或交换两个整数,用按位与判断一个数是否为奇数,手写代码检验用法,熟悉底层优化技巧。

练习3:单目操作符 + 表达式求值

分析int a=3; int b=++a + a-- + sizeof(a);的结果,结合优先级、结合性分步推导。

练习4:访问类操作符

定义结构体指针,用.和->两种方式访问其成员并赋值,检验结构成员访问操作符的掌握。

练习5:优先级/结合性

修正表达式int a=2+3*4>5&&6;的写法,明确运算顺序并计算结果。

练习6:坑点排查

手写一段包含位操作符、结构成员访问、表达式求值陷阱的错误代码,找出错误并修正,检验高频坑点的掌握。

九、总结

延伸思考:操作符不仅是编程的基础工具,其底层逻辑(补码、位运算)更是后续学习指针、内存优化、底层开发的核心;本次复习到位,能大幅提升后续编程效率和代码健壮性,为更难的知识点学习筑牢基础。

如果在操作符复习中也有混淆的知识点、常踩的坑,欢迎补充交流,分享位操作符、表达式求值的实用技巧,互相巩固复习、查漏补缺、共同进步。

补充:按位异或交换两个数的底层逻辑解析

要理解异或交换代码,需先掌握异或运算的3个核心特性(交换逻辑的基础):

异或运算(^)的核心特性

异或运算规则:两个二进制位相同,结果为0;两个二进制位不同,结果为1(简单记:同0异1)。基于规则推导的3个关键特性(对任意整数x、y都成立):

  • 自反性:x ^ x = 0(一个数和自己异或,结果一定是0);
  • 恒等性:x ^ 0 = x(一个数和0异或,结果还是它自己);
  • 交换律/结合律:x ^ y = y ^ x、x ^ y ^ z = x ^ (y ^ z)(异或的运算顺序不影响结果)。

逐行拆解交换逻辑(带变量值推导)

初始值:a = 10,b = 20;用A表示初始a(10),B表示初始b(20),区分“原始值”和“更新后的值”。

  1. 第一步:a = a ^ b执行后:a = A ^ B(10^20),b仍为原始值B(20),未被修改;
  2. 第二步:b = a ^ b代入a的新值:b = (A^B) ^ B → 结合结合律+自反性:A ^ (B^B) = A ^ 0 = A;本质:将原始a的值(10)赋给了b,此时b=10(A),a=10^20(A^B);
  3. 第三步:a = a ^ b代入当前a和b:a = (A^B) ^ A → 结合交换律+自反性:B ^ (A^A) = B ^ 0 = B;本质:将原始b的值(20)赋给了a,此时a=20(B),b=10(A),交换完成。

具体数值验证(十进制)

已知:10 ^ 20 = 30、30 ^ 20 = 10、30 ^ 10 = 20;

  • 第一步:a = 10^20 = 30 → a=30,b=20;
  • 第二步:b = 30^20 = 10 → a=30,b=10;
  • 第三步:a = 30^10 = 20 → a=20,b=10。

异或交换的特点

优点:

  • 无临时变量:不需要定义第三个变量(如temp),节省少量内存空间;
  • 运算效率高:位运算是CPU直接支持的底层运算,比普通临时变量交换法速度更快。

注意点:

交换的两个变量必须是不同的内存地址!如果a和b指向同一块内存(如数组中交换同一个下标元素、a = &b),执行后变量值会变为0(第一步a=a^b就成了x^x=0,后续步骤均为0)。

开发建议

实际开发中,临时变量交换法(temp=a;a=b;b=temp)可读性更高,无内存地址限制,是更通用的方式;异或交换法仅适用于追求极致性能的场景。

拓展:位操作符经典笔试应用

n = n & (n - 1)是二进制位操作的经典技巧,核心作用:消除数字n的二进制补码中「最右侧的那个1」,每执行一次消去一个1,直到n变为0。

应用1:统计二进制中1的个数

方法一(基础版): 就像求一个十位数的方法一样,通过不断/10的方法,我们不断/2,直到余数为0(有局限,适用于正数)

#include <stdio.h>
int main() {
    int a = 10;
    scanf("%d", &a);
    int sum = 0;
    while (a) {
        if (a % 2 == 1) {
            sum++;
        }
        a /= 2;
    }
    printf("Sum: %d\n", sum);
    return 0;
}

输入一个正数5(101,原码等于补码),结果为2是正确的。但如果输入-1呢?

  • 原码:1000 0000 0000 0000 0000 0000 0000 0001(最高位1是负号,其余是数值位)
  • 反码:1111 1111 1111 1111 1111 1111 1111 1110(符号位不变,数值位取反)
  • 补码:1111 1111 1111 1111 1111 1111 1111 1111(反码+1)

代码在逻辑和实际运行显然都是错误的!

方法二(改进局限版): 任何一个数&1得到二进制最低位,然后>>1是计算机中提取二进制每一位的经典方法,核心是通过 &1 取二进制最低位、>>1 右移舍弃最低位,循环执行就能从低位到高位依次拿到一个数的所有二进制位,不管正数还是负数(按补码处理)都适用。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {
    int a = 0;
    scanf("%d", &a);
    int count = 0;
    for (int i = 0; i < 32; i++) {  // 考虑负数的情况,循环32次
        if (a & 1) {  // if((a & 1) == 1)
            count++;
        }
        a >>= 1;
    }
    printf("Sum: %d\n", count);
    return 0;
}

这样每次就要循环一遍,效率低下!

方法三(效率最高):

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 方法三:n = n & (n-1) 消去最右侧1,循环次数=1的个数
int main() {
    int a = 0;
    scanf("%d", &a);
    int count = 0;
    while (a) {
        a = a & (a - 1);  // 每次消去最低位的1
        count++;          // 消去一次则计数+1
    }
    printf("Sum: %d\n", count);
    return 0;
}

应用2:判断一个数是否是2的次方数

核心原理:2的次方数的二进制补码中仅有1个1;若n是2的次方数,则n & (n - 1) == 0(唯一的1被消去,结果为0)。判断条件:if ((n & (n - 1)) == 0) → 成立则为2的次方数。

拓展:二进制位置0/置1的实现

需求:将13的二进制序列的第5位修改为1,再改回0(位序从0开始计数)。

步骤分析:

  1. 13的32位二进制(原/反/补码):0000 0000 ... 0000 1101;
  2. 第5位置1:需构造掩码B=1<<4=16(二进制0000 0000 ... 0001 0000),通过a |= B置1(按位或可保留原有位,指定位置1);
  3. 第5位改回0:构造掩码~B(二进制1111 1111 ... 1110 1111),通过a &= ~B置0(按位与可保留原有位,指定位置0)。

实现代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
    int a = 13;
    printf("原数为%d\n", a); // 输出13
    int b = 1 << 4; // 构造第5位为1的掩码,16
    a |= b; // 第5位置1
    printf("第5位修改为1后为%d\n", a); // 输出29
    a &= (~b); // 第5位改回0
    printf("第5位改回0后为%d\n", a); // 输出13
    return 0;
}