数字系统(十进制、二进制、十六进制和八进制)
本节阅读量:
在日常生活中,我们使用十进制来计数,其中每一位的数字可以是 0、1、2、3、4、5、6、7、8 或 9。十进制也被称为“基数 10”,因为它一共有 10 个可选的数字(0 到 9)。在这个系统中,我们是这样计数的:0、1、2、3、4、5、6、7、8、9、10、11……在 C++ 程序中,数字默认都被视为十进制。
1
|
int x { 12 }; // 12 默认是十进制
|
在二进制中,只有 2 个数字:0 和 1,因此它被称为“基数 2”。在二进制中,我们这样计数:0、1、10、11、100、101、110、111……
十进制和二进制是数字系统的两个例子。C++ 中主要有 4 种数字系统,按常用程度排序依次为:十进制(基数 10)、二进制(基数 2)、十六进制(基数 16)和八进制(基数 8)。
注
本节是可选阅读的。
后续的少数几节课程会用到十六进制数,因此在继续学习之前,你至少应该对这一概念有大致的了解。
八进制和十六进制
八进制是基数 8——也就是说,可用的数字只有:0、1、2、3、4、5、6 和 7。在八进制中,我们这样计数:0、1、2、3、4、5、6、7、10、11、12……(注意:没有 8 和 9,因此我们从 7 直接跳到 10)。
| 十进制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
| 八进制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
要使用八进制字面值,在前面加上一个 0(零)即可:
1
2
3
4
5
6
7
8
|
#include <iostream>
int main()
{
int x{ 012 }; // 在 C++ 中以 0 开头的数字代表八进制
std::cout << x << '\n';
return 0;
}
|
该程序的输出为:
为什么是 10 而不是 12?因为默认情况下数字是以十进制输出的,八进制的 12 等于十进制的 10。
八进制几乎很少被使用,我们建议你避免使用它。
十六进制是基数 16。在十六进制中,我们这样计数:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F、10、11、12……
| 十进制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
| 十六进制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
10 |
11 |
要使用十六进制字面值,需要在前面加上 0x。
1
2
3
4
5
6
7
8
|
#include <iostream>
int main()
{
int x{ 0xF }; // 在 C++ 中以 0x 开头的数字代表十六进制
std::cout << x << '\n';
return 0;
}
|
该程序的输出为:
由于十六进制的每一位有 16 种不同的取值,因此一个十六进制数字正好可以表示 4 个比特(bit)。这样,我们就能用两位十六进制数精确地表示一个字节。
考虑一个 32 位整数,其二进制值为 0011 1010 0111 1111 1001 1000 0010 0110。由于数字很长且内容重复,这并不容易阅读。而在十六进制中,同样的值只是 3A7F 9826,看起来要简洁得多。因此,十六进制值常被用于表示内存地址或内存中的原始数据(其类型未知时)。
二进制字面值
在 C++14 之前,C++ 并不支持二进制字面值。我们通常借助十六进制字面值作为一种替代方案(你在现有代码库中可能仍会看到这种写法):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <iostream>
int main()
{
int bin{};
bin = 0x0001; // 对应二进制 0000 0000 0000 0001
bin = 0x0002; // 对应二进制 0000 0000 0000 0010
bin = 0x0004; // 对应二进制 0000 0000 0000 0100
bin = 0x0008; // 对应二进制 0000 0000 0000 1000
bin = 0x0010; // 对应二进制 0000 0000 0001 0000
bin = 0x0020; // 对应二进制 0000 0000 0010 0000
bin = 0x0040; // 对应二进制 0000 0000 0100 0000
bin = 0x0080; // 对应二进制 0000 0000 1000 0000
bin = 0x00FF; // 对应二进制 0000 0000 1111 1111
bin = 0x00B3; // 对应二进制 0000 0000 1011 0011
bin = 0xF770; // 对应二进制 1111 0111 0111 0000
return 0;
}
|
到了 C++14,可以通过 0b 前缀来使用二进制字面值:
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <iostream>
int main()
{
int bin{}; // 假设 int 是 16 位
bin = 0b1; // 对应二进制 0000 0000 0000 0001
bin = 0b11; // 对应二进制 0000 0000 0000 0011
bin = 0b1010; // 对应二进制 0000 0000 0000 1010
bin = 0b11110000; // 对应二进制 0000 0000 1111 0000
return 0;
}
|
数字分隔符
由于较长的字面值可能不太好读,C++14 还新增了使用单引号(’)作为数字分隔符的功能。
1
2
3
4
5
6
7
8
9
|
#include <iostream>
int main()
{
int bin { 0b1011'0010 }; // 对应二进制 1011 0010
long value { 2'132'673'462 }; // 阅读起来更容易
return 0;
}
|
还要注意,分隔符不能出现在数字的第一位之前:
1
|
int bin { 0b'1011'0010 }; // error: ' 出现在了第一位之前
|
数字分隔符只是为了方便阅读,并不会以任何方式改变对应的数值。
以十进制、八进制或十六进制输出值
默认情况下,C++ 以十进制输出数值。不过,你可以通过 std::dec、std::oct 和 std::hex 这几个 I/O 操纵器来改变输出格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <iostream>
int main()
{
int x { 12 };
std::cout << x << '\n'; // 十进制 (默认)
std::cout << std::hex << x << '\n'; // 十六进制
std::cout << x << '\n'; // 现在仍是十六进制
std::cout << std::oct << x << '\n'; // 八进制
std::cout << std::dec << x << '\n'; // 切回十进制
std::cout << x << '\n'; // 十进制
return 0;
}
|
输出结果为:
1
2
3
4
5
6
|
12
c
c
14
12
12
|
注意,一旦设置了某种 I/O 操纵器,输出格式就会一直保持,直到下次再被改变。
以二进制格式输出数值
以二进制格式输出数值会稍微麻烦一些,因为 std::cout 并没有内置这项功能。幸运的是,C++ 标准库中提供了一个名为 std::bitset 的类型(定义在 <bitset> 头文件中)可以帮我们完成这项工作。
要使用 std::bitset,我们可以定义一个 std::bitset 变量,并告诉它需要存储多少位。位数必须是编译时常量。我们可以用任意格式(包括十进制、八进制、十六进制或二进制)的整数值来初始化 std::bitset。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <bitset> // 引入 std::bitset
#include <iostream>
int main()
{
// std::bitset<8> 表示存储 8 个 bit
std::bitset<8> bin1{ 0b1100'0101 }; // 二进制的 1100 0101
std::bitset<8> bin2{ 0xC5 }; // 十六进制的 1100 0101
std::cout << bin1 << '\n' << bin2 << '\n';
std::cout << std::bitset<4>{ 0b1010 } << '\n'; // 创建一个临时 bitset 并打印
return 0;
}
|
输出结果为:
1
2
3
|
11000101
11000101
1010
|
在上面的代码中,这一行:
1
|
std::cout << std::bitset<4>{ 0b1010 } << '\n'; // 创建一个临时 bitset 并打印
|
创建了一个临时的(未命名的)4 位 std::bitset 对象,用二进制字面值 0b1010 进行初始化,以二进制格式打印其值,之后再把这个临时对象销毁。
在 C++20 和 C++23 中,借助新的格式化库(C++20)和打印库(C++23),我们有了更好的选择:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <format> // C++20
#include <iostream>
#include <print> // C++23
int main()
{
std::cout << std::format("{:b}\n", 0b1010); // C++20
std::cout << std::format("{:#b}\n", 0b1010); // C++20
std::print("{:b} {:#b}\n", 0b1010, 0b1010); // C++23
return 0;
}
|
输出结果为:
1
2
3
|
1010
0b1010
1010 0b1010
|