diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..b5df79a --- /dev/null +++ b/404.html @@ -0,0 +1,778 @@ + + + +
+ + + + + + + + + + + + + + +约 4 个字
+约 787 个字 129 行代码 预计阅读时间 4 分钟
+Degree of a node: the number of subtrees of the node.
+Degree of a tree: the maximum degree of all nodes.
+Parent: a node that has subtrees.
+Children: the roots of the subtrees of a parent.
+Silblings: the children of the same parent.
+Leaf/terminal node: a node that has no children.
+Path from node A to node B: a (unique) sequence of nodes starting from A and ending at B, such that each node is a child of the previous one.
+Length of path: the number of edges on the path.
+Depth of a node: the length of the unique path from the root to the node.
+Height of a node: the length of the longest path from the node to a leaf.
+Height of a tree: the height of the root.
+Ancestors of a node: all nodes on the path from the node up to the root.
+Descendants of a node: all nodes in its subtrees.
+Every tree can be transformed into a binary tree with FirstChild-NextSibling representation.
+A binary tree is a tree in which no node can have more than two children.
+Expression Trees: a binary tree used to represent expressions. Interesting.
+Info
+这种版本的节点的定义包括了节点的高度,但是在一般的树中,这是没有必要的,只是在AVL树中,节点高度才尤其重要.在下面的操作中就干脆省略了.
+遍历时间复杂度都是\(O(n)\),迭代的空间复杂度是\(O(n)\).
+层序遍历从顶部到底逐层遍历二叉树,并且按照从左到右的顺序遍历每一层的节点,从本质上来讲,层序遍历其实属于广度优先遍历/Breadth-first Traversal.我们一般借助队列来实现层序遍历.
+先访问根节点,再访问子树,左子树优先于右子树.
+先访问子树,左子树先于右子树,再访问根节点.
+ +先访问左子树,再访问根,最后访问右子树.
+ +什么?你还想折磨自己?
+你需要在循环里边手动建一个堆栈来模仿系统堆栈的行为,想想都觉得受不了,消停写你的迭代版得了。
+二叉搜索树满足以下性质:
+二叉堆首先是一个满足堆性质的完全二叉树,对于最大堆为例,所谓堆性质是:某个树的所有子树的根节点值都大于等于子节点值。同理,我们可以定义最小堆。
+堆其实是一个完全二叉树,完全二叉树又很容易表示成数组,所以堆基于数组来实现,但是存储数组的时候很有讲究:数组的元素代表二叉树的节点值,索引代表层序遍历中节点在二叉树中的位置,所有的索引将由 1
开始,这样就可以很方便的通过索引来找到节点的父节点和子节点。下面是获取父节点、左子节点、右子节点的函数:
并查集只不过是数据结构画在图上很像树
+ + + + + + + + + + + + + +约 842 个字 99 行代码 预计阅读时间 4 分钟
+邻接矩阵/Adjacency Matrix +
+邻接表/Adjacency List +
广度优先搜索/Breadth First Search:从一个点出发,依次访问其邻接点,再访问邻接点的邻接点,以此类推,直到所有点都被访问过;
+时间复杂度:\(O(V+E)\)(若不使用队列硬遍历则\(T=O(V^2)\))
+深度优先搜索/Depth First Search:从一个点出发,访问其邻接点,再访问邻接点的邻接点,以此类推,直到所有点都被访问过,再回溯到上一个点,继续访问其他邻接点;
+单源最短路径/Single Source Shortest Path:从一个点到其他所有点的最短路径
+最大流:给定一个正权有向图\(G\),每个边上都有一个流量 \(c\),从源点 \(s\) 到汇点 \(t\) 的最大流量。
+求解方法:建立残差图,残差网络的边权如下,每在残差网络中寻找到一条增广路径,就更新一下残差图,知道找不到增广路径为止。
+增广路径:从源点到汇点的一条简单路径路径,流量是路径上的最短路径。
+ +最小生成树/Minimum Spanning Tree:给定一个无向连通图\(G\),每个边上都有一个权重 \(w\),找到一个树,包含这个图的所有节点并且使得所有边的权重之和最小。可以使用贪心算法!
+Prim 算法:从一个节点开始,每次选择一个与当前生成树距离最小并且不会产生环的节点加入生成树。
+Kruskal 算法:从所有边中选择权重最小的边,如果这条边不会产生环就加入生成树。
+ + + + + + + + + + + + + + +约 754 个字 12 行代码 预计阅读时间 3 分钟
+哈希表/Hash Table 也被称为散列表,将关键字值映射到表中的一个位置来访问记录,以加快查找的速度,哈希表需要支持查找关键词是否在表中,查询关键词,插入关键词,删除关键词等操作。
+哈希表通常使用一个数组来实现,哈希表的每个位置都叫做一个桶/Bucket,一个桶可以有多个槽/Slot,当多个关键字对应一个位置的时候,将不同的关键词存放在同一个位置的不同槽中。
+哈希表的核心是哈希函数/Hash Function,我们通过哈希函数将关键字/标识符/Identifier 映射到哈希表中的一个位置/索引。
+对于大小为 b
,最多有 s
个槽的哈希表,定义 \(T\) 为哈希表关键字可能的所有不同值的个数,\(n\) 为哈希表中所有不同关键字的个数,关键字密度定义为 \(n / T\), 装载密度定义为 \(\lambda = n / (sb)\)。
当存在 \(i_1 \neq i_2\) 但是 \(h(k_1) = h(k_2)\) 的时候,我们称发生了碰撞/Collision,当把一个新的标识符映射到一个已经满了的桶的时候,我们称发生了溢出/Overflow。
+在没有溢出的情况下,哈希表的查找时间、插入时间、删除时间都是 \(O(1)\),但是在发生溢出的情况下,哈希表的性能会下降。
+哈希函数应该满足以下条件:
+约 83 个字
+Abstract
+这是我在浙江大学《数据结构基础》与《高级数据结构与算法分析》两门课上的课程笔记与延伸学习内容.
+参考书籍:
+约 0 个字
+约 1950 个字 13 行代码 预计阅读时间 7 分钟
+Abstract
+GNU Make是一个用于自动化编译的工具,它可以根据文件的依赖关系自动执行编译任务.本文将介绍GNU Make的基本使用方法.
+为啥要学Make?
+属于是为了这盘醋包了顿饺子,由于数据结构基础这门课的大作业或多或少需要用到多文件编译,我干脆就希望使用Make进行自动化编译,而且在系统课上接触了Makefile的编写,所以就有了这篇文章.
+gcc的编译过程可以简要分为四个阶段:预处理、编译、汇编、链接.
+gcc编译工具链是以gcc编译器为核心的一整套工具,主要包含以下三部分内容:
+预处理阶段的主要任务是处理源文件以#
开头的预处理指令,比如#include
、#define
等.这里主要是将#include
的一些头文件与宏定义进行展开,生成一个.i
文件.
预处理过程输入的是C的源文件,输出的是一个中间/预加载文件,这个文件还是C代码.此阶段使用gcc参数-E
,同时参数-o
指定了最后输出文件的名字,下面的例子就将main.c
文件经过预处理生成main.i
文件:
编译过程使用gcc编译器将预处理后的.i
文件通过编译转换为汇编语言,生成一个.s
文件.这是gcc编译器完成的工作,在这部分过程之中,gcc编译器会检查各个源文件的语法,即使我们调用了一个没有定义的函数,也不会报错,
编译过程输入的是一个中间/预加载文件,输出的是一个汇编文件,当然,直接以C文件作为输入进行编译也是可以的.此阶段使用gcc参数-S
,具体例子如下:
汇编阶段的主要任务是将汇编语言文件经过汇编,生成目标文件.o
文件,每一个源文件都对应一个目标文件.即把汇编语言的代码转换成机器码,这是as汇编器完成的工作.
汇编过程输入的是汇编文件,输出.o
后缀的目标文件,gcc的参数-c
表示只编译源文件但不链接,当然,我们也可以直接输入C源文件,就直接包含了前面两个过程.
Linux下生成的.o
目标文件、.so
动态库文件以及下一小节链接阶段生成最终的可执行文件都是elf格式的, 可以使用 readelf 工具来查看它们的内容.
从 readelf 的工具输出的信息,可以了解到目标文件包含ELF头、程序头、节等内容,对于.o
目标文件或.so
库文件,编译器在链接阶段利用这些信息把多个文件组织起来,对于可执行文件,系统在运行时根据这些信息加载程序运行.
最后将每个源文件对应的.o
文件链接起来,就生成了一个可执行程序文件,这是这是链接xx器ld完成的工作.
例如一个工程里包含了A和B两个代码文件,在链接阶段,链接过程需要把A和B之间的函数调用关系理顺,也就是说要告诉A在哪里能够调用到fun
函数,建立映射关系,所以称之为链接.若链接过程中找不到fun
函数的具体定义,则会链接报错.
链接分为两种:
+--static
,它在编译阶段就会把所有用到的库打包到自己的可执行程序中.所以静态链接的优点是具有较好的兼容性,不依赖外部环境,但是生成的程序比较大.Makefile的规则包括两个部分:一个是依赖关系/prerequisites,另一个是生成目标的方法/command.在Makefile中,规则的顺序是很重要的,Makeflie中有且仅有一个最终目标,其他目标都是这个目标连带出来的,一般来说,定义在第一条规则的第一个目标就是最终目标,make完成的就是这个目标.
+但是我们经常会遇见make a.o
这样的命令,make后可以跟着一个或多个target,a.o
作为make的参数,指定了执行的内容,优先级比Makefile里边的定义要高.换句话说,倘若make后边有目标,这个目标就是最终目标,make后边没目标,默认执行Makefile的第一个目标.
规则的语法是这样的:
+ +或者这样的:
+ +targets是文件名,可以使用通配符,基本来说,我们的目标基本上是一个文件,可以是一个目标文件,可以是一个可执行文件,还可以是一个标签,是多个文件也是有可能的.
+prerequisites是生成该target所依赖的文件或者target.
+recipe是命令行,可以是任意的shell命令,如果其不与target:prerequisites
在一行,那么,必须以 Tab
键开头,如果和prerequisites
在一行,那么可以用分号做为分隔.如果命令太长,我们可以使用反斜杠\
来作为换行符.
规则告诉make两件事:一个是文件的依赖关系,target依赖于prerequisites中的文件;另一个时就会如何生成目标文件,生成规则定义在recipe中,makefile最核心的内容是:
+prerequisites中如果有一个以上的文件比target文件要新的话,recipe所定义的命令就会被执行.
+Makefile中的变量就像是C语言的宏一样,代表着一个文本字符串,在执行的时候会自动展开在所使用的地方,不过,我们可以在Makefile中改变其值.变量可以使用在目标,规则,依赖目标或者其他部分之中.
+在声明变量的时候,我们需要给予其初值,使用的时候需要在变量名前面加上$
符号,最好还用小括号括起来()
,变量的名字可以包含字符,数字,下划线,甚至还可以数字开头,但是不能含有:
,#
,=
或者空字符.
我们可以使用其他变量来构造变量的值,比如foo = $(bar)
,这里的bar
不一定非要是已经定义好的值,我们可以使用后面定义的值,这就很好了嘛,我们可以把变量的真实值退到后面去定义,但是无法避免递归定义,虽然make有能力检测这样的定义。
为了避免这个问题,我们使用:=
操作符,对于VAR := value
,右边的value
会在定义的时候就被展开.
约 1 个字
+约 0 个字
+约 4 个字
+Warning
+待更新!
+约 2 个字
+约 305 个字 5 行代码 预计阅读时间 1 分钟
+Abstract
+这份笔记将专注于bash(也就是Bourne Again SHell),暂不涉及其他种类的shell。
+下面代码均以本人 Ubuntu 22.04.3 系统中的bash为例。
+我们所熟悉的图像用户界面/Graphical User Interface/GUI在某些情况下反而限制了我们对计算机的使用,shell为我们提供了一种充分利用计算机的方式,它允许我们执行程序、输入并且获取某种半结构化的输出。
+打开终端,我们就可以使用shell,下面就是我们一般看起来的样子。
+其中test
是用户名;testMachine
是主机名;~
是当前工作目录,特别地,~
代表home
;$
是提示符,表示现在的身份不是root用户。在提示符后面,我们可以输入命令,命令最终会被shell解析并且执行。我们现在执行一些基本的命令:
在这个例子中:
+我们不仅可以直接输入类似date
的命令,还可以向shell传递参数,比如echo hello
。
如果我们想让shell输出hello world
,方法之一是使用单引号或者双引号包裹起来,
我们让shell执行了echo
命令
约 0 个字
+约 78 个字
+What is tmux?
+tmux’s authors describe it as a terminal multiplexer. Behind this fancy term hides a simple concept: Within one terminal window you can open multiple windows and split-views (called "panes" in tmux lingo). Each pane will contain its own, independently running shell instance (bash, zsh, whatever you're using). This allows you to have multiple terminal commands and applications running side by side without the need to open multiple terminal emulator windows.
+约 519 个字 4 行代码 预计阅读时间 2 分钟
+初始化是指在创建对象(为特定类型的对象申请存储空间)的同时赋初始值。C++ 的初始化方式与规则五花八门,大概包括以下几种:直接初始化、拷贝初始化、列表初始化、默认初始化、值初始化、类内初始值、构造函数初始值列表。
+一般有着四类初始化方式,现代 C++ 的内置类型和类类型都支持这四种初始化方式:
+int a = 1;
或者 std::string s = "hello";
int a = {1};
或者 std::string s = {"hello"};
int a{1};
或者 std::string s{"hello"};
int a(1);
或者 std::string s("hello");
默认初始化/Default Initialization:当对象未被显示地赋予初值时执行的初始化行为。
+int
、double
、float
、bool
、char
等)及其数组:值初始化/Value Initialization:默认初始化的特殊情况,此时内置类型会被初始化为 0。基本场景:
+vector<int> vec(10);
:10 个 int
,初始化为 0
;new
类型,后面带括号,如:new int(), new string{};{}
,如 double d{};
、int *p{};
。对于类类型,其实不需要区分默认初始化和值初始化,因为类类型的初始化只决定于构造函数,与对象在函数内/外、全局/局部/类成员、静态/非静态、默认初始化/值初始化无关。
+约 0 个字
+约 2915 个字 15 行代码 预计阅读时间 10 分钟
+容器是一些特定类型对象的集合,顺序容器提供了控制元素存储与访问顺序的能力。在这里,我们将介绍对于所有容器都适用的操作,除此之外的仅仅针对于顺序容器、关联容器以及无序容器或一小部分特定容器的操作将在后续介绍。
+容器均定义为模版类,需要提供额外信息来特定的容器类型,对于大多数容器,我们需要额外提供元素类型信息,比如 vector<int>
、list<Sales_data>
等。容器几乎可以保存任意类型的对象,甚至可以是另一个容器。
对于一个类型为 C
的容器对象 c
来说,我们有下面操作或者属性:
iterator
:此容器类型的迭代器类型;const_iterator
:可以读取元素,但是不能修改元素的迭代器类型;size_type
:无符号整数类型,足够存储容器中最大可能的元素数量;difference_type
:有符号整数类型,足够存储两个迭代器之间的距离;value_type
:容器中元素的类型;reference
:元素的左值类型,与 value_type&
的类型相同;const_reference
:元素的 const
左值类型,也就是 const value_type&
。C c
:默认构造函数,创建一个空容器;C c(c2)
:构造 c2
的拷贝 c
;C c(b, e)
:构造 c
,并且将迭代器 b
和 e
之间的元素拷贝到 c
中,但是 array
类型的容器除外;C c{a, b, c, d}
:列表初始化 c
。c = c2
:将 c2
中的元素拷贝给 c
;c = {a, b, c, d}
:将 c
中的元素替换为列表中的元素,不适用于 array
;c.swap(c2)
:交换 c
和 c2
中的元素;swap(c, c2)
:交换 c
和 c2
中的元素。c.size()
:返回 c
中元素的数量,不支持 forward_list
;c.max_size()
:返回 c
中最多可以存储的元素数量;c.empty()
:判断 c
是否为空,非空则返回 false
。array
):c.insert(args)
:将 args
中的元素拷贝到 c
中;c.erase(args)
:删除 args
中指定的元素;c.emplace(inits)
:使用 inits
在 c
中构造一个元素;c.clear()
:删除 c
中所有元素,返回 void
。c.begin()
:返回指向 c
的首元素的迭代器;c.end()
:返回指向 c
尾元素的下一个位置的迭代器;c.cbegin()
:返回 const_iterator
,指向 c
的首元素;c.cend()
:返回 const_iterator
,指向 c
尾元素的下一个位置;reverse_iterator
:按逆序寻址元素的迭代器;const_reverse_iterator
:不能修改元素的逆序迭代器;c.rbegin()
:返回指向 c
尾元素的迭代器;c.rend()
:返回指向 c
首元素前一个元素的迭代器;c.crbegin()
:返回 const_reverse_iterator
,指向 c
尾元素;c.crend()
:返回 const_reverse_iterator
,指向 c
首元素前一个元素。并不是所有标准库容器都支持下标运算符,但是所有标准库容器都支持迭代器,迭代器类似于指针类型,提供对对象的间接访问。我们一般有下面操作:
+C::iterator iter
:声明一个 C
类型容器的迭代器,比如 vector<int>::iterator iter
;C::const_iterator iter
:声明一个只能读取元素的 C
类型容器的迭代器;auto iter = c.begin()
:返回指向容器 c
的首元素的迭代器,类型根据 c
的类型而定;auto iter = c.cbegin()
:返回的迭代器类型为 const_iterator
;auto iter = c.end()
:返回指向容器 c
的尾后元素的迭代器,类型根据 c
的类型而定;auto iter = c.cend()
:返回的迭代器类型为 const_iterator
;*iter
:解引用迭代器,返回迭代器指向的元素的引用,但是不能对尾后迭代器解引用;iter->mem
:解引用迭代器,返回迭代器指向的元素的 mem
成员的引用,等价于 (*iter).mem
;++iter
:递增迭代器,返回当前指示元素的下一个元素;--iter
:递减迭代器,返回当前指示元素的前一个元素;iter1 == iter2
与 iter1 != iter2
:比较两个迭代器是否相等,如果两个迭代器指示的是同一个元素或者是同一个容器的尾后迭代器,则相等;需要注意的是:首先,如果对于容器的操作让容器的容量发生了变化,那么指向该容器元素的迭代器就会失效;另一方面,并不是所有容器类型都支持了更多的迭代器运算,比如关系运算或者 +
与 -
。
迭代器的接口是公共的,如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。迭代器范围由一对迭代器表示,两个迭代器分别指向同一个中的元素或者尾后位置,一般分别称为 begin
和 end
,first
和 last
。迭代器范围包含着 [begin, end)
之内的所有元素,这种范围叫做左闭合区间。这其实就隐含着构成迭代器范围的迭代器的另一个要求:begin
必须在 end
之前,也就是可以通过反复递增 begin
来得到 end
。
每个容器类型都定义了一个默认构造函数,除了 array
之外的所有容器类型的默认构造函数都会创建一个指定类型的空容器,并且可以接受指定容器大小和元素初始值的参数。
容器的初始化方式很多,不接受参数就构造一个空容器,括号初始化就会创建一个拷贝,使用 =
也会初始化为一个拷贝,列表初始化需要列表内的元素类型与容器内元素类型相同,对于 array
类型而言,需要列表内元素树木小于等于 array
的大小,任何遗漏的部分都会进行值初始化。使用迭代器进行初始化的时候,给定范围 [begin, end)
之间的元素会被拷贝到新容器中,但是 array
类型的容器并不适用这一点,我们还可以让顺序容器内包含 n
个相同的元素 val
,如果不提供元素的值 val
就会执行值初始化,但是只有顺序容器支持这一点。
赋值运算符 =
将左侧容器的全部元素替换成右边容器中元素的拷贝,对于除了 array
之外的顺序容器而言,我们还有对应的赋值函数 assign
:
seq.assign(b, e)
:将迭代器范围 [b, e)
之间的元素拷贝到 seq
中,但是迭代器不能指向 seq
中的元素;seq.assign(n, val)
:将 seq
中的元素替换为 n
个 val
;seq.assign(il)
:将 seq
中的元素替换为列表 il
中的元素。赋值相关的运算会导致左侧容器内部的迭代器、引用和指针失效,但是 swap
对于除了 array
和 string
之外的容器而言,就不会出现这种情况,这是因为 swap
仅仅交换了两个容器内部的数据结构,而不会改变容器内部的元素,但是在 swap
完成之后,这些元素都不属于原来的容器了。
assign
比简单的 =
更加灵活,并且生成的还不是拷贝了(这才是真正的赋值),它允许我们从一个不同但相容的类型赋值,比如将 char*
赋值给 string
,或者将 vector<int>
赋值给 list<int>
。
除了 array
之外,所有的标准库内的顺序容器都提供灵活的内存管理,运行时可以动态添加或者删除元素来改变容器大小,下面是例子:
c.push_back(t)
:在 c
的尾部添加一个值为 t
的元素,返回 void
;c.emplace_back(args)
:在 c
的尾部构造一个元素,元素由参数 args
构造;c.push_front(t)
:在 c
的首部添加一个值为 t
的元素,返回 void
;c.emplace_front(args)
:在 c
的首部构造一个元素,元素由参数 args
构造;c.insert(p, t)
:在迭代器 p
指向的元素之前插入一个值为 t
的元素,返回指向新元素的迭代器;c.emplace(p, args)
:在迭代器 p
指向的元素之前构造一个元素,元素由参数 args
构造;c.insert(p, n, t)
:在迭代器 p
指向的元素之前插入 n
个值为 t
的元素,返回指向第一个新元素的迭代器,如果 n
为 0 则返回 p
;c.insert(p, b, e)
:在迭代器 p
指向的元素之前插入迭代器范围 [b, e)
之间的元素,返回指向第一个新元素的迭代器,如果范围为空则返回 p
,b
和 e
不能指向 c
中的元素;c.insert(p, il)
:il
是一个花括号抱着的元素值列表,将列表中的元素插入到 p
指向的元素之前,返回指向第一个新元素的迭代器,如果列表为空则返回 p
。需要注意的是,forward_list
有自己专用版本的 insert
和 emplace
,并且也不支持 push_front
和 emplace_front
; vector
和 string
也不支持 push_front
和 emplace_front
,因为在头部插入元素就需要移动元素,代价太高了。并且向 vector
string
和 deque
插入元素会使得所有指向容器的迭代器、引用和指针失效。
使用一个对象去初始化容器的时候,或者将一个对象插入容器之中的时候,实际上放进去的是对象值的一个拷贝,并不是对象本身,这和参数传递一样,容器内的元素和提供的对象之间没有任何关联。
+insert
返回插入的第一个元素的迭代器这一点可以允许我们在一个容器的同一个特定位置反复插入元素。
对于顺序容器而言,emplace
系列成员函数允许我们通过参数 args
构造元素并且插入,而不是类似于 push_back
与 insert
那样拷贝元素。调用 emplace
成员函数的时候,接受的参数被传递给元素类型的构造函数,使用这些参数在容器管理的内存空间中直接构造函数,这就要求 emplace
接受的参数必须和元素类型的构造函数相匹配。
需要使用 #include<vector>
来使用 vector,vector 是一个可变大小的数组,支持快速随机访问,但是在尾部以外的部分插入或者删除元素的速度可能很慢。
除了最初提到的通用操作,vector 还有下面的操作:
+vec.at(n)
:返回 vector 中第 n 个元素的引用,同时检查是否越界;vec.front()
:返回 vector 的第一个元素的引用;vec.back()
:返回 vector 的最后一个元素的引用;vec.data()
:返回指向作为存储工作的底层数组的指针;vec.push_back(ele)
:在 vector 尾部添加元素;vec.pop_back()
:删除 vector 尾部元素;vec.insert(pos, ele)
:在 pos 位置插入元素;vec.erase(iter)
:删除迭代器 iter 指向的元素;vec.erase(beg, end)
:删除 [beg, end)
区间的元素;vec.find(first, end, v)
:在 [first, end)
区间内查找值为 v 的元素,返回指向该元素的迭代器,否则返回 end;vec[n]
:下标运算符,返回 vector 中第 n 个元素的引用;==
, !=
, <
, <=
:诸如此类按照字典序比较两个 vector,同时考虑元素数量;vector 支持了更多的迭代器运算,一方面可以使得迭代器的每次移动都夸跨过多个元素,也支持迭代器进行关系运算,这些运算都被称为迭代器运算:
+iter +/- n
:返回迭代器 iter 向前/后移动 n 个元素的迭代器;iter +/-= n
:迭代器 iter 向前/后移动 n 个元素;iter1 - iter2
:返回两个迭代器之间的距离;>
>=
<
<=
:若某个迭代器在另一个迭代器之前,则认为前者小于后者;需要使用 #include<queue>
来使用 queue,queue
Warning
+未完工!等我学完CS61A后再接着写吧QAQ。
+约 1171 个字 175 行代码 预计阅读时间 6 分钟
+这个笔记建立的初衷是帮助笔者深刻记忆C语言的语法和特性,也作为加深对C语言的理解的工具(似乎终极目的就是提升程算分数编程能力)使用。
+C语言因为其比较贴近底层,语法精简而高效,扩展性和可移植性强而闻名,因而作为计算机专业学生的第一门语言存在。另外,笔者在学习完C后,学习Python的过程极其愉悦()
+字面量即一个值:
+整型:123
表示十进制的123;0123
表示八进制的123,亦即十进制的83;0x123
是十六进制的123,亦即十进制的291。
字符型:
+一个字符类型相当于一个字节的整型,所以字符类型可以通过整型来表示:char c = 65
。
\
有转义的效果,比如'\n'
表示一个控制字符,而反斜杠只有通过\\
才能表示出来。'\101'
表示A
,而'08'
由于8超过了八进制的范围,这就是两个字符放在了一个单引号里边,是错误的用法,如果写成字符串,"\08"
就表示两个字符:一个空字符和一个8
。\x
后边接在0-9
、A-F
内的字符,可以通过十六进制表示一个字符,不过没有长度限制,遇到范围外的字符就结束,比如\x000041
也是一个字符。字节分配:char
1byte,short
2byte,int
4byte,long
4byte,long long
8byte, float
4byte,double
8byte,pointer
4/8byte。
sizeof
、对齐。?:
。,
。坑:% & <<
不能用在double\float
上。
printf()
的转换说明修饰符¶const
¶重要:声明一个指针只会分配一个给指针变量的空间(这部分空间用来存储它指向的位置的地址值),而不会分配指向的空间。使一个指针可用可以将其它变量取地址赋值给它,这样它指向的位置就是有效的。或者通过 malloc
来新分配一块堆上的内存,malloc
的返回值就是这块内存的首地址,也是你可用的。
坑:二维数组不能退化为二级指针;数组名不能被重新赋值。
+坑:数组是数组,指针是指针(这里指类型)。
+坑:指针相减的意义是计算两个指针相差几个“单位”的距离,而不是将其值简单的相减。比如:
+c
+ int a[] = {1, 2, 3, 4, 5};
+ int *p = a, *q = &a[2];
+ printf("%lu", q-p); // Output: 2
c
+ double a[]={1, 2, 3, 4, 5};
+ printf("%d", (int)&a[3] - (int)&a[0]); // Output: 24
神坑:变长数组不能通过int a[n] = {0};
的方式初始化
字符串其实是以空字符\0
结尾的char
类型数组,因此,我们可以像处理一般数组的方式处理字符串,比如:
如果要打印MSG[22]
,则输出的是空字符,空字符不是空格,不会在输出窗口占用位置,只是标志字符串数组的结束。
我们一般用三种方法定义字符串:字符串常量、char
类型数组、指向char
类型的指针。被双引号括起来的内容被视为指向该字符串存储位置的指针,这类似于将数组名作为指向该数组的指针。比如以下程序:
我们对字符串用%c%p
进行转换的时候,转换过去的其实是字符串第一个元素的地址和其对应的字符
数组形式的字符串(如char arr1[] = "III"
)在计算机的内存中分配一个内含4个元素的数组,每个元素作为一个字符,且最后一个元素为空字符。先将字符串常量存储在静态存储区中,程序开始运行之后为数组分配内存,初始化数组将静态存储区的字符串拷贝到数组中,编译器将数组名arr1
作为该数组首元素地址的别名,而且作为地址常量,不能被改变。
一般来说,指针形式的定义一般于字符串字面量一起使用,被双引号括起来的内容是字符串字面量,而且被视为字符串的地址。指针形式(如char *pt1 = "III"
)让编译器在静态存储区中分配4个元素的空间,开始运行程序时,编译器为指针变量(*pt1
)留出一个存储位置,该变量最初指向该字符串的首字母,但是它的值可以被改变,即可以使用递增运算符。
由于指针形式字符串的存储形式,一般建议将指针初始化为字符串自变量时使用const
限定符。
编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量,所以下面程序打印出来的都是"Jey"
由于数组名是一个指针变量,所以不能用str1 = str2
来简单地拷贝数组,这样只会让两个指针指向相同的内存区域。
我们可以定义字符串数组,也就是通过数组下标来访问不多个不同的字符串,有两种方式:使用存储字符串指针的数组或者多维数组:
+这两种方式最后实现的效果是几乎一样的,都代表着五个字符串,只使用一个下标时只代表一个字符串。比如strarr1[0]
和strarr2[0]
都代表着字符串 "Hello"
一般来说对数组的操作都是依赖于指针进行的。
+最简单的分配空间的方式就是在 stack 上建立数组变量,而且还只能如此建立
+ +再就是利用C库函数malloc()
分配内存,比如char *name = (char *) malloc (sizeof(char)*8)
这样就可以按照数组形式的字符串来使用字符串了
gets()
函数¶C11标准中,废弃了不安全的gets()
函数,但是大多数编译器为了兼容性,仍然保留gets()
函数。
gets()
函数读取一整行输入,直到遇到换行符,然后丢弃换行符,存储其余字符在传递进来的字符串指针指向的地址上,并在字符的末尾添加一个空字符,使其成为字符串。比如:
puts()
函数经常和gets()
函数一起使用,这个函数用于显示字符串,并且在字符串的末尾添加换行符。
使用gets()
函数时,gets()
函数只知道数组的开始处,而不会检查数组的长度和字符串的长度是否相融洽。如果输入的字符串过长,超出了数组的存储范围,就会造成缓冲区溢出(buffer overflow),读取的数据将一直向后存储,覆盖掉后边内存上的内容,如果这些多余的字符只是占用了未被使用的内存,就不会立刻出现问题,而如果擦写掉了程序中的其余内存,这样就会让程序异常终止,或者出现其他情况。
出现fragmentation fault
的错误的时候,一般是程序试图访问某些未被分配的内存。
gets()
的替代品¶fgets()
和fputs()
函数fgets()
函数接受三个参数:字符串存储的位置、读入字符的最大数量和要输入的文件。
fgets()
函数接受的第二个参数时读入数组的最大数量,如果该参数的值是n
那么fgets()
将读入n-1
个字符,并且在最后加上一个空字符,或者读到第一个换行符号为止。
fgets()
函数的第三个参数指明要读入的文件,如果是从键盘读入数据,那么以stdin
作为参数,或者输入文件指针。
当输入行不溢出的时候,fgets()
函数将换行符放在结尾,这与fputs()
函数的特性相仿:这个函数在打印字符串的时候不会在最后加上换行符。可是如果使用puts()
函数一起使用,那么可能就会发现出现了两个换行。
fputs()
函数接受两个参数:第一个指出要写入的字符串的位置,第二个指出目标写入的位置,如果输出到屏幕上,那么输入stdout
作为参数。
fgets()
返回指向char
的指针,如果一切顺利,函数返回的地址和传入的第一个参数相同,如果传到文件的结尾,将返回一个特殊的指针:空指针(null pointer),这个指针不会指向有效的数据,所以可以用来标识特殊情况。在代码钟可以用数字0
来代替,但是C利用宏NULL
来代替。下面是一个很有意思的例子:
这个程序的实际操作过程是:首先fgets()
函数读入9个字符,在后边加入\\0
之后交给fputs()
函数输出,但是此时不输出换行符,接着进入下一轮迭代,fgets()
函数继续读入字符、交给fputs()
函数输出……
gets_s()
函数
s_gets()
函数
我们可以利用fgets()
函数自行创建一个读取整行输入,并且利用空字符取代换行符、或者读取一部分字符,丢弃溢出的字符(其余部分的字符)的函数:
利用字符串函数,我们可以对函数进行修改,让它更加简洁。
+如果fgets()
函数返回NULL
,则证明读到文件结尾或者读取错误,s_gets()
函数跳过了这个过程。
我们丢弃多余的字符的原因是:这些多余的字符都存储于缓冲区之中,如果我们下一个要读取的数据是double
类型的,那么就可能造成程序崩溃(因为输入了char
类型甚至char*
类型的数据),丢弃剩余行的数据可以令读取语句和键盘输入同步。
这个函数并不完美,因为它在遇到不合适的输入的时候毫无反应,并且丢弃多余的字符的时候,不会告诉程序也不会告诉用户。但是至少会比gets()
函数安全的多;-)。
scanf()
函数scanf()
函数和%s
转换说明可以读取字符串,但是scanf()
函数在读取到空白字符(包括空格、换行符和空字符)的时候会终止对字符串的读取。scanf()
函数还有另外一种确定输入结束的方法,也就是指定字符宽度,比如%5d
,那么scanf()
将在读取完五个字符或者读取到第一个空白字符后停止。
char words[]={'H','e','y','!'};
由于这个字符数组(不是字符串!)结尾并未有空字符,所以words
不是字符串,如果我们使用这样的代码:put(words)
,puts()
函数由于未识别到空字符,就会一直向下读取、输出后续内存中的内容,这或许是 garbage value ,直到读到内存中的空字符(内存中还是有不少空字符的)。
puts()
函数很容易使用,只需要传入需要输出的字符串的地址就可以了,它在输出的时候会在后边加上一个换行符。但是puts()
函数的返回值众说纷纭:某些编译器返回的是输出的字符的个数,某些编译器输出的是输出的最后一个字符,有的干脆就返回一个非零的整数。
fputs()
函数需要接受两个参数,一个是字符串的地址,另一个是写入的地址:这一般是文件指针,如果需要输出到屏幕上,传入stdout
则可。这个函数的特点在于不会输出换行符。
printf()
函数需要转换说明,它的形式更复杂些,需要输入更多的代码,计算机执行的时间会更长,但是优点在于可以更容易地输出更复杂、更多的字符串。
这里讲的字符串函数是指定义在头文件string.h
内的函数。
strlen()
函数strlen()
函数的实现其实很简单,我们写一个while
循环就好了(),strlen()
函数接受字符串地址,返回一个unsigned int
的值来表示字符串的长度。
重要的是我们可以利用strlen()
函数来得到字符串的一些性质参数,进而更容易实现对字符串的操作,比如我们可以利用下面自行设计的函数来实现字符串的截断:
strcat()
函数strcat()
函数接受两个字符串作为参数,用于将两个字符串拼接在一起,更确切地说是将第二个字符串的拷贝附加在第一个字符串的末尾,并且将拼接后的字符串作为第一个字符串,第二个字符串不变。strcat()
函数返回第一个参数。
strcat()
函数和gets()
函数一样,如果使用不当,也会导致缓冲区溢出。但是gets()
函数被废弃的原因在于无法控制用户向程序里边输入什么,但是程序员是可以控制程序干什么的。因此,在经历输入的检查之后,我们认为至少程序是比较安全的,而使用strcat()
函数不当导致缓冲区溢出的情况,被认为是程序员粗心导致的,而C语言相信程序员,程序员也有责任确保strcat()
函数的使用安全。
strncat()
函数为了避免strcat()
函数的不安全的可能,我们类似fputs()
函数那样,添加第二个参数,确定最大添加字符数,这就是strncat()
函数的逻辑。
strncat()
函数接受三个参数,两个字符串指针和最大添加字符量,在加到最大字符量或者遇到空字符的时候停止。
配合strlen()
函数,strncat()
函数可以很好用。
strcmp()
函数和strncmp()
函数首先,我们比较两个字符串的时候,比较的是字符串的内容,而不是字符串的地址,所以我们不能做判断指针是否相等的操作,而利用循环挨个判断还蛮复杂,这就是strcmp()
函数诞生的逻辑。
strcmp()
函数接受两个字符串指针参数,如果字符串内容完全相等(包括大小写),strcmp()
函数就会返回0,否则返回非零值。
在字符串内容不一样的时候,如果第一个字符串的字符在ASCII码在第二个字符串的之前,strcmp()
返回负数,反之返回正数;在某些编译器中,会作更加复杂的操作,也就是返回两字符的ASCII码的差。
strcmp()
函数会一直比较字符是否相同,直到出现不同或者字符串结束,这样的比较方式显得就非常笨重,而strncmp()
函数提供了一种更为灵活的选择:strncmp()
函数接受的第三个整数参数指定了比较到第几个字符(这里从1开始计数 ;-) )比如strncmp(str1,"strings",7)
就指定只查找strings
这七个字符。
strcpy()
函数和strncpy()
函数strcpy()
函数
sprint()
函数
memcpy()
函数
Others.
+strchr()
函数
strrchr()
函数
strstr()
函数
atoi()
函数
Character Classification
+isalpha()
函数
和isalpha()
函数属于一类的函数还有
tolower()
和toupper()
函数
cppreference 上对这两个函数归类为 Character Manipulation 解释是:converts a character to lowercase/uppercase.
+
+static
关键词让变量具有内部链接,同时具有静态存储期。extern
关键词让变量具有外部链接,同时具有静态存储期。作用域描述程序中可以访问标识符的区域,包括:块作用域,函数作用域,函数原型作用域和文件作用域。
+块是用一对花括号括起来的代码区域,包含for
循环、while
循环、do while
循环和if
语句所控制的代码,就算这些代码没有被花括号括起来,这也算是一个块。定义在块中的变量具有块作用域(block scope),它的可见范围只是在块内,或者说从定义处到包含该定义的块的末尾。此外,函数的形式参数虽然在花括号表示的块之前,但还是具有块作用域。只有在块内的语句才能访问具有块定义域的变量。
函数作用域仅仅用于goto
语句的标签,当这个标签首次出现在函数的内层时,作用域也延伸到整个函数。函数作用域有效防止了标签混乱的情况发生,当然更好的处理方式或许是干脆不用goto
语句()
函数原型作用域的作用范围时从形式参数定义处到函数原型声明结束。这表明编译器更多的关心形式参数的类型而不是形参名,而只有在变长数组中,形参名才更有用。
+如果在函数的外边定义了一个变量,比如以下程序:
+这里的变量glb_val
就具有文件作用域,更确切地说,具有外部链接的文件作用域,我们也叫它为全局变量。
Tip:这里的glb_val
它的作用域是从定义处到文件结束。
某些我们认为的多个文件可能在编译器里边以单个文件的形式出现,比如C预处理器就将头文件里边的内容替换#include
指令。所以,编译器将源代码文件和所有的头文件都看作是一个包含着信息的单独文件,这个文件被称为是翻译单元(translation unit)。
如果程序由多个源代码文件组成,那么这个程序也由多个翻译单元组成,每个翻译单元对应着一个源代码文件和它的头文件。
+目前我们的程序还不进行多文件处理。
+C 文件有着三种链接属性:外部链接、内部链接和无连接。具有块作用域、函数作用域和函数原型作用域的变量都是无连接变量。具有文件作用域的变量可以是外部链接也可以是内部链接。具有内部链接的变量只能在一个翻译单元使用,而具有外部链接的变量能在多文件程序中使用。
+使用extern
关键词,或者直接在函数外边定义的变量都是具有外部链接的变量,而使用static
关键词的变量是具有内部链接的变量。
存储期(storage duration)描述了通过这些标识符访问的对象的生存期,某些变量存储期一过,它所占的内存就会被释放,相应的,存储的内容也会丢失。C对象有着四种存储期:静态存储期、自动存储期、线程存储期和动态分配存储期。
+extern
和static
表明了对象的链接属性与存储期(静态存储期)。The static
/extern
specifier specifies both static storage duration (unless combined with _Thread_local) and internal/external linkage.声明在函数头、块内的变量属于自动存储类别的变量,具有自动存储期,块作用域且无连接。我们可以在C中使用关键词auto
来表明这个变量的存储类型是自动变量。
我们使用关键词register
来表示该变量的存储类型为寄存器变量。寄存器变量存储在CPU的寄存器之中,寄存器是计算机最快的可用内存,因此访问并且处理这些变量的速度会更快,但是无法获取寄存器变量的地址(因为它没有内存位置)。寄存器变量在绝大多数方面都和自动变量一样,也就是具有块作用域、无链接和自动存储期。
声明变量为register
类型更像是一种请求而不是命令,因为编译器必须根据寄存器或者最快可用内存数量来衡量请求;并且由于寄存器的大小有限(通常是一个字,亦即4或8字节),可以声明为寄存器变量的数据类型有限,比如寄存器可能就没有足够大的空间来存储double
类型的值。计算机很可能会忽略我们的请求,变量则被声明成一般的自动变量(也就是存储在内存之中),即使这样,仍然不能对该变量使用取地址运算符。
我们可以创建具有块作用域、无连接的静态变量,只需要在块中(这样就提供块作用域和无连接了)用存储类别说明符static
(提供静态存储期)说明这个变量就可以了。
编译器在程序的生命周期内保证静态变量的存在,静态变量只会在程序中被初始化一次,不会在离开和进入作用域时被销毁或者重置。这是因为静态变量和外部变量在程序被载入内存的时候已经执行完毕,所以在逐个步骤调试的时候会发现含有 static
声明的变量不太像时程序中的变量 ;-)
外部链接的静态变量具有文件作用域、外部链接和静态存储期,该类别有时被称为外部存储类别,属于该类别的变量称为外部变量。如果未初始化外部变量,则其被默认初始化为0;只能用常量表达式初始化文件作用域变量(除了变长数组以外,sizeof()
表达式可以看作常量表达式)。
全局变量在main()
函数执行之前完成初始化。
我们在文件之间共享全局变量的时候需要特别小心,可以使用以下两个策略:其一,遵循外部变量的常用规则,亦即在一个文件之中使用定义式声明,在另一个文件之中使用引用式说明(使用extern
关键字);其二,将需要共享的全局变量放在一个头文件之中,在其他文件中包含这个头文件就可以了,然而,这种处理方式需要我们在头文件中使用static
关键词,如果我们不使用static
关键词或者使用extern
关键词,那么我们就在每一个文件之中都包含了一个定义式声明,C标准是不允许这样子的。然而头文件实际上是给每一个文件提供了一个单独的数据副本,数据是重复的,浪费了很多的内存。
考虑上面的例子:对于外部变量来说,第一次声明declaration 1
被称为定义式声明(defining definition),为变量预留了存储空间;第二次声明declaration 3
被称为引用式声明(referencing definition),关键词extern
表明此次声明不是定义,指示编译器到别处查询定义,这表明declaration 2
是不正确的,这时编译器假定falseint
定义在程序别处,不会引起分配空间。因此我们不要用extern
关键字创建外部定义,只使用它引用外部定义。
使用关键字static
可以声明内部链接的静态变量,只需要在函数外使用static
声明就可以,并且在函数内使用时使用extern
进行引用式声明即可,但是extern
并不改变链接属性。
函数也有存储类别,可以是外部函数(默认)、静态函数或者内联函数。
+extern
关键词定义的函数是外部函数,是为了表明当前文件中使用的函数被定义在别处,除非使用static
关键词,一般函数声明都默认为extern
。static
关键词定义的函数是静态函数,静态函数只能用于其定义所在的文件。可以在其他文件中定义与之同名的函数,这样子就避免了名称冲突的问题。inline
我们在前面所探讨的存储类别都有一个共同之处,在确定好存储类别之后,就只能根据确定好的内存存储规则,自动指定存储期和作用域。但是我们也可以利用库函数灵活分配和管理内存,只不过必须好好利用指针。
+我们下面讨论malloc()
、free()
、calloc()
和realloc()
函数。
void* malloc(size_t size)
函数¶malloc()
函数接受一个参数:所需要的内存字节数,之后它会找到合适的内存块,匿名分配size
个byte
大小的内存,返回动态分配内存块的首字节地址。如果无法分配内存,malloc()
函数就会返回一个空指针。最早,由于char
类型只占用一个字节,所以malloc()
函数返回一个char *
类型的指针,后来malloc()
返回void *
类型的通用指针,指向什么都可以,完全不需要考虑类型匹配的问题,但是为了增加代码的可读性,应该坚持强制类型转换。
我们可以利用malloc()
函数提供第三种声明数组的方式:将调用malloc()
函数的返回值赋给指针,利用指针访问数组的元素,这样创建的其实是一个动态数组。比如:
我们完全可以使用正常声明数组一样的方式访问这个数组ptd
,比如ptd[18]
。
malloc()
函数也可以声明多维数组,但是语法会复杂一些:
先看第一种定义方式:在第二行创建了一个二级指针,也就是存储着指针的数组array2
,在接下来的循环中,逐个为二维数组的每一行分配空间,同时将数组指针存储在array2[i]
中。在读取元素array2[1][2]
的时候,我们先读取出array2[1]
,发现是个指针(其实是数组),然后读取这个数组的第三个元素(编号是2),这样就读出来了元素array2[1][2]
。
再看第二种定义方式:简而言之,等号左侧定义了一个指针变量array
,指向的是int[numcolumn]
类型的指针,说白了array
也是一个二级指针。如果还要整花活,我们发现*(*(array+1)+2)
和array[1][2]
其实是一样的。换句话说,array
指向一个内含6个整型的数组,因此array[i]
表示一个由numcolumn
个整数构成的元素,array[i][j]
表明一个整数。
逻辑上看,二维数组是指针的数组(亦即二级数组);但是从物理上来看,二维数组是一块连续的内存,对于二维数组array3[4][5]
:我们完全可以按照5进制来理解这块内存的排布,五进制数 ij 表示的数所对应的内存上边的内容就是array[i][j]
存储的内容。
void* calloc(size_t num,size_t size)
函数¶calloc()
函数分配num
个size
大小的连续内存空间,并且将每一个字节都初始化为0,所以calloc()
调用的结果是分配了num*size
个字节长度的内存空间。calloc()
函数的返回值和malloc()
函数的一样。
void* realloc(void* ptr,size_t new_size)
函数¶realloc()
函数重新分配指针ptr
指向位置内存块的大小。函数返回新分配内存块的起始位置的地址,并且原指针ptr
在调用后不再可用。
realloc()
函数被调用时,只会做下面两种行为之中的一种:
ptr
指向的内存区域扩大或者缩小,并且尽可能保留剩余原有区域的内容(The contents of the area remain unchanged up to the lesser of the new and old sizes.),如果内存区域扩大,新的内存内容为未定义的(the contens of the new part of the array are undefined.)。void free(void *ptr)
函数¶free()
函数接受先前被malloc(),calloc(),realloc()
动态分配过的内存地址,之后将这些内存释放(deallocate),如果free()
接受一个空指针,那么它什么都不会做。free()
函数不返回任何值。如果free()
函数接受的参数不是先前被malloc(),calloc(),realloc()
分配过的内存地址,它的行为并未被定义。(The behavior is undefined if the value of ptr
does not equal a value returned earlier by malloc(),calloc(),realloc()
)我们也不能释放同一内存两次(The behavior is undefined if the memory area referred to by ptr
has already been deallocated, that is, free()
, free_sized()
, free_aligned_sized()
(since C23), or realloc()
has already been called with ptr
as the argument and no calls tomalloc(), calloc(), realloc()
or aligned_alloc()
(since C11) resulted in a pointer equal to ptr
afterwards.)。
最重要的是:动态分配的内存必须被释放,否则会发生内存泄漏(memory leak)。
+值得注意的是,C99标准为限定符增加了一个新的属性:幂等性。也就是说可以在同一个声明之中使用多个相同的限定符,多余的限定符将被忽略。
+const
限定符¶被const
关键词声明的对象将成为只读变量,其值不能通过赋值、递增或递减等方式修改,但是至少初始化变量是没问题的,这样我们就只可以使用但不能修改对象的值了。
如果对指针使用const
限定符,如果const
限定符在*
的前面,也就是const int *num
或者int const *num
,其实限定了指针指向的值为const
,num
指向了一个int
类型的const
值。如果const
限定符在*
的后面,也就是int * const num
,则我们创建的指针本身的值不能改变,但是它指向的值可以改变。
更加常见的用法是声明为函数形参的指针。比如void display(const int array[], int num)
,另外一个更熟悉的例子是字符串函数void strcat(char * restrict string1,const char * restrict string2)
。 这使得传进去的数组的值没有被修改,这其实表明了const
限定符实际提供了一种保护数据的方法。
我们同样可以对全局变量使用const
限定符保护数据,因为extern
限定符使得程序的任何一个部分都能使用并且改变这个变量,所以会平白无故产生许多危险,而const
限定符让变量变成只读变量,这样就可以另程序更加安全。
volatile
限定符¶restrict
限定符¶文件其实是硬盘上的一段已经被命名的存储区域,C将文件看成一系列连续的字节,每一段字节都可以被单独读取。
+C提供两种文件模式:文本模式和二进制模式。
+C程序会自动打开三个文件:标准输入、标准输出和标准错误输出。通常时候下,标准输入是普通的输入设备,一般是键盘;标准输出和标准错误输出都是系统的普通输出设备,一般是显示屏。函数getchar()
、函数printf()
和函数puts()
都使用的是标准输出。标准错误输出提供了一个逻辑上不同的地方来显示错误输出,如果我们将输出发送给文件,那么发送到标准错误输出的内容仍然会被发送到屏幕上。
[[noreturn]] void exit(int exit_code)
函数exit()
函数关闭所有打开的文件并且结束程序,正如函数声明处所说
File *fopen(const char *restrict filename, const char *restrict mode)
函数fopen()
函数打开一个文件,其文件名由传入函数的第一个参数标识,返回文件指针。其需要的第二个参数是一个字符串,指定了待打开文件的模式。
我们常见的打开文件模式有下面这些:
+"r"
以只读模式打开文件;"w"
以写模式打开文件,并且将现有文件的长度截为 0,如果文件不存在,则创建一个新文件;"a"
以写模式打开文件,在现有文件结尾添加内容,若文件不存在,则创建一个新文件;"r+"
以更新模式打开文件,亦即可以读写文件。如果打开文件失败,且不创建新文件,返回一个空指针
+值得注意的是,文件指针并不指向任何实际文件,只是指向一个包含文件信息的数据对象(换句话说是一个结构)其中包含了操作文件所用函数所需要的缓冲区信息。
+int fclose(FILE *stream)
函数fclose()
函数关闭由stream
给出的文件流,无论关闭是否成功,stream
均与这个文件无关。The behavior is undefined if the value of the pointer stream
is used after fclose
returns.
如果关闭成功,fclose()
函数返回0
,反之返回EOF
。
int fprintf(FILE *restrict stream, const char *restrict format, ...)
函数fprintf()
函数和printf()
函数基本相同,只不过输出流从默认的stdout
变成了需要自行给出的stream
,亦即函数接受的第一个参数表示需要输出的位置。
int fscanf(FILE *restrict stream,const char *restrict format, ...)
函数这个函数和scanf()
函数大差不差,只不过接受的第一个参数需要是待读取文件的文件指针。
char *strerror(int errnum)
函数返回一个指针,指向错误代码errnum
代表的文字描述。errnum
一般需要从变量errno
中取得。
long ftell(FILE *stream)
函数ftell()
函数返回一个long
类型的值,为stream
的位置标识符的当前值,亦即。如果出现错误,ftell()
函数将会返回-1
,全局变量errno
被设置为一个正值,我们可以使用errno
变量来查看错误代码。比如:
int fseek(FILE *stream, long offset, int origin)
函数fseek()
函数将文件看做是数组,将位置标识符stream
移动到目标位置。函数的第三个参数是模式,这个参数确定起始点。stdio.h
头文件内有三个表示模式的文件常量:SEEK_SET
表示文件开始处;SEEK_CUR
表示当前位置;SEEK_END
表示文件末尾。第二个参数是相对于origin
的偏移量,以字节为单位,可以为正值(前移)、负值(后移)或者0(保持不动)。
如果一切正常,fseek()
返回0
,若出现错误(比如试图移动的距离超出文件范围了),返回值为-1
。
ftell()
函数在文本模式和在二进制模式的工作方式不同,ANSI C规定,ftell()
函数的返回值可以当做fseek()
函数的第二个参数。对于MS-DOS,ftell()
返回的值将\r\n
当做一个字节计数。
函数
+函数
+函数
+size_t fread(void *restrict buffer, size_t size, size_t count, FILE * resrict stream)
函数
fread()
函数接受的参数和fwrite()
相同。在fread()
函数之中,buffer
是待读取文件数据在内存之中的地址,stream
指定要读取的文件,该函数可以用于读取文件之中的数据,size
代表着待读取数据每个元素的大小,count
代表待读取项的项数。函数返回成功读取项的项数,一般是count
,如果出现错误或者读到EOF
,返回的值就会比count
小。
值得一提的是:The file position indicator for the stream is advanced by the number of characters read.
+size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)
函数fwrite()
函数将缓冲数组buffer
里面的count
个元素写入到流stream
之中。函数将需要写入的数据重新编译为unsigned char
类型的数组,通过重复调用fptc()
函数将其写入stream
之中。和fread()
函数相同,The file position indicator for the stream is advanced by the number of characters read.
+ 在实际使用fread()
函数和fwrite()
函数读写文件之中的数据时,文件位置指示符不断前进,这使得我们不会重复读取数据,亦即可以实现这样的操作,一个循环就能实现文件从头读到尾的操作:
在C中,我们处理的是流,流是一系列连续的字节,不同属性和不同种类的输入由属性更加统一的流来表示。流告诉我们,我们可以利用和处理文件相同的方式来处理键盘输入。打开文件的过程就是把流与文件相关联,读写都通过流来完成。
+在标准头文件stdio.h
之中,定义了三个文本流stdin stdout stderr
,
结构的声明描述了一个结构的组织布局:
+ +结构体struct book
描述了由两个整数类型组成的一个结构体,后续程序中可以利用模板struct book
来声明具有相同数据组织结构的结构变量。
一般使用的结构初始化方式有三种,除了对于每一个结构成员进行值初始化之外,可以直接进行列表初始化和利用初始化器初始化:
+ +指向结构的指针具有指针的优点。指向结构的指针比结构本身更加容易操控、在函数中执行更有效率,并且有可能数据表示数据的结构中包含指向其他结构的指针。结构和数组不同,结构名代表的是这个数据集合,而不是结构变量的地址。
+结构允许嵌套,也允许使用匿名结构,还允许定义结构数组,但是结构中的的匿名结构就很恶心。
+在两个结构类型相同的情况下,我们允许将一个结构赋值给另外一个结构,这是将每个成员的值都赋给另外一个结构的相应成员。
+结构不仅可以作为参数传递,也可以作为返回值返回。与传递结构参数相比,传递结构指针执行起来很快,并且在不同版本的C上都可以执行;缺点是不能保护数据(const
限定符就可以解决了)。将结构作为参数传递的优点是,函数处理的是原始数据的副本,可以保护数据,但是需要拷贝副本,浪费了时间和内存。
我们可以在结构之中使用以下两种方式存储字符串:
+如果仅仅是声明数组并且随即初始化,那么两种声明方式是没有区别的。但是如果使用类似于scanf("%s",&names1.first1);
的方式,第二种声明方式就会出现危险。因为程序并未给first1
分配存储空间,它对应的是存储在静态存储区的字符串字面量,换言之,结构体names1
里存储的只是两个字符串的指针,如果仍要实行该操作,因为scanf()
函数将字符串放在names1.first1
对应的地址上,但是由于这是没有经过初始化的变量,地址可以是任何值,因此程序会将字符串放在任何地方。简而言之,结构变量中的指针应该只是用来程序中管理那些已经分配或者分配在别的地方的字符串。
如果使用malloc()
函数来分配内存并且用指针来存储地址,这样处理字符串就会比较合理,当然需要记得使用free()
释放相关内存。
复合字面量可以用于数组或者结构,如果活只需要一个临时结构值,符合字面量很好用,比如我们可以使用复合字面量创建一个结构赋给另外一个结构或者作为函数的参数。语法是将类型名放在圆括号之中,后面紧跟花括号括起来的初始化列表。比如(某些代码进行了适当的精简):
+利用伸缩型数组成员这一特性声明的结构,起最后一个数组成员具有一些特性:其一,该数组不会立刻存在;其二,使用这个伸缩型数组成员可以编写合适的代码,就好像这个数组确实存在并且具有所需数目的元素一样。但是对这个数组有这以下要求:首先,这个数组成员必须是结构的最后一个成员;其次,结构中至少拥有一个成员;最后,伸缩数组的声明类似于普通数组,只不过其中括号是空的。另外,声明一个具有伸缩型数组成员的结构时,我们不能使用这个数组干任何事,必须先给它分配内存空间后才能以指针形式使用它。比如:
+使用伸缩型数组成员的结构具有一下要求:第一,我们不能用结构进行赋值或者拷贝,比如*ptr1 = *ptr2
,不然这样只能拷贝非伸缩型数组成员外的所有成员,如果非要拷贝,应该使用mencpy()
函数;第二,不要按值方式将这种结构传递给函数,应该传递指针;第三,不应该将使用伸缩型数组成员的结构作为数组成员或者另外一个结构的成员。
联合(union)是一种数据类型,可以在同一个内存空间之中存储不同的数据类型(但是不是同时)。其典型用法是,设计一种表以存储既无规律,实现也不知道顺序的混合类型。使用联合类型的数组,每个联合都大小相等,每个联合可以存储各种数据类型。
+ +以上声明的联合可以存储一个int
类型、一个double
类型或者一个char
类型的值。联合只能存储一个值,其占用的内存空间是占用内存最大类型所占用的内存。使用联合的方法如下:
可以使用枚举类型(enumerated type)声明符号名称表示整型常量。使用关键字enum
可以声明枚举类型并且指定其可以具有的值(事实上,enum
常量就是int
类型,所有可以使用int
类型的地方都可以使用枚举类型)。enum
类型的用法如下:
在使用完枚举声明后,red
等就成了具有名称的常量。第二个语句声明了枚举变量color
,并且拿red
的值初始化color
。这种不连续的枚举直接按照整数进行处理,无法按照想象中直接遍历枚举内的元素。
typedef
¶利用typedef
可以为某一类型自定义名称。我们之前已经利用define
进行了自定义名称,但是define
只是单纯的文本替换,并且相比于typedef
要更为死板很多。比如:
这就明白为什么define
只不过是单纯的文本替换了。同时,利用typedef
为结构变量命名的时候,可以省略掉这个结构的标签,比如:
其实这是为一个匿名结构命名。typedef
更好的一点是可以为更加复杂的类型命名:比如typedef char (* FRPTC()) [5];
将FRPTC
声明为一个函数类型,这个函数返回一个指针,指针指向内含五个char
元素的数组。
计算机基于二进制,通过关闭和打开状态的组合来表示信息。C语言利用字节表示存储系统字符集所需的大小,通常一字节(byte)包含八位(bit),计算机界用八位组(octet)特指八位字节。可以从左到右给这八位编码为7 ~0,编号为7的位被称为高阶位,编号为0的被称为低阶位。
+如何利用二进制表示有符号整数取决于硬件,最通用的做法是利用补码。二进制补码利用一字节的第一位(高阶位)表示数值的正负,如果高阶位是0,则此时表示非负数,其值与正常情况相同;如果高阶位为1,则此时表示负数,但是这时负值的量是九位组合100000000
(256的位组合)减去这个负数的位组合。比如某负数为10011010
,其表示-122
;11111111
则表示-1
。这样,我们就可以用一字节来表示从-128~127
的所有数字。
浮点数分两部分存储:二进制小数和二进制指数。计算机中存储浮点数的时候,要留出若干位存储二进制小数,剩下的位存储指数。二进制小数用有限多个\(1/2\)的幂的和近似表示数字(事实上,二进制小数只能精确表示有限多个\(1/2\)的幂的和)。一般而言,数字的实际值是由二进制小数乘以\(2\)的指定次幂组成。
+计算机界常用八进制和十六进制计数系统,这些计数系统比十进制更加接近计算机的二进制系统。
+0
来特别表示一个数是八进制。0x
来特别表示一个数是十六进制。十六进制是表示字节的非常好的方式。按位取反(反码):~
按位与:&
按位或:|
按位异或:^
<<
>>
没看懂,先不写。
+assert
库¶stdio
库¶int sprintf(char *buffer, const char *format,...)
函数
向字符串buffer
里写入,相当于多了一个转换格式/读写的工具函数。
int fprintf(FILE *stream, const char *format,...)
函数
sscanf()
函数
fscanf()
函数
向输出流stream
中写入。
stdlib
库¶void srand(unsigned int seed)
srand()
函数为伪随机数生成器rand()
播种,正常的用法是:srand((unsigned int) time(NULL))
这段代码利用当前时间为伪随机数生成器rand(0)
提供种子,这样子就可以得到了近似于真随机的随机数。
int rand(void)
伪随机数生成器rand()
生成一个介于0
到RAND_MAX
的随机数。如果没有srand()
的播种,rand()
函数就会默认生成种子为1的随机数。每次调用rand()
函数,我们得到的都是上次生成的随机数的下一个数
值得注意的是,在调用函数rand()
之前的时候,伪随机数生成器只应该被播种一次。
++Generally speaking, the pseudo-random number generator should only be seeded once, before any calls to
+rand()
, and the start of the program. It should not be repeatedly seeded, or reseeded every time you wish to generate a new batch of pseudo-random numbers.
更重要的是,当rand()
接受相同的种子的时候,他会生成相同的随机数数列。
time
库¶time_t
这是一个适合储存日历时间的长整型(long int)
变量,表示着从POSIX time (1970年1月1日00:00)开始的总秒数。time_t time(time_t *seconds)
time()
函数将当前日历时间作为一个time_t
类型的变量返回,并且将这个变量存储在输入的指针seconds
中(前提是这个指针不为空指针)。
由于time_t
类型其实是一个long int
转换成int
(或者unsigned int
)的时候还是需要强制转换说明的2
约 0 个字
+约 6007 个字 57 行代码 预计阅读时间 21 分钟
+Abstract
+这是我学习Java语言的笔记,动机很简单,我想听的CS61B与Algorithms课程都是基于Java的,所以我需要学习Java。
+参考书籍:
+++Java is a high-level, class-based, object-oriented programming language.
+
Java的类库源文件在JDK中以压缩文件lib/src.zip
的形式发布,其包括了所有公共类库的源代码,解压缩这个文件就可以得到源代码。
使用命令行工具,我们可以编译和运行Java程序。编译Java程序使用javac
命令,javac
程序是一个Java编译器,将我们的代码编译成字节码文件,也就是类文件(扩展名为.class
);再使用java
命令启动Java虚拟机,执行编译器编译到类文件的字节码。
编译器需要文件名,需要提供扩展名.java
,而虚拟机需要类名,不需要提供扩展名。
调用方法的通用语法是object.method(parameters)
,其中object
是一个对象,method
是对象的一个方法,parameters
是方法的参数。
对于这段最简单的代码:
+关键词public
被称为访问修饰符/Access modifier,决定了控制程序其他部分对这部分代码的访问级别。class
表示Java程序中的全部内容都包含在类中,类是Java应用的构建模块。一个源文件只能有一个公共类,但是可以有任意数量的非公共类,源文件的文件名必须和公共类的类名相同,并且用.java
作为扩展名。
在执行已经编译的程序的时候,虚拟机总是从指定类的main
方法的代码开始执行,所以类的源代码中必须包含一个main
方法,且main
方法必须声明为public
,当然直接声明全套public static
也是极好的。方法其实就是函数的另外一种说法,我们也可以自行定义方法并且添加到类中。
Java是一种纯粹的面对对象的语言,面对对象的程序是由对象组成的,每个对象包括对用户公开的特定功能与隐藏的实现。在面对对象程序设计中,数据是第一位的,之后我们才考虑操作数据的大小。
+类/Class指定了如何构造对象,通过一个类构造/Construct对象的过程称为创建这个类的一个实例/Instance。封装/Encapsulation是面对对象程序设计的一个重要概念,是指将数据与行为组合在一个包中,并对对象的使用者隐藏了具体的实现细节。对象中的数据称为实例字段/Instance field,操作数据的过程称为方法/Method,作为一个类的实例,一个对象有一组特定的实例字段值,这些值的集合就是这个对象的当前状态/State。只要在对象上调用一个方法,它的状态就有可能发生改变。
+封装的关键在于,不能让其他类的方法直接访问这个类的实例字段。我们还可以通过扩展其他的类来构建新类,这个新类具有被扩展的类的所有属性与方法,这种通过扩展一个类来得到另外一个类的过程叫做继承/Inheritance。
+使用面对对象编程之前,必须清楚对象的三个主要特性:
+对象的标识是两两不同的,每个对象都有一个唯一的标识。
+类之间的最常见的关系有:依赖/uses-a,聚合/has-a与继承/is-a。
+依赖/Dependence是最一般且最明显的关系,比如Order
类使用了Account
类,因为Order
类需要访问Account
类来获取信息。应该尽可能减少相互依赖的类,或者说减少类之间的耦合/Coupling。因为耦合度越低,越不容易在修改一个类的时候影响其他类。
聚合/Aggregation表明了一个类包含另外一个类的对象。
+继承/Inheritance表示了一个更特殊的类与一个更一般的类之间的关系,在特殊化的类里边定义了更多的特殊方法与额外功能。
+在Java中,没有类就不能做任何事情,但是并不是所有类都表现出面对对象的典型特征,比如Math
类,它只包括了一些方法,甚至没有实例字段。下面我们将以Date
类与LocalDate
类为例说明类的使用。
在Java中,我们需要使用构造器/构造函数/Constructor来构造新的实例,构造器是一种特殊的方法,用来构造并且初始化对象,并且构造器总是与类同名。我们看几个例子:
+new Date();
:使用new
操作符,我们就可以构造一个Date
对象,并且将这个对象初始化为当前的日期与时间。String s = new Date().toString();
我们可以对这个对象应用一个方法,将这个日期转换成一个字符串。System.out.println(new Date());
我们也可以将这个对象传递给一个方法。Date rightNow = new Date();
我们定义了一个对象那个变量rightNow
,其可以引用Date
类型的变量,并且将新构造的对象存储在对象变量rightNow
中。对于对象变量而言,他们并不包含一个对象,只是引用一个对象,我们可以显式地将对象变量初始化为null
,这就表明这个变量目前没有引用任何对象,对一个赋值为null
的变量,我们不允许应用任何方法。
尽管我们使用了引用这个词,但是Java中的对象变量更像是C++中的对象指针,并且Java的语法甚至和C++的是一样的。所有的Java对象都存储在堆之中,当一个对象包含另外一个对象变量的时候,其实只是包含了另外一个堆对象的指针。
+上面提到的Date
类的实例有一个状态,就是一个特定的时间点,时间是距离另外一个固定的时间点的毫秒数,这个时间点就是所谓的纪元/Epoch,在Java中,纪元是UTC时间1970年1月1日00:00:00。
LocalDate.now();
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);
int year = newYearsEve.getYear();
/int month = newYearsEve.getMonthValue();
/int day = newYearsEve.getDayOfMonth();
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
:访问器方法/Accessor method,与更改器方法/Mutator method。我们不仅仅需要学会使用常用的类与配套的方法,还需要学会编写类。我们经常写的一种类叫做主力类/Workhorse class,这种类没有 main
方法,但是有自己的实例字段和实力方法,是构建一个完整程序的众多部分之一。
最简单的类形式为:
+ +实例字段可以是基本类型,也可以是对象。我们一般需要将实例字段声明为 private
,这确保只有这个类的方法可以访问这种字段,这就是封装的一部分。而方法可以声明为 private
或者 public
,public
方法可以被任何类的任何方法调用。private
方法只可以被本类的其他方法调用。
我们先看一个自定义类的例子:
+从构造器开始看,构造器与类同名。构造Employee
类的对象时,构造器会运行,将实例字段初始化为所希望的初始状态。构造器没有返回值,可以有参数,也可以没有参数。
在声明对象变量的时候,我们可以用var
关键字声明,Java会根据变量的初始值推导出其类型。比如,对上面的类,我们只需要声明var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
就可以了。
在使用构造器初始化一个对象的时候,有可能将某个实例字段初始化为null
,但又有可能对这个字段应用某个方法,一个“宽容”的解决方法就是将null
参数转换成一个适当的非null
值,Objects
类给予了一种便利方法:Objects.requireNonNullElse(T, defalt obj)
,如果输入的对象T
是null
,那么就将返回默认值default obj
。同样地,其实还提供了一种“严格”的方法Objects.requireNonNull(T, string message)
,如果输入的对象T
是null
,那么就将抛出一个NullPointerException
异常并且显示出问题的描述。
方法会操作对象并且访问其实例字段,方法一般会有两个参数,第一个参数是隐式参数/Implicit parameter,出现在方法名之前;第二个参数是显式参数/Explicit parameter,出现在方法名之后的括号里面。在方法中,我们可以使用this
关键字来引用隐式参数。
封装是极其有必要的,我们来看一些比较特殊的方法,这些方法只访问并且返回实例字段的值,因此被称为字段访问器/Field accessor。相比于将实例字段声明为public
,编写单独的访问器会更加安全:外界的代码不能修改这些实例字段,并且如果这些实例字段出了问题,我们直接调试字段访问器就可以了,如果是public
的话,我们就需要在所有的代码中寻找问题。
有时,想要获取或者改变一个实例字段的值,我们需要提供下面三项内容:
+这样的处理非常好,首先可以改变内部实现而不改变该类方法之外的任何其他代码;其次,更改器方法可以完成错误检查,这就非常好了!
+另外,不要编写返回可变对象引用的访问器方法,这样我们好好的封装就毁了!如果硬要返回一个可变对象的引用的话,首先应该对它进行克隆/Clone,克隆是指存放在另外一个新位置上的对象副本,使用类重的clone
方法可以完成这个操作。
访问权限是一件非常重要的事情。方法可以访问所属类的对象的任何数据,当然包括私有数据,但是不能访问其他对象的私有数据。
+尽管大部分方法都是公共的,但是某些情况下,私有方法可能会更加有用,比如我们希望将一个计算方法分解成若干个独立的辅助方法,但是这些方法不应该作为公共接口,这是因为其与当前实现关系非常紧密,或者需要一个特殊的协议或者调用次序,这些方法就应该实现为私有方法。实现私有方法很简单,只需要将关键字public
改为private
就好了。
如果一个方法是私有的,并且类的作者确信这个方法不会在别处使用,这个方法就可以被简单地剔除,但是如果方法是公共的,就不能简单地删除一个方法了,因为还有可能会有其余的代码依赖于这个方法。
+我们可以将一些变量、类与方法设置为final
。当我们定义一个类时使用了final
修饰,这个类就不能被继承,这个类的成员变量可以根据需要设为final
,但是类中的所有成员方法都会被设为final
。如果定义了一个方法为final
,这个方法就被“锁定”了,无法被子类的 方法重写,对方法定义为final
一般常见于认为这个方法已经足够完整,不需要改变了。修饰变量是final
用的最多的地方,final
变量必须显式指定初始值,并且一旦被赋值,就不能给被重新赋值;如果final
的是一个基本变量,这个变量就不能改动了,如果是一个引用变量,那么对其初始化之后就不能再指向其他变量。
比如private final StringBuilder evaluations = new StringBuilder();
,这里的evaluations
就不能指向别的对象了,但是这个对象可以修改,比如evaluations.append(LocalDate.now() + ":Yes!")
。final
修饰符对于类型为基本类型或者不可变类的字段尤其有用,并且final
修饰符一般与static
修饰符一起使用。
static
静态¶我们发现,先前的很多方法都标记了 static
修饰符,下面会讨论这个修饰符的含义。
static
,那么这个字段并不会出现在每个类的对象之中。静态字段只有一个副本,可以认为这个字段属于整个类,这个类的所有实例将共享这个字段。Math
类的 PI
字段,这个字段是一个不会改变的常量,事实上被定义为 public static final double PI = 3.14159265358979323846;
。System.out
也是一个经常使用的 final
的静态常量。this
关键字,但是可以访问静态字段。一般使用类名来调用静态方法,比如 Math.pow(2, 3)
,但是甚至可以使用对象调用静态方法,虽然静态方法的调用的结果和这个对象毫无关系。
+ 当方法不需要访问对象状态的时候,可以使用静态方法,这种情况下所有的参数都由现实参数提供;只要访问静态字段的时候,当然应该使用静态方法。LocalDate.now()
就是一个工厂方法。main
方法:main
方法是一个特殊的静态方法,是程序的入口,虚拟机调用这个方法来执行程序。事实上启动程序的时候还没有对象,就只好让入口方法是静态的了。程序设计语言中,将参数传递给方法一般有两种方法,按值调用/Call by Value与按引用调用/Call by Reference。在 Java 中,所有的参数总是按值调用的,也就是说,方法得到的是所有参数值的一个拷贝。下面看三段代码:
+percent
的值变为 30
,那么我们就错了,percent
的值仍然是 10
。这是因为传递的是 percent
的值,x
被初始化为 percent
的值的一个副本,然后 x
被修改,这个方法结束之后,参数 x
被丢弃,但是 percent
没有被修改。这个例子表明一个方法是无法修改基本数据类型的参数的。
+尽管基本数据类型的参数无法被修改,但是毕竟方法参数有两种,另一种是对象引用。对象参数还是可以修改的,尤其是作为隐式参数的对象的字段。 +
数据类型就是一组数据与对其能进行的操作的集合。
+Java是典型的C类语言,声明变量的方法与C极为相似,但是在Java中,变量名的标识符的组成得到了扩充:字母、数字、货币符号与“标点链接符”组成变量名,首字母不能为数字。特别地,字母、数字与货币符号的范围更大,字母可以是一种语言表示中的任何Unicode字符,数字可以是0
到9
与表示一位数字的任何Unicode字符。
Java最基本的类型有下面几种,六种数字类型,一种字符类型,一种布尔型:
+整型
+双精度实数类型,与其对应的算数运算符(double
)。
我们使用double
类型来表示双精度实数,使用64位,值域非常之大。
字符型char
:char
使用UTF-16方案进行编码,以前原本用于表示单个字符,但是现在情况变化了,有的Unicode字符需要两个char
值。char
类型的字面量要用单引号括起来,也可以表示为十六进制的值,比如\u0041
就是A
。
在Unicode编码之前,已经有许多编码标准了,我们最熟悉的就是美国的ASCII编码。标准不统一会出现下面两个问题:对一个特定的代码值,在不同的机制中对应不同的字母;大字符集的语言的编码长度会有不同,有的是单字节编码,有的就使用双字节或者多字节编码。
+Java的字符型使用的16位编码在当时设计时的确是很好的改进,但是现在的Unicode字符已经超过65536个,这就尴尬住了。所以一个实用的建议就是不要在程序之中使用char
类型,除非要处理UTF-16代码单元,否则就使用String
类型。
还是简单介绍一下Unicode编码吧:码点/Code point是指与一个编码表中某个字符对应的代码值,Unicode中的码点使用十六进制书写,并且在前面加上一个U+
,U+0041
就是A的码点。Unicode中的码点可以分为17个代码平面/Code plane。第一个代码平面被称为基本多语言平面/Basic multilingual plane,其包含了从U+0000
到U+FFFF
的“经典”Unicode编码,其余的16个代码平面从U+10000
到U+10FFFF
,包含了各种辅助字符/Supplementary character,这些平面包含了一些不常用的字符,比如一些古代文字、表意文字等等。
UTF-16使用不同长度的代码表示所有Unicode码点,在基本多语言平面之中,每个字符使用16位表示,被称为代码单元/Code unit,辅助字符使用一对连续的代码单元表示,这种编码对使用基本多语言平面中未采用的2048个值范围(称为替代区域,U+D800
到U+DBFF
用于第一个代码单元,U+DC00
到U+DFFF
用于第二个代码单元)。而Java中的char
类型描述就采用了UTF-16编码的一个代码单元。
概念上讲,Java 字符串就是字符序列,Java 没有内置的字符串类型,但是标准库中提供了预定义类 String
,每个被双引号括起来的都是一个 String
类的实例,下面聊的主要是 String
类的使用。
首先,字符串可以为空,比如 String emp = "";
,空串是一个长度为 0 的字符串,可以使用 ""
表示,null
是一个特殊的值,表示没有任何对象和这个对象关联。不能对 null
调用任何方法,否则会抛出 NullPointerException
异常。
如果要检查某个字符串既不是空串也不是 null
,一般会这样做 if (str != null && str.length() != 0)
。
子串更像切片,我们可以使用 substring
方法来获取一个字符串的子串,substring
方法有两个参数,第一个参数是子串的起始位置,第二个参数是子串的结束位置,但是不包括结束位置的字符,我们也可以认为第二个参数指的是尾后字符,这种尾后元素的使用其实蛮常见的,在 C++ 的学习中就可以看见这一点。比如 String greeting = "Hello"; String s = greeting.substring(0, 3);
,这样 s
就是 Hel
。
拼接很简单,使用 +
号就可以了,比如 String expletive = "Expletive"; String PG13 = "deleted"; String message = expletive + PG13;
,这样 message
就是 Expletivedeleted
。任何非字符串的值和字符串进行拼接的时候,Java 会将非字符串的值转换为字符串,甚至任何一个 Java 对象都可以转换成字符串。
值得一提的是,String
的运算符 +
和 +=
是 Java 里边仅有的重载的运算符,Java 不允许程序员重载别的运算符。
如果我们查看 JDK 文档,就会发现 String
类其实是不可变的/Immutable,每个看似会修改 String
值的方法,实际上都创建并且返回了一个全新的 String
对象,这个对象包括了修改后的字符串内容,但是原始的 String
对象保持不变。这样设计的原因之一是:参数一般是用来提供信息的,而不是用来修改的,这对代码的可读性和可理解性有很大的帮助。同时,编译器甚至可以让字符串共享。
使用 equal
方法检测两个字符串是否相等,对于表达式 s.equals(t)
,如果相等就返回 true
,否则返回 false
,这个方法是区分大小写的,如果不区分大小写,可以使用 equalsIgnoreCase
方法。
由于每个被双引号括起来的字符串都是一个 String
类的实例,所以我们可当然可以对其使用 equals
方法,"HeLLo".equals("HeLLo")
就是 true
。
int compareTo(String other)
和 int compareToIgnoreCase(String other)
方法用于比较两个字符串,如果调用字符串 this
在字典中排在参数字符串 other
之前,就返回一个负数,如果调用字符串在字典中排在参数字符串之后,就返回一个正数,如果两个字符串相等,就返回 0
。
matches
int length()
:返回字符串的长度。boolean isEmpty()
和 boolean isBlank()
:判断字符串是否为空或者由空白符组成。boolean startsWith(String prefix)
和 boolean endsWith(String suffix)
:判断字符串是否以指定的前缀或者后缀开始或者结束。从人类可读的文件或者标准输入中读取输入非常重要,但是比较痛苦,一般的解决方法是读入一行文本,然后进行分词解析,再使用 Integer
类和 Double
类中的方法解析数据。但是 Java 提供的 Scanner
类就大大减轻了这个负担。
传统一点的做法是:
+但是使用 Scanner
类就简单多了,Scanner
类定义在 java.util
包之中,首先还是需要构造一个与输入流相关联的 Scanner
对象,Scanner in = new Scanner(System.in);
,然后就可以使用 nextInt
、nextDouble
、next
等方法来读取输入了。
Scanner(InputStream in)
用给定的额输入流构造一个 Scanner
对象。String nextLine()
读取下一行输入。int nextInt()
和 int nextDouble()
读取下一个整数或浮点数,String next()
读取下一个单词,这些都是以空白符作为分隔符的。boolean hasNext()
用于检查是否还有其他单词。boolean hasNextInt()
和 boolean hasNextDouble()
用于检查下一个字符序列是否表示整数或浮点数。约 0 个字
+约 3112 个字 66 行代码 预计阅读时间 11 分钟
+变量名只能包含字母、数字和下划线 “_”,变量名的第一个字符只能是字母或者下划线,变量名对大小写敏感,且不能将关键字 (亦即保留字) 作为变量名。
+Python中的变量不需要声明数据类型,每个变量在使用之前必须赋值,变量在赋值之后才会被创建。我们在创建变量的时候不需要考虑变量的类型——这和C不同——变量就是变量,没有类型,而我们所说的“类型”只是变量所指的内存中对象的类型,因为不同的对象 (比如字符串和整数) 需要以不同的方式存储。并且我们可以为多个变量同时赋值,也可以为多个对象指定多个变量。比如下面的例子:
+Python内置的type()
函数可以用来查询变量所指的对象类型,并且可以使用isinstance()
来判断:
type()
和isinstance()
的区别在于:
type()
不会认为子类是一种父类类型;isinstance()
会认为子类是一种父类类型。字符串就是一系列字符,由引号 (可以是单引号或者双引号) 确定,其列表有两种索引方式:从左到右默认从0开始;或者从右向左从-1开始。对于字符串的某些处理和C类似,比如我们可以实现以下操作:
+我们可以发现,字符串的某些处理其实就是对列表的处理。但是值得注意的是,Python没有单独的字符类型,一个字符就是长度为1的字符串,并且Python的字符串不能被改变。
+在Python中,我们可以使用方法进行对数据的操作,某些方法会改变数据内容 (比如reverse()
),某些则不会 (比如下面这些)。对于字符串,利用lower()
、upper()
、title()
、strip()
、lstrip()
和rstrip()
,我们可以去除字符串两侧的空格,并且调整字符的大小写。比如:
在Python中,空白泛指所有非打印字符,比如空格、列表和换行符。print()
在打印结束之后会自动换行,但是如果在print()
函数之后参加end=""
参数,我们就可以实现不换行效果,事实上Python里end
参数的默认值为"\n"
,表明在打印的字符串的末尾自动添加换行符,来设定特定符号,比如上面代码的最后一行将字符串的末尾自动添加了一个@
。
Python支持int
、float
、bool
、complex
四种类型。整数类型只有一种int
,表示为长整型。
求模运算符%
、四则运算符+ - * /
与取余运算符%
均和C语言中的相同,只是默认情况下的除法/
的结果是一个浮点数,而使用运算符//
就可以得到一个整数。此外,运算符**
表示乘方。
以下函数均将接受的参数进行转化并返回。
+str()
函数
int()
函数将字符串表示的整形数值转化成整型数字。
float()
函数将字符串表示的浮点数转化成浮点数。
列表由一系列按照特定顺序排列的元素组成,我们用方括号 []
表示列表,并且用逗号分隔其中的元素。列表中的元素可以不相同,甚至可以包含列表 (也就是嵌套)。列表的索引和字符串的索引相同,从0
开始;或者从尾部开始,最后一个元素的索引为-1
,往前一位为-2
,以此类推。
需要注意的是,sorted()
函数和sort()
方法 (和排序相关的) 均不能用在字符串和数字混合的列表 (元组和字典) 排序之中。
使用圆括号 ()
表示元组,当元组只有一个元素的时候,只能写成 (a, )
的形式,这是因为 (a)
其实表示的是一个值。
使用花括号 {}
括起来表示集合,内部元素使用逗号来分割。
也是使用花括号 {}
括起来表示,但是字典内部存储的是键值对 {key: value, ...}
。
可以发现添加键-值对的语法和修改值的相同。
+如果要遍历字典内的键-值对,我们需要先声明两个变量,使用items()
方法,它返回一个键-值对列表,在遍历每一个键值对的过程之中,会将键和值依次存储到两个变量之中。同理keys()
方法和values()
方法分别返回一个存储着键和值的列表,值得注意的是这些列表的元素顺序和其原本的存储顺序不相同,因为Python只关心键与值的对应关系,而不关心存储顺序。
Python支持三个布尔运算符:and
、or
和not
,它们的内容和C中的&&
、||
和!
相同,优先级也相同:not
有最高的优先级 ,and
其次,or
的优先级最低。同时,布尔运算符还支持短路运算(short circiuting),即如果and
和or
的第一个操作数(也就是operand)一定可以决定表达式的真值,就不会检查后面的表达式,所以True or 1 / 0
和False and 1 / 0
甚至都没问题。
需要注意的是,and
和or
都不将返回值限定为True
或者False
,相反,它们返回最后一个求值的参数(the last evaluated argument),比如字符串s
需要在它为空的时候被替换成一个默认值,就可以使用s = s or "Default"
来处理。但是not
总需要创建一个新的值,所以不管接收到的参数是不是一个布尔值,都返回布尔值。
Python将0
、None
、''
(空字符串)和[]
(空列表)都规定为False
,其余值规定为True
。这表明了布尔值的范围其实比纯粹的True
和False
更加丰富。
语句值由解释器运行的,执行一项操作的语句(A statement is executed by the interpreter to perform an action)。
+复合语句由一个或者多个子句(clause)组成,每个子句由一个句头(header)和一个句体(suite)组成,组成复合语句的子句头都处于相同的缩进层级。 每个子句头以一个作为唯一标识的关键字开始并以一个冒号结束。 子句体是由一个子句控制的一组语句。 子句体可以是在子句头的冒号之后与其同处一行的一条或者由分号分隔的多条简单语句,或者也可以是在其之后缩进的一行或多行语句。 只有后一种形式的子句体才能包含嵌套的复合语句。简而言之,复合语句大概长这样:
+def
语句、if
语句和while
语句都是标准的复合语句。if
语句¶for
循环¶while
循环¶利用continue
和break
来跳过此次循环或者跳出循环,逻辑和C一样。
while
循环还可以这样用:根据对列表判断是否非空来控制循环。
这个循环只会在列表unconfirmed_users
非空的时候运行,当它变空的时候,就不会继续运行了。
assert
语句¶assert
语句的基本语法如下:assert_stmt ::= "assert" <expression> ["," <expression>]
。当assert
后面的<expression>
的布尔值是False
时,程序会中断运行,并且抛出AssertionError: <expression>
,这里的<expression>
对应的是方括号里的<expression>
。一个例子如下:
return
语句¶A return statement completes the evaluation of call
+函数就是一个带名字的代码块,用于完成具体的工作。关键字def
表示开始定义一个函数,向Python指出函数名,并且提供形参。函数定义下所有缩进行构成函数体,被三个引号引起来的部分是称作文档字符串的注释,描述函数是做什么的,Python用其生成有关程序中函数的文档。
在定义中括号里出现的变量是形式参数 (Formal Parameter),是函数完成工作需要的信息,在调用函数时传入的是实际参数 (Actual Argument),是调用函数的时候传递给函数的信息,值存储在相应的形式参数之中。
+我们认为赋值操作是一种简单的抽象(abstraction)方式,它将变量的名字与其值联系到了一起;而函数定义是一种更强大的抽象方式,它允许将名字与表达式联系到了一起。函数由函数签名与函数体组成。
+函数签名(function signature)<name>(<formal parameters>)
表明了函数的名字与接受的形式参数的数量;
函数体(function body)<expression>
决定了使用函数时的计算过程。
def
语句的执行过程如下:
<name>(<formal parameters>)
;<name>
to that function in current frame.Procedure for calling/applying user-defined functions(version 1):
+函数的定义域(domain):
+函数的值域(range):
+函数可以分为纯函数(pure function)和非纯函数(non-pure function)两类,纯函数指的是没有副作用(side-effect)的函数,反之,非纯函数有副作用,print()
函数就是典型的非纯函数,它将内容显示在终端上。如果一个函数体内没有return
语句,函数会自动返回None
,print()
就是这样返回的。A side effect isn't a value:it is anything that happens as a consequence of calling a function.
有意思的是,在终端中,print()
会显示没有引号的文本,但是return
会保留引号。下面是一个例子:
None
¶None
在Python里面表示一种特殊的值:Nothing。
None
其实有着其相应的数据类型:NoneType
,所以在进行None + 4
的操作的时候,就会出现类型错误:TypeError: unsupported operand type(s) for +: 'NoneType' and 'int',这表明None
和一般的数据类型不能相加。
input()
函数接受一个参数,即需要向用户显示的提示 (prompt) ,暂停程序运行并将提示输出到屏幕上,等待读取用户输入,在按下回车键之后继续运行,并且将用户的输入作为input()
函数的返回值。
Lambda expressions (sometimes called lambda forms) are expressions that evaluate to fuctions by specifying two things: the parameters and a return expression. Lambda expressions are used to create anonymous functions. The expression lambda parameters : expression
yields a function object, and this unnamed object behaves as a functions object defined with:
While both lambda
expressions and def
statements create function objects, there are some notable differences. lambda
expressions work like other expressions; much like a mathematical expression just evaluates to a number and does not alter the current environment, a lambda
expression evaluates to a function without changing the current environment: It does not create or modify any variables.
Definition: A function is called recursive if the body of that function calls itself, either directly or indirectly.
+Implication: Executing the body of a recursive function may require applying that function again.
+The anatomy of a recursive function:
+operator
模块¶opertaor
模块包含了一套和Python内置的运算符对应的高效率函数,包含的种类有:对象的比较运算、逻辑运算、数学运算和序列运算。
--- | +--- | +--- | +
---|---|---|
+ | + | + |
operator.add() | +add(1,2) | +3 | +
operator.mul() | +mul(2,3) | +6 | +
+ | + | + |
+ | + | + |
+ | + | + |
+ | + | + |
+ | + | + |
+ | + | + |
Docstring:
+An expression describes a computation and evaluates to a value.
+Evaluation procedure for call expressions:
+All expressions can use function call notation. (demo)
+Call expression:
+Environment diagrams visualize the interpreter's progress.
+a function's signature has all the information needed to create a local frame.
+Pure functions: only return values.
+Non-pure functions: have side effects.
+print()
is a non-pure function because it displays its output depending on the argument passed in , and returning None
.
print()
statements¶约 3220 个字 56 行代码 预计阅读时间 11 分钟
+条目 | +惯例 | +
---|---|
模块 Modules | +snake_case |
+
类型 Types | +UpperCamelCase |
+
特征 Traits | +UpperCamelCase |
+
枚举 Enumerations | +UpperCamelCase |
+
结构体 Structs | +UpperCamelCase |
+
函数 Functions | +snake_case |
+
方法 Methods | +snake_case |
+
通用构造器 General constructors | +new or with_more_details |
+
转换构造器 Conversion constructors | +from_some_other_type |
+
宏 Macros | +snake_case! |
+
局部变量 Local variables | +snake_case |
+
静态类型 Statics | +SCREAMING_SNAKE_CASE |
+
常量 Constants | +SCREAMING_SNAKE_CASE |
+
类型参数 Type parameters | +UpperCamelCase ,通常使用一个大写字母: T |
+
生命周期 Lifetimes | +通常使用小写字母: 'a ,'de ,'src |
+
使用 let
关键字绑定一个变量,变量绑定默认是不可变的,如果需要可变绑定,使用 let mut
关键字如果后边不会改变的变量被声明为 mutable 的话,编译器会给出警告,如果在存在没有使用的变量的话也会给出警告,在变量名字之前加上单下划线就会忽略未使用的变量。
使用 let
关键字还可以进行复杂变量的解构,这也就是从一个相对复杂的变量之中,匹配出这个变量的一部分内容。
作用域和别的编程语言没有区别,可以参考块作用域。
+对于以拷贝值的方式完成的赋值,没有所有权的转移。
+ +这段代码当然是通过拷贝值完成赋值的,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。当然没有所有权的转移。
+ +String
类型和上面的整数类型很不一样,是由存储在栈中的堆指针、字符串长度和字符串容量共同组成的,总之指向了一个在堆上的空间。let s2 = s1;
这行代码会让 s1
被赋给 s2
,而一个值同时只能被一个变量所拥有,所以 Rust 认为 s1
不再有效,在赋值完成之后就马上失效了。
这就是所有权转移,对应的操作是移动而不是拷贝,我们将对这个字符串的所有权从 s1
转移到了 s2
。s1
不指向任何数据,只有 s2
才有效。
如果不发生所有权转移,那么在两个变量同时同时离开作用域的时候,就会尝试释放相同的内存,这就会出现了二次释放的错误,会导致内存污染。而发生所有权转移后,如果还尝试使用旧的所有者 s1
,Rust 就会禁止你使用无效的引用。
如果我们确实需要深度复制 String
堆上的数据,就要使用克隆/深拷贝,let s2 = s1.clone();
会在堆上分配一块新的内存,将 s1
的数据拷贝到新的内存中,这样就不会发生所有权转移了。
与深拷贝相对的是浅拷贝,正常的拷贝其实就是浅拷贝,浅拷贝发生在栈中,效率很高。
+Rust 有一个叫做 Copy
的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。
那么什么类型是可 Copy
的呢?可以查看给定类型的文档来确认,这里可以给出一个通用的规则: 任何基本类型的组合可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的。如下是一些 Copy
的类型:
Copy
的时候。比如,(i32, i32)
是 Copy
的,但 (i32, String)
就不是&T
,但是注意:可变引用 &mut T
是不可以 Copy的。函数在传值的时候也会发生移动或者复制,相应的发生所有权的转移:
+如果在 takes_ownership
函数后边尝试再使用 s
,就会出现所有权报错,因为 s
对于 String
的所有权在函数传值的时候已经移动给了 some_string
,随后 take_owmership
结束的时候,some_string
的值内存被 drop
了,加上原本的 s
的所有权已经移动,所以 s
就无效了。如果函数调用完了还想使用 s
,一种方法是传递 s.clone()
,另一种方法是返回值:
这里利用了函数返回的时候也会发生所有权的转移,所以 some_string
的所有权在函数返回的时候又转移给了 s
,s
又可以使用了。但这里要求 s
是可变的,即便传来传去都是一个 String
类型,但是变量还是发生了变化。
Rust 也支持类似于使用指针和引用的方式简化传值的流程,利用借用/Borrowing这个概念完成上述目的。借用是指获取变量的引用。
+常规引用是一个指针类型,指向了对象存储的内存地址。使用 &
进行引用,使用 *
进行解引用。
使用借用可以进行函数调用,并且维持所有权:
+我们首先创建了 s
的引用并且将其传入,这样,我们通过操纵引用来操纵 s
,在函数调用结束的时候,string
离开作用域,但是它并不拥有任何值,所以不会发生什么。
上面创建的都是不可变引用,一直处于只读状态,也就是说,不能在 calculate_length
函数中修改 string
的值,比如 string.push_str("...");
,如果需要修改,可以使用 &mut
创建可变引用:
这里创建的就是可变引用了,可以通过引用来更改变量的值。但是对于可变引用, Rust 存在着一些限制:
+这样做的目的是避免产生数据竞争,以及防止不可变引用的值被可变引用所改变。数据竞争可由以下行为造成:
+另外,引用的作用域 s
从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }
。
以及如果存在引用,且后面用到了这个引用,则被引用的即使是 mut
的,也不能被修改,例如:
我们也应该注意悬垂引用/Dangling Reference,悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。Rust 编译器会在编译时检测到悬垂引用并且报错。下面是一个悬垂引用的例子:
+Rust 的类型可以分为两类:基本类型和符合类型。基本类型意味着其是一个最小化原子类型,无法解构为其他类型,有以下几种:
+i8
, i16
, i32
, i64
, i128
, isize
, 无符号整数 u8
, u16
, u32
, u64
, u128
, usize
, 浮点数 f32
, f64
;bool
, 字面量为 true
和 false
;char
,用单引号括起来的 Unicode 字符;()
,只有一个值 ()
,main
函数的返回值就是 ()
,这玩意其实就是一个零长度的元组。复合类型是由其他类型组合而成的,最典型的就是结构体 struct
,有以下几种:
序列是生成连续的数值的
+要显式处理溢出,可以使用标注怒对原始数字类型提供的这些方法:
+wrapping_*
:在所有模式下都按照补码循环溢出规则处理;overflowing_*
:返回该值和一个指示是否发生溢出的布尔值;saturating_*
:限定计算后的结果不超过目标类型的最大值或最小值;checked_*
:如果溢出则返回 None
。字符串大抵分为两种,被硬编码到程序代码之中的不可变的字面量 str
,和用堆动态分配内存的可变的 String
类型。在语言级别来说,其实只有一种字符串类型 str
,并且一般是以引用形式 &str
出现的,存储的时候是一个指针和字符串长度。String
类型是标准库提供的一个字符串类型,它是一个可变的、可增长的、具有所有权的 UTF-8 编码的字符串类型。
String
和切片¶使用 String::from
方法将一个字符串字面量转换为 String
类型,这里的 ::
是一种调用操作符,这里表示调用 String
模块中的 from
方法,由于 String
类型的变量 s
存储在堆上,因此它是动态的,如果 s
是 mut 的,可以通过 s.push_str("...")
来追加字面量:
基于上面的代码,下面介绍切片:切片就是对 String
类型之中某一部分的引用,类型就是 &str
,通过 [begin..off_the_end]
指定引用范围,这个范围是左闭右开的(参考 C++ 的尾后迭代器),这和别的编程语言一样。我们可以认为这个语法其实就是数值类型一节中范围的语法,所以 [begin..=end]
就生成了一个闭区间的范围。
简单说来:
+let
就是一个经典的语句,只负责绑定变量和值,但是不返回值;let a = 1;
就是一个语句,1
其实就是一个表达式;()
;()
。上面是典型的函数定义,下面是几个需要注意的点:
+fn
定义一个函数;()
,因为这种情况下编译器会自动推断返回类型,都要显式指定返回类型;return
关键字,带不带分号都可以;()
;!
,一般用于一定会抛出 panic 的函数,或者无限循环的函数。宏在编译过程中会扩展为 Rust 代码,并且可以接受可变数量的参数。它们以 !
结尾来进行区分。Rust 标准库包含各种有用的宏。
println!(format, ..)
在标准输出中打印一行字符串;format!(format, ..)
的用法与 println!
类似,但并不打印,它以字符串形式返回结果;dbg!(expression)
会记录表达式的值并返回该值;todo!()
用于标记尚未实现的代码段。如果执行该代码段,则会触发 panic;unreachable!()
用于标记无法访问的代码段。如果执行该代码段,则会触发 panic;assert_eq!(left, right)
用于断言两个值是否相等;约 88 个字
+Info
+为啥要学 Rust?我暂时还不知道,或许你可以问半年之后的我:-)
+Rust 有一些强大的优势,在 Comprehensive Rust 中是这么讲的:
+约 2 个字
+约 0 个字
+约 993 个字 20 行代码 预计阅读时间 4 分钟
+Abstract
+这是我的HTML笔记。
+为啥要学HTML?
+都学计算机了,总得学点前端吧。况且对于一个强迫症而言,我需要合适的工具来改进排版。
+Info
+HTML(HyperText Markup Language)是用来描述网页的一种标记语言。标记语言使用一套标记标签来描述内容。
+下面是一个完整的HTML页面:
+其中:
+- <!DOCTYPE HTML>
声明了文档类型,为HTML5文档。
+- <html>
元素是HTML页面的根元素,定义了整个HTML文档。
+- <head>
元素包含了文档的元(meta)数据,如标题、链接到外部样式表等。
+- <meta charset="utf-8">
这就是一个元数据,定义了网页的编码格式为UTF-8。
+- <title>
元素描述了文档的标题。
+- <body>
元素包含了可见的页面内容,定义了HTML文档的主体。
+- <h1>
元素定义了一个大标题,这是最高级的标题,<h6>
定义了最低级的标题。
+- <p>
元素定义了一个段落。
我们能看出来,HTML标签是由尖括号包围的关键词,比如<html>
,并且大多数标签都是成对出现的,比如<html>
和</html>
,其中前者是开始标签/开放标签,后者是结束标签/闭合标签。
HTML元素和HTML标签表示的是一样的意思,但是严格来说,HTML元素包括了开始标签和结束标签,元素的内容就是标签之间塞进去的部分。
+下面就是一个可视化的HTML页面结构:
+标题:标题是通过<h1>
到<h6>
标签来定义的,<h1>
是最大的标题,<h6>
是最小的标题。
段落:段落是通过<p>
标签来定义的。
链接:链接是通过<a>
标签来定义的,在href属性中指定连接的URL。
图像:图像是通过<img>
标签来定义的,通过src属性指定图像的URL。图像的名称和尺寸是通过属性的方式指定的。
HTML元素以开始标签开始,以结束标签终止,结束标签里边会塞一个斜杠/
,中间塞的就是元素内容。值得注意的是:
没有内容的HTML元素称为空元素,空元素在开始标签内关闭。比如定义换行的标签<br>
就是没有关闭标签的空元素。尽管空元素给我们一种错觉,似乎空元素就不用关闭了,但是在开始标签内添加一个斜杠才是空元素的正确关闭方式,比如<br />
,虽然现在的标准与当前的浏览器对于<br>
都是有效的,但是XHTML、XML和未来版本的HTML都要求所有元素必须关闭。
HTML标签对于大小写不敏感,<P>
和<p>
是一样的,甚至很多网站都使用大写的HYML标签。但是标准推荐小写,并且在XHTML和未来版本的HTML中,都强制使用小写。
对于HTML属性,有下面信息:
+name="value"
;我们拿链接元素来举例:
+属性值应该始终被包括在引号之内,双引号是最常用的,但是单引号也可以,如果属性值本身就有了双引号,那么必须在外边使用单引号。
+ + + + + + + + + + + + + +约 0 个字
+YAML是一种数据序列化语言,更多用于编写配置文件
+大小写敏感
+使用缩进表示层级关系
+缩进只能使用空格,不能使用tab
缩进的空格数目不重要,但是同一个层级上的元素左侧必须对齐
+#
表示注释,但是只支持单行注释
YAML支持下面几种数据类型:
+对象:是键值对的集合,也叫映射(Mapping)
+数组:一组按照次序排列的值,有成为序列(Sequence)或者列表(List)
+纯量:单个的、不可再分的值,也叫标量(Scalar)
+对象键值对使用冒号结构表示:key: value
,冒号的后面要加一个空格。对象支持多级嵌套,也支持流式风格的语法(亦即使用花括号包裹,拿逗号分割),比如:
当遇到复杂对象时,我们允许使用问号?
来声明,这样就可以使用多个词汇(其实是数组)来组成键,亦即对象的属性是一个数组,对应的值也是一个数组:
一组以区块格式(Block Format)(亦即短横线+空格)开头的数据构成一个数组:
+ +YAML也支持*内联格式(Inline Format)的数组(拿方括号包裹,逗号加空格分割):
+同样,嵌套格式的数组也是完全支持的,只需要使用缩进表示层级关系就好了。
+字符串一般不需要使用引号包裹,但是如果在字符串之中使用了反斜杠开头的转义字符就必须用引号包裹了:
+布尔值:True
、true
、TRUE
、Yes
、yes
、YES
都为真;False
、false
、FALSE
、No
、no
、NO
皆为假。
整数:除了可以正常写十进制,YAML还支持二进制(前缀0b
)和十六进制(前缀0x
)表示:
浮点数:字面量(3,14
)和科学计数法(6.8523015e+5
)都可以。
空:null
、Null
、~
都是空,不指定默认也是空。
时间戳:使用ISO 8601格式的时间数据(日期和时间之间使用T
链接,使用+
表示时区):
为了保持内容的简洁,避免过多的重复的定义,YAML使用&
表示建立锚点,*
表示引用锚点、<<
表示合并到当前数据:
上面的代码相当于:
+使用竖线|
来表示该语法,这时每行的缩进和尾行空白都会去掉,而额外的缩进则被保留:
+
使用右尖括号>
表示该语法,这时只有空白行才会被识别为换行,原来的换行符都会被转换为一个空格:
约 0 个字
+约 106 个字
+Abstract
+有关计算机的都在这了!
+这里是我在浙江大学图灵班期间的学习笔记。
+Warning
+这里有部分为了整齐而放弃了一部分考究的内容分类,请不要在意,谢谢!
+约 2 个字
+约 3014 个字 预计阅读时间 10 分钟
+A proposition is a declarative sentence that is either true or false, but not both. We use letters to denote propositional variables, or sentential variables, i.e. variables that represent propositions. The truth value of a proposition is true, denoted by T, if it is a true proposition, and similiarly, the truth value of a proposition is false, denoted by F, if it is a false proposition.Propositions that cannot be expressed in terms of simpler propositions are called atomic propositions.
+We can form new propostions from existing ones using logical connectives. Here are six useful logical connectives: Negation/NOT (\(\neg\)), Conjunction/AND (\(\land\)), Disjunction/OR (\(\lor\)), Exclusive Or/XOR (\(\oplus\)), Conditional/IF-THEN (\(\to\)), and Biconditional/IFF AND ONLY IF (\(\leftrightarrow\)).
+More on IMPLICATION:
+From \(p\to q\), we can form the converse \(q\to p\), the inverse \(\neg p\to \neg q\), and the contrapositive \(\neg q\to \neg p\). The converse and the inverse are not logically equivalent to the original conditional, but the contrapositive is.
+Construction of a truth table:
+The truth value of \(p\leftrightarrow q\) is the same as the truth value of \((p\to q)\land (q\to p)\), that is to say, \(p\leftrightarrow q\) is true if and only if \(p\) and \(q\) have the same truth value.
+Precedence of Logical Operators: From highest to lowest, the precedence of logical operators is \(\neg\), \(\land\), \(\lor\), \(\to\), and \(\leftrightarrow\).
+Basic terminology and its concepts:
+Two compound propositions \(p\) amd \(q\) are logically equivalent if \(q\leftrightarrow q\) is a tautology. We denote this by \(p\equiv q\) or \(p\Leftrightarrow q\).
+Compound propositions that have the same truth values for all possible cases, in other words, the columns in a truth table giving their truth values agree, are called equivalent.
+(Important) Conditional-disjunction equivalence states that for any propositions \(p\) and \(q\), we have
+Absorption laws states that for any propositions \(p\) and \(q\), we have
+De Morgan's Laws states that for any propositions \(p\) and \(q\), we have
+Distribution laws states that for any propositions \(p\), \(q\), and \(r\), we have
+Identity Laws states that for any propositions \(p\), we have
+Domination Laws states that for any propositions \(p\), we have
+Idempotent Laws states that for any propositions \(p\), we have
+Moreover, Commutative Laws and Associative Laws are also valid for logical connectives: for any propositions \(p\), \(q\), and \(r\), we have
+Involving Conditional and Biconditional statements, we have
+ + + +The Dual of compound proposition that contains only the logic operators \(\land\), \(\lor\), and \(\neg\) the proposition obtained by replacing each \(\land\) by \(\lor\), each \(\lor\) by \(\land\), each \(T\) by \(F\), and each \(F\) by \(T\). The dual of \(S\) is denoted by \(S^*\). For example, the dual of \(p\lor (q\land \neg r)\) is \(\neg p\land (q\lor \neg r)\).
+We already know that only two logical operators \(\{\neg,\land\}\) or \(\{\neg,\lor\}\) are enough to express all logical propositions. Thus, a collection of logical operators is called functionally complete if every possible logical proposition is logically equivalent to a compound proposition involving only these operators.
+The Sheffer Stroke/与非 is a functionally complete set of logical operators. It is denoted by \(p|q\), and \(p|q\) is false when both \(p\) and \(q\) are true, and true otherwise. The Peirce Arrow/或非 is also a functionally complete set of logical operators. It is denoted by \(p\downarrow q\), and \(p\downarrow q\) is true when both \(p\) and \(q\) are false, and false otherwise.
+A compound proposition is satisfiable if there is an assignment of truth values to its variables that makes it true. When no such assignments exits, the compound proposition is unsatisfiable.
+A compound proposition is unsatisfiable if and only if it is a contradiction or its negation is a tautology.
+A list of propositions is consistent if it is possible to assign truth values to the proposition variables so that each proposition is true.
+Propositional formula/命题公式 is a compound proposition that is built up from atomic propositions using logical connectives with the following criteria:
+Formulas can be transformed into standard forms so that they become more convenient for symbolic manipulations and make identification and comparison of two formulas easier. There are two types of normal forms in propositional calculus:
+the Disjunctive Normal Form/DNF/析取范式: A formula is said to be in DNF if it written as a disjuction, in which all terms are conjunctions of literals.
+ For example, \((p\land q)\lor (\neg p\land r)\), \(p\lor (q\land r)\), \(\neg p\lor T\) are in DNF, and the disjunction \(\neg(p\land q)\lor r\) is not.
the Conjunctive Normal Form/CNF/合取范式: A formula is said to be in CNF if it written as a conjunction, in which all terms are disjunctions of literals.
+We can introduce the concept of Clauses/子句 to simplify the concept of DNF and CNF. A disjunction/conjunction with literials as disjuncts/conjuncts are called a Disjunctive/Conjunctive clause/析取子句/合取子句. Disjunctive/conjunctive clauses are simply called clauses. Moreover, conjunctive clause is also called Basic product and disjunctive clause is also called Basic addition.
+Thus, a CNF is a conjunction of disjunctive clauses, and a DNF is a disjunction of conjunctive clauses.
+A Midterm/极小项 is a conjunction of literials in which each variable is represented exactly once.
+(IMPORTANT) There are \(2n\) different minterm for \(n\) propositional variables. For example there \(4\) different minterm for \(p\), \(q\), they are \(p\land q\), \(p\land\neg q\), \(\neg p \land q\), \(\neg p\land\neg q\). For the sake of simplification, we use \(m_j\) denote the minterms. Where \(j\) is a integer, its binary representation corresponds the evaluation of variables that make \(m_j\) be equal to T.
+If a proposition form is denoted by: \(f=m_j\lor m_k\lor\cdots\lor m_l\), then we simply denote
+Properties of minterms:
+If a function, as \(f\), is given by truth table, we know exactly for which assignments it is true. Consequently, we can select the minterms that make the function true and form the disjunction of these minterms.
+If a Boolean function is expressed as a disjunction of minterms, it is said to be in full disjunctive form.
+All above is the concept of DNF and the concept and use of minterms. Now we turn to CNF.
+A compound proposition is in CNF if it is a conjunction of disjunctive clauses. Every proposition can be put in an equivalent CNF. CNF is useful in the study of resolution theorem proving used in AI.
+A compound proposition can be put in conjunctive normal form through repeated application of the logical equivalences covered earlier.
+A Maxterm/极大项 is a disjunction of literials in which each variable is represented exactly once. If a Boolean function is expressed as a conjunction of maxterms, it is said to be in full conjunctive form.
+We can get the full conjunctive form of a Boolean function from its full disjunction form: Let \(f=\sum f(j,k,\cdots,l)\), \(g=\sum m(\{0,1,2,\cdots,2^{n-1}\}-\{j,k,\cdots,l\})\), then \(f\lor g = T\), \(f\land g = F\).
+The \(M_i\) is a maxterm defined by \(M_i=\neg m_i\).
+In this section, we will introduce Predicate logic/谓词逻辑. A predicate refers to a property that the subject of the statement can have. We can denote the statement "\(x\) is greater than \(3\)" by \(P(x)\), where \(P\) denotes the predicate "is greater than \(3\)" and \(x\) is the variable. The statement \(P(x)\) is also said to be the value of the propositional function \(P\) at \(x\).
+Propositional functions become propositions when their variables are each replaced by a value from the domain.
+We need quantifiers/量词 to express the meaning of English words including all and some. Two most important quantifiers are:
+Domain/domain or discourse/universe of discourse: the range of the possible values of the variable.
+Given the domain as \(\{x_1, x_2, \cdots, x_n\}\), the proposition \(\forall xP(x)\) is equivalent to \(P(x_1)\land P(x_2)\land\cdots\land P(x_n)\), and the proposition \(\exists xP(x)\) is equivalent to \(P(x_1)\lor P(x_2)\lor\cdots\lor P(x_n)\).
+Uniqueness Quantifier: \(\exists !\) means "There exists a unique", that is \(P(x)\) is true for one and only one \(x\) in the domain. The uniqueness quantifier can be expressed without the symbol \(!\) : \(\exists x(P(x)\land \forall y(P(x)\to y=x))\).
+Precedence of Quantifiers: The quantifiers \(\forall\) and \(\exists\) have higher precedence than the logical connectives \(\neg\), \(\land\), \(\lor\), \(\to\), and \(\leftrightarrow\). For example, \(\forall xP(x)\land Q(x)\) means \((\forall xP(x))\land Q(x)\).
+Bound Variable: A variable is bound if it is known or quantified. A variable is free if it is neither quantified or specified with a value.
+All the variables in a propositional function must be quantified or set equal to a particular value to turn it into a proposition.
+Scope of a quantifier: the part of a logical expression to which the quantifier is applied
+Logical Equivalence with Logical Quantifiers:
+ +De Morgan's Laws for Quantifiers:
+Nested Quantifiers: Two quantifiers are nested if one is within the scope of the other. For example, \(\forall x\exists yP(x,y)\) means "For every \(x\), there exists a \(y\) such that \(P(x,y)\) is true".
+Order of Quantifiers: Only both quantifiers are universal or both are existential, the order of quantifiers can be changed. +
+Distributions for Quantifiers over Logical Connectives: Here I list two examples: \(\forall x(P(x)\land Q(x))\equiv \forall xP(x)\land \forall xQ(x)\) is True, whereas \(\forall x(P(x)\to Q(x))\equiv \forall xP(x)\to \forall xQ(x)\) is False.
+Valid Argumemts:An argument in propositional logic is a sequence of propositions. All but the final proposition are called premises/前提. The last statement is the conclusion/结论. The argument is valid/有效 if the premises imply the conclusion. An argument form is an argument that is valid no matter what propositions are substituted into its propositional variables.
+If the premises are \(p_1, p_2,\dots, p_n\) and the conclusion is \(q\) then \((p_1\land p2 \land \cdots\land p_n )\to q\) is a tautology.
+Inference Rules are all argument simple argument forms that will be used to construct more complex argument forms.
+Modus Ponens/假言推理: If \(p\to q\) and \(p\) are true, then \(q\) is true. Corresponding Tautology: \((p\land (p\to q))\to q\).
+Modus Tollens/取拒式: If \(p\to q\) and \(\neg q\) are true, then \(\neg p\) is true. Corresponding Tautology: \(((p\to q)\land \neg q)\to \neg p\).
+Hypothetical Syllogism/假言三段论: If \(p\to q\) and \(q\to r\) are true, then \(p\to r\) is true. Corresponding Tautology: \(((p\to q)\land (q\to r))\to (p\to r)\).
+Disjunctive Syllogism/析取三段论: If \(p\lor q\) and \(\neg p\) are true, then \(q\) is true. Corresponding Tautology: \(((p\lor q)\land \neg p)\to q\).
+Addition/附加律: If \(p\) is true, then \(p\lor q\) is true. Corresponding Tautology: \(p\to (p\lor q)\).
+Simplification/简化律: If \(p\land q\) is true, then \(p\) is true. Corresponding Tautology: \((p\land q)\to p\).
+Conjunction/合取律: If \(p\) and \(q\) are true, then \(p\land q\) is true. Corresponding Tautology: \((p\land q)\to (p\land q)\).
+Resolution/消解律: If \(p\lor q\) and \(\neg p\lor r\) are true, then \(q\lor r\) is true. Corresponding Tautology: \(((p\lor q)\land (\neg p\lor r))\to (q\lor r)\).
+Universal Instantiation/全称实例: If \(\forall xP(x)\) is true, then \(P(c)\) is true for any \(c\) in the domain. Corresponding Tautology: \(\forall xP(x)\to P(c)\).
+Universial Generalization/全称引入: If \(P(c)\) is true for any \(c\) in the domain, then \(\forall xP(x)\) is true. Corresponding Tautology: \(P(c)\to \forall xP(x)\).
+Existential Instantiation/存在实例: If \(\exists xP(x)\) is true, then \(P(c)\) is true for some \(c\) in the domain. Corresponding Tautology: \(\exists xP(x)\to P(c)\).
+Existential Generalization/存在引入: If \(P(c)\) is true for some \(c\) in the domain, then \(\exists xP(x)\) is true. Corresponding Tautology: \(P(c)\to \exists xP(x)\).
+Universial Modus Ponens/全称假言推理: If \(\forall x(p(x)\to q(x))\) and \(p(a)\) are true, then \(q(a)\) is true. Corresponding Tautology: \((\forall x(p(x)\to q(x))\land p(a))\to q(a)\).
+A proof is a valid argument that establishes the truth of a statement. In math, CS, and other disciplines, informal proofs which are generally shorter, are generally used.
+A theorem/定理 is a statement that can be shown to be true using: definitions, other theorems, axioms (statements which are given as true), rules of inference.
+A lemma/引理 is a 'helping theorem' or a result which is needed to prove a theorem.
+A corollary/推论 is a result which follows directly from a theorem.
+Less important theorems are sometimes called propositions/命题.
+A conjecture/猜想 is a statement that is being proposed to be true. Once a proof of a conjecture is found, it becomes a theorem. It may turn out to be false.
+Direct Proof: Assume that \(p\) is true. Use rules of inference, axioms, and logical equivalences to show that \(q\) must also be true.
+Proof by Contraposition/反证法: Assume \(\neg q\) and show \(\neg p\) is true also. This is sometimes called an indirect proof method. If we give a direct proof of \(\neg q\to\neg p\) then we have a proof of \(p\to q\).
+Proof by Contradiction/归谬证明法/Reductio ad absurdum: To prove \(p\), assume \(\neg p\) and derive a contradiction such as \(r\land \neg r\). (an indirect form of proof). Since we have shown that \(\neg p\to F\) is true , it follows that the contrapositive \(T\to p\) also holds.
+Proof by cases: To prove \((p_1\lor p_2\lor \cdots\lor p_n)\to q\), using the tautology \((p_1\to q)\land (p_2\to q)\land\cdots\land (p_n\to q)\leftrightarrow (p_1\lor p_2\lor \cdots\lor p_n)\to q\), we need to prove \(p_1\to q\), \(p_2\to q\), \(\cdots\), and \(p_n\to q\).
+Existence Proofs/存在性证明,Without Loss of Generality/不失一般性,Nonconstructive Proofs/非构造性证明,Proof by Counterexample/反例证明,Uniqueness Proofs/唯一性证明,Backward Proof/逆向证明.
+ + + + + + + + + + + + + +约 1413 个字 预计阅读时间 5 分钟
+A set is an unordered collection of distinct objects, called elements or members of the set. A set is said to contain its elements. We write \(a\in A\) to denote that \(a\) is an element of the set \(A\). The notation \(a\notin A\) denotes that \(a\) is not an element of the set \(A\).
+Roster method: A set can be described by listing its elements between braces. For example, the set of vowels in the English alphabet can be written as \(V=\{a,e,i,o,u\}\). Listing an element more than once does not change the set. The set \(\{a,e,i,o,u\}\) is the same as the set \(\{a,e,i,o,u,u\}\).
+Set-builder notation: A set can be described by specifying a property that its members must satisfy, for example \(\{x:x\equiv 0\pmod 2\}\)
+Universal Set: The set \(U\) containing all the objects currently under consideration.
+Empty Set: The set containing no elements, denoted by \(\emptyset\) or \(\{\}\).
+Set Equality: Two sets are equal if and only if they have the same elements. i.e.
+Subset: A set \(A\) is a subset of a set \(B\) if every element of \(A\) is also an element of \(B\). We write \(A\subseteq B\).
+Proper Subset: If \(A\subseteq B\) and \(A\neq B\), then \(A\) is a proper subset of \(B\), denoted by \(A\subset B\).
+Set Cardinality: If there are exactly \(n\) distinct elements in a set \(A\), where \(n\) is a nonnegative integer, then \(A\) is a finite set otherwise it is an infinite set. The cardinality of a finite set \(A\), denoted by \(|A|\), is the number of elements in \(A\).
+Power Sets: The set of all subsets of \(A\), denoted by \(\mathcal{P}(A)\) is called the power set of \(A\). If \(|A|=n\), then \(|\mathcal{P}(A)|=2^n\).
+Tuples: The ordered \(n\)-tuple \((a_1,a_2,\cdots,a_n)\) is the ordered collection that has \(a_1\) as its first element, \(a_2\) as its second element, and so on. Two \(n\)-tuples are equal if and only if their corresponding elements are equal. \(2\)-tuple is called an ordered pair/序偶.
+Cartesian Product: The Cartesian product of sets \(A\) and \(B\), denoted by \(A\times B\), is the set of all ordered pairs \((a,b)\) where \(a\in A\) and \(b\in B\). Similarly, the Cartesian product of \(n\) sets \(A_1,A_2,\cdots,A_n\) is the set of all ordered \(n\)-tuples \((a_1,a_2,\cdots,a_n)\) where \(a_i\in A_i\) for \(i=1,2,\cdots,n\).
+Relation: A subset \(R\) of the Cartesian product \(A\times B\) is called a relation from \(A\) to \(B\).
+Truth Set: Given a predicate \(P\) and a domain \(D\), the truth set of \(P\) is the set of all elements in \(D\) for which \(P\) is true.
+Union: The union of sets \(A\) and \(B\), denoted by \(A\cup B\), is the set containing all elements that are in \(A\) or in \(B\) or in both.
+Intersection: The intersection of sets \(A\) and \(B\), denoted by \(A\cap B\), is the set containing all elements that are in both \(A\) and \(B\).
+Difference: The difference of sets \(A\) and \(B\), denoted by \(A-B\), is the set containing all elements that are in \(A\) but not in \(B\).
+Symmetric Difference: The symmetric difference of sets \(A\) and \(B\), denoted by \(A\oplus B\), is the set containing all elements that are in \(A\) or in \(B\) but not in both. Remember the XOR/\(\oplus\) operation.
+Complement: The complement of a set \(A\) with respect to the universal set \(U\), denoted by \(\overline{A}\) or \(A^c\), is the set \(U-A\).
+Includsion-Exclusion Principle: For anzy two sets \(A\) and \(B\),
+To prove set identies, the most effective way is to show that each side of the identity is a subset of the other side, builder notation and propositional logic are also used in our proof.
+Function: Let \(A\) and \(B\) be nonempty sets. A function \(f\) from \(A\) to \(B\) denoted by \(f:A\rightarrow B\) is an assignment of exactly one element of \(B\) to each element of \(A\). We write \(f(a)=b\) if \(b\) is the unique element of \(B\) assigned by \(f\) to the element \(a\) of \(A\). Functions are sometimes called mappings or transformations.
+A function \(f:A\rightarrow B\) can also be defined as a subset of the Cartesian product \(A\times B\), that is a relation. This subset is restricted be a relation where no two elements of the relation have the first element.
+Domain: The set \(A\) is called the domain of the function \(f:A\rightarrow B\).
+Codomain: The set \(B\) is called the codomain of the function \(f:A\rightarrow B\).
+Range: The set of all images of elements in the domain is called the range of the function.
+Two functions are equal if and only if they have the same domain, the same codomain, and assign the same value to each element in their domain.
+Definitions of Injection Surjection Bijection Inverse Function Composition and Graph of Function are omitted, because I assume you know them well.
+Cardinality: The cardinality of a set \(A\), denoted by \(|A|\), is the number of elements in \(A\). Two sets \(A\) and \(B\) have the same cardinality if and only if there is a bijection from \(A\) to \(B\).
+Countable Set: A set is said to be countable if it is either finite or its elements can be put into one-to-one correspondence with the set of positive integers \(\mathbb{Z}^+\). When an infinite set is countable/countably infinite, its cardinality is \(\aleph_0\).
+Cantor Diagonalization Method: The set of real numbers is uncountable.
+Proof: To show that the set of real numbers is uncountable, we suppose that the set of real numbers is countable and arrive at a contradiction. Then, the subset of all real numbers that fall between \(0\) and \(1\) would also be countable (because any subset of a countable set is also countable). Under this assumption, the real numbers between \(0\) and \(1\) can be listed in some order, say, \(r_1\), \(r_2\), \(r_3\), \(\dots\). Let the decimal representation of these real numbers be
+where \(d_{ij}\) is the \(j\)th digit in the decimal representation of \(r_i\). We construct a real number \(r\) between \(0\) and \(1\) as follows: We choose the first digit of \(r\) to be different from the first digit of \(r_1\), the second digit of \(r\) to be different from the second digit of \(r_2\), and so on. In general, we choose the \(i\)th digit of \(r\) to be different from the \(i\)th digit of \(r_i\). The real number \(r\) is different from every real number in the list, so the list does not contain all real numbers between \(0\) and \(1\). This contradiction shows that the set of real numbers between \(0\) and \(1\) is uncountable, which finishes the proof.
+Moreover, we can prove a stronger result: The set of all real numbers with decimal representations consisting only of \(0\) and \(1\) is uncountable. This comes from the observation that the Cantor diagonalization method only need two distinct digits to construct a real number that is different from every real number in the list.
+ + + + + + + + + + + + + +约 343 个字 预计阅读时间 1 分钟
+This part should be trivial and useless for ALL students learning computer science, and it is covered in the course Foundamental Data Structures.
+Properties of Algorithms: Input, Output, Definiteness (每一步都有明确定义), Finiteness (有限步出结果), Effectiveness (有限时间), Correctness, Generality.
+We will measure time complexity in terms of the number of operations an algorithm uses and we will use big-O and big-Theta notation to estimate the time complexity.
+If there exists a constant \(c > 0\) and a positive integer \(n_0\) such that \(\vert f(n) \vert \leqslant c \cdot \vert g(n) \vert\) for all \(n \geqslant n_0\), then \(f(n)\) is said to be \(O(g(n))\).
+If there exists a constant \(c > 0\) and a positive integer \(n_0\) such that \(\vert f(n) \vert \geqslant c \cdot \vert g(n) \vert\) for all \(n \geqslant n_0\), then \(f(n)\) is said to be \(\Omega(g(n))\).
+If there exists two constant \(c_1, c_2 > 0\) and a positive integer \(n_0\) such that \(c_1 \cdot \vert g(n) \vert \leqslant \vert f(n) \vert \leqslant c_2 \cdot \vert g(n) \vert\) for all \(n \geqslant n_0\), then \(f(n)\) is said to be \(\Theta(g(n))\).
+ +P Versus NP Problem: Go to ADS.
+ + + + + + + + + + + + + +约 428 个字 预计阅读时间 1 分钟
+Division: An integer \(a\) divides \(b\) if and only if there exists an integer \(c\) such that \(b = ac\). We write \(a|b\).
+Divisibility Properties:
+The Division Algorithm/带余除法: For any integers \(a\) and \(b\), with \(b > 0\), there exist unique integers \(q\) and \(r\) such that \(a = bq + r\) and \(0 \leq r < b\). Here \(a\) is called the dividend, \(b\) is called the divisor, \(q\) is the quotient and \(r\) is the remainder.
+Congruence: Let \(a\), \(b\) and \(n\) be integers with \(n > 0\). We say that \(a\) is congruent to \(b\) modulo \(n\) if \(n|(a - b)\). We write \(a \equiv b \pmod{n}\).
+The congruence relation has the following properties:
+The first three properties show that the congruence relation is an equivalence relation. The last property shows that the congruence relation is compatible with addition and multiplication.
+Moreover, \(a \equiv b \pmod{n}\) if and only if \(a\!\!\mod{n} = b\!\!\mod{n}\).
+Arithmetic Modulo \(m\): Define \(\mathbb{Z}_m = \{0, 1, 2, \cdots, m - 1\}\). The operationn \(+_m\) and \(\times_m\) are defined as \(a +_m b = (a + b)\!\!\mod{m}\) and \(a \times_m b = (a \times b)\!\!\mod{m}\). Thry satisfy the following properties:
+约 342 个字 预计阅读时间 1 分钟
+Strong Induction: To prove a statement \(P(n)\) for all \(n\in \mathbb{Z}\), we need to complete the following steps:
+Strong Induction is sometimes called the Second Principle of Mathematical Induction.
+Well-Ordering Property: Every nonempty set of nonnegative integers has a least element.
+Generalized Definition: A set is well-ordered if every subset has a least element.
+Recursive Definition: A recursive or inductive definition of a function consists of two steps.
+Sometimes the recursive definition has an exclusion rule, which specifies that the set contains nothing other than those elements specified in the basis step and generated by applications of the rules in the recursive step.
+Structural Induction: To prove a property of the elements of a recursively defined set, we use structural induction.
+Generalized induction is used to prove results about sets other than the integers that have the well-ordering property.
+Recursive Algorithms: An algorithm is called recursive if it solves a problem by reducing it to an instance of the same problem with smaller input.For the algorithm to terminate, the instance of the problem must eventually be reduced to some initial case for which the solution is known.
+ + + + + + + + + + + + + +约 888 个字 预计阅读时间 3 分钟
+The Product Rule: A procedure can be broken down into a sequence of two tasks. There are \(n_1\) ways to do the first task and \(n_2\) ways to do the second task. Then there are \(n_1*n_2\) ways to do the procedure.
+The Product Rule in Terms of Sets: If \(A_1\), \(A_2\), \(\cdots\), \(A_m\) are all finite sets, then the number of elements in the Cartesian product \(A_1 \times A_2 \times \cdots \times A_m\) is \(|A_1| * |A_2| * \cdots * |A_m|\).
+The Sum Rule: If a task can be done either in one of \(n_1\) ways or in one of \(n_2\) ways to do the second task, where none of the set of \(n_1\) ways is the same as any of the set of \(n_2\) ways, then there are \(n_1 + n_2\) ways to do the task.
+The Sum Rule in Terms of Sets: If \(A\) and \(B\) are disjoint sets, then \(|A \cup B| = |A| + |B|\).
+The Pigeonhole Principle: If \(k\) is a positive integer and \(k+1\) or more objects are placed into \(k\) boxes, then there is at least one box containing two or more of the objects.
+Corolary: A function \(f\) from a set with \(k+1\) elements to a set with \(k\) elements is not one-to-one.
+Generalized Pigeonhole Principle: If \(N\) objects are placed into \(k\) boxes, then there is at least one box containing at least \(\lceil N/k \rceil\) objects.
+I assume everyone has learned about permutations and combinations during high school period, so I omitted most of this part.
+Generalized Combinational Numbers: If \(n\geq 0\) and \(n\geq m\), then the Generalized Combinational Number \(\binom{m}{n} = C^n_m\) is defined as
+Combinatorial Proofs: A combinatorial proof of an identity is a proof thar uses one of the following methods:
+Binomial Expression: A binomial expression is the sum of two terms, such as \(x + y\).
+Binomial Theorem: Let \(x\) and \(y\) be variables and \(n\) be a nonnegative integer. Then
+Corollary 1: For all nonnegative integers \(n\),
+Corollary 2: For all positive integers \(n\),
+Pascal's Identity: If \(n\) and \(k\) are integers with \(0 \leq k \leq n\), then
+Vandermonde's Identity: If \(m\), \(n\), and \(r\) are nonnegative integers with \(r \leq m\) and \(r \leq n\), then
+We only need to consider choosing \(r\) elements from two sets \(A\) and \(B\) with \(m\) and \(n\) elements respectively.
+Corollary 4: If \(n\) is a nonnegative integer, then
+Identity: Let \(n\) and \(r\) be nonnegative integers with \(r \leq n\). Then
+Consider choosing \(r+1\) elements from a set with \(n+1\) elements, and the RHS sets the last element be the \(j\)th element, from the \(r+1\)th to the \(n+1\)th.
+Permutations with Repetition: The number of r-permutations of a set with n elements, where repetition is allowed, is \(n^r\).
+Combinations with Repetition: The number of r-combinations of a set with n elements, where repetition is allowed, is \(\binom{n+r-1}{r}\).
+Permutations with Indistinguishable Objects: The number of different permutations of n objects, where \(n_1\) are of one type, \(n_2\) are of a second type, \(\cdots\), and \(n_k\) are of a \(k\)th type, is \(\frac{n!}{n_1!n_2!\cdots n_k!}.\)
+Lexicographic Order: The permutation \(a_1a_2\cdots a_n\) precedes the permutation \(b_1b_2\cdots b_n\) in lexicographic order if there is an integer \(j\) with \(1 \leq j \leq n\) such that \(a_i = b_i\) for \(i = 1, 2, \cdots, j-1\) and \(a_j < b_j\).
+Find the Next Permutation: If \(a_1a_2\cdots a_n\) is a permutation of \(1, 2, 3, \cdots, n\), then the next permutation in lexicographic order is obtained by:
+Find the Next Combination: If \(S_j\in S = \{1, 2, 3, \cdots, n\}\) is the \(j\)th element in a combination of \(r\) elements from \(S\), then the next combination in lexicographic order is obtained by:
+约 782 个字 预计阅读时间 3 分钟
+A solution of a Recurrence Relation is a sequence that satisfies the recurrence relation.
+Normally, there are infinitely many sequences which satisfy a recurrence relation. We distinguish them by the initial conditions, the values of \(a_1\), \(a_2\), \(\cdots\), \(a_k\) to uniquely identify a sequence.
+The Degree of a Recurrence Relation: \(a_n = a_{n-1} + a_{n-5}\) is a recurrence relation with degree 5.
+Linear Homogeneous Recurrence Relations: A Linear Homogeneous Recurrence Relation of degree \(k\) with constant coefficients is a recurrence relation of the form \(a_n = c_1a_{n-1} + c_2a_{n-2} + \cdots + c_ka_{n-k}\), where \(c_1, c_2, \cdots, c_k\) are constants and \(c_k \neq 0\).
+Solving Linear Homogeneous Recurrence Relations: The general solution of a linear homogeneous recurrence relation of degree \(k\) with constant coefficients \(a_n = c_1a_{n-1} + c_2a_{n-2} + \cdots + c_ka_{n-k}\) can be obtained by:
+Linear Nonhomogeneous Recurrence Relations with Constant Coefficients: A Linear Nonhomogeneous Recurrence Relation with Constant Coefficients is a recurrence relation of the form \(a_n = c_1a_{n-1} + c_2a_{n-2} + \cdots + c_ka_{n-k} + f(n)\), where \(f(n)\) is a function of \(n\). The general solution of a linear nonhomogeneous recurrence relation with constant coefficients is in the form of
+where \(g(n)\) is a particular solution of the nonhomogeneous recurrence relation for \(f(n)\).
+Theorem: For \(a_n = c_1a_{n-1} + c_2a_{n-2} + \cdots + c_ka_{n-k} + F(n)\) and \(F(n) = (b_tn^t + b_{t-1}n^{t-1} + \cdots + b_1n + b_0)s^n\), then:
+Generating Functions: The Generating Function of a sequence \(a_0, a_1, a_2, \cdots\) is the formal power series
+Generating functions can be used to solve a wide variety of counting problems, such as
+Principle of Inclusion-Exclusion: For any two sets \(A\) and \(B\),
+Generalized Principle of Inclusion-Exclusion: For any \(n\) sets \(A_1, A_2, \cdots, A_n\),
+Derangements: A derangement is a permutation of objects that leaves no object in the original position.
+Theorem: The number of derangements of a set with n elements is
+The Hatcheck Problem: A new employee checks the hats of n people at restaurant, forgetting to put claim check numbers on the hats. When customers return for their hats, the checker gives them back hats chosen at random from the remaining hats. What is the probability that no one receives the correct hat.
+ + + + + + + + + + + + + +约 2043 个字 4 行代码 预计阅读时间 7 分钟
+Binary Relations: A binary relation \(R\) from a set \(A\) to a set \(B\) is a subset \(R\subset A\times B\). We can represent
+Example: Let \(A = \{0, 1, 2\}\) and \(B = \{a, b\}\), then \(\{(0, a), (0, b), (1, a), (2, b)\}\) is a relation from \(A\) to \(B\).
+Reflexive Relations/自反关系: A relation \(R\) on a set \(A\) is reflexive if and only if for all \(a\in A\), \((a, a)\in R\). Written symbolically, \(R\) is reflexive if and only if
+Symmetric Relations/对称关系: A relation \(R\) on a set \(A\) is symmetric if and only if for all \(a, b\in A\), if \((a, b)\in R\), then \((b, a)\in R\). Written symbolically, \(R\) is symmetric if and only if
+Antisymmetric Relations/反对称关系: A relation \(R\) on a set \(A\) is antisymmetric if and only if for all \(a, b\in A\), if \((a, b)\in R\) and \((b, a)\in R\), then \(a = b\). Written symbolically, \(R\) is antisymmetric if and only if
+Transitive Relations/传递关系: A relation \(R\) on a set \(A\) is transitive if and only if for all \(a, b, c\in A\), if \((a, b)\in R\) and \((b, c)\in R\), then \((a, c)\in R\). Written symbolically, \(R\) is transitive if and only if
+Composition: The composition of relations \(R\) and \(S\) is the relation \(T\) such that \((a, c)\in T\) if and only if there exists an element \(b\) such that \((a, b)\in R\) and \((b, c)\in S\).
+Powers of a Relation: Let \(R\) be a binary relation on a set \(A\). The \(n\)th power of \(R\) is the relation \(R^n\) defined recursively as follows:
+Theorem 1: The relation \(R\) is transitive if and only if \(R^n\subseteq R\).
+Inverse Relation: The inverse of a relation \(R\) is the relation \(R^{-1}\) such that \((b, a)\in R^{-1}\) if and only if \((a, b)\in R\).
+For a set with \(n\) elements, there are \(2^{n^2}\) possible relations, \(2^{n(n+1)/2}\) possible symmetric relations, \(2^{n}3^{n(n-1)/2}\) possible antisymmetric relations, \(3^{n(n-1)/2}\) asymmetric relations, \(2^{n(n-1)}\) irreflexive relations, \(2^{n(n-1)/2}\) reflexive and symmetric relations, \(2^{n^2}-2^{n(n-1)+1}\) neither reflexive nor irreflexive relations.
+Combining Relations: Given two relations \(R_1\) and \(R_2\), we can combine them using basic set operations to form new realtions such as \(R_1\cup R_2\), \(R_1\cap R_2\), \(R_1 - R_2\), \(R_1\oplus R_2\).
+Matrix Representation: A relation \(R\) between finite sets can be represented using a zero-one matrix. The matrix \(M\) representing the relation \(R\) is defined as follows: Suppose \(R\) is a relation from \(A=\{a_1, a_2, \ldots, a_m\}\) to \(B=\{b_1, b_2, \ldots, b_n\}\), then the matrix \(M\) representing \(R\) is an \(m\times n\) matrix such that \(M[i, j] = 1\) if \((a_i, b_j)\in R\) and \(M[i, j] = 0\) otherwise.
+If \(R\) is reflexive, then all the elements on the diagonal of \(M_R\) are \(1\). If \(R\) is symmetric, then \(M_R\) is symmetric, i.e. \(m_{ij} = 1\) if and only if \(m_{ji} = 1\). \(R\) is antisymmetric if and only if \(m_{ij} = 0\) or \(m_{ji} = 0\) for all \(i\neq j\).
+Graph Representation: A directed graph, or digraph, consists of a set \(V\) of vertices/nodes together with a set \(E\) of edges/arcs, where each edge is an ordered pair of vertices. The vertex \(a\) is called the initial vertex of the edge \((a, b)\), and the vertex \(b\) is called the terminal vertex of the edge \((a, b)\). An edge of the form \((a, a)\) is called a loop. Then we can draw a graph to represent a relation.
+Reflexivity: A relation \(R\) on a set \(A\) is reflexive if and only if there is a loop at each vertex in the graph representing \(R\).
+Symmetry: A relation \(R\) on a set \(A\) is symmetric if and only if \((a, b)\) is in the graph representing \(R\) whenever \((b, a)\) is in the graph.
+Antisymmetry: A relation \(R\) on a set \(A\) is antisymmetric if and only if \((y, x)\) is not an edge when \((x, y)\) with \(x\neq y\) is an edge. In other words, whenever there is an edge from one vertex to another, there is no edge comming back.
+Transitivity: A relation \(R\) on a set \(A\) is transitive if and only if whenever \((a, b)\) and \((b, c)\) are edges in the graph representing \(R\), then \((a, c)\) is also an edge.
+Reverse in the Version of Relation Representation: For matrix representation, the inverse relation is the transpose of the matrix. For graph representation, the inverse relation is the graph with all the edges reversed.
+Properties of Relation Operations: Suppose \(R\) and \(S\) are the relations from \(A\) to \(B\), \(T\) is the relation from \(B\) to \(C\), \(P\) is the relation from \(C\) to \(D\), then
+Closure: The Closure of a relation \(R\) with respect to the property \(P\) is the relation obtained by add the minimum numnber of ordered pairs to \(R\) to satisfy property \(P\).
+Reflexive Closure: \(r(R) = R\cup \Delta\) where \(\Delta = \{(a, a)\vert a\in A\}\).
+Symmetric Closure: \(S(R) = R\cup R^{-1}\).
+Transitive Closure: \(T(R) = R\cup R^2\cup R^3\cup \cdots = \cup_{n=1}^{\infty}R^n\). To get this, we need some lemmas:
+Lemma 3: A is a set consisting of \(n\) elements, \(R\) is a relation on \(A\). If there is a path from \(a\) to \(b\) in \(R\), then there is a path of length not exceeding \(n\). If \(a\neq b\), then such path has length not exceeding \(n-1\).
+From Lemma 3, we can see \(t(R) = \cup_{i=1}^{n}R^i\) or \(t(R) = \cup_{i=1}^{n-1}R^i\cup \Delta\).
+Moreover, \(M_{R^*} = M_R\lor M_{R^2}\lor M_{R^3}\lor \cdots\lor M_{R^n}\).
+Warshall's Algorithm: The transitive closure \(T\) of a relation \(R\) on a set \(A\) can be computed using Warshall's algorithm. The algorithm is as follows:
+Equivalence Relation: A relation \(R\) on a set \(A\) is an equivalence relation if and only if \(R\) is reflexive, symmetric, and transitive. And we denote \(a\sim b\) for \((a, b)\in R\) or \(aRb\).
+Equivalence Class: The equivalence class of an element \(a\in A\) with respect to an equivalence relation \(R\) is the set of all elements in \(A\) that are related to \(a\), and \(a\) is called the representative of the equivalence cass \([a]_R\).
+Theorem 1: Let \(R\) be an equivalence relation on a set \(A\). Then the statements for elements \(a\) and \(b\) of \(A\) are equivalent:
+Partition: A partition \(\mathit{pr}(A) = \{A_i\vert i\in I\}\) of a set \(A\) is a collection of disjoint nonempty subsets of \(A\) whose union is \(A\). In other words, the collection of subsets \(A_i\) where \(i\in I\) (\(I\) is an index set), forms a partition of \(A\) if and only if
+Theorem 2: Let \(R\) be an equivalence relation on a set \(A\). Then the equivalence classes of \(R\) form a partition of \(A\). Conversely, given a partition \(\mathit{pr}(A) = \{A_i\vert i\in I\}\) of the set \(A\), there is an equivalence relation \(R\) on \(A\) such that the equivalence classes of \(R\) are the sets in the partition.
+Let \(R\) and \(S\) be equivalence relations on the set \(A\), then \(R\cap S\) is also an equivalence relation on \(A\).
+However, \(R\cup S\) is not necessarily an equivalence relation. It is indeed reflexive and symmetric. And \((R\cup S)^*\) is an equivalence relation.
+Partial Ordering: A relation \(R\) on a set \(A\) is called a partial ordering or partial order if and only if \(R\) is reflexive, antisymmetric and transitve. A set with a partial ordering is called a partially ordered set or poset and denoted by \((A, R)\).
+Comparability: The elements \(a\) and \(b\) of a poset \((S, \leq)\) are comparable if and only if either \(a\leq b\) or \(b\leq a\). Otherwise, \(a\) and \(b\) are incomparable.
+Total Ordering: A partial ordering \(R\) on a set \(A\) is called a total ordering or total order if and only if for all \(a, b\in A\), either \(aRb\) or \(bRa\). A totally ordered set is called a chain.
+Well-ordered: A set \(A\) with a partial ordering \(R\) is called well-ordered if and only if \(R\) is totally ordered and every nonempty subset of \(A\) has a least element.
+Lexicographic Order
+Hasse Diagram: A Hasse diagram is a visual representation of a partial ordering that leaves out edges that must be present because of the reflexive and transitive properties.
+Hasse Diagram Terminology: \(a\) is a maximal in \((A, \leq)\) if there is no \(b\in A\) such that \(a\leq b\).
+ + + + + + + + + + + + + +约 3221 个字 12 行代码 预计阅读时间 11 分钟
+Graph: A graph \(G = (V, E)\) consists of a nonempty set of Vertices/Nodes \(V\) and a set of Edges \(E\). Each edge has either one or two vertices associated with it, called its Endpoints. An edge is said to connect its endpoints. If the edges connect only one vertex, it is called a Loop.
+Simple Gpaph: A graph in which each edge connects two different vertices and no two edges connect the same pair of vertices.
+Multigraph: A graph that may have multiple edges connecting the same vertices.
+Pseudograph: A graph that may have loops, and possibly multiple edges connecting the same pair of vertices.
+Directed Graph/Digraph: A graph \((V,E)\) consists of a nonempty set of vertices \(V\) and a set of Directed Edges/Arcs \(E\). Each directed edge is associated with ann ordered pair of vertices. The directed edge associated with the ordered pair \((u,v)\) is said to start at \(u\) and end at \(v\).
+For an undirected graph \(G = (V, E)\):
+The Handshaking Theorem: Let \(G = (V, E)\) be an undirected graph with \(e\) edges. Then
+Theorem 2: An undirected graph has an even number of vertices of odd degree.
+For a directed graph \(G = (V, E)\):
+Theorem 3: Let \(G = (V, E)\) be a directed graph. Then
+Some special simple graphs:
+Theorem 4: A simple graph is bipartite if and only if it is possible to assign one of two different colors to each vertex of the graph so that no two adjacent vertices have the same color.
+Matching: A matching \(M\) in a simple graph \(G = (V, E)\) is a subset of \(E\) such that no two edges are incident with the same vertex.
+A Maximum matching is a matching with the largest number of edges.
+We say that a matching \(M\) in a bipartite graph \(G = (V, E)\) with bipartition \((V_1, V_2)\) is a complete matching from \(V_1\) to \(V_2\) if every vertex in \(V_1\) is an endpoint of an edge of the matching.
+Hall's Marriage Theorem: The bipartite graph \(G = (V, E)\) with bipartition \((V_1, V_2)\) has a complete matching from \(V_1\) to \(V_2\) if and only if \(\vert N(A)\vert \geqslant \vert A\vert\) for all subsets \(A\) of \(V_1\). A vertex that is the endpoint of an edge of a matching \(M\) is said to be matched by \(M\).
+A good but long proof.
+Proof:
+For \(G = (V, E)\) and \(H = (W, F)\).
+Adjacency Matrix: A simple graph \(G = (V, E)\) with \(n\) vertices \((v_1,v_2,\cdots, v_n)\) can be represented by its adjacency matrix, A, with respect to this listing of the vertices, where
+For multigraphs and pseudographs, the adjacency matrix is defined similarly, but the entries can't be just \(0\) or \(1\) anymore.
+Incidence Martix: Let \(G = (V, E)\) be an undirected graph. Suppose that \(v_1, v_2, \cdots, v_n\) are the vertices and \(e_1, e_2, \cdots, e_m\) are the edges of \(G\). Then the incidence matrix with respect to this ordering of \(V\) and \(E\) is \(n\times m\) matrix \(M = [m_{ij}]_{n\times m}\), where
+Isomorphism of Graphs: Graphs with the same structure are said to be isomorphic. Formally, two simple graphs \(G_1= (V_1, E_1)\) and \(G_2= (V_2, E_2)\) are isomorphic if there is a \(1-1\) and onto bijection \(f\) from \(V_1\) to \(V_2\) such that for all \(a\) and \(b\) in \(V_1\), \(a\) and \(b\) are adjacent in \(G_1\) iff \(f(a)\) and \(f(b)\) are adjacent in \(G_2\). Such a function \(f\) is called an isomorphism.
+In other words, when two simple graphs are isomorphic, there is a one-to-one correspondence between vertices of the two graphs that preserves the adjacency relationship.
+Important Invariants:
+A path of length \(n\) in a simple path is a sequence of vertices \(v_0, v_1, \cdots, v_n\) such that \(\{v_0, v_1\}\), \(\{v_1, v_2\}\), \(\cdots\), \(\{v_{n-1}, v_n\}\) are \(n\) edges of the graph.
+The path is a circuit if the beginning and ending vertices are the same and the length of the path is greater than \(0\).
+A path is simple if it doesn't contain the same edge more than once.
+A path of length \(n\) in a directed graph is a sequence of vertices \(v_0, v_1, \cdots, v_n\) such that \((v_0, v_1)\), \((v_1, v_2)\), \(\cdots\), \((v_{n-1}, v_n)\) are \(n\) edges of the graph. Circuits, cycles and simple paths are defined as before.
+Theorem 5: The number of different paths of length \(r\) from \(v_i\) to \(v_j\) is equal to the \((i, j)\) entry in the matrix \(A^r\), where \(A\) is the adjacency matrix of the graph. Easy to prove.
+Connected Graph: An undirected graph is connected if there is a path between every pair of distinct vertices of the graph.
+Theorem 6: There is a simple path between every pair of distinct vertices in a connected undirected graph. Just the definition.
+Components: The maximally connected subgraphs of \(G\) are called the connected components or just the components.
+Cut Vertex/Articulation Point: A vertex is a cut vertex or articulation point if removing it and all edges incident with is results in more connected compotents than the original graph.
+Similarly, if removal of an edge results in more connected components, then the edge is a cut edge or a bridge.
+Strongly Connected: A directed graph is strongly connected if there is a directed path from \(u\) to \(v\) and a directed path from \(v\) to \(u\) for every pair of vertices \(u\) and \(v\) in the graph.
+Weakly Connected: A directed graph is weakly connected if the underlying undirected graph is connected. Every strongly connected graph is weakly connected.
+Strongly Connected Components: For a directed graph, the maximal strongly connected subgraphs are called the strongly connected components.
+Left issues: Kosaraju's algorithm and Tarjan's algorithm.
+Some Other Invariants:
+Euler Path: An Euler Path is a simple path containing every edge in \(G\).
+Euler Circuit: An Euler Circuit is not only an Euler path but also a circuit.
+Euler Graph: A graph that contains an Euler circuit is called an Euler graph.
+Theorem 7: A connected multigraph has an Euler circuit if and only if each of its vertices has even degree.
+Proof:
+Build and Find a Euler Circuit and Path:
+This will produce an Euler circuit, since every path is included and no edge is included more than once.
+Theorem 8: A connected multigraph has an Euler path but not an Euler circuit if and only if it has exactly two vertices of odd degree.
+Theorem 9: A directed multigraph having no isolated vertices has an Euler circuit if and only if the graph is weakly connected and the in-degree and out-degree of each vertex are equal.
+++所有顶点的出度入度相等的弱联通有向多图有欧拉回路。
+
Theorem 10: A directed multigraph having no isolated vertices has an Euler path but not an Euler circuit if and only if:
+++没法翻译了,太抽象了。
+
Hamilton Path: A Hamilton path in a graph \(G\) is a path which visits every vertex exactly once.
+Hamilton Circuit: A Hamilton circuit is a circuit that visits every vertex exactly once except for the first vertex.
+Hamilton Graph: A graph that contains a Hamilton circuit is called a Hamilton graph.
+Theorem 11 (DIRAC): If \(G\) is a simple graph with \(n\) vertices \((n \geqslant 3)\) and if the degree of each vertex is at least \(n/2\), then \(G\) has a Hamilton circuit.
+Theorem 12 (ORE): If \(G\) is a simple graph with \(n\) vertices \((n \geqslant 3)\) and if for every pair of nonadjacent vertices \(u\) and \(v\) of \(G\), the sum of the degrees of \(u\) and \(v\) is at least \(n\), i.e. \(\deg(u) + \deg(v) \geqslant n\), then \(G\) has a Hamilton circuit.
+Necessary Condition for Hamilton Path and Halmiton Circuit: For undirected graph: The necessary condition for the existence of Hamilton path:
+The necessary condition for the existence of Hamilton circuit:
+The degree of each vertex is larger than \(1\).
+Some properties:
+\(G\) is a Hamilton graph, for any nonempty subset \(S\) of set \(V\), the number of connected components in \(G-S\) does not exceed \(\vert S\vert\).
+Weighted Graph: \(G = (V, E, W)\), where \(W\) is a function that assigns a real number \(W(e)\) to each edge \(e\) in \(E\). The number \(W(e)\) is called the weight of edge \(e\).
+Length of a Path: The length of a path in a weighted graph is the sum of the weights of the edges in the path.
+Shortest Path: The shortest path between two vertices \(u\) and \(v\) in a weighted graph is a path of minimum length between \(u\) and \(v\).
+For undirected graphs with positive graphs: Dijkstra's Algorithm.
+Theorem 13: Dijkstra’s algorithm finds the length of a shortest path between two vertices in a connected simple undirected weighted graph.
+Theorem 14: Dijkstra’s algorithm uses \(O(n^2)\) operations (additions and comparisons) to find the length of the shortest path between two vertices in a connected simple undirected weighted graph.
+Floyd's Algorithm: Allow negative weights but no negative cycles.
+Planar Graphs: A graph is called planar if it can be drawn in the plane without any edges crossing. Such a drawing is called a planar representation of the graph.
+Region: A region is a part of the plane completely disconnected off from other parts of the plane by the edges of the graph. We have Bounded Region and Unbounded Region.
+There is one unbounded region in a planar graph.
+Theorem 15 (Euler's Formula): Let \(G\) be a connected planar simple graph with \(e\) edges and \(v\) vertices. Let \(r\) be the number of regions in a planar representation of \(G\). Then \(r=e-v+2\).
+Proof: OMITTED.
+Degree of a Region: Suppose \(R\) is a region of a connected planar simple graph, the number of the edges on the boundary of \(R\) is called the Degree of \(R\), denoted by \(\mathrm{Deg}(R)\).
+Corollary: If \(G\) is a connected planar simple graph with \(e\) edges and \(v\) vertices where \(v\geqslant 3\), then \(e\leqslant 3v-6\). The equality holds if and only if every region has exactly three edges.
+Proof: From \(2e = \sum \mathrm{Deg}(R) > 3r\), we can derive \(r \leqslant \dfrac{2e}{3}\). Under the Euler's formula, we have \(r = e - v + 2\), so \(e - v + 2 \leqslant \dfrac{2e}{3}\), which means \(e \leqslant 3v - 6\).
+Corollary: If \(G\) is a connected planar simple graph, then \(G\) has a vertex of degree not exceeding five.
+Corollary: If a connected planar simple graph has \(e\) edges and \(v\) vertices with \(v\geqslant 3\) and no circuits of length \(3\), then \(e \leqslant 2v-4\). Generally, if every region of a connected planar simple graph has at least \(k\) edges, then
+Elementary Subdivision: If a graph is planar, so will be any graph obtained by removing an edge \(\{u, v\}\) and adding a new vertex \(w\) together with edges \(\{u, w\}\) and \(\{w, v\}\). Such an operation is called an elementary subdivision.
+++移除度为 2 的顶点或者在边上加一个顶点。
+
Homeomorphic: The graph \(G_1=(V_1,E_1)\) and \(G_2=(V_2,E_2)\) are called homeomorphic if they can be obtained from the same graph by a sequence of elementary subdivision.
+Theorem 16 (KURATOWSKI): A graph is nonplanar if and only if it contains a subgraph homeomorphic to \(K_{3,3}\) or \(K_5\).
+Each map in the plane can be represented by a graph, namely the dual graph of the map.
+Coloring: A coloring of a simple graph is the assignment of a color to each vertex of the graph so that no two adjacent vertices are assigned the same color.
+Chromatic number/色数: The Chromatic number of a graph is the least number of colors needed for a coloring of this graph, denoted by \(x(G)\).
+The Four Color Theorem: Every planar graph is \(4\)-colorable.
+Flowgraph: Directed graph with distinguished vertices s/source and t/sink.
+Capacities on the edges: \(:c(e) \geqslant 0\).
+Target: Maximize the flow from \(s\) to \(t\) with the constraint that the flow on each edge does not exceed its capacity.
+Cut: Partition of \(V\) into disjoint sets \(S\), \(T\) with \(s\) in \(S\) and \(t\) in \(T\).
+\(Cap(S, T)\): Sum of the capacities of edges from \(S\) to \(T\).
+\(Flow(S, T)\): Net flow out of \(S\), i.e. the sum of flows out of \(S\) minus sum of flows into \(S\).
+Residual Graph: For flow graph \(G\), the residual graph \(G_f\) is defined as follows:
+Argumenting Path: A path from \(s\) to \(t\) whose flow can be increased. Iff for all edges \(f(u, v) < c(u, v)\) and \(f(v, u) > 0\).
+Ford-Fulkerson Algorithm: Build argument path until there is no forward path from source to sink.
+Augmenting path theorem: Flow \(f\) is a max flow iff there are no augmenting paths.
+Max-flow Min-cut Theorem: The value of the max flow equals the capacity of the min cut.
+ + + + + + + + + + + + + +约 510 个字 14 行代码 预计阅读时间 2 分钟
+I assume you have learned FDS.
+Prefix Codes: To ensure that no bit string corresponds to more than one sequence of letters, the bit string for a letter must never occur as the first part of the bit string for another letter. Codes with this property are called prefix codes.
+We can use a binary tree to construct prefix codes: The left edge and the right edge at each internal vertex are labeled by 0 and 1 respectiely.
+Huffman Coding: Minimize the average number of bits per letter: \(\mathrm{min}(f_1l_1 + f_2l_2 + \cdots + f_nl_n)\), where \(f_i\) is the frequency of the \(i\)th letter and \(l_i\) is the length of the code for the \(i\)th letter.
+++出现概率最小的两棵树最小在右,第二小在左。
+
I assume you have learned FDS.
+Spanning Tree: Let \(G\) be a simple graph. A spanning tree of \(G\) is a subgraph of \(G\) that is a tree containing every vertex of \(G\).
+Find a Spanning Tree: By removing edges from simple circuits in \(G\).
+Theorem: A simple graph is connected if and only if it has a spanning tree.
+Proof:
+Depth-First Search (DFS): A procedure that forms a rooted tree, and the underlying graph is a spanning tree.
+Breadth-First Search (BFS):
+Minimum Spanning Tree (MST): A Minimum Spanning Tree in a connected weighted graph is a spanning tree that has the smallest possible sum of weights of its edges.
+Prim's Algorithm:
+Kruskal's Algorithm:
+约 143 个字
+Abstract
+这是我在2023-2024学年春夏学期修读《离散数学理论基础》的课程笔记。
+离散数学的内容繁杂,包含逻辑、集合论、图论等内容。对于计算机专业的学生来说,这部分包含的内容更加宽泛,可以说是“在数理基础课上讲不到的都在这了”。
+参考书籍:
+约 39 个字
+The Brundtland Commission laid out the most famous definition of sustainable development as development that "meets the needs of the present without compromising the ability of future generations to meet their needs."
+ + + + + + + + + + + + + +约 11 个字
+Info
+Taking Sides,亦即《立场》丛书
+Info
+未完工!我会加紧进度的!
+Warning
+本文仅作为本人对《The Western Heritage》一书的笔记,除记录知识以外再无其它意义
+约 2051 个字 预计阅读时间 7 分钟
+The western heritage emerges from an evolved and evolving story of human actions and interactions, peaceful and violent, that arose in the eastern Mediterranean, then spread across the western Mediterranean into northern Europe, and eventually to the American continents, and in their broadest impact, to the peoples of Africa and Asia as well.
+The Western Heritage as a distinct portion of world history descends from the ancient Greeks, who saw their own political life based on open discussion of law and policy. The Greeks invented the concept of citizenship, defining it as engagement in some form of self-government. The Greeks also established their conviction that reason can shape and analyze physical nature, politics, and morality.
+Rome spread its authority through military conquest across the Mediterranean world, embracing Greek literature and philosophy. Romans' conquest and imposition of law created the Western world as a vast empire stretching from Egypt and Syria in the east to Britain in the West. Although the Roman Republic, governed by Senate and popular political institutions (元老院与公众政治机构), gave way to the autocratic rule of Roman Empire, the idea of a free republic law and constitutional (宪法的) arrangements limiting political authority survived centuries of arbitrary (武断专制的、随心所欲的) rule by emperors.
+Emperor Constantine reorganized the Roman Empire in two fundamental ways: First, he moved the capital from Rome to Constantinople. Thereafter (其后) large portions of the Western empire became subject to the rulers of Germanic tribes. In the confusion of these times, most of the texts embodying ancient philosophy, literature, and history became lost in the West, and for centuries Western Europeans became intellectually severed from that ancient heritage. Second, Constantine's recognition of Christianity as the official religion of the empire.
+康斯坦丁将基督教作为帝国的官方宗教,由于基督教是单神论宗教,康斯坦丁对基督教的接纳导致了异端多神论宗教的消亡。此后,西方世界或多或少的都有与基督教相连,或者与承认罗马主教为首的基督教会相连。
+随着皇权逐渐崩溃,主教变成了西欧许多区域的事实上的统治者,但是基督教会从未在未与世俗的统治者协商或者冲突的情况下加以统治,并且宗教法也没有取代世俗法,况且世俗的统治者也无法在忽略教会的影响下加以统治。Hence, from the fourth century C.E. to the present day, rival claims to political and moral authority between ecclesiastical and political officials have characterized the west.
+In the seventh century, the rise of Islam, a new monotheistic religion, which spread rapidly through conquests across North Africa and eventually into Spain, confronted a new challenge to the Western World. Christians attempted to reclaim the Holy Land (圣地,亦即巴勒斯坦) from Muslim control in church-inspired military crusades (十字军东征) that still resonate negatively in the Islamic world.
+However, while intellectual life languished in the West, most of the texts of ancient Greek and Latin learning survived and were studied in the Muslim world. By the fourteenth century, European thinkers redefined themselves and their intellectual ambitions by recovering the literature and science from the ancient world, reuniting Europe with its Graeco-Roman past.
+From the twelfth through the eighteenth centuries, a new European political system arose based on centralized monarchies (中央集权的君主制) characterized by large armies, navies and bureaucracies loyal to the monarch (忠诚于皇帝的官僚体制), and by the capacity to raise revenues (提升税收). Most of these monarchies recognized both the political role of local or national assemblies drawn from the propertied elites (有产阶级精英) and the binding power of constitutional law on themselves. ** (宪法对于他们自己的约束力) The monarchies, their military, and their expanding commercial economies became the basis for the extension of European and Western influence around the globe.**
+In the late fifteenth and early sixteenth centuries, two transforming events occurred. The first was the European discovery and the conquest of American continents, thus opening the Americas to Western institutions, religion, and economic exploitation. The labor shortage of Americas led to the forced migration of millions of Africans as slaves to the America. By the mid-seventeenth century the West consequently embraced the entire transatlantic world and its multiracial societies.
+Second, shortly after the American encounter, a religious schism erupted within Latin Christianity. (基督教分裂) Reformers rejecting both many medieval Christian doctrines as unbiblical and the primacy of the Pope in Rome established Protestant churches across much of northern Europe. (宗教改革者不仅反对许多中世纪的基督教义,认为它们是不符合圣经的,还反对罗马天主教教皇的至高无上的地位,并且在北欧的大部分土地上建立了新教教堂) As a consequence, for almost two centuries religious warfare between Protestants and Roman Catholics overwhelmed the continent as monarchies chose to defend one side or the other. The religious turmoil meant that Europeans who conquered and settled the Americans carried with them particularly energized religious convictions, with **Roman Catholics dominating Latin America and English Protestants most of North America. **
+By the late eighteenth century, the idea if the West denoted a culture increasingly dominated by two new forces. First, science arising from a new understanding of nature achieved during the sixteenth and seventeenth centuries persuaded growing numbers of the educated elite that human beings can rationally master nature for ever-expanding productive purposes improving the health and well-being of humankind. From this era to the present, the West has been associated with advances in technology, medicine, and scientific research. Second, during the eighteenth century, a drive for economy improvement that vastly increased agricultural production and then industrial manufacturing transformed economic life, especially in Western Europe and later the United States. Both of these economic development went hand in hand with urbanization and the movement of industrial economy into cities where the new urban populations experienced major urban dislocation (社会失序).
+During the last quarter of eighteenth century, political revolution erupted across the transatlantic world. The British colonies of North America revolted, and then revolution occurred in France and spread across much of Europe. The Wars of Independence liberated Latin America from its European conquerors. Those revolutions created bold new modes of political life, rooting the legitimacy of the state in some form of popular government and generally written constitutions. Thereafter, despite the presence of authoritarian governments on the European continent, the idea of West, now including the new republics of the United States and Latin America, became associated with liberal democratic governments. (这些革命创造了新的政治生活模式,将国家的合法性和某种形式的人民政府和成文宪法联系到了一起。自此之后,除了欧洲大陆某些独裁政府,西方的概念,变得与自由民主的政府联系到了一起。)
+During the nineteenth century, most major European states came to identify themselves in terms of nationality - language, history, and ethnicity - rather than loyalty to a monarch. Nationalism eventually inflamed popular opinion and unloosed unprecedented political ambition by European governments.
+These ambitions led to imperialism and the creation of new overseas European empires in the late nineteenth century. For people living in European-administered Asian and African colonies, the idea and reality of the West embodied foreign domination and often disadvantageous involvement in a world economy. Even after colonial peoples around the globe challenged European imperial authority and gained independence, these former colonial peoples often suspected the West of seeking to control them. Hence, anticolonialism like colonialism before it redefined definitions of the West far from its borders.
+Late nineteenth-century nationalism and imperialism also unleashed with World War I in 1914 unprecedented military hostilities among European nations that spread around the globe, followed a quarter century later by an even greater world war. As one result of World War, revolution occurred in Russia with the establishment of the communist Soviet Union. During the interwar years, a Fascist Party seized power in Italy and a Nazi Party took control of Germany. In response to these new authoritarian regimes, West European powers and the United States identified themselves with liberal democratic constitutionalism, individual freedom, commercial capitalism, science and learning freely pursued, and religious liberty, all of which they defined as the Western Heritage. (将他们自己认为是自由民主立宪政府、拥有个人自由、商业资本主义、拥有追求知识和科学的自由、宗教信仰自由,他们将这些定义为 Western Heritage。)
+During the Cold War , conceived of as an East-West, democratic versus communist struggle that concluded with the collapse of the Soviet Union in 1991, the Western Powers led by the United States continued to embrace those values in conscious opposition to the Soviet government, which since 1945 had also dominated much of Eastern Europe.
+Since 1991 the West has again become redefined in the minds of many people as a world political and economic order dominated by the United States. Europe clearly remains the West, but political leadership has moved to Northern America. That American domination and recent American foreign policy have led throughout the West and elsewhere to much criticism of United States.
+Such self-criticism itself embodies one of the most important and persistent parts of the Western Heritage. From Hebrew prophets and Socrates to the critics of European imperialism, American foreign policy, social inequality, and environmental devastation, voices in the West have again and again been raised to criticize often in the most strident manner the policies of Western governments and the thought, values, social conditions, and the inequalities of Western societies.
+Consequently, we study the Western Heritage not because the subject always or even primarily presents an admirable picture, but because the study of the Western Heritage like the study of all history calls us to an integrity of research, observation, and analysis that clarifies our minds and challenges our moral sensibilities. The challenge of history is the challenge of thinking, and it is to that challenge that this book invites its readers.
+Culture may be defined as the ways of living built up by a group and passed on from a generation to another. It includes behavior such as courtship and child-rearing practices; material things such as tools, clothing, and shelter; and ideas, institutions, and beliefs. Language, apparently a uniquely human trait, lies behind our ability to create ideas and institutions and to transmit culture from one generation to another. Our flexible and dexterous hands enable us to hold and make tools and so to create the material artifacts of culture. Because culture is learned and not inherited, it permits rapid adaption to changing conditions, making possible the spread of humanity to almost all the lands of the globe.
+During the Paleolithic,
+ + + + + + + + + + + + + +约 47 个字
+Abstract
+在这里的都是我的读书笔记!包括但不限于哲学、历史学、心理学与社会学。
+当然挖的坑越多就越难填(x
+加油哦!
+约 0 个字
+约 4 个字
+约 271 个字 143 行代码 预计阅读时间 3 分钟
+在 RV32I 之中只有寄存器 x5
x6
x7
是临时寄存器,被命名为 t0
t1
t2
,这些寄存器可以用于存储临时变量或者任意修改,同时可以将临时变量存储在栈之中,这就需要调整栈指针 x2
也就是 sp
的值。下面是几个例子:
if-then
statements¶if-then in C | |
---|---|
while
loop¶do-while
loop¶for
loop¶使用移位指令与 lw
实现 lbu
指令。
使用移位指令与 sw
实现 sb
指令。
Six steps of calling a function:
+Arguments registers:
+a0
to a7
for arguments.a0
and a1
for return values.sp
for the stack pointer, holding the current memory address of the bottom of the stack.Example:
+Basic structure of a function:
+Factorial in C | |
---|---|
Factorial in RISC-V | |
---|---|
Fibonacci in C | |
---|---|
Stack Pointer sp
holds the address of the bottom of the stack.
sw
to write to a variable.约 3 个字
+约 2 个字
+约 6 个字
+约 99 个字
+ADDI R1, R2, #100
reads the value of R2, adds 100 to it, and stores the result in R1.LDR R1, 100
reads the value at memory address 100 and stores it in R1.LDR R1, (100)
the data at the memory location 100
is an address, and we uses the address to read the value.约 102 个字
+Abstract
+这是我对《深入理解计算机系统》一书的读书笔记。
+参考:
+约 613 个字 预计阅读时间 2 分钟
+计算机系统的硬件组成包括:总线/Bus,I/O设备/IO devices,主存/Main memory,处理器/Processor。
+总线是贯穿整个系统的一组电子管道,携带信息字节并负责在各个设备之间传输。总线被设计成传送定长的字节块,被称为字/Word,现代的计算机的字长大多为4个字节/32位或者8个字节/64位。
+I/O设备
+主存是一个临时存储设备,在处理器执行程序的时候,用来存放程序和程序处理的数据。主存是由一组动态随机存取存储器/Dynamic random access memory/DRAM组成的,在逻辑上看是一个线性的字节数组,每个字节都有唯一的地址,且地址从零开始。
+中央处理器单元/Central processing unit简称为处理器,处理器负责解释(或者执行)主存中的指令。处理器的核心是大小为一个字的寄存器/Register,称为程序计数器/Program counter/PC,
+当我们对系统的某个部分加速的时候,其对整个系统性能的影响取决于该部分的重要性和加速程度。特别地,假设系统执行某个应用程序需要的时间为\(T_{old}\),某部分所需执行时间与该时间所用的比例为\(\alpha\),而这部分性能提升的比例为\(k\),改进之后,总的执行时间为
+那么,我们可以的到加速比\(S\)为
+我们同时也考虑极限情况,也就是 \(k\to\infty\) 的情况,此时加速比\(S\)为
+这就是加速比的上界了。
+总之,Amdahl's Law告诉我们,为了显著提升系统性能,必须提升全系统中相当大的部分的性能
+并发(concurrency)和并行(parallelism)是两个不同的概念。并发是一个通用的概念,指一个同时具有多个活动的系统。并行是指使用并发来使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用,我们按照系统层次结构中从高到底的顺序重点强调三个层次:
+约 4 个字
+约 4 个字
+约 4 个字
+约 1359 个字 预计阅读时间 5 分钟
+Why Bits?
+Because of electronic implementation: Easy to store bistable elements, and reliable transmitted on noisy and inaccurate wires.
+十九世纪中期,布尔通过将逻辑值TRUE
和FALSE
编码为二进制值1
与0
,能够设计为一种代数,以研究逻辑推理的基本原则,这种代数叫做布尔代数/Boolean algebra。
布尔代数有四种基本运算~
、&
、|
、^
,分别对应于逻辑运算NOT、AND、OR与EXCLUSIVE-OR,我们可以列出简单的真值表如下:
接下来,我们将上述四个布尔运算推广到位向量/Bit vectors的运算,所谓位向量就是固定长度\(w\),由0
与1
组成的串。所谓的推广也非常简单,就是将上述四个布尔运算应用到位向量的每一位上,得到的结果也是一个位向量。换句话说,就是我们在C语言中学的按位运算。
C语言提供了一组移位运算,右移有两种,算数右移与逻辑右移,算数右移在左侧补上最高位有效值/符号位,逻辑右移在左侧补上0。左移就简单的多,就是在右侧补上0。
+为什么会有两种右移呢?因为两种右移对应着两种不同的数据类型的计算:算数右移对应的是有符号数的计算,逻辑右移对应的是无符号数的计算,这在下面会讲到。
+无符号数的编码就是经典的二进制编码,假设一个无符号整数数据有\(w\)位,我们可以将位向量写作\(\vec{x}\),也就是\([x_{w-1},x_{w-2},\cdots,x_0]\)来表示向量的每一位。我们用一个函数\(B2U_w\)(是Binary to Unsigned的缩写)来表示二进制向无符号整数的转换:
+我们很容易可以得知:
+最常见的有符号整数编码是补码/Two's-complement编码。在补码编码中,一个\(w\)位的有符号整数\(\vec{x}\)的值可以表示为:
+最高有效位也称为符号位,其权重为\(-2^{w-1}\),其余位的权重和无符号整数编码一样。同样,我们可以得知:
+类似的,我们可以定义四进制与十六进制的编码。
+补码编码有十分有趣的特性:
+在C库中的limits.h
中定义了一些常用的整数的最大值与最小值,用来限制编译器运行的不同整型数据的取值范围,例如INT_MAX
、INT_MIN
、UINT_MAX
等。
在C库中的stdint.h
中定义了一些固定大小的整数类型,例如int8_t
、uint8_t
、int16_t
、uint16_t
等,这些类型很好地提升了程序的可移植性。
有符号数还有下面两种其他的表示方法:
+这两种编码方式都有统一的缺点:对于数字0
,有两种完全不同的表示方法,并且这两种编码不能很好地支持算数运算,因而,我们现在开始使用更加方便的补码编码。
对于32位的整型,下面的几个数还是很重要的:U{Max}=4294967295
,T{Max}=2147483647
,T{Min}=-2147483648
。
另外,T{Min}
被定义为-T{Max}-1
而不是-2147483648
,这是因为-2147483648
在C语言中是对2147483648
取负号,而2147483648
超出了32位正整型的范围,所以只好用-T{Max}-1
来表示。
Case 1:规格化值
+这种情况下阶码的位表示不全为 0
或 1
,
Case 2:非规格化值
+阶码的位表示全为 0
,尾数的位表示不全为 0
,这时指数 \(E = 1 - \mathrm{bias} = -2^{k-1} + 2\),尾数 \(1\),这时数值表示为 \(1\)。
Case 3:特殊值
+1
,尾数全为 0
,表示无穷大;1
,尾数非全为 0
,表示 NaN
;0
,尾数全为 0
,表示 0
。约 3091 个字 33 行代码 预计阅读时间 11 分钟
+CMU 15-213
+这里使用的 x86-64 汇编的形式是 AT&T 语法,而不是 Intel 语法。AT&T 语法与 Intel 语法的区别在于操作数的顺序,AT&T 语法是 source, destination
,而 Intel 语法是 destination, source
。这种描述的语法会用在 Linux 系统之中,但是当我们阅读 Intel 和 Microsoft 的代码与文档的时候,就要换换脑筋了。
一个 x86-64 的 CPU 包含一组 16 个存储 64 位值的通用目的寄存器/General-Purpose Registers,这些寄存器可以用来存储整数数据和指针。这些寄存器用来存储整数数据和指针。由于指令集与处理器架构的历史演化,它们的长度从 16 位逐渐变成 64 位,名字也并不规则,但是寄存器的分布保证了令人震惊的向下兼容性。虽然他们的名字是通用目的的,但是根据 x86-64 规定的使用惯例,每个寄存器都有一定的特殊用途,这样就保证了后调用的进程可以接受传递过来的参数,同时运行的过程之中不会覆盖调用进程的寄存器的值。
+ +除了整数寄存器,CPU 还维护着一组单个位的条件码/Conditional Code 寄存器,他们描述了最近一次算术或者逻辑操作的结果。这些条件码寄存器包括:
+以算数操作 t = a + b
举例,设置条件码的操作与逻辑类似于下面的 C 代码:
(unsigned) t < (unsigned) a
t == 0
t < 0
((a < 0 ) == (b < 0)) && ((a < 0) != (t < 0))
除了 leaq
指令类以外——因为 leaq
是用来完成地址计算的——所有操作都会隐式的设置操作码。对于逻辑操作而言,进位标志与溢出标志都会被设置成 0
。对于移位操作,进位标志被设置成为最后一个被移出的位,溢出标志设置成 0
. inc
和 dec
指令会会设置溢出标志和零标志,但是不会改变进位标志。
cmp
和test
指令类会设置条件码,但是不会改变目的寄存器的值。cmp
指令会计算第二个操作数减去第一个操作数的结果,但是不会存储结果。test
指令会计算两个操作数的按位与,但是不会存储结果。
条件码一般不会直接读取,常用的使用方法有三种:
+set
指令。jmp
指令会导致程序执行切换到程序中的一个全新的位置,这些跳转的目的地通常用一个标号来标识。jmp
指令可以是直接跳转,亦即跳转目标是作为指令的一部分编码的,直接在跳转指令后边跟上一个标号作为跳转目标;也可以是间接跳转,亦即跳转目标是从寄存器或者内存之中读出来的。我们还允许有条件的跳转,这就可以对程序进行控制。
实现条件操作的传统方法就是通过使用控制的条件转移,当条件满足的时候,就沿着一条路径进行运行,不满足就使用另一种路径。这种策略简单而通用,但是在现代计算机上可能很低效,原因就是现代计算机使用流水线/Pipelining来获得高性能,流水线中,每条指令的处理都被分为了多个小阶段,每个阶段执行操作的一小个部分,通过重叠连续指令的方法来获得高性能,比如在取一条指令的同时,执行前面一条指令的算数运算。要做到这一点,就需要实现确定要执行的指令序列,这样才能保持整个流水线充满了指令,进而保持最优的性能。
+一旦遇到条件跳转了(也就是分支),流水线处理器就会采用分支预测,猜测每条跳转指令是否会执行,猜对了还好,猜错了就会遇到招致很严重的惩罚,会浪费大约 15~30 个时钟周期,程序性能会大幅下降。为了减少这种性能损失,除了不断提升分支预测的正确率之外,现代处理器会使用条件传送指令,这种指令会根据条件码的值来决定是否传送数据,这样就可以避免分支预测错误的惩罚,并且极大避免随机情况下分支预测的错误。
+但是条件传送也不总是好的,下面三种情况会让条件传送变得难办:
+val = Test(x) ? Hard1(x) : Hard2(x);
,这样我们就要算两个很难算的东西,反而更加浪费时间;val = p ? *p : 0;
,这样就有可能会出现段错误;val = x > 0 ? x*=7 : x+=13;
,这样算出来的结果就不对了。所以我们的编译器(gcc)当分支的计算比较简单(至少多算一个花费的时间要比预测错误惩罚低)、比较安全并且没有副作用的时候,才会使用条件传送进行优化。
+switch
语句可以根据一个整数索引值来进行多重分支,我们使用跳转表/Jump Table 这种数据结构来让实现更加高效。跳转表是一个数组,数组的每一个元素都是一个代码段的地址,在代码段内实现当开关的索引值等于 i
的时候程序应该采取的动作。当开关情况值的数量比较大而且跨度比较小的情况下,跳转表是一个很好的选择。
下面是描述 switch
的实现的一个例子,右侧依赖了 gcc 对跳转表语法的支持
Original Code | |
---|---|
在 x86-64 中,绝大多数的过程之间的数据传送是通过寄存器实现的,我们可以将被调用过程需要用到的参数按照约定的顺序塞到某些特定的寄存器之中,这样被调用的过程就可以使用这些参数了。类似地,当被调用过程返回的时候,要先将返回值放到特定的寄存器 %rax
之中,这样就可以让调用者使用这个返回值了。
在 x86-64 之中,我们可以使用寄存器最多传送 6 个整型(包括整数和指针)参数,寄存器的使用是有特殊顺序的,使用的名字取决于要传递的数据类型的大小,比如 64 位的指针类型就可能用到寄存器 %rdi
,并且这个还是作为第一个参数传递,而 32 位的整数就可能用到寄存器 %esi
,并且这个还是作为第二个参数传递。下边这个表还是记一下,不然查表很痛苦 (谁愿意看代码的时候被打断呢),不过看多了汇编就会形成肌肉记忆了。
操作数大小(位) | +参数数量 | +|||||
1 | +2 | +3 | +4 | +5 | +6 | +|
64 | +%rdi |
+%rsi |
+%rdx |
+%rcx |
+%r8 |
+%r9 |
+
32 | +%edi |
+%esi |
+%edx |
+%ecx |
+%r8d |
+%r9d |
+
16 | +%di |
+%si |
+%dx |
+%cx |
+%r8w |
+%r9w |
+
8 | +%dil |
+%sil |
+%dl |
+%cl |
+%r8b |
+%r9b |
+
如果有超过 6 个参数,我们就需要使用栈来传递参数了,前 6 个参数照常被复制到寄存器传递,后边的参数依次被放入栈中,参数 7 置于栈顶,所有的数据大小都向着 8 的倍数对齐,所以一般我们不使用 pushq
指令,而是使用 subq
指令来调整栈的大小,然后使用 movq
指令来将参数放入栈中。
有些时候,局部数据必须存储在内存之中,常见的情况包括:
+&
,而寄存器是没有地址而言的;可以通过减小栈指针 %rsp
的方法来在栈上分配空间,一般分配的空间都是 8 字节的倍数,分配的结果作为栈帧的一部分,标号为局部变量。局部变量的存储不需要考虑向 8 的倍数对齐,能存下就行。在程序结束之前,需要将栈指针恢复,释放掉局部变量的空间。
没啥好说的,多维数组其实就是数组的数组,数组的分配和引用的一般原则也成立,并且会在内存里面分配连续的空间。比如对于声明成 ElementType D[R][C]
的数组,它的数组元素 D[i][j]
的地址就是 &D[i][j] = D + L(C * i + j)
,其中 L=sizeof(ElementType)
。
结构的所有组成部分都存放在内存的一段连续的区域内,指向结构的指针就是结构第一个字节的地址。
+也没啥好说的,只需要知道编译器维护每个字段的字节偏移量进而产生内存引用指令的位移,从而产生对结构元素的引用,进而操纵字段。值得注意的是,结构之中的数组等元素是嵌入在结构之中的,而不是存储指向数组的指针,所以产生结构体 struct {int* i; long array[2];}
需要分配 24 字节的空间,而不是 16 字节。
最需要注意的是对齐的问题,下边马上就讲了。
+联合可以有效规避 C 的类型系统。一个联合的总的大小等于它的最大字段的大小,而不是所有字段的大小之和。联合的字段是共享存储空间的,所以修改一个字段会影响到其他字段。联合的字段的地址都是相同的,所以可以通过一个字段来访问其他字段。联合还可以用来实现类型转换,换句话说就是访问不同数据类型的位模式,比如在联合可以包含一个长整形 long
和一个整数数组 int [2]
,这样就可以令两个 int
的位组合来表示一个 long
。
数据对齐要求对基本数据类型的合法地址进行了一些限制,要求地址必须是某个值的倍数,这个值一般是 2、4 或 8。这是为了提升内存访问的效率,并且还可以简化处理器和内存之间的接口的硬件设计。基本原则是任何 K 字节的基本对象的地址必须是 K 的倍数。比如 4 字节的整数 int
的地址必须是 4 的倍数,8 字节的双精度浮点数 double
的地址必须是 8 的倍数。编译器会在汇编代码中放入命令,指明全局数据要求的对齐,比如在跳转表前放入伪指令 .align 8
,这样就可以保证后边紧接着的的数据的起始地址是 8 的倍数,因为每个表项都长为 8 个字节,所以后边的元素就都遵守 8 字节对齐的限制了,自然而然就对齐了。
对于含有结构体的数据,编译器会在字段的分配之中加入间隙,保证每个字段元素都满足对齐要求,并且结构对本身的地址也有一些要求,对齐要求是结构体中最大的字段的对齐要求,比如一个结构体 struct {char c; int i;}
,c
的对齐要求是 1,i
的对齐要求是 4,所以整个结构体的对齐要求是 4,结构体的地址就要是 4 的倍数。
如果数据没有对齐,某些行好的处理器对于某些实现多媒体操作的 SSE 指令就无法正确执行,这些指令对 16 字节数据块进行操作,在 SSE 单元和内存之间传送数据的指令要求内存地址必须是 16 的倍数,任何以不满足对齐要求的资质访问内存就会导致异常。所以任何针对 x86-64 处理器的编译器和运行时系统都必须保证分配用来保存可能会被 SSE 寄存器读或写的数据结构的内存,都必须满足 16 字节对齐,所以就有下面两个后果:
+malloc
分配的内存块的起止地址都要是 16 的倍数;这其实也解释了有一些“莫名其妙”在栈上多分配 8 字节内存的现象,这是为了保证栈帧的对齐要求。
+指针类型只不过是 C 提供的一种类型抽象,并不是机器代码的一部分。这里只是介绍一些基本原则:
+void *
;NULL
;&
操作符来获得,反过来,通过对指针使用 *
操作符来间接引用指针;int func(int x, int* y)
,可以令一个函数指针 int (*fp)(int, int*) = func
,这样就可以通过 fp(1, &x)
来调用函数 func
。Important Tool
+在 Linux 的存储结构之中,栈位于最顶部,尽管 64 位允许访问的空间非常大,由于硬件与 ISA 设计因素,当时(CMU 15-213 fa15 上课时)只能使用 47 位的地址空间,栈底的地址因此是 0x7fffffffffff
,并且栈的空间是有限的,一般只有 8MB,我们使用 limit
命令可以查看栈的大小。
约 4 个字
+约 5 个字
+约 3 个字
+约 5 个字
+约 4 个字
+约 1312 个字 预计阅读时间 4 分钟
+集成电路/Integrated Circuit是一个硅半导体晶体,包含用来构建逻辑门和存储单元的电子元件,将二极管、晶体管以及其他原件都制作在同一块芯片上,通过陶瓷或者塑料来封装,通过焊接芯片和外界引脚来形成集成电路。
+CMOS/互补金属氧化物半导体/Complementary Metal-Oxide Semiconductor工艺具有高密度、高性能与低功耗的优势,是现代数字集成电路的主流工艺。具体观察 CMOS 晶体管的结构,在衬底之上有栅极/Gate,栅极之下有绝缘层/Insulator,绝缘层之下有源极/Source和漏极/Drain,源极与漏极之间有一个间隙,一般称之为沟道。CMOS 电路中的晶体管有两种类型:n 沟道晶体管/nMOS和p 沟道晶体管/pMOS。
+传播延迟/Propagation delay是信号的变化从输入传播到输出所需要的时间。电路运行速度与电路中经过门的最长传播延迟成反比关系。
+我们有三个传播延迟参数:高到低的传播时间/High-to-low propagation time\(t_{PHL}\),低到高的传播时间/Low-to-high propagation time\(t_{PLH}\),传播延迟/Propagation delay\(t_{pd}\)。
+在模拟过程对门建模的时候,往往使用传输延迟与惯性延迟。传输延迟/Transport delay是指输出响应输入的变化,在指定的传播延迟之后发生改变。惯性延迟/Inertial delay类似于传输延迟,但是如果输入变化使输出在一个小于拒绝时间/Rejection time的时间内改变,那么两次变化中的第一次将不会发生。拒绝时间是一个确定的值,不大于传播延迟,一般等于传播延迟。
+所谓时序逻辑电路,其结果输出不仅仅取决于当前的外界输入,而且取决于系统所处的内部状态,而系统当前的内部状态都是由系统的初态经过前序若干输入信息的处理加工之后到达的状态,所以我们可以认为时序逻辑电路输出的结果不仅仅取决于当前的外部输入,还取决于所有前序输入。但是对于一个简单的电路系统而言,记录所有前序输入是不现实的。通常,我们记录前序输入可能导致的影响结果输出的不同状态,根据这些状态和输入产生的结果输出。
+处理时序逻辑的重要理论工具是有限状态机/Finite State Machine,有限状态机是一个刻画状态与状态转换的理论工具。在没有任何输入的情况下,系统处于初始状态,随着外部信息的到来,系统状态根据应用逻辑的实际需要,进入相应的下一个状态,系统还可能向外界发出信号,作为对当前输入的响应。
+为了解决状态记忆问题,我们设计出了双稳态器件记忆状态信息,这就是下面的锁存器与触发器。
+时序电路的逻辑图由触发器与组合逻辑门组成,在组合逻辑电路中,为触发器产生输入信号的组合电路部分可以通过一个布尔函数集合来描述,这个布尔函数集合称为触发器输入方程/Flip-flop Input Equation。组合电路的输出端与触发器的输入端项链,输入方程之中使用带有触发器输出符号的下标的触发器输入符号表示依赖变量,比如\(D_{A}=AX+BX\)就表示本触发器输出为\(A\),输入为\(AX+BX\)。
+时序电路的输入、输出和触发器状态之间的功能关系可以用一个状态表/State Table列出。状态表由四栏组成,分别是当前状态/Present State、输入/Inputs、输出/Outputs和下一状态/Next State。当前状态表示触发器在任意给定时刻 t 的状态,输入栏表示在当前状态下的输入,下一状态栏指的是触发器在一个时钟周期之后的状态,可以哦由逻辑图或者触发器的输入方程得到,输出栏表示在当前状态与输入的组合下输出的值。
+我们不得不提到时序逻辑电路的分类:根据输出逻辑对其输入信号依赖情况的不同,时序逻辑电路可以分为Mealy 型电路/Mealy Model Circuit和Moore 型电路/Moore Model Circuit。Mealy 型电路的输出取决于当前状态和输入,而 Moore 型电路的输出仅仅取决于当前状态。
+约 5 个字
+约 0 个字
+约 1 个字
+约 3032 个字 201 行代码 预计阅读时间 13 分钟
+FPGA/Field Programmable Gate Array/现场可编程门阵列:FPGA器属于专用集成电路/ASIC的一种半定制电路,是可以编程的逻辑列阵,可以按照设计人员的需求配置指定的电路结构,让客户不必依赖于芯片制造商设计和制造的专用集成电路就可以实现所需要的功能,同时实现非常高效的逻辑运算。其基本结构包括可编程输入输出单元,可配置逻辑块,数字时钟管理模块,嵌入式块RAM,布线资源,内嵌专用硬核,底层内嵌功能单元。
+Verilog HDL是一种硬件描述语言,用于从算法级、门级到开关级的多种抽象设计层次的数字系统建模。Verilog HDL提供了编程语言接口,通过这个接口可以在模拟、验证期间从设计外部访问设计,包括模拟的具体控制和运行。
+根据逻辑电路的不同特点,数字电路可以分为组合逻辑和时序逻辑。其中:
+Verilog 这种硬件描述语言都基于基本的硬件逻辑之上,因此 Verilog 具有一套独特的基于电平逻辑的数值系统,使用下面四种基本数值表示电平逻辑:
+我们还经常用到整数,可以简单使用十进制表示,也可以使用立即数表示,基于如下的基数规则表示:<bits>'<radix><value>
,其中 <bits>
表示二进制位宽,空缺不填就会根据后边的数值自动分配;<radix>
表示进制, <radix>
可以是 b/o/d/h
,分别是二进制,八进制,十进制以及十六进制;<value>
表示数值,插入下划线 _
可以有效提升可读性。
wire
用于声明线网型数据。wire
本质上对应着一根没有任何其他逻辑的导线,仅仅将输入自身的信号原封不动地传递到输出端。该类型数据用来表示以 assign
语句内赋值的组合逻辑信号,其默认初始值是 Z(高阻态)。
wire
是 Verilog 的默认数据类型。也就是说,对于没有显式声明类型的信号,Verilog 一律将其默认为 wire
类型。
wire
的电器特性:
assign
输入;assign
输出。reg
用于声明在 always
语句内部进行赋值操作的信号。一般而言,reg
型变量对应着一种存储单元,可以在赋值之间存储数据,其默认初始值是 X(未知状态)。为了避免可能的错误,凡是在 always
语句内部被赋值的信号,都应该被定义成 reg
类型。
如果 always
描述的是组合逻辑,那么 reg
就会综合成一根线,如果 always
描述的是时序逻辑,那么 reg
才会综合成一个寄存器/触发器。
按位运算符:
+&
:按位与;|
:按位或;^
:按位异或;~
:按位取反;~^
或者 ^~
:按位同或;算数运算符:
+localparam
与 parameter
¶localparam
类似于 C 中的 const
变量,看似是定义了一个变量,其实在生成的时候,只会生成一个立即数代替 localparam
变量。localparam
只能被赋值一次,赋值表达式可以是任意的 localparam
、parameter
与立即数的计算结果,但不能是电路输出,这就类似于 C++ 的常量表达式 constexpr
。
Verilog 的基本单元就是模块,模块是具有输入输出端口的逻辑块,可以代表一个物理器件,也可以代表一个复杂的逻辑系统,比如基础逻辑门器件或者通用的逻辑单元。一个数字电路系统一般由一个或者多个模块组成,模块化设计将总的逻辑功能分块实现,通过模块之间的互联关系实现所需要的整体系统需求。
+所有模块以关键词 module
开始,以关键词 endmodule
结束,从 module
开始到第一个分号的部分是模块声明,类似于 C 中的函数声明,包括了模块名称、参数列表与输入输出口列表。模块内部可以包括内部变量声明、数据流赋值语句 assign
、过程赋值语句 always
以及底层模块例化。
端口是模块与外界交互的接口,对于外部环境来说,模块内部的信号与逻辑都是不可见的,端口的存在允许我们将端口视为一个黑盒,只需要正确链接端口并且了解模块作用,而不需要关心模块内部实现细节。端口的类型有三种:输入端口 input
,输出端口 output
,和双向端口 inout
。端口会被默认声明为 wire
类型,如果声明为 reg
类型就不能省略对应的 reg
声明。
模块名与模块输入输出列表之间可以加入形如 #(parameter 参数=默认值)
的参数列表,参数可以有多个,拿逗号隔开,可以提供默认值也可以不提供默认值。
下面举个小小的例子,模块内部的内容就省略了吧:
+assign
¶always
/initial
¶除了直接使用信号作为敏感变量,Verilog 还支持通过使用 posedge
和 negedge
关键字将电平变化作为敏感变量。其中 posedge
对应上升沿,negedge
对应下降沿。我们将电平从低电平变成高电平的时刻称为上升沿,从高电平变为低电平的时刻称为下降沿.
实际上 reg
的触发边沿和复位电平是由寄存器本身的电气特性决定的,比如 FPGA 的触发器一般是上升沿触发和高电平复位。但是我们可以通过给 clk
和 rstn
经过非门在连接到 reg
的方式实现所谓的下降沿触发和低电平复位(将非门和 reg
看成一个整体的话)。
这个地方幺蛾子比较多,下边是几个常见的问题:
+核心原因:触发器只有一个时钟输入端口,综合的时候实际上并不能做到多时钟触发。 +
从语义上看这个 always
块既在 clk1
上升沿触发,又在 clk2
上升沿触发。当我们仿真的时候,可以实现这个逻辑功能,但是因为触发器只有一个时钟输入端口,所以综合的时候实际上并不能做到 clk1
、clk2
同时作为时钟触发。
核心原因:不存在这样的触发器。 +
+从语义上看只要clock
、a
、b
、c
数据变化就会引起 always
块触发。仿真可以接受这样的逻辑设计,但是因为不存在即上升沿触发又下降沿触发,所以实际上并不能综合得到这样的时序电路。
+
+虽然不能得到时序电路,但是可以综合得到组合电路。d
的结果依赖于 a
、b
、c
,一但 a
、b
、c
的输入发生了变化,则 d
随着变化,这是符合组合电路的逻辑语义的。最后会得到 b
、c
作为数据,a
作为选择子的二选一多路选择器。既然所有的信号只依赖于 a
、b
、c
,所以简化成只要有信号的改变就触发 always
块。这就是我们的组合电路。
+
+核心原因:一个触发器不能被两个时钟沿触发。 +
d
载入 a
的值,下降沿的时候 d
载入 b
的值。仿真允许 reg
在不同的 always
块被不同的时钟沿触发,但是在综合的时候一个 reg
不能被两个时钟沿触发。
+d
被两个过程同时仲裁,即使我们知道这两个过程并不冲突,但是也不可以被编译通过。类似于 wire
不能被两个输出同时输入,reg
也不能在两个 always
块内被赋值,这都会引起 multi-driven 错误。
+核心问题:数据竞争,不同路径的电平传播速度有快慢,电平在传播期间电路本身就处在不稳定状态,很多中间态在逻辑粉丝上无法覆盖。
+当 cond1 和 cond2 同时为 1,problem 变为 1 的时候触发 always 块。这在仿真的时候不容易发现问题,但是请考虑下面这个情形:
+ +在仿真的时候会看到problem
永远等于 0
,always
块不会触发。但是真实的电路综合之后会因为时延造成问题。当 cond1
从 0
变为 1
的时候高电平需要一段时间在可以到的 problem = cond1 & cond2
的与门;cond2
从 1
变为 0
,低电平也需要一段时间到达与门。如果 cond1
的高电平在 cond2
的低电平之前先到达,则会短暂的出现与门的输入都是高电平,最后 problem
短暂出现高电平,进而 always
块被触发,寄存器被复制。
+解决方法也很简单,转换为同步电路就好了,即在 always
块中使用 posedge clk
作为触发边沿。剩下的使用 if
语句来判断是否触发。
阻塞赋值是顺序执行的,即下一条语句执行前,当前语句一定会执行完毕。这与 C 语言的赋值思想是一致的。阻塞赋值语句使用等号 =
作为赋值符。
非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行,并且相互之间没有依赖关系。非阻塞赋值语句使用小于等于号 <=
作为赋值符。
generate
语句¶为了提升代码变量的局部性(毕竟对于常用的迭代参数,最好是每一个 generate
块对应一个 genvar
变量),所以我们常用下面的语法:
在使用 generate
语句之后,Verilog 会在对应的模块内生成一个专用的命名空间 genblk
,参数式编程下,相同的模块会对应不同的名字,比如 genblk[i]:module_name
,这就不需要担心命名冲突的问题。
最简单的触发器塞了两个寄存器和一根输入的线,在时钟上升沿的时候,将输入的值非阻塞地赋给第一个寄存器,在时钟下降沿的时候,将第一个寄存器的值非阻塞地赋给第二个寄存器。
+某些寄存器会有一个额外的使能引脚EN,只有当 EN=1
的时候,寄存器才会载入输入信号,相应的 Verilog 语法如下:
Info
+我们之前将 always@(*)
的时候,if
是需要搭配 else
使用的,不然会导致环路错误,但是这里不需要,因为 always@(*)
综合得到的电路是用 wire
搭建的,它只是借用了 reg
的 always
块语法而已。但是 always@(posedge clk)
得到的电路使用真实的寄存器搭建的,是不会形成环路问题的。
这段代码对应的寄存器是异步复位寄存器,这类寄存器除了有时钟输入 clk
、使能输入 CE
、数据输入 data
之外,还会有一个额外的输入引脚 rst/rstn
。这个引脚如果输入 0
则寄存器会被复位,则该寄存器是低电平异步复位,复位引脚标注为 rstn
;这个引脚如果输入 1
则寄存器会被复位,则该寄存器是高电平异步复位,复位引脚标注为 rst
。
由于复位操作不需要等待时钟信号为上升沿,只要有复位信号就可以立即复位,所以复位操作是异步的。
+这段对应的是同步复位寄存器,对于高电平同步复位寄存器来说,没有显式的异步复位引脚,只有时钟信号 clk
,复位信号 rst
会在时钟上升沿的时候生效,所以复位操作是同步的。
同步复位寄存器更像是一个带有多路选择器的使能寄存器,还是对于高电平同步复位寄存器来说,使能信号 CE
接的是 rst | wen
,这样只有在复位信号 rst
为 1
或者写使能信号 wen
为 1
的时候,寄存器才会被写入。复位信号 rst
作为选择子对写入值进行选择,当 rst
为 1
的时候,选择初始值 INTIAL_VALUE
,当 rst
为 0
的时候,选择输入数据 data
。
FPGA 的复位信号 rstn
由 FPGA 芯片的 C12 引脚引入。当 vivado 将 bitstream 下载到 FPGA 板之后,rstn
信号会先保持一段时间的 0
,使得所有的寄存器可以被充分初始化,然后 rstn
信号变为 1
且一直保持不变,这样所有的寄存器就从初始化阶段进入工作阶段,开始载入数据。
FPGA 进入工作阶段后,我们也可以按开发板的 reset 按钮,让 rstn
再次输入 0
,重新复位所有寄存器的值。
因为 FPGA 板的 rstn
在初始化阶段是低电平,所以该信号只能直接用于复位低电平复位寄存器。对于高电平复位的寄存器可以将 rstn
取反,然后用 rst
作为复位信号。
+
logic
与 bit
¶除了熟悉的 0 与 1 之外,还拥有未知值/Unknown (x) 与高阻态/High-impedance (Z) 的值的类型叫做 4 状态类型/4-state types。注意到常用的 reg
只可以在像 initial
和 always
的过程赋值中被驱动,而 wire
只可以在连续赋值 assign
中被驱动,这就很不方便,所以 SystemVerilog 就引入了一种新的4状态类型 logic
,它的默认值为 x,可以出现在过程赋值与连续赋值之中。
在一般的测试程序之中,我们并不需要未知值与高阻态值,所以衍生了只有 0 和 1 的 2 状态类型。使用 2 状态类型有很多好处,比如减少内存使用、提升模拟速度,因而在数字设计中很好用。当 4 状态类型转化为 2 状态类型的时候,未知值和高阻态都会被转换成 0。 SystemVerilog 引入的最重要的 2 状态类型就是 bit
,表示单独 1 位的值(电平)。
typedef
语法¶enum
枚举¶直接上代码。
+需要逐句逐字分析的只有不几行:
+enum
定义了一个枚举类型,logic [2:0]
表示这个枚举类型是一个3位的逻辑类型,所有的枚举变量和长度都是3位数据。{S0, S1, S2, S3, S4, S5, S6, S7}
一次性定义了枚举常量,从左到右依次是 0-7 的逻辑常量,这就避免了显示提供立即数的麻烦。typedef
和 state_t
是类型定义,将这个枚举变量定义为 state_t
类型,使用 state_t
类型的变量 state
代替了原来的 logic [2:0]
类型,就不用费尽心思保持位宽相同了。state
,其可以存储定义的枚举类型的任意一个值。queue
队列¶queue
语法仅用于仿真,不要用它实现电路,但是作为基本数据结构辅助还是很好的,
struct
结构¶对应的模块端口语法需要是 input/output data_vector data_instance;
这样就将很多个数据打包成一个数据结构,输入与输出的结构端口可以直接用结构变量进行链接,结构变量之间可以直接赋值,方便传输与处理。
+package
包¶++包是用来定义一堆杂七杂八的参数用的。
+
我们可以在包中定义各种需要的参数/parameter,类型/typedef,结构/struct,函数/function。
+使用类似于 C++ 中的命名空间的语法来使用包中的定义,比如 output Conv::data_vector result;
,如果要引入 Conv
内的所有定义,可以使用 import Conv::*;
来实现。
将包的定义放在一个文件的开头就可以引用包定义的内容,我们一般使用 Verilog 头文件 .vh
与宏实现,比如 `include "Conv.vh"
。
interface
接口¶interface
接口是用来简化交互信号处理的。
interface name ... endinterface
定义一个 interface
块,并且可以进行参数配置。
always_comb
扩展¶约 0 个字
+约 14 个字 6 行代码
+第一个报错:
+解决方法:
+约 0 个字
+约 1455 个字 1361 行代码 预计阅读时间 22 分钟
+我们首先以一种特别的角度看与门:与的运算的作用之一就是屏蔽,当某个输入的值为零时,与的输出就是零,不管另一个输入是什么。这就使得我想要的数据都未被屏蔽,不想要的都被屏蔽为0。比如对于运算\(A\land S\),\(S\)可以看作一个选择子,当\(S=T\)的时候,输出就是\(A\),不论\(A\)的真值为多少,输出的值就是\(A\)的值;当\(S=F\)的时候,输出就是\(F\),这时候\(A\)就被屏蔽了。
+二路选择器的逻辑就是“屏蔽”,对于下面的二路选择器,最重要的结构就是上下两个与门和中间一个非门,选择信号\(S\)分成两份,通过非门变成两个不同的信号,分别接向两个与门,如果\(S\)的信号为\(1\)/\(T\),那么就将下面的门屏蔽,输出上边的门信号;反之亦然。 +
+这里边利用了Verilog内置的一些门,比如AND
和OR
门。这种描述方式的优点就是可以很好的与真实的电路相对应,但是缺点就是不够简洁,写起来很坐牢。
+
这种描述方法充分利用了与&
、或|
、非~
以及异或^
等运算符代替了AND
、OR
、NOT
等门的描述,使得描述更加简洁。忍不住了,直接写数组。
+
~
>&
>|
,所以这里的写法是正确的。
+我们还应该知道:
+if-else 必须在always块中使用,并且输出必须是reg类型。但是在always@(*)
中,内部的reg被综合成wire类型
多路选择器可以根据选择子从多个单bit输入中选择单bit输出,但是如果我们需要从多个多bit输入中选择多bit输出,那么就需要使用复合多路选择器。复合多路选择器在硬件实现上其实是由多个单路选择器级联而成的。
+七段数码管的显示译码的对应关系如下,使用复合多路选择器,就不难得到下面源码。解释源码的方法很简单,把它的接口a
到g
分开,当卡诺图写就好了。
好看一点并且比较符合选择想法的写法。
+这个是对应的图片,非常的朴素。 + +但是这个是老实人写法,就直接按照真值表画电路硬刚,千万别这么写,丑死了。
+说是有限状态机,其实就是完成 C 程里面一个常见的小程序,记录输入 a 的数量,当连续输入三个 a 的时候,结束程序,当输入 b 的时候,计数清零。
+实现了一个分频器,将输入的时钟信号分频为 ½ 的幂的频率输出。
+很简单有效的实现,clk_div[0]
每个时钟周期翻转一次,其频率是时钟频率的一半,而翻转的时候会向上产生进位,从而 clk_div[1]
的频率是 clk_div[0]
的 ½,也就是时钟频率的 ¼,后边的频率依次减半。
丑陋版,因为当初做的时候改了好多,这种形式比较好 debug,当然可能用枚举会好看一丢丢,不过好看不到哪里去。 +
还有优化的空间,但是最重要的想法在于全局使能信号,只有当低位有进位的时候,也就是 low_co
为 1
的时候,传给高位的信号(包括低位向高位的信号)才会有意义,否则就有可能出现乱进位的情况。
+
定义了需要的参数与数据类型。
+ +作为外壳调用移位器与卷积计算模块
+移位器模块,读入数据并且输出,注意结构体与数组/向量的转换与链接。
+卷积计算模块,乘法器调用的是前一个实验的乘法器。需要注意卷积核与数据的乘积(调用乘法器)与加法树在宏观上其实是组合逻辑的想法,我们完全将其作为模块化硬件的实现,不依赖于有限状态机。乘法器完成计算的时候需要传递信号给有限状态机,这里的实现容易被忽略,需要注意一下。
+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 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 |
|
约 0 个字
+约 0 个字
+约 0 个字
+约 4862 个字 129 行代码 预计阅读时间 18 分钟
+寄存器 | +ABI 名称 | +用途描述 | +caller-saved | +callee-saved | +
---|---|---|---|---|
x0 |
+zero |
+硬件 0 | ++ | + |
x1 |
+ra |
+返回地址(return address) | +yes | ++ |
x2 |
+sp |
+栈指针(stack pointer) | ++ | yes | +
x3 |
+gp |
+全局指针(global pointer) | ++ | + |
x4 |
+tp |
+线程指针(thread pointer) | ++ | + |
x5 |
+t0 |
+临时变量/备用链接寄存器(alternate link reg) | +yes | ++ |
x6-7 |
+t1-t2 |
+临时变量 | +yes | ++ |
x8 |
+s0/fp |
+需要保存的寄存器/帧指针(frame pointer) | ++ | yes | +
x9 |
+s1 |
+需要保存的寄存器 | ++ | yes | +
x10-11 |
+a0-a1 |
+函数参数/返回值 | +yes | ++ |
x12-17 |
+a2-a7 |
+函数参数 | +yes | ++ |
x18-27 |
+s2-s11 |
+需要保存的寄存器 | ++ | yes | +
x28-31 |
+t3-t6 |
+临时变量 | +yes | ++ |
pc |
+pc |
+程序计数器(program counter) | ++ | + |
Caller-Saved:调用者保存寄存器,也被称为可变寄存器/Volatile Registers,被调用者可以自由地改变这些寄存器的值,如果调用者需要这些寄存器的值,就必须在执行程序调用之前保存这些值。t0
-t6
(临时寄存器)、a0
-a7
(返回地址与函数参数)与 ra
(返回地址)都是调用者保存寄存器。
Callee-Saved:被调用者保存寄存器,这些寄存器的值在过程调用之前和之后必须保持不变,被调用者如果要使用这些寄存器,就必须在返回之前保存这些值。这就意味着要保存原来的值,正常使用寄存器,恢复原来的值。s0
-s11
(被保存的寄存器)和 sp
(栈指针)都是被调用者保存寄存器。
全局指针 gp
和线程指针 tp
很特殊,先不考虑。
RV32I 有 4 种基础的指令格式(R/I/S/U),再根据立即数解码的不同又分出两种(B/J),总共六种指令格式
+R 型指令
+31 | ++ | 25 | +24 | ++ | 20 | +19 | ++ | 15 | +14 | ++ | 12 | +11 | ++ | 7 | +6 | ++ | 0 | +||||||||||||||
funct7 | +rs2 | +rs1 | +funct3 | +rd | +opcode | +
使用寄存器进行数字逻辑运算的指令格式,运算由 opcode funct3 funct7
决定,rd = rs1 op rs2
(shift
类例外,它们用 rs2
位置表示移位数的立即数)。
I 型指令
+31 | ++ | 20 | +19 | ++ | 15 | +14 | ++ | 12 | +11 | ++ | 7 | +6 | ++ | 0 | +|||||||||||||||||
imm[11:0] | +rs1 | +funct3 | +rd | +opcode | +
使用寄存器和立即数进行数字逻辑运算,以及 load
类指令等的指令格式,运算类型等由 opcode funct3
决定,如果是 ALU 运算,则 rd = rs1 op imm
。
立即数是 {{20{inst[31]}}, inst[31:20]}
,也就是对 imm[11:0]
进行符号位扩展到 32 位。
S 型指令
+31 | ++ | 25 | +24 | ++ | 20 | +19 | ++ | 15 | +14 | ++ | 12 | +11 | ++ | 7 | +6 | ++ | 0 | +||||||||||||||
imm[11:5] | +rs2 | +rs1 | +funct3 | +imm[4:0] | +opcode | +
store
类指令,store
的大小由 funct3
决定,以变址模式进行寻址,即 rs1 = [rs2+imm]
。
立即数是 {{20{inst[31]}}, inst[31:25], inst[11:7]}
。
B 型指令
+31 | ++ | 25 | +24 | ++ | 20 | +19 | ++ | 15 | +14 | ++ | 12 | +11 | ++ | 7 | +6 | ++ | 0 | +||||||||||||||
imm[12,10:5] | +rs2 | +rs1 | +funct3 | +imm[4:1,11] | +opcode | +
由 S 型指令分来,与之区别是立即数读取顺序不同,是所有分支类指令。是否分支由 funct3 rs1 rs2
决定。
立即数是 {{19{inst[31]}}, inst[31], inst[7], inst[30:25], inst[11:8], 1'b0}
。
U 型指令
+31 | ++ | 12 | +11 | ++ | 7 | +6 | ++ | 0 | +|||||||||||||||||||||||
imm[31:12] | +rd | +opcode | +
LUI
和 AUIPC
,立即数都是在高 20 位,而且没有源操作数。
立即数是 {inst[31:12], 12'b0}
。
J 型指令
+31 | ++ | 12 | +11 | ++ | 7 | +6 | ++ | 0 | +|||||||||||||||||||||||
imm[20,10:1,11,19:12] | +rd | +opcode | +
由 U 型指令分来,区别也是立即数读取不同,仅有 JAL
一个指令。
立即数是 {{11{inst[31]}}, inst[31], inst[19:12], inst[20], inst[30:21], 1'b0}
。
与正常无异,十六进制数前加 0x
,二进制数前加 0b
,十进制数直接写或者在前面加 0
。用单引号括起来的字母数字字符/Alphanumeric Characters 会根据 ASCII 表转化为相应的数值,比如 'a'
会转化为 97
。
符号是和数值联系在一起的名字,标签会自动地转化为符号,我们也可以使用 .set
指令显式地声明一个符号。符号名可以包含数字、字母与下划线,但是不能以数字开头。符号名是大小写敏感的。在下面的程序中,我们就定义了一个符号 max_temp
,并且将其值设置为 100
。
标签是表示程序位置的一个记号,可以被汇编指令(Instructions and Directives)引用并且在汇编与链接过程之中翻译为地址。GNU 汇编器一般会接受两种类型的标签:符号标签/Symbolic Labels 和数字标签/Numeric Labels。符号标签以符号存储在符号表中,一般用来表示全局变量与某个过程,使用标识符加上冒号 name:
来定义,标识符的命名与符号的命名一样。
数字标签通过单独一位数字加上冒号来定义,一般用于局部的引用,并且也不会存储在可执行文件的符号表之中。数字标签可以在同一个汇编程序之中重复定义,但是符号标签不能。
+对数字标签的引用需要加上后缀 b
或 f
来表明是引用前边定义的标签 b
还是后边定义的标签 f
。下面是对标签的一个例子:
汇编指令(Directives)用来控制汇编器,比如 .section .data
就是用来告诉汇编器接下来的指令将放到 .data
节之中;.word 10
则告诉汇编器分配一个 32 位的值并且将其放到当前节之中。
一般的汇编指令被编码成一个字符串,包含着指令名与指令参数。在 GNU 汇编器上,汇编名以一个点 .
作为前缀,比如 .section
,.word
,.globl
等等。
下面是一些最常用的汇编指令:
+.dword arg_expr [, arg_expr]*
: 添加一个或多个以逗号分隔的 64 位值到程序中;.word arg_expr [, arg_expr]*
: 添加一个或多个以逗号分隔的 32 位值到程序中;.half arg_expr [, arg_expr]*
: 添加一个或多个以逗号分隔的 16 位值到程序中;.byte arg_expr [, arg_expr]*
: 添加一个或多个以逗号分隔的 8 位值到程序中;.string string
: 添加一个以 NULL
结尾的字符串到程序中;.ascii string
: 添加不以 NULL
结尾一个字符串到程序中。.asciz string
:这是 .string
的别名;.bss
节添加值and
,or
,xor
,andi
,ori
,xori
,均执行按位运算;lop dest, src1, src2
,lopi dest, src1, imm
;sll
,srl
,sra
,slli
,srli
,srai
,算数移位和逻辑移位根据 a
和 l
区分;sop dest, src1, src2
,sopi dest, src1, imm
;memop reg, off(addr)
memop
为指令名,reg
为指令的目标/源寄存器,off
为偏移量,addr
为基址寄存器/Base Address。off(addr)
可以计算地址为 addr + off
的内存位置,按字节寻址。branch
类条件跳转指令:
jump
类无条件跳转指令:
注意到,我们这时候我们在这里用的是 offset
而不是 label
,这就必须要说一下直接目标地址的编码了。当我们想要跳转到某个地址的时候,第一个反应都是直接在指令之中塞进去这个地址,但是在 RV32I 之中,我们使用的是 32 位的地址,而 RV32I 的指令都是 32 位编码的,所以将一个 32 位的地址塞到一个 32 位的指令中是不可能的,而对于一个经典的 beq
指令而言,除去 7 位的 opcode
与 3 位的 funct3
,还有 5 位的 rs1
与 rs2
之外,我们只剩下 12 位留给 label
/offset
了。
为了克服这个限制,直接目标地址其实会编码成相对于在这条指令执行时相对于程序计数器的偏移量,本质上是一个立即数,这样我们就可以用一个 12 位的偏移量来表示跳转目标的地址了,这也阻止了指令跳转到太过于远的地方,只能跳转到 ±4KiB 范围之内。
+注意到我们这里用到了直接目标地址/Direct Target Address 这个词,这种情况下,目标地址会直接编码到指令之中。相对地就有间接目标地址/Indirect Target Address,这种情况下,目标地址会存储在一个寄存器或者内存之中,指令会跳转到这个地址之中,典型例子就是 jr rs1
。
jal
指令其实是 Jump and Link 的缩写,表示跳转并链接,连接的是返回地址,它会将下一条指令的地址存到 rd
寄存器之中,然后跳转到 offset
之后的地址。想象一下函数调用的情景,我们跳转到函数的地址之后,根据现有寄存器的内容进行操作,然后再返回,也就是跳回来,这就需要我们提前就将返回的地址 pc + 4
存到一个规定好了的寄存器之中,下面是一个例子:
这里的 jr ra
就是返回到函数调用前下一个命令的地址——它的任务就是返回,自然也没有 link 的职责,RISC-V 只提供了 jalr
指令,所以我们看到 jr
其实是将存储链接地址的寄存器换成了硬件零寄存器 x0
/zero
,也就是 jalr zero, 0(ra)
。
当我们不显式指定存储返回地址的寄存器时,比如 jal offset
,我们就将返回地址存储在 x1
/ra
之中,ret
指令就是读取 x1
之中的地址并跳转回去的。
CS61C
+But sometimes, for the programmer’s benefit, it’s useful to have additional instructions that aren't really implemented by the hardware but translated into real instructions.
+伪指令 | +实际指令 | +意义 | +
---|---|---|
lla rd, offset |
+auipc rd, offset[31:12] addi rd, rd, offset[11:0] |
+加载局部地址 | +
la rd, symbol |
+PIC: auipc rd, GOT[symbol][31:12] l{w|d} rd, GOT[symbol][11:0](rd) Non-PIC: lla rd, symbol |
+加载全局地址 | +
l{b|h|w} rd, symbol | +auipc rd, delta[31:12] + delta[11] l{b|h|w} rd, delta[11:0](rd) |
+加载全局变量 | +
s{b|h|w} rd, symbol, rt | +auipc rt, delta[31:12] + delta[11] s{b|h|w} rd, delta[11:0](rt) |
+保存全局变量 | +
nop |
+addi x0, x0, 0 |
+不进行任何操作 | +
li rd, imm |
+addi rd, imm if \(imm \in [0, 4096]\) lui rd, (imm >> 12) addi rd, rd, (imm & 0xFFF) |
+将立即数加载到 rd 中 |
+
mov rd, rs |
+addi rd, rs, 0 |
+从 rs 拷贝到 rd |
+
not rd, rs |
+xori rd, rs, -1 | +rd = ~rs 按位取反 | +
neg rd, rs | +sub rd, x0, rs | +rd = -rs | +
seqz rd, rs | +sltiu rd, rs, 1 | +set rd if rs == 0 | +
snez rd, rs | +sltu rd, x0, rs | +set rd if rs != 0 | +
sltz rd, rs | +slt rd, rs, x0 | +set rd if rs < 0 | +
sgtz rd, rs | +slt rd, x0, rs | +set rd if rs > 0 | +
beqz rs, offset |
+beq rs, x0, offset |
+branch if rs == 0 | +
bnez rs, offset |
+bne rs, x0, offset |
+branch if rs != 0 | +
blez rs, offset |
+bge x0, rs, offset |
+branch if rs <= 0 | +
bgez rs, offset |
+bge rs, x0, offset |
+branch if rs >= 0 | +
bltz rs, offset |
+blt rs, x0, offset |
+branch if rs < 0 | +
bgtz rs, offset |
+blt x0, rs, offset |
+branch if rs > 0 | +
bgt rs, rt, offset |
+blt rt, rs, offset |
+branch if rs > rt | +
ble rs, rt, offset |
+bge rt, rs, offset |
+branch if rs <= rt | +
bgtu rs, rt, offset |
+bltu rt, rs, offset |
+branch if > unsigned | +
bleu rs, rt, offset |
+bgeu rt, rs, offset |
+branch if <= unsigned | +
j offset |
+jal x0, offset |
+无条件跳转,不存返回地址 | +
jal offset |
+jal x1, offset |
+无条件跳转,返回地址存到 ra |
+
jr rs |
+jalr x0, 0(rs) |
+无条件跳转到 rs 位置,忽略返回地址 |
+
jalr rs |
+jalr x1, 0(rs) |
+无条件跳转到 rs 位置,存返回地址 |
+
ret |
+jalr x0, 0(ra) |
+通过返回地址 x1 返回 |
+
call offset |
+auipc ra, offset[31 : 12] jalr ra, offset[11:0](ra) |
+远调用 | +
tail offset |
+auipc t1, offset[31 : 12] jalr zero, offset[11:0](t1) |
+忽略返回地址远调用 | +
标签/Label 其实是表示程序某个位置的符号,一般通过 name:
来定义,并且可以插入一个汇编程序之中来表征一个位置,这样就可以被别的汇编指令引用。符号/Symbols 是与数值相联系的名字,符号表/Symbol Table 是一个将符号与其值联系起来的映射表。汇编器将标签自动地转化成符号,并且将其与其地址联系起来,还将所有的符号放到符号表之中。使用 riscv64-unknown-elf-nm
可以查看某个目标文件的符号表。
符号/Symbol 可以分为全局符号/Global Symbols 和局部符号/Local Symbols。全局符号可以使用连接器在其他的目标文件中解析未定义引用,局部符号只在当前文件之中可见,也就是不可以用来解析其他文件之中的未定义引用。
+++Symbols are classified as local or global symbols. Local symbols are only visible on the same file, i.e. the linker does not use them to resolve undefined references on other files. Global symbols, on the other hand, are used by the linker to resolve undefined reference on other files.
+
默认来讲,汇编器会将标签/Label 作为局部符号,但是可以通过 .globl
指令来声明全局符号。程序的入口和出口都需要定义全局符号,比如:
对于一个典型的 C 程序,程序的源代码经过编译器与连接器等工具处理、打包成一个包含了数据等信息与指令的可执行文件,载入进内存之后,操作系统再执行它。精确地讲,一个 .c
程序经过编译器得到一个 .s
的汇编程序,然后通过汇编器得到一个机器语言模块目标文件 .o
,与别的库文件如 lib.o
一起通过连接器得到一个可执行文件。
编译过程可以分解为下面的步骤:词法分析/Lexical Analysis,语法分析/Syntax Analysis,语义分析/Semantic Analysis,中间代码生成/Intermediate Representation Code Generation,中间代码优化/Intermediate Representation Code Optimization,目标代码生成/Object Code Generation,目标代码优化/Object Code Optimization 这几步。
+中间代码生成这一步允许我们将整个编译过程模块化,对于很多不同的语言,比如 C,C++,Rust,我们可以生成同一种中间语言 IRC,然后再将 IRC 转化为不同的机器码。不仅如此,IRC 的存在还显著降低编译优化的难度。
+汇编器不仅仅只做将汇编代码转换成机器码的工作,还会将不在指令集架构之中的伪指令转换成真实的指令,进而生成 ELF/Extensible Linking Format 格式的目标文件。ELF 文件是可执行文件,目标文件,共享库与核心转储文件/Core Dump 的标准格式。目标文件主要有三种:可重定位文件/Relocatable File,可执行文件/Executable File 与共享目标文件/Shared Object File。
+可重定位文件存储着二进制代码与数据,适合与别的目标文件链接并生成一个可执行文件或者共享目标文件。可执行文件包含了二进制代码与数据,告诉操作系统如何加载程序,初始化程序的内存与状态并且进行执行,在执行可执行文件的时候,shell 调用操作系统中一个称为加载器/loader 的函数,将可执行文件中的代码与数据复制到内存之中,然后将控制转移到这个程序的开头。共享目标文件主要面对链接,连接器处理共享目标文件与其他的可重定位文件或者共享目标文件来生成另一个目标文件,动态连接器将功效目标文件与可执行文件结合,创建进程映像。
+从上到下,ELF 文件由 ELF 头,程序头表,数据(节或者段)与节头表组成。ELF 头包含了文件的基本信息,节头表是一个节头数组,每一个节头记录了对应的的节的信息,比如节的名字、大小、在文件的偏移与读写权限等,连接器与装载器都是通过节头表来定位和访问节的属性,我们使用 readelf
工具来查看节头表。程序头表描述了系统如何创建一个程序的进程映像,每一个表项都定义了一个段/Segment,并且引用了节。
可以清晰的得出,可重定位文件必须要有节头表,但是程序头表并不必须,可执行文件必须要有程序头表,但是节头表并不必须。汇编器生成的就是
+每一个可执行文件都有一个包含了程序的信息的文件头,其中的一个字段就存储了程序的入口地址/Entry Point Address。一旦操作系统将整个程序加载进主存,就将程序计数器的值设置成程序的入口地址,这样程序就开始执行了。
+连接器负责设置可执行文件的入口地址字段。它首先会寻找一个全局符号 start
(某些连接器会寻找 _start
),如果找到了,就将程序的入口字段设置为 start
的地址。如果没有找到,就将入口地址设置为一个默认的值,一般是整个程序的第一个指令的地址。比如:
使用下面指令进行编译:
+在链接的时候,我们将 exit.o
放在了 main.o
的前面,这样 exit.o
的内容就会放在 main.o
的前面,使用 riscv64-unknown-elf-objdump
就能发现这一点,但是程序仍然以 main.o
的 _start
作为入口。使用 riscv64-unknown-elf-readelf
可以查看程序的入口地址,这就可以验证程序的入口地址是 _start
。
无论是可执行文件、目标文件还是汇编程序,他们都是按照不同的节组织的,每个节都包含了数据或者指令,并且都映射到内存中一段连续的区域。Linux 系统的可执行文件中,一般会出现下面四个节/节:
+.text
:包含了程序的指令;.data
:包含了程序中初始化过的全局变量,这些全局变量的值需要在程序开始执行之前就初始化掉;.bss
:包含了程序中未初始化的全局变量;.rodata
:包含了程序中的只读数据/常量,比如字符串常量,这些数值在程序执行过程中不能修改。++When linking multiple object files, the linker groups information from sections with the same name and places them together into a single section on the executable file. For example, when linking multiple object files, the contents of the
+.text
sections from all object files are grouped together and placed sequentially on the executable file on a single section that is also called.text
. The following figure shows the layout of anRV32I
executable file that was generated by theriscv64-unknown-elf-ld
tool, and is encoded using the Executable and Linking Format. This file contains three sections: the.data
, the.rodata
, and the.text
sections. The contents of section.text
are mapped to addresses8000
to8007
, while the contents of section.data
are mapped to addresses800d
to8011
.
当链接不同的文件的时候,连接器会将相同名字的节合并到可执行文件的一个节之中。比如,当链接多个目标文件的时候,所有的 .text
节都会被顺序地合并到一个叫 .text
的节之中。默认情况下,GNU 汇编器会将所有的信息都放到 .text
节之中。如果想要将信息放到其他节,可以使用 .section secname
指令,这个指令会告诉汇编器将接下来的信息放到叫 secname
的节之中。
这里的第二行包含了一个标签 x:
和一个 .word
指令,它们一起使用可以声明一个全局变量 x
,并且初始化为 10
。这个全局变量会被放到 .data
节之中。接下来的指令将下面的内容存回 .text
节之中。连接器通过不同节的分块与重定位,避免了将指令与数据混在一起的冲突。
约 426 个字 3 行代码 预计阅读时间 1 分钟
+Info
+User-Level ISA defines the normal instructions needed for computation:
+Any assembly programs are encoded as plain text files and contain four main components:
+A Load/Store Architecture is an instruction set architecture that requires values to be loaded/stored explicitly from/to memory before operating on them. In other words, to read/write a value from/to memory, the software must execute a load/store instruction.
+The RISC-V ISA is a Load/Store Architecture. Hence, to perform operations (e.g. arithmetic operations), on data stored on memory, it requires the data to be first retrieved from memory into a register by executing a load instruction. As an example, let us consider the following assembly code, which loads a value from memory, multiply it by two, and stores the result on memory.
+The following toolchain is included in the binutils-riscv64-unknown-elf
package:
riscv64-unknown-elf-as
: a version of the GNU Assembler that generates code for RISC-V ISAs.-march=rv32i
: specify the target ISA as RV32I.-mabi=ilp32
: specify the target ABI as ILP32.riscv64-unknown-elf-as -march=RV32I -mabi=ilp32 hello.o -o hello.s
riscv64-unknown-elf-ld
: a version of the GNU linker that links object files into an executable file.-m elf32lriscv
: specify the object file format as ELF32.riscv64-unknown-elf-objdump
: a version of the GNU objdump that displays information about object files.-D
or --disassemble-all
: disassemble the contents of a binary file.riscv64-unknown-elf-ld -m elf32lricsv trunk.o -o trunk.x && riscv64-unknown-elf-objdump -D trunk,x
-r
or --reloc
: inspect the contents of the relocation table on the file.riscv64-unknown-elf-nm
: a version of the GNU nm that inspects the symbol table of an object file.riscv64-unknown-elf-nm trunk.o
riscv64-unknown-elf-readelf
: inspect the ELF header of an executable file.-h
or --file-header
: display the ELF file header.约 5 个字
+约 68 个字
+Abstract
+这是我在浙江大学图灵班学习计算机系统相关课程的笔记与学习记录。
+约 2 个字
+约 112 个字
+《数学分析》 梅加强 著
+《数学分析讲义》 陈天权 著
+《Real And Complex Analysis》 Walter Rudin 著
+《哲学导论》 王德峰 著
+《西方现代思想讲义》 刘擎 著
+《The Western Heritage》
+《枫丹白露宫 千年法国史》 让·弗朗索瓦·埃贝尔、蒂埃里·萨尔芒 著
+约 407 个字 159 行代码 预计阅读时间 3 分钟
+Info
+为了有效缓解重装系统之后配环境的痛苦,我决定在这里写下我的环境配置纪实。
+什么?配环境不痛苦?那你给我配。
+忍不了了!所以写了个配环境的脚本!
+我不说了,假定所有人都会。
+删除本版本的 WSL:wsl --unregister Ubuntu-20.04
后边跟的是版本号,使用 wsl --list
就能看见了。
zsh
¶安装 zsh
并设置为默认 shell:
+
使用 oh-my-zsh
美化 zsh
:
+
使用 vim 打开 ~/.zshrc
,修改 ZSH_THEME
为 ZSH_THEME="half-life"
:
+
配置插件: +
还要修改 ~/.zshrc
,在 plugins
里边加入 zsh-autosuggestions
和 zsh-syntax-highlighting
:
+
原版的 vim 不太好看,而且功能也有限,虽然现在不太用 vim 了,但是还是好看一点比较好,这里选择安装 lunarvim:
+安装步骤大概分几步:安装 lunarvim 的所有依赖,改环境变量,安装 lunarvim,改环境变量。环境变量千万别改错,改回去稍微费点劲。
+开始享受 lunarvim 吧!
+首先需要安装依赖: +
其实就是按照系统 I 的文档开抄:
+说是使用 git submodule
,其实就是 git clone https://github.com/verilator/verilator.git
makefile 的内容放在下边:
+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 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 |
|
首先添加 ppa 源:
+下面利用 update-alternatives
进行 gcc 版本切换:
之后按照提示选择 gcc 版本即可。
+添加不了就拉倒,手动装就好了,但是可能用不了几个版本了,因为这样默认的 gcc 就是 gcc-13 了,但是仍然不建议手动编译安装,因为会出现坑爹的情况。
+约 44 个字
+Abstract
+挖坑时间到!这真的只是我对我的学习期望而已,想学不代表一定会学(x
+当然,我是一定会奋力学的!
+约 0 个字
+