explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What]Information storage

理解信息是如何在计算机中存储的。

16 进制符号

一般在计算机系统中,都是以字节为其基本的操作单位,编译器根据源码中的数据类型来决定其具体的操作方式。

而对编码来说,数据的表示使用 16 进制比使用 2 进制或十进制更加有优势,即不像二进制那么冗余,不像 10 进制那么不容易转为二进制。

2 的 n 次方来计算 16 进制

当一个数等于 2 的 n 次方时( n 为非负数),其就相当于将 1 左移 n 位,其对应的 16 进制就是 0x100..,0x200..,,0x400..,,0x800..

快速计算的公式为: n = i + 4j

  • 也就是将 2 的 n 次方中的 n 使用 i + 4j 来拟合。
  • 其中 i 取值范围为 0~3,分别对应 16 进制中的 1,2,4,8。
  • 最后计算出来的 j 就是代表后面跟几个 0

比如 2048 = 2 ^ 11
其中 n = 11 = 3 + 4 * 2, i = 3 对应 16 进制的 8 ,j = 2 对应后面跟两个 0
最终的16进制就是 0x800

10进制转16进制

10进制通过循环除以16,并依次取其余数从最低位到最高位来倒序排列来转换为16进制。

比如一个十进制数为 314156,依次计算如下:
-> 314156 = 19634 * 16 + 12 (余数12,对应16进制为C)
-> 19634 = 1227 * 16 + 2 (余数2,对应16进制为2)
-> 1227 = 76 * 16 + 11 (余数为11,对应16进制为B)
-> 76 = 4 * 16 + 12 (余数12,对应16进制为C)
-> 4 = 0 * 16 + 4 (4)
==> 最终16进制为 0x4cb2c

而 16 进制转 10 进制,则是每一位依次乘以 16 的 (n-1) 次方的求和。

数据与大小

计算机系统中的字长,也就是一个指针的大小,也决定了其虚拟地址的范围大小。
目前 32 位与 64 位机共存,为了使得代码具有移植性,关于整型的操作还是应该使用stdint.h 中的定义。

c 标准并没有规定 char 类型一定是有符号类型,这一点是需要注意的。
如果仅仅是用来存字符,那么可以直接使用 char 来定义。
如果是为了表示一个有符号整数,那么使用 int8_t 是正确的选择。

地址与大小端

代码中的对象,在内存中以两个方面来反应:

  1. 对象存储的地址
  2. 对象的字节序:字节序分为小端模式(低字节在前)和大端模式(高字节在前)
    • 很多处理器都具有大小端选择控制,但在选中操作系统后,一般都固定以其中一种字节序

字节序的问题在以下几种情况下需要注意:

  1. 与其他主机通信
    • 在通过 socket 通信时,需要调用标准的 socket 接口以转换统一的字节序
    • 在嵌入式系统上多个不同处理器通信时,需要统一确认大小端
  2. 在代码中嵌入汇编时,当需要给某个地址写一个类似整型的多字节对象时,需要注意大小端
  3. 当进行类型强制转换或使用联合体时,需要注意大小端

对于多个字节来一起表示一个完整对象时(比如 两字节及以上的整型、浮点型),才会有大小端问题。而对于字符串来讲,由于数据都是依次存储的,就不存在大小端问题。那么可以说,以字符串形式存储的内容,具备平台独立性。

判断当前系统的大小端使用下面代码即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>

bool is_big_endian(void){
uint32_t val = 0x12345678;
char *str = (char *)&val;

if(*str == 0x12){
return true;
}

return false;
}


int main(void){
printf("your system is %s endian\n", is_big_endian() ? "big" : "little");

return 0;
}

位运算

c 中可用的位运算有: |(或),&(与),~(按位取反),!(取反),^(异或)。

异或有个常用的特点:与 1 异或就是求反,与 0 异或就是保持原来的值

下面的代码可以不需要临时变量 temp 来实现二者的交换:

这种方式效率并不高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void inplace_swap(int *x, int *y){
*y = *x ^ *y; // *y = a ^ b
*x = *x ^ *y; // *x = a ^ a ^ b = 0 ^ b = b
*y = *x ^ *y; // *y = b ^ a ^ b = 0 ^ a = a
}

int main(void){
int a = 123;
int b = 456;

printf("the result swap a = %d, b = %d, ", a, b);
inplace_swap(&a, &b);
printf("is a = %d, b = %d\n", a, b);

return 0;
}

逻辑运算

c 中支持的逻辑运算就是 &&(与) ,||(或), !(非)。

需要注意的有以下两点:

  1. 逻辑运算的返回只有0(false)和1(true)两种结果
  2. 逻辑运算中,采用最少判断法则,只要前面的参数足以判断逻辑式的真假,后面的参数就不用判断了
    • 比如a && 5 / a,当 a 为 0 时,并不会触发除零错误,因为第一个判断就知道此表达式为假了,后面的判断就不会执行了。

      移位操作

      基于硬件支持的移位操作是:
  • 左移 : 地位补零
  • 逻辑右移 : 高位补零
  • 算术右移 : 最高位补符号位的值,符号位是 0 就补 0 ,符号位是 1 就补 1

在c上实践的结果是:对于正数,其操作与逻辑右移一致。对于负数,高位补 1 。

为什么没有算术左移?

因为负数是以补码的形式表示的,所以逻辑操作的结果就是算术操作的结果。