写在前面
本文是对 《csapp》 第二章 “信息的表示和处理” 做的笔记,读者有兴趣可以去读原文,以下内容全来自对原文的总结。
信息存储
计算机大多数采用 8 bit 的块,作为最小单元寻址。每个块都有唯一的数字标识,也就是地址。
我们可以将整个虚拟内存看作是一个巨大的 char
类型数组。
进制
人类所擅长使用的是 十进制,所谓十进制,就是 逢十进一。按照相同的定义方式,我们可以定义出 X 进制。
转十进制
- 十进制: 321 = 3 × 10 2 + 2 × 10 1 + 1 × 10 0 {321=3 \times \mathop{{10}}\nolimits^{{2}}+2 \times \mathop{{10}}\nolimits^{{1}}+1 \times \mathop{{10}}\nolimits^{{0}}} 321=3×102+2×101+1×100
- 八进制: ( 501 ) 8 = 5 × 8 2 + 0 × 8 1 + 1 × 8 0 = 321 { \left( \mathop{{\left. 501 \right) }}\nolimits_{{8}}=5 \times \mathop{{8}}\nolimits^{{2}}+0 \times \mathop{{8}}\nolimits^{{1}}+1 \times \mathop{{8}}\nolimits^{{0}}=321\right. } (501)8=5×82+0×81+1×80=321
- 十六进制: ( 141 ) 16 = 1 × 16 2 + 4 × 16 1 + 1 × 16 0 = 321 \mathop{{ \left( 141 \right) }}\nolimits_{{16}}=1 \times \mathop{{16}}\nolimits^{{2}}+4 \times \mathop{{16}}\nolimits^{{1}}+1 \times \mathop{{16}}\nolimits^{{0}}=321 (141)16=1×162+4×161+1×160=321
- 二进制: ( 1 0100 0001 ) 2 = 1 × 2 8 + 1 × 2 6 + 1 × 2 0 = 321 \mathop{{\left. { \left( 1\text{ }0100\text{ }0001\right. } \right) }}\nolimits_{{2}}=1 \times \mathop{{2}}\nolimits^{{8}}+1 \times \mathop{{2}}\nolimits^{{6}}+1 \times \mathop{{2}}\nolimits^{{0}}=321 (1 0100 0001)2=1×28+1×26+1×20=321
再次强调,计算机只存储 二进制,以上风格迥异的数字,在计算机底层都是一串相同的二进制序列。
在 c/c++ 中,没有前缀的则是十进制,
0
开头的则是八进制,0x
开头的则是十六进制,无二进制字面量表示。
int main() {
int a = 321; // 十进制
int b = 0501; // 八进制
int c = 0x141; // 十六进制
printf("%d %d %d \n", a, b, c);
}
二进制与十六进制、八进制转换
二进制 -> 八进制:3 个二进制位顶 1 个八进制位;八进制 -> 二进制:1 个八进制位顶 3个二进制位。
2 进制:101 000 001
8 进制:5 0 1
恰好三位划分,可一一映射。
二进制 -> 十六进制:4 个二进制位顶 1 个十六进制位;十六进制 -> 二进制:1 个十六进制顶 4 个二进制位
2 进制:0001 0100 0001
16 进制:1 4 1
恰好四位划分,可一一映射。
大小端序
前面我们说到,我们将整个虚拟内存看出一个 char
数组,一个 char
类型是一个字节的。而几乎所有数据类型都超过了一个字节,这时我们要如何将一个多个字节在内存中排列呢?在计算机中有两种排列方式。
- 大端:最高有效字节在前
- 小端:最低有效字节在前
以 0x1234567
为例:
在此前我们强调过,计算机内部是一串二进制序列,本身是没有意义的。如果想要将其产生某种意义,那就需要对其做出解释。双方的解释不同,那就毫无意义、没法交流了(鸡同鸭讲)。
大部分的计算机内部都采用小端序,而网络传输统一规定使用大端序。
联合体判断大小端序
#define BIG 0
#define SMALL 1
int is_little_endian() {
union {
int a;
char b;
} u;
u.a = 1;
return u.b == 1 ? SMALL : BIG;
}
利用联合体特性,联合体共用一块内存
小端存储时。u.a = 1
最高位直接置为 1
,自然不等于占用最低字节的 u.b
大端存储时。u.a = 1
最低位直接置为 1
,所以会等于占用最低字节的 u.b
表示字符串
c 中的字符串是以 '\0'
结尾的字符串数组,通过结尾标识就能断定字符串在哪儿结束。
NULL
和'\0'
都是0
如下面计算字符串长度函数。
int str_len(char str[]) {
int len = 0;
while (str[len]) len++;
return len;
}
但是这种字符串表示是非常不安全的,如果数组末尾的标志符丢失,将无法断定字符串的结束位置。
所以有些字符串的实现,会将其封装成一个结构体,用第一个字段标志长度。或用数组的第一个元素指明字符串的长度。
结构体也是按序排列的,如果你第一个字段就丢了,那就相当于没有过这个串。
位运算
与:&
,同一出一
或:|
,有一出一
非:~
,按位取反
异或:无进位加法
位运算有很多骚操作,再此不做赘述,详情请看我以前写的文章:传送门
用作集合
二进制序列中,将 x
位上置为 1
,就将其看成在这个集合中包含 x
这个元素。所以一个 char
可以视为最多包含 0~7
的集合。
a = [0110 1001] ---> A = {0, 3, 5, 6}
b = [0101 0101] ---> B = {0, 2, 4, 6}
交:a & b = [0100 0001] = {0, 6}
并:a | b = [0111 1111] = {0, 1, 2, 3, 4, 5, 6}
补:~a = [1001 0110] = {2, 3, 5, 7}
用位运算对一串二进制序列位上的操作,充当对一个集合的增删改查操作,这在计算机中很常见。这类还有一个很高大上的名字,掩码。
移位运算
算术右移:>>
,往右移动,低位直接丢弃,高位由符号位补充。
逻辑右移:Java 中是 >>>
,c\c++ 中没有。往右移动,低位直接丢弃,高位由 0 补充。
左移:往左移动,高位直接丢弃,低位由 0 补充。也分算术和逻辑,但效果是一样的。
对于算术左移
k
位或右移k
位,最直观的就是对一个数乘 2 k \mathop{{2}}\nolimits^{{k}} 2k 或除 2 k \mathop{{2}}\nolimits^{{k}} 2k。对除来说又有略微的不同,再此不做缀诉,详情请看我以前写的文章:传送门
如果对一个数的移位 k
超过数本身的长度 w
,那么就会做 k mod w
移位。
优先级问题:不建议写很复杂的移位运算,如果要写请打上括号,很多人都搞不清优先级。
+
-
的优先级都比移位要高- 移位运算是从左到右结合的
如:
1 << 2 + 3 << 4
,很多人都弄不清怎么计算,正确的结合方式是(1 << (2 + 3)) << 4
,所以结果是512
。别说上面这种复杂的移位运算,就连
l + r >> 1
的结合方式,具有多年工作经验的程序员来说都分不清。所以再次建议,不要作复杂的位运算,如果非要做!那么请封装好一个函数或宏。
逻辑运算
与 &&
或 ||
非 !
,切不可与位运算中的与&
或 |
非 ~
混淆。前者是对两个数的布尔运算,后者是对两个数的位运算。
且 &&
运算与 ||
运算具有短路属性。所谓短路,即前面的布尔运算能确定整个布尔运算的结果,则后半段就不会执行。
比如:
a || b
,a
如果为真,就不需要再做后面的b
部分。a && b
,a
如果为假,就不需要再做后面的b
部分。
再比如,下面的算法,当 i = n
时,后面的 str[i] != c
就会被短路,所以不存在越界的风险。
// 在一个字符串中搜寻某个字符的索引并返回。
int search(const char str[], int n, char c) {
int i = 0;
while(i < n && str[i] != c) i++;
return i;
}
再比如,求 1 + 2 + ... + n
的和,不能使用乘除法、for
、while
、if
、else
、switch
、case
等关键字及条件判断语句 (A?B:C)
实现。
int getSum(int n) {
int res = n;
(n > 0) && (res += getSum(n - 1)); // 利用短路运算终止递归
return res;
}