重构(二)代码的坏味道
原图
神秘命名(Mysterious Name)
整洁代码最重要的一环就是好的名字,所以我们会深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法。 然而,很遗憾,命名是编程中最难的两件事之一。正因为如此,改名可能是最常用的重构手法,包括改变函数声明(用于给函数改名)、变量改名、字段改名等。很多人经常不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。
改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。为一个恼人的名字所付出的纠结,常常能推动我们对代码进行精简。
重复代码(Duplicated Code)
如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。
最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。这时候你需要做的就是采用提炼函数提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。如果重复代码只是相 ...
重构(一)重构的原则
原图
何谓重构
“重构”这个词既可以用作名词也可以用作动词。
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。因此,在重构的过程中,我的代码很少进入不可工作的状态,即便重构没有完成,我也可以在任何时刻停下来。
Tip 如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构。
在上述定义中,我用了“可观察行为”的说法。它的意思是,整体而言,经过重构之后的代码所做的事应该与重构之前大致一样。这个说法并非完全严格,并且我是故意保留这点儿空间的:重构之后的代码不一定与重构前行为完全一致。比如说,提炼函数会改变函数调用栈,因此程序的性能就会有所改变;改变函数声明和搬移函数等重构经常会改变模块的接口。不过就用户应该关心的行为而言,不应该有任何改变。如果我在重构过程中发现了任何 bug,重构完 ...
C++(四) Effective-C++(下)
原图
模板与泛型编程
条款 41:了解隐式接口和编译期多态
面向对象编程世界总是以显式接口( explicit interfaces)和运行期多态( runtime polymorphism)解决问题。Templates 及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口( implicit interfaces)和编译期多态( compile-time polymorphism)移到前头了。
123456789template<typename T>void doprocessing(T& w){ if (w.size() > 10 && w != somenastywidget) { T temp (w); temp.normalize (); temp.swap(w); }}
w 必须支持哪一种接口,系由 template 中执行于 w 身上的操作来决定。本例看来 w 的类型好像必须支持 size, normalize ...
C++(三) Effective C++(中)
原图
设计与声明
条款 19:设计 class 犹如设计 type
C++就像在其他 OOP(面向对象编程)语言一样,当你定义一个新 class,也就定义了一个新 type。这意味你并不只是 class 设计者,还是 type 设计者。重载( overloading)函数和操作符、控制内存的分配和归还、定义对象的初始化和终结全都在你手上。因此你应该带着和“语言设计者当初设计语言内置类型时”一样的谨慎来研讨 class 的设计。
条款 20:宁以传递 const 引用替换传递值
缺省情况下 C++以 by value 方式传递对象至(或来自)函数。除非你另外指定,否则函数参数都是以实际实参的复件(副本)为初值,而调用端所获得的亦是函数返回值的一个复件。这些复件(副本)系由对象的 capy 构造函数产出,这可能使得 pass-by-value 成为费时的操作。
如果有什么方法可以回避所有那些构造和析构动作就太好了。有的,就是 pass by reference-to-const:这种传递方式的效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。修订后的这个参数声明中 ...
C++(二) Effective C++(上)
原图
让自己习惯 C++
条款 03:尽可能使用 const
如果关键字 const 出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
12345char greeting[] = "Hello";char* p = greeting; //non-const pointer,non-const dataconst char* p greeting; //non-const pointer,const datachar* const p = greeting; //const pointer,non-const dataconst char* const p = greeting; //const pointer,const data
条款 04:确定对象被使用前已先被初始化
为内置型对象进行手工初始化,因为 C++不保证初始化它们。
构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其 ...
C++(一)对象模型
原图
对象模式
在 C++中,有两种 class data members:static 和 nonstatic,以及三种 class member functions:static、nonstatic 和 virtual。已知下面这个 class Point 声明:
123456789101112class Point { public: Point(float xval); virtual ~Point(); float x() const static int PointCount(); protected: virtual ostream& print(ostream &os) const; float _x; static int _point_count;};
这个 class Point 在机器中将会被怎么样表现呢?也就是说,我们如何模塑(modeling)出各种 data members 和 function members 呢?
C++对象模型(The C++ Object Model)
St ...
链接装载与库(五)动态链接
原图
为什么要动态链接
内存和磁盘空间: /usr/bin 下就有数千个可执行文件,还有其他数以千计的库如果都需要静态链接,那么空间浪费无法想象
程序开发和发布: 一旦程序中有任何模块更新,整个程序就要重新链接、发布给用户
程序可扩展性和兼容性: 动态链接还可以加强程序的兼容性。一个程序在不同的平台运行时可以动态地链接到由操作系统提供的动态链接库
存在的问题: 当程序所依赖的某个模块更新后,由于新的模块与旧的模块之间接口不兼容,导致了原有的程序无法运行
简单的动态链接例子
我们分别需要如下几个源文件:“Program1.c”、“Program2.c”、“Lib.c”和“Lib.h”。
12345678910111213141516171819202122232425262728293031323334清单 7-1 SimpleDynamicalLinking/*Program1.c*/#include "Lib.h"int main(){ foobar(1); return 0;}/*Program2.c */#include "Lib.h"int main(){ ...
链接装载与库(四)可执行文件的装载与进程
原图
装载的方式
程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将程序运行所需要的指令和数据全都装入内存中,这就是最简单的静态装入的办法。后来研究发现,程序运行时是有局部性原理的,所以我们可以将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盘里面,这就是动态装入的基本原理。
覆盖装入(Overlay)和页映射(Paging)是两种很典型的动态装载方法,它们所采用的思想都差不多,原则上都是利用了程序的局部性原理。动态装入的思想是程序用到哪个模块,就将哪个模块装入内存,如果不用就暂时不装入,存放在磁盘中。
页映射
将内存和所有磁盘中的数据和指令按照“页(Page)”为单位划分成若干个页,以后所有的装载和操作的单位就是页。
为了演示页映射的基本机制,假设我们的 32 位机器有 16KB 的内存,每个页大小为 4096 字节,则共有 4 个页,如下表所示。
页编号
地址
F0
0x00000000 - 0xFFFFF000
F1
0x00001000 - 0x00001FFF
F2
0X00002000 - 0X00002FFF ...
链接装载与库(三)静态链接
原图
在这一节里,我们将使用下面这两个源代码文件“a.c”和“b.c”作为例子展开分析:
假设我们的程序只有这两个模块“a.c”和“b.c”。首先我们使用 gcc 将“a.c”和“b.c”分别编译成目标文件“a.o”和“b.o”:
1gcc -c a.c b.c
从代码中可以看到,“b.c”总共定义了两个全局符号,一个是变量“shared”,另外一个是函数“swap”;“a.c”里面定义了一个全局符号就是“main”。模块“a.c”里面引用到了“b.c”里面的“swap”和“shared”。我们接下来要做的就是把“a.o”和“b.o”这两个目标文件链接在一起并最终形成一个可执行文件“ab”。
空间与地址分配
对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件?
按序叠加
如图 4-1 所示,就是直接将各个目标文件依次合并,但是在有很多输入文件的情况下,输出文件将会有很多零散的段,这种做法非常浪费空间,因为每个段都须要有一定的地址和空间对齐要求。
相似段合并
一个更实际的方法是将相同性质的段合并到一起,比如将所有输入文件的“.text”合并到输出文件的“.text”段, ...
链接装载与库(二)目标文件
原图
目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。
目标文件的格式
现在 PC 平台流行的可执行文件格式主要是 Windows 下的 PE(Portable Executable)和 Linux 的 ELF(Executable Linkable Format),它们都是 COFF(Common fileformat)格式的变种。
ELF 文件标准里面把系统中采用 ELF 格式的文件归为如下表所列举的 4 类。
ELF 文件类型
说明
实例
可重定位文件 Relocatable File
这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类
Linux 的。o Windows 的。obj
可执行文件 Executable File
这类文件包含了可以直接执行的程序,它的代表就是 ELF 可执行文件,它们一般都没有扩展名
比如/bin/bash 文件 Windows 的。exe
共享目标文件 Shared Object File ...