编译与调试环境
在 Windows 下用 GNU ToolChain 真的伤不起。课程 ppt 里的链接下不了,搞了一大堆无用操作,还是不行。最后(全局)科*上网用 MinGW.org 的 mingw-get 下载成功。lab5.2 有了 Makefile 之后,要 link 到 pthread,所以还要下。按理说装好 gcc,make 和 pthread 应该就好了。然而这个下载器,直接连接的话除了 gcc/g++ 都下不了,也没有搜索功能,找个包找到海枯石烂。(然后 Makefile 里也没给 $(CC_PTHREAD_FLAGS)
,main()
也没有返回值,宏 debug("xxxx")
也不知道在干嘛......)
MinGW Installer 要装的包如下
路径里的 msys1.0in 包含了 make 在内的一票命令行工具,之前下过 GNU Make 貌似也可以。
命令行编译每个 lab 里的代码的话,大概格式如下:
> gcc menu.c linktable.c -o menu.exe -lpthread
调试的话,在 vscode 可以选择 gdb 来调试。
代码成长轨迹
这个命令行菜单小项目演示了一个代码不断成长的过程。各个版本都根据软件开发中的一些原则加入新的东西。
版本 | 内容变化 | 涉及 |
---|---|---|
lab1 | +hello.c +menu.c | 运行测试、伪代码 |
lab2 | -hello.c | 源文件信息头、主函数框架 |
lab3.1 | 命令链表遍历代替分支语句 | |
lab3.2 | 隐藏命令链表的遍历操作 | |
lab3.3 | +linklist.h +linklist.c | 将命令链表操作独立成另一个模块 |
lab4 | -linklist.h -linklist.c +linktable.h +linktable.c +test.c +testlinktable.c | 把链表与命令分离、动态创建命令、可重入函数与线程安全、模块测试 |
lab5.1 | -test.c | call-back 函数 |
lab5.2 | -testlinktable.c +Makefile | 增加 Mackefile |
lab7.1 | +test.c +menu.h | 把菜单独立成模块 |
lab7.2 | +readme.txt | 项目说明 |
代码分析与感悟
模块化与接口
软件工程这件事,说白了就是为了人。计算机科学和自然科学不同,它是个人造的东西。人干什么事情都是有目的,人发明的技术都是为了解决问题。为什么要有模块化?因为我没必要重新写以前写过的东西,因为我想用别人写好的我想要的东西。要重用代码,这个代码如果乱成一坨,那我还不如写个新的。所以代码要有规范才能好理解、要容易往里加东西但又不容易被破坏原本的结构……用正式的话总结出来就变成一条条的原则写在教科书上,再也没人看得懂了。这很矛盾。前人总结的经验没有经验你看不懂,你看不懂硬要用的话,就会过度设计;你自己遇到问题之后,你再去看这些,除了发出点“噢,我也跳过这个坑,你们还给他起了个名字”之类的感慨,也没啥了。
接口和抽象紧密相连。计算机本身就是从电路一层层抽象上来的,各个层次之间都要定义好接口,这样才能接得上。作为上层软件的程序员,不可能理解完所有的细节才去编程,否则日子没法过了。所以我不关心这一层层的黑盒里具体干了什么,我只关心他们提供的功能,而这些功能通过接口获取。代码里,把 linktable 变成模块以后,我只需要调用其接口函数,就能用到他的功能了。
那可重用的接口该如何设计?应该脱离依赖,低耦合。打个比方,lab5.1 的 linktable.c 的 SearchLinkTableNode 函数,他的搜索命中判断条件,为什么不硬编码到 if 里,而是给一个谓词参数传进来?因为要脱离依赖。不同的人用链表查找的事情不同,你都直接规定了查找条件,谁还能重用你这个模块?
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))
{
if(pLinkTable == NULL || Conditon == NULL)
{
return NULL;
}
tLinkTableNode * pNode = pLinkTable->pHead;
while(pNode != pLinkTable->pTail)
{
if(Conditon(pNode) == SUCCESS)
{
return pNode;
}
pNode = pNode->pNext;
}
return NULL;
}
再看 lab5.2 的 linktable.c 里的 SearchLinkTableNode,增加了一个参数。而 call-back 函数也增加了这个参数。这里的主要目的是,把 SearchCondition 里的判断参数 cmd 独立出来(他本来是全局的)。
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
{
if(pLinkTable == NULL || Conditon == NULL)
{
return NULL;
}
tLinkTableNode * pNode = pLinkTable->pHead;
while(pNode != NULL)
{
if(Conditon(pNode,args) == SUCCESS)
{
return pNode;
}
pNode = pNode->pNext;
}
return NULL;
}
// menu.c
int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
{
char * cmd = (char*) args;
tDataNode * pNode = (tDataNode *)pLinkTableNode;
if(strcmp(pNode->cmd, cmd) == 0)
{
return SUCCESS;
}
return FAILURE;
}
个人感觉,这样并没啥特别的改变。因为这个 Condition 函数, 他的参数是随着 SearchLinkTableNode 函数参数的改变而改变的。比起 lab5.1,SearchLinkTableNode 甚至还知道了关于判断条件更多的信息(Condition 除了 pNode 还需要 args)。要我想个解决方案的话,干脆不要 Condition 函数,把他换成类(如果在支持类机制的语言里)。也许可以这么写:
class Conditon()
{
Condition(char *args) { ... }
// 需要可变长参数的话?
// Condition(char *args, int len) { ... }
int evaluate(tLinkTableNode * pNode)
{
// 任何判断条件都可以
if (pNode ... args)
{
...
}
}
char* args;
}
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, const Conditon* condition)
{
if(pLinkTable == NULL || Conditon == NULL)
{
return NULL;
}
tLinkTableNode * pNode = pLinkTable->pHead;
while(pNode != pLinkTable->pTail)
{
if(conditon.evaluate(pNode) == SUCCESS)
{
return pNode;
}
pNode = pNode->pNext;
}
return NULL;
}
继承与多态
我以前貌似在某设计模式的书里看到过 “不要 if-else 要类” 的说法。如下代码片段所示,通过给所有的命令套进一个 Node 里,Node 里封装一个统一的函数指针 int (*handler)()
,这样就可以通过这个统一的指针,调用到不同的函数。再看下下面,LinkTableNode 作为一个链表结点,他逻辑上只需要能指向下一个结点就行了,至于他里面放什么数据,不是他非得关心的问题。所以把他作为父类,不同的结点继承他。这些不同的结点作为子类,放不同的具体数据。
// lab2/menu.c
while(1)
{
scanf("%s", cmd);
if(strcmp(cmd, "help") == 0)
{
printf("This is help cmd!
");
}
else if(strcmp(cmd, "quit") == 0)
{
exit(0);
}
else
{
printf("Wrong cmd!
");
}
}
// lab3.1/menu.c
while(1)
{
char cmd[CMD_MAX_LEN];
printf("Input a cmd number > ");
scanf("%s", cmd);
tDataNode *p = head;
while(p != NULL)
{
if(strcmp(p->cmd, cmd) == 0)
{
printf("%s - %s
", p->cmd, p->desc);
if(p->handler != NULL)
{
p->handler();
}
break;
}
p = p->next;
}
if(p == NULL)
{
printf("This is a wrong cmd!
");
}
}
// lab4
// linktable.c
typedef struct LinkTableNode
{
struct LinkTableNode * pNext;
}tLinkTableNode
// menu.c
typedef struct DataNode
{
tLinkTableNode * pNext;
char* cmd;
char* desc;
int (*handler)();
} tDataNode
可重入函数与线程安全
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入的(reentrant)。可重入和线程安全密切相关。可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。操作系统说,信号量用来保护临界资源,而临界资源是一段可共享的代码。在 linktable.c 里可以看到很多类似的代码:
pthread_mutex_lock(&(pLinkTable->mutex));
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex));
// 简化一下
P(mutex)
// do something...
V(mutex)
信号量的初始化和销毁:
pthread_mutex_init(&(pLinkTable->mutex), NULL);
// ...
pthread_mutex_destroy(&(pLinkTable->mutex))
工程化
当程序员多了起来,代码多了起来,工程复杂了起来,就必须要使用一定的方法来管理项目了。c/c++ 项目的构建系统,在 Linux / Unix 下有 make,Windows 下 MSVC 的 proj 和 sln,跨平台的 CMake。这些工具是通过编写一些规定好语法的配置文件,来指导编译链接甚至安装的过程(说白了就是把一行行敲的命令和编译链接选项,写进了文件里)。项目里展示的 make 工具的 Makefile :
#
# Makefile for Menu Program
#
CC_PTHREAD_FLAGS = -lpthread
CC_FLAGS = -c
CC_OUTPUT_FLAGS = -o
CC = gcc
RM = rm
RM_FLAGS = -f
TARGET = test
OBJS = linktable.o menu.o test.o
all: $(OBJS)
$(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) $(CC_PTHREAD_FLAGS)
# CC_PTHREAD_FLAGS 不要忘记加进来
.c.o:
$(CC) $(CC_FLAGS) $<
clean:
$(RM) $(RM_FLAGS) $(OBJS) $(TARGET) *.bak
Makefile有三个非常有用的变量。分别是(@、)^和$<,代表的意义分别是: (@ 表示目标文件;)^ 表示所有的依赖文件;(< 表示第一个依赖文件。 目标.c.o和)<结合起来是精简写法,表示要生成一个.o目标文件就自动使用对应的.c文件来编译生成。
项目里展示的工程化的流程还有,开发前先测试环境啦(helloworld)、写完一个模块先写测试试一下啦(testlinktable)、增加 readme 啦……总的来说,还是值得学习的一个小项目。
参考
[1] https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程