代码中的软件工程

巨人的肩膀:

代码中的软件工程

Menu

一、编译和调试C/C++环境配置

1. 安装VS Code

首先是下载安装VSCode,这自然不必说,建议在安装的时候勾选通过code打开(如下图):

 

 

 

搜索下载扩展C/C++Chinese(中文见面)

2. 安装minGW64

由于VS Code只是单纯的一个文本编辑器,本身并不集成编译器的功能,因此需要自行下载安装和配置编译器,windows下选择minGW(链接).

下载相应的版本即可,将得到的解压包解压得到minGW64文件夹,将其剪切到指定的位置(注意路径不要有中文),然后将环境变量配置好,调起命令行输入 gcc -v 即可得到对应安装的minGW版本号:

当前VS Code版本下,在安装配置好minGW64环境下,如没有更为具体和细节的要求,VS Code已经能够自动完成配置 task.json 和 launch.json 配置文件。

新建一个工作区文件夹,导入课程代码,选择 hello.c 文件,按下F5进行调试:

选择环境:C++(GDB/LLDB)

选择配置:gcc

c文件对应的是gcc,如果是cpp文件对应的则是g++.

可选项:配置调用外置终端,而不是内置终端。将生成的 launch.json 中的 externalConsole 项配置为 true 即可。

执行结果:

 

至此,环境配置的工作就已经完成了。

 

二、编程的艺术

1. 代码风格与规范

代码风格应遵循的原则:简明、已读且无二义性。

代码的风格与规范是一整套社区程序员们都应当遵守的编码规范,虽然代码的执行是由机器来完成的,但是项目的协作开发、后期维护等都要求我们编写的代码不应只有自己能”看得懂“。好的代码风格与规范不仅能提升易读性,还能在开发的过程中减少一些语法错误。同时,保证风格和规范的统一,也能让我们保持思路的清晰。

代码风格的三重境界:

①规范整洁。遵守常规语言规范,合理使用空格、空行、缩进、注释等;

②逻辑清晰。没有代码冗余、重复,让人清晰明了的命名规则。做到逻辑清晰不仅要求程序员的编程能力,更重要的是提高设计能力,选用合适的设计模式、软件架构风格可以有效改善代码的逻辑结构,会让代码简洁清晰;

③优雅。优雅的代码是设计的艺术,是编码的艺术,是编程的最高追求。

以孟老师代码文件中的一段程序头部注释为例:

其中简明地将函数功能、各参数的含义和输入/输出用途等一一列举,还包括了更新日志,便于追踪。

 

代码风格规范总结

  • 缩进:4个空格;

  • 行宽:<100个字符;

  • 代码行内要适当多留空格,二元操作符的前后应当加空格。但对于表达式比较长的 for 语句和 if 语句,为了紧凑起见可不必加空格;

  • 在一个函数体内,逻辑上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行;

  • 在复杂的表达式中要用括号来表示逻辑优先级;

  • 不要把多条语句和多个变量的定义放在同一行;

  • 命名:合适的命名大大增加代码的可读性;

  • 类名、函数名、变量名等的命名一定要与程序利的函数保持一致,便于阅读理解;

  • 一般变量名、对象名等使用 LowerCamel 风格;

  • 类型、类、函数名等一般使用Pascal风格;

  • 函数名一般使用动词或者动宾短语,如get/set,RenderPage;

  • 注释和版权信息:注释也要使用英文不使用中文。可能编码错误。

  • 每个源文件头部应该有版权、作者、版本、描述等相关信息。

  • 不解释程序如何工作,要解释程序做什么,为什么这样做以及特别的地方。

 

2. 模块化编程

模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。

这个做法背后的基本原理是关注点的分离,关注点的分离在软件工程领域是最重要的原则,其思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合、松散耦合和无耦合,一般在软件设计中我们追求松散耦合。

内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性。

以 menu 项目中的模块化为例:

 1 typedef struct DataNode
 2 {
 3     char*   cmd;
 4     char*   desc;
 5     int     (*handler)();
 6     struct  DataNode *next;
 7 } tDataNode;
 8 
 9 /* find a cmd in the linklist and return the datanode pointer */
10 tDataNode* FindCmd(tDataNode * head, char * cmd);
11 /* show all cmd in listlist */
12 int ShowAllCmd(tDataNode * head);
 1 tDataNode* FindCmd(tDataNode * head, char * cmd)
 2 {
 3     if(head == NULL || cmd == NULL)
 4     {
 5         return NULL;        
 6     }
 7     tDataNode *p = head;
 8     while(p != NULL)
 9     {
10         if(!strcmp(p->cmd, cmd))
11         {
12             return p;
13         }
14         p = p->next;
15     }
16     return NULL;
17 }
18 
19 int ShowAllCmd(tDataNode * head)
20 {
21     printf("Menu List:\n");
22     tDataNode *p = head;
23     while(p != NULL)
24     {
25         printf("%s - %s\n", p->cmd, p->desc);
26         p = p->next;
27     }
28     return 0; 
29 }

这是 lab3.3 中的代码,分别用 .h.c 文件完成声明和定义,从而将数据结构和它的操作与菜单业务逻辑分开处理。

 

3. 可重用接口

接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。对于使用接口的一方来说,不需要去了解其内部的实现逻辑,而只需要知道如何使用接口即可。

一般来说,接口规格包含五个基本要素

  • 接口的目的;

  • 接口使用前所需要满足的条件,一般称为前置条件或假定条件;

  • 使用接口的双方遵守的协议规范;

  • 接口使用之后的效果,一般称为后置条件;

  • 接口所隐含的质量属性。

给Linktable增加回调(call back)机制的接口,需要两个函数接口:一个是Call in 方式函数,其有一个参数为函数,这个作为参数的函数就是 call back 函数。

结合 menu 项目中的例子进行分析:

 1 /*
 2   * Search a LinkTableNode from LinkTable
 3   * int Conditon(tLinkTableNode * pNode);
 4   */
 5  tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))
 6  {
 7      if(pLinkTable == NULL || Conditon == NULL)
 8      {
 9          return NULL;
10      }
11      tLinkTableNode * pNode = pLinkTable->pHead;
12      while(pNode != pLinkTable->pTail)
13      {    
14          if(Conditon(pNode) == SUCCESS)
15          {
16              return pNode;                   
17          }
18          pNode = pNode->pNext;
19      }
20      return NULL;
21  }

在这个例子中,SearchLinkTableNode 函数即为 call in 方式函数;Conditon 函数即为 call back 函数。利用好传入的 condition 函数,这个查询接口的功能就变得更加通用,是为可重用接口。

定义通用接口的基本方法:

  • 参数化上下文:通过参数来传递上下文的信息,而不是隐含依赖上下文环境;

  • 移除前置条件:即放松对形参形式的紧约束;

  • 简化后置条件。

 

4. 可重入函数和线程安全

可重入函数可以由多于一个任务并发使用,而不必担心数据错误,相反,非可重入函数不能。

可重入函数的基本要求:

  • 不为连续的调用持有静态数据;

  • 不返回指向静态数据的指针;

  • 所有数据都由函数的调用者提供;

  • 使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;

  • 使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突;

  • 绝不调用任何不可重入函数。

线程安全:如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,那么我们就说这是线程安全的。

二者的关联:不可重入函数一定不是线程安全的,可重入函数不一定是线程安全的。(不同的可重入函数在多个线程中并发使用时会有线程安全问题)

在 menu 项目中分析:

 1 /*
 2  * Add a LinkTableNode to LinkTable
 3  */
 4 int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
 5 {
 6     if(pLinkTable == NULL || pNode == NULL)
 7     {
 8         return FAILURE;
 9     }
10     pNode->pNext = NULL;
11     pthread_mutex_lock(&(pLinkTable->mutex)); // 加锁
12     if(pLinkTable->pHead == NULL)
13     {
14         pLinkTable->pHead = pNode;
15     }
16     if(pLinkTable->pTail == NULL)
17     {
18         pLinkTable->pTail = pNode;
19     }
20     else
21     {
22         pLinkTable->pTail->pNext = pNode;
23         pLinkTable->pTail = pNode;
24     }
25     pLinkTable->SumOfNode += 1 ;
26     pthread_mutex_unlock(&(pLinkTable->mutex)); // 解锁
27     return SUCCESS;        
28 }

如上图的函数,在临界区前后,要进行加锁和解锁操作,以确保多线程下的线程安全。

 

三、总结

通过这一次作业,加强了我对软件工程重要性的理解,并在其中学到了不少以前忽视和看清的知识,包括代码的风格规范、模块化编程、可重用接口和线程安全等等,这些都是在项目开发中能够减轻工作量、清晰开发路线的得力帮手。感谢孟老师的指导!

posted @ 2020-11-10 18:54  晚风吻尽荷花叶  阅读(131)  评论(0编辑  收藏  举报