一、要记得重构重要性和基本的设计原则
高内聚低耦合MVC架构插件化微内核化架构,依赖倒置接口原则/接口隔离原则,开放关闭原则/里氏原则,迪米特法则/组合优先,单一职责原则。
代码需要不断重构 架构分离MVC,接口提取,类的抽象,分解组合,函数提取去掉重复冗余,通信尽量集中简单。重复冗余,结构混乱,通信混乱需要消耗巨大资源和陷入泥潭漩涡需要时刻警惕和规范化和不断重构。函数简单只做一件事情,健壮可靠,可读,可维护,可复用,可拓展,可测试。
第一代架构实现为主,第二代架构重构分层设计横向解耦,第三代架构设计模式面向接口设计插件化设计横向纵向解耦,第四代架构微内核或多内核引擎结构+组件插件化设计内核可以放置在客户端组件插件放置在服务器。
无论是接口,类,函数,变量都应该只做一件事,就是单一职责。
二、编码规范细节
1.文件包含
用class XXX前置声明减少头文件依赖,在.cpp中引入方便支持相互调用,头文件的引入顺序很重要先自身.h文件,然后CC++库,接着第三方库,接着自己的模块头文件。
2.健壮性安全性检查
多线程中可重入数据的加锁或信号量,函数参数合法性检验由被调用者或调用者维护。
3.命名空间的使用
匿名命名空间在cpp中,头文件中必须要命名的命名空间且在include和全局声明之后,命名空间用using 具体命名空间类变量和函数,除了std以外不用用全局 using namespace,也可以用命名空间别名,合理使用命名空间可以有效的避免命名冲突特别是多人协作开发时候。一个命名空间下可以嵌套其它命名空间类似irrlich中。
4.内嵌类使用
内嵌类在和外部类关系很密切时候用,一般是类似结构体一样使用的。
5.过程设计的灵活性优势和管理好
静态成员,内联成员,共用函数可封装为独立的类,这样整个框架更加清晰,更少冗余。
6.变量声明和初始化设计
局部变量声明时候需要初始化,尽可能就近原则声明,类对象有时候要恰当处理避免构造函数不停调用,gcc和vc都是支持循环作用域限定的。
7.全局对象不能使用/只能用全局基本类型和全局指针
全局变量内建类声明时候要初始化;非內建类型一般依赖于其它类,其它系统资源如设备句柄网络数据库不能声明为全局变量,若一定要使用要声明为全局指针的单件模式;多线程下不要声明非常数的全局变量,交互数据通过局部对象new出来的缓存进行数据同步访问。字符串全局常量需要用c语言风格; 因为stl类对象调用顺序也是不可控的;所有全局变量都是可以封装到命名空间,放到cpp类中,或者类静态数据中的。
8.构造函数不能做太多事情/虚函数不能放置在构造函数中
类实质的初始化不要写在构造中,因为异常,可能失败,还有构造函数中虚函数不会得到正确的调用即使父类不实现。默认构造必须写,否则不会初始化且数组调用会有问题。子类的父类实现了且子类没有数据成员那么可以不用实现。
9.explict好习惯和赋值拷贝构造函数一般禁止
单参构造需要explict声明。
赋值拷贝构造函数大多数应该禁止掉,即使stl需要用到但是stl可以存指针。
10.应该优先使用组合复用,然后才是继承复用
应该优先考虑组合,只有is a关系时候才使用继承,或者接口协议框架时候才继承。继承都应该public的。
11.继承不要写父类普通函数/需要写虚函数virtual关键字和virtual析构函数
继承后需要注意,不能重写父类非虚函数,只能添加函数和实现虚函数,实现时候也要保持virtual目的是为了阅读者方便。父类析构函数都应该是虚的。
12.多继承不提倡,除非是实现多接口
多继承一般不使用,除非在接口继承中,继承一个父类和一个接口协议类。
13.接口只有纯虚函数,虚析构函数,不允许非静态非虚函数
接口,一般接口只包含纯虚函数,虚析构函数和可以有protected的无参构造函数,静态成员函数和数据都可以但非静态非虚成员都是不允许的(除了构造函数)。
面向接口编程中不仅要面向接口,同样要考虑实现。
14.运算符重载不要轻易去做,用成员函数是更好的做法
运算符重载,一般类型不要去做而是用函数代替,一些stl容器结构体中在声明容器时候也可以用仿函数代替,若要用应说明原因,基于重载异议的原因define typedef也应该少用。
15.数据成员需要私有化,提供内联存取函数,简化赋值点,方便定位问题
类的数据成员要私有,目的是为了添加内联的存取控制函数,统一赋值的点,方便定位问题归一化,而不是到处赋值,这个要强制形成习惯,不然害人害己。
赋值语句要非常谨慎,不要在函数传参时候传入表达式和计算函数,不要在表达式中做多次计算一个表达式应该只进行一种计算,使用公共全局变量函数传入函数参数不应该函数参数压栈顺序。
16.智能指针用scrop_ptr和share_ptr
关于智能指针,尽量不要使用指针,要用局部需要scrop_ptr,全局需要用boost share_ptr,不要使用auto_ptr。
17.规范化函数的输入输出,常量引用作为输入,指针参数作为输出
一般引用都是用常量引用用于输入参数,指针做为输出参数,stl容器中不支持引用需要用指针。存取函数很重要,函数参数传递也很重要,包括赋值,父类子类间也需要更加简单化。能使用常量的尽快使用常量包括常量函数,但是不应该过度使用常量比如常量指针(是指针指向常量,可以p++)和指针常量(是常量有一个指针可以*p赋值),返回常量的函数只是为了避免被作为左值更加明确。常量写在类型前面,更加突出常量的含义。
18.函数成员要简化,不要太多二义的成分
函数重载要有明确的使用目的且避免实现冗余。
少用函数默认参数。
19.不要用alloca,如果要使用要非常小心
不用变长数组,alloca否则会导致莫名其妙的内存泄露。
20.函数参数合法性检验,统一在调用者,还是被调用者( 倾向被调用者, 因为调用者太多 )
函数参数合法性检验,需要被调用检验传入的参数,调用者需要检验被调用者的返回值,一般一个函数既是被调用者也是调用者(非参数列表输入数据),健壮性和去除冗余的要求。
21. 模块/函数扇入扇出要合理,太深或者太浅都是有坏味道的
函数,类,接口应该保持3-5左右的扇出,公共层较多的扇入,扇出是主动调用,扇入是被调用。扇出太少会导致深度太深,扇出太多说明层次太少,扇出太少或太多都需要重构;扇入没有要求看需求。
22. 友元类或者函数需要在同一个文件中,避免跳来跳去阅读
友元的定义必须在同一个类中,避免到处跳转,一般委托或组合紧密类中使用。
23.异常需要少用
异常使用会增大代码和处理复杂,简单异常可以使用,复杂抛出多个异常不要使用用,google代码中禁止使用。
24.类型转换用cpp风格,不要用C风格
类型转换,使用cpp风格的,不要用c风格的转换,dynamic_cast和RTTI都不要使用 一个是效率问题 一个是说明设计有问题,使用接口虚函数统一接口或者子类提供相应的查询对象实现来做,或者使用双分发visitor设计模式。
25.指针使用NULL, 整型使用0
指针使用NULL,不使用0
26.sizeof变量名更好,因为类型如果改变就会有问题(例如子类)
sizeof 变量名比类型好 因为类型会改变。读取二进制文件不能用strlen需要用sizeof。
27.命名规则是最重要的规则,需要统一
命名规则是最重要的规则:
1.需要统一命名方式,无论是unix风格还是windows风格,新增和重构代码需要和原有代码风格统一。
2.明确含义,不能过于缩略。
3.变量要名词,函数动词。
4.文件路径和工程名称需要清楚明确。
28.注释要简单明了,二义性和无用的注释宁愿不要
注释:简单的命名好的代码函数不用注释;复杂的代码函数注释要说明使用原因条件而不是代码翻译无用注释,一定避免二意性。
对外接口函数一定明确的注释清楚,全局函数,包括默认参数和多态函数,设计思想,使用方法,和约束条件。
类和模块头要写明作者和模块设计思路和使用,以及约束条件。
注释还要注意格式,比如利于导出文档。
TODO注释
临时的或未完成的或者做的不够好,方便查找 开始干活。
TODO(jiayuan qq):为何这样做,及将来需要怎么做。
29.缩进要明确
编程格式,明显的运算符表达式不用空格分开例如括号指针地址位运算,不明显的要空格分开例如逻辑运算比较,多变量声明等,其它缩进简单明显就好。
30.警告全部视为错误,用pc-lint检查代码
编译警告需要全部设置视为错误,如果不能去掉的需要单个文件中设置编译选项注明原因,需要pclit工具持续集成确保软件的质量,设计代码要输入输出明确,可测试性。
31.函数参数要规则化
函数参数全部一行或者单个一行,且要紧凑,无论声明实现还是调用。
32.类声明需要按顺序,对外的public,对内的private,或许需要继承的用protected需要简单明了
类的声明public protected private原因是使用该类的人一般关注外部接口就可以了,需要从使用者出发。
类声明要写分号结束,不然会报其它莫名奇妙的错误。
33.统一空白风格,重要的才需要空白下
空行括号这些,应该越少越好,但是为了突出或是统一风格更加重要,编码风格一致性包括原来代码的一致性,花些时间阅读项目中别人的代码,统一风格,即使再优秀的风格破坏原有的风格也是不对的,更加专注于实现而不是风格。
34.魔鬼数字需要注释,否则需要用命名常量
向函数传入参数用到非命名常量需要注释,除非用命名常量且命名明确常量本身的含义。
35.宏定义需要括号,需要do{} while来处理
宏定义,一定要用括号括起参数,大括号处理多行语句但是大括号不替换if else调用会有问题,用do{}while(0)可以完美处理,宏中传入参数不要改变否则会有问题。
36.内存越界和边界值要充分检查和积累经验
避免数组堆栈内存越界,要做好检查,包括字符串的\0预留空间,使用安全的字符串操作函数snprintf代替sprintf vsprintf,用strncpy strncat代替无n的,用fgets代替gets。用sizeof和strlen+1来代替手工计算字符串长度。
37.避免内存泄露(严格申请释放(特别重新分配时),函数参数返回资源,多维结构体资源,继承体系中虚析构,stl string vector容器
内存和资源(文件网络数据库,设备句柄,线程间进程间信号量锁),1.不使用和重复分配资源时候需要释放; 2.函数传出资源的释放;3.结构体容器中持有资源的全部释放,及引用计数释放; 4.stl本身string vector,或者其它语言,或者自定义的容器,注意内存持有要释放;5.继承体系中的资源释放,虚析构函数,构造函数本身失败时候不要有虚函数,拷贝构造和赋值函数禁止,或者采用引用计数管理内存。
38.不同模块间使用指针要非常小心,用智能指针引用计数或者不用指针才能保证
避免使用野指针,要形成好习惯,释放资源后指针赋值为NULL,使用时候进行非NULL判断;容器中保存指针要特别小心除非指向的资源不会释放或者只有容器管理否则不要存放其它模块的指针;引用计数类型的一定要计数增减匹配否则会导致问题,不同模块间持有对方指针时候需要引用计数确保内存安全。
39.边界值要非常小心,屡试不爽
要非常注意差1情况,边界值情况,每个分枝要考虑到测试到,否则很容易导致错误发生,特例情况,例如文件大小为0,字符串\0,注意数据类型的溢出。
40.需要从细节,又要从程序整体来把控逻辑错误问题
需要注意内部调用机制,外部调用机制,多线程执行,不断process执行情况,事件回调执行,进程间通信,系统调度,系统缓存,网络数据库配置设置等条件,避免大型系统时候考虑不周全,文件句柄超越上限,堆栈耗尽,线程死锁,网络限制不稳定等情况。
41.先编写正确的代码再去优化,要从数据格式上,IO上,内存上,CPU上,GPU上减少计算或不计算,提前计算,推后计算,重用计算结果,更少的计算,更多的线程上结合性能分析工具去解决性能问题
程序效率,简单正确的代码提高效率远比复杂高效率的代码改正确简单,去掉不必要的判断和计算,高频执行的提前计算存放结果和低频计算的推迟计算当需要时候才计算;采用代替计算方案会更加快比如贴图光照,快速纹理加载和快速文件读取,多线程技术,合理安排资源调度例如美术资源合并变大且规则化,常驻显存; 采用内存池和线程池可以有效提高效率。