关于计算机编程的一点理解(1)

 看了很多计算机的书,从来没有自己写过东西,其实,写写东西还是不错的。下面谈谈对计算机语言的认识,以c\c++为主,略谈其它。

1.计算机是怎么执行程序的

  最简单的计算机应该由一个CPU和RAM组成,但是考虑到RAM在掉电的情况下无法保存数据,还需要一个在掉电情况下能够保存数据的存储器(为了方便就叫永久存储器,用英文就叫Permanent Storage,以下就叫PS,顺便说一下这是我临时取的名字),比如硬盘,ROM,光盘等。现在的计算机都是基于存储程序的原理,意思就是把程序放在PS里面,然后在计算机开机后,自动从PS中的一个地址开始读取程序,一般来说,这段程序是一段启动代码,负责把剩下的主要程序读入RAM,然后跳到RAM开始执行(因为RAM读写比较快),这样计算机就跑起来了。所以程序的一切都在RAM里面。

  程序执行过程中会进行些什么操作呢?大概包括以下这些:1.读入RAM中某几个地址的数据到寄存器2.对寄存器中的数据执行逻辑和算术操作3.把数据放到RAM中的某个位置。仅此而已。计算机有一个专门的计数器IP,用来记录下一条指令的地址。每执行一条指令,IP就增加一点指向下一条指令,然后,CPU从IP中记录的地址读取下一条指令执行。可见,CPU只是不断往下执行而已,不过有一种特殊指令可以修改IP的值,使得程序能够改变执行路径,在X86中就有jmp,call,int等。

2.汇编语言

  计算机CPU只能识别二进制指令,但是用二进制编写程序太难懂,易出错,编写效率低下。所以有了汇编语言,汇编语言基本就是把二进制指令用不同的单词代替,让汇编器去把它们转换成二进制指令。概念上基本和二进制一样。很多C编译器就是先把C语言先编译成汇编语言程序,再调用汇编器转换成二进制机器指令的。如果要看到编译成的汇编代码可以加上相应的编译器参数。

3.C语言

  由于汇编语言和二进制机器语言是一一对应的,所以给不同的cpu写程序,要使用不同的指令集。完全没有可移植性。而且,由于要不断的根据不同机器学习不同语言,显然让人头疼。高级语言应运而生,最开始的高级语言就是FORTRAN,编译器是用汇编语言写成的。然后出现了C语言,并用它完成了UNIX操作系统。

  C语言提供了什么呢?它提供了一套标准的操作符号。但实现的功能无非还是跳转,读写内存和逻辑算术运算。其实还有一点,就是提供了统一的内存空间申请和释放方法,以及给地址空间中的数据赋予了类型的概念。并把子程序抽象成了函数的概念,提供了标准的参数传递规则。

4.C语言中的类型

  C语言中的变量一般都有个类型,类型分为基本类型,比如int,char,float,long,wchar_t等等,这些都是基本类型。

还有复合类型。

       比如:

    typedef struct _NODE{

        int size;

        node_t  * next;

    }node_t;

       node_t就是一个复合类型。它由基本类型组成。

  为什么要类型呢?下面看看类型对于编译器意味着什么。

比如一个

    int a;

      就会分配一个空间(一般是4bytes),它的地址由编译器和运行时的状况决定。那么这个4bytes就是由a的类型int决定的。

再比如:

    int a=1;float b;b=a*3;              

  这里先分配一个sizeof(int)的空间给a,再一个sizeof(float)大小的空间给b,这个已经讲了。a*3呢,这是两个整数相乘,现在编译器就知道编译成X86的整数乘法指令,如果a是float型的,就需要使用浮点乘法指令(需要专门硬件:浮点协处理器),编译完后的指令是完全不同的。a*3的值保存在寄存器中,然后编译器发现b是个浮点型的,由于float和int是二进制不兼容的,所以必须进行整数转换成浮点型的转换操作。这些操作编译器都自动完成了,我们看不到而已。

  从这里可以知道,当类型和操作符联系在一起就会决定产生什么指令。所以类型决定了如下东西:

(1)具有该类型的变量的大小

(2)具有该类型的变量的操作

还看一点:

    node_t  *p=(node_t*) malloc(sizeof(node_t));

    p->next=NULL;

         这里malloc从堆(heap)里面分配了大小为sizeof(node_t)大小的内存,并返回内存首地址,把这个地址放到p中(X86 32bit中指针是32bit,所以指针很特殊,无论是指向什么类型,指针的大小是不变的,这也是为什么会有void *类型存在的原因)。p->next=NULL做了什么事情?其实在C中如下代码和这句是等价的(假设struct中的数据时连续存储的,不考虑地址对齐问题):

    (node_t *)((char *)p+sizeof(int))=NULL;

  就是说用把p的值加4让p指向next的首地址,然后根据next的类型给其赋值为NULL(也就是0)。所以复合结构中类型也规定了复合结构的变量的空间的分布,这种分布也是根据基本类型的大小得到的。

  上面的代码中,p之所以要转换成char*是因为指针类型的加法操作,比如p+N(p为A类型)实际上是把p的值增加sizeof(A)*N.,如果不转换,这里就是把p增加了sizeof(int)*sizeof(node_t),因为我们只想增加sizeof(int)所以要保证sizeof(A)==1,为此先把p变成char *类型,因为sizeof(char)==1.

4.C内存分配

  其实任何计算机语言都要为变量申请内存,那么变量是怎么获得内存的呢?一份程序编译成二进制了之后,载入到内存是这样的:

  一部分内存用来放代码,称为代码段。

  一部分内存用来放数据,称为数据段。数据段比较复杂。

  比如我们:

    int a;

    int b=1;

    int main(){return 0;}

  a和b就会被放到数据段,也就是说变量a,b的地址在数据段中。数据段又分为两类,初始化数据和未初始化数据段。a没有赋值是没有初始化了,b初始化为1.这两类数据分别放到不同数据段。b的地址中在文件中就已经有值1,只要载入文件到内存就获得了数据,但是a呢,一般来说,在main运行以前,编译器插入的CRT 启动代码会吧未初始化数据段全部清零的。这两种数据段都是放静态数据的。

  除此之外还有:

  栈(stack)。栈也是一个数据段,即一段内存。x86中,cpu有一个sp寄存器(stack pointer),指向栈的末地址。

看下面代码:

    void fun(int a ){

        int b;

        return;

    }

         这里a,b都是变量,它们其实只有当调用函数fun的时候才分配空间,这个空间就在栈中。基本方法就是把sp减去几个值(sizeof(a)+sizeof(b)),然后认为a的地址就是sp+sizeof(b),b的地址就是sp。在函数结束后会插入代码把sp加上8回归原址。(这里只是笼统说一下,要说清楚,实在太费口舌)。

         当然还有malloc函数是在(heap)中分配的。heap其实是一块内存,使用链表连接起来。由一堆函数来管理,malloc,free就是。当然malloc是C运行时的标准函数,其实除了malloc也可以使用操作系统提供的其它函数,来获取更大的灵活性和性能,不过操作系统函数不能操作标准堆,而是另外的空间。

5函数

        在使用汇编语言编程序的时候,没有函数这个东西。但是为了把程序简化,会手工编写子程序。同时会遇到一个问题,即参数传递问题。

        一个子程序有个入口地址,我们可以直接跳到这个地址,开始子程序的执行。但是我们希望子程序根据不同的参数返回不同的答案。那么参数怎么传给子程序呢?一般有这么几种方法:

  1.把参数放到寄存器中,然后由子程序从寄存器中读取参数。当然使用哪个寄存器必须约定好。

  2.把参数放到栈中。按约定从栈中读取。这个约定就是函数的类型。前面已经稍微提到过一点栈。

  3.放到一个静态数据中。其实和放到寄存器意思差不多,只不过慢一些。

       X86中标准C语言使用方法2。所以C函数支持递归操作,因为每次进入都是重新从栈中分配内存,不会覆盖原有数据,是可重入的。不过在一些嵌入式的小型CPU编译器也有使用寄存器的,那么函数将是不可重入的。                                    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值