代码中的软件工程
代码中的软件工程
非常感谢孟宁老师的亲切指导,项目代码地址: https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
以下是对学习内容的一个总结回顾。主要包括了如何在VsCode中配置C/C++编译调试环境,以及结合老师给的menu项目源代码,自己对软件工程中的模块化设计、可重用接口、线程安全等问题进行简单分析。
所有操作皆在
macos平台完成
1 编译与调试
Install Mingw-w64/GCC and GDB
mac安装比较简单,直接命令行就完事儿了
brew install gcc gdb # for macos

2 代码风格规范
2.1 什么样的代码是好代码:简明、易读、无二异性
- 第一层:规范整洁
- 第二层:逻辑清晰
- 第三层:优雅
但是,一些工作文档通常来说会规定代码规范,下面是一个示例
-
缩进:4个空格;
-
行宽:< 100个字符;
-
代码行内要适当多留空格,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d));
-
在一个函数体内,逻揖上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行以示区隔;
-
在复杂的表达式中要用括号来清楚的表示逻辑优先级;
-
花括号:所有 ‘{’ 和 ‘}’ 应独占一行且成对对齐;
-
不要把多条语句和多个变量的定义放在同一行;
2.2 程序块头部的注释
一般而言,在头部注释我们要注意一下三点
- 注释和版权信息:注释也要使用英文,不要使用中文或特殊字符,要保持源代码是ASCII字符格式文件;
- 不要解释程序是如何工作的,要解释程序做什么,为什么这么做,以及特别需要注意的地方;
- 每个源文件头部应该有版权、作者、版本、描述等相关信息。
下面给一个示范样例

2.3 如何编写高质量代码
一般来说,我们要注意一下几点
- 使用控制结构简化
- 使用数据结构简化
- 一定要有错误处理
- 关于性能问题:我们在具体编程实现过程中已经不再需要考虑代码性能问题,将更多精力放在提高工作效率、质量保证、代码的可读性、可扩展性等方面,让性能问题在更高层的软件架构设计层面考虑更加合理有效
- 拒绝修修补补要不断重构代码
3 模块化设计
3.1 模块化的由来——关注点分离(分而治之)
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出,没错就是Dijkstra最短路径算法的作者。
- 关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是
“分而治之”的方法。 - 关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。
3.2 耦合度(Coupling)和内聚度(Cohesion)
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。

内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
3.3 代码分析
linklist.c - 链表文件被抽离出来,形成一个模块
#include <stdio.h>
#include <stdlib.h>
#include "linklist.h"
tDataNode* FindCmd(tDataNode * head, char * cmd)
{
if(head == NULL || cmd == NULL)
{
return NULL;
}
tDataNode *p = head;
while(p != NULL)
{
if(!strcmp(p->cmd, cmd))
{
return p;
}
p = p->next;
}
return NULL;
}
int ShowAllCmd(tDataNode * head)
{
printf("Menu List:\n");
tDataNode *p = head;
while(p != NULL)
{
printf("%s - %s\n", p->cmd, p->desc);
p = p->next;
}
return 0;
}
menu.c - 引入链表文件,这样的话menu模块和list模块耦合度降低
#include <stdio.h>
#include <stdlib.h>
#include "linklist.h"
int Help();
#define CMD_MAX_LEN 128
#define DESC_LEN 1024
#define CMD_NUM 10
/* menu program */
static tDataNode head[] =
{
{"help", "this is help cmd!", Help,&head[1]},
{"version", "menu program v1.0", NULL, NULL}
};
main()
{
/* cmd line begins */
while(1)
{
char cmd[CMD_MAX_LEN];
printf("Input a cmd number > ");
scanf("%s", cmd);
tDataNode *p = FindCmd(head, cmd);
if( p == NULL)
{
printf("This is a wrong cmd!\n ");
continue;
}
printf("%s - %s\n", p->cmd, p->desc);
if(p->handler != NULL)
{
p->handler();
}
}
}
int Help()
{
ShowAllCmd(head);
return 0;
}
4 可重用软件设计
4.1 消费者重用和生产者重用
4.1.1 消费者重用
-
消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。软件开发者在重用已有的软件模块代码时一般会重点考虑如下四个关键因素:
-
该软件模块是否能满足项目所要求的功能;
-
采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作;
-
该软件模块是否有完善的文档说明;
-
该软件模块是否有完整的测试及修订记录;
-
4.1.2 生产者重用
-
我们清楚了消费者重用时考虑的因素,那么生产者在进行可重用软件设计时需要重点考虑的因素也就清楚了,但是除此之外还有一些事项在进行可重用软件设计时牢记在心,我们简要列举如下:
-
通用的模块才有更多重用的机会;
-
给软件模块设计通用的接口,并对接口进行清晰完善的定义描述;
-
记录下发现的缺陷及修订缺陷的情况;
-
使用清晰一致的命名规则;
-
对用到的数据结构和算法要给出清晰的文档描述;
-
与外部的参数传递及错误处理部分要单独存放易于修改;
-
4.2 可重用接口规格设计
再进行了初步的模块化设计之后,我们要求一个软件模块只用做好一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。
这时进一步优化这个初步的模块化代码,就需要设计合适的接口,接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:
- 接口的目的;
- 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
- 使用接口的双方遵守的协议规范;
- 接口使用之后的效果,一般称为后置条件;
- 接口所隐含的质量属性。
4.3 参考代码
/*
* Create a LinkTable
*/
tLinkTable * CreateLinkTable();
/*
* Delete a LinkTable
*/
int DeleteLinkTable(tLinkTable *pLinkTable);
/*
* Add a LinkTableNode to LinkTable
*/
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
/*
* Delete a LinkTableNode from LinkTable
*/
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
/*
* Search a LinkTableNode from LinkTable
* int Conditon(tLinkTableNode * pNode,void * args);
*/
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
/*
* get LinkTableHead
*/
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
/*
* get next LinkTableNode
*/
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
5 线程安全
5.1 线程安全基本概念
-
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
-
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
5.2 线程安全与可重入函数
说到线程安全,一定需要提到可重入函数。可重入函数指可以被多个任务并发使用,而不必担心数据发生错误。与之相反的不可重入函数则要求不能超过一个任务来共享,除非能保证函数的互斥。而线程安全又与可重入密切相关,为了保证线程安全,我们在必要时需要通过“上锁”来保护临界资源。
线程安全主要还是体现在修改和读取的时序问题上,一般来说通过锁的机制来保证。线程安全问题大多是由全局变量及静态变量引起的,局部变量逃逸也可能导致线程安全问题。
为了测试线程安全的具体实现问题,在test.c的基础上还增加了另外三个c文件。
-
test_fork.c提供了fork操作,可以在父进程中起一个子进程;
-
test_exec.c在test_fork.c的基础上提供了exec操作,在子进程中执行hello;
-
test_reply.c中通过起不同的子进程模拟客户端与服务器进行TCP通信。
5.3 代码参考
//linktable.c
/*
* Delete a LinkTableNode from LinkTable
*/
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
if(pLinkTable == NULL || pNode == NULL)
{
return FAILURE;
}
pthread_mutex_lock(&(pLinkTable->mutex));
if(pLinkTable->pHead == pNode)
{
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
if(pLinkTable->SumOfNode == 0)
{
pLinkTable->pTail = NULL;
}
pthread_mutex_unlock(&(pLinkTable->mutex));
return SUCCESS;
}
tLinkTableNode * pTempNode = pLinkTable->pHead;
while(pTempNode != NULL)
{
if(pTempNode->pNext == pNode)
{
pTempNode->pNext = pTempNode->pNext->pNext;
pLinkTable->SumOfNode -= 1 ;
if(pLinkTable->SumOfNode == 0)
{
pLinkTable->pTail = NULL;
}
pthread_mutex_unlock(&(pLinkTable->mutex));
return SUCCESS;
}
pTempNode = pTempNode->pNext;
}
pthread_mutex_unlock(&(pLinkTable->mutex));
return FAILURE;
}
浙公网安备 33010602011771号