Java程序设计入门教程.pdf
在专用双向链表中,dlist_printf的实现非常简单,如果里面存放的是整数,用 %d 打印,存放的是字符串,用 %s 打印。现在的麻烦在于双向链表是通用的,我们无法预知其中存在的数据类型,也就是说我们要面对数据类型的变化。怎么办呢?初学者可以参考的常用方法有以下几种。
比如实现dlist_print_int用来打印存放整数的双向链表,dlist_print_string用来打印存放字符串的双向链表等,其他类型都有自己的打印函数。
不过这种做法也有一些缺点。一是每个函数的实现方式类似,会带来大量重复的代码。二是由于数据类型的种类不确定,如果为每种数据类型都实现一个print函数,当要存放新的数据类型时,就不得不修改dlist的实现。
比如传入1表示按整数方式打印,传入2表示按字符串方式打印,以此类推。
这种做法比第一种好一点,至少不会造成大量重复的代码。但是同样存在增加新类型时要修改dlist_print函数的问题。
这种方法没有前面两种方法的缺点,而且是一种相当直观的方式。但奇怪的是偏偏很少有人使用这个方法,原因可能有两个:其一是太拘泥于传统的实现方式而没有想到这一种;其二是担心性能问题,因为通过索引取值,每一次都要从头开始定位,其性能开销为O.
其实这种方法是可以接受的,dlist_print函数只是用于辅助测试,我们并不需要太在乎它的性能开销,而且我们很少会在链表中存放成千上万的数据,因此这个函数带来的性能影响根本没有想的那样严重。所以在这里我们要介绍一种新的方法。
dlist_print的大体框架如下。
在上面代码中,我们主要是不知道如何实现 print(iter->data); 这行代码。那么谁知道呢?很明显,调用者知道,因为调用者知道链表里面所存放的数据类型。好吧,那就让调用者来做好了,调用者在调用dlist_print时会提供一个函数给dlist_print来调用,这种回调调用者所提供函数的方法,我们可以称之为回调函数法。
调用者如何提供函数给dlist_print呢?当然是通过函数指针了。变量指针指向的是一块数据,指针指向不同的变量,则取到的是不同的数据。函数指针指向的是一段代码(即函数),指针指向不同的函数,则具有不同的行为。函数指针是实现多态的手段,多态就是隔离变化的秘诀,这里只是一个开端,后面我们会逐步地深入学习。
请看详细实现过程
我见过不少任劳任怨的程序员,别人让他做什么他就做什么,不管是不是份内的事,不管是上司要求的还是同事要求的,都来者不拒。别人说需要一个某某功能的函数,他就写一个在他的模块里,日积月累,他的模块就成了一锅“大杂烩”。我亲眼见过有程序员在系统设置和桌面两个模块里,提供很多毫不相干的函数,这些函数会造成不必要的耦合和复杂度。在这里也是一样的,求和与求最大值并不是dlist应该提供的功能,放在dlist里面实现是不应该的。为了能实现这些功能,我们提供一种满足这些需求的机制就好了。热心肠是好的,但一定不要“管得太宽”,否则就费力不讨好了。
对于初学者来说这道题有点难度,很少有人能完全做对。不过没关系,我并不是要出一道难题来难倒大家,而是要刺激大家去思考,以期达到加深学习印象的效果。有了前面两次的经验,我想应该没人会去写一个dlist_to_upper函数,大家都会调用dlist_foreach来实现。不过新的问题又出现了,初学者还是有可能犯以下几种常犯的错误。
这是我们在课本里学到的写法,但在工程中是不能这样做的。因为大小写字母在不同语言中的定义是不一样的,“a”是一个字符常量,它的值在任何时候都是97,但在不同语言中,97却不一定代表“a”。我们不能简单地认为在97(a)—122(z)之间的字符就是小写字母,而是应该调用标准C函数islower来判断,同样转换为大写应该调用toupper而不是减去一个常量。
运行时会出现“Segmentation fault”错误。原因是“It”等字符串是常量,常量是不能被修改的。
运行时发现打印出几个感叹号。原因是执行dlist_append时没有复制一份,所以在dlist中存放的是同一个地址。而且这个dlist在当前函数返回后,里面保存的数据都无效了,因为这些数据指向的是临时变量。
这里看起来工作正常了,但存在内存泄露的bug。strdup调用malloc分配了内存,但没有地方去释放它们。
初学者对内存和指针只有一知半解的认识,常常犯一些连自己都莫名其妙的错误。为了避免这些不必要的错误,今天我们要学习各种数据存放的位置以及它们的特性,让初学者对编程有更进一步的认识。在程序中,数据存放的位置主要有以下几个。
通俗地讲,bss段被用来存放那些没有初始化或初始化为0的全局变量。它有什么特点呢,让我们先来看看一个小程序的表现。
变量bss_array的大小为4M,而可执行文件的大小只有5K。由此可见,bss类型的全局变量只占运行时的内存空间,而不占用文件空间。
现在大多数操作系统在加载程序时,会把所有的bss全局变量清零。但为了保证程序的可移植性,最好能手工把这些变量初始化为0,这样可以使这些变量都有个确定的初始值。
当然了,作为全局变量,在整个程序的运行周期内,bss数据是一直存在的。
与bss相比,data段就容易理解多了,看名称就大概能知道它里面存放着数据。当然,如果数据全是0,为了优化考虑,编译器会把它当作bss处理。通俗地讲,data段被用来存放那些初始化为非0值的全局变量。那么它又有什么特点呢,我们还是先来看看一个小程序的表现。
仅仅是把初始化的值改为非0值了,文件就变为4M多。由此可见,data类型的全局变量是既占文件空间,又占用运行时内存空间的。
同样,作为全局变量,在整个程序的运行周期内,data数据也是一直存在的。
rodata的意义同样明显,ro代表read only(只读),rodata就是用来存放常量数据的。关于rodata类型的数据,要注意以下几点。
由此可见,把在运行过程中不会改变的数据设为rodata类型是有好处的。在多个进程间共享,可以大大提高空间利用率,甚至能不占用RAM空间。同时由于rodata在只读的内存页面中是受保护的,任何试图对它进行修改的行为都会被及时发现,这样一来还可以提高程序的稳定性。
字符串会被编译器自动放到rodata中,其他数据要放到rodata中,只需要为其加const关键字修饰即可。
text段存放代码(如函数)和部分整数常量,它与rodata段很相似,相同的特性我们就不重复了,主要的区别在于text段是可以执行的。
栈是用来存放临时变量和函数参数的。将栈作为一种基本数据结构,我并不感到惊讶;将其用来实现函数调用,也是大家司空见惯的作法。直到我试图找到另外一种方式实现递归操作时,我才感叹于栈的巧妙。要实现递归操作,不用栈不是不可能,只是找不出比使用栈更优雅的方式。
通常情况下,栈是向下(低地址)增长的,每向栈中PUSH一个元素,栈顶就向低地址扩展,每从栈中POP一个元素,栈顶就向高地址回退。这里有一些比较有意思的问题:在x86平台上,栈顶寄存器为ESP,那么ESP的值是在PUSH操作之前修改呢,还是在PUSH操作之后修改呢?PUSH ESP这条指令会向栈中存入什么数据呢?据说x86系列CPU中,除了286外,都是先修改ESP,再压栈的。由于286没有CPUID指令,因此有的操作系统会用这种方法检查286的型号。
要注意的是,存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了,继续访问这些变量会造成意想不到的错误。
堆是最灵活的一种内存,它的生命周期完全由使用者控制。标准C提供以下几个函数来使用堆内存。
本文通过一个简单需求的完成过程讲述了程序员应具备的态度和技能,是程序员进阶的必经之路。
Java程序设计入门教程.pdf