?/P>
下面说说动态内存分配的问题!
从前面讨论可以知道,在c语言中由#define定义的常量是在预编译做了宏替换,而在程序中的这些字面量有的被编译到代码中(ELF文件的.text段),有的在只读数据段(比如字符串,另外双精度double占用8个字节,运算时需要FPU 的ST寄存器或者叫浅栈,不容易编译到代码中),剩下的有初值的外部变量(包括static限定的变量)在数据段(.data),为赋初值的外部变量在.bss段中(ELF文件没有给分配空间,加载到内存映像时分配),还有一种是局部变量(自动变量)在用户栈中(参看前面的内存映像图).前面我们讲过变量生存期的问题,局部变量决定于调用栈,生存周期在函数调用过程中,外部变量(包括static限定的变量)在整个进程运行期中,还有一种情况就是通过malloc函数调用动态生存期的变量,他们的生存期是程序员自己控制的(其实,还要取决于malloc等函数的实现,不过先这么认为吧)!理解动态内存分配先要有下面的前提概念:
(二)程序级异常
对于一种现代编程语言来说,对异常控制流的支持是非常重要的!尤其是开发过类库的同学一定深有体会! 举个例子,比如你开发一个读取某种自定义XML格式的类库(比如叫做LFML吧,用来描述一种特定业务),当用户程序使用你的类库时有一种情况是肯定的,就是不管你的LFML描述的格式多么复杂(肯能需要许多函数去解析,还有函数的嵌套调用),但如果格式被破坏,只要有一条不符合你的LFML规则,你的解析就不需要进行下去了!这样的程序如何实现?如果有异常控制流,只需要在任意函数(不管嵌套多少层)中抛出一个异常,在客户程序中捕获这个异常就可以了!
当然程序级的ECF(异常控制流)还有许多有用的地方!
在操作系统中,举个例子,你的程序正在执行,这是用户按下一个ESC键,操作系统如何告知你ESC键按下(或者正在运行的程序如何知道ESC键按下),在dos时代有个常见的词-中断.
其实中断也是一种ECF!还有你的程序在调用内核的功能时(因为你的程序运行在用户空间),内核运行在内核空间,如何完成这种调用呢?还有,操作系统是如何完成多个进程(线程)”同时”执行的呢?这些都是OS级的ECF!需要这方面的资料可以baidu,google!
(3)
关于可执行文件格式,有许多好于的工具可以使用:window平台上查看PE格式的工具很多(比如PEJump等,百度google去吧),Linux上有个很好的工具是objdump,自己看帮助文件objdump还可以反汇编哦.
就使用上面用到的main1.c strtest.h strlen.c externvalue.c ,我们再家一个文件strcpy.c
#include 'strtest.h'
void strcpy_lf(char *dest,const char *src)
{
}
(我使用了stcpy_lf 这样的名字是区别于标准c库的,因为在gcc编译是默认链接标准库的,
会有名字冲突,你可以用-static 参数编译main2.c看看链接后的可执行文件大小)
main2.c
#include <stdio.h>
#include 'strtest.h'
#define HELLO 'Hello,world'
int main(int argc, char *argv[])
{
(2)
在windows平台上可执行程序就是exe文件,dos时代的小于64K的com文件现在已经不见了!windows上可执行文件格式我们一般成为PE(Portable Executable)文件(.net 编译成的exe也是PE文件,但有本质的差别),PE文件源于Unix系统的COFF文件.现代Unix系统,比如Linux使用一种叫ELF(Executable and Linkable Format)的文件,意思是可执行和可链接的文件,名称概况的很好! 可执行文件,可重定位的目标文件(obj)以及动态链接库文件(.so)文件都是ELF文件. Windows上的动态库是DLL文件!
其实PE格式和ELF格式都有许多相同的地方,包括可执行文件加载到内存中的内存映像(就是进程,进程、文件和虚拟储存器是操作系统的几个重要的抽象概念,当然windows是以线程调度的,这在后面的内容讨论)也是大同小异!所以就简单的说说linux中的ELF。详细深入的研究可以参考各种关于ELF文件的文
(一)可执行文件可链接文件.
(涉及到编译原理,操作系统,动态链接等知识)
(1)
一个c程序的文本文件(ASCII码文件)如何变成一个可执行的程序? C程序大概需要这么几步:
预处理是将c文件中的根据#开始的命令修改生成新的源程序,比如宏替换,#include将相应的文件内容包含进来.
编译就是把预处理后的c源文件转换成汇编语言源文件(仍然是文本文件).对于特定的宿主(操作系统和CPU体系),汇编语言是一样的,c和pascal编译后得到一样的汇编语言.所以这样类型的语言都可以叫做编译语言
汇编是把汇编语言文件翻译为机器指令(二进制文件).这种格式一般叫可重定位目标程序
就是大家熟悉的obj文件,(下面的文件格式再说)
链接链接的结果是一个可执行文件(一般指由操作系统直接加载的文件).程序中用到的外部变量和函数的定义是在链接过程中实际定位的!编译汇编时只需要有声明就可以了!(相关的知识需要了解链接时的符号解析等等,有兴趣的可以看看).
例子说明!
C语言的特性相对比较少,其他类c语言也是一样.OO的语言中的class可以看做一个特殊的struct,对象的方法其实就是函数(只是要求有一个默认的指向其对象的地址作为参数,比如this).用c现有的元素也可以模拟许多OO的特性,比如interface,多态等等.我见过一些纯c程序,也使用了后绑定,interface等特性降低程序的耦合以及其他优良的特性.
知道c语言的语言特性,就可以编程了吗?当然可以,但仅仅是语言本身除了学习之外,好像还没有别的什么用途!
那么,真正的软件开发需要哪些知识呢?
先扯点题外话
人类似乎总是遭到天神的嘲弄,比如给了人类了解”天意”的智慧,却没有给予足够的时间,人类连自己也搞不清楚,却要去”改造大自然”.软件行业也是一个典型,现代自动化和高科技支撑产业的软件行业,却是几千年前就存在的”手工业作坊”,知识工具变成了键盘,鼠标.
函数和程序结构
1.函数
函数调用非常依赖编译实现,函数的参数和返回值一般存放在线程堆栈中.
对于c语言,函数都是外部的!(函数中不能包含函数)
重要的一条,函数的参数传递统一为值传递
这一点很重要!
一般来说,在调用一个函数时,参数是放在栈里的(有时候编译器会把参数放到寄存器中,不影响我们讨论),值传递的意思是,编译器会将传递给函数的参数(我这里不打算引入形参和实参的概念,其实或许无助于理解)的值赋值到栈中,这样的处理会使编译简单,也使程序员容易理解.
int AddTen(int aNum)
{
}
int a =100;
int main()
{
return 0;
}
调用函数AddTen 时 变量a的值 100 被赋值到栈中,也就是在函数AddTen中 aNum是一个局部变量,值是100.
传值的缺点是明显的,如果参数尺寸很大,则复制会影响性能,而且大多数复制是没有必要的!
另外,如果一个函数有多个返回值怎么办?
有经验的C程序员在传递尺寸大的参数或者需要多个返回值时,会采用传递指针的方法:
比如上面定
(3)
c语言数据对象的生存期 分为三种:
外部变量: 生存期由编译器和操作系统决定,一般来说生存期从定义开始到程序结束为止.
内部变量(c语言也叫auto,另外寄存器变量就不考虑了),一般在函数内部(函数的子代码块中的定义的变量也一样)从定义开始(其实是函数开始),到函数结束为止.
动态创建的数据对象只是那些比如通过malloc调用得到的内存块上的数据对象,生存期靠程序员决定(不错,给程序员很大的发挥空间,不过是把”双刃剑”,以至于java不允许干这个事情).
另外注意一个关键字 static
如果是外部变量static修饰限制了这个外部变量的作用域,只能在本文件中使用.
如果是内部变量static ,改变了这个变量的生存期(类似一个外部变量).
当然函数也是外部的,如果用 static修饰,也是改变了这个函数的作用域,只能在本文件中使用.
没有static修饰的外部变量其实作用域可以是整个程序.
另外c引入c++的const关键字只是说明该变量的只读特性.仅此而已.
复杂类型的定义与声明
数组