代码中的软件工程
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 }
如上图的函数,在临界区前后,要进行加锁和解锁操作,以确保多线程下的线程安全。
三、总结