CS50
lesson_3
回顾
从scratch图形化编程 ->等价的c语言程序
代码就是纯文本,计算机无法理解这个,它只能看懂二进制。
从源代码到二进制程序,有一个必要的步骤,是编译。比如昨天看到的clang对c语言的编译。(.out
文件就是它的输出)而make并不是编译,它是一个构建工具(虽然在外人看来,它帮我们编译了程序,但它就包含一些命令,有-o之类的)
编译
编译好像是一个善意的谎言,你在编译,你把源代码转换为计算机认识的代码,但其实有很多其他的步骤会在你背后发生(clang隐藏了他们),而今天,我们会给它们取名字,探究一下编译,看看编译是怎么回事,希望这能让我们以后编译的代码更容易理解。我们分为四部
preprocessing(预处理)
在文件的开始,有很多的include指令,这些被称为预处理指令。这些行的起始,有一个#
标记,这可以告诉编译器(clang),这些东西应该先被处理掉。
例如,再cs50.h里面包含着一些函数的声明/原型(如get_string)。而在预处理当中,这就可以告诉电脑这函数是什么,返回类型和参数是什么。
如,因为cs50.h里有队get_string函数的声明,所以第一行的#include cs50.h就能代替
string get_string (string prompt)这行代码,来告诉电脑,grt_string是什么
compiling(编译)
预处理过的代码,(如翻译了各种头文件后),你的代码就会发生一些改变,变成这样
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nhRFcPNQ-1639728300996)(image/15.png)]
这就是64位 x86汇编指令。
这语言之前有提过,a.out。它是很低级的语言,只有cpu能看的懂(他只能看懂这个)。c语言这些高级语言,只不过是对cpu可理解事物的高层抽象。
assembling(汇编)
之后,进一步转换汇编语言,变成0和1.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bWUOnf0i-1639728300998)(image/16.png)]
linking
但我们如何和其他人的0和1融合在一起呢?毕竟,像printf这些函数,cs50.h,这些都是别人写的。
这样,就要使用link,把几块的0和1,融合成1片。
数组(array)
我们的电脑里不仅仅只有cpu(中央处理器),它还有ram or access memory(随机存取储存器)。是笔记本,台式机,服务器都会用到的储存器。你每运行一个程序,打开一个文件都会用到。另一种储存器叫硬盘,你电脑所有的文件都永久储存在那里。(就算电池没电,它们也不会消失)。但ram不同,它是暂时的,但把文件和程序存在ram里,会更高效,你双击,就能打开他们。如打开word,你双击,他就会从硬盘拷贝到ram存储器里,ram存储器虽然小,但他很快。(你打开程序,网页,文档,它们的内容就都会存在里面)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xH30xLzv-1639728300998)(image/17.png)]
这个芯片(ram)可以看作一堆字节的集合,我们将它抽象成1格格小方块。如果你在写c语言,要创建一个字符,它占一字节,那么电脑就会把他存在这里面。如果你要存一个整数,四个字节,电脑就会分配给你连续的四个格子。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KBDdWERj-1639728300998)(image/18.png)]
但当字节一个个的连续在一起的时候,他就会有一个专门的术语,连续储存(数组)。
数组可以让我们在电脑连续输入我们想要的东西,它有利于我们改进代码,也可以提高代码运行速度。
如:
我们想用c语言写一个程序。连续输入score1,score2,score3的分数,然后连续输出
第一个方法,就是直接写。但这种方法很烦琐
int main(void)
{
// Scores
int score1 = 72;
int score2 = 73;
int score3 = 33;
}
第二个就是使用数组。但在c语言里,你使用数组,就必须先告诉电脑 ,数组的长度。
int main(void)
{
// Get scores
int scores[N];
for (int i = 0; i < N; i++)
{
scores[i] = get_int("Score: ");
}
// Print average
printf("Average: %f\n", average(N, scores));
}
而之前我们也讲过类似的东西,字符串。他其实也是数组,只不过他是字符的集合,字符是另一种数据结构。所以,我们也可以用s[i]来进行描述字符串,对他的每个字符进行索引。
int main(void)
{
string s = get_string("Input: ");
printf("Output: ");
for (int i = 0, n = strlen(s); i < n; i++)
{
printf("%c", s[i]);
}
printf("\n");
}
//strlen就是字符串长度的函数
但是,当你储存了一个字符串,转头又写另一个字符串的时候,电脑还会记得住之前那个字符串从哪开始,从哪结束吗,还能找到它么?
当你存入zamyla
的时候,其实你也同时的存入了起始和结束标记。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbJpstki-1639728300999)(image/19.png)]
前面那个是它的变量名,后面结束符号是一个全0字符(空字符),8个位都由0构成,(也可以写作/0)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h07tgvYx-1639728301000)(image/20.png)]
这里的while语句,实现了strlen函数的功能。
//这一段程序能够实现输出字符串单个字符,后面还跟着他的ascll码
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
string s = get_string("String: ");
for (int i = 0; i < strlen(s); i++)
{
int c = (int) s[i];
printf("%c %i\n", s[i], c);
}
}
这个程序 int c = (int) s[i];
直接把一种数据类型转换成了另一种。这是显示类型转换(explicit casting),而其实也可以进行隐式类型转换,直接把c改成s[i],前面的%i(%d)会告诉电脑,利用不同的方式对待相同的位码。
这个程序在进行小写字母转大写。(但以下源码在c语言里行不通)
int main(void)
{
string s = get_string("Before: ");
printf("After: ");
for (int i = 0, n = strlen(s); i < n; i++)
{
if (s[i] >= 'a' && s[i] <= 'z')
{
printf("%c", s[i] - 32);
}
else
{
printf("%c", s[i]);
}
}
它可以优化成以下程序。但要在前面加这个头文件ctype.h
int main(void)
{
string s = get_string("Before: ");
printf("After: ");
for (int i = 0, n = strlen(s); i < n; i++)
{
if (islower(s[i])) //islower存在ctype.h里面
{
printf("%c", toupper(s[i])); //toupper存在ctype.h里面
}
else
{
printf("%c", s[i]);
}
}
printf("\n");
}
而当你不懂这个函数存储于哪个头文件,有什么用的时候,可以在终端输入man,后面接着你想了解的函数,它会提示你(好像是只有cs50
有)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-conInZ2H-1639728301000)(image/21.png)]
我们一般默认对main的写法是int main(void)
,main函数不接受任何输入也可以运行。而当你给里面增加参数(main (int argc,string ardv[])),实际上你在告诉clang,我想这个程序接受一个或更多的单词或数字。这样直接可以写./hello david而不是在getstring函数运行的时候写david。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jZtVmGec-1639728301001)(image/22.png)]
所以如果你在编程里改变了main函数,使他接受了两个参数,如果他输入了两个单词,则会运行if语句。(两个单词是.\argv0 和 Zamla)
而因为argv是一个字符串数组,所以还可以这么写>
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(int argc, string argv[])
{
for (int i = 0; i < argc; i++)
{
for (int j = 0, n = strlen(argv[i]); j < n; j++)
{
printf("%c\n", argv[i][j]);
}
printf("\n");
}
}
//外面的循环迭代字符串
//里面的循环迭代字符串的每个字符(每行输出一个字符)
这种方法可以对一些明文进行加密,(打乱每个字符排序)。利用算法加密文件,并不少见。main函数要是你不返回任何值,他会自动默认返回0
排序
数组可以用于解决很多算法问题。他可以被看成一串储物柜,里面可以是整数,字符,字符串。但他一次只能看到一扇门,他需要一个个的去看。
手机里的通话列表,按字母进行排序,他其实也是有某种类型的数组(数据结构)来储存。但如何为数组中的元素排序?
-
冒泡排序:像冒泡一样一点一点的排好序。(Bobble Sort).
-
选择排序:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。(Selection Sort)
-
归并排序,这个排序要利用到数组。
将所有数组拆分成一个个的状态,然后进行第一轮的两两合并(这个经过是从一个大数组变成n个小数组再到2/n的数组的过程,只不过此时每个数组里都是有序的。然后不断合并。
算法复杂度
选择排序的运行次数是:(n-1)+(n-2)+(n-3)+…+1。等于n(n-1)/2。算法复杂度是O(n^2)。因为平方这个量级太大(这个式子里最大的了)到了最后完全可以忽视其它因数。而冒泡排序的算法复杂度也是这个。
当然还有其他的类型(量级):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RTeCrmV-1639728301001)(image/23.png)]
如:查找某人电话,一次翻一页,是O(n)。一半一半的查找,是log级。而归并排序的算法复杂度是n*log(n)。有n个数字,每个数字排列log(n)次
算法复杂度
选择排序的运行次数是:(n-1)+(n-2)+(n-3)+…+1。等于n(n-1)/2。算法复杂度是O(n^2)。因为平方这个量级太大(这个式子里最大的了)到了最后完全可以忽视其它因数。而冒泡排序的算法复杂度也是这个。
当然还有其他的类型(量级):
[外链图片转存中…(img-7RTeCrmV-1639728301001)]
如:查找某人电话,一次翻一页,是O(n)。一半一半的查找,是log级。而归并排序的算法复杂度是n*log(n)。有n个数字,每个数字排列log(n)次