C/C++编译调试环境配置
Click the Extensions view icon on the Sidebar (Ctrl/?+Shift+X). Configuring VS Code:
- tasks.json (build instructions)
- launch.json (debugger settings)
- c_cpp_properties.json (compiler path and IntelliSense settings)
代码最初是如何生长起来的?
??UML三巨头之一的Ivar Jacobson曾说“银弹不存在,我们需要的仅仅是明智的软件开发方法( smart software development ),软件必须从一个小的可运行的skinny system 开始,逐渐充实生长称为full-fledge 的成熟系统。” 我们就采用这个思路,从hello world 开始不断迭代调试使代码长的越来越像一个命令行的菜单小程序。写代码要小步快跑不断迭代,罗马不是一天建成的,不要期望一撮而就。 ??另外需要说明的是,做实际项目并不鼓励一开始就从头开始写代码,而是找已有的类似项目做对比分析,对开源代码做逆向工程和再工程,对项目有深刻理解的基础上,再考虑是从头构建还是维护一个已有的项目来达成目标。
简约而不简单——代码规范和代码风格
代码风格的原则:简明、易读、无二义性
??一个项目代码的风格就如同一个人给人的印象,代码风格之所以那么重要,是因为它往往决定了代码是否规范、是否易于阅读。 ??代码虽然最终是要给机器看的,但毕竟还是面向程序猿们的编程,程序猿们是要陪伴整个项目开发过程的。在编写代码的过程中,尤其是在协作开发的过程中,如果对方的代码杂乱无章,读起来都费劲,更别说还需要在此基础上进一步开发,这对程序员来说是个巨大的挑战。 ??好的代码风格不仅易于代码的阅读和理解,还能在很大程度上减少一些不必要的语法错误,例如少了"}" ,如果在编码的时候严格遵循了花括号的对齐规则,那此类错误将容易被避免。
到底什么样的代码是好代码呢?
??一是规范整洁。遵守常规语言规范,合理使用空格、空行、缩进、注释等; ??二是逻辑清晰。没有代码冗余、重复,让人清晰明了的命名规则。做到逻辑清晰不仅要求程序员的编程能力,更重要的是提高设计能力,选用合适的设计模式、软件架构风格可以有效改善代码的逻辑结构,会让代码简洁清晰; ??三是优雅。优雅的代码是设计的艺术,是编码的艺术,是编程的最高追求。
代码风格规范总结
缩进:4个空格; 行宽:< 100个字符; 代码行内要适当多留空格,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。 在一个函数体内,逻揖上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行以示区隔; 在复杂的表达式中要用括号来清楚的表示逻辑优先级; 花括号:所有‘{’ 和‘}’ 应独占一行且成对对齐; 不要把多条语句和多个变量的定义放在同一行; 命名:合适的命名会大大增加代码的可读性;
- 类名、函数名、变量名等的命名一定要与程序里的含义保持一致,以便于阅读理解;
- 类型的成员变量通常用m_或者_来做前缀以示区别;
- 一般变量名、对象名等使用LowerCamel风格,即第一个单词首字母小写,之后的单词都首字母大写,第一个单词一般都表示变量类型,比如int型变量iCounter;
- 类型、类、函数名等一般都用Pascal风格,即所有单词首字母大写;
- 类型、类、变量一般用名词或者组合名词,如Member
- 函数名一般使用动词或者动宾短语,如get/set,RenderPage;
注释和版权信息:注释也要使用英文,不要使用中文或特殊字符,要保持源代码是ASCII字符格式文件; 不要解释程序是如何工作的,要解释程序做什么,为什么这么做,以及特别需要注意的地方; 每个源文件头部应该有版权、作者、版本、描述等相关信息。
编写高质量代码的基本方法
通过控制结构简化代码
- 代码的基本结构分为顺序执行、条件分支和循环结构,还有很多语言中支持的递归结构。我们要利用代码的基本结构特点来有效地梳理需求,从而写出思路清晰的代码。
通过数据结构简化代码 一定要有错误处理 - 程序的主要功能(80%的工作)大约仅用20%时间,而错误处理(20%的工作)却要80%的时间
性能优先策略背后隐藏的代价
??传统上由于CPU计算资源和存储资源较为昂贵,在编写代码时往往更多地考虑最大限度地高效利用计算机资源,因此在编写代码的习惯上追求性能优先的策略。但是随着计算资源的硬件成本逐步降低,尤其是云计算技术的发展计算资源的价格大幅度下降,性能优先的策略背后隐藏的代价逐步显露。
- cost to write the code faster。当软件工程师的人力成本远大于所消耗的计算资源成本时,提高代码编写的工作效率将更有价值;
- cost to test the code。质量保证的人力成本和质量保证的成效也比所消耗的计算资源成本更有价值;
- cost to understand the code。性能优先的策略往往会让代码很难理解,结果需要消耗更多的工时;
- cost to modify the code。面向机器的代码修改起来更困难,可扩展性差,同样会消耗更多工时。
??因此,我们在具体编程实现过程中已经不再需要考虑代码性能问题,将更多精力放在提高工作效率、质量保证、代码的可读性、可扩展性等方面,让性能问题在更高层的软件架构设计层面考虑更加合理有效
拒绝修修补补要不断重构代码
如果您觉得控制流程盘根错节、判定过程难以理解、或者无条件的分支难以消除,那么就该重新返回到设计了。重新检查设计,搞清楚您遇到的问题是设计中的固有问题,还是设计转化为代码的过程中引入的问题。 返回设计重新思考设计,使得设计结构和代码结构在逻辑上保持一致,而不是“头痛医头脚痛医脚”的方式对代码修修补补。不断重构代码是编写代码的基本方式。
模块化的基本原理
??模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离(SoC, Separation of Concerns), 从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。 整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。 软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度.
耦合度(Coupling)
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合。
内聚度(Cohesion)
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。 理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
模块化代码的基本写法
命令行菜单在开源社区中常见的写法 将数据结构和它的操作与菜单业务处理进行分离处理,尽管还是在同一个源代码文件中,但是已经在逻辑上做了切分,可以认为有了初步的模块化。 进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。
软件设计中的一些基本方法
KISS(Keep It Simple & Stupid)原则
- 一行代码只做一件事
- 一个块代码只做一件事
- 一个函数只做一件事
- 一个软件模块只做一件事
使用本地化外部接口来提高代码的适应能力
先写伪代码的代码结构更好一些
- 设计通常为程序提供了一个框架,程序员需要用自己的专业知识和创造性来编写代码实现设计。在从设计到编码的过程中加入伪代码阶段要好于直接将设计翻译成实现代码。
- 因为伪代码不需要考虑异常处理等一些编程细节,最大限度地保留了设计上的框架结构,使得设计上的逻辑结构在伪代码上体现出来。从- 伪代码到实现代码的过程就是反复重构的过程,这样避免了顺序翻译转换所造成的结构性损失。因此,先写伪代码的代码结构会更好一些。
- 另外我们也要有意识地用设计上的逻辑结构给代码提供一个编写框架,避免代码的无序生长,从而破坏设计上的逻辑结构。
消费者重用和生产者重用
Consumer Reuse:消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。软件开发者在重用已有的软件模块代码时一般会重点考虑如下四个关键因素:
- 该软件模块是否能满足项目所要求的功能;
- 采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作;
- 该软件模块是否有完善的文档说明;
- 该软件模块是否有完整的测试及修订记录;
Producer Reuse:我们清楚了消费者重用时考虑的因素,那么生产者在进行可重用软件设计时需要重点考虑的因素也就清楚了,但是除此之外还有一些事项在进行可重用软件设计时牢记在心,我们简要列举如下: - 通用的模块才有更多重用的机会;
- 给软件模块设计通用的接口,并对接口进行清晰完善的定义描述;
- 记录下发现的缺陷及修订缺陷的情况;
- 使用清晰一致的命名规则;
- 对用到的数据结构和算法要给出清晰的文档描述;
- 与外部的参数传递及错误处理部分要单独存放易于修改;
接口的基本概念
??尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。 ??这时进一步优化这个初步的模块化代码就需要设计合适的接口。定义接口看起来是个很专业的事情,其实在我们生活中无处不在,比如我们看的电视剧中“天王盖地虎,宝塔镇河妖”就是黑社会接头定义的接口,比如两个人对话交流沟通使用的就是汉语普通话或标准英语这么一个接口规范。 ??接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。 ??在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。 ??接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:
- 接口的目的;
- 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
- 使用接口的双方遵守的协议规范;
- 接口使用之后的效果,一般称为后置条件;
- 接口所隐含的质量属性。
可重用软件模块的接口设计范例
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
- 该接口的目标是从链表中取出链表的头节点,函数名GetLinkTableHead清晰明确地表明了接口的目标;
- 该接口的前置条件是链表必须存在使用该接口才有意义,也就是链表pLinkTable != NULL;
- 使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;
- 使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;
- 该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;
#微服务的概念
由一系列独立的微服务共同组成软件系统的一种架构模式; 每个微服务单独部署,跑在自己的进程中,也就是说每个微服务可以有一个自己独立的运行环境和软件堆栈; 每个微服务为独立的业务功能开发,一般每个微服务应分解到最小可变产品(MVP),达到功能内聚的理想状态。微服务一般通过RESTful API接口方式进行封装; 系统中的各微服务是分布式管理的,各微服务之间非常强调隔离性,互相之间无耦合或者极为松散的耦合,系统通过前端应用或API网关来聚合各微服务完成整体系统的业务功能。 微服务架构的基本概念可以简单概括为通过模块化的思想垂直划分业务功能,传统单体集中式架构和微服务架构如下图示意
RESTful API
REST即REpresentational State Transfer的缩写,可以翻译为”表现层状态转化”。有表现层就有背后的信息实体,信息实体就是URI代表的资源,也可以是一种服务,状态转化就是通过HTTP协议里定义的四个表示操作方式的动词:GET、POST、PUT、DELETE,分别对应四种基本操作: GET用来获取资源; POST用来新建资源(也可以用于更新资源); PUT用来更新资源; DELETE用来删除资源。
接口与耦合度之间的关系
更细致地对耦合度进一步划分的话,耦合度依次递增可以分为无耦合、数据耦合、标记耦合、控制耦合、公共耦合和内容耦合。这些耦合度划分的依据就是接口的定义方式,我们接下来重点分析一下公共耦合、数据耦合和标记耦合。
- 公共耦合:当软件模块之间共享数据区或变量名的软件模块之间即是公共耦合,显然两个软件模块之间的接口定义不是通过显式的调用方式,而是隐式的共享了共享了数据区或变量名。
- 数据耦合:在软件模块之间仅通过显式的调用传递基本数据类型即为数据耦合。
- 标记耦合:在软件模块之间仅通过显式的调用传递复杂的数据结构(结构化数据)即为标记耦合,这时数据的结构成为调用双方软件模块隐含的规格约定,因此耦合度要比数据耦合高。但相比公共耦合没有经过显式的调用传递数据的方式耦合度要低。
通用接口定义的基本方法
参数化上下文 通过参数来传递上下文的信息,而不是隐含依赖上下文环境,因此我们可以重新定义sum函数的接口如下代码:
移除前置条件 参数化上下文之后,我们发现这个接口还是有很大的局限性,就是在调用这个接口时有个前提,就是你有三个数,不是两个数,也不是5个数。必须有三个数就是前置条件。将这个前置条件移除掉,那就是我们可以求任意个数的和。 这个接口显然更通用了,既参数化了上下文又移除了原来的只能三个数求和的约束,但是又增加了一个约束条件,就是len的数值不能超过numbers数组定义的长度,否则会产生越界。后置条件也较为复杂,可能是只对numbers数组前len个数求和,所以后置条件不仅是返回值,还隐含了这个返回值是numbers数组前len个数的和。 简化后置条件 如果编程语言支持直接获得数组的个数,或者通过分析数组数据智能得出数组的个数,我们可以进一步移除前置条件len与numbers数组长度之间的约束关系,这样后置条件变为numbers数组所有元素的和,更加简单清晰。
可重入函数与线程安全
线程的基本概念
函数调用堆栈
可重入函数
概念
??可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
可重入函数的基本要求
- 不为连续的调用持有静态数据;
- 不返回指向静态数据的指针;
- 所有数据都由函数的调用者提供;
- 使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;
- 使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突;
- 绝不调用任何不可重入函数。
什么是线程安全?
??如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 ??线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
函数的可重入性与线程安全之间的关系
- 可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;
- 不可重入的函数一定不是线程安全的。
Linktable软件模块的线程安全分析
所有的函数是不是都是可重入函数 不同的可重入函数有没有可能同时进入临界区
看待软件质量的几个不同角度
- 产品的角度,也就是软件产品本身内在的质量特点;
- 用户的角度,也就是软件产品从外部来看是不是对用户有帮助,是不是有良好的用户体验;
- 商业的角度,也就是商业环境下软件产品的商业价值,比如投资回报或开发软件产品的其他驱动因素。
软件设计的方法和原则
设计方法论: 不断地重构 几个重要的设计指导原则:
- Modularity
- Interfaces
- Information hiding
- Incremental development
- Abstraction
- Generality
|