去掉视图中的显示段落标记可让文档更干净些html
软件开发的困难在哪里?对于这个问题,不一样的人有不一样的答案,同一我的在不一样职业阶段linux
也会有不一样的答案。做为一个系统程序员来讲,我认为软件开发有两大难点:程序员
一是控制软件的复杂度。软件的复杂度愈来愈高,而人类的智力基本保持不变,如何以有限 的智力去控制无限膨胀的复杂度?我经历过几个大型项目,也分析 过很多现有的开源软件, 我得出一个结论:没有单个难题和技术细节是咱们没法搞定的,而全部这些问题出如今一个 项目中时,其呈指数增加的复杂度每每让咱们束 手无策。算法
二是隔离变化。用户需求在变化,应用环境在变化,新技术不断涌现,全部这些都要求软件 开发可以射中移动的目标。即便是开发基础平台软件,在超过几年 时间的开发周期以后, 需求的变化也是至关惊人的。需求变化并不可怕,关键在于变化对系统的影响,若是牵一发 而动全身,一点小小的变化可能对系统形成致命的 影响。编程
培训能够制造合格的程序员,却没法造就一流的高手。培训是一个被 动的过程,咱们要变被动为主动。小程序
make的改进版 automake,如今你能 写出下面这种简单的 Makefile 就好了:bash
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\n",__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("\nsum=%lld max=%d\n", 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的大小