去掉视图中的显示段落标记可以让文档更干净些
软件开发的困难在哪里?对于这个问题,不同的人有不同的答案,同一个人在不同职业阶段
也会有不同的答案。作为一个系统程序员来说,我认为软件开发有两大难点:
一是控制软件的复杂度。软件的复杂度越来越高,而人类的智力基本保持不变,如何以有限 的智力去控制无限膨胀的复杂度?我经历过几个大型项目,也分析 过不少现有的开源软件, 我得出一个结论:没有单个难题和技术细节是我们无法搞定的,而所有这些问题出现在一个 项目中时,其呈指数增长的复杂度往往让我们束 手无策。
二是隔离变化。用户需求在变化,应用环境在变化,新技术不断涌现,所有这些都要求软件 开发能够射中移动的目标。即使是开发基础平台软件,在超过几年 时间的开发周期之后, 需求的变化也是相当惊人的。需求变化并不可怕,关键在于变化对系统的影响,如果牵一发 而动全身,一点小小的变化可能对系统造成致命的 影响。
培训可以制造合格的程序员,却无法造就一流的高手。培训是一个被 动的过程,我们要变被动为主动。
make的改进版 automake,现在你能 写出下面这种简单的 Makefile 就行了:
all:
gcc -g test.c -o test
clean:
rm -f test
在这里,你可以把 all 看作一个函数名,gcc -g test.c -o test 是函数体(前面加 tab),它的功能 是编译 test.c 成 test,在命令行运行 make all 就相当于调用这个函数。clean 是另外一个函数, 它的功能是删除test。
用 C 语言编写一个双向链表。
专业程 序员与业余程序员之分主要在于一种态度,如果缺乏这种态度,拥有十年工作经验也还是业余的。专业的程序员是很 注重自己的形象的,当 然程序员的形象不是表现在衣着和言谈上,而是表现在代码风格上,代码就是程序员的社交 工具,代码风格可是攸关形象的大事。
有人说过,傻瓜都可以写出机器能读懂的代码,但只有专业程序员才能写出人能读懂的代码。 作为专业程序员,每当写下一行代码时,要记得程序首先是给人 读的,其次才是给机器读 的。你要从一个业余程序员转向专业程序员,就要先从代码风格开始,并从此养成一种严谨 的工作态度,生活上的不拘小节可不能带到编程 中来。
专业程序员要有精益求精的精神。至于要精到什么程度,与 具体需求有关,如果只 是写个小程序验证一下某个想法,那完成需要的功能就行了,如果是开发一个基础程序库, 那就要考虑更多了。侯捷先生说过,学从难处学, 用从易处用。这里我们是学习,就要精 得不能再精为止,精到钻牛角尖为止。
请读者思考下面几个问题:
1. 什么是封装?
2. 为什么要封装?
3. 如何实现封装?
1.什么封装?
人有隐私,程序也有隐私。有隐私不是什么坏事,没有隐私人就不是人了,程序也不成其为 程序了。问题是隐私不应该让别人知道,否则伤害的不仅仅是自 己,相关人物也会跟着倒 霉,“艳照门”就是个典型的例子。程序隐私的暴露,造成的伤害不一定有“艳照门”大, 也不一定比它小,反正不要小看它就行了。封装 就是要保护好程序的隐私,不该让调用者 知道的事,就坚决不要暴露出来。
2.为什么要封装? 总体来说,封装主要有以下两大好处(具体影响后面再说):
隔 离 变 化 。 程序的隐私通常是程序最容易变化的部分,比如内部数据结构,内部使用的函 数和全局变量等等,把这些代码封装起来,它们的变化不会影响系统的其它部分。
降 低 复 杂 度 。 接口最小化是软件设计的基本原则之一,最小化接口容易被理解和使用。封 装内部实现细节,只暴露最小的接口,会让系统变得简单明了,在一定程度上降低了系统的
复杂度。 3.如何封装? 隐藏数据结构
暴露内部数据结构,会使头文件看起来杂乱无章,让调用者发蒙。其次是如果调用者图方便, 直接访问这些数据结构的成员,会造成模块之间紧密耦合,给以后的修改带来困难。隐藏数据结构的方法很简单,如果是内部数据结构,外面完全不会引用,则直接放在C 文件中就 好了,千万不要放在头文件里。如果该数据结构 在内外都要使用,则可以对外暴露结构的 名字。
隐藏内部函数
内部函数通常实现一些特定的算法(如果具有通用性,应该放到一个公共函数库里),对调用 者没有多大用处,但它的暴露会干扰调用者的思路,让系统看起 来比实际的复杂。函数名 也会污染全局名字空间,造成重名问题。它还会诱导调用者绕过正规接口走捷径,造成不必 要的耦合。
隐藏内部函数的做法很简单:
在头文件中,只放最小接口函数的声明。 在 C 文件上,所有内部函数都加上 static 关键字。
禁止全局变量
除了为使用单件模式(只允许一个实例存在)的情况外,任何时候都要禁止使用全局变量。这 一点我反复的强调,但发现初学者还是屡禁不止,为了贪图方便而使用全局变量。请读者从 现在开始就记住这一准则。
全局变量始终都会占用内存空间,共享库的全局变量是按页分配的,那怕只有一个字节的全 局变量也占用一个page,所以这会造成不必要空间浪费。全局 变量也会给程序并发造成困 难,想把程序从单线程改为多线程将会遇到麻烦。重要的是,如果调用者直接访问这些全局 变量,会造成调用者和实现者之间的耦合。
关于对象:对象就是某一具体的事物,比如一个苹果, 一台电脑都是一个对象。每个对象都 是唯一的实例,两个苹果,无论它们的外观有多么相像,内部成分有多么相似,两个苹果毕
竟是两个苹果,它们是两个不同的对 象。对象可以是一个实物,也可以是一个概念,比如 一个苹果对象是实物,而一项政策就是一个概念。在软件中,对象是一个运行时概念,它只 存在于运行环境中, 比如:代码中并不存在窗口对象这样的东西,要创建一个窗口对象一 定要运行起来才行。
关 于 类 : 对象可能是一个无穷的集合,用枚举的方式来表示对象集合不太现实。抽象出对 象的特征和功能,按此标准将对象进行分类,这就引入类的概念。类就是一类事物的统称, 类实际上就是一个分类的标准,符合这个分类标准的对象都属于这个类。当然,为了方便起见,通常只 需要抽取那些对当前应用来说是有用的特征和功能。在软件中,类是一个设计时概念,它只存在于代码中,运行时并不存在某个类和某个类之间的交互。我们说,编写一个双向链表,实际上指的是双向链表这个类。
需求简述
Write Once, Debug Everywhere。据说这是流传于 JAVA 程序员中间的一句笑话,Sun 公司用 来形容 JAVA 的跨平台性的原话是 Write once, run anywhere(WORA) 。后者是理想的,前者 才是现实。如果我们的双向链表可以到处运行,那就太好了。Write once, run anywhere(WORA)是我们的目标。
列问题:
1.专用双向链表和通用双向链表各自的特点与适用范围。
2.如何编写一个通用的双向链表?
typedef int Type;
typedef struct _DListNode
{
struct _DListNode* prev;
struct _DListNode* next;
Type data;
}DListNode;
这样的链表算不上是通用的,因为你存放整数时编译一次,存放字符串时,重义 Type 再编 译一次,存放其它类型同样要重复这个过程。麻烦不说,关键是 没有办法同时使用多个数 据类型。
为了让 C 语言实现的函数在 C++中可以调用,需要在头 文件中加点东西才行:
#ifdef __cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif
c语言如何打印出当前源文件的文件名以及源文件的当前行号?
打印文件,函数,行号
printf("file=%s,func=%s,line=%d
",__FILE__,__FUNCTION__,__LINE__);
在专用双向链表中,dlist_printf 的实现非常简单,如果里面存放的是整数,用”%d”打印, 存放的字符串,用”%s”打印。现在的麻烦在于双向链表是通用的,我们无法预知其中存在 的数据类型,也就是我们要面对数据类型的变化。怎么办呢?
dlist_print 的大体框架为:
DListNode* iter = thiz->first;
while(iter != NULL)
{
print(iter->data);
iter = iter->next;
}
在上面代码中,我们主要是不知道如何实现 print(iter->data);这行代码。可是谁知道呢?很明 显,调用者知道,因为调用者知道 里面存放的数据类型。OK,那让调用者来做好了,调用 者调用dlist_print时提供一个函数给dlist_print调用,这种回调调用者提供的函 数的方法, 我们可以称它为回调函数法。
调用者如何提供函数给 dlist_print 呢?当然是通过函数指针了。变量指针指向的是一块数据, 指针指向不同的变量,则取到的是不同的数据。函 数指针指向的是一段代码(即函数), 指针指向不同的函数,则具有不同的行为。函数指针是实现多态的手段,多态就是隔离变化的秘诀.
回到正题上,我们看如何实现 dlist_print: 定义函数指针类型:
typedef DListRet (*DListDataPrintFunc)(void* data);
声明 dlist_print 函数: DListRet dlist_print(DList* thiz, DListDataPrintFunc print);
实现 dlist_print 函数:
DListRet dlist_print(DList* thiz, DListDataPrintFunc print) {
DListRet ret = DLIST_RET_OK;
DListNode* iter = thiz->first;
while(iter != NULL)
{
print(iter->data);
iter = iter->next;
}
return ret;
}
调用方法
static DListRet print_int(void* data)
{
printf("%d ", (int)data);
return DLIST_RET_OK;
}
...
dlist_print(dlist, print_int);
需求简述
这里我们请读者实现下列功能:
对一个存放整数的双向链表,找出链表中的最大值。
对一个存放整数的双向链表,累加链表中所有整数。
int main(int argc, char* argv[])
{
int i = 0;
int n = 100;
long long sum = 0;
MaxCtx max_ctx = {.is_first = 1, 0};
DList* dlist = dlist_create();
for(i = 0; i < n; i++)
{
assert(dlist_append(dlist, (void*)i) == DLIST_RET_OK);
}
dlist_foreach(dlist, print_int, NULL);
dlist_foreach(dlist, max_cb, &max_ctx);
dlist_foreach(dlist, sum_cb, &sum);
printf(" sum=%lld max=%d ", sum, max_ctx.max);
dlist_destroy(dlist);
return 0;
}
static DListRet sum_cb(void* ctx, void* data)
{
long long* result = ctx;
*result += (int)data;
return DLIST_RET_OK;
}
typedef struct _MaxCtx
{
int is_first;
int max;
}MaxCtx;
static DListRet max_cb(void* ctx, void* data)
{
MaxCtx* max_ctx = ctx;
if(max_ctx->is_first)
{
max_ctx->is_first = 0;
max_ctx->max = (int)data;
}
else if(max_ctx->max < (int)data)
{
max_ctx->max = (int)data;
}
return DLIST_RET_OK;
}
static DListRet print_int(void* ctx, void* data)
{
printf("%d ", (int)data);
return DLIST_RET_OK;
}
DListRet dlist_foreach(DList* thiz, DListDataVisitFunc visit, void* ctx)
{
DListRet ret = DLIST_RET_OK;
DListNode* iter = thiz->first;
while(iter != NULL && ret != DLIST_RET_STOP)
{
ret = visit(ctx, iter->data);
iter = iter->next;
}
return ret;
}
这两个函数没有什么实用价值,但是通过它们我们可以学习几点:
1.不要编写重复的代码
按传统的方法写出 dlist_find_max 之后,每个人都知道这个函数与 dlist_print 很类似,在写出 dlist_sum之后,那种感 觉就更明显了。在这个时候,不应该停下来,而是要想办法把这些 重复的代码抽出来。即使因为经验所限,也要极力去想思考和查资料。
写重复的代码很简单,甚至凭本能都可以写出来。但要想成为优秀的程序员,你一定要克服
自己的惰情,因为重复的代码造成很多问题:
重复的代码更容易出错。在写类似代码的时候,几乎所有人(包括我)都会选择 Copy&Paste 的 方法,这种方法很容易犯一些细节上的错误,如果某个地方修改不完整,那就留下了”不定 时”的炸弹,说不定什么时候会暴露出来。
重复的代码经不起变化。无论是修改 BUG,还是增加新特性,往往你要修改很多地方,如 果忘掉其中之一,你同样得为此付出代价。请记住古惑仔的话,出来混迟早是要还的。大师 们说过,在软件中欠下的 BUG,你会为此还得更多。
去除重复代码往往不是件简单的事情,需要更多思考和更多精力,不过事实证明这是最值得
的投资。
2.任何回调函数都要有上下文
大部分初学者都选择了回调函数法,不过都无一例外的选择了用全局变量来保存中间数据,
这里我不想再强调全局变量的坏处了,记性不好的读者可以看看前面的内容。我们要说的是,
在这种情况下,如何避免使用全局变量。
很简单,给回调函数传递额外的参数就行了。这个参数我们称为回调函数的上下文,变量名 用 ctx(context 的缩写)。
下面我们看看怎么实现这个 dlist_foreach:
DListRet dlist_foreach(DList* thiz, DListVisitFunc visit, void* ctx)
{
DListRet ret = DLIST_RET_OK; DListNode* iter = thiz->first; while(iter != NULL && ret != DLIST_RET_STOP) {
ret = visit(ctx, iter->data);
iter = iter->next;
}
return ret;
}
3.只做份内的事
我见到不少任劳任怨的程序员,别人让他做什么他就做什么,不管是不是份内的事,不管是 上司要求的还是同事要求的,都来者不拒。别人说需要一个 XXX 功能的函数,他就写一个
函数在他的模块里,日积月累后,他的模块变得乱七八糟的,成了大杂烩。我亲眼见过在系 统设置和桌面两个模块里,提供很多毫不相干的 函数,这些函数造成不必要的耦合和复杂 度。
在这里也是一样的,求和和求最大值不是 dlist 应该提供的功能,放在 dlist 里面实现是不应 该的。为了能实现这些功能,我们提供一种满足这些需求的机制就好了。热心肠是好的,但一定不能违背原则,否则就费力不讨好了。
需求简述
这里我们请读者实现下列功能:
对一个存放字符串的双向链表,把存放在其中的字符串转换成大写字母。
存放时拷贝了数据,但没有 free 分配的内存。
DList* dlist = dlist_create();
dlist_append(dlist, strdup("It"));
dlist_append(dlist, strdup("is"));
dlist_append(dlist, strdup("OK"));
dlist_append(dlist, strdup("!"));
dlist_foreach(dlist, str_toupper, NULL);
dlist_foreach(dlist, str_print, NULL);
dlist_destroy(dlist);
这里看起来工作正常了,但存在内存泄露的 BUG。strdup 调用 malloc 分配了内存,但没有地 方去 free 它们。
strdup()在内部调用了malloc()为变量分配内存,不需要使用返回的字符串时,需要用free()释放相应的内存空间,否则会造成内存泄漏。
在程序中,数据存放的位置主要有以下几个:
1.未初始化的全局变量(.bss 段)
BSS(Block Started by Symbol)
BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是:可读写的,在程序执行之前BSS段会自动清0。所以,未初始的全局变量在程序执行之前已经成0了。
注意和数据段的区别,BSS存放的是未初始化的全局变量和静态变量,数据段存放的是初始化后的全局变量和静态变量。
李先静: bss 段是用来存放那些没有初始化的和初始化为 0 的全局变量的。
2.初始化过的全局变量 (.data段)
通俗的说,data 段用来存放那些初始化 为非零的全局变量。
3.常量数据 (.rodata段)
rodata 的意义同样明显,ro 代表 read only,rodata 就是用来存放常量数据的。
关于 rodata 类型的数据,要注意以下几点:
o 常量不一定就放在 rodata 里,有的立即数直接和指令编码在一起,存放在代码段(.text)中。
o 对于字符串常量,编译器会自动去掉重复的字符串,保证一个字符串在一个可执行文件 (EXE/SO)中只存在一份拷贝。
o rodata 是在多个进程间是共享的,这样可以提高运行空间利用率。 o 在有的嵌入式系统中,rodata 放在 ROM(或者 norflash)里,运行时直接读取,无需加载到
RAM 内存中。 o 在嵌入式 linux 系统中,也可以通过一种叫作 XIP(就地执行)的技术,也可以直接读取,
而无需加载到 RAM 内存中。
o 常量是不能修改的,修改常量在 linux 下会出现段错误。
由此可见,把在运行过程中不会改变的数据设为 rodata 类型的是有好处的:在多个进程间共 享,可以大大提高空间利用率,甚至不占用RAM空间。同 时由于rodata在只读的内存页面 (page)中,是受保护的,任何试图对它的修改都会被及时发现,这可以提高程序的稳定性。
字符串会被编译器自动放到 rodata 中,其它数据要放到 rodata 中,只需要加 const 关键字修 饰就好了。
4.代码 (.text段) text 段存放代码(如函数)和部分整数常量,它与 rodata 段很相似,相同的特性我们就不重复了,主要不同在于这个段是可以执行的。
5. 栈(stack)
栈用于存放临时变量和函数参数。
尽管大多数编译器在优化时,会把常用的参数或者局部变量放入寄存器中。但用栈来管理函
数调用时的临时变量(局部变量和参数)是通用做法,前者只是辅助手段,且只在当前函数
中使用,一旦调用下一层函数,这些值仍然要存入栈中才行。
通常情况下,栈向下(低地址)增长,每向栈中 PUSH 一个元素,栈顶就向低地址扩展,每从栈中POP一个元素,栈顶就向高地址回退。一个有兴趣的问 题:在x86平台上,栈顶寄 存器为 ESP,那么 ESP 的值在是 PUSH 操作之前修改呢,还是在 PUSH 操作之后修改呢? PUSH ESP 这条指令会向栈中存入什么数据呢?据说 x86 系列 CPU 中,除了 286 外,都是先 修改ESP,再压栈的。由于286没有CPUID指令,有的OS用 这种方法检查286的型号。
要注意的是,存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些
数据也自动释放了,继续访问这些变量会造成意想不到的错误。
6.堆(heap) 堆是最灵活的一种内存,它的生命周期完全由使用者控制。标准 C 提供几个函数:
malloc 用来分配一块指定大小的内存。
realloc 用来调整/重分配一块存在的内存。
free 用来释放不再使用的内存。
最后,我们来看看在 linux 下,程序运行时空间的分配情况:
每个区间都有四个属性:
r 表示可以读取。 w 表示可以修改。 x 表示可以执行。 p/s 表示是否为共享内存。
“ 快”是指开发效率高,“好”是指软件质量高。呵呵,写得又快又好的人就是高手了。 记得这是林锐博士下的定义
UNIX下可使用size命令查看可执行文件的段大小信息。如size a.out。
fdf:data_store chaixiaohong$ gcc -g bss.c -o bss.exe
fdf:data_store chaixiaohong$ ls
Makefile bss.exe.dSYM dlist.h dlist_toupper_test.dSYM
bss.c data.c dlist_toupper.c heap_error.c
bss.exe dlist.c dlist_toupper_test toupper.c
fdf:data_store chaixiaohong$ ls -l bss.exe
-rwxr-xr-x 1 chaixiaohong staff 4624 1 6 16:18 bss.exe
fdf:data_store chaixiaohong$ objdump -h bss.exe | grep bss
-bash: objdump: command not found
fdf:data_store chaixiaohong$ otool -h /bin/ls
/bin/ls:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x80 2 19 1816 0x00200085
fdf:data_store chaixiaohong$ otool -h bss.exe
bss.exe:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x80 2 16 976 0x00200085
fdf:data_store chaixiaohong$
ls 显示的时文件大小 5975, 00400020是bss_array的大小