零基础鸿蒙应用开发第十一节:计算机数据存储与运算
【教学目标】
- 理解计算机存储负数的核心痛点,明白为何不能直接用二进制表示负数。
- 掌握原码、反码、补码的定义、转换规则及各自的优缺点。
- 彻底搞懂补码成为现代计算机存储整数标准方案的根本原因。
- 掌握32位/64位整数的存储范围及溢出问题的产生原因与表现。
- 在ArkTS中独立验证整数的存储范围、溢出行为及补码的实际应用,建立“整数存储依赖补码”的认知。
【本节重点】
- 痛点核心:直接用二进制表示负数会导致减法运算出错,原码直观但存在致命缺陷。
- 进化路径:原码 → 反码(改进但未根治问题) → 补码(完美解决负数存储与运算)。
- 补码本质:基于“模运算”将减法转化为加法,适配计算机硬件运算逻辑。
- 存储范围:不同位数整数(8位/32位/64位)的取值边界,及溢出的底层逻辑。
- 代码实操:独立编写代码验证补码运算、整数存储上限/下限及溢出行为。
一、工程结构
本节我们将创建名为IntegerStorageDemo的工程,基于鸿蒙5.0(API12)开发,使用DevEco Studio 6.0+工具,项目结构目录如下:
IntegerStorageDemo/
├── AppScope # 应用全局配置
├── entry/
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/
│ │ │ │ ├── entryability/ # 应用入口(默认生成)
│ │ │ │ │ └── EntryAbility.ets
│ │ │ │ ├── pages/ # 页面代码
│ │ │ │ │ └── Index.ets # 核心页面(
│ │ │ │ ├── utils/
│ │ │ │ │ └── IntegerStorageTest.ets # 补码 原码 反码学习内容
│ │ │ ├── resources/ # 资源文件(默认生成)
│ │ │ └── module.json5 # 模块配置(默认生成)
二、开篇痛点:负数存储的核心难题
上一节我们知道计算机用二进制存储数据,但如果直接给二进制加符号位表示负数,会出现减法运算无法正确执行的致命问题——这是计算机存储负数的核心痛点,也是原码、反码、补码诞生的根源。
2.1 问题场景:5 - 3 = 2 的二进制运算矛盾
数学中减法可转化为加法(5 - 3 = 5 + (-3)),我们尝试用“符号位+数值位”的直观方式表示负数并计算:
- 十进制5的二进制(8位,最高位0为正):
00000101 - 十进制-3的二进制(8位,最高位1为负):
10000011 - 二进制相加结果:
00000101 + 10000011 = 10001000(对应十进制-8,与预期结果2完全不符)
2.2 问题根源
计算机的硬件运算器只能执行加法运算(减法需要额外设计电路,成本高、效率低)。直接用“符号位+数值位”表示负数,虽然符合人类直觉,但破坏了加法运算的统一性,导致减法无法正确转化为加法。
为解决这个问题,计算机科学家设计了“原码→反码→补码”的进化方案,最终补码成为现代计算机存储整数的唯一标准。
三、原码:最直观但有致命缺陷的方案
3.1 原码的定义
- 符号位:最高位为符号位,
0表示正数,1表示负数; - 数值位:其余位为数值的二进制绝对值;
- 位数约定:本节以8位二进制为例(计算机中常用8位、32位、64位)。
3.2 原码的示例(8位)
- 十进制+5:原码为
00000101(符号位0,数值位0000101); - 十进制-5:原码为
10000101(符号位1,数值位0000101); - 十进制+0:原码为
00000000(符号位0,数值位全0); - 十进制-0:原码为
10000000(符号位1,数值位全0)。
3.3 原码的优缺点
- 优点:直观易懂,正数的原码就是其本身的二进制表示,无需额外转换。
- 缺点:
- 运算错误:减法转化为加法时结果完全错误(如5 + (-3) = -8);
- 存在两个0:
+0(00000000)和-0(10000000),浪费一个存储位置(0的正负没有实际意义)。
四、反码:对原码的改进但未根治问题
4.1 反码的定义
- 正数的反码:与原码完全相同(符号位0,数值位不变);
- 负数的反码:符号位不变(仍为1),数值位按位取反(0变1,1变0)。
4.2 反码的示例(8位)
- 十进制+5:原码为
00000101,反码与原码相同,即00000101; - 十进制-5:原码为
10000101,符号位不变,数值位0000101取反为1111010,反码为11111010; - 十进制+0:原码为
00000000,反码与原码相同,即00000000; - 十进制-0:原码为
10000000,符号位不变,数值位0000000取反为1111111,反码为11111111。
4.3 反码的运算规则与优缺点
- 运算规则:反码相加后,若最高位产生进位,需将进位“循环”加到结果的最低位;
- 优点:部分减法运算可正确执行;
- 缺点:
- 仍存在两个0:
+0(00000000)和-0(11111111),浪费存储位置; - 需要循环进位:增加硬件运算复杂度,效率低。
- 仍存在两个0:
五、补码:现代计算机的标准方案(核心重点)
5.1 补码的核心原理:模运算
补码的设计灵感来自“模运算”——在一个固定范围(模)内,减法可以转化为加法。
- 模的定义:对于n位二进制,模是
2^n(如8位二进制的模是2^8=256,即1 00000000); - 核心公式:
a - b = a + (模 - b) (mod 模)(“模 - b”称为b的“补数”,减去一个数=加上它的补数); - 生活示例:时钟的模是12,10 - 3 = 10 + 9 = 19(mod 12)= 7(9是3的补数,12-3=9)。
5.2 补码的定义
- 正数的补码:与原码、反码完全相同(符号位0,数值位不变);
- 负数的补码:有两种等价计算方式(推荐第二种,更直观):
- 反码 + 1(符号位不变,数值位取反后加1);
- 模 - 数值的绝对值(如8位补码:
256 - |num|)。
5.3 补码的示例(8位,模=256)
- 十进制+5:原码
00000101,反码00000101,补码与原码相同,即00000101; - 十进制-5:原码
10000101,反码11111010,补码为反码+1(11111010+1=11111011),或用模计算(256-5=251,对应二进制11111011); - 十进制0:补码唯一,为
00000000; - 十进制-128:8位补码特有表示,原码/反码无法表示,对应二进制
10000000。
5.4 补码的运算规则(完美解决问题)
补码运算的核心规则:补码相加,舍弃超出位数的进位,结果的补码直接对应正确的十进制数。
- 示例:5 + (-3)(8位补码)
- +5的补码:
00000101 - -3的补码:
11111101 - 相加:
00000101 + 11111101 = 1 00000010 - 舍弃进位:保留8位得
00000010(对应十进制2,结果正确)。
- +5的补码:
5.5 补码的三大优势
- 运算统一:减法完全转化为加法,无需额外减法电路;
- 唯一零值:无
+0/-0区分,不浪费存储位置; - 存储范围更大:8位补码范围
-128 ~ 127(原码/反码为-127 ~ 127)。
六、整数的存储范围与溢出问题
6.1 有符号整数(补码存储)
- 8位:
-2^7 ~ 2^7 - 1→-128 ~ 127; - 32位:
-2^31 ~ 2^31 - 1→-2147483648 ~ 2147483647; - 64位:
-2^63 ~ 2^63 - 1→-9223372036854775808 ~ 9223372036854775807。
6.2 无符号整数(无符号位)
- 8位:
0 ~ 2^8 - 1→0 ~ 255; - 32位:
0 ~ 2^32 - 1→0 ~ 4294967295。
6.3 溢出问题的底层逻辑
当整数超出存储范围时,计算机会舍弃高位,数值“循环”:
- 示例:8位有符号整数127+1 = -128;
- ArkTS表现:
number类型(64位浮点数)精确范围-2^53 ~ 2^53,超出丢失精度;bigint无溢出问题。
七、示例代码
7.1 测试代码:utils/IntegerStorageTest.ets
/**
* 整数存储测试工具类:原码、反码、补码的转换与测试
* 适配鸿蒙5.0(API12)、DevEco Studio 6.0+
*/
/**
* 辅助方法:将数字转换为8位原码字符串(正数/负数)
* @param num 要转换的数字
* @param isNegative 是否为负数(仅用于原码符号位设置)
* @returns 8位原码字符串
*/
export function to8BitBinary(num: number, isNegative = false): string {
// 特殊处理-0的原码
if (num === 0 && isNegative) {
return "10000000";
}
// 限制8位原码范围(-127~127)
num = Math.abs(num);
if (num > 127) {
console.warn(`${num}超出8位原码表示范围(0~127),已截断为127`);
num = 127;
}
let binary = num.toString(2);
// 不足7位数值位补0(留1位给符号位)
binary = binary.padStart(7, '0');
// 拼接符号位(0为正,1为负)
return isNegative ? `1${binary}` : `0${binary}`;
}
/**
* 辅助方法:将数字转换为8位反码字符串
* @param num 要转换的数字
* @returns 8位反码字符串
*/
export function toInverseCode(num: number): string {
if (num >= 0) {
// 正数反码=原码
return to8BitBinary(num);
} else {
// 负数反码:符号位不变,数值位取反
const absBinary = to8BitBinary(Math.abs(num)).slice(1); // 取原码的数值位
let inverseBits = "";
for (const bit of absBinary) {
inverseBits += bit === "0" ? "1" : "0";
}
return `1${inverseBits}`; // 拼接符号位1
}
}
/**
* 辅助方法:将8位反码字符串转换为十进制数
* @param inverseCode 8位反码字符串
* @returns 十进制数
*/
export function inverseCodeToDecimal(inverseCode: string): number {
if (inverseCode.length !== 8) throw new Error("仅支持8位反码");
const signBit = inverseCode[0];
if (signBit === "0") {
// 正数反码=原码,直接转换
return parseInt(inverseCode, 2);
} else {
// 负数反码:数值位取反后转换为十进制,再加负号
let valueBits = "";
for (let i = 1; i < 8; i++) {
valueBits += inverseCode[i] === "0" ? "1" : "0";
}
return -parseInt(valueBits, 2);
}
}
/**
* 辅助方法:将数字转换为8位补码字符串
* @param num 要转换的数字(8位补码范围:-128~127)
* @returns 8位补码字符串
*/
export function toComplementCode(num: number): string {
if (num >= 0) {
// 正数补码=原码
return to8BitBinary(num);
} else {
// 负数补码:模256 - 绝对值
const absNum = Math.abs(num);
// 限制8位补码范围
const complement = absNum > 128 ? 0 : (256 - absNum);
return complement.toString(2).padStart(8, '0');
}
}
/**
* 辅助方法:将8位补码字符串转换为十进制数
* @param complementCode 8位补码字符串
* @returns 十进制数
*/
export function complementCodeToDecimal(complementCode: string): number {
if (complementCode.length !== 8) throw new Error("仅支持8位补码");
const signBit = complementCode[0];
if (signBit === "0") {
// 正数补码=原码,直接转换
return parseInt(complementCode, 2);
} else {
// 负数补码:模256 - 二进制数值
const value = parseInt(complementCode, 2);
return -(256 - value);
}
}
/**
* 测试1:直接用符号位表示负数的运算错误
*/
export function testNegativeBinaryError(): void {
console.log("========== 开篇痛点:负数二进制运算矛盾 ==========");
// 数学逻辑:5 - 3 = 5 + (-3) = 2
const mathResult = 5 - 3;
console.log(`数学计算:5 - 3 = ${mathResult}`);
// 二进制直观表示(符号位+数值位)
const binary5 = "00000101"; // +5的二进制
const binaryNeg3 = "10000011"; // -3的二进制(最高位1为负)
// 二进制相加(模拟计算机运算)
const decimal5 = parseInt(binary5, 2);
const decimalNeg3 = parseInt(binaryNeg3, 2); // 注意:此处仅为模拟,实际计算机不会这样解析
const wrongResult = decimal5 + decimalNeg3;
console.log(`二进制直观表示:5(${binary5}) + (-3)(${binaryNeg3}) = ${wrongResult}(对应二进制10001000,错误)`);
console.log("核心问题:符号位破坏了加法运算统一性,减法无法正确转化为加法");
}
/**
* 测试2:原码的表示与缺陷
*/
export function testOriginalCode(): void {
console.log("\n========== 原码:直观但有致命缺陷 ==========");
// 8位原码示例
console.log("8位原码表示:");
console.log(`+5 → ${to8BitBinary(5)}`); // 00000101
console.log(`-5 → ${to8BitBinary(5, true)}`); // 10000101
console.log(`+0 → ${to8BitBinary(0)}`); // 00000000
console.log(`-0 → ${to8BitBinary(0, true)}`); // 10000000
// 原码运算缺陷验证
const original5 = 0b00000101; // +5的原码
const originalNeg3 = 0b10000011; // -3的原码
const originalSum = original5 + originalNeg3;
console.log(`原码运算:5(00000101) + (-3)(10000011) = ${originalSum}(对应十进制${originalSum},错误)`);
console.log("原码两大缺陷:1. 减法运算错误;2. 存在两个0(浪费存储位置)");
}
/**
* 测试3:反码的表示、运算与缺陷
*/
export function testInverseCode(): void {
console.log("\n========== 反码:改进但未根治问题 ==========");
// 8位反码示例
console.log("8位反码表示:");
console.log(`+5 → 原码${to8BitBinary(5)} → 反码${toInverseCode(5)}`);
console.log(`-5 → 原码${to8BitBinary(5, true)} → 反码${toInverseCode(-5)}`);
console.log(`+0 → 原码${to8BitBinary(0)} → 反码${toInverseCode(0)}`);
console.log(`-0 → 原码${to8BitBinary(0, true)} → 反码${toInverseCode(-0)}`);
// 反码运算:5 + (-3) = 2(正确,需循环进位)
const inverse5 = inverseCodeToDecimal("00000101"); // +5的反码
const inverseNeg3 = inverseCodeToDecimal("11111100"); // -3的反码
let sum = inverse5 + inverseNeg3;
// 模拟8位反码循环进位(若和超过255,说明有进位)
if (sum > 0xFF) {
sum = (sum & 0xFF) + 1; // 舍弃高位,进位加最低位
}
console.log(`反码运算:5(00000101) + (-3)(11111100) = ${sum}(正确)`);
console.log("反码缺陷:1. 存在两个0;2. 运算需循环进位,效率低");
}
/**
* 测试4:补码的表示、运算与优势(核心重点)
*/
export function testComplementCode(): void {
console.log("\n========== 补码:现代计算机的标准方案 ==========");
// 8位补码示例
console.log("8位补码表示:");
console.log(`+5 → 原码${to8BitBinary(5)} → 补码${toComplementCode(5)}`);
console.log(`-5 → 原码${to8BitBinary(5, true)} → 补码${toComplementCode(-5)}`);
console.log(`0 → 补码${toComplementCode(0)}(唯一零值)`);
console.log(`-128 → 补码${toComplementCode(-128)}(8位补码特有,原码/反码无法表示)`);
// 补码运算:5 + (-3) = 2(完美正确)
const complement5 = complementCodeToDecimal("00000101"); // +5的补码
const complementNeg3 = complementCodeToDecimal("11111101"); // -3的补码
let sum = complement5 + complementNeg3;
// 模拟8位补码舍弃进位(仅保留低8位)
sum = sum & 0xFF;
console.log(`补码运算:5(00000101) + (-3)(11111101) = ${sum}(正确)`);
// 补码的三大优势
console.log("补码优势1:运算统一——减法完全转化为加法,适配硬件逻辑");
console.log("补码优势2:唯一零值——无+0/-0区分,不浪费存储位置");
console.log("补码优势3:存储范围更大——8位补码范围-128~127(原码/反码-127~127)");
}
/**
* 测试5:整数的存储范围与溢出问题
*/
export function testIntegerStorageRange(): void {
console.log("\n========== 整数存储范围与溢出 ==========");
// 1. 不同位数整数的存储范围
console.log("8位有符号整数(补码)范围:-128 ~ 127");
console.log("32位有符号整数(补码)范围:-2147483648 ~ 2147483647");
console.log("64位有符号整数(补码)范围:-9223372036854775808 ~ 9223372036854775807");
console.log("8位无符号整数范围:0 ~ 255");
// 2. 模拟8位有符号整数溢出
const max8 = 127; // 8位有符号整数最大值
const overflow8 = max8 + 1; // 128 → 对应补码10000000(-128)
console.log(`8位有符号整数溢出:127 + 1 = ${overflow8} → 补码表示10000000(对应-128)`);
// 3. ArkTS中number类型与bigint类型的溢出差异
const maxSafeInt = Number.MAX_SAFE_INTEGER; // 2^53 - 1 = 9007199254740991
const minSafeInt = Number.MIN_SAFE_INTEGER; // -2^53 + 1 = -9007199254740991
console.log(`ArkTS number类型精确整数范围:${minSafeInt} ~ ${maxSafeInt}`);
// number类型超出安全范围丢失精度(溢出)
const overflowNum = maxSafeInt + 2;
console.log(`number类型溢出:${maxSafeInt} + 2 = ${overflowNum}(丢失精度)`);
// bigint类型无溢出问题
const bigIntMax = BigInt(maxSafeInt);
const bigIntResult = bigIntMax + BigInt(2);
console.log(`bigint类型无溢出:${bigIntMax}n + 2n = ${bigIntResult}`);
}
/**
* 统一调用所有测试方法
*/
export function runAllIntegerStorageTests(): void {
testNegativeBinaryError();
testOriginalCode();
testInverseCode();
testComplementCode();
testIntegerStorageRange();
}
7.2 页面文件:pages/Index.ets
// 导入工具函数
import { runAllIntegerStorageTests } from '../utils/IntegerStorageTest';
@Entry
@Component
struct Index {
// 页面加载时自动执行所有测试
aboutToAppear(): void {
runAllIntegerStorageTests();
}
build() {
Column() {
Text("ArkTS原码、反码、补码与整数存储")
.fontSize(30)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
Text("测试日志已输出到控制台")
.fontSize(16)
.fontColor(Color.Grey)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center);
}
}
八、内容总结
- 核心痛点:直接用符号位表示负数会破坏加法运算统一性,导致减法无法正确执行,这是原码/反码/补码诞生的根源;
- 进化路径:原码(直观但运算错误、双零值)→ 反码(修复运算但仍有双零值)→ 补码(完美解决所有问题,成为标准);
- 补码核心:基于模运算将减法转化为加法,唯一零值,存储范围更大,适配计算机硬件逻辑;
- 存储范围:n位有符号整数(补码)范围为
-2^(n-1) ~ 2^(n-1)-1,超出范围会触发溢出; - ArkTS实操:
number类型有精确整数范围限制,bigint类型可表示任意大整数,无溢出问题。
九、代码仓库
- 工程名称:
IntegerStorageDemo - 仓库地址:https://gitee.com/juhetianxia321/harmony-os-code-base.git
十、下节预告
下一节将聚焦“小数如何存储”的核心问题,解开“0.1+0.2≠0.3”的最终谜题:
- 浮点数的存储痛点:为什么小数不能像整数那样直接用补码存储?
- IEEE 754标准:现代计算机存储浮点数的统一规范(符号位、指数位、尾数位的分工)。
- 0.1的二进制存储细节:无限循环小数如何被截断、规范化,导致精度损失?
- 浮点数精度问题的解决方案:在ArkTS中如何避免小数运算误差?
- 代码实操:验证浮点数的存储结构、精度限制及解决方案。
通过下一节学习,你将彻底掌握计算机存储小数的底层逻辑,解决开发中常见的精度问题!
浙公网安备 33010602011771号