第二章:数据类型

第一节:hello-world详解

首先要说一下,我们的编程环境是Ubuntu,编译器是gcc。关于编程环境的搭建,参考我的课程,在这里写这个环境搭建的内容,太无聊了,而且还没有视频说的清楚,大家参考冒牌程序员毛哥的视频就可以了。

第一个C程序源文件如下:(C程序源文件就是一个文本文件,所谓文本文件就是一堆文字放在一起形成的一个文件,没有其他啥意思,就是一堆文字而已,文件名叫hello.c)

/*
冒牌程序员-毛哥
*/
/***************hello.c**********/
//hello.c

#include<stdio.h>
int main()
{
    printf("Hello World\n");
    return 0;
}

大家看上面一段代码,是不是非常简单。在看代码以前,有几个原则必须给大家申明下。


  1. 我们所写的C语言程序,是我们人类容易阅读和理解的一段话,这段话最终被翻译成计算机能够理解的话(也就是二进制指令)。所以,C语言程序我们人类看了,即使不懂C语言,也大概能够才出来这段C语言程序干了啥。


  1. 软件是分层的,每层软件都完成本层的工作,至于其他层的工作,由其他层的软件去完成。我们目前知道的软件层有应用程序层和操作系统层。这个在上一章提到过。(这个是非常重要的,就像生活中,你随时必须知道你自己所在的位置)


  1. 现在我们所学习的C语言程序,是应用层的软件,也就是我们编写的是应用程序,并不是操作系统软件,不在操作系统层。


  1. 应用层软件的代码集中考虑程序功能的实现,至于对计算机硬件的操作,只要将对应的操作交给操作系统即可。因此,在我们编写的应用程序的代码中,不涉及具体硬件的操作,只在程序中说明我们要达到什么目的就可以了。


  1. 我们所写的计算机程序中,没有废话,如果有,那就给他删除。


  1. 我们有很多前辈,这些前辈给我们写了很多计算机程序,这些已有的计算机程序对我们很有帮助,可以让我们少写很多代码(我们可以直接使用这些前辈所写的程序)。这些前辈所写的这些供我们使用的计算机程序,一般都在我们计算机中存在了,我们只要知道他在哪里,使用就可以了。当然我们要知道如何使用前辈给我们写的这些计算机程序。因为前辈所写的程序要给大家用,因此就制定了一套规则,大家只要知道了这些规则,那么就可以使用前辈们给我们写好的程序了。


  1. C语言中的一个一个程序模块,被叫函数(function),其实就是一段子程序。子程序和程序其实没啥区别,子的意思一般就是说他比较小而已,叫着方便,没啥特别的含义。子程序的功能当然和程序功能没啥区别,都是对数据进行处理,这个就涉及到我们如何把要处理的数据,以什么样的方式传递给子程序,子程序如何将结果再返回给使用子程序的主程序,这些都要有规则。


  1. 所有计算机语言,都是形式语言,就是由语言中,语句的长相(准确来说是格式),来决定语句的含义,简单的说,长的像个人,就是人。首先我们说一个程序,其实也就是一个子程序,也就是一个函数,这几个东西可以认为就是一个东西的不同名字。现在问题来了,一个子程序,或者说一个函数,或者说一个C语言程序,长啥样子。或者说一个函数,长啥样子。样子如下:
函数返回值类型 函数名字(参数类型 函数参数1,参数类型 函数参数2,。。。)
{
函数的语句;
。
。
。
}

其中,参数是要被函数处理的数据,而返回值类型代表函数的处理结果,是一个数(这个数可以是整数,可以是小数,所以我们要指明,函数返回的到底是哪种类型的数据)。这个返回的数可以有不同的涵义,到底是啥含义,由创建这个函数的人来确定。大括号中的函数语句,就是函数如何处理数据,如何返回一个处理结果的C代码,也就是我们主要的学习内容。

现在我们可以总结一下。根据上面所说的几条,一个程序,就是一个子程序,就是一个函数,是一个东西。那么一个C语言的程序是不是就是一个函数呢?答案很确定,就是。一个C语言程序就是一个函数,这个函数有一个特殊的名字,叫main。有给定的返回值类型,是int,有固定的参数(现在我们先认为main函数没有参数)。所有C语言的程序,都是一个main函数,函数内容的不同,就造成了程序的不同。也就是说,我们所写的所有C语言程序,都长一个样子,不同的是瓤子(也就是大括号内的内容)。

另外说一下,这个main,其实可以有其他名字,现在我们作为初学者,只要记住,main就是C程序的函数就可以了,不要被那些无聊的东西干扰了。为什么要给C程序确定一个固定的函数名字呢?这个问题其实让大家思考过。如果要执行一个程序,第一条被执行的指令应该是哪条指令呢?现在就告诉大家,大家记住,我们所写的C程序,第一条被执行的指令,就是main函数的第一条指令,main函数的特殊含义就在这个地方。

现在看main中的内容,

printf(“Hello World\n”);

return 0;

这两句话中,printf是一个函数名,也是一个子程序名字。这就是我前面说的,前辈给我们写好的一段子程序,我们只要使用就可以了。当然我们要知道这个函数的名字,函数的用法(所谓的用法就是函数的格式,也就是样子要知道,然后函数的返回值代表什么意思要知道,函数的每个参数代表什么意思也必须知道)。C语言学习过程中,有很大一部分学习内容就是学习前辈所写的函数的用法。

要使用一个函数,必须知道函数的格式,我们把函数的格式叫“函数的声明-declare”。这是专业的叫法。为了让我们能够顺利使用函数,函数的声明已经以文本的形式提供给我们了。先别急,一会我们就能看见这个函数的声明在什么地方了。有了函数的声明,知道了返回值和参数的含义,我们就可以正确使用这个函数了(前提是你学会了C语言)。

现在我们来看printf函数的声明。在程序的开头,有这么一句话:

#include<stdio.h>

从字面意思,就是把stdio.h这个文件包含到本文件中,也就是我们的hello.c文件中。有人问,这个stdio.h文件的内容是啥,有啥用。还有这个文件在哪个地方?

我们一个一个说,先说这个文件在什么地方。这个文件在你计算机的这个目录下面:/usr/include/stdio.h。有人问,你咋知道这个文件在这个目录下面?一会告诉你。现在我们看看这个文件的内容,你自己看一下。在这个文件中,搜索printf,你会搜到这么一句话:

extern int printf(const char *__restrict __format, ...);

这个就是一个函数的声明,里面说明了函数返回一个啥样的数(int),函数的名字(printf),函数的参数(const char *__restrict __format, ...)。有人问,函数的返回值和参数的含义没有告诉我呀,这里没有这个信息,因为啥呢?一会再说。这里只说函数长啥样子,至于函数的代码,这里也没有,可能是前辈不想让我们看到自己的秘密吧,也可能是其他原因,随着课程的进度,大家自然就知道原因了。

main函数内有两句话,剩下的一句就是return 0;这句话了。这句话的意思就是,函数返回值是0。也就是函数的执行结果是0。

程序源代码介绍完了。下面我们就费劲把这个源代码文件转换成计算机可以执行的文件。步骤如下:

gcc -E hello.c -o 11.c

先解释下这条命令的含义。gcc是语言转换器,就是把一种语言转换成另外一种语言的一个工具程序,专业名字叫编译器。-E是gcc这个命令的选项,这个选项的含义是,把源文件hello.c中的#include<stdio.h>这句话展开(#include被叫做预编译指令)。所谓展开,就是用stdio.h的内容,替换#include<stdio.h>这句话。这样printf函数的声明就在新的生成的C源文件11.c中了。

如果你在上述命令的后面添加一个-v选项,如下:

gcc -E hello.c -o 11.c -v

那么屏幕上会出现一堆文字,其中如下文字比较关键:

#include”...”search starts here:
#include<...> search starts here:

/usr/lib/gcc/x86_64-linux-gnu/13/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include

你屏幕上的输出可能和我的不一样,但不会差很多,从这几句话,我们就能够知道,stdio.h文件,大概就在这几个目录下面,在经过find命令查找,就可以知道stdio.h在哪个目录下面了:

find /usr/ -name “stdio.h”

在把预编译指令都展开以后,我们得到了文件11.c。我们再把这个文件转换成汇编语言源文件。(所谓汇编语言,就是计算机可以识别的二进制指令的文字表达形式,也就是我们给每个计算机二进制指令取了一个好记的名字,然后这些名字就形成一种语言,就是汇编语言,有了汇编语言,我们只需要将汇编语言中每一句话使用对应的二进制机器指令替代,就形成了机器语言)。如何转换呢,还是我们的编译工具gcc:

gcc -S 11.c -o 11.s

gcc是命令,-S是选项,意思是将c语言源文件11.c转换成汇编语言源文件11.s。


此时我们得到了一个汇编语言源文件。然后我们将汇编语言转换成机器语言:

as 11.s -o 11.o

as我们叫他汇编器,其实和gcc的性质差不多。


现在有了机器语言文件,我们是不是就可以执行了呢?不是的。前面我们说过,我们所写的程序只是一小部分,大部分程序是先辈写的,我们必须使用先辈的程序,和我们写的程序合并起来,才能完成一个完整的功能。在我们的程序中,我们使用了printf函数。这个函数的机器代码我们还没有找到,我们找到的是printf函数的声明,也就是函数长的样子我们知道了。在知道了函数的声明后,编译器gcc就知道如何把函数的参数传递给函数的执行代码了。但是,函数的执行代码在哪里,gcc一般来说是不知道的(为啥说一般呢?)。所以我们现在得到的11.o这个文件里面,没有printf函数的代码,也就是没有printf函数对应的子程序指令流。所以程序是不完整的,没法执行。

现在我们要做的是,把printf函数的二进制指令和11.o中的二进制指令合并到一起,形成一个完整的程序。我们必须知道,我们所写的是一个应用程序,应用程序需要操作系统帮忙,才能完成一个完整的功能。printf函数的指令流其实也不完整,这个函数的代码所完成的工作只是请求操作系统为我们做一件事情而已。其实操作系统也是我们的前辈帮我们写好的程序,已经在电脑里面了。

如何合并呢?

ld 11.o -lc /usr/lib/x86_64-linux-gnu/Scrt1.o --dynamic-linker /lib64/ld-linux-x86-64.so.2 -o 1

上面的ld命令就是干这个的,ld我们叫做静态链接器,这个过程我们叫链接。我们要把那些文件合并到一起呢,这些文件就是-lc(我们叫做C库,这个里面就有printf的二进制指令,有但不全)。/usr/lib/x86_64-linux-gnu/scrt1.o是我们C语言的一段初始化代码,现在大家记住这句话就OK了,就是我们使用C语言的C函数所需要做的准备工作都在这里了。printf就是一个C函数。还有--dynamic-linker /lib64/ld-linux-x86-64.so.2。这个也是一堆二进制指令,这个文件中的二进制指令的作用我现在给大家概解释一下。首先这个东西叫做动态链接器,和ld叫做静态链接器对应。动态链接器是干啥的?这里就又要提到我们前面说的,要执行一个程序,需要把程序从硬盘装入内存。程序被从硬盘装入内存后,其实还是不能直接被执行的,通常程序执行过程中需要的一些二进制指令此时还不知道在哪里,一些程序的数据也不知道在哪里。这个时候,就需要一段代码对装入内存的程序进行再次加工,补充程序所缺部分。完成这个工作的这一段代码就叫动态链接器,也是把一些必要的程序合并到内存中的可执行程序里面。现在大家只要记住,程序要执行,大概率需要这个过程(为什么是大概率?)。


-o 1 说的是最终生成的可执行文件是1。这个文件可以被执行。


其实上述过程可以用一条命令完成,如下:


gcc hello.c -o 1


我拆开来讲是为了让大家明白其中的过程。

现在我们可以执行这个程序,在命令行输入 ./1回车就可以了,屏幕上会打印出hello world这句话。