关于程序语言与操作系统的个人理解

程序编写原则:

   用最简单的语法,构建比较高效的代码。

      不要迷恋那些复杂的语法和技术,极度不建议采用带有二义性语法。最好采用各种语言通用的语法结构,采用最简单的语句,除非只有痛过构建特殊语法才能提高性能。

      if ( a == b && b > 100)

         当你不确定 逻辑运算与比较运算谁优先时,那么

      if ( (a == b) && (b > 100))

         更加清晰,出错的概率就越小。这样的书写并不会改变编译后的代码,但是增加可阅读性。

      不要采用一些复杂的处理技术,这些技术有可能对操作系统、编译器或某些特定的运行环境具有依赖性。

   采用统一的程序编写风格。

   采用统一的技术处理方式。

   采用统一的编程语言,降低团队协作风险,降低团队协作成本。

   一个函数只做一件事情,提高函数的通用性,降低代码的耦合性,提高代码的可维护性。

汇编语言:可以无限制地访问内存单元、寄存器以,仅仅受硬件的限制。

C语言:基本也可以访问任意内存单元,但是堆栈、寄存器等是编译器管理的。语言本身并不提供直接访问这些存储单元的方法。

C++:是C和面向对象语言的混合,如果抛开其C语言的特性,面向对象语言本身对于内存进行了更多自动化的管理,减少程序员出错的概率,这是通过编译器实现的,而不是C++编译出来的程序与C语言有多大的不同,而是C++编译器在内存分配及控制方面比C语言编译器复杂得多,一旦编译完成,编译器就退出了控制角色,因此C++程序一样可以任意访问内存,只是编译器会在编译时检查你的代码访问内存是否符合面向对象语言的规定。

 

内存就是数据,一切都是数据。代码也是数据,函数是一个有意义代码段的入口,只要有函数地址,只要在约定的寄存器或堆栈存放合适的数据,那么可以从任意代码调用该函数。

函数可以作为参数:因为函数就是 一个内存地址。

函数可以作为返回值:因为函数就是 一个内存地址。

数据可以被执行:只要数据包含的就是一个可执行函数代码,当然前提是操作系统规定了该数据存放的内存页是可执行的。

 

系统调用:  操作系统为了便于其他程序能使用自己,就给其他程序打开了一个窗口,应用程序就是利用操作系统的这些函数对硬件行为进行定制的,应用程序不能直接操作硬件,只能利用操作系统实现了的功能,通常应用程序对内存数据逻辑处理比较复杂。

操作系统API:是操作系统自己的外壳,是对系统调用的封装,因为直接进行系统调用是很麻烦的,很多的操作系统外壳程序也必须利用API来进行系统调用,操作系统API不仅仅是对系统调用的封装,而且做了更多的程序逻辑处理,是对内核功能的补充,API是操作系统比较稳定的接口,而系统调用是随操作系统升级而变化的,因此我们编写程序通常是直接面对操作系统API,就是调用API来完成所有与操作系统的交互。

SDK: 我们编写程序,通常要采用工具,而这些工具的价值就是提供一些函数库,让我们编写程序更佳容易,否则我们所有的都要调用原生API来做,那么工作量很大,当然我们可以直接调用原生API实现自己的功能。Borland C++/ Delphi/ VC++ 都有自己的库,如MFC/VCL。

编译器:通常编译器要将一些源代码变成通用的代码段,而这些代码段就是编译器支持库,SDK中就包含了这些代码库。还有编译器通常对于库文件的格式有要求,其实如果希望编译器比较通用,就可以让编译器能尽可能解析多种文件格式。但是为了保护壁垒,工具开发者通常不让自己的编译器能使用别人的库,Borland公司就可以让C++可以使用Delphi(Object Pascal)编译出来的库代码。

 

C语言中对地址的引用是通过指针,C++是则通过对象名,当然也可以通过变量地址,只是不推荐这样操作。所谓函数调用的地址传递,就是将地址值复制到参数存储单元(寄存器或栈,这取决于编译器,如果是库调用,那么就取决于被调用函数的参数约定),

 

宏定义:

  #define SETBIT(b,bit)  b & 1<< bit

   宏定义是在预编译时处理的,因此不会处理代码,而是简单的替换,这与函数调用不同。

   SETBIT(a+b, 3) 会得到  a+b & 1<< 3

   因此在定义有参数的宏时,必须给参数加上括号,

  #define SETBIT(b,bit)  (b) & 1<< (bit)

 

引用:

   int &a = b   // 定义了b的别名,a和b指同一个内存地址。

   int &a = * new int;

   delete &a;

 

数据类型

   整型 int  ,short int,long int ,byte ,char

   浮点数  float, double

   枚举enum,实际上存储单元就是一个字节或整数,只是这个数据是在固定的几个值中的一个,并不是存储限制,定义枚举类型,是方便编译器检查程序是否符合规定。

   联合union,其实是一个字节或多个字节的别名,一个数据区可以定义为多个别名,将多个字节拆分成不同的字节或位进行解析。当多个程序片段必须要共享某个内存,采用联合是比较合适的。

   结构体Struct, 是对一个存储块的数据的定义。

   string, 字符串,是对象范畴,只有面向对象的程序语言才有,C语言是没有的,通常是存放内存堆中,痛过malloc来申请内存。string必须以“\0”结束,否则程序无法判断字符串长度。

   char[],是c语言处理字符串的方式。通常同时有char[]数组的有效长度。

   链表、Set、Map、Directory、HashTable、BTree

 

   编译器会对类型进行检查,以保证类型的一致性,但是高级语言基本都容许进行类型的强制转换,这是为了构建复杂程序的方便性。强制类型转换是具有风险的,因此程序员自己必须保证程序的健壮性,编译器对强制类型转换后的数据无法进行检查,因为内存中到底存储了什么内存,只有程序员自己清楚。强制类型转换是一个地址操作,将某个地址开始的N个字节看作新设定的类型值。多少个字节,依赖转换后的类型,例如如果强制转换成String,那么这个String应该有“\0”结尾符号。

 

类:

   采用类方法,将一部分数据保护起来,让其他人不能随意更改该部分数据,这样就减少程序的耦合性,减少了内存管理的相互干扰。这部分数据就是类对象。

 

int &getx(){return x}//返回&x

classA.getx() = 10;  // x=10

对象的定义,构造函数

   C++

     ClassA   ca(10,10);//编译器管理对象

     ClassA   *ca = new ClassA(10,10); // 程序员自己管理, 必须delete ca。

     前者定义了一个对象,就如一个结构体,数据本身存储在作用域中,如在一个类中定义了类成员,那么该对象的大小就包含了声明的类对象的大小,*ca则是一个指针,在作用域中只一个4字节的地址值,离开作用域(如对象释放),则指针指向的内存就不会回收(不会深度回收)。

   java

     ClassA   ca = new ClassA(10,10);

   Delphi

     ClassA   ca = ClassA.create();

   ObjectC

     ClassA  ca = [[ClassA malloc]init];

 

  java是纯面向对象的语言,语法相对比较简单,但是一样可以构建复杂的系统,说明程序语言并不是决定其能力的因素,任何的程序逻辑解决,使用任何语言几乎都可以通过一定的方法能达到。

 

新的语言越来越增加了动态特性,如动态联编。动态特性就是指运行时才决定代码的行为,也可以说更加智能。而要实现这种灵活(智能),数据字典是不可少的(或者叫元数据),就是在运行时维护一个数据字典,从中可以找到对于内存中数据(或代码)的定义,让我们的程序在运行时知道数据的内容是什么,以前通常是在编译时由编译器已经决定了的,只有编译器才知道所操作的是什么类型数据,一旦编译成目标代码,那么数据的类型就已经不清楚了。很多时候,我们需要清楚在运行时所处理的数据具有什么样的特性(如实否某个类的实例,实否能进行某种处理,是否具有某个数据结构),因此加入动态特性后,程序就会更加灵活和智能。

要实现动态特性,就必须有运行时内存管理机制,这些通常就是运行时环境,实现上通常就是运行时基础库,这些基础代码被操作系统按需要加载到内存,所有程序共享这些基础代码,而这些基础代码就可以协调资源的使用。实际上,运行时库可以被看作是操作系统的一个扩充,原来操作系统的核心就是内核+提供API的动态库,现在增加的运行时库其实就是对API库的补充,同时在内存构建了更多的共享对象。

       没有共享库(运行时环境),语言的动态特性就没法实现,而实现动态特性的编译器和链接器也是依赖运行时库的,编译器编译程序将代码编译成规定的格式,同时也会加入很多特定的代码来实现与运行时环境的接口,因此C++编译的代码,不能放到支持ObjectC的运行时环境中运行,但是微软通过建立.net通用运行时环境,那么C++编译的代码,只要编译器能在编译时加入与运行时环境的接口的代码,那么C++一样可以编写托管代码,只是有些操作方式只有托管代码才有,如运行时类型检查,如果C++编译器不支持这样的语法,那么就没法用C++编写这样的代码,一切都由编译器决定。

 

     基于MFC的C++代码编译后是不需要.net平台的支持的,也就是说,MFC是直接调用了原生API实现功能,而C#则调用CLR来实现功能。MFC是基于API的系统行为定制,而C#是基于运行时环境CLR的定制,两者基于的类库是完全不同的。

 

    WDM驱动程序是直接调用windows的内核API实现驱动开发的,而后来Microsoft推出了WDF,WDF是对内核API的封装,增加了很多的静态和动态库函数内容,因此WDM驱动可以运行在新的框架结构中的,只要内核提供的API不变,那么WDM驱动就可以运行,但是随着Microsoft对WDF的推荐,可能很多windowsAPI技术并不对外公布,而直接公布WDF框架下的库函数(或类),内核实现就可以更加灵活。实际上,所有的这些技术,是通过牺牲性能来获得灵活性的,就是通过增加中间层来实现变化之间的隔离,通过“接口”实现更佳灵活的对接。如果仅仅为了隔离,那么我们可以通过修改API来实现不同的行为,而增加新的层更多的是为了实现“共享”和“省力”,在运行时,让内存中的代码被更多的应用程序共享,对于程序员,让所有的通用代码尽可能多地被复用,从而程序员仅仅需要关心需要修改的部分。

 

       如果一个计算机只完成一个功能,如单片机,如果你的程序只有一个应用程序使用,那么共享就显得没有意义,如果你能保证你的代码健壮性,那么你完全可以自己建立一个运行时环境。你可以建立起一个自己的SDK。

       一个语言,SDK才是王道,才是核心,一个语言的成熟,离不开标准库函数的成熟。如果仅仅是那简单的语法,那么语言本身并没有多大含金量。如C++语言,语言本身并不会太复杂,如果你自己构建起所有的底层库,那么需要多久呢?正因为那么多公司为C++开发了成熟的基础库(如MFC、WCL、QT),才让C++开发变的有乐趣。语言总是由一些基础库 +扩展库支持,语言必须是通用的,而通用就必须制定规范。满足基本规范的库就是一个编译器工具的基础。如TurboC,如果你没有基础库,那么即使putc这样简单函数的功能,也必须调用API来实现。离开基础库,那么就只能是简单的内存操作,即使printf这样的函数也是没有的,这一点在编写单片机程序时就会有深刻的感触,对于单片机,基本上很多标准C的函数都是要自己实现的。因为单片机的内存操作是那么的奇特(原始)。

 

硬件:

  CPU,内存,外设,计算机的三个必要硬件要素。

  CPU负责指令的执行,包含取指、缓冲、执行、写回。现在的CPU通常包含多个内核,是为了提高性能。但是需要操作系统的支持,因为操作系统负责进程和线程的调度,由于操作系统为了适应所有应用情况,因此并不针对特定情况进行优化,因而并行处理并不理想。当然如果有多个进程同时执行时,通常进程之间没有多大关系,实现多处理器的并行处理相对简单,只是对硬件的操作时需要进行队列。但是对于一个进程的多个线程,或者一个线程的多个执行部分,如果要并行执行,就必须检测代码之间的依赖关系。对于多核和多处理器,如果不是经常执行多个任务,那么多处理的利用率是非常低的,一方面是处理器被大量浪费,另一方面是程序等待执行(因为我们希望程序运行越快越好)。

   处理器的并行是非常复杂的,通常必须借助操作系统,如果自己写程序直接实现并行操作,那是需要相当的能力,除非是对于速度要求非常高的场合,需要程序员自己编写调度程序,否则程序员一般将处理器的调度交由操作系统处理,我们的程序处理相对比较简单,但是处理速度就并不是可预期的。

   内存是CPU唯一需要处理的数据区,CPU仅仅与内存打交道,对于外设寄存器,也可以看成是内存的一部分,因为那同内存条一样是连接到内存总线上的。程序就是一系列的CPU指令代码,而每个指令就是要么是处理CPU内部的数据,要么就是将数据从外部(包括内存、外设寄存器)读入CPU寄存器,要么就是将寄存器的值传输到内存或外设寄存器(这就是访存指令MOV),当然还有一些条件转移指令。

   外设:是一个独立的设备,都有自己的处理器,而外设与CPU之间的接口就是外设寄存器,CPU指令能读写外设寄存器,而外设的处理器也可以读写该寄存器。这里要弄明白外设与实际的硬件之间的关系,例如键盘、鼠标,针对计算机的CPU来说,指令访问的是键盘、鼠标的控制器寄存器,而这个控制器目前通常放在主板南桥芯片中,这与键盘、鼠标是两个概念,键盘、鼠标是一个产生信号的设备,键盘自己也有处理器,产生信号后传输给键盘控制器,控制器负责与CPU的通信。因此这里所说的CPU外设,是指鼠标、键盘控制器,而不是指键盘、鼠标实物。因此通过键盘、鼠标接口可以接入一些其他特殊设备,如游戏操作杆等,只要产生的信号能被键盘控制器所识别,并触发CPU中断来处理键盘控制器的寄存器内容,如果中断程序能识别寄存器的内容,那么就会交由相应的处理程序来处理寄存器信息,这就是驱动程序对外设信号的处理。驱动程序就是为控制器服务的,而控制器就是我们通常说的总线,而控制器(寄存器内容处理程序)处理程序就是总线驱动程序,总线驱动程序是由操作系统支持的,对于特殊总线,则是由主板提供商开发的,当操作系统安装时,默认是不提供该驱动的,而要用户自己安装主板的驱动程序,这样才能使用特定的总线。对于MAC OSX系统,硬件也由Apple提供,因此通常我们不需要安装主板驱动,对于品牌电脑,通常硬件厂家就已经安装好了这些驱动。当我们自己安装操作系统时,我们通常发现操作系统开始时对于显卡只安装标准的VGA驱动,因此分辨率很低,当安装完成后我们安装需要的主板驱动后,分辨率才会变正常,当然通常windows操作系统会携带大量的驱动程序,安装时,会根据硬件自动安装合适的驱动程序。

   

操作系统:

    操作系统是针对硬件平台的,我们说Linux可以适应很多硬件平台,并不是一个操作系统可以运行在不同的硬件平台上,而是Linux的模块化做得比较好,可以根据硬件平台选择不同的模块组合,而通过裁剪后的操作系统需要经过编译后才能成为一个合适特定平台的操作系统。例如X86平台的Linux操作系统就是通过针对X86编译的。

     操作系统也不是万能的,都是针对特定用途的,如果针对PC机的操作系统,就不可能安装在手机上,因为它们的基础硬件设备都不一样,因此驱动程序基础也不一样。

    但是操作系统有一些共性,而这些控制逻辑被抽象后形成一些通用化的代码,如进程调度、内存管理、文件系统管理,这些就组成了操作系统核心。这些代码不会是汇编代码编写的,因为汇编指令是针对特定处理器的,如X86指令与ARM指令是有很大差别的。但是当用C语言编写时,因为要经过编译器转化,因此C程序可以基本一样,通过不同的编译器就可以产生不同的CPU指令代码。因此操作系统核心通常就由C语言或C++语言编写。

    

    对于问题的处理,操作系统有各种不同的方式,Linxu是宏内核,内核与用户的代码结合紧密,运行速度快,MAC操作系统经历过微内核与混合内核的过程,目前的主流操作系统通常结合了微内核与宏内核的优点,属于混合内核。所谓微内核,就是将操作系统的一些服务程序以进程方式运行在用户空间,与普通程序具有同样的特性,而普通程序要访问操作系统服务(如访问网络,访问文件),就要发送一个信息给内核,内核负责转发给相应的服务程序,服务程序处理好后,返回数据给用户程序,可见这样的开销是很大的,特别是对于一些复杂的操作,进程间的消息发送就变得频繁和复杂。对于宏内核,处理方式则不同,内核程序就类似一个函数,用户进程要调用操作系统功能时,就是直接调用操作系统内核函数。不管这个调用过程多么复杂(通过系统调用进入内核空间,复制或引用用户空间内存),但都是在同一个进程内的程序转移。当然如果不考虑进程间切换的时间,那么宏内核与微内核其实没有多大差别,因为不论怎样,都是涉及到内存数据的复制和处理,并将处理结果的地址返回给调用者。当然两者各自有自己的优势:

     宏内核执行速度相对较快,因为少了些信息传递的成本。

     微内核相对稳定,因为核心服务以进程方式运行,因此即使其中一个模块(服务)崩溃,也仅仅是一个功能失去能力,并不会导致整个系统崩溃。当然宏内核也可以做到隔离,但是相对来说没有那么清晰。

    采用混合内核,对于一些复杂的调用,采用调用服务,而对于一些独立性比较强的服务则采用进行服务。

   采用微内核模式,实现分布式操作系统相对简单,因为发送信息时只要将信息发送给远程操作系统就可以,而直接的内存调用模式,传递的是本地地址,因此实现远程操作,需要借助远程服务程序,那样与微内核是一样的模式。

   

 

汇编语言:

     程序员可控性非常强,几乎就是直接的硬件操作,因此几乎可以实现最简洁的、最快速的程序,因此对于一个计算机语言,通常一些核心库就采用汇编实现。但是汇编的可读性比较差,如果不看说明,基本不明白程序要做什么,因此可维护性相当差。

对于现代的计算机来说,似乎可维护性、代码可重复利用性比速度本身更重要。因此微内核曾经红极一时,因为微内核让编写内核更加简单,可扩展性更好,可维护性更好,更稳定。

而操作系统采用C++编写,而不采用C语言编写,也是为了更好的维护性和扩充。

 

C语言:

     受限于编译器的设计,编译器对于语言元素有很多限制,对于内存的管理完全由编译器本身管理,因此程序员对于内存的管理变的轻松,但是对于内存的操作就不会像汇编那样灵活。例如函数调用时的参数传递,寄存器的利用分配,栈的管理,都由编译器决定,而编译器是根据硬件(和操作系统,如果有的话)来确定内存资源的利用的。

    C语言编译并不实现对内存堆的管理,因为那是程序的事情,计算机通常提供的资源包含“寄存器、栈、内存”,而编译器也就是对内存进行划分,利用寄存器和栈传递参数。

    栈:CPU提供一个寄存器和指令,PUSH、POP指令实现对栈指针SP寄存器的增1减1操作,而SP寄存器中存放的是内存中的一个地址,这个地址在最开始时由程序指令输入,C语言程序编译时,编译器可根据内存的大小,分派一个栈地址,这个地址完全由编译器的策略决定,并不是一定指向固定的内存位置,例如可以初始化SP=0xFFFF,也可以指定SP=0x1000。编译器策略是这样的,通常最低地址部分就存放代码(对于不带操作系统的单片机程序通常就是这样),然后是程序中的全局变量(及常量)区域,然后是未初始化的变量,可能还有资源段,SP就指向可用内存最大地址位置,当程序运行时,当要分配新的内存时,就从未用内存的最低地址分配空间,而如果是局部变量,那么就从SP指向的地址分配一个内存单元,然后SP=SP+1,而子程序在编译时,变量就变成了相对于SP的偏移量,但是由于SP经常变动,因此,在进入任意子程序时,就将SP寄存器的内容复制到bp寄存器(基址寄存器)保存,然后编译器就将局部变量变成了[BP+0x]地址,当退出子程序时,恢复BP到SP,这样其实局部变量的内容还是可以通过SP偏移量访问到。只是编译器并不会容许在C语言作用域外访问局部变量,因为那是不安全的,因为当进入第二个子程序后,该存储单元就会被修改,访问该地址单元也就没有意义,如果从子程序退出,通过汇编指令可以直接获得局部变量的值。

     栈是编译器维护的,例如在delphi中,如果定义编译器选项的栈大小小于0x100000,那么编译器会将栈定位到0x400000以下的地址空间,而此空间是没有被使用的,因为程序默认被加载到内存地址0x400000的位置,当然这个加载地址也是可以定义的,编译器和链接器也会根据设定的地址确定程序指令中的地址相对关系。

    当栈大小设定过大时,那么栈就会设定到0x10000000以下的地址空间,因为这个地址之上是windows为DLL库加载时保留的,这样我们的程序通常就只能小雨256MB,当然对于单个程序来说,这已经足够大了,如果程序太大,我们通常会将程序的一些模块变成DLL,而这些DLL就会进入0x10000000以上的空间,当然DLL的加载是一个复杂的过程,地址是动态的。

   当然EXE文件的加载也是复杂的,地址也可以不按照编译链接时的基地址加载,而要加载到连接器预期地址之外的地方,就必须重新定位EXE或DLL文件中代码的所有绝对地址引用(所谓绝地地址就是指令操作的就是一个地址值,相对地址则是相对寄存器中代码的相对量,通常就是相对BP或IP的偏移量,在单片机中编译的位置无关代码BIN文件通常就是相对于IP的偏移量,数据与代码是混合保存的),而要修改的部分就在文件中的重定位代码段,它有固定的格式,加载器根据其中的说明修改所有用到的绝对地址(就是将原地址加上新基地址与链接器设定基地址之间的差,如链接器假定程序被加载到0x400000,而实际上加载到了0x500000,那么所有绝对地址引用都要加上0x100000)。

    函数调用时,每个编译器传递参数方式是不同的,如GCC编译器与Delphi的编译器对参数的传递处理是不一样的,Delphi默认采用寄存器传递参数,只有寄存器个数不够用时,才用栈传递参数,而且传递参数的顺序也有规定。因此多种编译器的程序之间要互相调用代码时,就要对参数传递方式进行规范,保证传递参数的正确性,例如Delphi调用windows API函数,就要在函数声明时写上stdcall,这就让Delphi编译器采用与windows API兼容的参数调用模式(栈传递参数)。

    高级语言在传递参数时,总是有值传递和地址传递问题,值传递就是将变量的值复制到寄存器或栈中,而地址传递就是将变量的地址复制到寄存器或栈中。这样子程序当然也要清楚传递的是值还是地址。如果将值作为地址来操作,那么就可能产生异常。当然程序语言编译器已经对这些做出了规定,就是编译时会检查函数调用时参数是地址还是值,这必须与函数声明是一样的。

   实参与形参:实参就是函数调用时传入的变量名称(及类型),而形参就是函数声明时的变量名(类型)。形参与实参只是相对于函数调用那一刻的名称说法,函数调用时,实参是已经分配了实际地址的内存变量,而形参则是还没有在寄存器中或栈中分配内存的变量符号。

 

   堆:是一个内存管理的产物,其实上,对于C语言本身没有堆的概念,而是C语言库实现内存管理的一个概念,对于在内存中动态分配内存时,就必须要清楚哪些内存可以使用,内存释放时,应该释放给谁,这就是一个内存结构记录着内存的使用情况,这就是堆管理,这些功能由一些函数实现,如malloc、free。在操作系统内核中,还涉及到kmalloc、vmalloc这些函数,这是对内核内存堆、虚拟地址内存堆的管理。堆管理属于一个开发工具SDK的函数库部分。在比较简单的单片机开发中,通常不包含堆管理的库,因此不能使用malloc等函数。

 

C语言通过指针操作可以遍历内存单元,在没有保护模式的计算机系统中,通过指针可以遍历内存所有单元,而在有保护模式的系统中,处于用户模式的C代码可以访问除内核外的所有内存空间。但C语言不能直接操作寄存器,要操作寄存器就必须借助汇编语言,这可以通过C嵌入式汇编实现。

 

内核模式:是计算机系统为了操作系统而设计的硬件模式,让CPU处于特权模式时(可能有多种模式),CPU指令可以执行所有指令,而处于非特权模式时,就只能执行某些指令。这样一些修改特殊寄存器的指令就只能特权模式才能使用,通过这样的模式,操作系统将自己的核心代码放在特定内存区域,通过页表描述符来确定只有特权模式才能访问这些内存。内核模式就是将CPU修改为特权模式后的状态,此时程序可以遍历和修改任意内存。因此处于内核模式的驱动程序和内核模块是没有访问限制的,因此驱动程序可以修改任意内存,这也是为什么驱动程序开发必须非常谨慎的原因。驱动程序的一个错误可以让整个操作系统崩溃。用户态程序要访问内核代码,就必须借助中断服务程序,而进入中断的指令(软中断)是特定的,服务程序也是特定的,因此进入内核的途径就被操作系统中断服务程序所控制。

   

java/C++/Object Pascal/ObjectC /C#

    面向对象语言虽然都有一些共性,但是由于其复杂性,通常一门语言都有特定的编译器支持,C++兼容C语言的一些特性,又增加了一些面向对象的特性,但是对RTTI的支持比较简单,运行期仅记录了对象名及类型等信息,而Delphi(object Pascal)则增加了对类方法和属性的记录(方法函数名与地址对应表,属性类型表)。在Delphi与C++之间有一个共同的东西,就是虚函数表,delphi提供了与C++兼容的虚函数表,可以实现两个语言对象之间的运行时互相访问。java则是完全面向对象语言(C++和Delphi均可以定义全局变量和全局函数),对于运行期的反射具有更多信息,java可以动态修改类的成员。ObjectC具有类似java的方式,属于动态语言,ObjectC具有运行期类型检查,但是这也降低了运行速度。java和ObjectC通常不建议直接引用对象属性,而是通过方法函数访问属性,而这些高级语言都有安全检查控制机制,就是通过控制对象方法函数调用时检查的。每次方法函数调用不是直接地址引用(C++和Delphi通常是直接的地址引用,delphi的动态函数也是遍历方法名),而是通过查询类的方法函数名找到函数地址。因此在ObjectC中,调用对象方法叫给对象发送信息。这的确是可以这样理解,当调用方法函数时,第一个参数就是对象或类对象的地址,第二个参数就是方法名(是字符串参数)。系统通过查询类的方法名表来确定是否支持这个函数调用,因此对于动态语言只有当调用的时候才能决定是否调用的方法正确。当然对于编译期能确定的类对象,编译器是可以检查方法调用是否符合要求的,但是对于一些没有确定类型的对象,如objectC中的id类型对象,java无类型变量,就必须在运行期通过查询方法表才能决定是否对象是否支持该方法。在objectC中,甚至不需要知道对象的类类型,就可以通过selector来确定是否支持某个方法。

java对于类是动态加载的,并不是一开始就加载入内存,而是当访问某个类的时候,系统才会查询类表来确定类的地址。因此java相对于其他语言来说,对类和对象进行了全程管理,而C++等语言在编译后的代码中类的概念就淡化了(如对于类方法的调用就已经变成了函数地址的调用,与C语言的函数调用几乎没有区别)。

    C#和ObjectC属于动态语言,javascript也属于动态语言。C#需要运行期代码的支持,也就是在操作系统API之上还增加了.net的framework,C#是托管代码,而VS MFC、Delphi则是直接调用API实现的,而API是windows自带的,因此程序可以直接运行。而C#在没有装.net Framework的电脑上是不能运行的。

 

     类方法与实例方法

     类方法是直接向类发送调用的函数,类方法调用时,编译器按照普通函数一样传递参数。但是对于实例方法,编译器编译时,会自动增加一个函数参数,这个参数就是delphi和objectC的self(C++和java为this),就是对象指针,这个参数是对象在栈中的地址,也就是对象局部变量,而对象变量保存的就是对象在堆中的地址,在x86系统中,delphi和ObjectC都采用EAX(64位为RAX)寄存器传递self变量。这可以在delphi中采用汇编进行测试,通过MOV EAX,self,然后call 对象方法,就如直接的objectpascal调用函数一样。

     类对象(self)与实例对象(self)

     类对象:是动态语言为了保存运行期一些类信息而由编译器自动创建的内存对象,这个对象在编译期就产生,通常放在代码区,当然也可以放在数据段。当程序被加载时,类对象随代码加载而产生。在delphi中,class function 的self地址是在代码段中,没有生成栈变量。在ObjectC中,类加载比Delphi更复杂,会在栈和堆中生成描述类的对象,这个描述比一般的语言要复杂,也是为什么ObjectC更加智能的原因。

     实例对象:实例self是运行期动态产生的,产生的基础当然是根据类信息(类的构造函数),产生的对象保存在堆中,而对于对象的引用变量则在栈中,在ObjectC中,必须将对象变量声明为指针类型。实例对象(堆)地址的第一个指针(64位为8字节)为特殊指针,每种语言保存的信息不同,通常保存的为找到类信息(类对象)的地址,面向对象语言的多态就是通过动态查询self的第一个指针找到类对象,然后根据类对象信息找到方法地址,在C++中多态是通过虚拟函数表实现的,类对象的第一个地址指针就是虚拟函数表,在Delphi中,还有dynamic。在ObjectC中和java中,则全部采用了动态查询方式来调用对象方法函数,方法调用时,最少有两个参数,第一个是对象指针,第二个参数是函数名。在ObjectC中通常称为向某对象发送消息(方法调用)。

     在ObjectC中,有代码块的概念,而代码块其实就是一个闭包,闭包就是说一个代码引用了外部数据时,将创建代码块时的外部变量的状态(数据值)拷贝一份与代码块变量保存在一起,代码块就像一个带有函数指针的结构体,也有些类似对象。当我们声明一个代码块时,编译时,会创建一些额外代码 ,代码完成以下工作:首先在栈中申请一个空间,包含指向代码块的地址 以及保存 要用到的外部变量内存空间,然后将当前变量值copy到刚申请到的栈地址空间中。当我们调用代码块时,参数按照函数调用方式传递参数,第一个参数是代码块变量地址(栈地址),这非常类似于对象的函数调用方式,对象保存在堆中,而代码块变量(对象)保存在栈中,对象第一个指针指向类对象,而代码块变量第一个指针指向代码地址,后面则是引用的外部变量值。

     C++的结构体变量,特别是有函数指针的结构体,与对象非常类似。只是在内存中的位置不同。而C语言通过含函数指针的结构体可以 实现面向对象编程。

     ObjectC中引入代码块的目的当然不是为了解决面向对象编程,而是为了并行运行,因为代码块可以独立运行(已经保存了变量状态),非常适合内核的并行调度。在内核也有负责代码块(任务)调度的服务程序,任务分别放入不同的队列,内核则根据不同的队列任务创建线程进行处理,这不同于linux和windows的线程调度,一个线程可以处理多个任务,只要队列任务足够多,那么多处理器(核)的并行处理事务就更加高效。apple鼓励程序员多使用这种并行编程模式,只有这个生态圈的程序多了,MacOSX才能充分实现并行。

 

     程序员对于自己编写的每行代码,要清楚编译后会产生什么行为,程序对象在内存中是怎样的状态。对于汇编来说,这是显而易见的,但是对于高级语言来说,因为编译器的介入,程序源代码简单了,但是运行期的代码就变得更复杂了。例如C语言的main函数运行前,必须有一段引导程序,而这段引导程序是编译器自动加上的,因为这段程序完成的功能就是固定的,如初始化C语言运行环境(如寄存器初始化)。

 

     操作系统下的程序,自己并不会运行,而是通过操作系统进行引导后才能运行的,当我们启动程序时,是通过发送信息给操作系统。操作系统收到信息后(如双击图标、输入应用程序名),首先在文件系统找到应用程序文件,如果找到则开始创建进程,构建并初始化进程块(一个进程在内存中就是一个描述块),将可执行文件加载入内存,这是文件镜像,并不能执行,而要执行就需要将可执行文件的各部分copy到虚拟内存的合适位置,操作系统为新进程分配虚拟内存空间,分析可执行文件的各部分(代码段、数据段、输入输出符号表),将文件映射到虚拟内存空间(通过构建页表),初始化(重定位)输入符号表,找到程序入口,然后启动进程,程序转移到程序入口地址执行。一旦执行完毕,程序向操作系统发出结束命令,或者其他程序发出kill进程命令,那么进程被操作系统接管,操作系统会将进程虚拟地址页表记录的所有内存回收,关闭进程打开的所有文件对象,清除所有对象句柄,然后关闭和清除程序文件,最后释放进程描述块。

 

      MVC模型:通常就只有V和MC两个文件,我们通常在控制器中编写了模型代码。其实M是通过类实现的,是一个存储数据的对象,而控制器则是将数据展现到图形界面,并从图形界面接受信息,所以C是一个桥梁。而Delphi的MVC与XCode的MVC模型极其相似,M实现一个数据逻辑处理,通过不同的类来实现,而C则是创建对象并调用方法来完成M的创建、修改。而视图则是通过一个图形化的界面来实现的,实际保存的文件则是一个用于描述图形界面各元素的文本文件,文件内容包含控件的外形、位置、关联的事件函数。设计时(如Delphi/Xcode在拖放设计界面时)设计器也是通过界面文件(delphi为dfm,xcode为nib)的内容来绘制设计期界面的。我们在属性编辑器修改的属性(包括事件函数)都保存到了界面文件中。Delphi在编译时就将dfm文件内容变成了二进制代码,进入了可执行文件中。而在xcode编程时,nib文件被独立了出来,在运行期根据创建界面的需要才被读入内存,awakefromnib就是从nib文件加载时执行的事件。

     在MVC编程中,大量的程序代码已经被工具创建,而我们需要编写的就是那些特定的业务需求,这大大地简化了程序员的工作。界面创建变成了一个绘图过程,而界面元素与代码的关联是被消息处理函数接管,我们只要设置控件的事件属性,就将界面与代码关联,这个过程就是将控件的消息处理函数与函数地址绑定。例如frame的创建过程,创建窗口的代码是SDK实现的,如果我们有特定的需要,那么SDK的函数在frame对象创建之后会判断Frame对象的创建事件是否被用户赋予了函数地址,如果有那么就执行该函数。而一些通用的过程,通常所有代码一样,SDK就不会让程序员介入,也就不提供特例化的事件函数接口,SDK在类的实现中,参数化越多,那么类的开放性越强,可设置的属性就越多,开发越灵活也越复杂。

     现在的VS程序编写,也开始让界面设计和程序设计分开,这同nib文件设计有些类似,对于delphi来说这是做不到的。

     语言之高级就是让程序以更小的代码量完成更多的功能,而要实现这个目标当然就是提供函数库实现一些常用的功能,汇编提供了沙子,而C语言则提供了砖头,面向对象语言则提供了一些模块(如幕墙、窗子),对于程序员来说,要重新去造砖是一个漫长的过程。对于单片机编程来说,有时候用C语言完成一个功能比汇编快不了多少,因为特定单片机的C语言库可能很贫乏,不是因为实现不了库,而是因为要操作硬件,采用库往往不能解决特定问题。

 

上一篇: 一个老程序员和他的1000万小挑战
下一篇: 51、AVR、PIC、ARM单片机的总线与存储空间的理解
wjgit
博客等级 码龄18年 12粉丝 30原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值