├── .gitlab-ci.yml ├── README.md ├── illustration ├── img-1-1.png ├── img-2-1.png ├── img-2-2.png ├── img-2-3.png ├── img-2-4.png ├── img-2-5.png ├── img-5-2.png ├── img-5-3.png ├── img-5-4.png ├── img-5-5.png ├── img-5-6.png ├── img-5-8.png └── img-5-9.png ├── slides ├── binary-arithmetic.html ├── binary-arithmetic.md ├── binary.html ├── binary.md ├── chapter-5-1.png ├── chapter-5-2.png ├── chapter-5-3.png ├── chapter-5-4.svg ├── chapter-5-5.svg ├── index.html ├── introduction.html ├── introduction.md ├── multimedia.html ├── multimedia.md ├── numbers.html ├── numbers.md ├── qrcode-bilibili-space-coding-log.png ├── qrcode-coding-log.jpg ├── qrcode-fmsoft-online-lectures-c.png ├── qrcode-wechat-channel-coding-log.png ├── text.html ├── text.md ├── what-is-operating-system.html └── what-is-operating-system.md └── textbook ├── foreword.md ├── part-1-chapter-1.md ├── part-1-chapter-2.md ├── part-1-chapter-3.md ├── part-1-chapter-4.md ├── part-1-chapter-5.md ├── part-1-chapter-6.md ├── part-1-chapter-7.md ├── preface.md └── toc.md /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | markdownSlides: 2 | # image: busybox:latest 3 | stage: deploy 4 | script: 5 | - echo "copy images..." 6 | - cp slides/*.png slides/*.jpg slides/*.svg /var/www/courses.fmsoft.cn/the-basic-computer-software-methods/assets -f 7 | - echo "copy HTML files..." 8 | - cp slides/*.html /var/www/courses.fmsoft.cn/the-basic-computer-software-methods/ -f 9 | - echo "copy markdown files..." 10 | - cp slides/*.md /var/www/courses.fmsoft.cn/the-basic-computer-software-methods/gitlab/ -f 11 | - echo "done" 12 | only: 13 | refs: 14 | - main 15 | changes: 16 | - slides/* 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 计算机软件方法导论 2 | 3 | 本仓库包含魏永明的《计算机软件方法导论》课程的讲义及对应的教材。 4 | 5 | ## 已发表课程 6 | 7 | - 第一讲于 2022 年 3 月 17 日发表。 8 | - [B 站视频](https://www.bilibili.com/video/BV1u3411s7Jy) 9 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/introduction.html) 10 | - 相关教材:[引言](textbook/foreword.md) 11 | - 相关教材:[前言](textbook/preface.md) 12 | 13 | - 第二讲于 2022 年 3 月 23 日发表。 14 | - [B 站视频](https://www.bilibili.com/video/BV1DU4y1d7wi/) 15 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/binary.html) 16 | - 相关教材:[为什么选择二进制](textbook/part-1-chapter-1.md) 17 | 18 | - 第三讲于 2022 年 3 月 29 日发表。 19 | - [B 站视频](https://www.bilibili.com/video/BV1VY411J7AU/) 20 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/binary-arithmetic.html) 21 | - 相关教材:[二进制及其运算](textbook/part-1-chapter-2.md) 22 | 23 | - 第四讲于 2022 年 4 月 7 日发表。 24 | - [B 站视频](https://www.bilibili.com/video/BV1zq4y1a7ae/) 25 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/numbers.html) 26 | - 相关教材:[整数、浮点数和定点数](textbook/part-1-chapter-3.md) 27 | 28 | - 第五讲于 2022 年 4 月 13 日发表。 29 | - [B 站视频](https://www.bilibili.com/video/BV1DU4y1d7wi) 30 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/text.html) 31 | - 相关教材:[文字:字符集及编码](textbook/part-1-chapter-4.md) 32 | 33 | - 第六讲于 2022 年 4 月 20 日发表。 34 | - [B 站视频](https://www.bilibili.com/video/BV1cY4y1Y7c1/) 35 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/text.html#/10) 36 | - 相关教材:[文字:字符集及编码](textbook/part-1-chapter-4.md) 37 | 38 | - 第七讲于 2022 年 4 月 26 日发表。 39 | - [B 站视频](https://www.bilibili.com/video/BV1eY4y187vM/) 40 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/multimedia.html) 41 | - 相关教材:[多媒体:图像、图形及音视频](textbook/part-1-chapter-5.md) 42 | 43 | - 第八讲于 2022 年 5 月 6 日发表。 44 | - [B 站视频](https://www.bilibili.com/video/BV1TT4y167S6/) 45 | - [在线讲义](https://courses.fmsoft.cn/the-basic-computer-software-methods/multimedia.html#/7) 46 | - 相关教材:[多媒体:图像、图形及音视频](textbook/part-1-chapter-5.md) 47 | 48 | 49 | ## 版权声明 50 | 51 | 版权所有 © 2022 魏永明 52 | 保留所有权利 53 | -------------------------------------------------------------------------------- /illustration/img-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-1-1.png -------------------------------------------------------------------------------- /illustration/img-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-2-1.png -------------------------------------------------------------------------------- /illustration/img-2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-2-2.png -------------------------------------------------------------------------------- /illustration/img-2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-2-3.png -------------------------------------------------------------------------------- /illustration/img-2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-2-4.png -------------------------------------------------------------------------------- /illustration/img-2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-2-5.png -------------------------------------------------------------------------------- /illustration/img-5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-5-2.png -------------------------------------------------------------------------------- /illustration/img-5-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-5-3.png -------------------------------------------------------------------------------- /illustration/img-5-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-5-4.png -------------------------------------------------------------------------------- /illustration/img-5-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-5-5.png -------------------------------------------------------------------------------- /illustration/img-5-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-5-6.png -------------------------------------------------------------------------------- /illustration/img-5-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-5-8.png -------------------------------------------------------------------------------- /illustration/img-5-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/illustration/img-5-9.png -------------------------------------------------------------------------------- /slides/binary-arithmetic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机软件方法导论:二进制及其运算 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

魏永明的计算机软件方法导论二进制及其运算

21 |

考鼎®

22 |
23 |
24 |
29 |
30 |
31 |

扫一扫

32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /slides/binary-arithmetic.md: -------------------------------------------------------------------------------- 1 | ## 二进制及其运算 2 | 3 | - 电子计算机的运转原理和性能衡量指标 4 | - 二进制术语 5 | - 令人讨厌的东西 6 | - 常见二进制运算 7 | 8 | 9 | ## 1. 电子计算机的运转原理 10 | 11 | * 时钟周期:节拍 12 | * 指令:在一个或者多个时钟周期内完成的不可分隔的操作 13 | * 指令集及架构:IA32、x86-64、ARM、ARM64、MIPS、RISC-V、LoongArch 14 | 15 | 16 | ## 2. 电子计算机的性能衡量指标 17 | 18 | * 比特位宽:可同时处理的比特位数量(8、16、32、64)。 19 | * 运算速度:每秒百万指令数(MIPS)、每秒浮点运算次数(FLOPS)。 20 | 21 | 22 | ## 3. 二进制术语(1/2) 23 | 24 | * 比特(bit),缩写时用小写 b。 25 | * 字节(byte),缩写时用大写 B。 26 | * bps:每秒位数,通常用来表示传输速度,比如宽带 1000Mbps 或者 1Gbps。 27 | * K、M、G 等前缀引起的混乱。 28 | 29 | 30 | ## 4. 二进制术语(2/2) 31 | 32 | * 最高位(MSB) 33 | * 最低位(LSB) 34 | * 字节的二进制(binary)表示:1000.1000b 35 | * 字节的十六进制(hexadecimal)表示:0x88,88h 36 | * 字(word)、双倍字(double word)、四倍字(quadruple word)、双四倍字(double quadruple word) 37 | 38 | 39 | ## 5. 二进制令人讨厌的部分 40 | 41 | * 字节序(endianness) 42 | * 对齐(alignment) 43 | 44 | 45 | ## 6. 二进制的特殊运算 46 | 47 | * 位运算:与(AND)、或(OR)、取反(NOT)、亦或(XOR) 48 | * 移位运算 49 | 50 | 51 | ## 7. 二进制运算技巧 52 | 53 | * 判断奇偶性:x & 1 54 | * 使用移位优化乘除法:x >> 3 55 | * 用符号位表示负数:~x = -x -1 56 | * 判断是否是 2 的正整数幂:(~(n & (n - 1))) 57 | * 圆整为大于等于自己(n)的最小的 2 的正整数(N)幂:((n + (N - 1)) & -N) 58 | * 最简单的加解密:x ^ y ^ y = x 59 | 60 | 61 | ## 8. 二进制运算的综合应用 62 | 63 | 1. 把字节序列组装成字、双倍字等 64 | 1. 将比特位作为标志使用 65 | 1. 传输校验:奇偶校验 66 | 1. 图形学中的应用 67 | -------------------------------------------------------------------------------- /slides/binary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机软件方法导论:为什么选择二进制 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

魏永明的计算机软件方法导论为什么选择二进制

21 |

考鼎®

22 |
23 |
24 |
29 |
30 |
31 |

扫一扫

32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /slides/binary.md: -------------------------------------------------------------------------------- 1 | ## 为什么选择二进制 2 | 3 | - 为什么讲计算机要从二进制讲起? 4 | - 图灵机到底是个什么玩意儿? 5 | - 图灵机和冯诺依曼架构到底谁更重要? 6 | 7 | 8 | ## 1. 机械计算的需求 9 | 10 | * 机械计算试图将所有的运算工作变成可重复的简单运算工作 11 | * 机械计算的需求:准确无误地重复,越快越好,越节能越好 12 | 13 | 多快好省地重复 14 | 15 | 16 | ## 2. 各种可能的实现方法 17 | 18 | * 三体中的人肉计算机 19 | * 巴贝奇的分析机:齿轮 20 | 21 | 22 | ## 3. 二进制的优势 23 | 24 | * 十进制的麻烦:基础零件太过复杂 25 | * 巴贝奇时代,二进制概念尚未形成,布尔代数尚未提出 26 | * 使用二进制可以构造数学上最简单的加法器 27 | * 电子晶体管可以用来构建二进制的基础运算单元,而且足够小、足够快 28 | * 过去五十年,电子计算机的大发展时代 29 | 30 | 31 | ## 4. 程序和图灵机 32 | 33 | * 程序和数据组成了软件 34 | * 一组有序且可被计算机执行的数据形成程序 35 | * 图灵机:执行输入的程序并可产生输出 36 | * 理解图灵机:解决实数的可计算性问题 37 | 38 | 39 | ## 5. 冯诺依曼架构 40 | 41 | * 软件和硬件分离 42 | * 通用的硬件设计,通过不同的软件来实现不同的功能 43 | 44 | -------------------------------------------------------------------------------- /slides/chapter-5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/slides/chapter-5-1.png -------------------------------------------------------------------------------- /slides/chapter-5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/slides/chapter-5-2.png -------------------------------------------------------------------------------- /slides/chapter-5-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/slides/chapter-5-3.png -------------------------------------------------------------------------------- /slides/chapter-5-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /slides/chapter-5-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 演示 animation 元素 8 | 10 | 11 | 14 | 16 | 18 | 20 | 22 | 24 | 25 | 26 | 28 | 29 | 37 | 39 | It's alive! 40 | 42 | 44 | 47 | 50 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /slides/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机技术系列教程 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

计算机技术系列教程

21 |

考鼎®

22 |
23 |
24 |
25 |

魏永明的计算机软件技术导论

26 |
27 |

0. 前言

28 |
29 |
30 |

1. 信息的计算机表述

31 |

1-1. 为什么是二进制

32 |

1-2. 二进制及其运算

33 |

1-3. 数:整数和实数

34 |

1-4. 文字:字符集及编码

35 |

1-5. 多媒体:图像、图形及音视频

36 |

1-6. 抽象对象及结构化数据

37 |
38 |
39 |

2. 信息的计算机存储

40 |
41 |
42 |

3. 信息的计算机处理

43 |
44 |
45 |

4. 信息的计算机展现

46 |
47 |
48 |

5. 信息的计算机传输

49 |
50 |
51 |

6. 计算机编程语言

52 |
53 |
54 |

7. 操作系统

55 |

7-1. 什么是操作系统

56 |
57 |
58 |

8. 软件工程方法

59 |
60 |
61 |

9. 计算机软件技术的前沿热点

62 |
63 |
64 |
65 |

扫一扫

66 | 67 | 68 | 69 |
70 |
71 |
72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /slides/introduction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机软件方法导论:前言 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

魏永明的计算机软件方法导论前言

21 |

考鼎®

22 |
23 |
24 |
29 |
30 |
31 |

扫一扫

32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /slides/introduction.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | - 为什么要开设这门课程? 4 | - 讲给谁听? 5 | - 包含哪些内容? 6 | 7 | 8 | ## 1. 全民编程时代即将来临 9 | 10 | * 只要用计算机就离不开编程 11 | * 办公室白领未来也需要编程 12 | * 小学高年级已经开设有编程的课程 13 | * 听说有初中生写出了自己的操作系统 14 | 15 | 16 | ## 2. 中国码农的瓶颈 17 | 18 | * 缺乏常识,仅凭可怜的知识储备(近乎直觉)写程序 19 | * 遇到问题怀疑一切,工作效率极低 20 | 21 | 22 | ## 3. 为什么中国码农普遍缺乏计算机软硬件常识? 23 | 24 | * 不了解计算机软硬件发展的历史 25 | * 缺乏近现代科学文化的熏陶 26 | 27 | 28 | ## 4. 本课程的目标 29 | 30 | * 扫盲 31 | * 补钙 32 | 33 | 34 | ## 5. 计算机的本质特征 35 | 36 | * 离散的数字 37 | * 编码 38 | 39 | 40 | ## 6. 硬件和软件 41 | 42 | * 硬件:物理存在的计算机或其组件 43 | * 软件:编码及编码的方法 44 | 45 | 46 | ## 7. 计算机软件 47 | 48 | * 通用操作系统的软件栈 49 | * 智能操作系统的软件栈 50 | 51 | 52 | ## 8. 计算机软件的演进 53 | 54 | * 纸带 55 | * 汇编 56 | * 高级编程语言 57 | * 操作系统 58 | 59 | 60 | ## 9. 本课程内容 61 | 62 | * 信息的计算机表述 63 | * 信息的计算机存储 64 | * 信息的计算机处理 65 | * 信息的计算机展现 66 | * 信息的计算机传输 67 | * 计算机编程语言 68 | * 操作系统 69 | * 软件工程方法 70 | * 前沿热点 71 | 72 | 73 | ## 10. 教材开源,协作撰写 74 | 75 | - [GitHub 仓库][https://github.com/VincentWei/the-basic-computer-software-methods] 76 | - [GitLab 仓库][https://gitlab.fmsoft.cn/VincentWei/the-basic-computer-software-methods] 77 | 78 | -------------------------------------------------------------------------------- /slides/multimedia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机软件方法导论:多媒体:图像、图形及音视频 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

魏永明的计算机软件方法导论多媒体:图像、图形及音视频

21 |

考鼎®

22 |
23 |
24 |
29 |
30 |
31 |

扫一扫

32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /slides/multimedia.md: -------------------------------------------------------------------------------- 1 | ## 多媒体:图像、图形及音视频 2 | 3 | - 多媒体及计算机输出设备 4 | - 图像 5 | - 矢量图形 6 | - 动画:基于静止图像或基于矢量图形 7 | - 音频 8 | - MIDI:矢量化音频 9 | - 视频压缩原理 10 | 11 | 12 | ## 1. 多媒体及计算输入输出设备 13 | 14 | - 多媒体(multimedia) 15 | - 图像(image)及图形(graphics) 16 | - 音频(audio) 17 | - 视频(video) 18 | 19 | 20 | - 视觉输入输出设备 21 | - 字符终端(character terminal) 22 | - 图形终端(graphics terminal) 23 | - 摄像头(camera) 24 | 25 | 26 | - 听觉输入/输出设备 27 | - 麦克风(microphone) 28 | - 扬声器(speaker) 29 | - 耳机(earphone) 30 | 31 | 32 | - 其他输入/输出设备 33 | - 陀螺仪(gyroscope) 34 | - 振动器(vibrator) 35 | 36 | 37 | ## 2. 显示器、显存及帧缓冲区 38 | 39 | - 显示器(display) 40 | - 显存(video RAM、VRAM) 41 | - 帧缓冲区(frame buffer) 42 | 43 | 44 | ## 3. 静态图像的表述 45 | 46 | - 用位图(bitmap/pixmap):像素点组成的矩阵 47 | - 像素(pixel) 48 | - 每像素位数(bits per pixel, color depth),bpp 49 | - 每像素字节数(bytes per pixel, color depth),Bpp 50 | - 每行占用的字节数(pitch) 51 | - 透明像素或每像素的 Alpha 值 52 | 53 | 54 | ## 4. 像素值的两种记录方式 55 | 56 | 1. 调色板(palette) 57 | - 像素值用调色板中的颜色编号表示。 58 | - 适合于单色、四位色、八位色 59 | 1. 直接记录 RGBA 分量 60 | - 使用 16 位、24 位或者 32 位来记录 RGBA 分量 61 | 62 | 63 | ## 5. 静态图像显示到屏幕上的过程 64 | 65 | 1. 读取图像文件头部,获取基本信息(高度、宽度、色深等) 66 | 1. 按照图像文件的编码方式解码像素值,生成设备无关位图。 67 | 1. 将设备无关位图转换成适合屏幕当前像素格式的设备相关位图。 68 | 1. 将设备相关位图复制到帧缓冲区中。 69 | 70 | 71 | ## 6. 其他色彩空间 72 | 73 | - YUV:通过亮度(luminance)、色调和饱和度定义颜色(即色度)。 74 | - YIQ:和 YUV 类似,用于美式电视信号标准 NTSC。 75 | - CMYK:主要用于打印机和印刷业,通过青、洋红、黄和黑四种颜色的不同取值来定义一种颜色。 76 | 77 | 78 | - 颜色空间转换 79 | 80 | ``` 81 | Y = 0.30R + 0.59G + 0.11B 82 | U = (B − Y) × 0.493 83 | V = (R − Y) × 0.877 84 | 85 | Y = 0.30R + 0.59G + 0.11B 86 | I = 0.60R − 0.28G − 0.32B 87 | Q = 0.21R − 0.52G + 0.31B 88 | ``` 89 | 90 | 91 | ## 7. 常见的图像文件格式 92 | 93 | - 静止图像的基本编码方式 94 | - 无损压缩(调色板、RLE、特定的压缩算法) 95 | - 有损压缩(JPEG) 96 | 97 | 98 | - GIF 99 | - 基于调色板,只能包含最多 256 种颜色 100 | - LZW 算法,一种无损压缩算法 101 | - 支持透明像素 102 | - 支持基于多帧静止图片的动画 103 | 104 | 105 | - PNG 106 | - 为替代 GIF 而生,支持多种像素格式 107 | - 支持 Alpha 分量 108 | - 使用基于 LZ77 的一种派生压缩算法 109 | 110 | 111 | - JPEG 112 | - 支持 RGB、YUV 及灰度色彩空间 113 | - 同时支持无损和有损压缩 114 | 115 | 116 | - 其他图像格式 117 | - WebP,Google 定义,派生自 VP8 视频编码技术 118 | - BMP,微软为 Windows 定义的位图格式 119 | - TIFF,多用于扫描仪和传真机 120 | 121 | 122 | ## 8. 抖动算法 123 | 124 | 使用调色板中可用颜色的扩散来获得近似效果,此时人眼会自动将扩散的不同颜色混合成新的颜色。 125 | 126 | 127 | ![色带效果](assets/chapter-5-1.png) 128 | 129 | 130 | ![抖动原理](assets/chapter-5-2.png) 131 | 132 | 133 | ![抖动效果](assets/chapter-5-3.png) 134 | 135 | 136 | ## 9. 矢量图形 137 | 138 | - 分清图像(image)和图形(graphics)的区别 139 | - 矢量图形:使用数学方法描述图形的轮廓和填充属性 140 | 141 | 142 | - SVG 143 | - 支持矢量图形对象,包括矩形、圆、椭圆、多边形、直线、任意曲线以及填充方式等。 144 | - 支持嵌入式外部图形,包括 P NG、JPEG 等外部图像以及外部 SVG 等。 145 | - 支持文字对象。 146 | - 支持各种滤镜和特殊效果。 147 | - 可嵌入 HTML 页面,支持动画。 148 | 149 | 150 | - SVG 样例 151 | 152 | ```svg 153 | 154 | 156 | 157 | 159 | 160 | 162 | 163 | 165 | 166 | ``` 167 | 168 | - 渲染效果 169 | 170 | ![矢量图形](assets/chapter-5-4.svg) 171 | 172 | 173 | ## 10. 基于静止图像的动画 174 | 175 | - GIF89a 176 | - 文件中包含多帧使用 GIF 编码定义的图像,轮换播出。 177 | - 除了第一帧之外,其他帧可定义局部变化。 178 | - MJPEG 179 | - 有多个同样分辨率的连续 JPEG 图像组成 180 | 181 | 182 | ## 11. 基于矢量图形的动画 183 | 184 | ```svg 185 | 186 | 188 | 189 | 191 | 演示 animation 元素 192 | 194 | 195 | 198 | 200 | 202 | 204 | 206 | 208 | 209 | 210 | 212 | 213 | 221 | 223 | It's alive! 224 | 226 | 228 | 231 | 234 | 237 | 238 | 239 | 240 | ``` 241 | 242 | 243 | - 渲染效果 244 | 245 | ![基于矢量图形的动画](assets/chapter-5-5.svg) 246 | 247 | 248 | ## 12. 音频信号的量化处理 249 | 250 | 音频数字化后的保真度取决两个因素: 251 | 252 | 1. 采样频率 253 | 1. 采样值量化位数 254 | 255 | 人耳可以听到的声音频率范围在 20Hz 到 20KHz 256 | 257 | 258 | 1. 电话质量 259 | - 8000 Hz 的采样频率 260 | - 8 位的采样值量化位数 261 | - 领导半个小时的讲话,则其原始的音频量化数据大小为:8000 * 60 * 30 = 14,400,000B,约为 13.4 MiB 262 | 263 | 1. 音乐质量 264 | - 44100 Hz 的采样频率 265 | - 16 位的量化位数 266 | - 半个小时的交响乐演奏以 CD 质量、双声道(立体声)做量化处理,则原始数据大小为:44100 * 2 * 60 * 30 * 2 = 317,520,000,约为 302 MB 267 | 268 | 269 | ## 13. 音频量化数据的压缩手段 270 | 271 | 1. 采样阶段完成,无损压缩 272 | 1. 对量化后的数据使用专用于音频数据的无损压缩算法,如 FLAC。 273 | 1. 将时域数据基于快速傅里叶变换或者小波变换而成为频域数据,仅记录重要的频率和振幅,属于有损压缩。 274 | 275 | 276 | ## 14. 采样阶段的无损压缩 277 | 278 | - PCM:脉冲码调制(Pulse Code Modulation),原始数据。 279 | - DPCM:差分脉冲码调制(Differential Pulse Code Modulation),约 50% 的压缩率。 280 | - ADPCM:自适应差分脉冲码调制(Adaptive Difference Pulse Code Modulation),约 25% 的压缩率。 281 | 282 | 283 | ## 15. MIDI 284 | 285 | - 类似矢量图形,我们可以将 MIDI 理解为一种音频的“矢量化”处理方法。 286 | - MIDI:乐器数字接口(Musical Instrument Digital Interface)。 287 | - 由美国加州的音乐人 Dave Smith 于 20 世纪 80 年代发明。 288 | 289 | 290 | 只要知道了音高、强度 、节奏(快慢和时值)以及音色(对应的谐振波组成),就可以使用数字方法合成出对应的音频信号。 291 | 292 | 293 | ## 16. MIDI 音色的两种实现方法 294 | 295 | 1. 合成谐振波合成某种音色,缺点是音色的还原度低,优点是简单,存储量小。 296 | 1. 波表合成:事先记录特定乐器之不同音符对应的音频信号,然后再根据每个音轨对应的音符强弱和时长合成在一起播放。 297 | 298 | 299 | ## 17. 视频的相关概念 300 | 301 | - 清晰度(definition) 302 | - 高清电视(HDTV,High Definition TV)的水平扫描线可达到 1080 条,每条扫描线上有 1920 个像素点,对应的屏幕分辨率为 1920x1080; 303 | - 超高清电视(Ultra HDTV,俗称的 “4K”),对应的屏幕分辨率为 3840×2160。 304 | 305 | 306 | - 帧率(frames per second) 307 | - 胶片电影:24 fps。 308 | - PAL 制式:25fps。 309 | - NTSC 制式 29.97fps。 310 | 311 | 312 | - 扫描模式: 313 | - 交错扫描(Interlaced Scan),也称隔行扫描。 314 | - 逐行扫描(Progressive Scan)。 315 | 316 | 317 | ## 18. 视频压缩原理 318 | 319 | - 帧内压缩:比如 Motion JPEG,将每一帧静态图像利用 JPEG 技术进行压缩。 320 | - 帧间压缩:第一帧画面的完整图像,然后对其后的每一帧,只记录和前一帧的差异数据,类似音频处理中的差分脉冲编码。 321 | - 码率(bit rate):视频内容的清晰度、帧率以及所采取的压缩技术,我们可以确定一个流畅展现一段视频内容所需要的最高传输速度。 322 | 323 | 324 | 播放分辨率为 1080p、帧率为 25 fps 的高清视频,采用 H.264 编码技术,尖峰时候的网络带宽要求大概为 10 Mbps。 325 | 326 | 327 | ## 19. 实例分析:MPEG-1 编码技术 328 | 329 | - YUV 色彩空间 330 | - 每一帧图像由三个部分组成: 331 | 1. 亮度 332 | 1. 色调 333 | 1. 饱和度 334 | - 亮度部分像素点数量在水平和垂直方向是色调和饱和度的两倍。 335 | 336 | 337 | 三种帧类型: 338 | 339 | 1. I-Frame(Intra-coded Image,内编码图像)。 340 | 1. P-Frame(Predictive-coded Image,预测编码图像)。记录的是其相对于之前的 I-Frame(以及其他前置 P-Frame)的变化部分。 341 | 1. B-Frame(Bi-directionally Predictive-coded Image,双向预测编码图像)。前一张静态图像和后续 I-Frame 或 P-Frame 之间的差异。 342 | 343 | 实践中 MPEG-1 的不同类型图像编码以下面的顺序出现:IBBPBBPBBIBBPBBPBB… 344 | -------------------------------------------------------------------------------- /slides/numbers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机软件方法导论:数:整数和实数 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

魏永明的计算机软件方法导论数:整数和实数

21 |

考鼎®

22 |
23 |
24 |
29 |
30 |
31 |

扫一扫

32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /slides/numbers.md: -------------------------------------------------------------------------------- 1 | ## 数:整数和实数 2 | 3 | - 为什么使用补码表示负数 4 | - 整数运算的陷阱 5 | - 浮点数 6 | - 定点数和任意精度 7 | 8 | 9 | ## 1. 正整数及负整数的表达 10 | 11 | * 补码的概念 12 | * 使用补码表达负整数的优点 13 | 14 | 15 | ## 2. 整数运算的陷阱 16 | 17 | * 溢出 18 | * 被零除 19 | 20 | 21 | ## 3. 任意实数的表述方式 22 | 23 | * 计算机只能有限或者近似表达任意实数 24 | * 实数的表述方法:浮点数、定点数和任意精度。 25 | 26 | 27 | ## 4. 浮点数原理 28 | 29 | * 数学上的科学计数法:x * 10 ^ E(abs(x) >= 1 && abs(x) < 10) 30 | * 光速:299792458 m/s,2.99792458E8 m/s 31 | * 计算机中的浮点数:x * 2 ^ E(abs(x) >= 1 && abs(x) < 2) 32 | 33 | 34 | ## 5. 浮点数类型 35 | 36 | ``` 37 | sign e base 38 | 16 1 5 10 39 | 32 1 8 23 (Single) 40 | 64 1 11 52 (Double) 41 | 96 1 15 64 (Double-Extended) 42 | 128 1 15 112 (Quadruple) 43 | ``` 44 | 45 | 46 | ## 6. 浮点数表述的特点 47 | 48 | * 特殊表达: 49 | - 零:0.0 和 -0.0。 50 | * 无限大(INF,Infinity)、无限小(-INF) 51 | * 非实数(NaN,Not a Number) 52 | 53 | 54 | ## 7. 浮点数的表述误差 55 | 56 | * 使用单精度浮点数表示十进制 0.1: 57 | 58 | ```c 59 | 0.100000001490116119384765625 60 | ``` 61 | 62 | * 0.1 的平方 0.01: 63 | 64 | ```c 65 | 0.010000000298023226097399174250313080847263336181640625 66 | ``` 67 | 68 | 69 | ## 8. 浮点数表述的特征值 70 | 71 | * EPSILON:两个可表述实数之间的最小间隙值。 72 | * MAX SAFE INTERGER:可以无误差表述的最大整数(正整数)。 73 | * MIN SAFE INTERGER:可以无误差表述的最小整数(负整数)。 74 | * MAX VALUE:可表述的最大正整数。 75 | * MIN VALUE:可表述的最小正整数。 76 | 77 | 78 | ## 9. 定点数的概念 79 | 80 | * 使用整数当中的一半比特位来表示整数部分,另外一半比特位来表示小数部分 81 | * 性能好,精度有限: 82 | * 实数的四则运算可使用整数进行,性能最好。 83 | * 其他非线性运算,可采用查表法。 84 | 85 | 86 | ## 10. 任意精度运算 87 | 88 | * bc:可支持 2,147,483,647 位小数点位数 89 | * gmp:GNU 的多精度运算库 90 | 91 | 92 | ## 11. 高级编程语言中的数 93 | 94 | * C、C++、Java 等,明确区分无符号整数、有符号整数以及浮点数 95 | * Python:整型、长整型、浮点数和复数。 96 | * JavaScript:抽象的实数,不区分整数及浮点数,内部实现一般使用 64 位浮点数表达。 97 | 98 | -------------------------------------------------------------------------------- /slides/qrcode-bilibili-space-coding-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/slides/qrcode-bilibili-space-coding-log.png -------------------------------------------------------------------------------- /slides/qrcode-coding-log.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/slides/qrcode-coding-log.jpg -------------------------------------------------------------------------------- /slides/qrcode-fmsoft-online-lectures-c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/slides/qrcode-fmsoft-online-lectures-c.png -------------------------------------------------------------------------------- /slides/qrcode-wechat-channel-coding-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VincentWei/the-basic-computer-software-methods/38f571b7af9415823e9b4299c71c18b24d5803e5/slides/qrcode-wechat-channel-coding-log.png -------------------------------------------------------------------------------- /slides/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机软件方法导论:文字:字符集及编码 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

魏永明的计算机软件方法导论文字:字符集及编码

21 |

考鼎®

22 |
23 |
24 |
29 |
30 |
31 |

扫一扫

32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /slides/text.md: -------------------------------------------------------------------------------- 1 | ## 文字:字符集及编码 2 | 3 | - 对文字进行编码的需求 4 | - 各种字符集及编码的混战 5 | - Unicode 的胜利 6 | - Unicode 字符集及其编码 7 | - 字符集/编码转换 8 | - 其他编码形式 9 | 10 | 11 | ## 1. 对文字进行编码的需求 12 | 13 | 听个故事 14 | 15 | 16 | ## 2. ASCII 字符集的特点 17 | 18 | * < 0x80 19 | * 分类: 20 | - 控制字符( < 0x20) 21 | - 制表符 22 | - 换行符 23 | - 空白字符(whitespace) 24 | - 大写字母 25 | - 小写字母 26 | - 标点符号 27 | 28 | 29 | ## 3. 问题 30 | 31 | 为什么 ASCII 只使用了七位二进制数据? 32 | 33 | 34 | ## 4. 如何表示英语之外的文字? 35 | 36 | * ASCII 只考虑到了英语 37 | * 其他语言怎么办? 38 | 39 | 40 | ## 5. 各种字符集及编码的混战 41 | 42 | * EUC:Unix 扩展编码(IBM) 43 | * ISO8859 系列字符集 44 | * GB2312 -> GBK -> GB18030 45 | * BIG5 46 | * ISO10646 47 | 48 | 49 | ## 6. 字符集和编码的区别 50 | 51 | 52 | ## 7. Unicode 的历史 53 | 54 | * Unicode 最早由微软、苹果、IBM 等发起。 55 | * 1991 年 Unicode 变身财团,发布了 Unicode 1.0。 56 | * 1997 年发布 Unicode 2.0。 57 | * Unicode 3.0 总共定义了 65,534 个码位(0xFFFE 和 0xFFFF 为非字符)。 58 | * 目前最新版本为 Unicode 14.0.0(2021 年 9 月发布)。 59 | 60 | 61 | ## 8. Unicode 的特点和术语 62 | 63 | * Unicode 有 0x10FFFF 个码点(code point)。 64 | * Unicode 14.0.0 总共定义了 144,697 个字符(character),其中也常用符号,如 Emoji 表情。 65 | * Unicode 14.0.0 总共定义了 159 种文字(script)。 66 | * 每个字符都有一个英文的名称,通常用 U+0020 这种表示方式。 67 | 68 | 69 | ## 9. Unicode 编码 70 | 71 | * UTF-32 72 | - 大小头 73 | - 固定长度编码 74 | * UTF-16 75 | - 大小头 76 | - 变长编码(2或者4字节) 77 | * UTF-8 78 | - 大小头无关 79 | - 变长编码(1到6个字节) 80 | - ASCII 兼容 81 | 82 | 83 | ## 10. 码点转 UTF-16LE 编码 84 | 85 | ```c 86 | static int utf16le_conv_from_uc32 (Uchar32 wc, unsigned char* mchar) 87 | { 88 | Uchar16 w1, w2; 89 | 90 | if (wc > 0x10FFFF) { 91 | return 0; 92 | } 93 | 94 | if (wc < 0x10000) { 95 | mchar [0] = LOBYTE (wc); 96 | mchar [1] = HIBYTE (wc); 97 | return 2; 98 | } 99 | 100 | wc -= 0x10000; 101 | w1 = 0xD800; 102 | w2 = 0xDC00; 103 | 104 | w1 |= (wc >> 10); 105 | w2 |= (wc & 0x03FF); 106 | 107 | mchar [0] = LOBYTE (w1); 108 | mchar [1] = HIBYTE (w1); 109 | mchar [2] = LOBYTE (w2); 110 | mchar [3] = HIBYTE (w2); 111 | return 4; 112 | } 113 | ``` 114 | 115 | 注:代码选自 [MiniGUI](https://github.com/VincentWei/minigui) 116 | 117 | 118 | ## 11. UTF-16LE 编码转码点 119 | 120 | ```c 121 | static Achar32 utf16le_get_char_value (const unsigned char* mchar, int len) 122 | { 123 | Uchar16 w1, w2; 124 | Uchar32 wc; 125 | 126 | w1 = MAKEWORD16 (mchar[0], mchar[1]); 127 | 128 | if (w1 < 0xD800 || w1 > 0xDFFF) 129 | return w1; 130 | 131 | w2 = MAKEWORD16 (mchar[2], mchar[3]); 132 | 133 | wc = w1; 134 | wc <<= 10; 135 | wc |= (w2 & 0x03FF); 136 | wc += 0x10000; 137 | 138 | return wc; 139 | } 140 | ``` 141 | 142 | 注:代码选自 [MiniGUI](https://github.com/VincentWei/minigui) 143 | 144 | 145 | ## 12. 为什么 UTF-8 编码获胜 146 | 147 | * Unix、C 语言作者之一肯·汤普逊提出。 148 | 149 | ``` 150 | 0x00000000 - 0x0000007F: 151 | 0xxxxxxx 152 | 153 | 0x00000080 - 0x000007FF: 154 | 110xxxxx 10xxxxxx 155 | 156 | 0x00000800 - 0x0000FFFF: 157 | 1110xxxx 10xxxxxx 10xxxxxx 158 | 159 | 0x00010000 - 0x001FFFFF: 160 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 161 | 162 | 0x00200000 - 0x03FFFFFF: 163 | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 164 | 165 | 0x04000000 - 0x7FFFFFFF: 166 | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 167 | ``` 168 | 169 | 注:UTF-8 编码现有的 Unicode 码点,最高只需要四个字节。 170 | 171 | 172 | ## 13. Unicode 相关算法(上) 173 | 174 | * UAX #9:双向(bidirectional)算法 175 | * UAX #10:定序(collation)算法 176 | * UAX #11:东亚宽度 177 | * UAX #14:换行算法 178 | * UAX #15:正规化 179 | 180 | 181 | ## 14. Unicode 相关算法(下) 182 | 183 | * UAX #24:文字属性 184 | * UAX #29:文本分段 185 | * UAX #38:汉字数据库(Unihan) 186 | * UAX #50:垂直文本布局 187 | 188 | 189 | ## 15. 为何 Unicode 最终胜利? 190 | 191 | 经济基础决定上层建筑 192 | 193 | 194 | ## 16. Unicode 引入的混乱 195 | 196 | * C 语言中的宽字符 `wchar_t`,不同平台宽度不一。 197 | * Java 语言中的 16 位宽的字符。 198 | * Python 2.0 对字符串不区分编码。 199 | * Python 3.0 中字符串内部使用 UTF-16 编码,其他编码的字符串使用字节序列表达。 200 | * Python 使用 `#coding:xxx` 告知解释器当前默认编码。 201 | 202 | 203 | ## 17. 字符集/编码的转换(C) 204 | 205 | ```c 206 | static Achar32 gb2312_0_char_to_achar32 (const unsigned char* pre_mchar, 207 | int pre_len, const unsigned char* cur_mchar, int cur_len) 208 | { 209 | int area = cur_mchar [0] - 0xA1; 210 | if (area < 9) { 211 | return (area * 94 + cur_mchar [1] - 0xA1); 212 | } 213 | else if (area >= 15) 214 | return ((area - 6)* 94 + cur_mchar [1] - 0xA1); 215 | return 0; 216 | } 217 | 218 | static const unsigned short __mg_gbunicode_map[] = { 219 | 0x3000, 0x3001, 0x3002, 0x30fb, 220 | 0x02c9, 0x02c7, 0x00a8, 0x3003, 221 | 0x3005, 0x2015, 0xff5e, 0x2225, 222 | 0x2026, 0x2018, 0x2019, 0x201c, 223 | 0x201d, 0x3014, 0x3015, 0x3008, 224 | ... 225 | 0x9eea, 0x9eef, 0x9f22, 0x9f2c, 226 | 0x9f2f, 0x9f39, 0x9f37, 0x9f3d, 227 | 0x9f3e, 0x9f44 228 | }; 229 | 230 | static UChar32 gb2312_0_conv_to_uc32 (AChar32 achar32) 231 | { 232 | return (UChar32)__mg_gbunicode_map[achar_value]; 233 | } 234 | ``` 235 | 236 | 237 | ## 18. 字符集/编码的转换(Python 3) 238 | 239 | * encode():将 Unicode 字符串编码为指定字符集/编码。 240 | * decode():将指定字符集编码字节序列编码为 Unicode 字符串。 241 | 242 | 243 | ```python 244 | # coding: utf-8 245 | 246 | str_uni = u"中文" // 定义一个 Unicode 字符串 247 | 248 | bytes_gbk = str_uni.encode("gbk") // 按 GBK 编码 249 | bytes_utf8 = str_uni.encode("utf8") // 按 UTF8 编码 250 | 251 | byets_gbk.decode("gbk") // 按 GBK 解码为 Unicode 字符串 252 | byets_gbk.decode("utf8") // 异常 253 | ``` 254 | 255 | 256 | ## 19. HVML 的设计 257 | 258 | * 字符串类型始终使用 UTF-8 编码,只保存有效的 Unicode 字符。 259 | * 其他所有字符集/编码统一使用字节序列类型处理。 260 | * 提供类似 Python 3 的 decode 和 encode 方法。 261 | 262 | 263 | ```js 264 | $EJSON.fetchstr(bxFFFE000048000000560000004D0000004C0000002F6600006851000003740000969900002A4E0000EF530000167F00000B7A000007680000B08B0000ED8B0000008A000001FF0000", "utf32") 265 | // string: "HVML是全球首个可编程标记语言!" 266 | 267 | $EJSON.fetchstr(bx00000048000000560000004D0000004C0000662F00005168000074030000999600004E2A000053EF00007F1600007A0B0000680700008BB000008BED00008A000000FF01, 'utf32be') 268 | // string: "HVML是全球首个可编程标记语言!" 269 | 270 | $EJSON.pack("i16le:2 i32le", [10, 15], 255) 271 | // bsequence: bx0A000F00FF000000 272 | 273 | $EJSON.unpack("i16le i32le", bx0a000a000000) 274 | // array: [10L, 10L] 275 | ``` 276 | 277 | 278 | ## 20. 文字的其他编码形式 279 | 280 | - Base64:只使用有限的 ASCII 字符表示二进制数据(三个字节扩展为四个字节表示) 281 | - URL 编码: 对 `-_.` 之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数,空格编码为加号(+) 282 | - URL 裸编码:对 `-_.` 之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数 283 | 284 | 285 | 286 | ```js 287 | $EJSON.unpack("utf8", $EJSON.base64_decode('SFZNTCDmmK/lhajnkIPpppbmrL7lj6/nvJbnqIvmoIforrDor63oqIA=')) 288 | ['HVML 是全球首款可编程标记语言'] 289 | 290 | $EJSON.unpack("utf8", $URL.rawdecode('HVML%20%E6%98%AF%E5%85%A8%E7%90%83%E9%A6%96%E6%AC%BE%E5%8F%AF%E7%BC%96%E7%A8%8B%E6%A0%87%E8%AE%B0%E8%AF%AD%E8%A8%80')) 291 | ['HVML 是全球首款可编程标记语言'] 292 | ``` 293 | 294 | -------------------------------------------------------------------------------- /slides/what-is-operating-system.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 计算机软件方法导论:什么是操作系统 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

魏永明的计算机软件方法导论什么是操作系统

21 |

考鼎®

22 |
23 |
24 |
29 |
30 |
31 |

扫一扫

32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /slides/what-is-operating-system.md: -------------------------------------------------------------------------------- 1 | ## 操作系统的本质和技术生态 2 | 3 | - 混乱的认知 4 | - 操作系统的本质和新定义 5 | - 操作系统的技术生态 6 | 7 | 8 | ## 1. 混乱的认知 9 | 10 | 1. 错误使用 11 | * 将内核等同于操作系统 12 | * “微信上可以跑小程序,所以微信也是操作系统” 13 | 1. 操作系统概念的泛化 14 | * 城市大脑操作系统 15 | 1. 滥用生态 16 | * “开发个操作系统容易,难在生态建设”。但到底什么是操作系统的生态? 17 | 18 | 19 | ## 2. 操作系统定义的反思 20 | 21 | > 操作系统是管理计算机硬件与软件资源的计算机程序。 22 | 23 | 上面这个定义过于简单,忽略了如下要点: 24 | 25 | 1. 操作系统对计算机的硬件资源提供了或低级或高级的抽象管理能力,方便开发者开发应用软件。 26 | 1. 操作系统为用户提供了和计算机进行交互的操作界面以及工具。 27 | 28 | 29 | ## 3. 操作系统的本质 30 | 31 | 1. 一个东西的本质,指将这个东西区别于其他东西的特征。 32 | 1. 操作系统的本质,就是指一个操作系统区别于另一个操作系统的技术特征。 33 | 34 | 35 | ### 3.1 观点 36 | 37 | 操作系统对硬件的抽象方法构成了操作系统的本质特征 38 | 39 | - 编程语言 40 | - 应用框架 41 | - 应用开发接口 42 | 43 | 44 | ### 3.2 实例 45 | 46 | - Unix/Linux:基于 C 语言的 POSIX 接口。 47 | - DOS:基于 Int21 系统服务中断的汇编接口; 基于 C 语言的系统功能接口封装。 48 | - Windows:基于 C 语言和窗体的 Win32 接口;基于 C++ 语言和文档视图的 MFC;C# 的 .Net 框架。 49 | - macOS:基于 Objective C 和进程和窗体的接口(正在向 Swift 编程语言演进)。 50 | - Android:基于 Java 语言和 Activity 的接口(正在向 Dart 编程语言演进)。 51 | - iOS/tvOS/padOS/watchOS:基于 Objective C/Swift 语言和 UIKit 框架的接口。 52 | 53 | 54 | - ChromeOS:Web 前端框架及接口。 55 | - Fuchsia OS:谷歌正在开发中的下一代操作系统。 56 | 57 | 58 | ### 3.3 结论 59 | 60 | 1. 操作系统对应用程序的接口,是操作系统的本质,是一个操作系统区别于另一操作系统的技术特征,是操作系统的基因或者灵魂。 61 | 1. 操作系统的演进过程,就是操作系统对计算机硬件的抽象层次逐步提升的一个过程。 62 | - 更友好的编程语言。 63 | - 更好的开发工具。 64 | 1. 操作系统完成了更多的细节工作,开发者更加轻松。以多媒体播放和来电播放的协调为例, 65 | - POSIX:应用程序要自己处理。 66 | - 智能手机:应用程序不需要自己操心。 67 | 68 | 69 | ## 4. 操作系统的更严谨定义 70 | 71 | 操作系统是一种特殊的计算机程序,介于硬件和应用程序之间;它通过抽象的软件资源(如进程、窗体、服务等)来管理计算机的硬件资源,以方便应用开发者利用其提供的应用编程接口(API)及其工具来开发、调试和部署应用程序。 72 | 73 | 74 | ### 4.1 要点 75 | 76 | 1. 操作系统必须直接管理硬件 77 | 1. 操作系统为开发者服务 78 | 79 | 80 | ### 4.2 拨乱反正 81 | 82 | 1. 我们不能把内核称为操作系统,内核加上基础库、运行时环境以及开发、调试甚至应用的部署工具,才能称为一个操作系统。 83 | 1. 虽然微信提供了基于小程序的应用运行环境,但微信不是操作系统,因为微信管理不了计算机硬件。 84 | 85 | 86 | ### 4.3 兼容 POSIX 有错吗? 87 | 88 | 没错,新操作系统不兼容 POSIX 必死。因为 POSIX 已经成为操作系统和应用程序的基准和常识,而且 POSIX 不由单个厂商把控。但作为操作系统的厂商,你要考虑的是,除了兼容 POSIX,你的安身立命之本是什么?更好的实时性扩展、性能扩展、编程语言、应用框架还是开发工具? 89 | 90 | 91 | ### 4.4 兼容 Android 有错吗? 92 | 93 | 手机厂商角度看,没错;操作系统厂商角度看,大错特错: 94 | 95 | 1. 哲学角度看,兼容别人,你就丢失了自己的灵魂(忒修斯之船)。 96 | 1. Android 的接口及其演进由谷歌把控,你永远跟不上谷歌的步伐。 97 | 1. 为何 macOS 从来不提兼容 Windows?没有自己的灵魂,还做什么操作系统? 98 | 1. 没有设计能力、也没有自信的操作系统,才会首先想到兼容其他操作系统。 99 | 100 | 101 | ### 4.5 如何看 Windows 引入 WSL 和 Android 运行时? 102 | 103 | 1. Windows 拥有强大的基因,放一个兼容 Linux 的子系统,不会撼动自身的根基,反而会增强自己的技术生态。 104 | - WSL 让更多的 Linux 开发者留在 Windows 桌面上。 105 | 1. 作为通用操作系统,整合各种垂直技术生态是非常常见的,比如 Python、Java、Node.JS 等。 106 | - Android 其实并不是一个类似 Python 这样的垂直技术生态,所以 Windows 上引入 Android 的效果还有待观察。 107 | 108 | 109 | ## 5. 操作系统的技术生态 110 | 111 | 1. 操作系统的生态可以划分为技术生态和商业生态两个方面。 112 | 1. 技术生态指的是围绕操作系统接口形成的上下层软件之间的配合关系,向下延伸到芯片和硬件外设,向上延伸到应用和云端。 113 | 1. 商业生态指的是产业链上的厂商之间的配合关系。 114 | 1. 商业生态依附于技术生态。没有好的技术生态,商业生态是很难建立的。 115 | 116 | 117 | ### 为什么要单独提操作系统的技术生态? 118 | 119 | - 很多人抱怨操作系统的生态难搞。 120 | - 根本原因是,很多所谓国产自主操作系统,压根就不重视技术生态,只想搞商业生态。 121 | 122 | 123 | ## 6. 操作系统技术生态的内涵 124 | 125 | 1. 操作系统介于硬件和上层应用之间,故而,我们谈到操作系统接口的时候,除了面向应用的 API(应用编程接口)之外,还要看到另一个接口,就是向下面向芯片和硬件的接口。 126 | 1. 好的应用开发接口可有效帮助开发者开发应用,而好的芯片架构支持和驱动程序开发接口,可以帮助芯片厂商和硬件厂商快速适配新的芯片和硬件。 127 | 1. 特定操作系统的技术生态,由围绕这个操作系统的开发者构成。 128 | 129 | 130 | ### 6.1 GNU/Linux 的成功 131 | 132 | 1. Linux 在服务器、嵌入式等领域的成功不是偶然的: 133 | - FSF 的 GNU 项目奠定了大量基础,GNU 的 Hurd 内核半死不活,而 Linux 内核的出现恰逢其时; 134 | - Linux 内核使用 GPLv2 许可证平衡了索取和贡献,保证了商用免费; 135 | - Linux 内核借它的内部接口形成了自己的技术生态系统。 136 | 137 | 138 | 1. 作为内核,Linux 向上层提供了符合 POSIX 标准的接口,而向下为硬件外设提供了稳定和统一的驱动程序接口。正是因为 Linux 为硬件提供了统一和稳定的驱动程序接口,使得 Linux 成为硬件生产商开发计算机硬件外设时的首选,这让 Linux 内核中存在着大量的各类驱动程序,进而聚拢了大量的内核开发者,而这反过来又促进了 Linux 内核的广泛应用。 139 | 140 | 141 | ### 6.2 Linux 在桌面系统上的惨败 142 | 143 | 二十年来,Linux 桌面的问题一直就是这么几个: 144 | 145 | 1. 没有好的应用,没有好的游戏。 146 | 1. 兼容 Windows 的 WINE,技术问题很多。 147 | 1. 普通用户使用困难。 148 | 149 | 150 | ### 6.3 根本原因 151 | 152 | 1. 作为桌面,Linux 的窗口系统、GUI Toolkit 没有任何技术特色,相反理念陈旧,开发效率低下。 153 | 1. 截止目前,没看到任何一个 Linux 桌面系统以建立具有独特技术优势的技术生态为目的来打磨产品。 154 | 155 | 156 | ## 7. 总结 157 | 158 | 1. 操作系统的创新,向下围绕芯片和硬件支持进行,向上围绕应用开发进行。 159 | 1. 把 Linux 内核重写一遍不带来任何的技术创新。 160 | 1. 不为应用开发者考虑,不给开发者带来切实的好处的操作系统是不会有生命力的。 161 | 1. 为操作系统设计抽象层次更高的编程语言和接口,从而注入独特的基因,才能构建一个好的操作系统技术生态。 162 | 1. 要建设操作系统的商业生态,没有好的技术生态做保证,最终会失败。 163 | 164 | -------------------------------------------------------------------------------- /textbook/foreword.md: -------------------------------------------------------------------------------- 1 | @SUBJECT [计算机软件技术导论](toc.md) 2 | @AUTHOR [魏永明](https://github.com/VincentWei) 3 | @DATE 2015-xx-xx 4 | @LANG 简体中文 5 | @COPYING 版权所有 © 2022 魏永明 6 | @LICENSE 保留所有权利 7 | 8 | # 引言 9 | 10 | ## 初衷和目标 11 | 12 | 和二十年前相比,计算机的处理能力已经有了成倍的增长。现在,几乎人手一部的智能手机的硬件配置或运算能力都要比二十年前的大型主机还要强。“全民编程”看似遥远而触不可及,但作为一个趋势,可能会在二三十年内变成现实。届时,编程有可能会成为中学的必修课或者小学高年级的选修课。然而,计算机技术的飞速发展,使得相关知识呈爆炸式增长。如何在纷繁复杂的计算机编程语言、操作系统或平台、技术词汇、新技术的海洋中做到游刃有余,将是新一代程序员需要面临的实际问题。 13 | 14 | 笔者从上个世纪 90 年代开始接触编程,曾在 DOS、Windows、Linux 等操作系统上,使用 C、C++、JavaScript、HTML、PHP、SQL 等多种编程语言开发各种各样的计算机程序。总结笔者几十年的编程经验,我发现,不论计算机技术如何发展,其实我们可以用一个比较清晰的脉络来总结计算软件的技术或者知识,掌握了这些知识点,你将有“一览纵山小”的感觉——任何一个程序员都可以相对快速地学习新的操作系统、平台或者编程语言,从而将主要精力放在自己的应用程序上,而不是平台或者编程语言的细节上。 15 | 16 | 迄今为止,尚未有一本这样的书,以科普的方法来阐述计算机程序和编程的内在实质。大部分计算机相关技术书籍主要讲述某个特定的技术,比如特定的编程语言或者特定的平台开发技术等等。而那些存在于几乎所有的平台和编程语言上的共性,却鲜有书籍来讲述,或者仅存在于大学的专业教科书中,而这些教科书大多纵向阐述知识,鲜有横向阐述知识的教科书。 17 | 18 | 另外,尽管我们尚未进入“全民编程”时代,但各种和科技相关联的行业研发人员,都需要进行或多或少的编程。对这些人员来讲,如果可以快速掌握基础的计算机技术相关知识点,少走弯路,也将成为相关企业或者科研单位在激烈的市场竞争中立于不败之地的重要助推因素。其实,就算是互联网等计算机行业,现在也有大量非计算机相关专业的毕业生在从事编程工作;而软件产品的设计人员(产品经理、视觉设计人员甚至运营人员),也需要了解一些基本的计算机技术。因此,本书涉及的内容对这些人员来讲,将非常有帮助。 19 | 20 | 更进一步,计算机相关技术的发展速度非常快,许多技术可能在很短的时间内(比如一两年)变得过时,甚至无人问津。但不管是什么样的技术,只要曾被大规模使用,都会是计算机技术发展过程中必然要走过的道路。如果我们在学习新技术之前知悉其演进历史,也将帮助我们正确理解新技术,从而让我们有一种豁然开朗的感觉。但由于各种原因,新的程序员未必知悉某个技术的前世今生,从而在某种程度上阻碍了我们对这些技术的正确把握,也不利于我们正确把握计算机技术发展的节奏或“心跳”。 21 | 22 | 因此,本书旨在揭示计算机程序和编程的内在本质,以及相关技术的发展演进过程,从而为读者呈现一个完整的、清晰的计算机软件相关技术的发展脉络及内在联系。同时,本书力求用浅显、简洁的语言来阐述相关的技术和知识点;笔者的目的是,只要读者具有高中的数学知识并且成功编写过几百行程序,就能看懂本书所讲内容。本书不包含深奥的数学推导或者复杂的图表,笔者力求仅通过文字和简单的示意图将看似高深的计算机软件相关理论知识展现在读者面前。笔者期望读者读取此书,是一种享受而不是煎熬。 23 | 24 | 当然,对笔者来讲,完成这个任务将是一项巨大的挑战。2015 年,笔者恰好有一段闲暇时间,故而写就了本书规划中的第一篇。现今是 2022 年,15 年到现在的 7 年间,这个世界发生了很多巨大的变化,其中对中国影响最深远的,应该就是中美之间剑拔弩张的科技竞争了。如果放在这个历史背景下考量,这本书的内容对中国基础软件行业,甚至是整个信息技术行业进而辐射到全部的科技行业,都具有非常重要的历史意义。 25 | 26 | 然而,一个人的精力毕竟有限。笔者还有很多同等重要的事情要做,比如 HVML 编程语言的开发,MiniGUI 的更新以及用来糊口的生计等等。故而,我希望更多的人能够参与到这本书的撰写当中——就如同 2020 年,几个人通过协作撰写了中国第一部码农体长篇小说《考鼑记》一样。 27 | 28 | ## 编程的瓶颈 29 | 30 | 尽管计算机软件技术的发展可以用一日千里来形容,但如果我们仔细看其发展历史,会发现,基础的东西并没有变化多少。比如,C 语言和 UNIX 在上个世纪七十年代末发明出来,到现在已经有三十多年的历史,但从发明到现在,这两样东西并没有太大的变化,而且仍然非常流行。再比如,R 语言专为统计和绘制统计图表而诞生于上个世纪八十年代,很长一段时间内,很多人不知道有这样一种编程语言,但随着大数据的流行,R 语言再次被使用并发挥着巨大的作用。 31 | 32 | 另一方面,大量科学研究和开发人员开始使用各种各样的工具和编程语言编写计算机程序。虽然学习一门编程语言并使用该语言编写程序的门槛越来越低(毕竟大量的书籍、开源软件、专业博客站点、问答网站[^1]等为我们提供了最短的学习路径),但是,程序的质量并没有太大提升。当然,程序质量的度量本身是一个比较复杂的事情,但我们可以从越来越庞大的程序包,复杂的源代码规模,运行起来的速度越来越慢并出现卡顿、掉线等各种作为用户的切身体会中感受到:大量的程序并没有经过良好的设计和优化。 33 | 34 | 究其原因,是因为大部分程序员仅依赖于自己的“直觉”进行编程——他们仅仅将编程的重点放在功能而非性能、源代码的易读性、设计上的可扩展性、团队协作上的可维护性、规模上的可伸缩性等等方面。这就是当前程序员普遍遇到的编程瓶颈。 35 | 36 | 在移动互联网时代,市场竞争如此激烈,占得先机圈用户成了大部分互联网企业或创业团队的第一目标。甚至 Facebook 还提出了“Done always better than perfect”(其含义是“实现始终强于完美”)。进而,“先实现再优化”变成了一种流行的软件工程方法。笔者本人赞同这种软件工程方法,毕竟一个软件产品的开发过程涉及多种人员,如果有一个原型实现,然后再讨论改善要比纸上谈兵好很多。但是,遵循“先实现再优化”的原则,并不是说一开始就不需要设计以及必要的优化[^2]考量。 37 | 38 | 但如果我们只看到这一句话,那就会被大大地误导。要知道,Facebook 这种大公司的工程师水平应该是世界一流的,所以,就算是采纳这种策略,软件产品的质量也不会差到哪里去。 39 | 40 | 后来,作为对此原则的补充,Facebook 在 2013 年时提出了“全栈工程师[^3]”的概念,并声称 Facebook 只招聘全栈工程师。显然,Facebook 也意识到,只有从一开始就将软件设计上的普遍规律深深植入到软件产品中,才能保证软件产品在一开始不会出现大的技术纰漏。 41 | 42 | 而这些软件设计上的普遍规律就是本书要讲述的内容。一个软件工程师,必须将这些普遍性的知识和规律根植到自己的大脑中,才能锻炼自己成为一名“全栈工程师”,或者起码成为一名高级软件工程师,一名不仅仅依赖“直觉”编程的工程师,才能在面对新的技术时可以从容应对,快速学习并掌握这些新的技术。当然,这些基础的知识也可以为我们的日常工作提供帮助,帮助我们提高工作效率。 43 | 44 | 有一次,笔者所在的公司需要从一个有千万条记录的数据库表中抽取一些记录并将其保存到另外一个表中。一名指派完成此项工作的工程师好几天加班加点却没有丝毫进展。笔者感觉非常奇怪,仔细一看发现,因为符合条件的记录在整个表中非常稀疏(大概只占10%左右),将这些记录使用 SQL 语句查询出来将耗费大量的服务器资源,甚至会影响[^4]数据库其他功能的正常运行。另外,因为是从远程计算机上执行查询的,因此,如果一个查询的结果集非常大,将带来数据的传输问题[^5]。如何既高效又在不影响现有数据库功能的要求下完成此项工作就成了棘手的问题。 45 | 46 | 最终,我们使用了分段查询的方法解决了这一问题。简单来讲,就是按照记录的插入时间一天一天处理这些记录的子集,而恰好记录的插入时间字段是有索引的。这样,查询的结果集将变小,服务器资源占用将变小。在此基础上,使用 Python 语言编写一段不超过100行的程序,即可让其循环执行,从而完成此项工作。 47 | 48 | 在上面这个案例中,假如数据库表中的记录只有几十条,要完成类似的工作,大部分程序就会使用“直觉”进行编程,因为数据量非常少,不会遇到任何性能或者数据传输上的问题。问题将发生在面对千万条或更多记录的情况。假如完成此项工作的程序员恰好没有相关经验,那这个问题可能要耗上一两天时间还无法找到问题的症结所在,而且他可能会将时间花费在其他方面,比如: 49 | 50 | - 他会怀疑是不是 Python 语言或者使用的模块有问题?于是可能会换成 PHP 或者其他语言来完成这个工作,而为了学习这些语言或者找到适当的操作函数库,又要花费大量的时间。他不知道,一个普遍使用的语言或者模块,就算有缺陷,也早就被人发现并解决掉了,这方面的怀疑几乎没有任何价值。 51 | - 是不是网络配置上的原因导致网络传输发生中断?于是会调整各种网络参数,试图让传输可以正常完成。他不知道,远程获得大型查询结果集的问题,早就可以通过游标这种东西解决。 52 | - 如此等等…… 53 | 54 | 显然,没有相关基础知识,不了解软件或程序的普通规律的工程师,最大的问题就在于无法从根本上快速定位问题所在,甚至将大量时间浪费在其他方面。 55 | 56 | 根据笔者从业几十年的观察,当前程序员缺乏的并不是如何灵活使用某种编程语言,而是一些最基本的计算机软件基础知识。这些基础知识就算是计算机专业毕业的工程师,也鲜有全面掌握的。这在很大程度上将阻碍我们国家的信息技术发展。考虑到“全民编程”时代可能首先在美国成真,这本书的出现已“时不我待”! 57 | 58 | ## 本书内容的组织 59 | 60 | 归根结底,计算机程序所处理的东西是数据,或者用更加准确的词来讲就是“信息”。因此,本书将从信息的表述、存储、处理、传输、展现等角度阐述相关的技术或者知识。同时,还将用独立的篇章阐述编程语言、操作系统以及软件工程相关的内容。 61 | 62 | 每一篇最后一章,笔者将总结相关主题的基础方法,其中包括相关技术的演进和进化目标。计算机相关技术(包括硬件和软件技术)在快速演进,最近几年的演进速度尤其快,这和生物的进化有一些类似之处。我们了解计算机软件相关技术,最终目的是通过掌握“术”来把握其中的“道”,这也是笔者撰写这本书的主要目的。 63 | 64 | 在重要的计算机科学相关历史人物第一次出现时,本文会使用脚注的形式给以简单描述人物的主要贡献。在相关术语第一次出现时,会使用括号给出对应的英文术语。为了准确理解一些术语,本书附录B还给出了重要或易混淆的术语之解释,可供读者查阅。附录 C 则给出了全书所有代码或程序的快速查询索引,以方便读者查找。 65 | 66 | 除引言、序言、后记及附录外,本书共分七篇,各篇章节如下: 67 | 68 | - 第一篇:信息的计算机表述。计算机归根结底是为人服务的,所以,信息的计算机表述实质上追求的是一种“人机共读”的境界。一方面,我们期望计算机可以非常容易地理解人,但另一方面,因为物理机制的限制,计算机又无法做到这一点。如此一来,计算机软件技术的发展过程其实就是追求极致“人机共读”境界的过程。 69 | - 第 1 章 为什么是二进制? 70 | - 第 2 章 二进制及其运算。 71 | - 第 3 章 数:整数及浮点数。 72 | - 第 4 章 文字:字符集及编码。 73 | - 第 5 章 多媒体:图像及音视频。 74 | - 第 6 章 抽象对象及结构化数据。 75 | - 第 7 章 有关信息表述的方法总结。 76 | - 第二篇:信息的计算机存储。没有存储的计算机寸步难行。记忆不仅仅是为了持久保存一样东西,记忆也是完成一项计算任务的重要手段。 77 | - 第 8 章 文件系统。 78 | - 第 9 章 关系数据库。 79 | - 第 10 章 NoSQL 数据库。 80 | - 第 11 章 分布式存储。 81 | - 第 12 章 有关信息存储的方法总结。 82 | - 第三篇:信息的计算机处理。处理信息是计算机的核心功能。而为了让计算机更快、更好地处理信息,科学家和工程师设计了各种方法。 83 | - 第 13 章 常见算法。 84 | - 第 14 章 压缩及加密。 85 | - 第 15 章 大数据处理。 86 | - 第 16 章 人工智能。 87 | - 第 17 章 有关信息处理的方法总结。 88 | - 第四篇:信息的计算机展现。这部分内容仍然和“人机共读”相关联,但更加具体。 89 | - 第 18 章 字体。 90 | - 第 19 章 矢量图形。 91 | - 第 20 章 HTML 及 CSS。 92 | - 第 21 章 图形界面及交互。 93 | - 第 22 章 典型文件格式。 94 | - 第 23 章 有关信息展现的方法总结。 95 | - 第五篇:信息的计算机传输。沟通是人类的本质需求。计算机之间的数据传输需求带来了互联网的发明及发展,而信息洪流的出现又极大促进了计算机和互联网技术的发展。 96 | - 第 24 章 互联网及 TCP/IP 协议。 97 | - 第 25 章 常见应用层协议。 98 | - 第 26 章 远程过程调用及数据随动。 99 | - 第 27 章 物联网及相关传输协议。 100 | - 第 28 章 有关信息传输的方法总结。 101 | - 第六篇:计算机编程语言。编程语言的本质上就是不同的编码规则,是为了将算法变成“人机共读”之物而努力的结果。 102 | - 第 29 章 编程语言的本质及分类。 103 | - 第 30 章 面向对象编程。 104 | - 第 31 章 Web 编程。 105 | - 第 32 章 设计新的编程语言。 106 | - 第七篇:操作系统。操作系统是现代计算机软件体系中最关键的“栈”。操作系统管理计算机系统资源的方式,体现了人类博大无边的智慧思想:分类归纳、抽象…… 107 | - 第 33 章 通用操作系统。 108 | - 第 34 章 实时操作系统。 109 | - 第 35 章 智能设备操作系统。 110 | - 第 36 章 操作系统的本质及未来。 111 | - 第八篇:软件工程方法。你不是一个人在战斗。软件工程探寻的是一种将软件的开发工程化的方法。在短短几十年的软件发展史中,人类已经找到了多种组织软件开发的软件工程方法,但没有哪一种是放之四海而皆准的。 112 | - 第 37 章 软件工程方法的演进。 113 | - 第 38 章 敏捷开发模型。 114 | - 第 39 章 开源软件及开源协作模型。 115 | - 第 40 章 没有唯一、普适的软件工程方法。 116 | - 第九篇:计算机软件技术的发展热点。计算机软件接下来将走向何方?世界上顶尖的科学家或工程师正在试图解决什么样的计算机软件问题? 117 | - 第 41 章 新的编程语言。 118 | - 第 42 章 新的WEB开发技术或框架。 119 | - 第 43 章 下一代操作系统。 120 | - 第 44 章 人工智能及大数据。 121 | - 第 45 章 云计算。 122 | - 第 46 章 虚拟化技术。 123 | - 后记:计算机软件技术发展的哲学思考。 124 | - 附录 A:伪代码及其语法。 125 | - 附录 B:重要或易混淆术语。 126 | - 附录 C:程序或算法索引。 127 | 128 | (下面是原来规划的内容。这些内容被整合到第九篇中了。) 129 | 130 | - 第八篇:虚拟化技术。既然编码是一种映射,那为何不能让一个操作系统运行在另外一个操作系统上呢?甚至,让硬件用软件模拟?虚拟化技术就是解决这一问题的。 131 | - 第 35 章 虚拟化技术概述。 132 | - 第 36 章 软件虚拟化技术(Virtual Box、Kvm、Xen 及 Docker 等)。 133 | - 第 37 章 硬件虚拟化(Qmenu)。 134 | - 第 38 章 计算机仿真。 135 | - 第 39 章 云计算。 136 | - 第九篇:大数据处理。微信朋友圈开始展示广告了!屌丝看到的是“可口可乐”,白富美看到的是“香奈儿”,高帅富看到的是“宝马”… 137 | - 第 39 章 互联网时代的数据大爆发。 138 | - 第 40 章 MapReduce。 139 | - 第 41 章 Hadoop 及 Spark。 140 | - 第十篇:人工智能。人类很矛盾,一方面期望机器能和人一样啥事儿都能做,一方面又害怕有朝一日机器会取代人类,甚至毁灭人类。不过还不用着急,我们先看看现在的机器和人有多大的差距。 141 | - 第 47 章 从专家系统到人工神经网络。 142 | - 第 48 章 从模式识别到机器知觉、情感。 143 | - 第 49 章 机器学习及自主意识。 144 | - 第 50 章 深度学习。 145 | 146 | ## P 语言及程序片段 147 | 148 | 为说明某些程序处理方法,本书需要使用代码来描述对应的处理逻辑或算法。大部分情况下,笔者使用伪代码(pseudocode)程序。本书使用的伪代码之详细语法描述可参阅附录A。注意,本书某些小节所阐述的问题可能仅出现在(或仅适应于)特定的编程语言,如 C 语言或其他编程语言当中,此种情况下,相关内容会组织成为独立的小节。如果这些特定的编程语言不是您所熟知的,则可以跳过对应的小节,等需要了解的时候再来阅读不迟。 149 | 150 | 整体上,本书所使用伪代码语言属于强类型函数式语言,其语法规则主要来自于 C 语言、Python 语言以及 JavaScript 语言。为适应网络及书本页面,便于读者阅读,该语言有如下特点: 151 | 152 | - 强类型语言。要求每个变量都要明确定义其类型。 153 | - 大小写敏感,且使用大写来表示关键词,如 FUNCTION,IF 等;在本书中,变量、函数名等使用小写字母。 154 | - 使用缩进来控制代码的逻辑块,避免使用过多括号或者关键词,每条语句行尾不使用分号。 155 | 156 | 为描述方便,我们将本书使用的伪代码语言称为 P 语言[^6]。以下通过示例简要描述该语言特性,更详细的描述请参阅本书附录A。 157 | 作为示例,我们给出一段输出斐波那契[^7]数列的 P 语言程序。在数学上,斐波那契数列就是 0, 1, 1, 2, 3, 5, 8 这种形式的数列,其规律是从第三个数开始,后面的数是前两个数的和。我们可以使用递归函数定义这个数列: 158 | 159 |
160 |
161 |
162 |
163 |
164 | 165 | 使用上述递归函数,我们输出斐波那契数列的 P 语言程序如下所示: 166 | 167 | ``` 168 | 1. FUNCTION VOID output_fibonacci (WORD first = 0, WORD second = 1) 169 | 2. WORD third = first + second 170 | 3. IF (third < second) 171 | 4. RETURN 172 | 5. 173 | 6. STDIO.printl ($third) 174 | 7. output_fibonacci (second, third) 175 | 8. 176 | 9. output_fibonacci () 177 | ``` 178 | 179 | 该代码的第1行到第 7 行定义了一个函数(FUNCTION),其名称为 `output_fibonacci`。该函数接受两个 WORD 类型(16位二进制正整数)的参数,但不返回任何值。这个函数的两个参数均有默认值,分别为 0 和 1,这就是斐波那契数列的两个初始值。在这个函数中,程序首先计算新的斐波那契数(第 2 行),并赋值给 `third` 这个变量。第 3 行在形式上是判断 `third` 是否小于 `second`(IF 语句),但其实是在判断两个 WORD 类型参数的加法是否出现了溢出。如果出现溢出情况,则函数将返回,否则调用 STDIO 类提供的 `printl` 函数打印新产生的 `third` 的值(其中使用了 P 语言的 `$` 运算符,该运算符将给定的参数转换成十进制文本,即字符串),然后再次(递归)调用本函数。递归调用时,将 `second` 作为第一个参数传入,而 `third` 作为第二个参数传入。 180 | 181 | 在上面代码的第 9 行,程序没有传入任何参数调用了 `output_fibonacci` 函数。 182 | 183 | 这段代码的执行将从第 9 行开始(第 1 行到第 7 行的代码定义了 `output_fibonacci` 函数,仅在调用该函数时才会执行),其输出大致如下: 184 | 185 | ``` 186 | 1 187 | 2 188 | 3 189 | 5 190 | 8 191 | 13 192 | 21 193 | 34 194 | 55 195 | 89 196 | 144 197 | 233 198 | 377 199 | 610 200 | 987 201 | 1597 202 | 2584 203 | 4181 204 | 6765 205 | 10946 206 | 17711 207 | 28657 208 | 46368 209 | ``` 210 | 211 | 这段代码对应的 C 语言程序如下所示,熟悉 C 语言的读者可以自行对比: 212 | 213 | ```c 214 | #include 215 | 216 | typedef unsigned short WORD; 217 | 218 | void output_fibonacci (WORD first, WORD second) 219 | { 220 | WORD third = first + second; 221 | if (third < second) 222 | return; 223 | 224 | printf ("%d\n", third); 225 | output_fibonacci (second, third); 226 | } 227 | 228 | int main (void) 229 | { 230 | output_fibonacci (0, 1); 231 | } 232 | ``` 233 | 234 | 除了递归方法依次产生斐波那契数列之外,我们还可以通过如下数学公式[^8]计算给定位置的值: 235 | 236 |
237 |
238 |
239 | 240 | 相应 P 语言程序如下: 241 | 242 | ``` 243 | FUNCTION DWORD get_fibonacci (DWORD n) 244 | IF (n <= 0) 245 | RETURN 0 246 | IF (n = 1) 247 | RETURN 1 248 | 249 | FLOAT constant_a = (1 + MATH.sqrt(5)) / 2 250 | FLOAT constant_b = (1 – MATH.sqrt(5)) / 2 251 | FLOAT constant_c = MATH.sqrt(5) / 5 252 | FLOAT fib = (constant_c * (MATH.pow (constant_a, n) – MATH.pow (constant_b, n)) 253 | 254 | RETURN (DWORD)fib 255 | } 256 | 257 | BYTE i 258 | FOR (i = 2; i < 25; i++) 259 | STDIO.printl ($get_fibonacci (i)) 260 | ``` 261 | 262 | 上述代码定义了 `get_fibonacci` 函数,该函数根据斐波那契数列的序数计算对应位置的斐波那契数,其中使用 MATH 类提供的数学计算函数,如 `sqrt`、`pow` 等。在主执行代码中,使用 FOR 循环调用 `get_fibonacci` 函数,并使用 STDIO 类提供的 `printl` 函数输出了计算获得的斐波那契数。上述代码的执行效果和递归计算斐波那契数列的代码是一样的。 263 | 264 | 下面的代码展示了如何使用 P 语言的数组来生成斐波那契数列: 265 | 266 | ``` 267 | WORD[] fibs = [0, 1] 268 | 269 | BYTE i 270 | FOR i IN RANGE (2, 24) 271 | fibs [] = fibs[-2] + fibs[-1] 272 | ``` 273 | 274 | 其中,`WORD[]` 定义了一个 WORD 类型的数组;`FOR i IN RANGE (2, 24)` 等价于上面的 `FOR (i = 2; i < 25; i++)` 语句。另外,`fibs [] =` 用于向 `fibs` 数组追加一个新的成员,`fibs[-2]` 和 `fibs[-1]` 分别引用了 `fibs` 数组中倒数第二个成员和最后一个成员。 275 | 276 | ## 常用的软件文档记法 277 | 278 | 在计算机软件文档中,有一些常用的约定俗成的记法。比如我们在 Windows 系统中查看某个命令的用法时,经常会看到下面的内容: 279 | 280 | C:\>HELP REN 281 | 282 | 重命名文件。 283 | 284 | RENAME [drive:][path]filename1 filename2. 285 | REN [drive:][path]filename1 filename2. 286 | 287 | 请注意,您不能为目标文件指定新的驱动器或路径。 288 | 289 | 上面的程序输出告诉用户 `REN` 命令的用法。请注意第四行和第五行中使用中括号(`[]`)括起来的部分。中括号表示对应的部分是可选的,而未使用中括号的部分(`filename1`、`filename2`),表示在实际的应用场合中,需要使用真实的字符串来替换。按照上面提供的用法,如下的 DOS 中 `REN` 命令用法是正确的: 290 | 291 | ```dos 292 | C:\>REN \Windows\Temp\ABitMap.ini abitmap.ini 293 | C:\>RENAME C:\Windows\Temp\ABitMap.ini abitmap.ini 294 | ``` 295 | 296 | 我们再来看一个更加复杂点的例子,这个例子是 Linux 操作系统的 `cp` 命令用法说明: 297 | 298 | NAME 299 | cp - copy files and directories 300 | 301 | SYNOPSIS 302 | cp [OPTION]... [-T] SOURCE DEST 303 | cp [OPTION]... SOURCE... DIRECTORY 304 | cp [OPTION]... -t DIRECTORY SOURCE... 305 | 306 | DESCRIPTION 307 | Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. 308 | … 309 | 310 | 上面的内容来自 `cp` 命令的手册页(man page)。该手册页解释了 `cp` 命令的用途及详细用法,在 `SYNOPSIS`(原语)一节中给出了 `cp` 命令的三种形式(使用 `cp --help` 命令也可以获得类似的信息)。和 DOS 的帮助信息类似,Linux 的手册页也使用了中括号表示命令行中的可选部分,另外还使用了冒号表示命令行中特定的部分可以有多个,比如选项可以有多个,源文件可以有多个。下面这些 `cp` 命令的用法是正确的: 311 | 312 | ```bash 313 | $ cp -t d a b c 314 | $ cp a b c d/ 315 | ``` 316 | 317 | 根据手册页的描述,上面两种形式的 `cp` 命令完成的功能是一样的,都是将 `a`、`b`、`c` 这三个文件复制到 `d/` 目录下。 318 | 319 | 在一些更加严格的场合,为了防止和内容中不能更改的部分(比如上面的 `REN`、`cp` 等)混淆,还经常使用尖括号(`<>`)来表示必选部分,比如: 320 | 321 | cp [OPTION]... ... 322 | 323 | 使用省略号的情形也可以用星号(*)或加号(+)来表示。使用星号时,表示可有可无,使用加号时表示至少有一个。按此记法,上面的命令原语可写成: 324 | 325 | cp [OPTION]* + 326 | 327 | 上面使用星号和加号要比原先的省略号定义了更加严格的规则:OPTION 部分可有可无,SOURCE 部分则至少需要有一个。 328 | 329 | 另外,我们还可以使用管道符(|)来表示多个可选部分二选一或者多选一,如: 330 | 331 | man -K [-w|-W] [-S list] [-i|-I] [--regex] [section] term ... 332 | 333 | 本质上,这些计算机软件文档中经常用到的记法,其实来自于正则表达式。有关正则表达式的详细信息,可参阅本书第三篇“信息的计算机处理”。 334 | 335 | ## 引用说明 336 | 337 | 本书部分引用了来自维基百科[^9]、百度百科的相关内容,尤其在涉及人物和历史方面。这些被引用的内容通常会明显标记或者说明。 338 | 339 | 另外,本书的少量内容改编自笔者本人所(编)著的其他技术书籍或文章,包括: 340 | 341 | - 魏永明,“自主”操作系统——为什么及如何,互联网,2012 年 9 月; 342 | - 魏永明,开源软件及国内发展现状,《程序员》杂志,2012 年 9 月; 343 | - 魏永明,MiniGUI 十年回顾,互联网,2009 年 12 月。 344 | - 《嵌入式软件开发及 C 语言实现——MiniGUI 剖析》,电子工业出版社,2008 年著; 345 | - 《Linux 实用教程》,电子工业出版社,1999 年编著,第一作者; 346 | - 《学用 Linux 与 Windows NT》,电子工业出版社,1999 年编著,第一作者; 347 | 348 | 除此之外,本书的其他内容均为本人“逐字码入”。若有雷同,纯属巧合。 349 | 350 | --- 351 | 352 | [^1]: 如知名的 www.StackOverFlow.com 网站。 353 | 354 | [^2]: 软件的优化一词可以从很多角度理解或定义。让程序变得更快是一种比较常见的理解。但在今天,优化一个程序或软件,大致包含如下几个层面的含义。第一,优化代码使之占用更少系统资源,这涉及让程序运行得更快,或者使用一些技巧提高交互的反馈速度等;第二,程序代码的可维护性,这要求程序设计的分层、分模块实现使之结构清晰,且源代码易读并具有较高的可扩展性;第三,采用恰当的技术路线和其他辅助软件来加快软件的开发,使之具有很强的可扩展性,以适应可能的用户爆发增长。 355 | 356 | [^3]: 全栈工程师这一概念在国内还存在许多不同的理解,但大部分是误解。普遍的误解是将全栈工程师等同于“万金油”,什么都要懂,什么都要精。显然这是不可能的,工程界不存在这样的“完人”。如果说存在,那也是人家可以比其他人更快地学习新的知识,或者更快地发现问题症结所在,并快速给出解决方案。 357 | 358 | [^4]: 这是因为,就算在可以标识需抽取的记录对应的字段上设置索引,由于该种记录非常稀疏,索引将会失效,从而相关的查询语句仍然要执行全表扫描来完成。对有千万条数据的关系型数据库表来讲,全表扫描将占用大量的处理器和内存资源。 359 | 360 | [^5]: 大部分关系型数据针对远程网络连接设置了一次可传输的包大小限制,从而在传输有大量结果集的过程中会遇到网络传输中断的情形。 361 | 362 | [^6]: 本书编写完成之时,就是P 语言从伪代码语言演进称为真正可以使用的编程语言之时。 363 | 364 | [^7]: 【维基百科】在西方,最先研究这个数列的人是意大利人斐波那契(Leonardo Fibonacci),他描述兔子生长的数目时用上了这数列。斐波那契有很多特性,其中之一是相邻两个数的比值趋近于黄金分割比:1.618。 365 | 366 | [^8]: 这个公式很有意思,使用无理数运算,结果却是整数。 367 | 368 | [^9]: 在本书编撰过程中,笔者同时修改了维基百科某些词条中存在的一些错误。 369 | 370 | -------------------------------------------------------------------------------- /textbook/part-1-chapter-1.md: -------------------------------------------------------------------------------- 1 | # 第 1 章 为什么选择二进制? 2 | 3 | 我们知道,当前我们使用的电子计算机系统在内部使用二进制进行运算,所以有 32 位、64 位系统之分。那么,为什么电子计算机使用二进制而不是我们熟悉的十进制来表示数字?将来的量子计算机会不会继续使用二进制? 4 | 5 | ## 1.1 机械计算的需求 6 | 7 | 人类的最早计算需求,应该和历法以及商业活动有关。准确的历法有利于人类按时耕作,获得好的收成,确保人类的繁衍生息;《史记·货殖列传》中所说“天下熙熙,皆为利来;天下攘攘,皆为利往”则是对商业活动的形象描述。在中国古代,大致发明于东汉时期的算盘,出现在阿拉伯数字之前,是实现半机械式计算的典型工具,一直延续使用上千年,在今天仍未绝迹[^1]。百度百科中如下解释算盘及珠算: 8 | 9 | > *用算盘计算称“珠算”,珠算有对应四则运算的相应法则,统称珠算法则。随着算盘的使用,人们总结出许多计算口诀,使计算的速度更快了。相对一般运算来看,熟练的珠算不逊于计算器,尤其在加减法方面。用时,可依口诀,上下拨动算珠,进行计算。珠算计算简便迅捷,在计算器及电脑普及前,为我国商店普遍使用的计算工具。* 10 | 11 | 随着建筑业以及制造业的发展,人类需要更加复杂的算术运算,甚至有了多元多次方程的求解需求。仅仅通过人的纸笔计算则非常繁复且容易出错。所以,人们开始寻求通过机器来完成计算任务的方法。这一需求在二战时期因为火炮的轨迹计算[^2]以及破解密文、加密解密的要求而变得最为紧迫,最终随美国制造原子弹的曼哈顿计划之一部分,电子计算机诞生了。在不足一百年的发展过程中,电子计算机从几十吨的庞然大物变成了现在可以装在口袋里边的智能手机、戴在手腕上的智能手表。这一切听起来实在不可思议。 12 | 13 | 简而言之,机械计算试图将所有的运算工作变成可重复的简单运算工作。从理论上讲,任何计算,都可以最终变成加法运算。拿简单的乘法运算来讲,我们可以通过重复的加法运算来获得结果;而更加复杂的曲线运算(比如抛物线、椭圆等),则可以通过泰勒级数展开、傅里叶级数展开等简化为重复的四则运算。因此,机械运算的一个最基本要求就是准确无误地重复。显然,人类不适合完成这种工作,因为人会疲劳,要吃饭、休息,工作中还容易开小差出错。相反,机器则是完成机械运算的最佳候选。 14 | 15 | ## 1.2 各种可能的实现方法 16 | 17 | 在刘慈欣著名的科幻小说《三体》中,作者描述了一种人肉计算机的构造方法。单个三体人可完成一个简单的二进制运算处理。假定三体人举起右手表示1,落下右手表示0。承担“取反”运算的三体人看到提供给他输入的三体人举起右手,他落下右手;对方落下右手,则他举起右手。如此就完成了二进制的取反运算。承担“或”运算的三体人则要看另外两个为他提供输入的三体人的举手状态,若其中有任何一个举起右手,则他也举起右手。如此,就可以将这些三体人串起来使用,只要有足够的三体人,就可以完成非常复杂的运算。三体人三五成群一行行排列,随着领袖的旗帜挥动,从一边开始计算。每挥动一下,一行三体人根据输入值完成计算,举起或落下他们的右手,再挥动一下,另外一行三体人根据新的输入完成一次新的计算,如此循环往复,成千上万三体人形成的人肉阵列可完成非常复杂的运算。 18 | 19 | 显然,《三体》作者的这个实现方法参考的是我们现今使用的电子计算机的实现方法,刚才所说承担不同的二进制运算功能的三体人,在我们的术语中称为“非门”、“或门”、“与门”等等。根据布尔代数的运算规则(见第二章),通过组合不同的逻辑门,我们就可以形成二进制加法器,再通过逻辑控制和存储机制,就可以实现指令的重复性操作,最终可以完成复杂的运算。 20 | 21 | 需要说明的是,上面的人肉阵列形成的计算机中,还要考虑逻辑控制和存储机制。比如计算结果需要存储起来作为下一次计算的初始值。所以,在人肉计算机中还要有很多用来传递计算结果的三体人组合单元;而逻辑控制部分,也由独立的三体人组合单元实现。这样,随着领袖的旗帜挥动,三体人形成的计算阵列就如随风摇曳的麦浪一般美丽和神奇。最后,旗帜对应的就是现代电子计算机中的时钟,时钟每滴答一下,完成一次计算。 22 | 23 | 当然,我们地球人在追求完美机械计算的过程中,并没有使用过上面的人肉计算机,三体人之所以使用这个方法,是因为三体人在恶劣的自然环境中积累了知识,但缺乏物质基础,所以在需要大量计算的时候,采用了人肉计算机。相反,地球人所生存的太阳系则有良好的自然环境,知识的积累水平和物质基础的发展水平是相当的。所以,在工业时代,地球人尝试使用蒸汽机、齿轮以及机械传动装置来实现机械计算,而在信息时代,使用电子晶体管技术或者将来的量子技术实现机械计算。 24 | 25 | 以齿轮和机械传动装置实现机械计算的实例,当属十九世纪由英国数学家巴贝奇发明的分析机[^3]。当然,分析机并没有最终完成,或者说,仅能完成一些演示。巴贝奇的分析机使用蒸汽机提供动力,使用齿轮传动系统完成**十进制**的运算,同时还设计有存储中间计算结果的“存储器”。因此,巴贝奇的分析机被认为是一部完备的图灵机。遗憾的是,因为制造工艺复杂、投资过大、进展缓慢等原因,分析机并没有在巴贝奇有生之年完成。在两百年之后,巴贝奇的设计蓝图才被现代机械工程师变为现实,并在硅谷的计算机历史博物馆展出。但是,分析机是此后大约一百年才出现的电子计算机的先驱。 26 | 27 | 尽管巴贝奇的分析机在当时并没有完成,却激发了天才女性艾达·洛夫莱斯的兴趣,艾达为没有完成的分析机编写了世界上第一个计算机程序,该程序可以计算伯努利数[^4]。 28 | 29 | 除了前述的算盘之外,在巴贝奇设计分析机之前,法国人布莱士·帕斯卡[^5]于 1642年发明了自动进位加法器,称为Pascalene。1694年,德国数学家弗里德·莱布尼茨[^6]改进了Pascaline,使之可以计算乘法。后来,法国人查尔斯·科尔还发明了可以进行四则运算的计算器。这些计算用设备和我们中国的算盘一样,不属于完备的图灵机范畴,而仅仅是一种辅助计算工具。毕竟,复杂计算中经常会遇到的循环、递归运算,无法通过这些计算设备进行机械式处理——本质上,这些计算工具缺乏图灵机定义的纸带(现代计算机意义上的存储器及读写控制器),从而无法将一个计算结果变成下一个计算的初始条件然后循环处理之。 30 | 31 | ## 1.3 二进制的优势 32 | 33 | 在讨论二进制的优势之前,我们先看巴贝奇分析机使用的十进制带来的麻烦。 34 | 35 | 分析机使用十进制进行运算。假定我们要用一个机构来完成十以内的加法,则可能的加法有 (0+0), (0+1), (0+2), …, (0+9),(1+0), (1+1), …, (1+9), …, (9+0), (9+1), …, (9+9),总共 100 种情形[^7]。现在我们构造一个十进制的单个位加法器。我们可以用十种齿轮分别表示 0 到 9 的数字,要完成一个位上的加法,我们的加法器需要三个齿轮作为输入,其中两个是加数,另外一个是低位上加法的进位值,还要有两个齿轮作为输出,分别表示在这一位上的加法结果(和),以及进位值。比如我们要完成 (45+89),低位上的 (5+9) 产生的和为 4,进位值为 1,高位上的 (4+8) 在运算时,要考虑低位加法产生的进位值,实际就是 (4+8+1),结果是和为3,进位值为1,于是,在我们的加法器上,其最终的输出为 134。 36 | 37 | 我们略去加法器内部的机械构造,假定表示加数和低位进位值的齿轮在一个轴上,表示结果(和以及进位值)的齿轮在另外一个轴上,驱动输入轴转一周(360 度)可完成一次运算,结果就是输出轴上的齿轮。我们大致需要100 种不同构造的加法器,用来实现上面的 100 种十进制单个位的加法,每个加法器需要五个齿轮,所以总共需要五百个齿轮。如果考虑到不同情形下通过加数来选择不同的加法器的传动装置,这个分析机还需要更多的机械零件。倘若我们要一次完成最多十位十进制整数的加法计算,则可想而知,整个分析机的机构复杂性将超过想象。 38 | 39 | 十进制带来的问题很明显。除去机械传动方式速度慢、能耗高、容易出现故障的不足之外,十进制计算机还有如下不足: 40 | 41 | - 用来表示或存储单个位的设施(对分析机而言就是“齿轮”)的种类太多(十种)。这点上,中国的算盘比分析机要巧妙得多,算盘使用单个珠子表示一或者五。 42 | - 用来实现单个位加法的加法器种类太多(一百种),根据不同加数选择对应加法器的过程(一百种里边选一种)也同样复杂。 43 | 44 | 因此,从制造的角度上讲,十进制计算机的基础零件(加法器)的制造难度和复杂性都非常高,大致不亚于制造一台汽车。系统内部构造的复杂性也非常高,随着计算位数的增加,复杂性将呈几何级数增加。 45 | 46 | 如果巴贝奇在设计分析机时使用二进制,则可能更易成功。遗憾的是,十九世纪三十年代,二进制运算的许多优势尚未被数学家们所熟知。 47 | 48 | 十进制的劣势就是二进制的优势。比如,数字的二进制表达中的单个位只有两种可选值:0 和 1,而更为神奇的是,我们只需要一种加法器就可以实现二进制单个位的加法。 49 | 50 | 表 1-1[^8] 给出了二进制表述情形下,不同输入(x 和 y 为加数,z 为低位运算结果的进位值)情况下的输出(S 为和,C 为进位值): 51 | 52 | 表 1-1 二进制全加法器的真值表 53 | 54 | | x | y | z | C | S | 55 | |:----------|:----------|:----------|:----------|:----------| 56 | | 0 | 0 | 0 | 0 | 0 | 57 | | 0 | 0 | 1 | 0 | 1 | 58 | | 0 | 1 | 0 | 0 | 1 | 59 | | 0 | 1 | 1 | 1 | 0 | 60 | | 1 | 0 | 0 | 0 | 1 | 61 | | 1 | 0 | 1 | 1 | 0 | 62 | | 1 | 1 | 0 | 1 | 0 | 63 | | 1 | 1 | 1 | 1 | 1 | 64 | 65 | 66 | 根据布尔代数的运算规则,我们会发现: 67 | 68 |
69 |
70 |
71 |
72 |
73 | 74 | 其中, 表示“异或”运算(见第二章);`·` 表示“与”运算;`+` 表示“或”运算。这样,二进制的加法器可以通过一系列二进制的逻辑运算来得出结果。而在物理上,类似三体人的人肉计算机那样,我们可以组合两个“异或门”、两个“与门”、一个“或门”来构造一个二进制的加法器。若要同时计算多位的二进制数,复制加法器并将它们连接起来就可以了。 75 | 76 | 随着电子晶体管的发明和应用,人们发现电子晶体管的物理特性对二进制的各种逻辑运算来讲,简直可以形容为自然天成。也就是说,我们可以利用晶体管来构造不同的逻辑“门”,用来实现与、或、非、异或等基本的二进制逻辑运算,而这些逻辑运算则可以组合构成加法器,也可以用来控制各种运算的逻辑过程,也就是算法。 77 | 78 | 晶体管还有一个好处就是非常小,可以被设计为集成电路,其中可包含成千上亿的晶体管,并进而形成只有几厘米见方(或更小的)“芯片”。而且根据对制造工艺的观察和推测,英特尔创始人之一戈登·摩尔还提出,单个芯片上可以集成的晶体管数量每十八个月就可以翻一番,性能也将翻番。这就是著名的“摩尔定律”。 79 | 80 | 当然,随着晶体管尺寸变得越来越小,集成电路终将触及量子物理所描述的不确定性范畴,从而限制了其尺寸可以达到的极限,届时,“摩尔定律”也将走向终点。最终,量子计算机会替代电子计算机,那时,新的“摩尔定律”又将成为可能。 81 | 82 | 但即使在量子计算机中,最底层的运算仍然会基于二进制。因为在量子尺度,人们便于测量或通过仪器感知的量子之物理属性,比如自旋方向,其最自然的表达就是二进制的 0 和 1。 83 | 84 | 从电子计算机的发展来看,有人会认为因为晶体管的物理特性,才导致计算机科学家在电子计算机中选择了二进制。这似乎是历史的选择,而不是科学自身的选择。然而,在计算机中选择二进制作为最底层的表达,不仅仅是因为晶体管的发明,归根结底是因为数学上的进展,也就是布尔代数的发现。因此,计算机选择二进制不仅仅是历史的必然。 85 | 86 | 几年前,国内有人描述过一种十进制计算机,然而仔细看其实现思路可知,作者描述的十进制计算机从本质仍然是二进制的;他所描述的十进制计算机仅仅参考算盘的实现,以五个比特为单位进行计算。这相当于实现了一种新的不依赖于布尔代数逻辑规则的加法器。除此之外,并无新意。 87 | 88 | ### 1.4 电子计算机设计之道:冯·诺伊曼架构 89 | 90 | 1945 年 6 月,冯·诺伊曼[^9]与戈德斯坦、勃克斯等人,联名发表了一篇长达 101 页纸的报告,即计算机史上著名的“101 页报告”。这个报告是现代计算机科学发展的里程碑式文献。其中明确规定用二进制替代十进制运算,并将计算机分成五大组件(即算术逻辑单元、控制单元、存储器、输入和输出,见图 1-1)。这一卓越的思想为电子计算机的逻辑结构设计奠定了基础,已成为计算机设计的基本原则。由于冯·诺伊曼在计算机逻辑结构设计上的伟大贡献,他被誉为“计算机之父”。在冯·诺伊曼架构的指导下,EDVAC 计算机于 1951 年宣告完成。 91 | 92 |
93 | ![冯·诺伊曼架构示意图](illustration/img-1-1.png)
94 | 图 1-1 冯·诺伊曼架构示意图 95 |
96 |
97 | 98 | 冯·诺伊曼架构的各部分功能如下: 99 | 100 | - 输入设备和输出设备,用于进行人机交互或者计算机与计算机之间的通信。用户通过输入设备将所需要的程序和数据送至计算机中,计算机处理结束后通过输出设备展示给用户。输入设备包括键盘、鼠标、扫描仪、触摸屏、绘图板等人机交互设备。输入设备包括显示屏、投影仪、打印机、声卡等。网口、蓝牙等用于实现机器间通信的设备也属于输入输出设备。 101 | - 存储器用于存储程序和数据。在处理过程中,某些结果是中间性质的,只需临时存储,而某些结果是持久性质的,必须长期存储。因此,计算机的存储设备可被简单划分为易失性存储器和持久性存储器。前者通常具有访问速度快,但容量小的特点,如内存(RAM)[^10];后者通常具有访问速度慢,但容量大的特点,如硬盘。 102 | - 控制器用来控制程序的执行,比如在内存中访问(读取或写入)数据,然后对这些数据四则运算或其他逻辑运算。 103 | - 算术逻辑单元用于完成算术运算和逻辑运算。在现代计算机中,算术逻辑单元和控制器通常被整合在一起,形成所谓的中央处理单元(CPU)。 104 | 105 | 106 | 冯·诺伊曼架构的最关键特征是 RAM 和 CPU 的分离设计。使用冯·诺伊曼架构的设计的 CPU,按照计算机指令能够同时处理的二进制位数确定数据的最小单位,如 32 位、64 位。这些数据在RAM 中线性存储,且每个数据有一个对应的访问地址,而 CPU 通过总线访问存储在 RAM 中的数据。 107 | 108 | 109 | [^1]: 【维基百科】以往认为算盘为中国发明的观点并不可靠。事实上,在古中国开始广泛流行算盘已晚至宋元时期,此前通行的是筹算。反而在巴比伦、罗马都出土过接近今日中国算盘形制的算板实物,其算盘的形制相对上有着较清晰的演进轨迹。此外,包括北非的古埃及、中亚的俄罗斯(Russian Shoty)、中古欧洲都有形制与之类似的手算盘。因此不能说算盘为中国所独有之巧思。客观来看,算盘是各地人类因应计算需要,透过文明交流而相互参考、逐渐完善的过程。 110 | 111 | [^2]: 火炮炮弹的轨迹计算需要建立相当复杂的数学模型。试想火炮要击落一家飞机,因为飞机在运动,就算火炮是固定的,但炮弹击发出去后也在运动,还要考虑到瞄准时飞机的位置以及计算过程所消耗的时间差,如果再加上风速等其他因素,整个火炮的控制系统会非常复杂。怪不得二战时期,科学家尤其是数学家成了各国竞争的一个焦点。 112 | 113 | [^3]: 巴贝奇最早设计的计算用机器称为“差分机”,主要用来实现导航表,后来发现差分机用途有限,改而设计更加通用的“分析机”。 114 | 115 | [^4]: 伯努利数是18世纪瑞士数学家雅各布·伯努利提出的,伯努利数的生成公式属于一种典型的递归计算。 116 | 117 | [^5]: 压强的单位“帕斯卡”即来自这位数学家兼科学家。 118 | 119 | [^6]: 和牛顿一起并称为微积分的奠基人。 120 | 121 | [^7]: 当然,根据整数的加法交换律,我们可以把 100 种情形缩减为一半,即 50 种。也许还有其他方法可以进一步缩减,但仍然很多。 122 | 123 | [^8]: 在布尔代数中,这种表称为真值表(Truth table)。 124 | 125 | [^9]: 【维基百科】约翰·冯·诺伊曼(John von Neumann,1903~1957),出生于匈牙利的美国籍犹太人数学家,现代计算机创始人之一。他在计算机科学、经济、物理学中的量子力学及几乎所有数学领域都作出过重大贡献。 126 | 127 | [^10]: 在本书中,我们使用“内存”这个术语时,特指易失性存储器,即随机访问存储器(RAM)。需要注意的是,在移动互联网时代,“内存”一词被一些不严谨的产品人员用作表示手机的持久存储器。 128 | 129 | -------------------------------------------------------------------------------- /textbook/part-1-chapter-2.md: -------------------------------------------------------------------------------- 1 | # 第 2 章 二进制及其运算 2 | 3 | 我们现在知道,你通过电脑或者智能手机获得或处理的所有信息,在计算机内部都是用二进制数字表示的。本章将描述二进制相关的一些基本概念以及常见的二进制运算。 4 | 5 | ## 2.1 电子计算机的性能衡量指标 6 | 7 | 比特(bit)是二进制数据的最小表述单元,每个比特可取 0 或 1 两个值。通过前述的逻辑门的组合,可以形成加法器等运算单>元以及逻辑运算及控制单元,从而完成复杂的算术运算。在衡量电子计算机之运算能力的参数中,最常见的有 32 位、64 位的概念。这表示电子计算机能够同时处理的比特位数。比如,32 位电子计算机,在一个时钟周期内(电子计算机中时钟的作用,相当>于指挥三体人的人肉计算机阵列的旗帜;一个时钟周期相当于指挥者的旗帜挥动一次),可以完成 32 位比特的加法运算;而 64 位电子计算机,则可以完成 64 位比特的加法运算。 8 | 9 | 另外,除了加法运算之外,电子计算机可以完成其他的操作,如逻辑运算、访问存储单元等等。这些可在一个或多个时钟周期内完成的不可拆分操作被定义为电子计算机的指令,而复杂的运算和逻辑算法,就是指令及其数据的有序集合。进而,每秒可以执行的计算机指令数量就变成了衡量电子计算机运算速度的一个重要指标。在电子计算机出现的早期阶段,运算能力相对较弱,该指标的单位通常称为“每秒千指令(kIPS)”,而当今的电子计算机,该指标的单位通常称为“每秒百万指令(MIPS,Millions of Instructions per Second)”。以英特尔酷睿 2 P8800 CPU 为例,在时钟频率为 2.66GHz 时的运算速度为 7047.88 MIPS[^1]。显然,电子计算机可同时处理的二进制比特位数量越大、每秒指令数越高,其性能越好。 10 | 11 | 类似地,人们使用每秒浮点运算次数(亦称每秒峰值速度)这一指标来衡量超级计算机的运算能力。每秒浮点运算次数(FLOPS,Floating-point operations per second)指计算机每秒可执行的浮点数操作次数。中国天河二号超级计算机目前保有此项指标的>世界纪录,为 33.86 PFLOPS[^2],公布于 2013 年。 12 | 13 | 需要注意的是,一条电子计算机指令的执行需要一个时钟周期或者多个时钟周期。比如对简单的二进制逻辑运算(与、或等)或者加法,通常在一个时钟周期内即可完成;但对复杂的乘除法或者浮点运算,则需要多个指令周期才能执行完毕。这很容易理解,比如整数的乘法操作,就是重复执行多次加法的结果。 14 | 15 | ## 2.2 比特、字节及其计量 16 | 17 | 尽管计算机最基本的信息存储单位是比特,但如果我们始终使用比特这个单位来表达数据,则会非常麻烦。因此,人们选择使用八个比特为一个单位来表示计算机所处理的数据。八个比特形成一个字节(Byte)。选择八比特作为最基本的计算机信息计量单位,大致原因有二。其一,8 是 2 的 3 次方,便于进行二进制处理;其二,八位比特可表达 0~255 总共 256 个数字,比起两位比特(4 个数字)或者四位比特(16 个数字)能表达的数字个数多,但又比十六位比特为单位情况下能表达的数字个数(65,536)少,基本上恰恰好。因此,字节作为衡量计算机信息量的基本单位一直沿用至今,比如衡量电子计算机的存储能力(内存大小、硬盘容量等)时,使用的就是字节这个单位。 18 | 19 | 有关比特和字节的计量,读者需要了解如下习惯用法: 20 | 21 | - 在计算机领域,K、M、G、T、P、E、Z、Y 分别用来表示千(1K=1,024)、兆(1M=1,024K)、吉(1G=1,024M)、太(1T=1,024G)、拍(1P=1,024T)、艾(1E=1,024P)、泽(1Z=1,024E)、尧(1Y=1,024Z)等单位前缀。注意这些单位前缀以 1,024(210)作为倍数逐级增加单位量;但在其他科学界,这些单位前缀又往往使用 1,000 作为倍数。这的确带来了一定程度上的混乱[^2]。准确的方法是在使用 1,024 作为倍数时,在这些单位前缀后面增加小写的 i 作为后缀。比如,1MiB,表示的就是 1,024 字节,>而 1MB 表示 1,000 字节。但如果是在计算机领域,如果不添加i 后缀,往往默认使用 1,024 倍数。 22 | - 小写的 b 通常用来表示比特,而大写的 B 通常用来表示字节。在通信领域,人们经常使用 bps(bits per second)这个单位来衡量通信线路的容量或速率。比如,当前智能手机所使用的 3G 通信标准可达到 20Mbps 的下行传输速率,亦即每秒可传输 20M 比特。需要注意的是,这个数字除以 8 并不能得到以字节为单位的传输速率,因为在传输过程中,会使用一些冗余的比特位(比如校验位,用来校验所传输数据的准确性)。举例说明,在 8Mbps 的传输速率中,作为最终用户基本上无法获得 1MBps 的传输速率。 23 | 24 | 下图给出了一个典型字节的比特位组成: 25 | 26 |
27 | ![一个典型的字节](illustration/img-2-1.png)
28 | 图 2-1 一个典型的字节 29 |
30 |
31 | 32 | 注意其排列顺序,最左侧的位称为“最高位(most significant bit,MSB)”,最右侧的位称为“最低位(least significant bit,LSB)”。这一排列顺序和书写十进制数字时左侧表示高位、右侧表示低位是一样的。 33 | 34 | 为了便于记述,人们又使用十六进制方式来表述一个字节,分别用十六进制的符号(0, 1, …, 9, A, B, …, F)来表示高低四位比特。这样,上图中的字节可表示为 0x0F 或者0Fh。前者使用 0x 作为前缀表示这是一个十六进制表达的字节,后者使用 h 作为后缀来表示这是一个十六进制表达的字节。类似地,人们还使用 b 作为后缀来表示字节的二进制表达,如 00001111b。如果不使用>前缀或者后缀,则表示十进制的表达,如 15。当前,这种表达方法作为约定俗成,被大部分编程语言或者计算机相关技术文献使用。 35 | 36 | 另外,除了字节之外,人们还经常使用“字”(word)来表达一个 16 位的二进制数,或简称十六位元;用“双倍字”(dword,double word)来表达一个 32 位的二进制数,或简称三十二位元;用“四倍字”(qword,quadruple word)、“双四倍字”(dqword,double quadruple word)分别表示 64 位、128 位二进制数等等。 37 | 38 | ## 2.3 字节序 39 | 40 | 字节序(byte order)指电子计算机处理器在处理多字节的数据对象(比如字、双字、四倍字以及接下来讲述的浮点数)时,在寄 41 | 存器及内存中以什么样的顺序保存字节。 42 | 43 | 我们知道,桌面系统通常运行在英特尔的 x86 架构处理器上,而这种处理器按照低地址存放低位数据的原则保存多字节数据对象。假定整数 0x1234 是一个 16 位整数,则在 Intel x86 平台上,处理器将按图 2-2 所示的方式存储这个整数。即低地址 A 中保存 0x34 这个字节,而在高地址 A+1 中,保存 0x12 这个字节。这种字节序称为“小头(little-endian)”。见图 2-2。 44 | 45 |
46 | ![小头存储](illustration/img-2-2.png)
47 | 图 2-2 小头(little-endian)存储 48 |
49 |
50 | 51 | 大头系统存储多字节数据对象时,采用和小头系统完全相反的方式,见图 2-3。 52 | 53 |
54 | ![大头存储](illustration/img-2-3.png)
55 | 图 2-3 大头(big-endian)存储 56 |
57 |
58 | 59 | 许多 RISC 处理器都使用大头字节序,比如 PowerPC、M68k等(ARM 和 MIPS 处理器可灵活设置采用哪种字节序,通常被设置为小头)。ARM 处理器还有一个特殊之处,即整数的表述通常被设置为小头系统,但始终使用大头字节序来存储浮点数。 60 | 61 | 除了常见的小头存储和大头存储方式之外,还有一种 32 位整数的存储方式,即 PDP ENDIAN。PDP ENDIAN 很少使用,它在存储 32 位整数时,用来形成 32 位整数的两个 16 位整数采用大头存储形式,而形成 16 位整数的两个 8 位字节却采用小头存储形式。图 2-4 给出了这三种 ENDIAN 系统存储 0x04030201 这个 32 整数时的字节顺序。 62 | 63 |
64 | ![存储顺序](illustration/img-2-4.png)
65 | 图 2-4 Little-endian、Big-endian 以及 PDP-endian 对 32 位整数的存储顺序
66 |
67 |
68 | 69 | 通常在我们的程序开发过程中,不同的大小头并不会给我们带来大的麻烦。但是,假如我们将一个小头的二进制数保存在一个文件中,然后再在大头的系统上读取时,就可能出现问题。这是因为: 70 | 71 | - 将一个多字节二进制数保存到文件中时,将保留字节在处理器内部存储的顺序; 72 | - 文件的读取操作都是以字节为单位进行的。 73 | 74 | 比如在小头系统上,我们使用如下的伪代码[^4]将 0x1234 这个 16 位整数保存到某个文件: 75 | 76 | ``` 77 | WORD my_word = 0x1234 78 | 79 | FILE my_file = OPEN ("temp.bin") 80 | WRITE (my_file, my_word) 81 | CLOSE (my_file) 82 | ``` 83 | 84 | 当我们再次从文件中读取这个整数时,我们可以用下面的伪代码: 85 | 86 | ``` 87 | WORD my_word 88 | 89 | STDIO::FILE my_file = STDIO.fopen ("temp.bin") 90 | my_word = STDIO.fread (my_file, 2) 91 | STDIO.fclose (my_file) 92 | ``` 93 | 94 | 如果上述代码仍然小头处理器上运行,则调用 `READ` 过程之后,`my_word` 中的值仍然为 0x1234。然而,当我们在大头处理器上执行上述读取代码时(这里假定文件由小头系统写入),我们会发现 `my_word` 的值变成了 0x3412。因此,我们要特别注意多字节二进制数据在不同处理器架构上的读写问题。 95 | 96 | 字节序的问题,通常在我们从文件或者网络中读取一个不同于本机字节序的多字节二进制数据时出现,也就是使用某种中介传送数据时出现。如果我们的程序从来不从文件系统或者网络中读取 8 位以上的整数或者浮点数,或者始终发生在使用同一种处理器的计算机上时,则程序不必关心字节序问题。因为这种情况下,所有的整数或者浮点数都会采用相同的字节序,即兼容于处理器的字节序来处理。然而,现实情况中,我们经常需要读取一些已经存在的文件来获得数据。比如,图形应用程序经常要读取一些图片文件,而这些图片文件可能是由运行在 PC 上的应用软件保存的,这时,就有可能出现字节序不匹配的问题。像我们常用的 Windows BMP 文件,会使用 32 位整数来保存图片的大小信息,且以小头字节序保存,如果我们要在大头系统上正确读取BMP 文件,就需要首先对读取的 32 位整数做字节序转换,才能正确使用。 97 | 98 | 当然,还有一种简单的方法可以绕开字节序的问题,即将所有的多字节数据在存储或传输时,始终使用可打印的字符来表示。这种方法在计算机软件中一般称为序列化,将在后面的章节中介绍。序列化处理后,所有复杂的多字节数据将转换为可打印的字符(单个字节)序列,即字符串或俗称“文本”,读取后做反序列化操作即可。 99 | 100 | ### 2.3.1 C 语言处理字节序问题 101 | 102 | 在 C 语言中,我们如何判断程序运行在大头系统还是小头系统上呢?我们可以通过清单 2-1 中的 `is_big_endian` 函数来判断。 103 | 104 | 清单 2-1 判断目标系统的字节序(check_endian.c) 105 | 106 | ```c 107 | #include 108 | 109 | inline int is_big_endian (void) 110 | { 111 | union { 112 | unsigned short i; 113 | unsigned char c[2]; 114 | } data; 115 | 116 | data.i = 0x1234; 117 | 118 | if (data.c[0] == 0x12) 119 | return 1; 120 | 121 | return 0; 122 | } 123 | 124 | int main (void) 125 | { 126 | if (is_big_endian ()) 127 | printf ("This is a big-endian system.\n"); 128 | else 129 | printf ("This is a little-endian system.\n"); 130 | 131 | return 0; 132 | } 133 | ``` 134 | 135 | 注意,上述代码段中的 `is_big_endian` 函数使用了 C 语言的联合类型,这种类型在处理这种形式的问题时具有非常好的便利性。 136 | 137 | 另外,我们希望在程序中可以根据目标系统的字节序自动完成额外的数据转换。这种转换通常就是互换字节中的值。为了完成这种转换,我们可以使用类似清单 2-1 中的程序来判断本机的字节序,然后根据要读取或者写入的目标字节序来完成相应的转换。但在实践当中,我们采用条件编译来完成这种字节序的判断: 138 | 139 | ```c 140 | #define _LIL_ENDIAN 1234 141 | #define _BIG_ENDIAN 4321 142 | 143 | #if defined(__i386__) || defined(__ia64__) || \ 144 | (defined(__alpha__) || defined(__alpha)) || \ 145 | defined(__arm__) || \ 146 | (defined(__CC_ARM) && !defined(__BIG_ENDIAN)) || \ 147 | (defined(__mips__) && defined(__MIPSEL__)) || \ 148 | defined(__LITTLE_ENDIAN__) || \ 149 | defined(WIN32) 150 | #define _BYTEORDER _LIL_ENDIAN 151 | #else 152 | #define _BYTEORDER _BIG_ENDIAN 153 | #endif 154 | ``` 155 | 156 | 程序可通过编译器预先定义的宏来判断目标平台是大头系统还是小头系统,然后将 `_BYTEORDER` 定义为相应的宏。然后在程序中采用下面的方法来处理字节序: 157 | 158 | ```c 159 | #if _BYTEORDER == _BIG_ENDIAN 160 | /* for the big-endian system */ 161 | #else 162 | /* for the little-endian system */ 163 | #endif 164 | ``` 165 | 166 | 比如,我们为了从文件中读取一个由小头系统保存的 16 位二进制整数,我们可以用下面的 C 代码编写程序: 167 | 168 | ```c 169 | unsigned short temp; 170 | 171 | /* read back the data. */ 172 | read (fd, &temp, sizeof (unsigned short)); 173 | close (fd); 174 | 175 | #if _BYTEORDER == _BIG_ENDIAN 176 | temp = ((temp << 8)|(temp >> 8)); 177 | #endif 178 | ``` 179 | 180 | 上面的 `temp = ((temp << 8)|(temp >> 8));` 语句将 `temp` 的高八位和低八位互换然后重新组合成了新的整数。 181 | 182 | 在使用 GNU C 函数库时,我们还可以在源程序中包含 `` 头文件,并通过该头文件中已经定义好的 `__BYTE_ORDER` 宏来帮助判断目标系统的字节序,比如: 183 | 184 | ```c 185 | #include 186 | #include 187 | 188 | int main (void) 189 | { 190 | #if __BYTE_ORDER == __LITTLE_ENDIAN 191 | printf ("This is a little-endian system\n"); 192 | #elif __BYTE_ORDER == __BIG_ENDIAN 193 | printf ("This is a big-endian system\n"); 194 | #elif __BYTE_ORDER == __PDP_ENDIAN 195 | printf ("This is a PDP-endian system\n"); 196 | #endif 197 | 198 | return 0; 199 | } 200 | ``` 201 | 202 | 清单 2-2 中的函数,给出了将内存中的大头或者小头整数转换为本机字节序的 C 语言例程(函数),可供读者参考或使用。 203 | 204 | 清单 2-2 将大头整数或者小头整数转换为本机字节序(conv_int.c) 205 | 206 | ```c 207 | static inline unsigned short ReadLE16Mem (const unsigned char** data) 208 | { 209 | unsigned short h1, h2; 210 | 211 | h1 = *(*data); (*data)++; 212 | h2 = *(*data); (*data)++; 213 | return ((h2<<8)|h1); 214 | } 215 | 216 | static inline unsigned int ReadLE32Mem (const unsigned char** data) 217 | { 218 | unsigned int q1, q2, q3, q4; 219 | 220 | q1 = *(*data); (*data)++; 221 | q2 = *(*data); (*data)++; 222 | q3 = *(*data); (*data)++; 223 | q4 = *(*data); (*data)++; 224 | return ((q4<<24)|(q3<<16)|(q2<<8)|(q1)); 225 | } 226 | 227 | static inline unsigned short ReadBE16Mem (const unsigned char** data) 228 | { 229 | unsigned short h1, h2; 230 | 231 | h1 = *(*data); (*data)++; 232 | h2 = *(*data); (*data)++; 233 | return ((h1<<8)|h2); 234 | } 235 | 236 | static inline unsigned int ReadBE32Mem (const unsigned char** data) 237 | { 238 | unsigned int q1, q2, q3, q4; 239 | 240 | q1 = *(*data); (*data)++; 241 | q2 = *(*data); (*data)++; 242 | q3 = *(*data); (*data)++; 243 | q4 = *(*data); (*data)++; 244 | return ((q1<<24)|(q2<<16)|(q3<<8)|(q4)); 245 | } 246 | ``` 247 | 248 | ## 2.4 字节对齐 249 | 250 | 在 8 位以上的电子计算机中,处理器指令处理的往往是字、双字、四倍字而不是字节。比如在 32 位处理器中,加法指令要求操作数是 32 位的二进制数。但在随机访问存储器(RAM,亦称“内存”)中,数据以字节为单位存放。从电子计算机设计的角度出发,将 32 位二进制数在内存中保存在 4 倍地址上则可以获得最好的访问性能。比如在 32 位大头系统中,我们可以将 0x12345678 的起始字节 0x12 保存在地址为 0 的内存单元中,也可以保存在地址为 1 的内存单元中。如果将起始字节保存在 0、4、8 等地址上时,则处理器指令在访问这个 32 位二进制数时,就可以获得最大程度的优化性能。这种存放规则称为字节对齐(alignment)。甚至在某些处理器上,如果我们访问的操作数地址不是对齐的,处理器会抛出异常。 251 | 252 | 当我们使用高级编程语言编写程序时,通常不会遇到因为字节对齐而导致的问题。这是因为高级编程语言已经仔细设计并处理了可能出现的问题。但在使用汇编语言,或者比较低级的编程语言[^5],如 C 语言时,则经常会遇到字节对齐导致的问题。 253 | 254 | ### 2.4.1 C 语言中的字节对齐问题 255 | 256 | 对齐问题是许多接触 C 语言不多的开发人员常常忽略的问题。为了对对齐有个感性认识,我们首先看下面的代码: 257 | 258 | ```c 259 | #include 260 | 261 | void align_1 (void) 262 | { 263 | struct { 264 | char c; 265 | int i; 266 | } my_struct; 267 | 268 | printf ("The size of my_struct is %d\n", sizeof (my_struct)); 269 | } 270 | 271 | int main (void) 272 | { 273 | align_1 (); 274 | 275 | return 0; 276 | } 277 | ``` 278 | 279 | 在编译并运行这段代码之前,读者可以首先推断一下该程序的正确输出。然后,我们在 PC 上运行这个程序看看它的实际结果: 280 | 281 | ```shell 282 | user$ gcc -o align align.c 283 | user$ ./align 284 | The size of my_struct is 8 285 | ``` 286 | 287 | 许多人会对此结果有疑问,然而,如果我们不作任何的特殊设置,这个结构的大小的确是 8。为什么会这样呢?这是因为,在 32 位处理器上,在存放多字节操作数时,编译器会根据操作数的大小确保在内存空间中该操作数对齐于地址边界。比如在上面这个结构中,char c 成员是个单字节的成员,这个成员可被保存在任意的地址;而 int i 成员的大小是 4 字节,编译器就会确保将 i 保存在地址为 4 的倍数的位置上。因此,上述结构在内存中存储时,它的实际内存布局不是我们想象的 c 占用一个字节,然后立即是 4 个字节的 i,而是像图 2-5 那样。 288 | 289 |
290 | ![my_struct 结构数组的内存布局](illustration/img-2-5.png)
291 | 图 2-5 my_struct 结构数组的内存布局 292 |
293 |
294 | 295 | 编译器这样做的目的有两个: 296 | 297 | - 根据操作数的大小和地址对齐存放操作数,可确保对该操作数的二进制运算速度最快,这是硬件设计决定的。 298 | - 某些处理器指令在处理不对齐的操作数时,会出现处理器异常,从而导致总线错误[^6]。 299 | 300 | 依此类推,我们可以看到下面这个结构的大小也是 8: 301 | 302 | ```c 303 | struct { 304 | char c; 305 | short s; 306 | int i; 307 | } my_struct; 308 | ``` 309 | 310 | 另外,编译器通常会提供某种方法,以便取消定义结构时的成员对齐。在 gcc 中,我们可以用 `__attribute__ ((packed))` 修饰词取消结构内部的成员对齐: 311 | 312 | ```c 313 | struct __attribute__ ((packed)) { 314 | int i; 315 | short s; 316 | char c; 317 | } my_struct; 318 | ``` 319 | 320 | 为了避免因为对齐给我们带来麻烦,有的情况下我们需要仔细编码以便确保程序的正确性。比如,下面的程序试图将内存中的一个 32 位整数值取出来: 321 | 322 | ```c 323 | unsigned int read_uint_from_mem (const unsigned char* data) 324 | { 325 | unsigned int u; 326 | memcpy (&u, data, sizeof (unsigned int)); 327 | return u; 328 | } 329 | ``` 330 | 331 | 这段程序看起来没有什么问题,我们甚至可以这样编码: 332 | 333 | ```c 334 | unsigned int read_uint_from_mem (const unsigned char* data) 335 | { 336 | unsigned int *u; 337 | u = (unsigned int*)data; 338 | return *u; 339 | } 340 | ``` 341 | 342 | 然而,上面的两个程序段,当传入的 data 地址不在 4 字节边界上对齐的时候,就会出现问题。读者可以在 PC 或者某些 RTOS(如 uC/OS)上测试上面的程序段,用下面的方式调用: 343 | 344 | ``` 345 | unsigned char data [1024]; 346 | unsigned int u; 347 | 348 | u = read_uint_from_mem (data + 1); 349 | ``` 350 | 351 | 我们会发现,在某些实时操作系统(RTOS,如 uC/OS)上,上述代码会给出总线错误,从而导致程序异常退出。正确的 `read_uint_from_mem` 函数应该如下设计,才能确保避免因为对齐造成的程序错误: 352 | 353 | ``` 354 | unsigned int read_uint_from_mem (const unsigned char* data) 355 | { 356 | unsigne int q1, q2, q3, q4; 357 | 358 | q1 = data[0]; 359 | q2 = data[1]; 360 | q3 = data[2]; 361 | q4 = data[3]; 362 | return ((q1<<24)|(q2<<16)|(q3<<8)|(q4)); 363 | } 364 | ``` 365 | 366 | 那么,一样的代码,为何在 Windows 和 Linux 等操作系统上运行就不会出错呢?这是因为 Windows 和 Linux 内核运行在具有 MMU 单元的处理器上,采用的是虚拟内存技术。当指令在非对齐的内存地址上访问整数值时,处理器将产生一个陷阱(trap),Linux 内核会捕捉该陷阱,并恰当处理由于非对齐产生的问题,从而可以让应用程序正常运行下去。有关细节,将在本书第七篇“操作系统”中讲述。读者亦可参阅其他讲述操作系统的书籍。 367 | 368 | ## 2.5 常见二进制运算 369 | 370 | ### 2.5.1 二进制逻辑运算 371 | 372 | 二进制逻辑运算就是比特位的逻辑运算。当两个字节、字或双字等(以下简称“操作数”)进行二进制逻辑运算时,对应位上的比特参与运算,和其他位没有关系。常见的逻辑运算有如下取反、或、与、异或等四种。 373 | 374 | 取反。该运算针对单个操作数进行处理,顾名思义,该运算对操作数的每个比特位做取反操作,亦即将 0 变成 1,将 1 变成 0。取反操作是单目运算,通常使用“`~`”运算符表示。比如,`~0xFF` 的结果是 0x00。 375 | 376 | 或。该运算是双目运算,两个操作数上的对应比特位的值做“或”运算,亦即只要其中有一个比特位的值是 1,则结果为 1,否则结果为 0。或运算通常使用“`|`”运算符表示。比如,`0xF0 | 0x0F` 的结果是 0xFF。 377 | 378 | 与。该运算是双目运算,两个操作数上的对应比特位的值做“与”运算,亦即当两个比特位的值都是 1 时结果 1,否则结果为 0。或运算通常使用“`&`”运算符表示。比如,`0xF0 & 0x0F` 的结果是 0x00。 379 | 380 | 异或。该运算是双目运算,两个操作数上的对应比特位的值做“异或”运算,亦即当两个比特位不同时结果1,否则结果为 0。异或运算通常使用“`^`”运算符表示。比如,`0xF0 ^ 0x0F` 的结果是 0xFF。异或运算有两个比较特殊的特性,一个是归零,即 A^A=0;一个是自反,即 A^B^B=A。前者可用于程序优化,比如在汇编程序中,可用来将某寄存器的值归零,而避免采用赋值操作(赋值操作通常需要一条额外的指令来完成);后者可用于简单的加解密程序。 381 | 382 | ### 2.5.2 二进制移位操作 383 | 384 | 移位操作就是针对二进制数执行整体左移或者整体右移给定位数的操作,使用“`<<`”或者“`>>`”运算符表示。比如:`0xF0 >> 4` 的结果是 0x00。在汇编级别,大部分处理器支持自定义移位操作后空出来的位填充 0 还是 1。在高级编程语言中,通常补 0。 385 | 386 | 根据数学法则,对任意二进制数执行左移一位操作,就相当于乘以 2;执行右移一位操作就相当于除以 2。需要注意的是,这个法则实际上是通用的,比如对十进制的表达,左移一位就相当于乘以 10。因此,移位操作也经常用来优化计算,尤其是对整数的乘 2 和除 2 这种运算。另外还要注意的是,左移情形下最高位会丢失,右移情形下最低位(余数)会丢失。 387 | 388 | ### 2.5.3 二进制运算的应用 389 | 390 | #### *1) 常见应用* 391 | 392 | 尽管二进制在计算机中非常基础,但大部分程序基本上无需或很少需要直接进行二进制的操作和运算。二进制数的许多特性,比如使用逻辑门来组成加法器,被直接设计到了计算机或通信设备的物理层面,也就是说,计算机或通信设备的物理器件会更多使用基础的二进制操作。 393 | 394 | 在我们平常的编程工作中,二进制运算通常应用于不多的几个场景。其中最为常见的是单个字节序列和多字节数据(字、双字等)的相互转换。比如,我们要将两个字节合并为一个字,则可以使用如下的伪代码: 395 | 396 | ``` 397 | WORD high_half_word = 0x12 398 | WORD low_half_word = 0x34 399 | 400 | high_half_word = high_half_word << 8 401 | WORD my_word = high_half_word + low_half_word 402 | ``` 403 | 404 | 执行上述伪代码程序之后,`my_word` 的值将为 0x1234。当然,我们还可以将这个功能写成一个函数(或例程),如: 405 | 406 | ``` 407 | FUNCTION WORD assemble_word (BYTE high_byte, BYTE low_byte) 408 | WORD high_half_word = high_byte 409 | RETURN (high_half_word << 8) | low_byte 410 | ``` 411 | 412 | 上面两个伪代码片段在实现同一功能时有稍许的不同。前者先将两个字节从字节数组中取出并分别赋值给了两个字变量(`high_half_word `和 `low_half_word`),然后使用加运算获得了最终的字;后者使用了或运算。但无论如何,两段代码均使用了移位操作将低位字节左移 8 位后赋值给了 `high_half_word` 变量。另外,后者没有使用 `low_half_word` 变量,这是因为伪代码会根据其他操作数的类型自动执行类型转换,这里就是将 `BYTE` 类型转换为 `WORD` 类型,新的 `WORD` 变量的低八位来自 `BYTE` 变量,高八位填充 0。 413 | 414 | 需要说明的是,前一段代码更贴近于最终的计算机指令执行情况。尽管后一段代码更加短小,但最终在计算机上执行的指令也会展开成前一段代码那样去执行。 415 | 416 | 类似地,我们可以编写将字、双字等多字节数据解开成字节序列的代码,如: 417 | 418 | ``` 419 | FUNCTION BYTE, BYTE disassemble_word_a (WORD word) 420 | low_half_byte = (BYTE)(word & 0x00FF) 421 | high_half_byte = (BYTE)((word >> 8)) & 0x00FF) 422 | RETURN high_half_byte, low_half_byte 423 | ``` 424 | 425 | 上面的 `disassemble_word_a` 函数将给定的字 `word` 解开成两个字节并返回。 426 | 427 | 我们也可以通过字节数组来实现类似的功能,但需要注意的是,使用字节数组时,就牵涉到前述的字节序问题了。比如: 428 | 429 | ``` 430 | FUNCTION BYTE[] disassemble_word_b (WORD word) 431 | 432 | BYTE[] bytes 433 | bytes[0] = (BYTE)word 434 | bytes[1] = (BYTE)(word >> 8) 435 | RETURN bytes 436 | ``` 437 | 438 | 上面的 `disassemble_word_b` 函数最终返回的字节数组中,低地址包含 `word` 的低八位,高地址包含高八位,也就是说,其字节序按小头进行了处理。另外,`disassemble_word_b` 和之前的 `disassemble_word_a` 函数还有一个区别是没有做“与”操作,这是因为从 `WORD` 类型转换为 BYTE 类型时,仅会保留低八位数据;也就是说,`disassemble_word_a` 函数中的“与”操作其实是多余的。 439 | 440 | 另外一个二进制运算的典型应用场合是将比特位作为标志(flag)使用,某个特定的比特位为 1 时,表明对应的判定为真,反之,特定的比特位为 0 时,表明对应的判定为假。比如我们一共有八个小篮子,每个篮子里边可以放一个鸡蛋,现在要随机放入或取走一个鸡蛋,可能连续有几次要放入鸡蛋,或者连续几次要取走鸡蛋,执行多次操作之后,有些篮子里边有鸡蛋而有些篮子里边没有鸡蛋。要快速确定哪些篮子是空的或者有鸡蛋,我们可以使用一个字节来表示篮子的使用情况,字节中的每个比特位表示对应的篮子里边是否有鸡蛋,比如第 0 位标识编号为 0 的篮子。现在想要取走一个鸡蛋,要想知道哪个篮子里边有鸡蛋,可以使用下面的函数: 441 | 442 | ``` 443 | FUNCTION BYTE first_true_bit_a (BYTE byte) 444 | 445 | IF (byte & 00000001b) 446 | RETURN 0 447 | IF (byte & 00000010b) 448 | RETURN 1 449 | IF (byte & 00000100b) 450 | RETURN 2 451 | IF (byte & 00001000b) 452 | RETURN 3 453 | IF (byte & 00010000b) 454 | RETURN 4 455 | IF (byte & 00100000b) 456 | RETURN 5 457 | IF (byte & 01000000b) 458 | RETURN 6 459 | IF (byte & 10000000b) 460 | RETURN 7 461 | 462 | RETURN 0xFF 463 | ``` 464 | 465 | 上面的 `first_true_bit_a` 函数非常啰嗦,其实我们可以循环语句来编写: 466 | 467 | ``` 468 | FUNCTION BYTE first_true_bit_b (BYTE byte) 469 | 470 | BYTE[]indexes = [0, 1, 2, 3, 4, 5, 6, 7] 471 | 472 | BYTE idx 473 | FOR idx IN indexes 474 | # FOR idx IN RANG (0, 7) 475 | IF (byte & (0x01 << idx)) 476 | RETURN idx 477 | 478 | RETURN 0xFF 479 | ``` 480 | 481 | 注意,上面的两个函数在没有找到有鸡蛋的篮子时,会返回 0xFF(十进制的 255)。上面这个函数还可以不使用 `indexes` 字节数组,而使用如下的方式编写: 482 | 483 | ``` 484 | FUNCTION BYTE first_true_bit_c (BYTE byte) 485 | 486 | BYTE idx = 0 487 | 488 | WHILE (idx < 8) 489 | IF (byte & (0x01 << idx)) 490 | RETURN idx 491 | idx = idx + 1 492 | 493 | RETURN 0xFF 494 | ``` 495 | 496 | 当然,要解决上面的鸡蛋篮子问题,也可以使用字节数组来表示对应篮子是空的还是有鸡蛋的。使用比特位则可以节省空间的使用,这种技巧在存储空间比较紧张的系统中经常使用。 497 | 498 | 另外一个二进制运算的典型应用场合是字符集编码的处理,可用来判断特定的字节序列是否是一个有效的字符。详情请见本篇第 4 章“文字:字符集及编码”。 499 | 500 | #### *2) 传输校验* 501 | 502 | 在二进制数据的传输或存储过程中,经常会遇到因为各种干扰或硬件失效而导致的数据丢失或损坏问题。此时,最有效的解决办法就是增加所传输数据的冗余量,从而可以根据数据的冗余信息恢复数据,本书第 X 章“”提到的磁盘阵列技术是最典型的用例。另外一些情形下,我们只需要知道数据的传输是否正确,如果不正确,可以要求对方再次传输。此时,我们可以在数据中增加一些专门用于校验数据正确性的校验码来解决这个问题。 503 | 504 | 一个最简单的校验方式就是奇偶校验。奇偶校验计算所传输的每个字节中位值为 1 的比特位个数,如果该个数是奇数,则校验位为 1,如果个数是偶数,则校验位为 0[^7]。这样,可以在一定概率[^8]基础上判断传输的正确性。通常,使用一个字节的最高位作为校验位,其余低七位包含了真正的数据。针对这里描述的奇偶校验方法,要判断一个收到的字节是否在传输过程中出现问题,则只需要计算该字节中位值为 1 的比特位(简称“真比特位”)个数,如果结果是偶数,可认为是正确的,否则需要重新传输[^9]。 505 | 506 | 如下函数计算给定字节中真比特位的个数: 507 | 508 | ``` 509 | FUNCTION BYTE get_nr_true_bits (BYTE byte) 510 | BYTE idx = 0 511 | BYTE count = 0 512 | 513 | WHILE (idx < 8) 514 | IF (byte & (0x01 << idx)) 515 | count = count + 1 516 | idx = idx + 1 517 | 518 | RETURN 0xFF 519 | ``` 520 | 521 | 上面这个函数可以正常工作,但每次调用需要八次循环才能获得结果。我们可以考虑使用查表法来优化这个函数,也就是说,将可能的值事先计算好,然后直接返回对应数组中的单元就可以了。基于此优化方法,我们可以将这个函数优化为只有下面两行代码: 522 | 523 | ``` 524 | FUNCTION BYTE get_nr_true_bits (BYTE byte) 525 | CONST BYTE[] counts = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4] 526 | RETURN counts [byte & 0x0F] + counts [byte >> 4 & 0x0F] 527 | ``` 528 | 529 | 注意,我们只保存了值从 0 到 0x0F(0~15)这十六个字节的真比特位个数,然后将一个字节分成了两半分别查询对应的真比特位个数,最终返回这两个个数的和。 530 | 531 | 在使用时,数据的发送端可调用上面的函数生成包含偶校验位(最高位)的字节(调用前,字节的最高位必须为 0,真正的数据仅包含在低七位): 532 | 533 | ``` 534 | BYTE count = get_nr_true_bits (my_byte) 535 | 536 | IF (count & 0x01) 537 | my_byte = my_byte | 0x40 538 | 539 | ... 540 | ``` 541 | 542 | 接收端可如下调用上述函数完成偶校验: 543 | 544 | ``` 545 | BYTE count = get_nr_true_bits (my_byte) 546 | 547 | IF (count & 0x01) 548 | RETURN FALSE 549 | 550 | ... 551 | ``` 552 | 553 | 在上面的使用代码中,通过和 0x01 相与,即可判断 `count` 变量的奇偶性。 554 | 555 | 奇偶校验其实是循环冗余校验(CRC,cyclic redundancy check)方法的一种特列。循环冗余校验在通信领域使用较多,我们可以将 CRC 算法看成是散列(哈希,hash)函数,其基本思想是将传输的数据当做一个位数很长的二进制数,将这个数除以另一个数,所得到的余数作为该数据的校验码。有关散列函数和 CRC 算法的详情,读者可参阅本书第 12 章“压缩及加密”。在其他的散列和加解密算法中,也经常使用各种二进制位运算,某些是算法本身利用了二进制算术的一些特性,某些则利用二进制做性能优化。 556 | 557 | #### *3) 图形学中的应用* 558 | 559 | 通常在 32 位电子计算机上,每条指令操作的数据是双倍字,而在 64 位电子计算机上,每条指令操作的数据是四倍字。如果在 32 位或 64 位计算机上,让每条指令仅处理一个字节,将会导致计算能力的浪费,或者反过来想,我们可以在一些情况下,将处理字节的算法组合成双倍字或者四倍字来进行处理,从而充分利用计算机的处理能力。这一优化思想在图形学中尤其有用。 560 | 561 | 计算机屏幕上的每个点一般使用红绿蓝(RGB)三种颜色来表示,称为颜色分量,取值范围一般为 0x00~0xFF,可表达 224 种颜色。针对不同的颜色分量取不同的值,就可以表示不同的颜色。有关详情,读者可参阅本书第五篇“信息的计算机展现”中的相关章节。 562 | 563 | 在计算图形学中,我们经常需要进行像素的混合运算。比如给定两个像素,各取两个像素当中 RGB 分量的一半形成结果像素,就实现了半透明效果。在程序中,RGB 三个颜色分量可使用 32 位二进制双倍字来表示,低八位表示B分量、八位到十六位表示 G 分量,十六位到二十四位表示 R 分量[^10]。要计算上面的半透明混合效果下的结果像素,可使用下面的代码: 564 | 565 | ``` 566 | FUNCTION DWORD blend_pixels (DWORD p1, DWORD p2) 567 | DWORD b1 = (p1 & 0xFF) 568 | DWORD g1 = (p1 >> 8) & 0xFF 569 | DWORD r1 = (p1 >> 16)& 0xFF 570 | 571 | DWORD b2 = (p2 & 0xFF) 572 | DWORD g2 = (p2 >> 8) & 0xFF 573 | DWORD r2 = (p2 >> 16)& 0xFF 574 | 575 | b1 = (b1 >> 1) | (b2 >> 1) 576 | g1 = (g1 >> 1) | (g2 >> 1) 577 | r1 = (r1 >> 1) | (r2 >> 1) 578 | 579 | RETURN (r1 << 16) | (g1 << 8) | b1 580 | ``` 581 | 582 | 上面的 `blend_pixels` 函数使用位运算替代了乘法和加法运算,速度将相当快,但只能适用于一半一半的透明效果情况下。当我们要实现一个像素 1/3 另一个像素 2/3 的半透明混合效果时,则无法使用这个方法,此种情况下的代码如下: 583 | 584 | ``` 585 | FUNCTION DWORD blend_pixels_with_alpha (DWORD p1, DWORD p2, BYTE alpha) 586 | DWORD b1 = (p1 & 0xFF) 587 | DWORD g1 = (p1 >> 8) & 0xFF 588 | DWORD r1 = (p1 >> 16)& 0xFF 589 | 590 | DWORD b2 = (p2 & 0xFF) 591 | DWORD g2 = (p2 >> 8) & 0xFF 592 | DWORD r2 = (p2 >> 16)& 0xFF 593 | 594 | b1 = (b1 * alpha/255) | (b2 * (255-alpha)/255) 595 | g1 = (g1 * alpha/255) | (g2 * (255-alpha)/255) 596 | r1 = (r1 * alpha/255) | (r2 * (255-alpha)/255) 597 | 598 | RETURN (r1 << 16) | (g1 << 8) | b1 599 | ``` 600 | 601 | 注意在上面的 `blend_pixels_with_alpha` 函数中,我们使用取值范围为 0\~xFF 的 Alpha 值来确定结果像素中第一个像素的作用大还是第二个像素的作用大。读者可以将 Alpha 值看作是一个权重参数。在更加一般的图形库(比如 OpenGL 中),通常使用 0~1 之间的实数来表示该权重。因为使用了整数的乘法和除法运算(或者实数的乘除运算), 602 | `blend_pixels_with_alpha` 函数的性能显然要远远低于上面的 `blend_pixels` 函数。 603 | 604 | 根据前述的优化思想,我们可以在 64 位系统上对该函数做适当的优化,其核心思想是将 RGB 三分量扩展到 64 位四倍字中进行计算,将上面的三次乘除运算简化为一次乘除运算。如下所示: 605 | 606 | ``` 607 | FUNCTION DWORD blend_pixels_on_64bit (DWORD p1, DWORD p2, BYTE alpha) 608 | BYTE b1 = (p1 & 0xFF) 609 | BYTE g1 = (p1 >> 8) & 0xFF 610 | BYTE r1 = (p1 >> 16)& 0xFF 611 | 612 | BYTE b2 = (p2 & 0xFF) 613 | BYTE g2 = (p2 >> 8) & 0xFF 614 | BYTE r2 = (p2 >> 16)& 0xFF 615 | 616 | QWORD qp1 = (r1 << 32) | (g1 << 16) | b1 617 | QWORD qp2 = (r2 << 32) | (g2 << 16) | b2 618 | 619 | qp1 = (qp1 * alpha/255) + (qp2 * (255-alpha)/255) 620 | 621 | b1 = (qp1 & 0xFF) 622 | g1 = (qp1 >> 16) & 0xFF 623 | r1 = (qp1 >> 32)& 0xFF 624 | 625 | RETURN (r1 << 16) | (g1 << 8) | b1 626 | ``` 627 | 628 | 以上优化的思路是,将8位的 RGB 分量在 64 位四倍字中扩展为 16 位(高八位取0),然后进行 Alpha 混合计算。扩展到 16 位的目的是,在进行 alpha 有关的乘法运算时,不会导致溢出。最后再从结果四倍字中取出 RGB 分量并组装成 32 位的 RGB 像素值返回。 629 | 当我们使用 C 语言实现上述函数时,我们还可以充分利用 C 语言的直接访问地址能力,快速分解和组装像素,这体现了 C 语言的优势。如下所示(注意该代码仅适用于 64 位系统): 630 | 631 | ```c 632 | typedef BYTE unsigned char; 633 | typedef UINT32 unsigned int; 634 | typedef UINT64 unsigned long; 635 | 636 | UINT32 blend_pixels_on_64bit (UINT32 p1, UINT32 p2, BYTE alpha) 637 | { 638 | BYTE* pp1 = (BYTE*)(&p1); 639 | BYTE* pp2 = (BYTE*)(&p2); 640 | 641 | UINT64 qp1 = 0, qp2 = 0; 642 | BYTE *pqp1 = (BYTE*)&qp1, *pqp2 = (BYTE*)&qp2; 643 | 644 | pqp1[0] = pp1[0]; 645 | pqp1[2] = pp1[1]; 646 | pqp1[4] = pp1[2]; 647 | 648 | pqp2[0] = pp2[0]; 649 | pqp2[2] = pp2[1]; 650 | pqp2[4] = pp2[2]; 651 | 652 | qp1 = ((qp1 * alpha) >> 8) + ((qp2 * (255-alpha)) >> 8); 653 | 654 | pp1[0] = pqp1[0]; 655 | pp1[1] = pqp1[2]; 656 | pp1[2] = pqp1[4]; 657 | 658 | return p1; 659 | } 660 | ``` 661 | 662 | 注意在上面的 C 代码中,我们还将除以 255 的运算用右移 8 位的操作替代(尽管有误差,但可以接受)。我们还可以将这条语句写成下面这种形式: 663 | 664 | ``` 665 | qp1 = (qp1 * alpha + (qp2 * (255-alpha))) >> 8; 666 | ``` 667 | 668 | 许多读者可能会担心上面的优化会导致扩展后的 RGB 16 位分量在执行加法运算后溢出,但在这种情况下应该不会发生此种情形。 669 | 670 | 通过上面的优化,对此类问题我们可以实现某种程度上的并行计算。类似地,我们也可以在 32 位系统上针对 16 位颜色深度做类似的优化。 671 | 672 | [^1]: 数据来源于维基百科。 673 | 674 | [^2]: 数据来源于维基百科。 675 | 676 | [^3]: 最典型的混乱就是硬盘容量大小的明明标记为 1TB,而实际使用时 Windows 告诉你的数值却不足 1TB。这是因为硬盘生产厂商使用的单位是 TB,而在计算机操作系统中实质上使用的是 TiB 这个单位,但界面又显示为 TB。 677 | 678 | [^4]: 除非程序使用文中所明示的特定编程语言给出,否则本书中的程序均使用伪代码。本书所使用伪代码的语法见本书附录A。 679 | 680 | [^5]: 我们说某种编程语言是低级编程语言,并不是说这种编程语言不好,而是指这种编程语言离处理器指令集较为接近。最低级的编程语言当属汇编语言。 681 | 682 | [^6]: 在支持虚拟内存的操作系统,比如 Linux 中,因为内核会捕捉这种处理器异常并作适当的处理,从而在不支持虚拟内存的 RTOS 系统上,对齐问题的表现却很明显。 683 | 684 | [^7]: 这里描述的奇偶校验方法中的校验位称为“偶校验位”。使用偶校验位时,可确保包括校验位在内的所有位值为 1 的比特位个数是偶数。与之相反的就是“奇校验位”。另外,当所传输的数据位出现偶数位失效(比如有两个比特位从 1 变成 0)的情况下,奇偶校验并不能有效侦测错误。 685 | 686 | [^8]: 差不多是 50%。 687 | 688 | [^9]: 实践当中,奇偶校验以及下面介绍的循环冗余校验,都由计算机的电子物理器件完成,上层软件基本不需要做相应的处理。 689 | 690 | [^10]: 这是颜色深度为 32 位的情形下的像素分量表示方式,在颜色深度为 16 位情形下,RGB 三个分量分别占用 5、6、5 比特位。详情见第五篇“信息的计算机展现”。 691 | 692 | 693 | -------------------------------------------------------------------------------- /textbook/part-1-chapter-3.md: -------------------------------------------------------------------------------- 1 | # 第 3 章 数:整数、浮点数及定点数 2 | 3 | 发明计算机的初衷就是为了替代人来进行复杂、繁重的工程计算问题。本章将讲述计算机如何表述数的世界。需要注意的是,由于计算机使用离散的二进制数来表示实数,所以,计算机处理实数只能获得近似的结果,但这对一般的工程计算而言已经足够了。当然,绝对的精确是不存在的也是没有必要的。就拿圆周率来讲,小数点后10位就足够日常使用了。 4 | 5 | ## 3.1 整数 6 | 7 | 之前看到,我们可以使用二进制的字节、字、双字等表示从 0 开始的有限的正整数,这非常自然。比如一个字节,其可以表达的整数范围在 0~0xFF 之间,也就是 [0, 255]。在此基础上,我们可以完成整数的基本运算。但问题马上来了,我们如何使用字节、字、双字来表达负整数? 8 | 9 | 一个比较容易想到的解决办法是使用将字节、字、双字的取值范围一分为二,除了0之外,一部分表示正整数,一部分表示负整数。这时,二进制的最高位(MSB)来可用来表示符号,比如负数时,MSB 为 1,正整数的 MSB 为 0。这样,我们用 0~0x7F 的范围可表示 [0, 127] 这个区间的 0 和正整数,用 0x80 ~ 0xFF 的范围来表示 [-128, -1] 这个区间的负整数。对后一种情况,0x80 表示 -128 还是 0x80 表示 -1 是一个需要仔细考量的问题。 10 | 11 | 所幸的是,根据二进制运算的一些特性,我们发现用 0xFF 表示 -1、0x80 表示 -128 更为合理。比如,如果将 0xFF 和 0x01 看成是正整数,则 0xFF + 0x01 的结果应是 0x0100,而这个结果的低八位值(0x00),刚好是将 0xFF 看成 -1 时的结果。这就是补码(complement number)的来由。也就是说,我们可以通过补码的方式来确定一个正整数的负值表达。在使用字节时,0x01 的补码是 0xFF,则 0xFF 用来表达 -1;0x02 的补码是 0xFE,则 0xFE 可表达 -2,依此类推。 12 | 13 | 对字、双字、双倍字等其他长度的二进制数,补码原理也一样适用。在我们的伪代码 P 语言中,使用 `INT` 类型来表示数学上的整数。和其他语言类似,整数类型在 16 位系统上的取值区间是 [-32,768, 32,767]、在 32 位系统上的取值区间是 [-2,147,483,648, 2,147,483,647]、而在 64 位系统上的取值区间是 [-9,223,372,036,854,775,808, 9,223,372,036,854,775,807]。 14 | 15 | 在下一章“文字:字符集及其编码”中,我们将给出一段 P 语言程序,该程序可以将一个使用二进制补码形式表达的整数转换为十进制表达的 100、-100 这种形式(文本字符串)。 16 | 17 | ## 3.1.1 整数运算 18 | 19 | 一般的编程语言针对整数提供有四则运算、取模(相除的余数)及比较逻辑运算。在 P 语言中,用以表示整数的数据类型是 INT,在 32 位系统上对应双字,在 64 位系统上对应四倍字。另外,我们可以将 P 语言的字节、字、双字、四倍字等类型分别看成是8位、16位、32位、64 位的无符号整数,将这些数据类型转换成 `INT` 型时,会根据补码规则转换为对应的有符号整数。 20 | 21 | 下面的 P 语言代码段,给出了一个判断给定的整数是否为素数(质数)的 P 语言函数: 22 | 23 | ``` 24 | FUNCTION BOOL is_prime (INT x) 25 | IF (x <= 1) 26 | RETURN false 27 | 28 | INT i = 2 29 | 30 | # 从 2 开始试除 x,当 (i ^ 2) 大于 x 时结束。 31 | WHILE ((i * i) <= x) 32 | # 判断 i 是否可以整除 x,使用取模运算。当余数为 0 就表明可被整除。 33 | IF ((x % i) == 0) 34 | RETURN true 35 | i = i + 1 36 | 37 | RETURN true 38 | ``` 39 | 40 | `is_prime` 函数接受一个整数型参数 `x`,通过试除法判断该整数是否为素数。 41 | 42 | 在整数运算中,需要特别注意溢出的情形。溢出是指,运算的结果超出了给定类型的表达范围。比如下面的代码: 43 | 44 | ``` 45 | BYTE b = 0xF1 + 0x0F 46 | ``` 47 | 48 | 执行上述语句后,b 的值将为 0x01,但我们期望的应该是 0x0101。这是因为要表达 0x0101 这一二进制数,需要至少 9 位,但 BYTE 数据类型只能表示 8 位二进制数。类似地,整数的乘法、减法以及除法运算都可能出现溢出的情形,这里除法溢出的情形特指被 0 除的情况。 49 | 50 | 在当今流行的电子计算机处理器设计中,整数四则运算的溢出通常通过指令执行结果的状态位(这种情形下使用溢出位来判断是否出现溢出)来表示,而被零除的情形则通过中断或者陷阱的形式通知程序。 51 | 52 | 为应对普通的溢出情况,我们可以将两个字组合成一个单位在 16 位处理器上进行 32 位整数的运算,或者在 32 位处理器上进行 64 位整数的运算。比如下面的代码演示了如何在 32 位系统上执行两个 64 位整数的加法及减法运算: 53 | 54 | ``` 55 | FUNCTION INT[] add_64b_int (INT[] augend, INT[] addhead) 56 | 57 | 58 | FUNCTION INT[] substract_64b_int (INT[] minuend, INT[] substrahead) 59 | ``` 60 | 61 | 上面的代码使用 `INT` 型数组(两个单元)在 32 位系统上表示 64 位的(有符号)整数。这段代码特别考虑了有符号整数的处理。 62 | 63 | 在支持多种长度整数类型的编程语言(如 C 语言),编译器可以帮我们完成类似上面的额外处理工作,从而在编写程序时,程序员只需要处理运算过程中是否会出现溢出情形以及被零除的情形。 64 | 65 | 对于被零除的情形,在使用高级语言进行编程的时候,我们可捕捉程序产生的异常(处理器通过中断或陷阱来通知此种情形的发生。在现代操作系统中,操作系统会处理被零除的中断或陷阱,如果正在执行的程序没有做相应的处理,则会终止这个程序的运行)。当然,最好的办法应该是检查算法,避免出现被零除的情况。在支持异常处理的语言(如 P 语言)中,我们可使用类似下面的代码来处理这种异常: 66 | 67 | ``` 68 | TRY 69 | a = a / 0 70 | CATCH (DividedByZeroError e) 71 | STDIO.printl ("Exception: Integer divided by zero.") 72 | ``` 73 | 74 | 在汇编语言或者比较低级的语言(如 C 语言)中,则需要通过使用中断处理器或信号处理器来完成相应的处理。相关详情可参阅本书第XX章“现代操作系统”。 75 | 76 | ### 3.1.2 C 语言中的整数类型 77 | 78 | C 语言的 C99 规范为整数定义了 `char`、`short`、`int`、`long`、`long long` 等多种类型。这些整数类型在不同的处理器上使用不同的长度,如表 3-1 所示。 79 | 80 | 表 3-1 C 语言的整数类型及其长度 81 | 82 | | 类型 | 32 位处理器 | 64 位处理器 | 83 | |:-------------|:------------|:------------| 84 | | char | 8 | 8 | 85 | | short | 16 | 16 | 86 | | int | 32 | 32 | 87 | | long | 32 | 64 | 88 | | long long | 64 | 64 | 89 | 90 | 91 | 根据上表,要在 64 位处理器上使用 64 位整数运算,需要将整数定义为 `long` 型而不是 `int` 型。 92 | 93 | 在 C 语言中,为避免出现整数的运算溢出,一般的做法是适当选择整数类型,尤其在中间计算过程中。比如下面的函数: 94 | 95 | ```c 96 | int multiply_divid (int multiplier_a, int multiplier_b, int divider) 97 | { 98 | long product = (long) multiplier_a * multiplier_b; 99 | return (int)(product/divider); 100 | } 101 | ``` 102 | 103 | 上面的 C 函数在计算 `multiplier_a` 和 `multiplier_b` 的乘积时,将其中一个转换成了 `long` 型(另外一个乘数会被隐式转换为 `long` 型),这是因为两个 32 位的整数相乘其结果有可能会溢出。当然,这个函数最终返回的结果仍有可能是溢出的(试想 `product` 被 1 整除的情形)。值得一提的是,在某些处理器上,有专门完成上述运算的指令。 104 | 105 | ## 3.2 浮点数 106 | 107 | 和整数不同,浮点数在计算机中的表达方式相对复杂很多。从数学上看,计算机中的整数只能用来表示有限范围中的自然数、整数和/或负数。如果要利用计算机进行实数(无限小到无限大,包含有理数,也包含无理数等)范围内的运算,则必须采用某种方式来表示实数。显然,使用基于字节为处理单元的现代计算机技术,是无法精确表达任意一个实数的,因此,我们必须采用某种方式来近似所要处理的任意实数,从而获得在一定范围内可以接受的运算精度。浮点数就是用计算机表达实数的一种方式,这种方式仍然采用有限多个字节的位数来表示单个实数,因此,其表达精度受到限制,但这种浮点数的表达方式,可以用计算机最容易理解的方式存储实数,从而可以在一定程度上提高实数的运算速度。我们也可以这样理解:浮点数是计算机表示实数时,在精度和性能之间的一种折衷结果。本小节将向大家简单介绍浮点数的类型以及存储格式。 108 | 109 | ### 3.2.1 浮点数的类型及表述 110 | 111 | 目前广泛使用的浮点数是由 IEEE(电气电子工程师协会)定义[^1]的,总共有四种浮点数,分别是单精度(32 位)、双精度(64 位)、扩展精度(80 位)和四倍精度(128 位)。 112 | 113 | 在计算机中,存储浮点数的基本思路如下: 114 | 115 | - 将任意一个实数,其绝对值都可以转换为 X*2^E 形式,其中 X 称为尾数,E 称为指数或者“阶)。如果我们限定 X 必须大于等于 1 小于 2 ,则对任意一个实数来讲,上述表达形式都将是唯一的。这一过程也称为实数的正规化。 116 | - 利用单个位来表示实数的符号,即正实数还是负实数。符号位为 0 时表示正数,符号位为 1 时表示负。 117 | 118 | 这样,一个浮点数就由三个部分组成:表示正负的单个符号位、指数(E)以及尾数(X)。注意尾数,因为我们已经将尾数限定在大于等于1而小于 2 的范围内了,因此,其实尾数表示的是减去 1.0 之后的有效小数,存储时也仅仅存储有效小数。另外,考虑到指数可能为正,也可能为负,因此,在存储指数时,要先加上一个偏移量;对单精度浮点数,偏移量为 127(0x7F),对双精度浮点数,偏移量为 1023(0x3FF)。不同精度的浮点数,除了指数和尾数所占的位数不同以外,没有其他区别。比如单精度浮点数,指数占 8 位,尾数占 23 位,符号位占1位;双精度浮点数,指数占 11 位,尾数占 53 位,符号位占1位。下面的公式给出了单精度浮点数转换成十进制时的计算方法: 119 | 120 | 121 | ``` 122 | d = (-1)^S * (1 + X) * 2^(E - 127) 123 | ``` 124 | 125 | 通过这样一种方式表示实数时,不同大小的实数其阶数会不一样,这也是浮点(float)这个名称的来由。另外,浮点数还有几种特殊表达方式: 126 | 127 | - 实数 0:全部位均为 0。符号位为 1 时称为负零,即 -0.0。 128 | - 无限大数:指数位全部为 1,尾数位全部为 0,符号位表示正无穷和负无穷,分别用 `INF` 和 `–INF` 表示。 129 | - 如果指数 达到最大可能值,且尾数非 0,这个数表示为 `NaN`(非实数),比如 -1.0 的平方根就是 `NaN`。 130 | 131 | 注意无限大的定义。浮点数的无限大其实是相对的。比如对单精度浮点数而言,能够有效表示的实数是在 (-2\*2^256, 2\*2^256) 这个范围内,因此,超过这个范围的实数都将成为单精度浮点数中的负无穷或者正无穷;而对其他精度的浮点数,正无穷和负无穷的定义就会相应改变。因此,我们要根据自己的浮点运算可能范围来决定采用哪种精度的浮点数进行运算。 132 | 133 | 表 3-2 给出了两个示例,表明两个实数的单精度浮点数表达。 134 | 135 | 表 3-2 单精度浮点数的表达 136 | 137 | | 十进制 | 正规化 | 符号 | 偏移后的指数 | 减去 1.0 之后的尾数 | 138 | |:--------|:---------|:-----|:----------------|:----------------------------| 139 | | -12 | -1.1x23 | 1 | 1000 0010(0x82) | 01000000 00000000 00000000 | 140 | | 0.25 | 1.0x2-2 | 0 | 0111 1101(0x7D) | 00000000 00000000 00000000 | 141 | 142 | 143 | 为了更进一步理解浮点数的表达形式,下面我们将给定的单精度浮点数转换成十进制。假定给定的单精度浮点数表示为:0x49E48E68,则: 144 | 145 | 1) 其第31位为0,即S= 0; 146 | 2) 第30~23 位依次为100 1001 1,转换成十进制就是147,即N = 147。 147 | 3) 第22~0 位依次为110 0100 1000 1110 0110 1000,也就是二进制的纯小数0.110 0100 1000 1110 0110 1000。因此,其十进制形式为: 148 | 149 |
150 | 151 | (0.110 0100 1000 1110 0110 1000 * 2^23)/(2^23) 152 | 153 | =(0x49E48E68 & 0x007FFFFF)/(2^23)=(0x648E68)/(2^23) 154 | 155 | = 0.78559589385986328125 156 | 157 |
158 | 159 | 160 | 即 X = 0.78559589385986328125。这样,该浮点数的十进制表示为: 161 | 162 |
163 | 164 | = (- 1)^S \* (1 + X) * 2(N - 127) 165 | 166 | = (- 1)^0 \* (1 + 0.78559589385986328125) * 2^(147 - 127) 167 | 168 | = 1872333 169 | 170 |
171 | 172 | 173 | 在浮点数的应用中,我们需要注意,我们常见的十进制表达的有限不循环小数不能用二进制浮点数做精确表达,因为我们要将这些实数使用 2 的阶数而不是 10 的阶数表示。因此存在误差,在经过一定的浮点数运算之后,这种误差会扩大。比如 0.1 和 0.01[^2],0.01 是 0.1 的平方。在单精度浮点数中,表示 0.1 的最接近的数是: 174 | 175 |
176 | 177 | 0.100000001490116119384765625 178 | 179 |
180 | 181 | 对此数做平方运算,其结果是 182 | 183 |
184 | 185 | 0.10000000298023226097399174250313080847263336181640625 186 | 187 |
188 | 189 | 但最接近 0.01 的二进制浮点数却是: 190 | 191 |
192 | 193 | 0.009999999776482582092285156250 194 | 195 |
196 | 197 | ### 3.2.2 浮点数的字节序 198 | 199 | 在特定架构上存储浮点数时,和整数一样,也存在字节序的问题。表 3-3 给出了浮点数的常见字节序以及对应的典型架构。 200 | 201 | 表 3-3 浮点数常见字节 202 | 203 | | 浮点数字节序 | 典型架构 | 示例
(1.2345678e10的单精度浮点数表达) | 204 | |:----------------|:----------------|:---------------------------------------| 205 | | 标准小头 | Intel 的 IA32 | 00 00 80 c5 e0 fe 06 42 | 206 | | 标准大头 | Motorola 的 M68k | 42 06 fe e0 c5 80 00 00 | 207 | | 大头字、小头字节 | ARM | e0 fe 06 42 00 00 80 c5 | 208 | 209 | 210 | ### 3.2.3 浮点数运算 211 | 212 | 对比整数和浮点数,浮点数的运算效率要低很多。为了提高运算速度,一些处理器通过浮点协处理器来完成浮点数的基本运算(如四则运算及比较运算),但浮点协处理器的设计往往要比简单的二进制加法器复杂很多,且功耗较大。因此,浮点协处理器并不是当前处理器的标配,毕竟大量的浮点运算仅出现在工程领域。在英特尔的 IA 架构处理器上,一般会包含有浮点协处理器,但也有例外,比如针对移动设备或者笔记本设计的英特尔处理器就不包括浮点协处理器,而智能手机使用的基于 ARM 架构的处理器基本上不包括浮点协处理器。对于后者,往往通过软件来实现浮点数的运算。但不论是否有浮点协处理器,对高级编程语言来讲,我们不需要知悉底层软件或计算机系统到底使用的是浮点协处理器还是软件实现,它们之间的最大区别就是运行速度会有显著差异。 213 | 214 | 根据浮点数相关规范[^3],浮点数的支持需要实现如下基本运算: 215 | 216 | - 加减乘除四则运算。在加减运算中负零和零相等,即:-0.0 = 0.0。 217 | - 平方根(sqrt)。约定 sqrt(-0.0) = -0.0。 218 | - 浮点数圆整(round)。近似到最近的整数,如果恰好在两个相邻整数之间,则近似到偶数。 219 | - 浮点余数。相当于 x - (round(x / y) * y)。 220 | - 比较运算。 221 | 222 | 在基本运算基础之上,我们可以通过数学方法(如级数展开)获得特定实数的非线性函数值,如对数、三角函数等。对浮点协处理器来讲,这些基本运算就形成了协处理器的指令集。而在基本运算基础之上通过软件实现的数学处理函数集合则形成了数学函数库,其中包含求对数、求幂、求正弦等各种常见函数。在 C 语言中,这些数学函数库由 C99 定义和规范化,并作为其他高级编程语言的基础。比如,在 P 语言当中,求解正弦值的函数(sin) 由 MATH 类提供,按如下方法调用: 223 | 224 | ``` 225 | FLOAT f = MATH.sin (3.14) 226 | ``` 227 | 228 | ### 3.2.4 C 语言中的浮点数 229 | 230 | 目前,C 语言通常提供三种浮点数类型,分别是: 231 | 232 | - C 语言类型为 `float`,用来表示 32 位单精度浮点数,占 4 个字节; 233 | - C 语言类型为 `double`,用来表示 64 位双精度浮点数,占 8 个字节; 234 | - C 语言类型为 `long double`,该类型表示的浮点数和平台相关。通常来讲,在 32 位处理器平台上,该类型表示 80 位的扩展精度浮点数,占 10 个字节;而在 64 位处理器平台上,该类型表示 128 位的四倍精度浮点数,占 16 字节。 235 | 236 | 目前,几乎所有的 C 语言数学库都会遵循 C99 规范提供相关接口。C99 规范为不同的浮点数类型定义了相同运算的不同接口。比如,C99 规范为 `float`、`double` 和 `long double` 类型分别定义了计算余弦值的函数接口: 237 | 238 | ```c 239 | #include 240 | 241 | double cos(double x); 242 | float cosf(float x); 243 | long double cosl(long double x); 244 | ``` 245 | 246 | 因此,我们在使用符合 C99 规范的数学库时,需要根据不同的浮点数类型来调用不同的函数接口。 247 | 248 | 另外,目前某些平台上的格式化输入和输出函数,如 `printf/scanf` 等,尚不能提供对 `long double` 浮点数的正确支持,因此,读者在使用 `long double` 时应该注意到这一点。 249 | 250 | ## 3.3 定点数 251 | 252 | ### 3.3.1 定点数的概念 253 | 254 | 在没有浮点协处理器的计算机系统上,如果要进行大量的浮点数运算,其性能会很低。因此,人们想到了另外一种办法来满足高性能实数计算的需求,也就是使用定点数。 255 | 256 | 定点数其实很好理解。比如在会计处理中,我们可以将表示现金的数字全部乘以 100,然后再进行计算。这时,原本需要浮点数的运算就变成了整数运算,将结果再除以 100,余数就是角和分。 257 | 258 | 因此,定点数实质上是整数。我们始终用整数中给定不变的位数来表示一个实数的整数部分,然后用其余的位来表示实数的小数部分(而浮点数的整数部分和小数部分所占的位数会根据实数的值发生变化)。这样,如果用定点数来表示实数,则四则运算可用整数的四则运算来处理,其他的非线性运算(比如平方根、立方根、三角运算等)则可以用查表法得到。 259 | 260 | 这种基于定点数的运算,存在如下优点和缺点: 261 | 262 | - 运算速度非常快。因为大多数的运算实质上是整数运算,或者简单的线性查表运算,因此,定点数的运算速度和整数运算相当。 263 | - 可表示的实数范围有限,且精度较低。比如,如果我们的定点数小数部分只有 4 位,则精度只能达到小数点后两位。 264 | 265 | 由于定点数运算的范围和精度有限,因此,定点数通常用于运算结果被限定在某个区间中的情形。比如,在图形学中,表示一个像素值的 RGB 分量取值区间是有限的([0,255]),此时,如果采用定点数进行像素的混合运算,则其结果是可接受的。 266 | 267 | ### 3.3.2 定点数的 C 语言实现 268 | 269 | 在 32 位计算机上,我们可以使用带符号的 32 位整数来表示一个定点数,取值范围从 -32767.0 到 32767.0;一个定点数用高 16 位表示符号及实数的整数部分,用低16位表示小数部分。类似地,在 64 位计算机上,我们可以使用 64 位整数来表示一个定点数,其取值范围和精度将大大超过 32 位系统。 270 | 271 | 使用 C 语言,浮点数和定点数之间相互转换的函数可以如下实现: 272 | 273 | ```c 274 | typedef int fixed; 275 | 276 | static inline fixed ftofix (double x) 277 | { 278 | if (x > 32767.0) { 279 | errno = ERANGE; 280 | return 0x7FFFFFFF; 281 | } 282 | 283 | if (x < -32767.0) { 284 | errno = ERANGE; 285 | return -0x7FFFFFFF; 286 | } 287 | 288 | return (long)(x * 65536.0 + (x < 0 ? -0.5 : 0.5)); 289 | } 290 | 291 | static inline double fixtof (fixed x) 292 | { 293 | return (double)x / 65536.0; 294 | } 295 | ``` 296 | 297 | 注意,上述函数使用 C 语言的全局 errno 来表示可能出现的超出运算范围等情形。因此,在调用这些函数之后,应该检查 errno 是否有被设置,并在出现错误的情形下做相应处理。 298 | 299 | 相比浮点数,整数和定点数之间的转换函数更为简单: 300 | 301 | ```c 302 | static inline fixed itofix (int x) 303 | { 304 | return x << 16; 305 | } 306 | 307 | static inline int fixtoi (fixed x) 308 | { 309 | return (x >> 16) + ((x & 0x8000) >> 15); 310 | } 311 | ``` 312 | 313 | 这样,定点数的加减运算就变成了整数的加减运算;如下是定点数加法运算: 314 | 315 | ```c 316 | static inline fixed fixadd (fixed x, fixed y) 317 | { 318 | fixed result = x + y; 319 | 320 | if (result >= 0) { 321 | if ((x < 0) && (y < 0)) { 322 | errno = ERANGE; 323 | return -0x7FFFFFFF; 324 | } 325 | else 326 | return result; 327 | } 328 | else { 329 | if ((x > 0) && (y > 0)) { 330 | errno = ERANGE; 331 | return 0x7FFFFFFF; 332 | } 333 | else 334 | return result; 335 | } 336 | } 337 | ``` 338 | 339 | 定点数的乘除法运算要相对麻烦一点,要仔细处理符号位以及小数部分相乘的进位问题,如下所示: 340 | 341 | ```c 342 | fixed fixmul (fixed x, fixed y) 343 | { 344 | int s1 = 1, s2 = 1; 345 | long result; 346 | unsigned long op1_hi; 347 | unsigned long op1_lo; 348 | unsigned long op2_hi; 349 | unsigned long op2_lo; 350 | unsigned long cross_prod; 351 | unsigned long prod_hi; 352 | unsigned long prod_lo; 353 | 354 | if (x < 0) { 355 | s1 = -1; 356 | x = -x; 357 | } 358 | 359 | if (y < 0) { 360 | s2 = -1; 361 | y = -y; 362 | } 363 | 364 | op1_hi = (x >> 16) & 0xffff; 365 | op1_lo = x & 0xffff; 366 | op2_hi = (y >> 16) & 0xffff; 367 | op2_lo = y & 0xffff; 368 | cross_prod = op1_lo * op2_hi + op1_hi * op2_lo; 369 | prod_hi = op1_hi * op2_hi; 370 | if(prod_hi > 0x7FFF){ 371 | errno = ERANGE; 372 | return 0x7FFFFFFF; 373 | } 374 | prod_lo = ((op1_lo * op2_lo) >>16) + cross_prod; 375 | 376 | prod_hi = (prod_hi << 16) + prod_lo; 377 | if(prod_hi > 0x7FFFFFFF){ 378 | errno = ERANGE; 379 | return 0x7FFFFFFF; 380 | } 381 | result = s1 * s2 * prod_hi; 382 | 383 | return (fixed)result; 384 | } 385 | ``` 386 | 387 | 对求平方根、三角运算等非线性运算则采用查表法实现。以平方根为例,该实现首先构造了一个 0~256 值的平方根线性表: 388 | 389 | ```c 390 | static unsigned short _sqrt_tabl[256] = 391 | { 392 | 0x2D4, 0x103F, 0x16CD, 0x1BDB, 0x201F, 0x23E3, 0x274B, 0x2A6D, 393 | 0x2D57, 0x3015, 0x32AC, 0x3524, 0x377F, 0x39C2, 0x3BEE, 0x3E08 394 | ... 395 | 0xF7E3, 0xF867, 0xF8EA, 0xF96E, 0xF9F1, 0xFA74, 0xFAF7, 0xFB79, 396 | 0xFBFB, 0xFC7D, 0xFCFF, 0xFD80, 0xFE02, 0xFE82, 0xFF03, 0xFF83 397 | }; 398 | ``` 399 | 400 | 然后利用如下数学公式推导,我们可将任意实数的平方根转化为依赖于上述线性表的四则运算: 401 | 402 | ```c 403 | 因为:sqrt (x) = sqrt (x/d) * sqrt(d) 404 | 设:d = 2^(2n) 405 | 则:sqrt (x) = sqrt (x / 2^(2n)) * 2^n 406 | ``` 407 | 408 | 在最后的等式中,只要将等式右边的 sqrt 运算限制到 0~255 之间,我们就可以利用在上面给出的 0 ~ 255平方根表中查表获得对应的值,再乘以 2^n 即可得到 sqrt (x) 的值。这样,找到 n 就可以仅仅通过查表和乘法运算计算出 sqrt (x)。而从十六进制的运算规律可知,x / 2^(2n) 恰好相当于将 x 右移 2n 位之后的值。因此,我们只需要知道将 x 右移多少位之后小于 256,即可得到 2n 的值。有了上述推导,定点数的 sqrt 函数实现方式如下: 409 | 410 | ```c 411 | fixed fsqrt(fixed x) 412 | { 413 | int i, dx; 414 | int cx = 0; /* if no bit set: default %cl = 2n = 0 */ 415 | 416 | /* 判断负值... */ 417 | if (x <= 0) { 418 | if (x < 0) 419 | errno = EDOM; 420 | return 0; 421 | } 422 | 423 | /* bit-scan is done on dx */ 424 | dx = x >> 6; 425 | for (i = 0; i < 32; i++) { 426 | if (dx << i & 0x80000000) { 427 | cx = 32 - i; 428 | break; 429 | } 430 | } 431 | 432 | cx &= 0xFE; /* 确保结果为偶数 --> %cl = 2n */ 433 | x >>= cx; /* 右移 x 使其取值范围在 0..255 之间 */ 434 | 435 | x = _sqrt_tabl [x]; /* 查表... */ 436 | 437 | cx >>= 1; /* %cl = n */ 438 | x <<= cx; /* `sqrt(x/2^(2n))' 乘以`2^n' */ 439 | 440 | return x >> 4; /* 调整结果 */ 441 | } 442 | ``` 443 | 444 | ## 3.4 任意精度运算 445 | 446 | 由于浮点数和定点数在实数表达上存在的缺点(浮点数在表示大数的时候会损失小数部分的精度,而定点数的表达范围有限且小数部分的精度也是有限的),仍无法满足某些领域的运算要求。因此,人们又发明了用于任意精度运算的程序或函数库。当然,任意精度运算本质上仍然是有限精度运算,只是其可以表达的精度范围随着计算机可以使用的内存可以无限制增长。 447 | 448 | 因为任意精度运算的应用场合较窄,因此尚未形成业界标准。现今,主要的任意精度运算由 GNU 项目的两个开源(自由)软件实现: 449 | 450 | bc(bench calculator),是一种运行在 UNIX 类操作系统上的任意精度计算器程序,可支持2,147,483,647 位小数点位数。该程序支持类似 C 语言的语法,可编程表达计算公式。 451 | 452 | GMP(GNU Multiple Precision Arithmetic Library,GNU 多精度运算库,简称 GMP),是使用 C 语言开发的多精度运算函数库,可支持整数、有理数以及浮点数,其精度仅受限于可用内存。GMP 可用于密码应用和研究、互联网安全应用、代数系统以及计算代数研究等等。 453 | 454 | [^1]: 【维基百科】在1980年,英特尔公司就推出了单片的8087浮点数协处理器,其浮点数表示法及定义的运算具有足够的合理性、先进性,被IEEE采用作为浮点数的标准,于1985年发布。而在此之前,浮点数的二进制表示混乱而缺乏统一标准。 455 | 456 | [^2]: 这个例子取自【维基百科】。 457 | 458 | [^3]: 即 IEEE 754。 459 | 460 | -------------------------------------------------------------------------------- /textbook/part-1-chapter-4.md: -------------------------------------------------------------------------------- 1 | # 第 4 章 文字:字符集、编码 2 | 3 | 计算机程序的运算有了结果就要输出给人看,且应以便于人类认知的方式来展示结果。最好的方式就是使用人类使用的文字来输出结果了。然而,人类文明的发展丰富多样,语言文字变化多端。对计算机来讲,正确表达不同的文字信息是需要首先解决的问题。本章讲述文字这类信息的计算机表述方法。 4 | 5 | ## 4.1 文字的编码需求 6 | 7 | 为了形象地描述使用计算机表述文字的历史演进过程,我们在这一小节引入两个主人公小明和李教授。小明在一所大学的电子工程实验室做自己的本科论文,李教授是他的导师。 8 | 9 | 小明这会儿正在使用纸带编写一段可连续打印圆周率的程序,这段程序源源不断地在纸带上打眼来输出二进制的圆周率。小明马上发现,二进制的数字得转换成十进制看呀,否则李教授怎么知道我这程序的输出是正确的?起码就现在大家都知道的圆周率小数点后十几位:“山巅一寺一壶酒(3.14159),尔乐苦煞吾(26535),把酒吃(897),酒杀尔(932),杀不死(384),乐尔乐(626)”,这程序要能正确输出吧? 10 | 11 | 为了实现这个目标,小明编写了一段程序,把圆周率的二进制表达变成了十进制的表达,每个字节表达一个十进制数,哦,还有小数点。反正就十一个字符,小明就用0x00 表示十进制 0,0x01 表示 1,0x02 表示 2,依此类推,然后用 0x10 表示小数点。这下,程序输出的圆周率就变成了: 12 | 13 | ``` 14 | 0x03 0x10 0x01 0x04 … 15 | ``` 16 | 17 | 但使用计算机,怎么把这个结果展示出来呢?要知道,那时的计算机可没有显示屏呀。小明很聪明,懂数字电路,他很快又搭了一个可以发光的数码管电路,这个电路根据 0x00 到 0x10 这几个数字,可以使用八段数码管显示十进制的 0 ~ 9 数字还有小数点。当小明的程序运行起来的时候,这个数码管从 3 开始,每隔一秒显示圆周率十进制表示的下个数字[^1]以及可能的小数点。 18 | 19 | 李教授看到小明的工作成果非常开心,李教授大大表扬了小明,称小明的这个发明是划时代的,这让计算机有了一种能力,可以用人类能够理解的方式来表达自己。嗯,的确有点意思。最终,小明凭这个作品拿到了本科学位并开始在李教授的项目组攻读研究生。 20 | 21 | 小明上研究生的头一天,李教授给了他一个题目,说现在有录音机了,可以录制和播放人的声音。你不是读过香农的信息论吗?看看能不能用磁带把你的计算好的圆周率录下来,将来也好给别人看呀。 22 | 23 | 小明很快将这个功能实现了,原理很简单,就是把计算好的圆周率结果录制到磁带上,用二进制方式,每个磁粉的极性表示二进制的 0 或者 1。小明甚至把自己的名字还有李教授的名字所使用的拼音字母(包括空格)也编了码,比如 0xA1 表示 a,0xA2 表示 b,依此类推。一起放到了录制好的磁带上,“THE PI IS CALCULATED BY A PROGRAM WRITTEN BY XIAO MING AND DIRECTED BY PROFESSOR LI.” 24 | 25 | 小明花了一年时间就把这项工作做完了,很快申请论文的答辩。作为研究生科研成果一部分,这磁带被寄给了小明研究生论文的评审教授王教授那里。王教授很欣赏这项工作,认为这项成果不仅仅可以用来记录程序的输出,甚至连同程序自身在内,亦可用磁带录制。不过王教授对录制的内容中没有出现自己的名字而感到不悦。于是,王教授告诉小明,你这项工作成果还不错,但尚不足以申请硕士学位论文,因为没有理论支撑啊!你需要将你的编码方法系统化,而且利于计算机处理。 26 | 27 | 小明很是郁闷,本来可以提前毕业去工作挣钱,那时的英特尔公司刚刚成立正在招人哩。要是丧失了这个机会,就没法成为创始员工了。李教授劝他,计算机行业刚刚开始,以后机会多得是,你好好把自己的成果拔拔高,将来一旦成名,那机会就自己找上门来了。 28 | 29 | 小明开始仔细琢磨王教授的评语,琢磨好几天没有头绪。有一日,小明跑到学校的图书馆,看到有人正在使用打字机打印求职信。小明一下子豁然开朗,我为什么不能将打字机和计算机连起来?打字机作为计算机的输入,把打字机上的每个按键编个号,程序记录这些编号,这样就可以直接用打字机来输入程序,然后执行,再将程序执行结果输出到磁带上。 30 | 31 | 于是,小明快速设计了计算机使用的键盘,这个键盘可以将用户的每个按键进行编码并传输到计算机中进行处理。在之前的基础上,小明扩充了若干控制字符,比如回车啊,换行之类的,而且还区分了英文的大小写字母,添加了常用的标点符号。 32 | 33 | 简单来说,小明的这个编码系统使用七个比特位来表示一个字符,取值范围为 0x00 到 0x7F,可以表示回车、换行、缩进(TAB)等控制字符,也可以表示 26 个拉丁字母(区分大小写),还包括十进制阿拉伯数字以及常用的标点符号。 34 | 35 | 为什么使用七位呢?小明多次实验发现,将键盘通过一条线连接到计算机上时,因为这根线传输距离长,而且没有屏蔽层保护,传输的比特位经常会出现错误的情形。为此,小明使用了我们在第二章中所说的“奇偶校验”方法,将一个字节的最高位作为偶校验码传输过去。计算机收到一个字节后,如果不符合偶校验规则,则认为传输失败,键盘灯就会亮一下,提醒敲键盘的人再次输入。这样,就只能使用字节的低七位来编码了。但同时,小明的硕士论文一下子就高大上了!有工程方法还有理论依据,而且小明还将自己的硕士论文全部使用打字机录入然后保存在了磁带上。虽然王教授的名字最终只是出现在了论文的致谢部分,但王教授也没啥可说的,毕竟只是硕士论文嘛。最终,小明欢欢喜喜拿到了硕士学位,走向社会,大踏步迈向了自己未来的高富帅生活。 36 | 37 | 尽管这个故事是虚构的,不反映任何真实历史。但小明在这里使用的编码方法,就是我们所熟知的 ASCII[^2] 的雏形。每个工程师和科学家都可以自己定义自己的编码方式,但为了交换数据方便,对文字的编码最终都会变成一项国家标准或者国际标准。ASCII 可以说是一切文字编码标准的源头,且深受其影响。 38 | 39 | ## 4.2 字符集及其编码 40 | 41 | 字符集(character set 或 charset),是为了表示某种语言文字而定义的字符集合;编码则是为了在计算机中表示某个字符集中的字符而设定的编码规则,它通常以固定的顺序排列字符,每个字符对应一个特定的字节或者字节序列,并以此作为记录、存储、传递、交换的统一内部特征。一般而言,字符集由某个国家或者国际标准化组织[^3]作为强制或推荐标准颁布。在字符集的定义当中,通常使用码值的概念,就是字符集当中各个字符的编号。比如在我们国家于 1980 年颁布的 GB2312-1980[^4] 字符集标准中,所定义的汉字/符号被分为87个区,每个区包含 94 个汉字/符号。这样,我们可以用特定汉字/符号在 GB2312 字符集中的哪个区、哪个位来指代它,这就是汉字“区位码”的概念。比如,“啊”字是 GB2312 中的第一个汉字,是第 16 区的第一个字符,它的区位码就表达为 1601,或者十六进制的 0x1001。我们将类似区位码的这种字符编号称为字符的“码值”。 42 | 43 | 但是,在计算机中存储或者表示特定字符集中的字符时,我们需要考虑很多其他因素,最重要的就是兼容其它已有及基础字符集的问题,以及易于表达和处理的问题。这样,特定字符集中的字符,会以某个不同于字符码值的方式表示,这就是编码的概念。比如上面的 GB2312 字符集中的字符,大部分情况下使用后面提到的 EUC-CN 编码方法,这种方法用两个字节来表示一个汉字。其中,第一个字节的取值范围为 0xA1-0xF7(区位码中的区号加上0xA0),第二个字节的取值范围为 0xA1-0xFE(区位码中的位号加上 0xA0)。例如,“啊”字的 EUC-CN 编码就是 0xB0A1。需要注意的是,某些字符集本身定义了编码的方式,这种情况下,码值和编码是一样的。 44 | 45 | 计算机领域中首次出现的字符集及其编码标准是 ASCII,即美国信息交换标准代码。该字符集编码最初由美国标准协会[^5]于 1968 年制定,并在之后成为国际标准化组织制定和颁布的 ISO 646 标准。ASCII 一共定义了 128 个字符,其中包含英语使用的 26 个小写拉丁字母和 26 个大写拉丁字母、阿拉伯数字、标点符号以及若干控制字符。在十六进制形式下,ASCII 使用 7 位字节来表示一个字符,取值范围从 0x00 到 0x7F。表 4-1 给出了 ASCII 字符的十六进制编码分布情况。 46 | 47 | 表 4-1 ASCII 字符的十六进制编码分布 48 | 49 | | 取值或范围 | 代表字符 | 50 | |:-----------|:--------------| 51 | | 0x00 | 空(NULL)字符,C 语言表达为 \0。 | 52 | | 0x01~0x1F | 控制字符,其中 0x07~0x0D 为常用控制字符,分别表示蜂鸣(\a)、回退(\b)、水平制表符(\t)、新行(\n)、垂直制表符(\v)、进纸(\f)、回车(\r)等。 | 53 | | 0x20 | 空格。 | 54 | | 0x21~0x2F | 感叹号等标点符号。 | 55 | | 0x30~0x39 | 阿拉伯数字0~9。 | 56 | | 0x3A~0x40 | 冒号等标点符号。 | 57 | | 0x41~0x5A | 大写拉丁字母A~Z。 | 58 | | 0x5B~0x60 | 左右中括号等标点符号。 | 59 | | 0x61~0x7A | 小写拉丁字母 a~z。 | 60 | | 0x7B~0x7E | 左右花括号等标点符号。 | 61 | | 0x7F | 删除符(DEL)。 | 62 | 63 | 64 | 注意表 4-1 中的 ASCII 字符的编码取值范围,我们很容易发现,小写拉丁字母和大写拉丁字母并不是连续排列的。也就是说,这 128 个字符的编码方式存在一些特殊的考量,如: 65 | 66 | - 给定一个大写的拉丁字母,通过对其编码做“或”0x20 的运算,得到的就是该字母的小写字符。比如对字母A,其编码值为 0x41,0x41 | 0x20 后的值为 0x61,得到的就是小写 a 的 ASCII 编码,反之亦然。这种设计,使得早期的机械键盘或者电子键盘可更加容易地实现按键的大小写切换;我们可以按住键盘上的换挡(Shift)键或者打开大写锁定(CapsLck)键来输入大写字母。当然,借助此编码特性,也方便我们在程序中实现快速的大小写字母转换。 67 | - 类似地,我们在 PC 键盘上看到的可通过换挡键切换输入字符的按键,在编码上也只有单个位的区别,比如 ! 和 1,在 PC 键盘上用单个按键输入,其编码分别为 0x21 和 0x31。 68 | - 除了字母、数字和标点符号之外, ASCII 中含包含了大量的控制字符。这些控制字符通常不是可打印字符,其主要用途有:1)用于在屏幕上显示或者打印时控制字符的输出位置(如换行、回车、制表符、送纸等);2)用于交互输入场景(如蜂鸣、回退、删除);3)用于串口通讯。第三类用途在当今已经非常少见了,只有在接近硬件的嵌入式系统开发才会遇到。 69 | 70 | 显然,一个字符集及其编码的定义是并不是随意拼凑、排列的结果,而需要一些技巧。当然,字符集及其编码的定义也会受限于当时的计算机处理能力以及制定者在当时可以预见的未来。 71 | 72 | 在计算机的应用范围扩大到全球各个地区的时候,仅仅使用 ASCII 无法满足非英语国家的需求。首先是经济较为发达的拉丁语系国家,ASCII 并没有包含法语、德语等拉丁语系以及西里尔语、阿拉伯语、希腊语、希伯来语使用的字母。为此,国际组织定义了 ISO 8859 系列字符集,作为 ASCII 字符集的扩展: 73 | 74 | - ISO 8859-1:西方欧洲语言(Latin-1) 75 | - ISO 8859-2:中部和东部欧洲语言(Latin-2) 76 | - ISO 8859-3:东南欧洲语言和其他语言(Latin-3) 77 | - ISO 8859-4:斯堪的纳维亚语/波罗的语(Latin-4) 78 | - ISO 8859-5:拉丁/西里尔语 79 | - ISO 8859-6:拉丁/阿拉伯语 80 | - ISO 8859-7:拉丁/希腊语 81 | - ISO 8859-8:拉丁/希伯来语 82 | - ISO 8859-9:Latin-1针对土耳其语的修订(Latin-5) 83 | - ISO 8859-10:拉普兰/北欧/爱斯基摩人语(Latin-6) 84 | - ISO 8859-11:拉丁/泰语 85 | - ISO 8859-13:波罗的海沿岸语言(Latin-7) 86 | - ISO 8859-14:凯尔特语(Latin-8) 87 | - ISO 8859-15:西部欧洲语言(Latin-9) 88 | - ISO 8859-16:罗马尼亚语(Latin-10) 89 | 90 | ISO 8859 系列标准所定义的字符集及其编码有如下特点: 91 | 92 | - 均为 ASCII 编码的扩展,使用八位单字节定义每个字符,除 ASCII 定义的字符之外,其余字符使用 0xA0\~0xFF 区间范围定义字符。也就是说,ISO 8859 系列字符集向前兼容 ASCII 字符集,且保留 0x80~0x9F 这个区间未被使用。 93 | - ISO 8859 系列字符集之间是互相不兼容的,但标准的制定者也为这些字符集中的公共字符定义了相同的编码值。 94 | 95 | 在 ISO 8859 系列字符集中,最为重要的字符集是 ISO 8859-1 字符集。使用该字符集,可支持大部分拉丁语系语言,包括南非荷兰语、巴斯克语、加泰罗尼亚语、丹麦语、荷兰语、英语、法罗语、芬兰语、法语、加利西亚语、德语、冰岛语、爱尔兰语、意大利语、挪威语、葡萄牙语、苏格兰、西班牙语和瑞典语。下个小节中提到的 Unicode 字符集之前 256 个字符来自于 ISO 8859-1 字符集,且 Unicode 的 UTF-8 编码兼容 ASCII 字符集。 96 | 97 | 对于上面提到的拼音文字(以拉丁语系为代表),基本上使用8位的单字节编码即可表达对应语言的所有文字。但对汉字为代表的象形文字,却无法使用单字节的编码形式来定义所有的文字,毕竟《康熙字典》收录的汉字就有大概四万多个。为了在计算机中方便处理以汉字为代表的语言文字,中国大陆、中国台湾、日本等国家在 ASCII 的基础上各自制定了自己的字符集及其编码标准。 98 | 99 | 上面提到的 GB2312 标准就是中国定义的简体中文字符集标准,其中含有 682 个符号、6,763 个汉字;它共分 87 个区,每个区含 94 个字符。类似的还有日本的JISX0201、JISX0208 字符集,以及中国台湾、香港等地区广泛使用的 BIG5 繁体中文字符集等。 100 | 101 | 一个字符集可以有不同的编码形式。拿 GB2312 字符集来讲,通常我们使用的是上面所讲的 EUC-CN 编码。还有一种常见的 GB2312 编码形式是 HZ 编码,它去掉了 EUC 编码的最高位,使得汉字可以用 ASCII 码中的字符来表示,比如 EUC 编码中的汉字“啊”编码为“0xB1A1”,而 HZ 编码则为“~{1!~}”。这种编码方式主要应用于早期互联网电子邮件系统,那时的电子邮件系统不能正确处理 ASCII 字符集之外的字符,所以人们使用 HZ 编码来传输汉字。当然,现在这种编码方式基本上不再使用了。 102 | 103 | 在上个世纪 90 年代,随着个人计算机在国内的普及,GB2312 字符集定义的六千多个汉字远远不能满足互联网、出版界以及教育界的需求。最为尴尬的就是朱镕基总理的“镕”字没有被收录到 GB2312 字符集当中,导致在互联网刚刚进入中国时(1996年左右)的中文网页上,无法正确显示“镕”字。这种标准缺位的问题存在了很长一段时间。为了解决现实问题,微软在其 Windows 95 操作系统中自行扩展了 GB2312 字符集,纳入了 BIG5 字符集定义的繁体汉字以及其他一些常用汉字,称为 GBK。需要注意的是,该字符集并不是中国国家标准。另一方面,从上个世纪 90 年代开始,美国的主要 IT 巨头公司开始联合制定 Unicode 字符集,我国政府则自行制定了 GB18030 字符集,并要求所有在国内销售的操作系统必须支持 GB18030 字符集。从字符集定义的字符范围来看,GB18030 和 Unicode 类似,可用来定义海量的字符,且 GB18030 的编码方式向前兼容 GB2312 和 GBK。但时至今日,Unicode 及其 UTF-8 编码最终成为事实上的工业标准。这一字符集标准的争夺战及其发展历史,值得回味和反思。 104 | 105 | 随着各个国家、地区字符集标准的出台和升级,兼容性问题的引入是不可避免的。比如,一个采用 GB2312 EUC-CN 编码的文本文件无法在采用 BIG5 编码的系统上正常显示,若要正常显示和处理,则需要额外进行字符集及编码的转换。为此,一些国际组织开始致力于全球统一字符集标准的开发,也就是我们熟知的 Unicode/ISO 10646。 106 | 107 | ## 4.3 Unicode 字符集及 UTF-8 编码 108 | 109 | 国际标准组织早在 1984 年 4 月就成立了 ISO/IEC JTC1/SC2/WG2 工作组,开始针对各国文字、符号定义统一的字符集及编码标准,即 ISO 10646 统计字符集(UCS[^6])标准。于此同时,位于美国加州的主要 IT 巨头公司如微软、苹果、IBM、惠普等则成立 Unicode 组织来定义自己的 Unicode 字符集。 110 | 111 | 1991 年 Unicode 组织变身为 Unicode 财团,并于当年 10 月与 ISO/IEC JTC1/SC2/WG2 达成协议,采用同一编码字符集,并密切协调各自标准的进一步扩展,同时公布了 Unicode 1.0。1997 年 9 月发布的 Unicode 2.0 是一个更为成熟的 Unicode 版本,并在各操作系统中广泛使用。自 Unicode 2.0 开始保持了向后兼容,即新的版本仅仅增加字符,原有字符不会被删除或更名。Unicode至今仍在不断增修,每个新版本都会加入更多新的字符。目前最新的版本为 2014 年 6 月 16 日公布的 7.0.0,已收入超过十万个字符(第十万个字符在 2005 年获采纳)。Unicode 8.0.0 将于 2015 年 6 月发布(已发布草案)。由于ISO 10646 统一字符集标准的扩展速度要比 Unicode 快,因此,人们通常认为前者是后者的超集。 112 | 113 | 当前主流的操作系统(如 Linux、Windows 等)所支持的 Unicode 版本为 Unicode 3.0。Unicode 3.0 版本包含字母及符号 10,236 个,CJK(中日韩)汉字 27,786 个,韩文拼音 11,172 个,总的已分配字符为 49,194 个;另有保留私用码位 6,400 个,替代码位 2,048 个,控制字符 64 个、非字符 2 个;总分配码位 57,709 个,未分配码位 7,827,共计 65,536 个码位。 114 | 115 | Unicode 3.0 总共定义了 65,536 个码位(除去0xFFFE 和 0xFFFF 两个非字符的话则为 65,534 个码位),可使用 16 位字来表示一个字符,基本满足全球各个国家和地区的日常文字处理需要。但由于地球文明的多样性以及不断发现和涌现的文字及表意符号,最新的 Unicode 7.0 版本已经定义了超过十万个码位。而由国际标准组织定义的 ISO 10646 统一字符集标准之最新版本,甚至需要 31 位的码位空间才能表达所有的字符。 116 | 117 | 在计算机程序中处理 Unicode 字符时,有多种编码方式。比如对 Unicode 3.0,我们可以使用 16 位字来表示单个字符,对应于 ISO 10646 标准的 UCS-2/UTF-16 编码方式,而针对 Unicode 7.0 等更大的字符集,则需要使用 32 位双倍字来表示单个字符,对应于 ISO 10646 标准的 UCS-4/UTF-32 编码方式。 118 | 119 | 但是 UCS-2 和 UCS-4 这两种编码方式存在一些问题: 120 | 121 | - 在不同的计算机系统上,UCS-2 和 UCS-4 编码方式存在字节序问题。 122 | - UCS-2 编码的数据在 UCS-4 编码的系统上进行处理时,需要重新转换编码。 123 | - 大量使用 ASCII 字符集编码的计算机程序等已有的数据使用单字节编码,若使用统一的 UCS-2 或者 UCS-4 编码方式,将带来极大的内存浪费。 124 | 125 | 为了解决这些问题,Unix、C 语言作者之一肯·汤普逊于 1992 年提出了 UTF-8 编码方式,并成为当前几乎所有互联网内容的主要编码方式。UTF-8 是一种针对 Unicode 的可变长度字节编码,属于一种前缀码。它可以表示 Unicode 标准中的任何字符,且其编码和 ASCII 兼容。其主要特性有: 126 | 127 | - UCS码位在 0x00000000 到 0x0000007F 之间的字符(即 ASCII 定义的字符)被简单编码为单个字节的 0x00 到 0x7F。这样,UTF-8 兼容 ASCII,也就是说,仅包含 7 位 ASCII 字符的文件和字符串在 ASCII 和 UTF-8 编码下的形式是一样的。 128 | - 所有码值大于 0x7F 的 UCS 字符,将被编码为多字节序列,而且其取值范围为 0x80 到 0xFD。这样,ASCII 字符将不会出现在其他字符的编码范围中,而且不会影响空字符等控制字符的行为。 129 | - UCS-4 字符串的词典排列顺序被保留。 130 | - UCS 定义的所有 31 位码位均可被 UTF-8 编码。 131 | - UTF-8 编码不使用 0xC0、0xC1、0xFE 和 0xFF 等字节。 132 | - 单个非 ASCII 字符的 UTF-8 编码的多字节序列中,第一个字节的取值范围始终为 0xC2 到 0xFD,使用高位值为1的位数可给出该多字节序列的长度。其后所有字节的取值范围为 0x80 到 0xBF,这种编码方式在丢失字节的情况下,可保持解码的同步并使得解码过程健壮而无状态。 133 | - 理论上,使用 UTF-8 编码 UCS 字符,其最大长度可达 6 个字节。但由于 Unicode 3.0 标准并未定义码值在 0x10FFFF 之上的字符,因此,Unicode 3.0 字符使用 UTF-8 编码时,其最大长度为 4 个字节。 134 | 135 | 表 4-2 给出了 UCS 码值对应的 UTF-8 编码(二进制形式)。 136 | 137 | 表 4-2 UTF-8 编码 138 | 139 | | UCS 码值范围 | UTF-8 编码(二进制) | 140 | |:-------------------------|:----------------------------| 141 | | 0x00000000 - 0x0000007F | 0xxxxxxx | 142 | | 0x00000080 - 0x000007FF | 110xxxxx 10xxxxxx | 143 | | 0x00000800 - 0x0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx | 144 | | 0x00010000 - 0x001FFFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 145 | | 0x00200000 - 0x03FFFFFF | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx | 146 | | 0x04000000 - 0x7FFFFFFF | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx | 147 | 148 | 在上表中,UTF-8 二进制编码中的 xxx 位取自对应码值的二进制表述。比如版权符号(©)的 Unicode 码值为 0xa9,其二进制表述为 1010 1001,则其 UTF-8 编码为:11000010 10101001,亦即 0xc2 0xa9。 149 | 150 | 需要注意的是,UCS 码值在 0xD800 到 0xDFFFF 范围(即替代码位)以及 0xFFFE 和 0xFFFF 这两个非字符,不会出现在 UTF-8 编码字节流中。 151 | 152 | 下面的 C 代码将给定的 UCS 码值转换成 UTF-8 编码: 153 | 154 | ```c 155 | static int utf8_conv_from_uc32 (UChar32 wc, unsigned char* mchar) 156 | { 157 | int first, len; 158 | 159 | if (wc < 0x80) { 160 | first = 0; 161 | len = 1; 162 | } 163 | else if (wc < 0x800) { 164 | first = 0xC0; 165 | len = 2; 166 | } 167 | else if (wc < 0x10000) { 168 | first = 0xE0; 169 | len = 3; 170 | } 171 | else if (wc < 0x200000) { 172 | first = 0xF0; 173 | len = 4; 174 | } 175 | else if (wc < 0x400000) { 176 | first = 0xF8; 177 | len = 5; 178 | } 179 | else { 180 | first = 0xFC; 181 | len = 6; 182 | } 183 | 184 | switch (len) { 185 | case 6: 186 | mchar [5] = (wc & 0x3f) | 0x80; wc >>= 6; /* Fall through */ 187 | case 5: 188 | mchar [4] = (wc & 0x3f) | 0x80; wc >>= 6; /* Fall through */ 189 | case 4: 190 | mchar [3] = (wc & 0x3f) | 0x80; wc >>= 6; /* Fall through */ 191 | case 3: 192 | mchar [2] = (wc & 0x3f) | 0x80; wc >>= 6; /* Fall through */ 193 | case 2: 194 | mchar [1] = (wc & 0x3f) | 0x80; wc >>= 6; /* Fall through */ 195 | case 1: 196 | mchar [0] = wc | first; 197 | } 198 | 199 | return len; 200 | } 201 | ``` 202 | 203 | 另外,除了 UTF-8 编码之外,UCS 相关的编码还有 UTF-7、UTF-16、UTF-16LE、UTF-16BE、UTF-32、UTF-32LE、UTF-32BE 等。其中的 LE、BE 后缀分别表示小头字节序或者大头字节序。 204 | 205 | ## 4.4 历史上曾使用过的其他文字编码体系 206 | 207 | 在计算机技术发展,尤其是操作系统的发展过程中,在标准缺失的情形下,操作系统厂商或者计算机系统厂商经常为了满足特定地区的市场需求而自行制定一些字符集或者编码体系。如上面提到的 GBK 就是微软公司为中国大陆设计的兼容 GB2312 的字符集,后来成为事实上的标准,并影响了 GB18030 字符集的制定。尽管当前几乎所有的最新软件都开始使用 Unicode 尤其是 UTF-8 编码来处理文字,但仍然有大量资料使用老的字符集标准存储和交换。本小节将简单介绍一下历史上曾使用过的主要字符集及文字编码体系。 208 | 209 | ### 4.4.1 EUC 210 | 211 | EUC 全称为 Extended Unix Code,即扩展 Unix 编码。顾名思义,EUC 和 Unix 操作系统相关。1991 年,Unix 操作系统相关公司制定了 EUC 标准,主要用于存储汉语文字、日语文字以及韩语文字。其中, 212 | 213 | - EUC-CN 是 GB2312 字符集的常用编码方法,亦即GB2312 的默认编码方法。这种编码方法兼容 ASCII。ASCII 字符使用 0x00~0x7F 单字节编码,GB2312 定义的其他字符使用两个字节来编码,第一个字节取值范围为:0xA1-0xF7,第二个字节的取值范围为:0xA1-0xFE。 214 | - UC-JP 是日本 JIS X 0208 和 JIS X 0212 两个字符集的扩展 Unix 编码方法。和 EUC-CN 类似,ASCII 字符使用 0x00~0x7F 单字节编码,而使用两个字节或者三个字节来编码半角片假名(两个字节)、JIS X 0208 定义的字符(两个字节)以及 JIS X 0212 定义的字符(三个字节)。但在 Windows 和 IBM 的日语操作系统中,普遍使用 Shift_JIS 编码方法。 215 | - EUC-KR 是韩国KS X 1001字符集的扩展 Unix 编码方法。和上面类似,ASCII 字符使用 0x00~0x7F 单字节编码,KS X 1001定义的其他字符使用两个字节来编码,两个字节的取值范围均为 0xA1-0xFE。 216 | - EUC-TW 是针对中国台湾CNS 11643 字符集标准制定的扩展 Unix 编码方法。除 ASCII 字符之外,其他字符使用两个字节或者四个字节的来表示。不过,台湾、香港、澳门、新加坡、马来西亚等繁体中文通行的国家和地区广泛使用的是 BIG5(大五码)及其编码方法,几乎不使用 EUC-TW 编码。 217 | 218 | ### 4.4.2 代码页 219 | 220 | 代码页(Code Page)是特定字符集编码的别称,这个名称在微软、苹果和 IBM 的操作系统软件或者计算机系统中使用,如 DOS、Windows、Mac电脑、IBM 大型机等。比如 Windows 操作系统中的 CP936 代码页,对应的就是 GB2312 的 EUC-CN编码表。 221 | 222 | 在Windows 95 等现代操作系统中,内部使用 Unicode 字符集的 UCS-2 编码进行字符的处理。但在 Unicode 尚未被完全普及的情形下,需要一个映射表将已有的 GB2312 等特定字符集编码情形下的文件或者字符串转为 Unicode 进行处理。通过切换不同的代码页,操作系统还可以支持不同的字符集及其编码处理。而在操作系统内部,这种映射表就称为代码页。 223 | 224 | ## 4.5 文字编码的转换 225 | 226 | 从前面的描述中可以看出,计算机系统为表述人类文字走过了一条复杂的道路。尽管当前 Unicode 的 UTF-8 编码已成为互联网上传输文字内容的事实标准,但因为历史原因,我们有大量的文字使用不同的字符集或者编码来保存。在国内,一些老的网站可能仍然使用 GB2312、GBK 等字符集及其编码来保存网页中的文字内容,这种情况下,网页浏览器就需要将这些不同字符集和编码的文字转换为 Unicode 然后再做进一步的处理。 227 | 228 | 在当今流行的 Linux 操作系统中,我们可以使用 iconv 命令完成文字的字符集及其编码转换。比如将 EUC-JS 编码的文本转换为 Shift_JIS 编码的文本,或者转换为 UTF-8 编码的文本。iconv 是一个开源软件,开发者可在 C 程序中调用其接口完成转换,亦可在 PHP、Python 等各种脚本语言中使用经过封装的接口或者模块。 229 | 230 | 显然,我们可以将前述 ISO-8859 系列、EUC 编码以及各种代码页下的文本转换为 Unicode 字符集的某种编码形式。但如果反过来,则可能无法正确转换,比如将某个汉字转换为 ASCII 码是行不通的。 231 | 232 | 在 Unicode 的 UTF-8 和 UTF-16 等编码之间进行转换是相对直接的,下面的 C 语言代码段完成了一个 UTF-8 编码到 UCS4 编码之间的转换: 233 | 234 | ```c 235 | static Glyph32 utf8_char_glyph_value (const unsigned char* pre_mchar, 236 | int pre_len, const unsigned char* cur_mchar, int cur_len) 237 | { 238 | UChar32 wc = *((unsigned char *)(cur_mchar++)); 239 | int n, t; 240 | 241 | if (wc & 0x80) { 242 | n = 1; 243 | while (wc & (0x80 >> n)) 244 | n++; 245 | 246 | wc &= (1 << (8-n)) - 1; 247 | while (--n > 0) { 248 | t = *((unsigned char *)(cur_mchar++)); 249 | 250 | wc = (wc << 6) | (t & 0x3F); 251 | } 252 | } 253 | 254 | return wc; 255 | } 256 | ``` 257 | 258 | 对其他字符集编码到 Unicode 之间的转换,由于字符集制定者的设计思路的不同,导致文字的编码转换无法通过一个简单的公式来处理。比如,GB2312 中的汉字字符在设计上遵循按普通话拼音为顺序的原则排列,但在 Unicode 中,汉字字符按笔画数从小到大排列。这样,当我们要将一个 EUC-CN 编码的文字转换成 Unicode 编码时,需要使用映射表。 259 | 260 | 下面的代码段使用 __mg_gbunicode_map 映射表中保存了六千多个 GB2312 字符对应的 Unicode UCS 码值。gb2312_0_char_glyph_value 函数则根据 EUC-CN 编码计算特定 GB2312 字符在 __mg_gbunicode_map 映射表中的索引值。 261 | 262 | 263 | ```c 264 | static Glyph32 gb2312_0_char_glyph_value (const unsigned char* pre_mchar, 265 | int pre_len, const unsigned char* cur_mchar, int cur_len) 266 | { 267 | int area = cur_mchar [0] - 0xA1; 268 | 269 | if (area < 9) { 270 | return (area * 94 + cur_mchar [1] - 0xA1); 271 | } 272 | else if (area >= 15) 273 | return ((area - 6)* 94 + cur_mchar [1] - 0xA1); 274 | 275 | return 0; 276 | } 277 | 278 | const unsigned short __mg_gbunicode_map[] = { 279 | 0x3000, 0x3001, 0x3002, 0x30fb, 280 | 0x02c9, 0x02c7, 0x00a8, 0x3003, 281 | 0x3005, 0x2015, 0xff5e, 0x2225, 282 | 0x2026, 0x2018, 0x2019, 0x201c, 283 | 0x201d, 0x3014, 0x3015, 0x3008, 284 | ... 285 | 0x9eea, 0x9eef, 0x9f22, 0x9f2c, 286 | 0x9f2f, 0x9f39, 0x9f37, 0x9f3d, 287 | 0x9f3e, 0x9f44 288 | }; 289 | 290 | static UChar32 gb2312_0_conv_to_uc32 (Glyph32 glyph_value) 291 | { 292 | return (UChar32)__mg_gbunicode_map [glyph_value]; 293 | } 294 | ``` 295 | 296 | 297 | [^1]: 其实这个方法现在还在用,比如你乘坐电梯时看到的楼层数字。 298 | 299 | [^2]: ASCII:American Standard Code for Information Interchange,美国信息交换标准代码。 300 | 301 | [^3]: 国际标准化组织:International Standard Orgnization,ISO。 302 | 303 | [^4]: 经常被简称为 GB2312 或者 GB2312-80。 304 | 305 | [^5]: 美国标准协会:United States of America Standards Institute,USASI。 306 | 307 | [^6]: UCS:Universal Character Set。 308 | 309 | -------------------------------------------------------------------------------- /textbook/part-1-chapter-5.md: -------------------------------------------------------------------------------- 1 | # 第 5 章 多媒体:图像、图形及音视频 2 | 3 | 早期的计算机主要通过文字和用户交互。比如,用户使用键盘输入命令或者编写程序,计算机输出文字或符号到屏幕上展示结果。自图形用户界面(GUI)被发明以来,人们可以通过屏幕看到图像、图形,甚至可以欣赏音乐,观看视频,玩游戏等等,极大拓宽了计算机的用途。在计算机技术中,除文字之外的这类信息类型被称为“多媒体(Multi-media)”。本章讲述计算机如何表述多媒体信息。 4 | 5 | ## 5.1 计算机输出设备 6 | 7 | 作为一种电子计算和通信工具,现代的个人计算机(包括我们现在日常使用的智能手机)和用户产生交互时,主要通过人类的视觉和听觉进行,因此其输出设备可划分为视觉输出设备和听觉输出设备两类。除此之外,智能手机也可以产生振动,尽管这种振动在智能手机中仅起到辅助作用,但仍然拓宽了计算机和用户之间的交互途径。在苹果公司 2015 年发布的智能手表产品中,根据不同的场景,智能手表会产生不同方式的振动,从而使得这种交互方式变成了除视觉、听觉之外的另外一种主要交互方式。相信在不远的将来,利用人类味觉、嗅觉等的新式交互方式会得到广泛应用,比如在智能机器人、虚拟现实等新型的计算机产品形态中。 8 | 9 | 本节将简要阐述计算机视觉、听觉输出设备的硬件工作原理。 10 | 11 | ### 5.1.1 计算机视觉输出设备 12 | 13 | 从计算机程序的角度看,计算机的输出设备有两类。一类称为字符输出设备,一类称为图形输出设备。 14 | 字符输出设备出现在计算机的早期发展过程中。那时,人们通过一个个带有键盘和 CRT 屏幕(阴极射线管)的终端通过串口连接到单个大型机(mainframe)上输入程序,执行程序并观察程序的执行结果。人们在键盘上输出的字符会立即显示在屏幕上,而程序的输出则会连续不断地显示在屏幕上;当屏幕上的字符显示满屏之后会自动滚屏显示新的字符。 15 | 16 | 字符输出设备为程序提供了非常简单的接口,在 C 语言中,我们调用 printf 函数,即可向屏幕上输出字符;调用 gets 函数,可等待并获得用户的输出。 17 | 18 | 与字符输出设备不同的是图形输出设备。图形输出设备提供给程序在计算机屏幕上使用不同的颜色绘制任意形状的能力。此时,程序通过操作我们俗称为“显存(Video RAM)”的内存区域来控制屏幕上的每个像素点(Pixel),从而显示文字、绘制图形。 19 | 20 | 尽管我们现在使用的个人电脑之显示屏均为图形显示设备,但字符输出设备仍然以直接或者间接的方式存在于我们的电脑上。比如,当我们打开个人电脑时,个人电脑的 BIOS 系统首先会将电脑的显示器以字符设备方式工作,显示一些有关 BIOS 版本、内存大小等信息,然后是 Windows 系统,此时,显示屏会以图形输出设备的方式工作,并显示图形用户界面。在 Windows 系统中,如果我们运行 cmd 命令,将打开字符终端模拟程序,此时,我们可以键入 DOS 命令,而其中的命令输出则会显示在字符终端模拟程序的窗口中。再比如,我们在 Linux 或者 Windows 系统中使用 telnet、SecureCRT 等登录到远程主机上时,运行其中的程序也以字符方式展示输出,而 telnet、SecureCRT 程序为这些运行在远程主机上的程序提供了一个模拟的字符输出设备。 21 | 22 | 本质上,字符输出设备是任何计算机系统的最基础设施,在完整的计算机系统中,字符输出设备和当前我们常见的图形输出设备共存。显然,字符输出设备主要处理的是文本的显示,而对我们本章所讲的多媒体信息中的图像、图形相关信息,则必须使用图形输出设备。 23 | 24 | 和计算机图形输出设备关联的计算机系统组件主要是显示卡(显示芯片)和显示器。以 PC 为例,显示卡(display adapter)是一块插在PC主机的电路板(或者集成在 PC 主板上)。一般显示卡由寄存器、存储器(Video RAM和ROM BIOS)、控制电路三大部分组成。大部分的图形显示卡都以兼容标准的VGA模式为基础。而在许多便携式智能设备上,用户面对的显示硬件通常是屏幕较小的 LCD 和 LCD 控制器(LCD controller)。 25 | 26 | 不管是 PC 的显示卡还是嵌入式设备的 LCD 控制器,都将有一个显示RAM(video RAM,VRAM)区域,代表了要在屏幕上显示的图像。VRAM必须足够大,以处理显示屏幕上所有的像素。程序通过直接或间接地存取 VRAM 中的数据来进行图形操作,改变屏幕上的显示。许多显示硬件提供从 CPU 的地址和数据总线直接访问 VRAM 的能力,这相当于把 VRAM 映射到了CPU的地址空间,从而允许更快的 VRAM 访问速度。VRAM 组成的内存段,就叫帧缓冲区(Frame Buffer)。 27 | 28 | 通常,CRT 显示器和 LCD 显示屏都是栅格设备,屏幕上的每一点是一个像素,整个显示屏幕就是一个像素矩阵。帧缓冲区中的数据按照显示器的显示模式进行存储,记录了显示屏幕上每一个像素点的颜色值,即像素值。 29 | 30 | 我们知道,计算机以二进制方式存储数据,每位有两种状态(0 与 1)。对于单色显示模式,屏幕上一个像素点的颜色值只需用帧缓冲区中的一位表示,该位为 1 则表示该点是亮点。而在彩色显示模式下,要表示屏幕上像素点的颜色信息,需要更多的位或字节。比如,对于 16 色显示模式,就需 4 位来存储一个颜色值。在 256 色显示模式下,一个像素点占 8 位即一个字节。在 16 位色彩色显示模式下,则需要两个字节来存储一个像素点的颜色值。随着计算机硬件处理能力的提升以及 32 位系统的普及,现在不管是 PC 还是智能设备,基本上均采用 32 位色或 16 位色显示模式,其他的显示模式已经非常少见了。显然,显示一个像素点使用的位数越多,可显示的颜色越多,显示出来的色彩就越丰富,越可能接近真实世界[^1]。 31 | 32 | ### 5.1.2 计算机听觉输出设备 33 | 34 | 计算机听觉输出设备相对来讲比较简单,和收音机、录音机等使用的扬声器(俗称喇叭)或者耳机并无二致。音频(audio)信号通过电磁、压电或者静电效应,使得扬声器的纸盆或耳机膜片振动挤压空气,从而发出声音。 35 | 36 | 当然,计算机中存储的音频信号都是数字化的,所以,和传统的收音机不同,计算机需要将数字化的音频信号通过 DA(数字到模拟)转换器转换成模拟的音频信号,然后再传输给扬声器发出声音。反过来,如果要让计算机录制声音,则需要经过 AD(模拟转数字)转换器将模拟的音频信号(由麦克风拾取)做数字化处理并存储为二进制数据。 37 | 38 | 随着科技的发展,新近又出现了一种骨传导耳机。骨传导耳机直接通过人类头骨、颌骨将声音传导到听觉神经,而不经过耳朵和鼓膜。因此,骨传导耳机不再需要纸盆或者膜片。骨传导耳机可用于听觉有障碍的人群,如老人或耳聋患者。另外对正常人来讲,使用骨传导耳机时,同时也可以听到通过耳朵传入的声音,因此,可以提高佩戴耳机行进过程中的安全性。还有一个好处就是,骨传导耳机不会影响身边的其他人。相信骨传导耳机在未来的虚拟现实、增强现实产品中会有较为广泛的应用,也会逐步替代现有的耳机产品。 39 | 40 | ## 5.2 图像 41 | 42 | ### 5.2.1 位图及相关概念 43 | 44 | 和图形输出设备的显存对应,在计算机程序中,我们使用位图(Bitmap;在某些系统中也称为像素图,Pixelmap)的概念来表示一副静态的图像。 45 | 46 | 我们可以将位图理解成一个二维数组,其中记录了图像中每一个点的颜色信息,这些点称为像素(Pixel)。位图的高度和宽度以像素个数为单位表示。在电子计算机中,图形显示设备(CRT 或 LCD)普遍使用红绿蓝(RBG)三原色来表示一个像素的颜色。RGB 三种颜色分量的取值范围均为 0x00~0xFF,这样,理论上可表达的颜色种类有 224 之多,大大超出人眼能够分辨的颜色数量。假如我们用三个字节来表示一个像素,则高度和宽度分别为 256 的位图,需要 256\*256\*3 = 192KB 字节。 47 | 48 | 除了位图的宽度、高度等信息外,还有一些和位图相关的概念,综述如下: 49 | 50 | - 每像素位数(bits per pixel),也称颜色深度(color depth),简称色深。色深表示位图中每个像素所占的位数。通常来讲,位图的色深可取 1、2、4、8、16、24 等,分别对应于单色、4 色、16 色、256 色、64K 色和 16M 色。同一位图中的各像素位数都相同,这样,使用不同的颜色深度,可表示的颜色数不同,存储每个像素所需要的空间也就不同。 51 | - 每像素字节数(bytes per pixel),通常表示为 bpp。bpp 表示每个像素所占的字节数。当然,这个概念仅仅出现在颜色深度大于等于 8 的情况下。当颜色深度为8时,bpp 为 1;颜色深度为 16 时,bpp 为 2;颜色深度为 24 时,bpp 为 3。需要注意的是,色深为 24 时,在 32 位系统上,为提高处理速度,每个像素会以四个字节为单位进行存储,这样,一次读取一个 32 位的整数即可获得这个像素的像素值。 52 | - 位图每行所占的字节数(bytes per line,也称为 pitch)。pitch 表示位图每行像素所占的字节数。通常来讲,位图的 pitch 一般被圆整为四的倍数。这样,在 32 位系统中装载位图数据之后,可确保位图的每行像素均对齐于四字节边界,便于应用程序优化处理位图像素值。 53 | - 位图的透明像素。某些图像格式(如 GIF),可定义透明色,当程序显示定义有透明色的位图时,要跳过透明像素,而和透明像素不同的像素应该覆盖已有的像素,这样就实现了透明的图片。 54 | - 像素的Alpha值。某些图像格式(如 PNG),可为每个像素点定义 Alpha 值。Alpha 值将参与位图像素值和要覆盖的像素值(已有像素值)之间的运算,使得最终的像素值是位图像素值和已有像素值以某个百分比混合后的像素值。通常,Alpha 值取 0 到 255 的整数值。当 Alpha 为 128 时,最终的像素值将是位图像素值和最终像素值各取一半之后的像素值,这样就实现了半透明效果。随 Alpha 值的不同,混合的效果不同;Alpha 为零时,将忽略位图像素值,而 Alpha 为 255 时,结果像素值就是位图像素值。如果位图中使用逐点 Alpha 值(即每个像素点都具有不同的 Alpha 值),则一半使用 32 位来表示一个像素,R、G、B 各占8位,Alpha 值占 8 位。 55 | 56 | 在早期的个人电脑系统中,用于存储像素的内存相对比较昂贵,且受内存寻址空间大小的影响,使用每个像素均以 RGB 三原色分量来表示的方法会消耗很大的内存。比如上面提到的 129KB 字节的位图,对只有 640KB 可用内存的 DOS 系统来说,显然过大。于是,人们使用其他变通的方法来表达位图,其中最有效的方法使用调色板技术。 57 | 58 | 使用调色板技术时,每个像素的颜色值不再包含直接的 RGB 分量信息,而是一个编号,使用该编号查找一个线性数组,即可获得这个像素对应的真实 RGB 颜色分量,而这个线性数组就是调色板。如图 5.1 所示。 59 | 60 |
61 | ![位图及其调色板](illustration/img-5-1.png)
62 | 图 5-1 位图及其调色板 63 |
64 |
65 | 66 | 通常,调色板由一个足够大的 RGB 数组表示,该数组每个成员含有三个字节,分别表示R、G、B 三种颜色分量。显示位图时,并不能直接使用像素值来表示 RGB 三种颜色,而是首先要以像素值为索引在调色板中查找对应的 RGB 三种分量的值,最终以这个值作为实际的颜色显示出来。当颜色深度为 1 时,调色板通常只有两个入口,分别表示像素值为0和1时的 RGB 颜色分量;而颜色深度为 2 时,调色板通常有四个入口,分别表示像素值为 0、1、2、3 时的 RGB 分量。以此类推,颜色深度为 8 时,调色板通常有 256 个入口。 67 | 68 | 当位图的颜色深度取 16、24 时,就不再采用调色板,而用直接使用像素值来表示颜色分量。比如在色深为 24 位的位图中,每个像素值存储的是直接的 RGB 分量数据,每个分量占 8 位;而在 16 位位图中,RGB 分量分别占5位、6 位和 5 位。如图 5.2 所示。 69 | 70 |
71 | ![十六位色像素和 RGB 分量](illustration/img-5-2.png)
72 | 图 5-2 十六位色像素和 RGB 分量 73 |
74 |
75 | 76 | 在计算机中,将一个静态图像显示显示到屏幕上的过程,大致经历如下几个步骤: 77 | 78 | 1) 从图像文件中获得有关图像大小、调色板等信息。 79 | 2) 按照图像文件的存储格式,解码图片,此时在计算机内存中用来表示图像的位图和当前计算机系统使用的具体显示模式无关,因此被称为设备无关的位图(Device-Indepdent Bitmap)。 80 | 3) 将这个位图中的每个像素转换为适配计算机系统当前显示模式像素值,形成设备相关位图(Device-Depedent Bitmap)。 81 | 4) 将设备相关位图逐点、逐行或者整个复制到图形显示设备帧缓冲区的指定内存位置或区域。 82 | 83 | 为了更好地理解设备相关位图和设备无关位图的概念,假设我们有一个使用调色板表示的 256 色位图,要在 16 位色的屏幕上显示。装载调色板以及对应的像素值到内存中时,由调色板和表示图像各像素点的数组表示的位图就是设备无关的。而要显示到屏幕上,我们需要将这个位图中的每个像素点转换为 16 位色形式,即根据设备无关的位图像素值查找调色板,然后将对应的调色板颜色 RGB 分量组装成图 5-2 所示的 16 位色像素值,然后复制到显存中。执行这种像素转换之后的位图就称为设备相关位图。 84 | 85 | ### 5.2.2 色彩空间 86 | 87 | 本章目前为止提到的是 RGB 色彩空间(color space),即通过红、绿、蓝三原色的不同取值来确定一个颜色。除了 RGB 色彩空间之外,业界还使用 YUV、YIQ、CMYK 等其他色彩空间来定义颜色。其中 YUV 色彩空间主要用于电视视频信号的传输,可兼容老式的黑白电视视频信号。YUV 色彩空间中的 Y 指亮度(luminance),即灰阶值(黑白电视使用灰阶值定义),而 U 和 V 表示色调和饱和度,用于定义像素的颜色,即色度(chrominance)。“亮度”通过 RGB 输入信号建立,方法是将 RGB 信号的特定部分叠加到一起。“色度”则定义了颜色的两个方面:色调与饱和度,分别用和 U(Cb)和 V(Cr)来表示。其中,U(Cb)反映的是 RGB 输入信号蓝色部分与 RGB 信号亮度值之间的差异;而V(Cr)反映了 RGB 输入信号红色部分与 RGB 信号亮度值之间的差异。RGB 到 YUV 色彩空间一般具有如下的转换公式: 88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 |
96 | 97 | 类似地,在美式电视信号标准 NTSC 中,还使用 YIQ 色彩空间,对应的颜色空间转换公式为: 98 | 99 |
100 |
101 |
102 |
103 |
104 |
105 | 106 | 另外,CMYK 色彩空间主要用于印刷业,通过青、洋红、黄和黑四种颜色的不同取值来定义一种颜色。RGB 色彩空间适合于背景为黑色的显示设施,而 CMYK 适合于背景为白色的显示设施。需要注意的是,色彩空间之间可以转换,但严格意义上讲,不同的色彩空间并不重叠,也就是说,不同的色彩空间中的颜色并不是一一对应的,因此色彩在不同的色彩空间转换时,会带来一些失真。 107 | 108 | ### 5.2.3 流行的图像格式 109 | 110 | 上个小节提到的位图,本质上指计算机在显示一个图像(Image)时,在内存中表示这个图像的方法。我们知道,我们让计算机显示一副图片时,对应的操作是打开这个图片文件。比如我们使用数码相机或者智能手机拍摄的照片文件,通常使用 JPG 后缀名,而常见的图标文件,则具有 PNG、GIF 等后缀名。这些文件,就是我们本节要讲述的图像文件格式。 111 | 112 | 图像文件本质上定义的是一个设备无关的位图,包括其高度、宽度以及像素值等信息。为了有效降低存储图像所使用的存储空间,不同的图像文件格式会定义自己特有的编码方式。这些编码方式本质上属于压缩算法。在一副图像中,我们经常会发现有很多相邻的像素具有同样的像素值,比如白色背景上显示一个黑点的图像。针对这种情形,我们就可以使用特定的压缩算法来降低最终图片文件的存储大小,从而节省存储空间和传输带宽。 113 | 114 | 图像的压缩算法又可以分为无损压缩和有损压缩两类。前者表示解压后的位图和原色位图一样,不损失任何像素值信息;后者表示解压后的位图和原始位图不同,会损失原始位图中的像素信息。用于照片的 JPEG 图片格式,使用有损压缩算法,在某些情况下,同一张照片使用 JPEG 格式存储,选择不同的压缩比,其大小可能有十倍之差,粗略观察,人眼可能无法有效分辨其内容的差别。有损压缩虽然会损失数据的精度,但可提高压缩比,非常适合处理图像、音频、视频等多媒体数据。 115 | 116 | #### *1) GIF* 117 | 118 | GIF(Graphics Interchange Format)格式。这种格式采用了一种非常适合于图像数据的压缩算法(LZW算法),但只能用于 256 色位图。GIF 格式采用调色板定义各个像素值对应的 RGB 分量。GIF 格式有两种规范,GIF87a 和 GIF89a。GIF89a 在 GIF87a 规范基础上定义了简单的动画效果,且可定义透明色。目前,我们在微博、微信等应用上看到的一些动画图片(俗称“动图”)或者动画表情,使用的是 GIF89a 定义的 GIF 图片格式。 119 | 120 | GIF 图片文件以 gif 为后缀名,二进制方式存储,其内容依次分为四个区块: 121 | 122 | 1) 签名及版本信息。GIF 文件的前三个字符始终为“GIF”,紧接着是“87a”或者“89a”这三个字符。这种设计有两个好处。第一,便于程序识别文件格式;第二,用于判断 GIF 文件格式所遵循的规范(或版本)。如果是 89a,则表示包含有多张图片,可用于实现动画。 123 | 2) 全局描述信息。其中包含 GIF 图片的宽度(16位无符号整数,小头)、高度(16位无符号整数,小头)、色深(用于确定调色板大小,可能为1、4、8)、背景色在调色板中的索引值(0~256)、全局调色板(RGB 三原色数组)。 124 | 3) 扩展区。扩展区以字符“c”打头,用来定义文本扩展信息(字符串)、应用扩展信息(字符串)、注释扩展信息(字符串),以及 89a 格式的扩展属性,比如动画各帧之间的延迟时间(以 10ms为单位)、是否需要用户输入(点击鼠标或者按下回车键)才开始动画、是否透明等。 125 | 4) 图片描述区。对于动画 GIF,可包含多个描述区。每个描述区以字符“;”打头,其后九个字节是头部信息。其中包含当前帧在整个图片中的偏移位置以及大小,最后一个字节含有多重信息,如这一帧图像是否使用全局调色板以及是否交错存储;若不使用全局调色板,则这个字节中还包含有色深信息,其后便是这一帧图像使用的局部调色板。头部信息之后,包含的是由 LZW 算法编码的图像数据。 126 | 127 | GIF 使用的 LZW 算法本质上是一种通用无损压缩算法,将在本书第三篇“信息的计算机处理”中讲述。交错(interlace)存储情况下,图片中首先保存序号为偶数的水平扫描线像素数据,然后是序号为奇数的水平线像素数据。这种设计,使得我们可以首先看到图片的整体样子,然后再看到完整全貌,这在传输速度比较低的情形下尤其有用。当然,现在网络和磁盘访问速度都很高了,这么做已经没有多大意义了。 128 | 尽管一张 GIF 只能描述 256 种颜色,但这对表情符号来说足够了。对于颜色较为丰富的图片,使用 GIF 格式来存储时,一般通过统计不同颜色的像素数量来确定调色板。对颜色特别丰富的图片,需要做一些近似处理才能生成比较接近原始图的 GIF 图片,当然,这个时候生成的 GIF 图片会出现色彩失真的情形。在互联网上,某些视屏片段会被转换成 GIF 动画格式,以方便传输和展现。色彩失真就会出现在这种情况下。 129 | 130 | 在使用 GIF 格式存储颜色丰富的图片时,为了能更加准确地反映真实颜色,生成 GIF 图片的程序还可以使用一些算法来弥补颜色损失。其中一种常见算法称为“抖动(Dither)”。 131 | 132 | 如图 5-3 所示[^2]。最后一幅图使用 24 位色表示一副渐变图像(如日落时的天空),由于 24 位色情况下的颜色空间足够大,可以真实体现出这幅图的细节部分;如果将这幅图保存为 GIF 格式,则会损失大量的颜色信息,如果不做任何处理,将出现色彩带(Color Banding),即第一幅图所示情形;第二幅图使用抖动算法,用小的混合色块来模拟颜色的渐变过程,从而可以在使用色深为 8 位的 GIF 格式之情形下,仍然能够获得较为接近真实情况的图片。 133 | 134 |
135 | ![色带及抖动处理](illustration/img-5-3.png)
136 | 图 5-3 色带及抖动处理 137 |
138 |
139 | 140 | 抖动处理的原理很简单,其目的是使用调色板中的可用颜色的扩散来获得近似效果,此时人眼会自动将扩散的不同颜色混合成新的颜色。如图 5-4 第一张图所示,当红色和蓝色的方块足够小时,图片将变成紫色;而第二张图则通过单色图来模拟出了灰度效果(如使用铅笔或者钢笔绘制的素描图一样)。 141 | 142 |
143 | ![抖动处理的原理](illustration/img-5-4.png)
144 | 图 5-4 抖动处理的原理 145 |
146 |
147 | 148 | #### *2) BMP* 149 | 150 | Windows BMP 格式。这是 Windows 系统定义的图像格式,可支持 4、8、16、24 等多种颜色深度,但不支持透明和 Alpha 混合。Windows BMP 格式针对 4 位色和 8 位色支持 RLE(Run Length Encode)编码方式。 151 | 152 | Windows BMP 文件的后缀名为 bmp,其文件格式和 GIF 类似,但不能定义多帧图像,更为简单些。一个 BMP 文件依次分如下几个数据区: 153 | 154 | 1) 文件头部区域。该区域定义了 BMP 图片文件的签名、文件大小以及位图数据在文件中的偏移量。对应的 C 语言结构如下面的清单所示。其中 bfType 始终为 19778,即两个字符“BM”对应的 16 位无符号整数值。 155 | 156 | ```c 157 | typedef struct BITMAPFILEHEADER 158 | { 159 | unsigned short bfType; 160 | unsigned long bfSize; 161 | unsigned short bfReserved1; 162 | unsigned short bfReserved2; 163 | unsigned long bfOffBits; 164 | } BITMAPFILEHEADER; 165 | ``` 166 | 167 | 2) 位图头部区域。该区域定义了位图的基本信息,包括位图头部信息的字节长度、位图宽度、位图高度、位面数量、色深、位图数据编码方式、图片大小等信息。对应的 C 语言结构如下面的清单所示。其中编码方式有适用于 24 位色的 RGB 各占三个字节的情况,适合 15 位色、16 位色情形的 RGB 位域(Bit field)方式,以及适合 4 位色、8 位色的 RLE 编码方式。若色深小于等于 16 位色,则其后包含有调色板数组。 168 | 169 | ```c 170 | typedef struct WINBMPINFOHEADER 171 | { 172 | unsigned long biSize; 173 | unsigned long biWidth; 174 | unsigned long biHeight; 175 | unsigned short biPlanes; 176 | unsigned short biBitCount; 177 | unsigned long biCompression; 178 | unsigned long biSizeImage; 179 | unsigned long biXPelsPerMeter; 180 | unsigned long biYPelsPerMeter; 181 | unsigned long biClrUsed; 182 | unsigned long biClrImportant; 183 | } WINBMPINFOHEADER; 184 | ``` 185 | 186 | 3) 位图数据。以水平扫描线为单位组织,每条扫描线占用的字节长度被圆整为 4 的倍数,以便优化装载过程。 187 | 188 | RLE(Run Length Encode)是一种非常简单的图像压缩方法。当一条扫描线上有多个像素值连续相同时,即可采用这种编码方法。以针对 8 位色的 RLE 编码为例,该编码首先给出的是其后具有相同颜色的像素数量,然后像素值。比如“0x02 0x00 0x04 0x66”解码后的实际像素值序列应该为“0x00 0x00 0x66 0x66 0x66 0x66”。 189 | 190 | #### *3) PNG* 191 | 192 | PNG(Portable Network Graphic Format,可移植网络图形格式),是 GNU 为了取代 GIF 格式[^3]而定义的图像格式,可支持各种颜色深度,同时支持透明和 Alpha 混合。PNG用来存储灰度图像时,灰度图像的色深最高可达16位,存储彩色图像时,彩色图像的色深可达 48 位,并且还可存储高达 16 位的 Alpha 通道数据。PNG使用从LZ77派生的无损数据压缩算法。 193 | 194 | PNG 是为替代 GIF 格式而生,同时也提供了超出 GIF 很多特性。比如,可以支持高于 8 位的色深,支持 256 级的 Alpha 混合效果。在使用调色板和 32 位色情形下,PNG 可使用 ARGB 颜色分量来表示每个像素值,从而可以定义每个像素独立的 Alpha 半透明混合效果。需要注意的是,PNG 不支持类似 GIF 89a 规范定义的动画效果。2004年末,APNG 规范提出了一个简单的 PNG 的动画扩展实现方案,但并没有流行开来,这主要是因为 GIF 相关的专利随后到期。 195 | 196 | 相比前述的 GIF 和 BMP 格式,PNG 文件格式要复杂很多。PNG 文件的后缀名一般为 png,在程序中我们使用开源的 libpng[^4] 库来解码 PNG 格式文件或者保存图片为 PNG 格式。 197 | 198 | #### *4) JPEG* 199 | 200 | JPEG(Joint Photographic Experts Group,联合图像专家小组)是第一个国际图像压缩标准。使用这种压缩标准保存的图像文件可用来存储色彩非常丰富的图片。因为 JPEG 文件使用有损压缩技术存储像素值,数据压缩比要比 GIF、PNG 等使用无损压缩算法的图像格式高很多,而且其图像的还原度高,因此被广泛使用在数码相机、智能手机等消费类电子产品中。 201 | 202 | JPEG 图片文件通常的后缀名可能为jpeg、jfif、jpg 或 jpe,最为常见的是 jpg。其中 JFIF 是 JPEG File Interchange Format(JPEG 文件交换格式)的缩写,是对 JPEG 压缩图片的一种封装形式,常用于电脑和数码相机、智能手机产品。某些情况下,也使用 ExifJPEG 文件格式。 203 | 204 | JPEG 支持使用 RGB、YUV 色彩空间以及灰度色彩空间来定义像素,并使用了混合型的图像压缩编码方法,即同时使用有损压缩及无损压缩算法。相关的有损压缩及无损压缩算法,我们将在本书第三篇中讲述。 205 | 我们将原始的位图压缩生成 JPEG 文件时,可指定不同的压缩级别,通常为 11 级,以 0—10 级表示。其中 0 级压缩比最高,但图像品质最差;在采用细节几乎无损失的 10 级质量压缩保存时,压缩比也可达 5:1,此时,JPEG 不使用有损压缩算法来压缩图像,其结果类似 PNG。读者可以在 PC 上尝试将某个 BMP 格式保存的图片另存为 JPEG 格式,会发现最终生成的文件可能仅有几百KB。 206 | 207 | JPEG2000 作为 JPEG 的升级版,其压缩率比 JPEG 高约 30% 左右,同时支持有损压缩和无损压缩。JPEG2000 格式有一个极其重要的特征:它能实现渐进传输,即先传输图像的轮廓,然后逐步传输数据,不断提高图像质量,让图像由朦胧到清晰显示。此外,JPEG2000 还支持所谓的“感兴趣区域”特性,利用这一特性,可以指定图像上感兴趣区域具有较高的压缩质量,还可以选择指定的部分先解压缩。但遗憾的是,JPEG2000 并没有取代 JPEG 成为流行的图片压缩格式。 208 | 209 | 在现代计算机系统中,由于 JPEG/JPEG2000 的压缩和解压过程相对复杂,使用软件方式时,压缩或者解压 JPEG/JPEG2000 要占用很大的 CPU 运算能力和内存,因此,JPEG/JPEG2000 的压缩和解压一般通过专门的硬件芯片来完成,尤其在数码相框和智能手机这类消费类电子产品中。在没有硬件支持 JPEG/JPEG2000 的压缩和/或解压的情形下,可使用 libjpeg[^5] 或者 OpenJPEG[^6] 开源软件来完成 JPEG/JPEG2000 的压缩或解压。 210 | 211 | #### *5) 其他图像格式* 212 | 213 | 除了上面提到的流行图像格式之外,在计算机技术发展的过程中,还出现过如下一些图像格式: 214 | 215 | - PCX 是一种由美国 Zsoft 公司所开发的图像文件格式,原本是该公司的PC Paintbrush 软件的文件格式(PCX代表PC Paintbrush Exchange),却成了最广泛接受的 DOS 图像标准之一,然而随着 Windows 的普及,这个图像格式逐渐被 GIF、JPEG、PNG 等取代。 216 | - TIFF(Tagged Image File Format,标签图像文件格式)是一种灵活的位图格式,通过使用标签,可以在单个文件中定义多幅图像和数据,因此尤其适合于文档和书籍的扫描存储,因此,该文件格式也主要用于扫描仪和传真图像。最初,TIFF 只提供单色(二值)图像格式,采用 CCITT Group IV 2D压缩算法,但随着随着扫描仪功能愈来愈强大, TIFF 逐渐支持灰阶图像和彩色图像,且可以灵活定义图像的压缩算法,如 JPEG 或者 LZW 算法。 217 | - WebP 是一种同时提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式 VP8,是由 Google 在收购 On2 Technologies 后发展而来的图片格式,以 BSD 授权条款发布。WebP 最初在2010年发布,目标是减少文件大小,但达到和 JPEG 格式相同的图片质量,希望能够减少图片文件在网络上的传输时间。2011 年 11 月 8 日,Google 开始让 WebP 支持无损压缩和透明色的功能。根据 Google 较早的测试,WebP 的无损压缩比相同的 PNG 文件可以减少 28% 到 45% 的文件大小。 218 | 219 | ## 5.3 二维矢量图形 220 | 221 | 在介绍本节内容之前,我们首先区分一下图形和图像这两个计算机术语。在计算机技术中,图形(Graphics)泛指一切显示在计算机屏幕上的东西,可以是一副图片,也可以是绘制出来的按钮等形状;而图像(Image)指以像素点数组为组织形式的图形,一般指图片(Picture)。从概念上讲,图形是图像的超集,而图像又可以称为栅格化(Rasterized)图形。 222 | 223 | 本节要讲述的矢量图形(Vectorized Graphics)指使用矢量化的方式来定义形状以及可能的填充方法在内的计算机图形表述方法。矢量图形区别于使用像素数组为形式的图形表述方法。相比图像而言,矢量图形具有如下优势: 224 | 225 | - 通过使用数学方法描述图形的轮廓和填充属性,我们可以随意放大或缩小矢量图形,不会出现放大图像时产生的马赛克效果。 226 | - 矢量化描述的图形比起特定大小的图像来讲,往往可以节省存储空间和传输带宽,加快传输速度。 227 | 228 | 由于矢量图形技术的如上优势,在现代计算机系统中,显示文字时使用的字型通常使用矢量图形来描述,如 TrueType 字体格式(详情可阅本书第四篇“信息的计算机表达”)。另外,几年前流行的 Flash 动画,也是基于矢量图形的。 229 | 230 | 当然,矢量图形的问题也很明显,比如: 231 | 232 | - 由于计算机的图形输出设备并不能直接处理矢量化图形,因此,程序需要首先将矢量图形进行栅格化(Rasterize)处理,转换为给定大小的位图,然后才能显示到屏幕上。但这需要较强的 CPU 或者 GPU 处理能力。 233 | - 在像素密度较小的显示设备上,经过处理的矢量图形会存在锯齿状等变形问题。这也是为什么很多 TrueType 字体文件中,针对小尺寸的字型直接内嵌有点阵字体的原因(见本书第四篇“信息的计算机展现”)。 234 | 235 | 当前网络上最为流行的矢量图形格式是 SVG。SVG(Scalable Vector Graphics 可缩放矢量图形)是用于描述二维矢量图形的一种图形格式,采用可扩展标记语言(XML,见本篇第六章“抽象对象及复杂对象的表述”)。SVG 由 W3C 制定,是一个开放标准。SVG 主要支持以下几种图形对象: 236 | 237 | - 矢量图形对象,包括矩形、圆、椭圆、多边形、直线、任意曲线等。 238 | - 嵌入式外部图形,包括P NG、JPEG 等外部图像以及外部 SVG 等。 239 | - 文字对象。 240 | 241 | SVG 格式有如下优势: 242 | 243 | - SVG 可嵌入到 HTML 页面中,用来替代栅格图形,亦可嵌入 JavaScript 脚本来控制 SVG 对象,从而形成类似 Flash 的动画效果。 244 | - SVG可方便地创建文字索引,从而实现基于内容的图形搜索。 245 | - SVG 图形格式支持多种滤镜和特殊效果,在不改变图形内容的前提下可以实现位图格式中类似文字阴影的效果。 246 | 247 | 清单 5-1 给出了一个简单的 SVG 示例文件[^7]。 248 | 249 | 清单 5-1 SVG 示例 250 | 251 | ``` 252 | 253 | 255 | 257 | 258 | 260 | 261 | 263 | 264 | ``` 265 |
266 | 267 | 上面的 SVG 文件定义了两个圆角矩形,边框颜色均为黑色,但一个用红色填充,一个用蓝色填充;蓝色矩形覆盖在红色矩形上,透明度为 0.7。该 SVG 的渲染效果见图 5-5。 268 | 269 |
270 | ![示例 SVG 文件的渲染效果](illustration/img-5-5.png)
271 | 图 5-5 示例 SVG 文件的渲染效果 272 |
273 |
274 | 275 | 除了上面的矩形(rect)之外,SVG 还定义有其他基本的形状,其中包括线段(line)、折线段(polyline)、圆(circle)、椭圆(ellipse)、多边形(polygon)等。而对复杂的二维图形,一般使用路径(Path)来描述。 276 | 277 | 计算机图形学中的路径就是各种曲线段连接起来的复杂曲线,一般用来定义对象的边界。如果路径定义的图形是封闭的,还可以定义其中的颜色填充模式,如单纯的颜色或者渐变色。图 5-6 给出了一个典型的路径。 278 | 279 |
280 | ![路径](illustration/img-5-6.png)
281 | 图 5-6 路径 282 |
283 |
284 | 285 | 在 SVG 中,一个路径由四类线段组成,分别是直线段、二次贝塞尔曲线、三次贝塞尔曲线、圆弧或椭圆弧。图 5-6 中的路径,由直线和椭圆弧/圆弧组成,对应的 SVG 代码如清单 5-2 所示[^8]。 286 | 287 | 清单 5-2 SVG 路径示例 288 | 289 | ``` 290 | 291 | 293 | 295 | 定义路径 296 | Picture of a pie chart with two pie wedges and 297 | a picture of a line with arc blips 298 | 300 | 301 | 303 | 305 | 306 | 312 | 313 | ``` 314 | 315 | 其中,path 元素中的 d 属性给出了路径的生成数据,M、l、a 等参数前缀表示执行的绘制动作。大写的 M、L、A 等给出的坐标是绝对坐标,分别表示移动到给定点、绘制直线到给定点、绘制圆弧到给定点等等,而 l、a 等表示其后给出的坐标是相对坐标。 316 | 317 | 和直线段由两个端点来描述类似,贝塞尔曲线用两个端点以及一个或两个个控制点来描述。图 5-7 给出了贝塞尔曲线的示例。 318 | 319 |
320 | ![贝塞尔曲线](illustration/img-5-7.png)
321 | 图 5-7 贝塞尔曲线 322 |
323 |
324 | 325 | 显然,借助基本形状和路径,计算机可以表述各种或简单或复杂的二维图形。 326 | 327 | 在矢量图形中,对封闭形状,我们还需要定义其内部的填充模式。SVG 定义有三种填充模式:单色、渐变色和模式。另外,SVG 还定义有剪切、遮罩、组合等操作,以及滤镜操作。SVG 提供的这些机制,足够用来描述复杂的二维图形,结合后面讲述的动画机制,还可以用来展现复杂的二维动画。 328 | 329 | ## 5.4 动画 330 | 331 | 计算机中常见的动画,主要使用两种方法表述。第一种基于静止图像,如前述 GIF89a 格式;第二种基于矢量图形表述,如 Flash 动画或者 SVG 动画。 332 | 333 | ### 5.4.1 基于静止图像 334 | 335 | 使用 GIF89a 格式表述的动画,本质上定义了一系列要连续播放的动画帧,每一帧对应一副图像;当我们快速展示不同的静止图像时,给人脑的感觉就是连续的。GIF89a 定义的最短切换时间为 10 毫秒,也就是说,最高可达到每秒切换 100 副静止图像的速度,而对人类来讲,每秒 25 帧即可达到较为平滑的动画播放视觉效果。 336 | 337 | GIF89a 这个格式的出现距今已有三十年的时间了,但仍然被广泛使用。比如我们现在使用微博、微信时,其中有很多动画表情(俗称“动图”)采用的就是 GIF89a 格式。另外,如前所述,我们还可以将一段视频转换为 GIF89a 格式保存并播放,但由于 GIF 每一帧图像只能包含 256 种颜色,故而会导致色彩出现较为严重的失真现象。 338 | 339 | 类似地,我们也可以用一组连续的 JPEG 图片形成一个动画文件,这就是 Motion JPEG,简称 MJPEG。MJPEG 中的每一帧图像使用 JPEG 格式压缩,常用于数码相机等移动设备中,用来记录动画短片。MJPEG 的另外一个常见应用场合是视频录制器(Video Recorder),这种设备可将模拟或数字电视的视频信号逐帧压缩为 JPEG,并最终保存为 MJPEG。因为 MJPEG 采用的是帧内压缩技术,相邻两帧之间的数据没有关联,因此,可做逐帧的视频编辑,并广泛应用于视频的非线性编辑。也正因为此特点,在使用 MJPEG 时,无法在画面基本不发生变化(如新闻播报节目)的情形下获得更高的压缩率,故而在视频播放领域,主要使用本章后面讲到的 MPEG 等压缩技术。 340 | 341 | ### 5.4.2 基于矢量图形 342 | 343 | 相比上面基于静止图像的动画,基于矢量图形实现动画要复杂一些。不过其原理并不复杂。比如,我们要描述地球围绕太阳的公转运动(圆形物体的椭圆运动),只要描述出地球公转的椭圆轨迹就可以了。渲染动画的程序,根据设定的速度和椭圆的轨迹即可动态计算出地球的位置,并按照当前时间更新地球的位置即可实现动画效果。 344 | 345 | 这种方法除了可以描述物体(对象)的运动轨迹之外,也可以用来描述对象的颜色、透明度、大小等的变化。 346 | 347 | SVG 1.1 版本增加了动画支持。清单 5-3 给出了一个简单的动画示例[^9]。 348 | 349 | 清单 5-3 SVG 动画示例 350 | 351 | ``` 352 | 353 | 355 | 357 | 演示 animation 元素 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 374 | 376 | 377 | 385 | 387 | It's alive! 388 | 390 | 392 | 395 | 398 | 401 | 402 | 403 | 404 | ``` 405 |
406 | 407 | 清单 5-2 中的 SVG 文档,首先定义了一个矩形,并通过其子元素 animation 定义了该矩形的四个属性:x、y、width、height 的变化,如: 408 | 409 | ``` 410 | 412 | ``` 413 |
414 | 415 | 上述代码表示,从 0 秒开始,在 9s 的时间内,使矩形的 x 值从 300 改变到 0。类似的,其他的 animation 元素定义了该矩形其他三个属性的变化情况。 416 | 417 | 其后的代码用来控制一个文本字符串(It's alive!)的动画效果。如注释所言,text 元素最初是隐藏的(visibility="hidden")。从第三秒开始,在六秒内: 418 | 419 | - 该元素变为可见, 420 | - 该元素沿视口的对角线连续移动, 421 | - 其颜色从蓝色向深红色变化, 422 | - 该元素从 -30 度向零度(水平)旋转, 423 | - 该元素的字号放大三倍。 424 | 425 | 在定义 text 元素及其动画子元素之前,代码还通过 g 元素重新定义了一个用户坐标系,将原坐标系中的 (100,100) 设置为新坐标系的原点。 426 | 427 | 图 5-8 中的四幅图,分别给出了上述 SVG 动画在初始时、三秒时、六秒时以及九秒时的效果。 428 | 429 |
430 | ![示例 SVG 动画在关键时间点上的效果](illustration/img-5-8.png)
431 | 图 5-8 示例 SVG 动画在关键时间点上的效果 432 |
433 |
434 | 435 | 在清单 5-2 给出的 SVG 动画中,矩形的大小、位置,文本的颜色、角度和字号,在其动画过程中是线性、匀速变化的。但在实际的动画效果中,鲜有这种线性和匀速的变化方式。比如,要实现一个真空中自由落体的物体,其下降轨迹显然要符合牛顿第二定律定义的速度。这时,动画运动轨迹(包括大小、速度等其他参数的变化)的描述,就可以采用前述的路径来表述复杂的运动轨迹。 436 | 437 | 清单 5-4 给出了使用路径定义元素运动轨迹的示例[^10]。 438 | 439 | 清单 5-4 使用路径定义元素运动轨迹的 SVG 动画示例 440 | 441 | ``` 442 | 443 | 445 | 448 | 演示运动和动画 449 | 451 | 452 | 454 | 455 | 456 | 457 | 460 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | ``` 469 |
470 | 471 | 清单 5-4 中前面的代码绘制了蓝色的圆弧和三个小圆,用来表示运动路径。之后的代码定义了一个黄色的三角形(也使用路径定义),然后通过三角形的 animationMotion 子元素定义了三角形的动画运动路径(使用 mpath 子元素指向标识符为 path1 的蓝色路径),该动画在六秒内(dur=6s)完成,并且无限重复(repeatCount="indefinite")、自动旋转(rotate="auto")。图 5-9 中的三幅图,分别给出了上述 SVG 动画在初始时、三秒时以及六秒时的效果。 472 | 473 |
474 | ![基于运动路径的 SVG 动画示例在关键时间点上的效果](illustration/img-5-9.png)
475 | 图 5-9 基于运动路径的 SVG 动画示例在关键时间点上的效果 476 |
477 |
478 | 479 | ## 5.5 音频 480 | 481 | 如前所述,声音(声波)经过麦克风拾取[^11]后变成音频信号。这时,音频信号是连续的模拟信号,但数字计算机无法处理模拟信号,因而需要经过量化(或数字化)处理。参照图 5-10,音频信号的量化处理包括两种情形: 482 | 483 | - 使用 ADC(Analog-to-Digital Converter,模数转换器)按固定的时间间隔对音频信号做采样,将声波在时间上做离散处理,见图 5-10 B。时间间隔决定了采样频率。 484 | - 每个采样值本身也会被量化做离散处理,通常取 8 位、12 位、16 位、24 位、32 位等二进制离散数字表达,见图 5-10 C。显然,量化位数越多,音频信号的保真度也越高。 485 | 486 |
487 | ![音频信号的量化处理](illustration/img-5-10.png)
488 | 图 5-10 音频信号的量化处理 489 |
490 |
491 | 492 | 显然,采样频率越高,音频信号的保真度越高;对采样值的量化位数越多,音频信号的保真度也越高。那么,对音频信号而言,采样频率和采样值位数取多少合适呢? 493 | 494 | 这通常取决于不同的音频信号类型。人耳可以听到的声音频率范围在 20Hz 到 20KHz,其中有噪音、语音,也有音乐。比如语音而言,8000 Hz 的采样频率,8 位的采样值量化位数就可以达到电话质量(能听清,且基本能听出来对方是谁)。对音乐而言,采样频率和量化位数都要比较高才行,毕竟很多发烧友的耳朵比指挥家还灵敏,因此,一般使用 44100 Hz 的采样频率,16 位的量化位数记录音乐,这就是所谓的 CD 质量。值得注意的是,44100 Hz 这个采样频率约为人耳可以分辨的声音频率上限(20KHz)的两倍,这是有一定的理论基础的。 495 | 496 | 如果我们将经过上述量化处理之后的数据原封不动地保存下来,就是音频信号的最原始数据了。用这种方式存储原始的音频量化数据的,以 Windows 平台上的 WAV 文件为典型。 497 | 498 | 本质上,WAV 文件提供了音频信号的高保真无损存储,支持多种音频量化位数、采样频率和声道,但其缺点是文件体积较大。比如,如果我们要按照电话质量保存某领导半个小时的讲话,则其原始的音频量化数据大小为:8000 \* 60 * 30 = 14,400,000B,约为 13.4 MB;而如果对半个小时的交响乐演奏以 CD 质量、双声道(立体声)做量化处理,则原始数据大小为:44100 \* 2 * 60 \* 30 * 2 = 317,520,000,约为 302 MB。由于存储容量的持续增加以及互联网带宽的持续提升,这些数据的大小现在看起来不怎么令人惊讶。但是,如果你想在一个小型的音乐随声听里边放上 1,000 首歌曲,每首平均三分钟长占用 30MB 空间,那就需要 30GB 的存储空间;就算在今天,30GB 的存储空间对便携式数码产品也是非常昂贵的。或者想象本世纪初的那几年,互联网带宽大概在 64Kbps,要想下载一首三分钟长(30MB)的歌曲,就要等上十几分钟的时间,这基本上也是没法接受的。 499 | 500 | 要解决这个问题,计算机工程师们首先想到的就是做数据压缩。音频量化数据的压缩有三种手段。 501 | 502 | 第一种手段在采样阶段完成,因为可以完全还原原始数据,所以属于无损压缩。 503 | 504 | 第二种手段类似我们常见的 ZIP/RAR 文件压缩方法,基于原始量化数据进行数据的压缩处理,所以也属于无损压缩,只是相应的压缩算法针对音频数据的特点做了特别设计,所以要比 ZIP 等通用压缩算法更加高效。这种手段的典型音频格式为 FLAC(Free Lossless Audio Codec,免费无损音频编解码器)。 505 | 506 | 第三种手段通过离散傅里叶变换或者其他数字信号处理原理下的数学变换(如小波变换),将时域信号转换为频域信号而压缩数据,属于有损压缩。典型的就是大家熟知的 MP3,见本章 5.7.3 小节“音视频编码技术:MPEG”。 507 | 508 | 本章我们重点看第一种手段。我们之前描述的将模拟音频信号进行量化处理的方法称为 PCM(Pulse Code Modulation,脉冲编码调制)。通常,经过 PCM 处理之后,我们存储的是采样点的绝对量化值,这通常需要一个完整的采样值量化位数来存储。但如果我们观察一个实际的音频波形的话,会发现相邻两个采样值之间的差相对较小;比如,对 8 位的量化位数(取值范围在 [-127, 128] 这个区间内)来讲,相邻两个采样值之间的差大部分会落在 [-16, 16] 这个区间范围内,前面这个区间的数值需要 8 位来存储,后后面这个区间的数值只需要 4 位存储就可以了。这样,我们就可以仅记录第一个采样点的量化数值,然后记录下一个采样点相当于上一个采样点的差就能降低存储空间的占用。这就是 DPCM(Differential Pulse Code Modulation,差分脉冲编码调制)的原理。 509 | 510 | 如果进一步分析,我们还会发现有很多采样点的量化值并没有发生变化,也就是说,可能有很多相邻、连续的采样点的量化值是一样的,这个时候,我们就可以仅记录这些采样点的数量和差分值,而不需要完整记录每个采样点的差分值,进而进一步降低对存储空间的需求。这就是 ADPCM(Adaptive Difference Pulse Code Modulation,自适应差分脉冲编码调制)的原理。一般来讲,相比标准的 DPM,采用 DPCM 可使存储量减少约 25%,而 ADPCM可压缩更多的数据。 511 | 512 | 第二、三种手段中使用的具体压缩算法,将在本书第三篇“信息的计算机处理”中讲述。在现实中,使用第三种手段最常见的就是 MP3 格式,使用 MP3 压缩一首三分钟的歌曲后,大概只需要 5MB 左右的存储空间。除了 MP3 格式之外,还有 Ogg Vorbis、Opus 等开源、无专利限制的音频压缩格式[^12],以及后来出现的 AAC 格式。 513 | 514 | AAC(Advanced Audio Coding,高级音频编码)现在正在取代 MP3 成为新的音乐压缩标准。AAC 最大可容纳 48 通道的音轨,采样频率最高可达 96KHz,并且在 320Kbps 的数据速率下能为 5.1 声道(杜比环绕声音效)音乐节目提供非常高的还原品质。和 MP3 比起来,它的音质更好,还能节省大约 30% 的储存空间与带宽。 515 | 516 | ## 5.6 MIDI 517 | 518 | 我们知道,不论用什么乐器,我们都可以用简谱或者五线谱来记录音乐,其中包括节奏、音高、强弱等。使用不同的乐器演奏同一个的乐谱,虽然其音调和节奏是一样的,但其音色会有显著变化,而音色取决于每种乐器在演奏不同音符时所形成的声波之谐振波。因此,假如我们要对不同乐器演奏的乐曲做数字化处理,我们只要知道音高(也就是频率)、强度(亦即声波的振幅)、节奏(快慢),以及音色(对应的谐振波组成),就可以使用计算机的方法合成出对应的音乐。和矢量图形类似,我们可以将这种音乐的合成方式称为矢量音乐。和矢量图形类似,使用矢量化的音乐表述方式,可以大大节省存储空间,且理论上不存在失真问题。 519 | 520 | 目前广泛使用的 MIDI(Musical Instrument Digital Interface,乐器数字接口)就是基于上述概念发明出来的。MIDI 最早出现于 20 世纪 80 年代,由美国加州的音乐人 Dave Smith 发明。 521 | 522 | MIDI是编曲界最广泛使用的音乐标准格式,可称为“计算机能理解的乐谱”。利用 MIDI,音乐家在家里就可以为大型交响乐团作曲。 523 | 524 | 一般而言,使用 MIDI 作曲时,每个音色(乐器)对应一个音轨,作曲家为每个音轨定义其音高(频率)、强弱(音量)、节奏(时长)等数据,最终形成一个复杂的 MIDI 音乐。除了计算机支持 MIDI 之外,现在的很多电声乐器(如电钢琴),本质上也是MIDI 设备。 525 | 526 | MIDI 当中的不同音色有多种实现方法。一种方法就是利用前述原理,使用谐振波合成的方法来合成某种音色,缺点是音色的还原度低,毕竟特定乐器的谐振波组合是非常复杂的。较为直接的方法就是事先记录特定乐器之不同音符对应的音频信号,然后再根据每个音轨对应的音符强弱和时长合成在一起播放。这种方法是目前普遍采用的方法,主要优点是音色还原度高,另外还可根据情况替代或者更新某种音色(从而形成音色库)。这种方法称为“波表合成”。 527 | 528 | 现代电子计算机(包括智能手机)中都普遍包含有独立的音频处理模块或者控制卡(在个人电脑上称为声卡),其中均包含有 MIDI 功能,可以直接播放 MIDI 音乐。MIDI 音乐通常存储为 MID、RMI 为扩展名的文件。 529 | 530 | ## 5.7 视频 531 | 532 | 计算机中的视频处理,和电视广播系统的视频技术发展密不可分。早先,电视系统中的视频信号是模拟的,主要有 NTSC 和 PAL 两种制式。后来,电视系统开始转向使用数字信号,通过使用数字化的视频信号,观众可以获得更高的画质(更高清晰度),甚至可用来传输三维视频内容。 533 | 534 | 而随着个人电脑和智能手机的普及,越来越多的人开始使用计算机、平板电脑或者智能手机来观看视频内容,甚至可用智能手机拍摄自己的视频内容。本质上讲,计算机可播放的视频信息和数字电视所使用的视频信息没有差别。但在压缩技术、编码方式等方面要比数字电视复杂一些。另外,随着互联网的发展,计算机的视频内容还需要同时满足网络传输的要求,比如在给定的传输速率下,获得高清视频的流畅播放效果。 535 | 536 | 相比音频来讲,视频数据在计算机中的表述方法要复杂很多。接下来我们分几个小节来阐述视频的计算机表述。 537 | 538 | ### 5.7.1 视频相关概念 539 | 540 | 首先是清晰度。我们在使用智能电视播放视频时,经常会看到标清、高清、超清等选项。显然,高清视频的视觉效果要超过标清视频,但需要更高的数据传输带宽才能流畅播放。 541 | 542 | 视频的清晰度(definition)之定义和电视系统有关。和计算机屏幕的分辨率(resolution)稍有不同,电视使用水平扫描线的概念来定义垂直方向的分辨率。如前所述,电视系统传输视频信号时,使用 YUV 或者 YIQ 色彩空间。以 NTSC 使用的 YUV 色彩空间为例,视频信号被分成三部分,第一部分定义了每条水平扫描线上每个像素点的亮度,而第二部分和第三部分(色度部分),在每条水平扫描线上仅给出了一半像素点(每两个亮度像素点对应一个色度像素点)对应的值。这来源于电视信号从黑白向彩色的过渡:彩色信号(色度)信号被夹杂在原始的黑白电视信号中一并传输,彩色电视可正常接收色度信号并显示,但黑白电视可忽略色度信号而仅显示灰度图像。因此,和计算机屏幕的分辨率准确对应的是视频信号的亮度部分所定义的像素分辨率。 543 | 544 | 以 NTSC 制式为准,每一帧图像由 480 条水平扫描线组成,每条水平扫描线上有 720、704 或者 640 个像素点;高清电视(HDTV,High Definition TV)的水平扫描线可达到 1080 条,每条扫描线上有 1920 个像素点,对应的屏幕分辨率为 1920x1080;超高清电视(Ultra HDTV,又称“4K电视”),对应的屏幕分辨率为 3840×2160。与之相关的概念是长宽比(aspect ratio),传统电视的长宽比为 4:3,而 HDTV 的长宽比为 16:9。现代的智能电视,其屏幕分辨率均为16:9,所以在全屏播放传统的 4:3 电视信号时,会出现人变“胖”的情形。 545 | 546 | 另外一个概念是帧率。帧率定义了每秒钟切换静态图像的帧数,通常以 fps(frames per second)来表示。我们知道,要利用人眼的视觉暂留特性,最低要达到 10fps 的帧率才行。电影胶片一般每秒播放 24 帧静态图像,从而可以保证比较平滑的视觉效果。PAL 制式的电视对应的帧率为 25fps,NTSC 制式对应的帧率为 29.97fps,而最新的一些技术已经将帧率提升到了 120fps。显然,增加帧率可获得更加流畅的视觉效果,尤其在播放画面变化很快的视频时,比如在警匪片中经常看到的追车场面。另外,帧率越高,视频的数据量越大,需要更高的数据传输带宽才能流畅播放。 547 | 548 | 还有一个概念和人们为了使用较低数据量达到较高清晰度而采用的技巧有关,即扫描方式。考虑到一个视频相邻两幅静态图像之间的差别通常较小,所以,如果我们让相邻两帧仅包含整个图像中的一半像素,则可以大大降低视频内容的数据量(一半),且视觉效果并不会差太多——人眼是最容易被欺骗的。这就是交错扫描(Interlaced Scan,又称隔行扫描)模式。在交错扫描模式下,第一帧静态画面包含序号为偶数的扫描线像素数据,而第二帧静态画面包含序号为奇数的扫描线像素数据。和交错扫描模式对应的就是逐行扫描(Progressive Scan)模式:所有静态画面中包含有所有扫描线的像素数据。这就是我们经常在智能电视铭牌上看到 1080p 或者 1080i 的区别;前者表示逐行扫描,后者表示交错或隔行扫描。逐行扫描和交错扫描在视觉上的效果还是比较明显的,使用隔行扫描时,观者会感觉图像有明显的闪烁,观看时间长了,会出现比较严重的视觉疲劳。需要注意的是,4K 超高清电视仅支持逐行扫描。 549 | 550 | ### 5.7.2 视频压缩原理 551 | 552 | 和音频类似,视频数据通常会通过压缩技术来降低数据量,否则经过简单的计算就可以知道一分钟的高清视频片段,如果不做任何压缩,则需要大概 8,898M 字节(约为 8GB)的存储空间,这是无法接受的。因此,几乎所有的视频数据都会经过不同方法的压缩,然后再进行存储或传输。最简单的压缩办法就是采用前述小节“动画”中讲述的 MJPEG 技术,即将每一帧静态图像利用 JPEG 技术进行压缩。另外,如果观察常见的视频画面,我们会发现很多视频画面的背景是不变的,典型的如播音员播报新闻的画面。更进一步说,相邻的静态图像之间会有很多共同之处,因此,我们可以利用这一特点在相邻的静态图像之间做压缩处理。首先,我们保存第一帧画面的完整图像,然后,对其后的每一帧,记录和前一帧的差异数据,类似音频处理中的差分脉冲编码。采用这种方法之后,将更进一步降低存储视频信息的数据量。 553 | 554 | 在视频压缩技术中,前一种方法称为“帧内压缩(intraframe compression)”,后一种方法称为“帧间压缩(interframe compression)”。我们将在本书第三篇“信息的计算机处理”中详细阐述视频压缩技术。 555 | 556 | 根据视频内容的清晰度、帧率以及所采取的压缩技术,我们可以确定一个流畅展现一段视频内容所需要的最高传输速度。这里的传输速度指从硬盘、光盘或者网络上获取视频流数据然后播放的速度要求[^13]。这就是视频流数据对应的码率(bit rate),以每秒传输位数(bits per second,bps)为单位表示。需要注意的是,码率表示的是压缩率较低情况下所需要的最高传输速率,而不是最低传输速率。毕竟使用帧间压缩技术,在画面保持不变的情况下,所需要的传输速率可能降低到几乎为零。 557 | 558 | 在网络视频播放环境下,视频码率对应的就是网络带宽,给出了流畅播放某个视频所要求的最高网络带宽。比如,播放分辨率为 1080p、帧率为 25 fps 的高清视频,尖峰时候的网络带宽要求大概为 10 Mbps。 559 | 560 | ### 5.7.3 MPEG 561 | 562 | MPEG(Moving Picture Experts Group,动态图像专家组)是 ISO 与 IEC[^14] 于1988年成立的专门针对运动图像和语音压缩制定国际标准的组织。在已有的 JPEG 标准以及 H.261 视频编码技术基础上,MPEG 最初的工作于 1993 年被接受为 ISO/IEC11172 标准,这就是我们常说的 MPEG-1。 563 | 564 | MPEG-1 主要针对当时的 VCD 光盘和电视清晰度制定了对应的运动图像编码技术。MPEG-1 定义视频码率不超过 1.2 Mbps,而像素分辨率不超过 768x576,支持 1:1、4:3 和 16:9 多种长宽比,还支持从 23.976 Hz 到 60 Hz 间的八种帧率。另外,MPEG-1 也同时支持音频编码,所支持的音频码率为 32 到 448 Kbps 之间。MPEG-1 标准的颁布对后来的 VCD 播放机及 MP3 便携式随身听的市场发展起到了决定性作用。 565 | 566 | #### *1) MPEG-1 视频编码* 567 | 568 | MPEG-1 定义的图像,使用类似于 YUV 的色彩空间;每一帧图像由三个部分组成,第一部分定义亮度,第二、第三部分定义色度,且亮度部分像素点数量在水平和垂直方向是第二、第三部分的两倍。 569 | 570 | MPEG-1 结合前述的帧内压缩技术和帧间压缩技术。MPEG-1 采用 JPEG 技术实现帧内压缩,为了获得更高的压缩比,使用了基于时间的静态图像预测技术来实现帧间压缩。MPEG-1 的数据流中包含如下几种图像编码类型: 571 | 572 | - I-Frame(Intra-coded Image,内编码图像)。I-Frame 使用 JPEG 压缩技术,是自包含的,无需引用其他图像内容。 573 | - P-Frame(Predictive-coded Image,预测编码图像)。如其名称所暗示,P-Frame 记录的是其相对于之前的 I-Frame(以及其他前置 P-Frame)的变化部分,因此,在编码和解码的过程中,要依赖于前一个 I-Frame 及所有的前置 P-Frame。 574 | - B-Frame(Bi-directionally Predictive-coded Image,双向预测编码图像)。如其名称所暗示,B-Frame 被定义为前一张静态图像和后续 I-Frame 或 P-Frame 之间的差异。显然,B-Frame 需要前后各一个 I-Frame 或者 P-Frame 图像才能正确编解码。假想一只球在一个静态背景上从左向右移动,左侧被球遮盖的部分图像包含在后续的图像中。通过 B-Frame,可通过预测后续图像而不是前置图像而获得。这就是使用 B-Frame 的意义。 575 | 576 | 在实际的 MPEG-1 数据流中,使用上述不同的编码类型的图像帧是周期性交替出现的,如图 5-11 所示。 577 | 578 |
579 | ![MPEG-1 的图像编码类型](illustration/img-5-11.png)
580 | 图 5-11 MPEG-1 的图像编码类型 581 |
582 |
583 | 584 | 周期性交替使用不同的图像编码类型,是为了在压缩率和随机定位、前进、后退等常见使用场景下获得一个良好的平衡: 585 | 586 | - 大量使用 I-Frame,将导致压缩率过低; 587 | - 大量使用 P-Frame 或者 B-Frame,压缩率变高,但无法在视频流的随机定位、前进、后退等使用场景下尽快重建完整的静态图像。另外,由于使用有损压缩技术,大量使用 P-Frame 或者 B-Frame,还会使图像发生失真或者扭曲变形。 588 | 589 | 故而,在实践中,MPEG-1 的不同类型图像编码以下面的顺序出现:IBBPBBPBB IBBPBBPBB …。这样,视频流的随机访问最小分辨率将是九张静态图像,当帧率为 30Hz 时,大概为 330 毫秒,这是可以接受的。 590 | 591 | 我们提及视频时,通常强调的是运动的图像,却容易忽视视频中包含有可和运动图像同步播放的音频信号这一事实。所以,视频的编码技术,同时需要考虑音频。 592 | 593 | #### *2) MPEG-1 音频编码* 594 | 595 | MPEG-1 定义的音频编码使用三种采样率:44.1KHz[^15]、48KHz[^16] 和 32KHz,均为 16 位。根据编解码器的复杂性和性能,MPEG-1 定义了三层实现,见图 5-12。 596 | 597 |
598 | ![MPEG-1 的音频编码步骤](illustration/img-5-12.png)
599 | 图 5-12 MPEG-1 的音频编码步骤 600 |
601 |
602 | 603 | 未压缩的音频数据使用 PCM 编码,可经过快速傅里叶变换(FFT)将其从离散的时域信号变换为离散的频域信号。经过 FFT 过滤后,离散频域信号被划分为不重叠的 32 个子频信号,分别记录每个子频信号的振幅。另外,通过调整音质模型,可确定每个子频信号的噪声级别;噪声级别高时,执行低分辨率量化,噪声级别低时,执行高分辨率量化。 604 | 605 | 对于未压缩的音频数据(Layer-1,第一层),使用 PCM 编码,经 FFT 过滤和音质模型调整后的音频数据(Layer-2,第二层),也使用 PCM 编码,而经过第三层处理后的音频数据,使用霍夫曼编码(一种无损压缩技术)。 606 | 607 | 另外,MPEG-1 所定义的音频编码,可支持单声道和立体声,而立体声可以使用两个独立的数据流来定义,也可以使用单个数据流来定义。前者也称为双声道(dual channel),后者称为混合立体声(joint stereo)。 608 | 609 | 在编码音频数据时,MPEG-1 的每一层可使用 16 个固定的码率,从 32 Kbps 到 448 Kbps 不等。第一层和第二层不支持变化的码率,而第三层可支持变化的码率。 610 | 611 | MPEG-1 第三层就是我们熟知的 MP3 音乐压缩格式。如前所述,MP3 的音频数据是经过有损压缩和无损压缩两种技术的混合压缩数据。 612 | 613 | #### *3) MPEG-1 数据流* 614 | 615 | MPEG-1 定义了音频和视频的交错数据流语法。 616 | 617 | 一个语音数据流由帧组成,而帧由音频访问单元组成。每个音频访问单元由多个槽(slot)组成。在第一层,一个槽包含四个字节,而在第二层和第三层,一个槽包含一个字节。每个音频访问单元是可被独立解压并播放的最小数据单元。一个帧中始终包含固定数量的采样点。比如在 48 KHz 的码率下,一帧的播放时间为 8 毫秒;32 KHz 情况下一帧的播放时间为 12 毫秒。见图 5-13。 618 | 619 |
620 | ![MPEG-1 音频数据流](illustration/img-5-13.png)
621 | 图 5-13 MPEG-1 音频数据流 622 |
623 |
624 | 625 | 视频数据流可分成六个层来看待: 626 | 627 | 1) 序列层(Sequence Layer)指构成某路节目的图像序列,序列起始码后的序列头中包含了图像尺寸、宽高比、帧率等信息。序列扩展中包含了一些附加数据。为保证能随时进入图像序列,序列头是重复发送的。 628 | 2) 序列层之下是图像组层(Group of Pictures Layer),一个图像组由相互间有预测和生成关系的一组I-Frame、P-Frame、B-Frame 图像构成,但头一帧图像总是I-Frame。图像组层的头部中包含了时间信息。 629 | 3) 图像组层下是图像层(Picture Layer),其中包含了 I-Frame、P-Frame 及 B-Frame。在其头部中包含了图像编码的类型和时间参考信息。 630 | 4) 图像层下是像条层(Slice Layer),一个像条包括了一定数量的宏块,其顺序与扫描顺序一致。宏块(Macro Block)是采用余弦变换(DCT)执行有损压缩时的最小处理单元,在 MPEG-1 中,一个静态图像按 16x16(亮度部分)或者 8x8(色度部分)划分成一个个的宏块。 631 | 5) 像条层下是宏块层(Macro Block Layer)。MPEG-1 中仅定义了 4\:2:2 一种宏块结构,而下面要讲到的 MPEG-2定义了三种宏块结构:4\:2:0、4\:2:2、4\:4:4。这个比例表示构成一个宏块的亮度像块和色差像块的数量关系。4\:2:0 宏块中包含四个亮度像块,一个 Cb 色差像块和一个 Cr 色差像块;4\:2:2 宏块中包含四个亮度像块,二个 Cb 色差像块和二个 Cr 色差像块;4\:4:4 宏块中包含四个亮度像块,四个 Cb 色差像块和四个 Cr 色差像块。 632 | 6) 宏块层下是像块层(Block Layer)。如上所述,像块层定义了宏块中的亮度和色差数据块。 633 | 634 |
635 | ![MPEG-1 的视频数据流](illustration/img-5-14.png)
636 | 图 5-14 MPEG-1 的视频数据流 637 |
638 |
639 | 640 | 图 5-14 给出了 MPEG-1 的视频数据量分层解析。 641 | 642 | #### *4) MPEG-2* 643 | 644 | MPEG-2 制定于 1994 年,设计目标是高级工业标准的图象质量以及更高的码率。MPEG-2所能提供的码率在 3-10 Mbps 间,其在 NTSC 制式下的分辨率可达 720X486,在 MPEG-1 基础上,MPEG-2 的音频编码可提供 5.1 声道[^17]和多达七个伴音声道(DVD 可有八种语言配音)。 645 | 646 | 众所周知,MPEG-2 标准颁布之后,DVD 播放器快速取代了 VCD 播放器。由于MPEG-2在设计时的巧妙处理,使得大多数 DVD 播放器也可播放 VCD 盘片。 647 | 648 | 在音频方面,MPEG-2 在 MPEG-1 的基础上增加了低采样频率,有 16 KHz、22.05 KHz 以及 24 KHZ,并支持 5.1 声道和七个伴音声道,此外,MPEG-2 还增加了对前述 AAC 音频编码技术的支持(其采样频率可以低至8 KHz、高至96 KHz,其最多可支持 48 个通道)。 649 | 650 | 在视频方面,MPEG-2 的视频编码方法并未发生本质的改变,但提供了更高品质的数字视频支持,包括更高的清晰度和更快的帧率。另外,MPEG-2 作为 MPEG-1 兼容性扩展,它提供了交错扫描视频的支持以及一些高级特性,如缩放、更丰富的颜色空间格式等。 651 | 652 | #### *5) MPEG-4* 653 | 654 | MPEG-4 于 2000 年成为国际标准。 655 | 656 | MPEG-4 与 MPEG-1 和 MPEG-2 有很大的不同。MPEG-4 不只是具体压缩算法,它是针对数字电视、交互式绘图应用(影音合成内容)、交互式多媒体(WWW、资料聚合与分发)等的整合,以及新的压缩技术的需求而制定的国际标准。 657 | 658 | MPEG-4 标准同以前标准的最显著的差别在于,它采用了基于对象的编码理念,即在编码时将一幅景物分成若干在时间和空间上相互联系的视频音频对象,分别编码后,再经过复用传输到接收端,然后再对不同的对象分别解码,从而组合成所需要的视频和音频。这样既方便我们对不同的对象采用不同的编码方法和表示方法,又有利于不同数据类型间的融合,并且这样也可以方便地实现对于各种对象的操作及编辑。例如,我们可以将一个卡通人物放在真实的场景中,或者将真人置于一个虚拟的演播室里,还可以在互联网上方便地实现交互,根据自己的需要有选择地组合各种视频音频以及图形文本对象。我们现在在大型演艺会场中经常看到的全息技术,就是上述理念的成功运用。 659 | 660 | 与 MPEG-1、MPEG-2 相比,MPEG-4 具有如下独特的优点: 661 | 662 | - 基于内容的交互性。MPEG-4提供了基于内容的多媒体数据访问工具,如索引、超级链接、上传下载、删除等。利用这些工具,用户可以方便地从多媒体数据库中有选择地获取自己所需的内容,通过内容的操作和码流编辑功能,可应用于交互式家庭购物,淡入淡出的数字化效果等。MPEG-4 提供了高效的自然或合成的多媒体数据编码方法,它可以把自然场景或对象组合起来成为合成的多媒体数据。 663 | - 高效的压缩性。MPEG-4 基于更高的编码效率。同 MPEG-1/2 标准相比,在相同的码率下,可获得更高的视觉听觉质量,这使得在低带宽的信道上传送视频、音频成为可能。同时 MPEG-4 还能对多路数据流进行编码。一个场景的多视角或多声道数据流可以高效、同步地合成为最终的数据流。这可用于虚拟现实、三维电影、飞行仿真练习等。 664 | - 通用的访问性。MPEG-4 提供了易出错环境下的容错性,从而可以保证某些非可靠通讯环境下(如无线网络)中的应用。此外,MPEG-4 还支持基于内容的分级特性,即把内容、质量、复杂性分成许多不同级别来满足不同用户的不同需求,从而可在具有不同带宽、不同存储容量的传输信道和接收端展现不同的内容质量。 665 | 666 | 读者应该会记得,在 MPEG-4 标准颁布之后的本世纪前十年,MP4 播放器曾在祖国大地上红极一时。 667 | 668 | ### 5.7.4 其他视频编码技术 669 | 670 | #### *1) H.264* 671 | 672 | 国际上制定视频编解码技术的组织有两个:一个是国际电信联盟,它制定的标准有 H.261、H.263、H.263+ 等;另一个是国际标准化组织,它制定的标准有 MPEG-1、MPEG-2、MPEG-4 等。而 H.264 则是由这两个组织共同组建的联合视频组(JVT,joint video team)制定的数字视频编码标准,所以它既是ITU-T的 H.264,又是 ISO/IEC 的MPEG-4 高级视频编码(Advanced Video Coding,AVC)的第 10 部分。因此,不论是MPEG-4 AVC、MPEG-4 Part 10,还是 ISO/IEC 14496-10,都是指 H.264。 673 | 674 | 相比 MPEG-1/2/4,H.264 的优势有: 675 | 676 | 1) 低码率。和 MPEG-2 和 MPEG4 ASP 等压缩技术相比,在同等图像质量下,采用 H.264 技术压缩后的数据量只有 MPEG-2 的 1/8,MPEG-4 的 1/3。 677 | 2) 高质量的图像。H.264 能提供连续、流畅的高质量图像(DVD质量)。 678 | 3) 容错能力强。H.264 提供了解决在不稳定网络环境下容易发生的丢包等错误的必要工具。 679 | 4) 网络适应性强。H.264 提供了网络抽象层,使得 H.264 的文件可方便地在不同的网络上传输(例如互联网和无线通信网络等)。 680 | 681 | H.264 最大的优势是具有很高的数据压缩比率。在同等图像质量的条件下,H.264 的压缩比是 MPEG-2 的 2 倍以上,是 MPEG-4 的 1.5~2 倍。举个例子,原始文件的大小如果为 88 GB,采用 MPEG-2 压缩标准压缩后变成 3.5 GB,压缩比为 25:1,而采用 H.264后变为 879 MB,从 88 GB到 879 MB,H.264 的压缩比达到惊人的 102:1。正因为如此,经过 H.264 压缩的视频数据,在网络传输过程中所需要的带宽更少,从而也更加经济。 682 | 683 | 随着 2010 年苹果公司宣布在 Safari 浏览器中支持 H.264 而不再支持 Flash 技术,H.264 几乎取代了 MPEG-4 成为 HTML5 Web 开发中的标准视频编码技术。 684 | 685 | #### *2) H.265* 686 | 687 | 作为 H.264 的后续演进版本,H.265保留了原来的某些技术,同时对一些相关的技术加以改进。新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,以达到最优化。2012 年 8 月,爱立信公司推出了首款 H.265 编解码器,而在仅仅六个月之后,国际电联就正式批准通过了 HEVC/H.265 标准,标准全称为高效视频编码(High Efficiency Video Coding),相较于之前的 H.264 标准有了相当大的改善。值得一提的是,华为公司拥有最多的核心专利,是目前该标准的主导者。 688 | 689 | 在即将到来的 4K 视频中,相信 H.265 将成为主流的视频编码技术。 690 | 691 | ### 5.7.5 常见音视频格式 692 | 693 | 从上个小节的介绍中我们可以看出,某些标准同时定义音视频编码技术,而有些标准仅定义视频或者音频。而在现实中,视频内容中往往同时包含有音频数据。除了音视频数据之外,复杂的多媒体数据流中还需要包含歌词、字幕(多种语言)、章节定义等等内容。在知识产品保护得到极大重视的今天,数据流中还需要包含数字版权认证信息。故而在实践中,出现了多种不同的音视频格式。有些格式由 MPEG 等标准定义,而有些格式是在这些标准形成之前,由操作系统或者软件平台厂商自己定义的,有些使用了自己的音视频编码技术。表 5-1 给出了常见的音视频格式。 694 | 695 | 表 5-1 常见音视频格式 696 | 697 | | 后缀名 | 说明 | 698 | |:---------|:------------| 699 | | MP3 | 使用 MPEG-1 Layer 3 编码技术的音频文件,主要用于音乐、歌曲等。 | 700 | | MPG/MPEG | 使用 MPEG-2 编码技术的视频文件。在早期的 VCD 盘片上,使用 MPEG-1 编码技术的视频文件通常具有 .DAT 后缀名。 | 701 | | MP4 | 使用 MPEG-4 编码技术的音频或者视频文件。 | 702 | | AAC | 使用 AAC 编码技术的音频文件。 | 703 | | OGG | Ogg 是一种容器格式,可封装由 Xiph.Org 基金会维护的各种开放、免费的音视频编码数据流。 | 704 | | AVI、ASF、WMV、WMA |这三种格式是微软公司为 Windows 操作系统开发的音视频格式。WMV/WMA 是目前 Windows 平台上的主流格式,分别用于视频和音频。 | 705 | | MOV | 这是苹果公司为 QuickTime 多媒体平台开发的视频格式。 | 706 | | FLV、F4V | 这两种格式是 Adobe 公司为 Flash 开发的视频流格式,主要用于在 Flash 中嵌入在线视频。F4V 采用 MPEG-4 视频编码技术。 | 707 | | RM、RMVB | 这两种格式由 Real Networks 公司开发,在网络带宽较小的年代曾红极一时。 | 708 | | DivX | DivX 原是由一群黑客为打破美国的 MPEG-4 技术禁运政策而发明的。最初 DivX 使用 MPEG-4 视频编码技术和 MPEG-1 Layer 3 音频编码技术来封装DVD 影片内容,由于大大缩小了文件体积而迅速走红互联网。DivX 现在已发展成为一种数字视频格式,支持MPEG-4、H.264和最新的 H.265 标准的视频,分辨率可高达 4K 超高清。 | 709 | | WebM | 这是由谷歌发展的一种音视频容器格式。其中的视频编码技术采用谷歌自己的 VP8,而音频编码技术采用 Ogg Vorbis。 | 710 | 711 |
712 | [^1]: 在计算机屏幕上显示的图像是否接近真实情况,还和显示屏的显示技术有关,有时同样类型的显示屏,因为电子元器件的漂移,也会带来明显的色差。 713 | 714 | [^2]: 此图取自维基百科“色彩带”词条。 715 | 716 | [^3]:【维基百科】在早期,GIF 文件所用的 LZW 压缩算法是 CompuServ 所开发的一种免费算法。然而令很多软件开发商感到意外的是,GIF 文件所采用的压缩算法忽然成了 Unisys 公司的专利。据 Unisys 公司称,他们已注册了 LZW 算法中的 W 部分的专利,如果要开发生成(或显示)GIF 文件的程序,则需向该公司支付版税。目前,GIF 相关专利已经失效,但在专利失效前曾引起部分开放源代码社区发起“Burn all GIFs”的运动抵制使用 GIF 格式。因此,人们开始寻求一种新技术,以减少开发成本。PNG标准就在这个背景下出现。 717 | 718 | [^4]: [http://libmng.com/pub/png/libpng.html](http://libmng.com/pub/png/libpng.html) 719 | 720 | [^5]: [http://libjpeg.sourceforge.net/](http://libjpeg.sourceforge.net/) 721 | 722 | [^6]: [http://www.openjpeg.org/](http://www.openjpeg.org/) 723 | 724 | [^7]: 该示例取自维基百科“SVG”词条。 725 | 726 | [^8]: 该示例来自于 SVG 1.1 规范描述文档([http://www.w3.org/TR/SVG11/paths.html#PathDataCurveCommands](http://www.w3.org/TR/SVG11/paths.html#PathDataCurveCommands0)。 727 | 728 | [^9]: 该示例来自于 SVG 1.1 规范描述文档([http://www.w3.org/TR/SVG11/animate.html#AnimateMotionElement](http://www.w3.org/TR/SVG11/animate.html#AnimateMotionElement))。略有修改。 729 | 730 | [^10]: 该示例来自于 SVG 1.1 规范描述文档([http://www.w3.org/TR/SVG11/animate.html#AnimateMotionElement](http://www.w3.org/TR/SVG11/animate.html#AnimateMotionElement))。略有修改。 731 | 732 | [^11]: 故而麦克风也经常被称作“拾音器”。 733 | 734 | [^12]: 这些开源、无专利的音视频编码技术(包括 FLAC),现在由 Xiph.Org 基金会管理,其官方网站为 [http://xiph.org/](http://xiph.org/) 。 735 | 736 | [^13]: 这里我们忽略了对视频数据进行解码和解压缩的过程所需要花费的时间。 737 | 738 | [^14]: International Electrotechnical Commission,国际电工委员会。 739 | 740 | [^15]: 即 CD-DA(Compact Disc Digital Audio,光盘数字音频)采样率。 741 | 742 | [^16]: 即 DAT(Digital Audio Tap,数字音频磁带)采样率。 743 | 744 | [^17]: 左右中、两个环绕声道以及一个加重低音声道。 745 | -------------------------------------------------------------------------------- /textbook/part-1-chapter-6.md: -------------------------------------------------------------------------------- 1 | # 第 6 章 抽象对象及结构化数据的表述 2 | 3 | ## 6.1 语言和区域 4 | 5 | 在第 4 章中,我们了解到了计算机如何处理文字。但通过适当的字符集以及编码,并不能解决所有有关语言的问题。比如拿汉语来讲,有繁简体之分,而且同样的名词,可能存在不同的叫法,如计算机“程序”,在台湾称为计算机“程式”,大量的海外地名、人名也有着不同的翻译方法。就算是同样使用英语的国家,在日期表达、货币符号等方面也存在着诸多差异。比如,美国人习惯使用“May 20, 2015”的方式表达日期,而英国人习惯使用“20 May 2015”的方式。 6 | 7 | 除此之外,大部分语言在书写的时候使用从左向右的习惯,但阿拉伯、希伯来等语言,使用从右向左的书写习惯[^1]。这一切,都为计算机处理语言相关的问题带来了麻烦。 8 | 9 | 最初,计算机系统仅能处理和显示英文,使用 ASCII 字符集基本上就够了。但随着计算机系统在全球各地广泛使用,就需要计算机软件能够根据计算机用户所使用的语言和国家/地区来做适当的处理。典型的就是计算机软件界面的文字需要翻译成特定的语言,而日期、货币等字符串,需要按照当地的习惯来表达。 10 | 11 | 为了解决这个问题,计算机系统引入了一个抽象的概念:区域(Locale)。区域定义了一个特定的语言和国家/地区组合,通过这个组合设置,告诉运行在操作系统之上的应用软件,当前的语言是什么,日期、货币等的表达要符合哪个地区的习惯。具体来讲,区域设置对数字、货币、时间和日期的格式化产生影响。比如对英语(美国)这个区域来讲,这些内容的格式化形式示例如下: 12 | 13 | - 数字:123,456,789.00 14 | - 货币:$123,456,789.00 15 | - 时间:4\:58:32 PM 16 | - 短日期:5/20/2015 17 | - 长日期:Wednesday, May 20, 2015 18 | 19 | 除了以上用户可以直观感觉到的区别之外,区域还对如下软件系统的接口有影响: 20 | 21 | - 正则表达式处理:如字符(尤其是字母)的排序规则、字符分类、大小写转换等。 22 | - 字符串的本地化处理。 23 | 24 | 在 Linux、Windows 等操作系统中,区域的定义通常具有 zh\_CN 这样的形式,其中 zh 是中文/汉语的国际标准代码,CN 是中华人民共和国的国家代码。当计算机操作系统的区域被设置为 zh\_CN 时,系统中的所有软件默认将使用中文简体来显示界面文字,使用“¥”作为货币符号,而日期的表达使用“2015 年 5 月 19 日星期二”这样的形式。如果区域被设置为 zh_TW 时,则使用中文繁体文字,使用中国台湾的习惯用法。 25 | 26 | 表 6-1 给出了常见的的语言和国家/地区代码。这个表中给出的语言和国家/地区代码,现在是国际标准,标准代码分别是 ISO 639 和 ISO 3166。我们一般使用这两种代码的短形式(两个字母),有时也使用长形式(三个字母)。 27 | 28 | 表 6-1 常见语言和国家/地区编码 29 | 30 | | 语言 | 语言代码 | 国家/地区 | 国家/地区代码 | 31 | |:----------|:---------|:----------|:--------------| 32 | | 中文/汉语 | zh (zho) | 中国 | CN (CHN) | 33 | | | | 台湾 | TW | 34 | | | | 香港 | HK | 35 | | | | 澳门 | MC | 36 | | 英语 | en (eng) | 美国 | US (USA) | 37 | | | | 英国 | UK | 38 | | 法语 | fr | 法国 | FR | 39 | | | | | | 40 | | 德语 | de | 德国 | DE | 41 | | | | | | 42 | 43 | 在计算机软件系统中,一些基础的功能模块,尤其是上述数字、货币、时间、日期的格式化功能函数,其行为受区域设置的影响,会根据当前的系统区域设置来相应调整输出。 44 | 45 | 在现代操作系统中,一般通过环境变量来设置区域。程序运行时,可继承全局的环境变量,亦可动态修改自己的环境变量。在 Linux 系统中,用于设置区域的环境变量称为 LC\_ALL[^2] 或者 LANG,默认情况下为 en\_US.UTF8 形式,其中的 .UTF8 后缀,指明了文字的字符集及编码。 46 | 47 | 另外,具有良好国际化和本地化设计的计算机软件,会根据当前的区域设置来显示不同的界面文字,实现界面文本的自动适配。 48 | 49 | 计算机软件的国际化和本地化是两个概念。首先,计算机程序中使用字符串时,要经过一次处理,这个处理通常是一个函数,该函数可根据一个唯一的标识符来返回当前区域对应的字符串,然后再行使用这个字符串。这个处理称为“国际化(internationalization[^3])”。其次,需要针对不同的区域增加对应的字符串集合,这个过程称为“本地化(localization)”。 50 | 51 | 在 Linux 操作系统中,一般直接使用 en\_US 区域下的字符串作为获得本地化字符串的唯一标识符,传递给字符串转换函数,该函数根据当前的区域设置,查找一个映射表,然后返回对应的本地化字符串。我们经常使用 GNU 的 gettext 开源软件完成这项工作,同时使用 msgfmt 工具来完成默认字符串和本地化字符串之间的映射关系,最终生成一个 mo 文件,保存在以区域为名称的子目录(如 zh\_CN)下。若 gettext 无法找到给定区域的字符串映射表时,就会返回原字符串。这样,在没有针对这个区域完成本地化的情况下,程序的输出字符串仍然为原字符串。 52 | 53 | 在 Windows 操作系统中,可以使用 GNU 的 gettext 工具实现国际化和本地化,但 Windows 开发工具提供的方案是使用整数的字符串标识符作为字符串对象的唯一标识。 54 | 55 | ## 6.2 时间、日期及时区 56 | 57 | 在计算机系统中,我们看到的时间和日期是“2015 年 5 月 20 日 18:54”这种形式,然而计算机内部的计时系统并不直接记录上面这种形式的时间和日期,而是以秒为单位的一个整数,这个整数记录了从 1970 年 1 月 1 日 00:00 UTC[^4] 开始到当前时间的秒数,这个秒数被称为“UNIX 时间戳”,而起始的“1970 年 1 月 1 日 00:00 UTC”这个时间被称为 UNIX 纪元时间(UNIX Epoch)。 58 | 59 | 操作系统在启动时,首先会从计算机系统的底层硬件时钟当中获得当前的时间,以此为基础换算为 UNIX 时间戳,然后根据时钟芯片产生的滴答中断(一般每秒 100 次)来维护时间戳向前增加。 60 | 61 | 在 32 位计算机系统中,操作系统最初维护的时间戳是一个 32 位的无符号整数。经过简单计算我们可知,这个 32 位无符号整数记录的时间戳将在 2038 年的某一天溢出。为提前应对这个问题,现代操作系统的新版本开始使用 64 位无符号整数来记录 UNIX 时间戳,同时也可提供毫秒甚至微秒级的时间信息,以便需要高精度时间数据的应用程序使用。 62 | 63 | “2015 年 5 月 20 日 18:54”这种形式的日期和时间表达方法,在计算机系统中称为“墙钟(Wall clock)”,应用程序可根据操作系统维护的当前时间戳以及时区设置计算出墙钟日期和时间。 64 | 65 | 系统时区的设置和上个小节提到的区域类似,使用 TZ 环境变量(在 Linux 操作系统中,若未定义 TZ 环境变量,则使用 /etc/localtime 中的数据),可通过操作系统的编程接口来获得或设置。 66 | 67 | 一个给定的时区可以有多种表达方式: 68 | 69 | - 使用标准名称,如北京时间的标准名称为 CST(China Standard Time)。 70 | - 使用标准名称及相对 UTC 的偏移量,其格式为 \\。如 CST-8\:00:00,其中 CST 是北京时间的标准名称,-8\:00:00 表示本地时间减去八个小时为 UTC。 71 | - 使用 GMT\ 格式。其中 GMT 为格林威治时间(亦即 UTC),OFFSET 表示对应的本地时间和 GMT 的偏移量。比如 CST 还可以表达为 GMT+8\:00:00。 72 | - 根据所在地区和城市确定时区,比如 Asia/Shanghai 或 America/Chicago 等。 73 | 74 | 若给定的时区支持夏令时,会给计算机系统处理本地时间带来更多的麻烦,而且时区的定义可能会发生变化(比如朝鲜就在 2015 年宣布使用自己全新定义的时区,新的朝鲜标准时间比北京时间早 30 分钟,而之前是和北京时间一致的)。为此,现代计算机系统一般会提供最新的时区信息文件,在这些文件中保存有完整的时区描述数据,比如标准名称、UTC 偏移量、是否支持夏令时以及夏令时的起止时间等。在 Linux 系统中,这种文件称为 tzfile;通过使用 TZ 环境变量可指定从特定的 tzfile 中获得对应的时区数据,比如新西兰,使用 TZ=":Pacific/Auckland" 的定义 TZ 环境变量,则对应的时区信息保存在 /usr/share/zoneinfo/Pacific/Auckland 文件中。 75 | 76 | 在现代计算机系统中,获取或设置时间戳的接口一般由操作系统内核提供。以 Linux/Unix 为例: 77 | 78 | - time () 函数返回当前的时间戳(以秒为单位); 79 | - gettimeofday/settimeofday 可以以微秒级精度来获取或设置当前的时间戳;这两个函数亦可用来获得或设置当前的时区[^5]; 80 | - 后来引入的 clock_gettime/clock_settime 函数可以以纳秒级精度来获得或设置当前的时间戳。 81 | 82 | 在以上的操作系统内核接口之上,根据时间戳计算得到某个特定时区下的具体日期及时间的功能,则由应用软件负责完成。在不同的编程语言下编程时,相应的平台会提供一些公共接口来帮助应用程序完成日期的计算、对比和转换等工作。比如在使用 Java 语言时,可使用 Calendar 类计算给定时间戳对应的分解时间,具体包括年份、月份、日期、当年第几周、当年第几天等等信息,再结合当前的区域设置,经过字符串的格式化操作,即可得到想要的日期及时间字符串(如“2015-08-27 11:17”)。在使用 C 语言时,C 语言的标准函数库也提供了 ctime、gmtime、localetime、mktime、strftime 等函数来完成时间戳到分解时间的计算,以及时间字符串的格式化。 83 | 84 | 当然,我们这里提到的日期都是公历(也称作格列高利历,即 Gregorian Calendar)。如果要显示中国的农历日期,则需要额外的程序执行公历到农历的转换,而这种转换功能并不是软件平台标准的一部分,毕竟这些标准都是西方国家设计的,没几个人懂农历。 85 | 86 | 值得一提的是,计算机硬件系统保存的时间和电子/石英手表一样,使用晶振来计时,时间一长,就可能出现大的误差,比如每过 24 小时会快上几秒或者慢上几秒。随着互联网的普及,操作系统开始使用互联网获得更为精确的时间。互联网上有众多提供标准时间的服务器,称为“时间服务器”,通过网络时间协议(NTP)或浮动时间同步协议(FTSP),任何访问这个服务器的系统都可以精确获知当前的时间。这样,很多操作系统在启动时使用上述协议重置系统时间,同时周期性地和时间服务器同步系统时间,以弥补硬件计时系统可能存在的误差。 87 | 88 | ## 6.3 HTML 89 | 90 | HTML 是 Web 页面的唯一描述语言,由 Web 发明者 Tim Berners-Lee 于 1989 年发明并逐步演进到今天广为接受的 HTML5。 91 | 92 | 在 Web 被发明之前,互联网用户主要通过电子邮件(email)、文件传输协议(FTP)等机制协同工作。在 CERN(欧洲核子研究组织)工作时,Tim Berners-Lee 开发了一个简单的 HTTP 服务器以及一个 WWW(World Wide Web,万维网)客户端程序。这个程序可以从 HTTP 服务器中获得一个 Web 页面文件,然后根据这个文件中定义的标签展示结构化的页面内容,其中包括主题、标题、段落等等。那时候,一个典型的 HTML 页面内容大概如清单 6-1 所示。 93 | 94 | 清单 6-1 一个最简单的 HTML 文件内容 95 | 96 | ``` 97 | 98 | 99 | 100 | 101 | 102 | Hello, World! 103 | 104 | 105 | 106 | 107 | 108 |

109 | HELLO, WORLD! 110 |

111 | 112 |

113 | This is the first Web page in HTML. 114 |

115 | 116 |

117 | Click HERE to see the second Web page. 118 |

119 | 120 | 121 | 122 | 123 | ``` 124 |
125 | 126 | 使用我们现在常用的 Web 浏览器(如 Chrome)渲染上述 HTML 内容,其显示效果如图 6-1 所示。 127 | 128 |
129 | ![一个最简单的 Web 页面渲染效果](illustration/img-6-1.png)
130 | 图 6-1 一个最简单的 Web 页面渲染效果 131 |
132 |
133 | 134 | HTML 是 Hyper Text Markup Language 的缩写,中文译为“超文本标记语言”。HTML 的基础语法来自于 1986 年由 ISO 标准化组织颁布的 SGML(Standard Generalized Markup language,标准通用标记语言)。仔细对比清单 6-1 中的内容和图 6-1 中的显示效果,我们可以看到: 135 | 136 | HTML 中由尖括号(<>)包围的称为标签(tag),标签往往成对出现,定义了一个要显示在页面中的元素(element)。如 \ 表示开始定义文档的主题,\表示结束文档主题的定义,这两个标签之间是主题的内容“Hello, World!”。类似地,H1 标签表示的是文档的一个一级标题,P 标签标识的普通段落。如本文档仅包括一个一级标题,其内容为“HELLO, WORLD!”;其后是两个普通段落。 137 | 138 | 除了上述具有实质性内容的标记之外,还有 HTML、HEAD、BODY 等定义文档整体结构的标签。其中 HEAD 定义了文档的头部信息,TITLE 标签包含在其中,而 BODY 标签定义了文档的正文内容,所有显示在浏览器页面中的元素定义在 BODY 标签中。 139 | 140 | 另外本示例还包含有一个重要的标签 A,该标签定义了超链接(Hyper Link)。在浏览器中,超链接显示为带有下划线的蓝色文字,当用户点击超链接时,将告诉浏览器跳转到另外一个页面。这个页面可能是由同一服务器提供的,也可能是由其他服务器提供的。 141 | 142 | 总而言之,HTML 通过标签定义了复杂的 Web 页面的文档结构,这些 Web 页面由浏览器处理并展现,并通过超链接将分布在世界各地的 Web 页面链接在了一起,这是 Google、百度等巨型互联网公司赖以出现并发展壮大的最重要技术基础。 143 | 144 | 在之后二十多年的发展中,HTML 从最原始的二十多个标签发展为近百个标签,同时引入了 CSS(级联样式表)技术来灵活定义文档元素的渲染效果,还引入了 JavaScript 编程语言来动态控制页面的内容。随着 HTML5 标准的正式发布,硬件处理性能的提高,大部分人们开始倾向于认为未来将是 Web App 的天下;2015 年,由优秀华人计算机专家宫力博士发起创立的 Acdemic 公司,开始面向全球开发和推广基于 HTML5 技术的操作系统:H5OS。这一切预示着 HTML5 及其相关标准和技术在未来可能具有不可限量的发展空间。为此,本书将分别在第五篇“信息的计算机展现”和第六篇“计算机编程语言”中专门讲述使用 HTML5、CSS3、JavaScript 相关技术来开发 Web 应用的基本概念及方法。 145 | 146 | 在今天我们看来,互联网之所以获得巨大成功,和 Web 以及相关技术(如 HTML)的发明有着绝对的直接关系。尽管 Tim Berners-Lee 并没有从 Web 的发明中获得直接利益,但时至今日,他仍然作为 W3C(万维网联盟,Web 相关标准的制定者)发起人和主席参与相关标准的制定,指挥着万维网的发展。 147 | 148 | ### 6.3.1 HTML 的演进 149 | 150 | HTML 从第一个版本到现今广为接受的 HTML5,走过了二十多年的历程,期间出现了许多 HTML 的版本,也出现了很多相关的标准或规范[^6]。有意思的是,尽管这些标准基本上都是出自 W3C 之手,但有些标准和规范并未获得业界的广泛接受。更有意思的是,就算是同一个标准,很多厂商(主要是浏览器厂商)并没有按照统一的标准和规范要求做相应的实现,有些厂商甚至刻意在其软件实现(浏览器)中引入了不兼容。 151 | 比如 XHTML,这个标准被戏称为 HTML 的 XML 版本;XHTML 和 HTML 并没有本质上的区别,但 XHTML 要求按照 XML 的语法来严格书写 HTML,比如标签和属性必须使用小写,属性值必须使用双引号(")包围,空标签必须使用 \
这样的形式,不能省略结束标签(如 \

),不能随意嵌套元素等等。然而,XHTML 并未被广泛接受,因为虽然有大量的网页并不符合这个标准的要求,但浏览器仍然可以正常处理而不会报任何错误,于是,没有开发者愿意花额外的时间来做检查并修复不符合规范要求的写法,况且很多 HTML 代码是由程序自动生成的,还涉及到程序的修改,其工作量可想而知。之所以提出 XHTML 这个标准,现在看来,大概是因为标准制定者的心理洁癖。目前,W3C 仍然致力于 XHTML 标准的演进,在颁布新的 HTML 标准之时,都会相应颁布对应的 XHTML 版本。比如HTML 4.01 对应的是 XHTML 1.0;HTML5 对应的是 XHTML5。 152 | 153 | 现代浏览器在处理从 HTTP 服务器端获得的网页时,根据内容类型(Content Type)来决定使用 HTML 的规范要求还是使用 XHTML 的规范要求。当服务器返回的内容类型为 text/html 时,浏览器按照 HTML 处理,不要求严格的语法,不会报任何语法错误;当内容类型为 application/xhtml+xml 时,则按照 XHTML 处理,浏览器会做严格的语法检查,存在任何不符合 XML 语法要求的情况均会报错。这大概是一种折衷处理办法,毕竟从长远看,符合标准的严格语法要求有利于长期发展,但面对现实情况又不得不做出让步。 154 | 155 | 在 HTML 4.0 提出之前比较成熟的标准是 HTML 3.2,该版本颁布于 1997 年 1 月。HTML 3.2 及之前的版本主要使用某些格式标签,如 font(字体)、big(大字体)、strike(删除线),以及一些属性,如 color(颜色)等定义文本的样式变化。但这种标签和属性的大量使用一方面导致 HTML 代码看起来非常混乱,另外一方面和 HTML 仅定义文档结构的初衷相背离。为解决这一问题,W3C 逐步引入了 CSS 的概念。 156 | 157 | 使用 CSS,我们可以将文档中的元素和该元素对应的渲染样式隔离开来。具体来讲,就是我们可以针对 HTML 文档中的某个特定元素或某类元素单独定义其渲染样式。一个 CSS 样式可定义一个元素的尺寸、边框、背景、前景色、字体、对齐方式等等样式属性,而且可以单独存放在 CSS 文件中。CSS 的发明给 Web 开发带来了非常深远的影响,大受 Web 开发者的欢迎。 158 | 159 | 然而,真正成熟和广泛接受的 HTML 4.01 版本是在 1999 年颁布的,对应的 CSS 版本是 CSS 2.0。在这之前,从 HTML 3.2 开始,CSS 1.0/1.2 已经得到一些浏览器的支持,好在并没有太多开发者使用,那时用户使用的浏览器绝大部分是微软的 IE(Internet Explorer),因此,向更高版本标准的演进并没有导致太多的麻烦。遇到真正的麻烦的是 CSS 3.0。CSS 3.0 引入了更加清晰的布局概念,同时也引入了 CSS 动画支持。从 CSS 2.0 向 CSS 3.0 演进的过程,刚好是 IE、FireFox、WebKit、Opera 等浏览器混战的年代,于是,在 CSS 3.0 标准的草案还在讨论当中时,这些浏览器就开始部分或者全部支持了 CSS 3.0 的特性,于是,我们看到很多 CSS 样式表中包含这样的写法: 160 | 161 | ``` 162 | -webkit-box-shadow:0 2px #135389; 163 | -moz-box-shadow:0 2px #135389; 164 | -o-box-shadow:0 2px #135389; 165 | box-shadow:0 2px #135389; 166 | ``` 167 |
168 | 169 | 上面这四行其实是在重复一个属性的定义,不过前三行具有不同的前缀,对应于不同的浏览器:WebKit、Mozilla(FireFox)、Opera。等到 HTML5 标准颁布并获得这些浏览器的支持之后,只要保留最后一行就可以了。 170 | 171 | 上面的例子只是一个简单的例子,真正令开发者头疼的是浏览器在某些方面的特性支持不一致带来的问题。比如,由于 Windows 操作系统的垄断地位,微软在 Internet Explorer 的开发中就引入了很多不符合标准的特性,最为著名的就是 ActiveX 控件。ActiveX 控件使用 Windows 平台特有的接口实现,如果某个网页使用了 ActiveX 控件,就无法在运行于 Unix/Linux 平台上的浏览器上正常展示或正常工作,最常见的就是各种网络银行网站。在最新的 W3C 标准的支持程度上,微软的 IE 始终比其他浏览器的表现差很远。当然,这给 FireFox、Opera、Chrome 等浏览器以机会来抢占市场份额。不过对 Web 开发者而言,Windows 平台上的 IE 浏览器是不能被轻易忽略的——因为其长期以来占据的显著市场份额。这导致了 Web 开发成本的上升,也阻碍了新标准的实施。 172 | 173 | HTML 标准的演进过程是一个典型的计算机软件标准发展过程,它反映了标准演进过程一些经常遇到的问题: 174 | 175 | 1) 标准无法具有足够的前瞻性,总是很难及时满足飞速发展的市场需求,这导致大部分标准形成于事实存在之后,如 CSS 以及 HTML5 新增的画布标签等等。 176 | 2) 某些势力会利用其垄断地位刻意在产品中引入和标准不兼容的特性,如 IE 中的 ActiveX 控件;另外,旧势力(如微软 IE)的存在,会极大阻碍新标准的推广。 177 | 178 | 另外,技术的发展会让某些技术变得过时或者被淘汰,比如,很早出现的 Java Applet 技术最终被 JavaScript 取代,而 HTML5 多媒体标签的出现让 Adobe Flash 插件变得过时。 179 | 180 | ### 6.3.2 HTML5 181 | 182 | 1999 年颁布的 HTML 4.01 可以说是 Web 开发中的一个重大转折点,这主要得益于 CSS和 JavaScript 技术的成熟。使用 HTML 4.01 开发的 Web 应用,可以获得更美观的排版效果以及动态特性。然而,HTML 4.01 仍然存在一些不足: 183 | 184 | - DIV 标签的滥用。为了和 CSS 配合形成美观的排版布局效果,大部分网页使用了大量无意义的 DIV 标签。这背离了 HTML 仅定义文档结构的出发点。 185 | - 在网页中嵌入音频、视频播放器时,仍然要通过插件来进行,比如使用 Adobe Flash 来播放视频。这需要用户在浏览器之外安装额外的插件,同时增大了 Web 开发的工作量。 186 | - 仅仅操作 HTML 文档内容,无法开发出精细的二维或者三维图形应用,比如大型网游。当然,开发者可以自行开发插件来提供相应的功能,但一旦开发自己的插件,就要针对不同的浏览器开发出对应的插件版本。 187 | - 仅仅依赖于 HTTP 协议,很难开发实时联网类应用,如实时聊天室。这里需要说明的是,HTTP 本质上是一种无状态的短连接协议。 188 | 189 | 本质上,HTML5就是为了解决上面这些问题而出现的。比较有意思的是,从 HTML 4.01 到 HTML5,中间经过了漫长的十五年!中间这段时间 W3C 干嘛去了呢? 190 | 191 | 这和之前提到的标准制定者的心理洁癖有关。颁布了 HTML 4.01 之后,W3C 采取了一个错误的路线,一心想把 Web 开发向 XML 方向上引导,所以制定了 XHTML 1.0、1.1、2.0 等标准和规范。尤其是 XHTML 2.0,这个版本一方面更加 XML 化,另一方面和之前的 XHTML 1.0/1.1 都不兼容,这显然是在闭门造车嘛。所以浏览器厂商不买 W3C 的账(其实开发者也不买 W3C 的账),于是浏览器厂商在 Opera 的牵头下另起炉灶办了一个超文本应用技术工作组,也就是 WHATWG。WHATWG 向后来的 HTML5 演进,而 W3C 则在错误的路线孤注一掷。 192 | 193 | 2006 年,W3C 主席(Tim Berners-Lee)认识到了 W3C 的错误,开始主动向 WHATWG 示好,并在 WHATWG 的工作基础上组建了一个新的 HTML 工作组。2009 年,W3C 宣布终止在 XHTML 方向上的工作。 194 | 195 | 虽然有过几年的时间,WHATWG 和 W3C 分头向 HTML5 演进,但最终两家合在了一起。2014 年 10 月 28 日,HTML5 正式成为 W3C 的推荐标准。 196 | 197 | HTML5 的主要特点如下: 198 | 199 | - 在 HTML 4.01 基础上废弃了一些用于定义样式和格式的标签或属性,进一步强化 HTML 仅用于定义文档结构的特征。废弃的标签有 basefont(基础字体)、big(大字体)、center(居中)、font(字体)、s(小字体)、strike(删除线)、u(下划线)、frames(框架)、frameset(框架集)、noframe(无框架)等。废弃的属性有 align(水平对齐)、bgcolor(背景色)、height(高度)、width(宽度)、valign(垂直对齐)等。 200 | - 新增 article(文章)、header(页眉)、footer(页脚)、section(小节)、nav(导航)、aside(边栏)、hgroup(标题组)等标签,可以用于定义文档中的不同类型的段落或者页面区域,这些标签的恰当使用,同时可以防止 div 标签的滥用。 201 | - 新增 canvas(画布)标签,Web 应用可使用 JavaScript 和 WebGL 接口在指定的画布上绘制二维或者三维图形。 202 | - 新增 audio(音频)、video(视频)标签,Web 应用可在网页中嵌入音频和/或视频播放器。 203 | - 通过 JavaScript API 提供了本地存储相关接口。Web 应用可在浏览器端保存数据;键值对形式或者数据库形式。 204 | - 通过 JavaScript API 提供了地理位置相关接口,Web 应用可获知用户所在的地理位置信息。 205 | - 通过 JavaScript API 提供了套接字支持,从而使得 Web 应用可以和服务器端建立持久的套接字连接(区别于基于 HTTP 的无状态短连接)。 206 | - 支持离线 Web 应用。 207 | - 为表单元素提供了更好的类型支持和本地检测机制。 208 | 209 | 显然,HTML5 标准制定者的目标很明显,通过相关标准和规范的实施,基于 HTML5 的 Web 应用最终将不仅仅限于展示网页:它本可以开发各种各样的,你能想象到的所有的应用程序。 210 | 211 | 作为本节的结束,我们看看清单 6-1 中的文档用 HTML5 规范书写是什么样子的。见清单 6-2。而有关 CSS 和 JavaScript 相关的内容,将在本书第五篇“信息的计算机展现”和第六篇“计算机编程语言”中讲述。 212 | 213 | 清单 6-2 一个简单网页的 HTML5 版本 214 | 215 | ``` 216 | 217 | 218 | 219 | 220 | 221 | Hello, World! 222 | 223 | 224 | 225 |
226 |
227 |

HELLO, WORLD!

228 |
229 | 230 |
231 |

This is the first Web page in HTML5.

232 |

Click HERE to see the second Web page.

233 |
234 |
235 | 236 | 237 | ``` 238 |
239 | 240 | ## 6.4 XML 241 | 242 | XML(eXtensible Markup Language,可扩展标记语言)也是由 W3C 颁布、维护和发展的标准。XML 最早于 1998 年 2 月成为 W3C 的推荐标准。和 HTML 类似,XML 也可看成是 SGML 的子集;但和 HTML 不同的是,XML 主要用来表述复杂的结构化数据,比如某个行政单位的组织机构或者人员清单以及人员特性,如年龄、性别、学历等等。另外,HTML 由浏览器解析并展现,而 XML 的处理更为灵活,任何程序都可以利用 XML 来存储、传输自己的结构化数据。 243 | 244 | XML 和 HTML 有如下不同: 245 | 246 | - XML 不是用来替代 HTML 的,但可以看成是 HTML 的补充,主要用来存储和传输结构化数据,其表述方式和特定的软件或硬件系统无关。 247 | - XML 的标签不是预先定义好的,而需要由用户自行定义。 248 | 249 | 除此之外,XML 具有严格的语法,HTML 则更加灵活。比如清单 6-1 中,我们可以省去两个

标签,浏览器仍然能够正常处理并展示其内容,但 XML 却不行。 250 | 251 | 由于 XML 的灵活性,加上是 W3C 的推荐国际标准,因此,XML被广泛应用于不同的场合,出现在各种各样的软件中。 252 | 253 | ### 6.4.1 XML 示例 254 | 255 | 清单 6-3 给出了一个典型的 XML 文件,用来定义一个数据库表的字段。 256 | 257 | 清单 6-3 一个定义数据库表格的 XML 文件 258 | 259 | ``` 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 |
276 |
277 | ``` 278 |
279 | 280 | 和 HTML 类似,XML 使用标签(tag)来定义一个元素(也称为节点或对象),通过标签的层级关系来定义元素之间的结构,另外,标签中可以定义一个或者多个属性(attribute)。 281 | 282 | 清单 6-3 给出的 XML 文件中,第一行相当于是 XML 文件的文档类型签名,以方便程序或开发者识别这是一个 XML 文件,其中包含了 XML 标准版本号以及该文件的字符编码方式(通常就是 UTF-8)。第二行定义了一个 schema 标签,这是这个 XML 文件的根元素,类似 HTML 文件中的 html 标签;每个 XML 文件只能包含一个根元素。随后,该文件使用 table 标签定义了一个数据库表格,其名称为 foo\_bar(使用 table标签的 name 属性定义),在 table 标签内,使用 field 标签定义了三个字段,以表明字段是表格的子元素。类似地,通过 field标签的属性定义了三个字段的名称、类型等特性[^7]: 283 | 284 | - 字段的名称使用 field 标签的 name 属性定义。 285 | - 字段的数据类型使用 filed 标签的 type 属性定义,其值为 I 表示是整数类型,其值为 C 表示是字符类型。当类型为 C 时,使用 size 属性定义其长度。比如上述示例定义的 id 字段是整数类型,foo 字段也是整数类型,但 bar 字段是字符类型,长度为 32。 286 | 287 | 需要注意的是,上述示例 XML 文件中,filed 标签中还包含有 key、unsigned、notnull、default 等子标签。理论上讲,这些子标签仍然定义的是数据库字段的特性,分别对应主键、无符号整数、非空、默认值等。但在上面的例子中,使用了子标签来定义这些数据库字段的特性。但我们也可以使用下面这种方式来定义 foo 这个数据库字段: 288 | 289 | ``` 290 | 291 | foo 292 | 293 | 294 | ``` 295 |
296 | 297 | 因此,除了上面所说标签是由开发者自行定义的之外,使用 XML 来定义结构化数据时,我们可以自行定义数据的结构化层级关系以及元素属性。比如定义一个字段时,我们可以将名称、类型、主键、无符号、默认值等特性作为字段的子元素来定义,也可以使用标签属性来定义。当然,结构化数据的层级关系往往是比较清晰的,但就特定的元素特性来讲,哪些适合定义为元素属性,哪些适合定义为子元素,则没有统一的准则,大部分情况下由特定 XML 文件格式的开发者自行确定。 298 | 299 | 比如上面给出的定义 foo 这个数据库字段的两种方式中,我们始终使用标签属性 type来定义数据库字段的类型,这是因为数据库字段的类型是固定的,而不是任意值,通过标签属性定义类型,可帮助我们使用 XML 的严格语法检查机制来查验文档的合法性。这涉及到 DTD(Document Type Definition,文档类型定义)的概念。 300 | 301 | ### 6.4.2 DTD 302 | 303 | 如果要使用 DTD 来验证一个 XML 文档的合法性,则需要在 XML 文档的头部增加 DOCTYPE 定义。本质上,DTD 定义了一个 XML 文档如下几个方面的规则: 304 | 305 | - 根元素名称。在我们的数据库表格示例中,根元素为 schema。 306 | - 元素之间的嵌套规则,比如一个元素可以包含什么样的子元素、哪些子元素是可选的等等。以清单 6-2 为例,根元素 schema 中可包含一个或多个 table 元素,table 元素中要至少包含一个 field 元素,而 filed 元素中可包含多个可选子元素,如 notnull、key 等等。 307 | - 有效的元素属性、属性默认值、属性的值类型(如字符串、枚举量等)。以清单 6-2 为例,filed 元素的属性有 name、type、size 等:name 属性是必须定义的,类型为字符串;type 属性是必须定义的,类型为枚举量;size 属性不是必须的,类型为字符串。 308 | 309 | DTD 的定义相对来讲比较复杂,比如就清单 6-3 所示的 XML 文档,对应的 DTD 大致如清单 6-4 所示。 310 | 311 | 清单 6-4 数据库表格的 XML 描述文档对应的文档类型描述 312 | 313 | ``` 314 | 315 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 330 | 331 | ]> 332 | 333 | ... 334 | 335 | ``` 336 | 337 | 清单 6-4 中,大写的单词是 DTD 的保留字,读者根据保留字的英文含义,大致就可以看明白 DTD 的定义方法。需要注意的是,清单 6-4 仅仅给出了针对清单 6-3 示例 XML 文档的最小文档类型定义;一个完整用于描述数据库表格格式的 XML 文档之 DTD,要比清单 6-4 复杂很多。清单 6-3 这个示例其实来自于 ADOdb XML Schema,其完整描述可见网页:[http://phplens.com/lens/adodb/docs-datadict.htm#xmlschema](http://phplens.com/lens/adodb/docs-datadict.htm#xmlschema),对应的 DTD 见清单 6-5。 338 | 339 | 清单 6-5 ADOdb XML Schema 对应的 DTD 340 | 341 | ``` 342 | 343 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 368 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 387 | 388 | 389 | 395 | ] > 396 | ``` 397 |
398 | 399 | 另外,我们也可以在 XML 文档中引用单独存放的 DTD 文件,而无需在 XML 文档中包含完整的 DTD 定义。比如针对上面的示例,我们可以将清单 6-4 中的 DTD 保存为 adodb\_xml\_schema.dtd 文件,并保存在互联网上,然后使用下面的方式访问这个 DTD 文件: 400 | 401 | ``` 402 | 403 | 404 | ``` 405 |
406 | 407 | 限于篇幅,本书不打算详细讲述 DTD 的细节,有兴趣的读者可自行参阅相关书籍或技术文档。 408 | 409 | ### 6.4.3 XML 的应用 410 | 411 | 由于 XML 可用来定义复杂的结构化数据,且具有较好的人机共读特性,因此,XML 得到了广泛应用。比如上面的示例使用 XML 文档描述数据库结构,而在 Android 系统中,XML 被用来描述图形用户界面,也被用来定义本地化字符串。但使用 XML 最多的场合则是 Web 服务。所谓 Web 服务,也是通过 HTTP 协议提供的,只是其返回的内容格式不是通常的 HTML,而是 XML;HTML 给浏览器使用,最终为普通用户渲染出图文并茂的网页,XML 则是提供给另外一个计算机程序使用的,由这个程序做进一步的处理。也就是说,我们可以这样理解 XML 的出现:XML 是为了将服务器可以提供的数据通过万维网传输给其他计算机(或程序)使用而发明的。知道了这一点,我们也就很容易理解为什么 XML 的语法要比 HTML 严格很多,当然也更加灵活。 412 | 413 | 因此,在 Web 服务大行其道的今天,大部分 Web 服务提供的结果是通过 XML 来描述的。然而,随着 JSON 的流行,一些新开发出来的 Web 服务,不再使用 XML 来描述结果,而使用JSON 描述结果。 414 | 415 | ## 6.5 JSON 416 | 417 | 和 XML 类似,JSON(JavaScript Object Notation,JavaScript 对象表示法)是一种轻量级的数据交换格式。但 JSON 明显比 XML 具有更好的特性:易于人阅读和编写(JSON 不使用尖括号和标签),同时也易于计算机解析和生成。从 JSON 的名称可知,这种结构化数据的表示法来源于 JavaScript 编程语言,也就是 Web 应用的唯一编程语言,这个编程语言以易学易用著称。 418 | 419 | JSON 采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族(包括C、C++、C\#、Java、JavaScript、Perl、Python 等)的习惯。这些特性使得 JSON 成为理想的数据交换格式。JSON 的构造基于如下两种基本形式: 420 | 421 | - 名称/值对的集合。在不同的编程语言中,“名称/值对”被理解为对象、记录、结构、字典、哈希表、键值对或者关联数组。 422 | - 有序的值列表。即大部分编程语言所指的数组。 423 | 424 | 这些都是各种计算机编程语言中常见的数据结构。事实上大部分现代计算机编程语言都以某种形式支持这些数据结构。因此,JSON 描述的数据在同样基于这些结构的编程语言之间交换时非常方便。 425 | 426 | 清单 6-6 给出了一个用 JSON 描述结构化数据的示例。这个示例用 JSON 描述了清单 6-3 中使用 XML 描述的数据库表 foo\_bar 的结构。 427 | 428 | 清单 6-6 用 JSON 描述数据库表的结构 429 | 430 | ``` 431 | { 432 | name: "foo_bar", 433 | fields: [ 434 | {name: "id", type: "I", key: "yes", unsigned: "yes"}, 435 | {name: "foo", type: "I", notnull: "yes", default: 0}, 436 | {name: "bar", type: "C", size: 32, notnull: "yes", default: ""} 437 | ] 438 | } 439 | ``` 440 |
441 | 442 | 对比清单 6-3 和清单 6-6,我们可以看到使用 JSON 描述的数据库表格的结构更加清晰。一个 JSON 对应于一个对象,一个对象可以有多个属性,每个属性有其名称和值。而属性的值可以是字符串、数值或者数组,也可以是另外一个对象,除此之外,还可以有 true(真)、false(假)、null(空) 等三个特殊值。比如我们定义 foo\_bar 这个数据库表格时,对应的 JSON 对象中包含两个属性,用花括号({})包围:一个是 name,用来表示表格的名称,其值为字符串“foo\_bar”;一个是 fields,用来定义表格的字段,其值为数组,用中括号([])包围。 443 | 444 | fields 包含三个字段,每个字段又是一个 JSON 对象,每个对象用花括号({})包围,中间用逗号(,)分隔。这些描述字段的对象有 name、type、key、unsigned、notnull、default 等属性,分别使用字符串、数值等作为相应属性的值。 445 | 446 | 需要注意的是,清单 6-6 在定义不同的字段时,这些字段对象的属性并不完全一样。在 JavaScript 中,未定义的属性对应的值是 undefined,这个值和空值(NULL)不同,而在其他编程语言中,可能无法区分这两种情况。因此,程序在解析上述 JSON 数据时,需要注意这个区别。当然,我们也可以强制所有的字段对象都具有相同的属性,并对这些额外的属性取默认值,如清单 6-7 所示。 447 | 448 | 清单 6-7 用 JSON 描述数据库表的结构(增强版) 449 | 450 | ``` 451 | { 452 | name: "foo_bar", 453 | fields: [ 454 | {name: "id", type: "I", size: null, key: true, unsigned: true, 455 | notnull: true, default: 0}, 456 | {name: "foo", type: "I", size: null, key: false, unsigned: false, 457 | notnull: true, default: 0}, 458 | {name: "bar", type: "C", size: 32, key: false, unsigned: null, 459 | notnull: true, default: ""} 460 | ] 461 | } 462 | ``` 463 | 464 | 清单 6-8 给出了一个真实的 Web 服务接口返回的 JSON 数据示例。这个 Web 服务接口返回指定国家的编码、英文名称、标准简写以及已知区域的本地化名称。需要特别说明的是 localizaed\_name 对应的字符串值使用了 \u 作为前缀,这指 \u 之后定义了一个 UNICODE 字符,用四个十六进制字符表示,是 UNICODE 字符的内码值。 465 | 466 | 清单 6-8 用 JSON 描述阿富汗的国家码以及不同区域下的名称 467 | 468 | ``` 469 | { 470 | "status": "ok", 471 | "endpoint": "/list/countries/item_detail/{token}/{numeric_code}", 472 | "items": { 473 | "numeric_code": "4", 474 | "name": "Afghanistan", 475 | "alpha_2_code": "AF", 476 | "alpha_3_code": "AFG" 477 | }, 478 | "extras": [ 479 | { 480 | "locale": "en", 481 | "localized_name": "Afghanistan" 482 | }, 483 | { 484 | "locale": "zh", 485 | "localized_name": "\u963f\u5bcc\u6c57" 486 | }, 487 | { 488 | "locale": "zh_CN", 489 | "localized_name": "\u963f\u5bcc\u6c57" 490 | }, 491 | { 492 | "locale": "zh_HK", 493 | "localized_name": "\u963f\u5bcc\u6c57" 494 | }, 495 | { 496 | "locale": "zh_TW", 497 | "localized_name": "\u963f\u5bcc\u6c57" 498 | } 499 | ], 500 | "elapsed_time": "0.0088", 501 | "memory_usag": "0.65MB" 502 | } 503 | ``` 504 |
505 | 506 | 如前所述,由于使用 JSON 描述结构化数据时简洁的表达方式、便于各种各样系统的处理、便于人机共读的这些特点,JSON 已有取代 XML 的趋势,很多新系统已经开始仅提供 JSON 格式的数据而不再提供 XML 格式的数据了。 507 | 508 |
509 | 510 | [^1]\: 有关文字输出的双向排版问题,将在本书第五篇“信息的计算机展现”中讲述。 511 | 512 | [^2]: 亦定义有针对不同处理类型的环境变量 LC\_COLLATE(字符串排序规则,用于正则表达式)、LC\_CTYPE(字符类型,用于正则表达式)、LC\_MESSAGES(本地化字符串)、LC\_MONETARY(用于货币格式化)、LC\_NUMERIC(用于数字格式化)、LC\_TIME(用于时间和日期的格式化)。 513 | 514 | [^3]: 亦简写为 i18n,其中的 18 表示 I 和 n 之间有 18 个字母。 515 | 516 | [^4]: UTC 是 Universal Time Coordinated(协调世界时)的简写,即格林威治时间。 517 | 518 | [^5]: 需要注意的是,从操作系统软件栈功能的划分角度讲,时区并不是由操作系统内核维护的,而是由标准函数库维护的。由于 gettimeofday/settimeofday 这两个接口涉及到了时区的获取和设置功能,因此在后来的标准修订中不再推荐使用这两个接口,转而推荐使用 clock\_gettime 等接口。 519 | 520 | [^6]: 感兴趣的读者可查阅 W3C 官方网站:http://www.w3.org。 521 | 522 | [^7]: 在这个小节,请注意特性(Property)和属性(Attribute)这两个术语的使用场合。前者泛指元素或对象的性质、特性,如人员年龄、性别,后者特指 XML 的标签属性。 523 | -------------------------------------------------------------------------------- /textbook/part-1-chapter-7.md: -------------------------------------------------------------------------------- 1 | # 第 7 章 结语:标准的产生和演进 2 | 3 | 在本篇中,我们讲述了计算机如何表述各种各样的信息类型,而这些表述方式,大部分会成为标准或者规范,以方便不同的计算机系统之间交换数据。本章作为本篇的总结,将探讨标准的产生和演进中的一些规律性主题。 4 | 5 | ## 7.1 标准的产生和制定 6 | 7 | 本篇我们了解了浮点数、UNICODE、JPEG/MPEG/H.26x、HTML/XML 等标准。这些标准最终由国际化的标准化组织通过各种各样的工作小组形成草案,经过讨论和修订而变成最终的标准。这些标准化组织往往是国际化的非营利性组织,比如 IEEE(电气和电子工程师协会)、ISO(国际标准化组织)、ITU(国际电信联盟)、W3C 等。 8 | 9 | 一般而言,一个标准形成之前,相应的技术都会有一个实现雏形。通过局部市场的检验,可证明相关技术的确是有其市场需求的,这时,将相应的技术进行标准化的工作就会提上标准化组织的日程。由于标准的制定者往往是国际顶尖的专家,故而最终的标准会超越当初的实现雏形。通过标准化组织对某项标准的推行,产业界会形成一个针对该标准的实施合力,相应的实施会极大促进产业的发展:大家分工协作,各赚各的钱。 10 | 这一规律在音视频编码技术的标准演进过程中表现得淋漓尽致。最初,影视界、信息技术界(微软、苹果、Adobe 为代表)的厂商或标准化组织为各自的产业制定有相应的标准(NTSC/PAL、AVI 等),随着技术的融合,MPEG 出现了,之后产业界形成了良好的配合关系。有厂商收取相关技术的专利费,其他厂商设计和生产音视频编解码器,而软件厂商则整合这些技术为消费者所用。 11 | 12 | 那些不符合以上规律而产生的标准,就可能会陷入不被市场买账的窘境。就像 XHTML 这个由 W3C 闭门造车开发出来的标准一样。 13 | 14 | 和计算机软件技术相关的标准的形成中,还有一个重要的玩家,即开源社区。由于某些原因,某些标准(或者事实上的工业标准),会存在专利或者高科技禁运限制。开源组织的存在,则会通过公益事业的方式来制定一些标准来对抗此类标准。如自由软件基金会制定 PNG 格式来对抗 GIF 专利,Xiph.Org 基金会通过发展免费、开源的音视频编码技术来对抗某些专有的音视频编码技术等等。 15 | 16 | 在标准的演进中,针对同一技术的不同标准之间的竞争也会给技术和产业的发展带来良好的促进作用。比如音视频编码技术中 MPEG 和 X.26x 系列标准之间的竞争关系。这种竞争关系此消彼长,有时候会通过互相借鉴而促进彼此发展。但有时候会很残酷,身处其中的厂商一旦选择了一种日后会被市场抛弃的标准,对该厂商来讲将是致命的。比如 HD DVD 和蓝光光盘(BD,Blue-ray Disc)之间的竞争,最后以蓝光光盘取胜而结束。再比如,比如移动视频广播(手机电视)领域,国内曾出现过 CMMB 和 TMMB 两个标准,最终 CMMB 获胜。不过遗憾的是,随着智能手机的普及和无线通信带宽的提高,手机电视最终失去了市场地位。 17 | 18 | ## 7.2 技术标准的演进规律 19 | 20 | 工业界的标准,往往会对某个领域的发展带来很大的促进作用,在计算机领域也不例外。比如 MPEG 系列标准的产生,带来了影音产业的革命性发展,产生了 VCD、DVD、MP3 播放器、MP4 播放器等等数码产品,将卡式录音机、磁带录像机埋进了坟墓,同时也带动了模拟电视系统向数字电视系统的演进。 21 | 22 | 在标准的演进过程中,每一次的技术进步几乎会重建整个工业界,一方面会给很多企业带来新的发展机遇,也会让很多企业快速丧失原本拥有的优势。比如苹果公司,在本世纪初瞄准了在线音乐市场,制造了时尚的 iPod 产品,并进而在 2007 年重新定义了智能手机,iPhone 随之大卖,最终将苹果公司推向了全球市值最高的上市公司行列。反例则是中国众多的 VCD、DVD 播放机制造企业。在 VCD 取代磁带录像机的那几年(上个世纪90年代晚期),造就了以“爱多”为代表的 VCD 播放机制造产业,但在随后的 DVD 时代,“爱多”等播放机企业纷纷倒下,未能赶上时代的发展。 23 | 24 | 标准的更迭速度往往在相关标准产生的前几年比较高。比如 MPEG-1 到 MPEG-2 标准,中间只相隔两年时间,而从 MPEG-2 到 MPEG-4 的演进,中间经过了四年时间。相同的情况也发生在 HTML 标准的演进过程中,从 HTML 2.0 到 HTML 3.2 仅用了两年时间,而从 HTML 4.01 到 HTML5,则花费了十五年时间。 25 | 26 | 这种现象是可以理解的,毕竟在标准刚出现之时,可能尚不成熟,版本的迭代速度自然会比较快,而随着标准变得更加成熟,向下个版本的演进时间会随之加长。这种现象值得产业界深思。对一个企业来讲,紧盯标准的发展动向,按照标准的演进规律及时更新产品是非常必要的,若不思进取,可能会像“爱多”等品牌一样,随着新技术的普及而迅速倒下。 27 | 28 | 另外,标准的产生往往滞后于市场需求。比如在文字编码标准的演进过程中,在 UNICODE 还没有成为国际标准,且没有得到广泛认同的情况下,相关企业或者国家会自行制定自己的文字编码标准。最典型的当属 GB2312 字符集,由于 GB2312 字符集定义的码位太少,无法适应互联网、字处理、出版等行业的市场需求,Windows 操作系统的开发者微软为了在中国大陆推广自己产品,不得不自行扩充 GB2312 字符集,形成了 GBK 字符集的雏形。类似的情况也发生在 HTML 标准的演进过程中,在 HTML5 标准尚未正式颁布之前,很多浏览器的开发参照标准的草案进行,或者按照对厂商有利的方式自行演进。比如微软在 IE 中引入的 ActiveX 控件,Adobe 的 Flash 插件等等。但这些技术在实际产品中的应用,最终也会影响标准的形成。比如 Adobe Flash 插件间接导致了 HTML5 中多媒体标签以及 Web Socket 等的引入。 29 | 30 | 某些标准会因为无法得到产业界的认可而最终丧失其地位,典型的如 HTML 演进过程中产生的 XHTML。还有杂七杂八的图像格式,如 PCX、TIFF 等等(尽管这些图像格式并不是如 UNICODE 那样的国际标准,但也是某个平台或者特定软件或系统环境中的标准)。另外如中国政府定义的 GB2312 字符集标准,很快被 GBK 以及 GB18030、UNICODE 替代。这些容易丧失地位的标准,往往具有如下的特征: 31 | 32 | - 某个软件厂商为自己的软件系统制定的标准,如 Adobe 的 Flash,微软的 AVI、ASF 等视频编码及格式。这些标准往往为封闭的市场开发,局限性较多,容易被开放的、跨平台的标准或技术取代。 33 | 34 | - 区域性组织(如政府)制定的标准,如各国各地区政府制定的字符集标准。这些标准在相关技术的发展和演进过程中占有一定的历史地位,但最终会被国际性的标准替代。 35 | 36 | - 标准制定者无视市场状况一厢情愿制定的标准,典型的如 XHTML。 37 | 38 | 当然,随着技术的发展,某些标准会丧失对应的软硬件载体或者市场需求,从而逐步淡出历史,比如 MPEG-1。虽然 MPEG-1 Layer 3(MP3)音频编码技术的生命力要强一些,但相信最终会被 AAC 替代。 39 | 40 | ## 7.3 可扩展性及兼容性设计 41 | 42 | 如前所述,每个标准都要经历一个从不太成熟走向成熟的演进过程。因此,在新的标准成型之前,必须考虑的一个重要问题就是向前的兼容性设计。比如字符集的设计,所有其后的字符集及编码技术,都要兼容 ASCII,而 GB18030 字符集也要兼容 GB2312 字符集。为了实现向前的兼容性,任何标准在设计时都要做可扩展性考虑。 43 | 44 | 比如 ASCII 字符集,假如在一开始时就将一个字节的全部八位都用光,定义 256 个字符,那其后的所有字符集及其编码都没法和 ASCII 兼容。同样的设计也体现在 UNICODE 字符集的演进过程中;而 UNICODE 的 UTF-8 编码设计,则完美展现了可扩展性及兼容性之间的平衡艺术。 45 | 46 | 类似地,MPEG-1 到 MPEG-2 的演进也充分考虑了兼容性,DVD 播放机可以支持 VCD 盘片的播放,而不需要做额外的设计,比如专门为 VCD 盘片设计一个独立的盘片仓。 47 | 48 | 当然,随着技术的发展,兼容性可能会带来巨大的历史包袱,使得相关的技术变得复杂而笨重。这时,就需要打破兼容性。打破兼容性的代价是巨大的,这需要标准制定者做出取舍。如果标准制定者不能顺应历史潮流,则可能会被其他标准替代。 49 | 50 | 不过,一般情况下,兼容性要比先进性重要得多。比如 W3C 的 XHTML 最终没有战胜市场需要的兼容性。而只有在新的市场出现时,新的标准才可能有机会全面替代不兼容的老标准。比如移动互联网的出现,让 H.264 替代 MPEG 成为事实上的移动互联网视频编码标准。那么正在快速普及的 4K 超高清视频系统,会不会让 X.265 标准独步天下呢?这还待市场检验。 51 | 52 | ## 7.4 提升综合实力,积极参与国际标准的制定 53 | 54 | 由于历史原因,我国在计算机和通信领域的技术基础相当薄弱,我们很少有企业有资格参与到上面提到的这些标准制定当中。 55 | 56 | 尽管大部分制定国际化标准的组织是非营利性的,但这些组织的人员却主要来自于企业,尤其是欧美日的跨国公司,如微软、苹果、IBM、英特尔、爱立信、索尼等。既然参与标准制定的专家背后是企业,自然就会有利益之争。可想而知,标准的制定过程其实是划定相关企业在市场上的势力范围的过程,这和当年列强瓜分中国的情形是差不多的。具体而言,就是很多标准中包含有很多专利技术,而这些专利技术是由企业拥有的。显然,一旦某些专利技术被纳入标准之中,那对应的企业就可以坐收专利许可费了。 57 | 58 | 因此,参与到国际化标准的制定过程中,对企业,对国家都是有利的。但问题是,一个国家或者企业的实力不够时,将很难获得相应的资格来参与到标准的制定当中,毕竟游戏规则是人家定的。 59 | 60 | 在实力达不到的情况下,政府会通过一些政治或者行政手段来保护自己的企业或产业。比如,通过自己的市场地位来换取一定的资格。这一点在无线通信领域的 3G 标准竞争中表现非常明显。TD-SCDMA 是由我们国家主导发展的 3G 标准,在技术和产业化水平上,其成熟度和 WCDMA、CDMA2000 等由欧美国家提出的标准相差很远。但将 TD-SCDMA 变成一项国际化标准,则可以打破无线通信领域的标准由欧美企业长期把持的现状,从而有利于我们国家未来在无线通信领域的话语权。处于此项考虑,经过艰难和长期的博弈,TD-SCDMA 成了国际任何的 3G 标准之一,而中国政府同意针对其他两个标准放开频段资源,由相关企业自主选择 3G 标准。这明显是一种利益的交换和平衡。 61 | 62 | 尽管后来由中国移动发展的 TD-SCDMA 3G 网络效果并不好,但自此后中国企业在无线通信领域的标准话语权得到了很大的提升。在 4G 无线通信标准中,中国相关企业拥有了越来越多的核心专利,而在面向未来的 5G 无线通信标准的研发中,据悉中国的华为公司占据了非常强势的地位。相同的故事也发生在视频编码技术领域,在 X.265 视频编码技术中,华为公司也占据了主导地位。 63 | 64 | 与此同时,中国的互联网相关企业,如阿里巴巴、腾讯、百度等,也开始重视标准的重要性,加入了 W3C 等组织当中。 65 | 66 | -------------------------------------------------------------------------------- /textbook/preface.md: -------------------------------------------------------------------------------- 1 | @SUBJECT [计算机软件技术导论](toc.md) 2 | @AUTHOR [魏永明](https://github.com/VincentWei) 3 | @DATE 2015-xx-xx 4 | @LANG 简体中文 5 | @COPYING 版权所有 © 2022 魏永明 6 | @LICENSE 保留所有权利 7 | 8 | # 前言 9 | 10 | ## 计算机是何物? 11 | 12 | 我们通常意义上所讲的“计算机”,其全名应为“数字计算机[^1]”,或者“电子计算机”。“电子计算机”这个名称强调当前计算机的构造方法,即通过电子的晶体管来构建计算机。这不同于通过机械的方法,或者前沿的量子的方法来构建计算机。和电子计算机这个名称相比,“数字计算机”则可以更加一般性地定义计算机,这个名称强调如下事实: 13 | 14 | 计算机本质上处理的是数,而且是离散的数字。通过有限的离散的数,我们可以运用代数运算来近似地描述连续的数学公式,从而帮助人们进行运算。通过适当的编码,我们可以使用这些离散的数来表示其他现实世界中的东西,比如文字、声音、图像等等。而这些统统可被称为“信息[^2]”。 15 | 16 | 从理论讲,不论我们如何构造计算机,最终我们只能从离散的数字角度来模拟真实的世界。比如,我们无法用包括计算机在内的任何仪器来完整描述一个无理数,尽管理论上讲无理数的数量远远大于有理数,更不用说整数或者自然数了。因此,不管是电子的、机械的(如算盘或者巴贝奇[^3]在十九世纪计划用蒸汽机及齿轮构建的分析机)还是量子的计算机,最终我们只能有限地描述世界。但这已经足够,因为从哲学上讲,真实的世界其实也不是模拟的,因为数学上的不确定性以及量子角度的不确定性都说明了这个现实:我们其实生活在离散的宇宙中,而不是连续的、模拟的宇宙中,即使是时间,本质上也是离散的[^4]。 17 | 18 | 因此,把握上述这个本质,我们就可以正确认识计算机——计算机的本质特征可以概括为如下两点: 19 | 20 | - 第一, 离散的数字。因为数学上的不确定性以及物理上的不确定性,期望一台机器可以表述连续的数学世界是不现实也是不可能的。因此,不论是什么样的仪器,其最终可处理的东西只能是离散的数字,计算机也不例外。计算机的计算能力以及容量都取决于计算机可以方便地表述多少个离散的数字。我们日常所说的 8 位、16 位或者 32 位、64 位计算机,是从信息最基础的度量单位“位(Bit)”的角度讲的;8位的计算机总共有 28(256)个离散的数字,而 64 位的计算机,则总共有 264(18446744073709551616)个离散的数字。后者的容量或者计算能力显然要比前者强很多倍。 21 | - 第二, 编码。通过编码,我们可以用离散的数字表述现实世界中的各种各样的东西,包括数据、文字、声音、图像等等。比如,我正在使用笔记本电脑敲进去的这本书,所使用过的汉字(包括英文字母、数字、标点等,可统称为字符)大致不会超过 5000 个,我们为每个字符编一个号,就可以用一个数字序列来表示这本书的内容。实质上,计算机内部也是这么做的,当然情况要复杂一些。编码还有一个好处就是,我们可以针对这些数字做各种各样的数学运算或者操作,比如可以统计这个数字序列的长度,再乘以0.01,结果差不多就是我可以通过出版这本书拿到的每本书的版权收益。这个过程本质上也是编码,因为我们可以将统计字数和计算版税的动作也编个号,建立它们之间的一个映射关系。这种包括运算和逻辑在内的编码,也可以称为“算法”。 22 | 23 | 上面这两个计算机的本质特征,对所有类型的计算机都是适用的。比如对人和算盘形成的“计算机”而言,显然其只能表述有限的数字,其容量取决于算盘的长度(或有多少个珠子),其编码就是算盘的数字表达规则(上面的珠子一个表示 5,下面的珠子一个表示 1)以及操作人掌握的口诀及计算公式。就其他形式的计算机来讲,不论电子的计算机在多大程度上或以多快的速度被量子的计算机取代,上面这两个本质特征都不会发生变化。 24 | 25 | 更进一步,我们可以将物理的计算机本身称为“硬件”,而编码以及编码的方法称为“软件”。如此一来,硬件是变化的(从机械到电子再到量子或者将来的任何可能形态),但软件则是“永恒”的(上面两个本质不会因为硬件的变化而变化)。另外要强调的是,我们通常所讲的“软件”,的确更多地和上述计算机的本质特征中的第二点(编码),发生更直接的关联。 26 | 27 | 于是,我们可以得出一个结论:软件才是计算机科学的灵魂,硬件只是一种载体、一种媒介。即使我们没有硬件,也可以编码(编程),就像艾达[^5]为巴贝特的分析机编程一样(分析机从来没有建成过,但艾达这位天才女性却为之编写了程序),或者数学家为仅在思维中存在的图灵机[^6]编程一样。 28 | 29 | 本书将从信息的计算机处理(表述、存储、计算、传输、展现等)这一角度来为读者讲述计算机软件的本质及方法。这从某种程度上可称之为“计算机软件方法论”。 30 | 31 | ## 计算机软件 32 | 33 | 如上所述,计算机软件实质上就是对各种计算机的编码方法及规则的统称。 34 | 35 | 笔者最初建立的“计算机软件”概念,是将一个程序从一张五英寸的软磁盘上复制到 IBM 8088 PC 机上然后通过 DOS 操作系统执行该程序。你会听到一段简陋的音乐,或者进入一个游戏场景,然后通过上下左右键等按键来控制游戏中的人或者枪。那时,笔者将这种称之为“程序”的东西视作“计算机软件”。但随着对计算机了解的深入,笔者发现,对计算机而言,软件就是除了硬件之外的所有东西,没有软件,PC 机也好,服务器也好,甚至我们现在日常使用的智能手机都将失去作用——这些机器什么都做不了。 36 | 37 | 本质上讲,计算机软件是指可在计算机中执行的程序集合。“程序”又可以看成是之前所说的某种编码,尤其指包含有计算和处理逻辑的编码序列。说白了,就是一堆数据,但数据是有序的。 38 | 神奇之处在于,这堆数据通过在计算机中执行,就可以完成很多事情,简单的如求平方根,复杂的如输出乐曲、编辑和打印文本等,而这一切由前述的编码以及编码的规则所定义。计算机则忠实执行给定的指令或编码序列,从而完成我们期望的任务。 39 | 40 | 运行在现代意义上的计算机之软件,根据其功能的不同,可以划分一种分层的堆栈结构。最底下的软件通常称为“固件(firmware)”,用于计算机的启动及自检(检查计算机的外设是否工作正常并执行初始化),其上的那层软件称为“操作系统(operating system)”,再往上的软件称为“应用程序(application)”。读到这里,读者应该可以明白本书引言中提到的“全栈工程师”这个名称的来由及其比较具体的含义。 41 | 42 | 图 1 通用操作系统的软件栈 43 | 44 | 在智能手机、平板电脑等针对个人的计算机设备上,为方便程序开发或者做系统保护,人们又设计了一层新的栈,这一层软件运行在传统的操作系统之上,被称为“框架(framework)”,而应用程序又被简称为“应用(app)”。框架对应用程序来讲,其实是一种限制,它限制了应用程序可以做什么,不能做什么。比如在 Android或者 iOS 系统上,一个应用不能随意访问所有的系统资源,以此来保护设备的基本功能不被病毒或者恶意程序所破坏,或者保护用户的隐私不被轻易泄露[^7]。这大概是应用(app)这个词从应用程序(application)这个词被截断而创造出来的原因。 45 | 46 | 图 2 智能操作系统的软件栈 47 | 48 | 在更为简单的计算机系统中,比如智能手机出现之前的功能手机(feature phone)或者一些简单的智能化电子设备(如电子词典)中,软件的栈式结构可能没有那么清晰,毕竟这种设备可以完成的功能是有限的。但开发人员会在此类简单的计算机系统中有意或无意地使用类似的栈式分层概念,以使得自己的软件结构变得清晰和可维护。 49 | 50 | 那么,为什么计算机软件要使用这种分层设计的方法呢?这要从计算机软件的演进历史谈起。 51 | 计算机软件的演进 52 | 53 | 现代意义上的电子计算机出现在上个世纪二战期间(1940年代)。那时,人们通过在纸带上打眼的方式表示数据和指令(即编码),然后计算机读取纸带完成计算。我们可以将纸带上的编码序列看成是对应的计算机软件,或程序。那时及其后很长一段时间,人们编写的计算机程序中全是计算机可以直接识别的指令及其要运算的数据,而且是二进制的[^8]。此时的程序如果写在纸上,大概是这个样子的: 54 | 55 | ``` 56 | 00000001 57 | 00000000 58 | 00000010 59 | 00000001 60 | ``` 61 | 62 | 上面这段程序是我虚构的,大致是说将0作为当前操作的数字(指令00000001),将1加到当前操作数的值上并将结果保存起来(指令00000010)。 63 | 64 | 二进制表示的数据(比如75,其二进制表述为01001011)显然不易识别,且容易出现差错。聪明的人立即可以想到,既然计算机的处理能力那么快,为什么不让它做更多的工作?比如我们可使用更加易读的数字表示方式,如十六进制来表示数字(75 用十六进制表示就是 4B),然后让计算机自己将该十六进制的数字变成二进制并执行不就可以了吗?如此以来,程序将更为简洁,且易读易维护。于是,我们可以将上面的程序写成下面这个样子: 65 | 66 | ``` 67 | 01 00 68 | 02 10 69 | ``` 70 | 71 | 如果我们将用于表示指令的编码使用更加易读的文字短语来表示,那这个程序可以更进一步写成: 72 | 73 | ``` 74 | SET 0 75 | ADD 1 76 | ``` 77 | 78 | 上面这段程序使用了更加复杂的编码映射关系,引入了来自人类语言的单词(SET、ADD)来表示计算机要完成的工作,因此,这种程序被称为“汇编”。因为使用了来自人类语言的单词或类似的语法,又称为“计算机编程语言”。汇编语言的诞生极大提高了编写计算机程序的效率,而最大的好处就是,我们可以借此编写更加复杂的程序。 79 | 80 | 另外,善于抽象和总结的人们又进一步意识到,我们还可以用更加接近人类的方式编写计算机程序,从而更容易地完成编程任务和表达复杂算法。于是,FORTRAN 等高级的编程语言被设计出来。这种符合特定规则(词法和语法)的编程语言被称为“高级编程语言”。比如上面的程序,可以使用高级编程语言写成: 81 | 82 | ``` 83 | a = 0 84 | a = a +1 85 | ``` 86 | 87 | 通过使用高级编程语言,人们可以使用类似人类语言的方式为计算机编写程序。当然,这种方法编写的程序,是不能直接被计算机所识别并执行的,而需要经过解释或者编译。 88 | 89 | 解释的过程就是将使用编程语言编写的程序直接读取并按照相应的规则翻译成计算机课识别的指令。汇编语言最为简单,只要建立所使用单词和给定计算机指令代码之间的映射关系即可,几乎不涉及语法规则问题,而其他高级语言的解释显然更为复杂。 90 | 91 | 编译的过程则将高级编程语言编写的程序先整个进行翻译并最终生成可被计算机直接运行的编码序列,然后将该编码序列交给计算机执行。 92 | 93 | 从执行效率上讲,对同一种语言而言,显然编译然后执行要比边解释边执行要快。 94 | 95 | 编程语言的出现让计算机编程变成一件非常惬意的工作,实现同一功能但使用更加精巧、更加快速的程序也变成了可以向他人炫耀的资本。最终的结果是,人类的智慧可以更好地融入到程序当中。这一方面极大提高了生产效率,另一方面也推动了计算机科学自身的发展。 96 | 97 | 上个世纪70年代,计算机科学家们开始设计一种称为“操作系统”的计算机程序。这种计算机程序为计算机和程序员之间提供了一种交互能力,借此人们还可以充分使用计算机的计算能力。编写程序的工作绝大部分情况下需要相当长的时间,而计算机运行起来却很快。因此,计算机作为一个庞然大物,大部分时间是在等待人们输入新的程序。这明显是个非常大的浪费。因此,计算机科学家们开始琢磨,是否可以让许多人同时共用一台计算机? 98 | 99 | 于是,作为美国军方和贝尔实验室一个计划的一部分,UNIX 操作系统被设计出来,同时被设计出来的还有 C 语言。而设计 UNIX 的两个主要工程师[^9],则在后来被授予图灵奖——这个计算机科学领域的最高成就奖。 100 | 101 | UNIX 操作系统对管理现代电子计算机的资源(计算能力、存储等)带来了划时代的影响,不论是其后出现的 BSD、Linux 等 UNIX 变种操作系统还是微软公司的 Windows 系列操作系统,都沿用了其中的大部分概念,如进程、文件、目录树等等,甚至最近出现的 Android、iOS 等面向智能设备的操作系统也无出其左右。 102 | 103 | 操作系统,将计算机硬件的处理器、存储等组成单元抽象为对应的程序对象,如进程、文件系统、文件等,一方面将上层的计算机程序和繁杂的底层汇编级操作隔离开来,另一方面,文件、管道等的引入,使得程序间的协作变成可能。 104 | 105 | 有了操作系统,我们可以使用自己喜欢的任意编程语言编写程序,然后借助解释器或者编译器编译后执行自己的程序。我们编写程序的方法也从直接操作硬件变成了读取和写入文件这种抽象的操作,而且无需关心计算机系统资源(如处理器、内存)等的使用,操作系统会帮我们管理这些资源,在不同的正在运行的程序(进程)之间进行调配。 106 | 107 | 那么,分层设计到底给计算机程序带来了什么?为什么会出现一个称为“操作系统”的软件?在笔者看来,本质上,操作系统实现了如下两个关键功能: 108 | 109 | - 计算机系统资源的抽象。比如将存储或者输入和输出设备抽象为文件。 110 | - 应用程序编程接口(API,application programming interface)。通过提供现成可用的接口集合,实现了程序的复用,一方面提高了编程效率,另一方面使得程序的内部结构变得更为清晰。 111 | 112 | 但无论如何分层,或者如何设计,计算机程序所能完成的功能无外乎两个:接收输入并按照自己的规则或算法完成输出。 113 | 114 | ## 计算机程序的本质功能和执行原理 115 | 116 | 在阐述本节议题之前,我们先给软件及程序一个定义。先说比较容易定义的程序。程序是指最终可在计算机上执行的算法集合,通过编译或者解释成计算机可以识别的编码序列而执行。 117 | 118 | 软件则相对比较难以定义。从宽泛的意义上讲,任何包含有序数据的文件或者文件集合都可以称为软件,其关键点在于有序数据,也就是信息(纯粹的无序数据集合无法表达有意义的讯息)。比如一个字体文件,也可以称为是计算机软件,而只有该字体文件被一个渲染字体的程序读取并显示出文字的样子,这个软件才有了起真正的价值。 119 | 120 | 我们通常所理解的软件,可以从狭义上定义为包含程序和各种有序数据(信息)在内的集合。因为程序本身也是一个数据序列,因此,软件又可以定义为:一个有序数据(信息)集合,其中的某个或多个数据序列可以被计算机执行。 121 | 122 | 本质上,计算机程序所做的工作就是接收输入然后按照自己的算法进行处理,之后产生输入。这和数学上的公式基本上属于同一类东西。 123 | 124 | 我们以阿兰·图灵的图灵机作为示例来阐述计算机程序的工作原理。以下是百度百科针对“图灵机”这一词条给出的解释: 125 | 126 | > 所谓的图灵机就是指一个抽象的机器,它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后结合自己的内部状态查找程序表,根据程序输出信息到纸带方格上,并转换自己的内部状态,然后进行移动。 127 | 128 | 我估计大部分的读者看到这段解释,几乎得不到太多有价值的内容,因为我们仍然无法清晰理解图灵机到底是个什么东西。为此,笔者在这里给大家换个更容易理解的方式来解释图灵机。 129 | 130 | 图灵机是阿兰·图灵在 1936 年(那时图灵年仅二十四岁)时,为研究“实数的可计算性”这一命题而设计出来的。可计算的实数是指,这个实数可以通过给定的初始条件,通过一定的规则(或公式)计算出来。比如,对自然数5,我们可以给定 4 这个初始值和 1 这个加数,通过简单的加法计算出来,也可以通过勾三股四这个公式计算出来。理解了可计算数这个定义,我们凭直觉就可以回答:整数、有理数[^10]都是可计算的。然而,实数中占绝大多数的无理数[^11]却不这么简单。根据常识,有些我们常见的无理数,比如 π 可以被计算,但其他更多的无理数是不是可以通过一个给定的条件和规则计算出来则不能轻易下结论。 131 | 132 | 图灵借助于图灵机的研究结果表明,不是所有的实数都是可计算的。当然,这个结论对笔者以及本书的绝大多数读者来讲没有多大的意义,我们知道有这样一个结论就可以了。 133 | 134 | 我们回过头来看图灵设计的图灵机,看看为了解决实数的可计算性问题,图灵是如何一步步设计出图灵机的。 135 | 136 | 我猜想图灵首先会尝试用逻辑证明的是圆周率 π 是可计算的,毕竟 π 这个无理数具有较好的代表性,事实上,图灵的确给出了使用图灵机计算π的方法(就是算法,或者程序)。历史上出现过多种计算 π 的值的方法,从阿基米德的几何逼近法到分析法等等。有关圆周率的计算方法,读者可自行搜索互联网。 137 | 138 | 但不论使用哪种方法计算 π,其实本质上无外乎要这么做: 139 | 140 | 1. 给定一个初始条件(输入),使用一个公式来计算该初始条件下的值(输出); 141 | 2. 然后以该值作为输入,或停止计算,或重复第一个步骤。 142 | 143 | 要假设使用一个机器来完成这个计算,那我们大致可以针对上述步骤提出如下需求: 144 | 145 | 1. 这个机器可以读取输入,也可以写下输出,还可以将自己的输出再次读取成为输入。 146 | 2. 这个机器使用一定的规则根据输入做一些处理,并将结果输出出来;在我们这里就是按照给定的公式进行计算。 147 | 3. 以上两点还蕴含一个要点,那就是机器识别输入所使用的符号;也就是说,需要设计一种符号体系,可以方便机器识别。 148 | 149 | 于是,图灵机就被设计为前述那个样子。其要点可以如《信息简史》作者所描述的那样总结如下: 150 | 151 | 1. 纸带。图灵将图灵机使用的纸带想象成一条无限长的一维的长纸条,上面是一个个格子,格子里边包含一个符号。图灵机可以读取、擦除或者写入特定的符号。纸带给以图灵机一个输入装置和临时或者永久的存储装载,而如何物理地存储这些符号则被忽略。以电子计算机为例,纸带相当于内存或者硬盘等存储设备。 152 | 2. 符号。符号及其特定的排列顺序定义了我们前述的“编码”。比如我们可以用十进制的阿拉伯数字来表示数字。在随后改进的图灵机上,图灵最终使用了 0 和 1 这两个最简单的符号,以二进制的方式来表示各种数字。 153 | 3. 状态。状态本质上可以理解为某种处理规则,它决定了针对当前由图灵机读取的符号,图灵机应该执行哪个或者哪几个动作,如前后移动纸带、擦除符号、写入新的符号、停机等等。给定某个特定的计算工作,图灵机的状态是有限的,如果将这些状态保存成一个表,则这个状态表对应于现代意义上的计算机的指令集。 154 | 155 | 尽管图灵机最初的设计目标仅仅是为了一个数学上的证明,然而,图灵机的出现却给其后的数学和信息论发展起到了很大的作用,现代电子计算机甚至将来的量子计算机本质上就是图灵机或者多个或简单或复杂的图灵机之有机结合,只是有如下两个主要区别: 156 | 157 | - 不论哪种计算机,其存储空间(纸带)并不是无限的; 158 | - 不论哪种计算机,执行程序都要消耗能量,其运算能力是有上限的。 159 | 160 | 当我们编写计算机程序时,时时刻刻都要受到上面两点的约束。 161 | 162 | 言归正传,图灵机不仅仅可以计算实数,通过编码(建立数字和其他现实物体之间的映射关系),图灵机可以做任何事情。比如,打印这本书的工作,大致可以用下面这台图灵机来完成: 163 | 164 | 图灵机不停读取以特定格式保存的编码序列,其中包含了这本书的文字以及格式(比如页眉、页脚、文字的大小、段落之间的分隔符等等),然后计算出每个字在纸面上要打印的位置,移动打印头,并根据这个字的字型[^12]数据,控制打印头打印出文字的字型来。 165 | 166 | 上面这台图灵机,其实就是现代针式或者激光打印机的工作原理。其中有两种形式的编码: 167 | 168 | 1. 用来控制文档内容和格式的编码。 169 | 2. 用来控制文字字型的编码。 170 | 171 | 甚至,我们可以将不同的图灵机组合起来使用,让某类图灵机专门做一件事情,而其他类型的图灵机做另外一件事情,前者的输出成为后者的输入。于是,我们现在看到的各种各样的电子设备就出现了。比如现代的电子计算机系统中,中央处理单元(CPU)是其中处于核心地位的图灵机,而包含在图形显示卡中的图形处理单元(GPU)则是另外一个复杂的图灵机。甚至在某种程度上,我们还可以将很多计算机外设(如硬盘、网卡、键盘等)视作简单的图灵机。当然,像鼠标这种外设恐怕很难被称为图灵机,毕竟其中不包含状态的概念,鼠标这种设备仅仅通过光电或者机械的方式将用户对鼠标的移动、按钮的点击等动作转换为计算机可以识别的某种编码,这类设备可以称为“编码器”或“解码器”。键盘之所以可以视作图灵机,是因为现在我们日常使用的键盘可以实现组合键、按键转义等相对较为复杂的逻辑功能。 172 | 173 | 更进一步,通过网线、USB 线或者无线网络,我们还可以将这些图灵机连接起来,在其中传输编码,这不仅仅可用来在人和人之间快速传输信息,同时也可以完成更为复杂和抽象的工作,比如流行的云端存储。哎呀,这不就是互联网吗?! 174 | 175 | [^1]: 数字计算机:Digital Computer。“数字的(Digital)”一词和“模拟的(Analogous)”相对应。传统的电话将声音转换为强弱变化的电流并经线路传输,然后再在接收端通过扬声器播放出来,人们认为这种处理办法是模拟的,这通常意味着“信息没有损失”;而大家现今日常使用的手机,则是将声音信号在非常短的时间内进行采样,将这些采样出来的数字一个个发送到接收方,然后再将这些数字组装并通过扬声器播放。后面这种经过采样的处理方法就称为“数字化处理”。但实际上,相比模拟而言,数字化的处理办法可以提供更好的防噪声及抗干扰能力,也便于存储和处理。这点,在香农于 1948 年发表在《贝尔系统技术期刊》中的《通信的数学理论》中已经过数学上的证明。这篇论文也被视为信息论的开山之作。 176 | 177 | [^2]: 信息:Information。可以简单理解为数据;最近被科学家视为一种新的、可度量的物理量,详情可见《信息简史》一书。 178 | 179 | [^3]: 【维基百科】查尔斯·巴贝奇(Charles Babbage,1791~1871),英国数学家、发明家兼机械工程师。 180 | 181 | [^4]: 对该议题感兴趣的读者可以阅读《数学:确定性的丧失》、《时间简史》、《上帝掷骰子吗?》、《信息简史》等著作。 182 | 183 | [^5]: 艾达·洛夫莱斯(Ada Lovelace,维基百科译作埃达·洛夫莱斯)。艾达是著名诗人拜伦的女儿,遗憾的是拜伦和其母在艾达一个月时分居,此后再也未能见面。艾达死后和其父葬在一起。另外,Ada 编程语言就是为纪念艾达而命名的。 184 | 185 | [^6]: 图灵机是由阿兰·图灵(Alan Mathison Turing,又译作艾伦·图灵)于1936年在其论文《论可计算数及其在判定性问题上的应用》中提出的一种虚拟机器,被视为现代计算机的理论模型 186 | 187 | [^7]: 一个简单的例子。在智能手机上,普通应用无法读取屏幕上的内容,而在 PC 上,却没有任何限制。 188 | 189 | [^8]: 为何采用二进制表示数字,读者可参阅本书第一章。 190 | 191 | [^9]: 【维基百科】肯·汤普逊(Ken Thompson),生于美国新奥尔良,计算机科学学者与软件工程师。他与丹尼斯·里奇(Dennis MacAlistair Ritchie,逝于2011年)设计了B语言、C语言,创建了Unix和Plan 9操作系统,他也是编程语言Go的共同作者。与丹尼斯·里奇同为1983年图灵奖得主。 192 | 193 | [^10]: 有理数是可计算的,是因为有理数包括整数、有限小数和无限循环小数;而有限小数和无限循环小数都可以表示为某两个整数的商。 194 | 195 | [^11]: 无理数其实要比有理数多得多得多!笔者大学的一个数学老师曾使用一个形象的比喻来说明这个事情。假设所有的实数均匀分布于一张纱窗上,有理数的数量就是这个纱窗上的点,而其他位置弥漫的全部都是无理数。这个比喻给我的印象之深,使我在几十年后对这位老师讲解这个形象比喻的场景仍然历历在目。 196 | 197 | [^12]: 从代表某个文字的编码得到这个文字的具体表现样子(字型)的过程,称为字型渲染。我们熟知的字体文件保存了这些文字或字符的字型信息。 198 | 199 | -------------------------------------------------------------------------------- /textbook/toc.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | @SUBJECT [计算机软件技术导论](toc.md) 4 | @AUTHOR [魏永明](https://github.com/VincentWei) 5 | @DATE 2022-03-12 6 | @LANG 简体中文 7 | 8 | *版权声明* 9 | 10 | 版权所有 © 2022 魏永明 11 | 保留所有权利 12 | 13 | - [引言](foreword.md) 14 | - [前言](preface.md) 15 | - 第一篇:信息的计算机表述。 16 | - [第 1 章 为什么是二进制?](part-1-chapter-1.md) 17 | - [第 2 章 二进制及其运算。](part-1-chapter-2.md) 18 | - [第 3 章 数:整数及浮点数。](part-1-chapter-3.md) 19 | - [第 4 章 文字:字符集及编码。](part-1-chapter-4.md) 20 | - [第 5 章 多媒体:图像及音视频。](part-1-chapter-5.md) 21 | - [第 6 章 抽象对象及结构化数据。](part-1-chapter-6.md) 22 | - [第 7 章 有关信息表述的方法总结。](part-1-chapter-7.md) 23 | - 第二篇:信息的计算机存储。 24 | - 第 8 章 文件系统。 25 | - 第 9 章 关系数据库。 26 | - 第 10 章 NoSQL 数据库。 27 | - 第 11 章 分布式存储。 28 | - 第 12 章 有关信息存储的方法总结。 29 | - 第三篇:信息的计算机处理。 30 | - 第 13 章 常见算法。 31 | - 第 14 章 压缩及加密。 32 | - 第 15 章 大数据处理。 33 | - 第 16 章 人工智能。 34 | - 第 17 章 有关信息处理的方法总结。 35 | - 第四篇:信息的计算机展现。 36 | - 第 18 章 字体。 37 | - 第 19 章 矢量图形。 38 | - 第 20 章 HTML 及 CSS。 39 | - 第 21 章 图形界面及交互。 40 | - 第 22 章 典型文件格式。 41 | - 第 23 章 有关信息展现的方法总结。 42 | - 第五篇:信息的计算机传输。 43 | - 第 24 章 互联网及 TCP/IP 协议。 44 | - 第 25 章 常见应用层协议。 45 | - 第 26 章 远程过程调用及数据随动。 46 | - 第 27 章 物联网及相关传输协议。 47 | - 第 28 章 有关信息传输的方法总结。 48 | - 第六篇:计算机编程语言。 49 | - 第 29 章 编程语言的本质及分类。 50 | - 第 30 章 面向对象编程。 51 | - 第 31 章 Web 编程。 52 | - 第 32 章 设计新的编程语言。 53 | - 第七篇:操作系统。 54 | - 第 33 章 通用操作系统。 55 | - 第 34 章 实时操作系统。 56 | - 第 35 章 智能设备操作系统。 57 | - 第 36 章 操作系统的本质及未来。 58 | - 第八篇:软件工程方法。 59 | - 第 37 章 软件工程方法的演进。 60 | - 第 38 章 敏捷开发模型。 61 | - 第 39 章 开源软件及开源协作模型。 62 | - 第 40 章 没有唯一、普适的软件工程方法。 63 | - 第九篇:计算机软件技术的发展热点。 64 | - 第 41 章 新的编程语言。 65 | - 第 42 章 新的WEB开发技术或框架。 66 | - 第 43 章 下一代操作系统。 67 | - 第 44 章 人工智能及大数据。 68 | - 第 45 章 云计算。 69 | - 第 46 章 虚拟化技术。 70 | - 后记:计算机软件技术发展的哲学思考。 71 | - 附录 A:伪代码及其语法。 72 | - 附录 B:重要或易混淆术语。 73 | - 附录 C:程序或算法索引。 74 | 75 | --------------------------------------------------------------------------------