首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C语言有符号(signed)与无符号(unsigned)区别,一篇看懂不踩坑

C语言有符号(signed)与无符号(unsigned)区别,一篇看懂不踩坑

作者头像
早起的鸟儿有虫吃
发布2026-05-08 16:28:23
发布2026-05-08 16:28:23
2730
举报

文章大纲

1

开篇引入:为什么要区分有符号和无符号?

2

基础用法:typedef统一定义有符号/无符号64位整数

3

核心差异1:C语言long long的平台隐患(对比Rust优势)

4

核心差异2:存储规则——有符号用补码,无符号纯数值

5

关键场景:-1的移位操作(右移/左移对比,逐位演示)

6

常见误区澄清(3个高频踩坑点)

7

总结:记住3个关键点,避免出错

正文内容

写C语言时,很多人分不清signed和unsigned的区别, 看似只是多了个前缀,实际踩坑后可能导致程序崩溃、数据错乱。

例如:

事件:Linux 内核发现一个严重漏洞,本地用户可以通过特制的系统调用获得 root 权限。

原因:内核代码中,一个有符号 int无符号 size_t进行比较。有符号数被隐式转成无符号数后变成超大正数,绕过了边界检查,导致缓冲区溢出。

后果:影响全球数十亿台 Linux 服务器,包括阿里云、腾讯云等主流云厂商都紧急发布了补丁。

代码语言:javascript
复制
unsigned char a = 255;  // 二进制:1111 1111(8个1)
signed char b = a;       // 隐式转换,位模式不变
printf("%d", b);         // 输出:-1

今天用最通俗的语言+逐位演示,把核心区别讲透

一、基础用法:typedef统一定义,规范编程

实际开发中,我们常用typedef统一定义有符号和无符号整数,避免重复书写,同时提升代码可读性,比如:

1

定义有符号64位整数:typedef signed long long WangS64_t;

2

定义无符号64位整数:typedef unsigned long long WangU64_t;

这样后续使用时,直接用WangS64_t、WangU64_t,就能明确整数类型和位数,减少出错

二进制中无符号与有符号整数的奇妙转换: 从 4294967295 到 -1

第一步:先明确 8 位整数的范围

类型

范围

存储规则

8 位无符号(unsigned char)

0 ~ 255

所有位都是数值位

8 位有符号(signed char)

-128 ~ 127

最高位是符号位(1 = 负,0 = 正),用补码存储

第二步:8 位版的 4294967295和 -1

8 位无符号的最大值是 255,对应 32 位的 4294967295。 8 位有符号的 -1,补码也是 全 1

8 位版代码:

代码语言:javascript
复制
unsigned char a = 255;  // 二进制:1111 1111(8个1)
signed char b = a;       // 隐式转换,位模式不变
printf("%d", b);         // 输出:-1

第三步:逐位看发生了什么

1. a 的存储(unsigned char)

代码语言:javascript
复制
二进制:1111 1111无符号解读:1×2⁷ + 1×2⁶ + ... + 1×2⁰ = 255

✅ 无符号数,所有位都是数值,直接算就是 255。

2. 赋值给 b(signed char)

关键:赋值时只拷贝二进制位,不改变位模式!

代码语言:javascript
复制
b 的二进制:还是 1111 1111(和 a 完全一样)

✅ 硬件存储的 0/1 序列没有任何变化

3. 解读 b(signed char)

现在用有符号补码规则解读 1111 1111

1

最高位是 1 → 是负数

2

补码转原码(负数的补码转原码:先减 1,再取反):

1

补码:1111 1111

2

减 1:1111 1110(反码)

3

取反:1000 0001(原码,最高位 1 表示负)

3

原码转十进制:-1

第四步:套回 32 位(你的原题)

32 位原理和 8 位完全一样,只是位数从 8 位变成 32 位:

1

unsigned int a = 4294967295;

1

二进制:11111111 11111111 11111111 11111111(32 个 1)

2

无符号解读:2³² - 1 = 4294967295

2

int b = a;

1

二进制:还是 32 个 1(位模式不变)

3

printf("%d", b);

1

有符号补码解读:最高位 1 是负数,补码转原码后是 -1


终极总结(背会这 3 句)

1

赋值只拷贝位模式:unsigned 转 signed 时,硬件存储的 0/1 序列完全不变

2

解读规则决定数值:同一个二进制,unsigned 当纯数值读,signed 当补码读

3

全 1 的特殊情况:全 1 的二进制,unsigned 是最大值,signed 是 -1


通俗比喻(一秒懂)

二进制 = 一串摩斯密码unsigned/signed = 两本密码本

同一串摩斯密码(全 1)

用 unsigned 密码本译:“最大值”

用 signed 密码本译:“-1”

密码没变,只是译法变了

需要我用同样的方式,

解释为什么 int x = 2147483647; x << 1 会变成负数吗?

补码是 有符号整数(signed)的通用存储规则不仅针对负数, 正数也有补码,只是规则更简单

. 补码的核心作用

计算机用补码存储有符号整数,目的是将 “减法运算” 转化为 “加法运算”,简化硬件设计(无需单独设计减法电路)。

2. 补码规则(分正数、负数)

正数的补码:和它的 “原码”(二进制原始表示)完全一样

示例(8 位有符号):+5 的原码是 0000 0101,补码也是 0000 0101

负数的补码:有固定计算步骤(对应你 canvas 中提到的 “补码转原码” 逆操作)

1

先写出负数对应的正数的原码(比如 - 1 对应 + 1,原码是 0000 0001

2

对原码 “按位取反”(0 变 1,1 变 0),得到反码(1111 1110

3

反码加 1,得到补码(1111 1111

二、核心差异1:C语言long long的平台隐患,Rust如何规避?

这是C语言的一个隐形坑——long long的位数不固定,而Rust从根源上解决了这个问题。

C语言标准(C99/C11/C17)规定:long long至少是64位,但没有强制必须是64位,也没有规定上限。

这就导致:不同平台(比如x86_64和某些嵌入式芯片)上,long long的位数可能不一样,进而引发严重问题:

比如跨平台传输数据时,用long long直接序列化,不同平台读出来的结果完全不同,导致协议解析失败。

而Rust的i64/u64,在所有平台都是固定64位,无需额外头文件,也不用担平台差异,而且禁止隐式类型转换,必须用as显式转换,更安全。

三、核心差异2:存储规则——有符号用补码,无符号纯数值

计算机存储整数时,有符号和无符号的规则完全不同,这是所有差异的根源,用8位二进制举例(32/64位原理一致):

1

有符号数(signed):用补码存储,最高位是符号位(1=负数,0=正数);

2

无符号数(unsigned):所有位都是数值位,没有符号位,只能表示非负数。

重点:-1的有符号存储(8位)和255的无符号存储,二进制完全一样,都是11111111,只是解读规则不同。

四、关键场景:-1的移位操作,看清两者区别

移位操作(左移<<、右移>>)是最能体现两者差异的场景,尤其是-1的移位,记住两个核心规则:

1. 右移:算术右移(有符号)vs 逻辑右移(无符号)

先记铁规则:

有符号数右移:算术右移,高位补符号位(负数补1,正数补0);

无符号数右移:逻辑右移,高位补0。

逐位演示-1(8位)右移1位:

原始二进制:11111111(有符号是-1,无符号是255)

有符号右移1位:高位补1,结果还是11111111,十进制还是-1(无论右移多少位,永远是-1);

无符号右移1位:高位补0,结果是01111111,十进制是127。

2. 左移:操作相同,解读不同

左移规则对两者完全一致:整体左挪1位,高位丢弃,低位补0。

还是-1(8位)左移1位:

原始二进制:11111111,左移后都是11111110;

有符号解读:按补码规则,结果是-2;

无符号解读:按纯数值规则,结果是254。

简单说:左移操作一样,二进制相同,但十进制不同,只因为解读规则不一样。

五、C语言代码验证(极简版,手机可看)

用一段简单代码,直观看到差异:

代码语言:javascript
复制
#include <stdio.h>
int main() {
    signed char a = -1;          // 有符号8位
    unsigned char b = (unsigned char)-1;  // 无符号8位,值为255

    printf("a = %d\n", a);          // 输出 -1
    printf("b = %u\n", b);          // 输出 255
    printf("a << 1 = %d\n", a << 1);  // 输出 -2
    printf("b << 1 = %u\n", b << 1);  // 输出 254
    return 0;
}

运行结果和我们演示的完全一致,建议动手试一次,印象更深刻。

六、常见误区澄清(3个高频踩坑点)

1

误区:左移有算术和逻辑之分

正解:硬件层面,算术左移和逻辑左移是同一个操作,都是低位补0、高位丢弃。

1

误区:有符号数左移会保留符号位

正解:所有位一起移动,符号位也会被移走,可能导致正负性变化。

1

误区:二进制相同,十进制就该相同

正解:二进制只是存储形式,解读规则(有符号补码、无符号纯数值)决定最终结果。

七、总结:3个关键点,记牢不踩坑

1

存储规则:有符号用补码(含符号位),无符号纯数值(无符号位);

2

移位差异:右移有区别(算术补符号位,逻辑补0),左移操作相同、解读不同;

3

平台隐患:C语言long long位数不固定,建议用typedef+stdint.h固定类型,或参考Rust的固定位数设计。

其实有符号和无符号的区别,核心就是“解读规则”和“移位方式”,搞懂这两点,就能避开大部分坑,新手也能轻松上手。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章大纲
  • 正文内容
    • 一、基础用法:typedef统一定义,规范编程
    • 8 位版代码:
    • 1. a 的存储(unsigned char)
    • 2. 赋值给 b(signed char)
    • 3. 解读 b(signed char)
  • 通俗比喻(一秒懂)
    • 二、核心差异1:C语言long long的平台隐患,Rust如何规避?
    • 三、核心差异2:存储规则——有符号用补码,无符号纯数值
    • 四、关键场景:-1的移位操作,看清两者区别
    • 五、C语言代码验证(极简版,手机可看)
    • 六、常见误区澄清(3个高频踩坑点)
    • 七、总结:3个关键点,记牢不踩坑
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档