1.2 计算机程序运行的目的
1.2.1 什么是程序
程序是什么?最为直观的表达就是:程序=数据+算法。对于计算机来说,一个程序就是一堆代码加一堆数据。代码告诉CPU如何加工数据,而数据则是被加工的对象。例如我们写一个加法程序,对于计算机来说,代码告诉CPU是执行加法,数据就是加数和被加数。当然,我们也可以将加法运算的过程封装成一个函数,即便不封装成一个子函数,那它也是在主函数(main)里。C语言程序就是由一个个函数组合而成,这也是C语言模块化的一个强烈表现。
1.2.2 计算机运行程序的目的
既然我们已经知道了程序是什么,那么我们接下来就可以来探讨计算机运行程序的目的是什么了。其实运行程序的目的无外乎如下几个:要么是得出一个确定的运行结果;要么是关注运行的过程;要么二者皆有。得到一个结果还是可以理解的,但说程序运行只是为了过程可能就不太好理解了,设想那些没有返回值的函数不都是在注重过程,它们并不会返回一个结果。
函数程序的形参就是待加工的数据,当然函数内还需要一些临时数据(局部变量),函数本体就是代码(程序的组成:数据+算法),函数的返回值就是结果,函数体的执行就是过程,所以说函数的运行目的是:结果、过程或者二者全有。
· 例子1
int add(int a, int b)
{
return a + b;
}
// 这个函数的执行就是为了得到结果
· 例子2
void add(int a, int b) { int c; c = a + b; printf("c = %d\n", c); } // 这个函数的执行重在过程(重在过程中的printf),不需要返回值
· 例子3
int add(int a, int b)
{
int c;
c = a + b;
printf("c = %d\n", c);
return c;
} // 这个函数既重结果又重过程
通过上面的例子,大家应该有了新的认识,理解了程序的组成和程序运行的目的。
1.2.3 静态内存SRAM和动态内存DRAM
上一节我们探讨了什么是程序,以及运行程序的目的是什么。这一节我们准备谈一谈存储和运行程序的硬件——内存。内存大致分为静态内存(Static RAM, SRAM)和动态内存(Dynamic RAM, DRAM)。
SRAM的性能非常高,是目前读写最快的存储设备了,但是它也非常昂贵,所以只在要求很苛刻的地方使用,如CPU的一级缓冲、二级缓冲;而DRAM的速度要比SRAM慢,但是DRAM的价格比SRAM便宜很多。DRAM又有好多代,譬如最早的SDRAM,后来的DDR1、DDR2……, LPDDR,我们这里介绍其中一种DDR。
DDR(Doubk Date Rate)是一种改进型的RAM,它可以在一个时钟读写两次数据,这样就使得数据传输速度加倍了,并且它有着很好的成本优势,因此DDR是目前电脑中用得最多的内存。目前在很多高端的显卡上,大都配备了高速DDR,用于提高带宽,以求大幅度提高对3D加速卡像素的渲染能力。
DDR内存
不管是SRAM还是DRAM,对于编程者来说,并不需要详细了解其内部原理,只需要使用即可。实际上内存就是存储代码和数据的,这就是内存的本质。那么内存中到底存储的是什么东西呢?数据和代码以什么样的方式存储在内存中呢?我们接下来就会讲到。
1.2.4 冯·诺伊曼结构和哈佛结构
按数据(全局变量、局部变量)和代码(函数)的存储方式的不同,可以分为冯·诺伊曼结构(又称作普林斯顿体系结构)和哈佛结构。
冯·诺伊曼结构:数据和代码放在一起。
哈佛结构:数据和代码分开存放。
冯·诺伊曼结构
哈佛结构
在冯·诺伊曼结构中,程序中的代码和数据统一存储在同一个存储器中,而且数据和代码共用一条传输总线。由于指令和数据都是二进制码,指令和操作数的地址又密切相关,因此当初选择这种结构是自然的。如ARM公司的ARM7、MIPS公司的MIPS处理器,都采用了冯·诺伊曼结构。但是这种指令和数据共享同一总线的结构,使得信息流的传输成为限制计算机性能的瓶颈,影响了数据处理速度的提高。
与冯·诺伊曼结构相反,哈佛结构是一种将指令和数据分开存储的结构。中央处理器首先到程序指令存储器中读取程序指令内容,解码后得到数据地址,再到相应的数据存储器中读取数据,然后执行操作并读取下一条指令。
在指令和数据分开存储的哈佛结构中,指令和数据的存取可以同时进行,可以使指令和数据有不同的数据宽度,并且在执行时还可以预先读取下一条指令,因此哈佛结构的微处理器通常都具很高的执行效率。目前使用哈佛结构的中央处理器和微控制器有很多,像Microchip公司的PIC系列芯片、摩托罗拉公司的MC68系列、Zilog公司的Z8系列、ATMEL公司的AVR系列甚至连51单片机也属于哈佛结构。
因为两种存储方式毕竟是不同的,所以其产生的效果也是不同的。不过现实中这两种方式均有应用。比如在三星推出的一款适用于智能手机和平板电脑等多媒体设备的应用处理器S5PV210上运行应用程序时,所有应用程序的代码和数据都存放在DRAM中,所以用的是冯·诺伊曼结构。又比如某些单片机里面既有代码存储器Flash,又有数据存储器RAM。当我们把代码烧写到内存(Flash)中,代码直接在Flash中原地运行,但是用到的数据(全局变量、局部变量)不能放在Flash中,而是放在RAM(SRAM)中,这里用的就是哈佛结构。
1.2.5 总结:程序运行为什么需要内存呢
我们从程序是什么、运行程序的目的是什么,再到内存种类以及程序在内存中的存储方式进行了探讨。总结起来,内存实际上是用来存储程序中可变数据的,而C程序中的可变数据为全局变量、局部变量等。当然在GCC中,其实常量也是存储在内存中的,而大部分单片机,常量是存储在Flash中的,也就是在代码段。另外从变量的名字来看,什么是变量?变量就是在内存中分配一块内存空间,并且将它的地址和变量名相关联。可见当我们在定义变量的时候就已经在和内存打交道了,所以内存对我们写程序来说非常重要。
程序越简单,所需要的内存也就会更少;程序越庞大、越复杂,那么所需要的内存也就越多。倘若没有内存,我们的数据将会没有地方可以存储。但反过来,即使有内存,内存也不是无限的,可见内存管理非常重要。其实很多编程的关键都是围绕内存而展开,如数据结构(数据结构研究数据如何组织并如何在内存中存放)和算法(算法研究如何加工存入的数据)。所以如何合理使用内存的同时让我们的程序更加完善,这一直是一名优秀程序员应该关注的东西。那究竟应该如何管理内存呢?接下来我们就来谈谈这个问题。
1.2.6 深入思考:如何管理内存(无OS时,有OS时)
对于计算机来说,内存容量越大,能够实现功能的可能性就更大,所以大家都希望自己电脑的内存越大越好。但是不管我们的内存有多大,一旦内存使用管理不善,程序运行时就会消耗过多的内存,这样内存迟早都被程序消耗殆尽。当无内存可用时,程序就会崩溃。因此我们说内存是一种资源,如何高效地管理内存对程序员来说是一个重要技术和话题。
在C语言中定义变量时,就会分配一块内存空间。如果想要获取更大内存空间的话,我们可以通过定义数组来实现。其实在有操作系统(OS)的前提下,我们还可以通过一些操作系统提供的接口来分配内存,这样的分配方式称为静态内存分配。在程序运行的时候,需要时随时分配,不需要时随时释放,这种分配叫动态内存分配。下面我们以有无操作系统这两种情况介绍内存的管理。
当有操作系统时,操作系统会管理所有的硬件内存。由于内存很大,所以操作系统把内存分成一块一块的页面(一块一般是4KB),然后以页面为单位来管理。页面内用更细小的字节为单位管理。操作系统内存管理的原理非常复杂,那么对我们这些使用操作系统的人来说,不需要了解这些细节,只要通过静态内存分配和动态内存分配就够了。动态内存分配时,操作系统给我们提供了接口,我们只需要用API即可管理内存。例如在C语言中使用malloc free这些接口来动态管理内存。
当没有操作系统时(裸机程序),程序需要直接操作内存,编程者需要自己计算内存的使用和安排,这属于静态内存分配。如果编程者不小心把内存用错了,产生的不良结果就由程序员自己承担。
从系统的角度介绍完,我们再从语言角度来讲:对比几种语言对内存的管理。
(1)汇编语言:根本没有任何内存管理,内存管理全靠程序员自己,汇编中操作内存时直接使用内存地址(譬如0xd0020010),非常麻烦,但如果用得好,程序执行效率是最高的。
(2)C语言:C语言中编译器帮我们管理内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以直接通过API(malloc free)来访问系统内存。裸机程序中所需的大块内存需要自己来定义数组等来解决。
(3)C++语言:C++语言对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),使用完后用delete来删除对象(其实就是释放内存)。所以C++语言对内存的管理比C语言要高级一些,也容易一些。但是C++中内存的管理还是靠程序员自己来做。如果程序员用new创建一个对象,但是用完之后忘记delete,就会造成这个对象占用的内存不能释放,这就是内存泄漏。
(4)Java/C#等语言:这些语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果我的程序申请了内存,使用完成后忘记释放,则虚拟机会帮助我释放掉这些内存。听起来似乎C#/Java等语言比C/C++有优势,但是其实虚拟机回收内存是需要付出一定代价的。所以说语言没有好坏,只有适应不适应。当程序对性能非常在乎的时候(如操作系统内核),就会用C/C++语言;当我们对开发程序的速度非常在乎的时候,就会用Java/C#等语言。