C++大型项目开发约束
第一章
简介
大型软件项目通常由相应的大型开发团队承担。大型团队生成的代码要有项目范围内可评测的质量,代码必须遵从于某一标准并以此来评价。因此,对大型的项目团队来说,建立一个编程的标准或一组指南很重要。
使用编程标准也使以下各项成为可能:
- 增加开发过程代码的强壮性、可读性、易维护性;减少有经验和无经验开发人员编程所需的脑力工作;
- 在项目范围内统一代码风格;
- 通过人为以及自动的方式对最终软件应用质量标准;
- 使新的开发人员快速适应项目氛围;
- 支持项目资源的复用:允许开发人员从一个项目区域(或子项目团队)移动到另一个,而不需要重新适应新的子项目团队的氛围。
本文的目的是表述 C++ 编程的规则、指南和提示(通常也称之为指南),它们可用来作为编程标准的基础。对工作在大型项目团队的软件工程师,这些都是需要的。
当前版本专注于程序的编制(虽然现在还很难在设计和编程间划定明确的界限);以后将会添加设计指南。
指南包括了以下 C++ 开发的方面:
- 如何组织项目代码;
- 编程风格(如何编写实际的源代码);
- 如何记录源代码;
- 代码内名称和源文件所使用的命名约定;
- 何时使用某些语言结构以及何时应避免某些语言结构。
它们从大量的行业知识中搜集而来。(请参见参考文献:作者及参考文献。)它们基于:
- 广为人知的软件原理;
- “好的”软件实现;
- 所学课程;
- 主观意愿。
大多会基于几个第一种类型,也会基于大量的第二种和第三种类型。不幸的是,某些也基于最后一种类型;这主要是因为编程是一个高度主观的活动:编程没有普遍接受的最好或正确的万灵药。
基本原则
前提
指南分类
要求或限制:
以上符号确定的一个指南就是一个要求或限制;不遵从它肯定会导致坏的、不可靠的或不可移植的代码。没有弃权就不能违反要求或限制
最根本的原则
使用常识
当无法找到可用的规则或指南时;当规则明显不适用时;或当其他都已失败时:使用常识,并核查基本原则。这条规则胜过其它所有的规则。即使存在规则或指南时也需要常识。
第二章
代码组织与风格
本章提供程序结构与层次的指导。
代码结构
不同文件中放置模块规约与实现
File_Name::=
<Module_Name> [<Separator> <Part_Name>] '.'<File_Extension>
- 为了更好的预测,对文件名使用相同的大小写,这与代码内名称的使用相同。
以下是模块划分与命名策略的示例:
- module.inlines.cc - 如果一个模块有多个潜在的内嵌函数,就将函数的定义置于一个单独的
内嵌
文件中(请参见“将模块内嵌函数定义置于单独文件中”)。 - module.private.hh - 如果模块有许多常用的被其他部分引用的私有实现声明,就把这些声明分隔出去组成一个私有部分,它可由其他实施文件包含。
- module.private.cc - 模块的私有实施函数定义,为编辑的方便将它分离出去。
- module.function_name.cc - 如果可执行的大小是个要考虑的问题,应当分离出许多程序不需要的特殊成员函数,组成各自的实施文件(请参见“如果程序大小需要考虑,将大型模块分成多个变换单元”)。如果重载的函数置于不同文件中,每一文件函数名都应有一实例数字的后缀。如,function_name1 表示第一个独立重载函数实例。
- module.nested_class.cc - 模块嵌套类的成员函数,置于其本身文件中。
- module.test.[hh/cc] - 如果一个模块需要扩展测试代码,则测试代码必须在友测试类中予以声明。友测试类应称为
Module.Test
。将测试代码声明为友类有助于模块及其测试代码的独立开发;这还允许测试代码从最终模块对象代码中删除而源并不改变。 - module.platform_name.cc - 分离出任意模块的平台依赖性并以平台名称命名部分名称(请参见“分离平台依赖性”)。
选择一个模块划分和命名的机制,使用它要前后一致。
示例
SymaNetwork.hh //包括类 “SymaNetwork”的声明。
SymaNetwork.Inlines.cc //内嵌定义子单元
SymaNetwork.cc //模块的主要实施单元
SymaNetwork.Private.cc //私有实施子单元
SymaNetwork.Test.cc //测试代码子单元
基本原理
从模块的实施中分离出其规约有助于用户和提供者代码的独立开发。
将一个模块的实施分割成多个变换单元,这可以更好的支持删除对象代码,并会导致更小的可执行大小。
使用规范化的文件命名与划分约定允许在不检查模块的实际内容时理解其内容与组织。
将名称从代码让渡给文件名增加了可预测性,有助于建立基于文件的工具而不需要复杂命名映射 [Ellemtel, 1993]。
只选择一组文件扩展名以区分头文件和实施文件
常用的文件扩展名是:对头文件,为.h、.H、.hh、.hpp
和.hxx
;对实施文件,为.c、.C、.cc、.cpp
和.cxx
。选择一组扩展名,使用它要前后一致。
示例
SymaNetwork.hh //扩展名“.hh”用来指定一个“SymaNetwork”模块的头文件。
SymaNetwork.cc //扩展名“.cc”用来指定一个“SymaNetwork”模块的实施文件
注意
对由名字空间封装的头文件,C++ 草拟标准工作底稿也使用扩展名“.ns”。
每个模块规约避免定义多个类
只有在极少数情况下多个类才能置于同一个模块中;此时他们必须是紧密关联关系的(如容器与它的迭代程序)如果所有类都需要对客户模块是可见的,可以把模块的主要类及其支持类置于同一个头文件中。
基本原理
减少模块的接口以及其他模块对此接口的依赖。
避免将私有实施声明置于模块的规约中
示例
//模块 foo 的规约,包含于文件“foo.hh”中;//class foo{.. 声明};//“foo.hh”结束;
//模块 foo 的私有声明,包含于文件“foo.private.hh”中;所有 foo 实施文件都使用它。... 私有声明
//“foo.private.hh”结束;
//模块 foo 的实施,包含于以下多个文件中// “foo.x.cc”和“foo.y.cc”
//文件“foo.x.cc”;
//#include "foo.hh" //包含模块本身的头文件
#include "foo.private.hh" //包含实施//所需的声明。... 定义
//“foo.x.cc”结束
//文件“foo.y.cc”;
//#include "foo.hh"#include "foo.private.hh"... 定义
//“foo.y.cc”结束
通常使用 #include
来访问模块的规约
示例
//模块 foo 头文件中的规约
// "foo.hh"//class foo{... 声明};
//“foo.hh”结束;
//模块 foo 在文件“foo.cc”中的实施;
//#include "foo.hh" // 实施文件包含//它本身的规约... foo 成员的定义
//“foo.cc”结束
#include
规则的一个例外是当模块只通过引用(使用指针或引用类型声明)使用或包含提供者模块的类型(类);这种情况下引用或包含通过使用预先声明来规约(请参见“减小编译依赖”),而不使用 #include
指令。
只包含绝对需要的文件:这意味着模块头文件不应包含只有模块实施需要的其他头文件。
示例
#include "a_supplier.hh"class needed_only_by_reference;//对只需要指针或引用的类 //使用 //预先声明来访问void operation_requiring_object(a_supplier required_supplier, ...);操作需要一个实际的提供者对象;//提供者规约必须已有 #include 指令。void some_operation(needed_only_by_reference& a_reference, ...);某些操作只需要对对象进行引用;//因此为提供者使用预先声明。
基本原理
这一规则保证了:
- 只存在唯一的模块接口声明,所有的客户只看到同一接口;
- 模块间的编译依赖最大程度的减小了;
- 客户不会为代码无故产生不需要的编译开支。
将模块内嵌函数的定义置于单独的文件中
当模块有许多内嵌函数时,它们的定义应置于一个分离的只有内嵌函数的文件中。在模块头文件的末尾应包含内嵌函数文件。
请参见“使用 No_Inline
条件编译符破坏内嵌编译”。
基本原理
这种技术使实施细节不会聚集到模块头文件中;因此,保留了一个清晰的规约。当不进内嵌编译时,它也有助于减少代码复制:使用条件编译,可将内嵌函数编译成一个对象文件而不是静态的编译成每一个使用的模块。相应的,内嵌函数定义不应在类定义中定义,除非它们非常琐碎。
如果程序规模是个要求考虑的问题,就把大的模块分割成多个变换单元
基本原理
链接器在对象文件中消除无引用代码的能力各不相同。将大型模块分割成多个文件允许这些链接器通过消除整个对象文件中的链接来减少可执行大小 [Ellemtel, 1993]。
注意
先考虑模块是否能分割成小的抽象,这也是很值得的。
分离平台依赖性
从独立于平台的代码中分离出依赖于平台的代码;这有助于提高可移植性。依赖于平台的模块应当具有受平台名限制的文件名以突出对平台的依赖。
示例
SymaLowLevelStuff.hh //“LowLevelStuff” 规约
SymaLowLevelStuff.SunOS54.cc // SunOS 5.4 实施
SymaLowLevelStuff.HPUX.cc // HP-UX 实施
SymaLowLevelStuff.AIX.cc // AIX 实施
注意
从构架与维护角度,将对平台的依赖包含在少数几个低层子系统中也是一个好方法。
采纳一个标准文件内容结构,使用它要前后一致。
建议文件内容就构包括以下次序的以下部分:
1. 重复包含保护(只适用于规约)。
2. 确定可选文件与版本控制。
3. 本单元所需包含的文件。
4. 模块纪录(只适用于规约)。
5. 声明(类、类型、常量、对象、函数)和其他文本规约(前置及后置条件,常量)。
6. 对这种模块内嵌函数定义的包含。
7. 定义(对象与函数)和私有实施声明。
8. 版权声明。
9. 可选版本控制历史。
基本原理
以上文件的内容次序,首先呈现给了客户相关的信息;这与安排类的公共、保护和私有部分次序的基本原理相一致。
注意
根据公司的政策,版权信息应置于文件的首部。
防止重复文件包含的保护
#if !defined(module_name) //使用预处理符
#define module_name //防止重复 //包含...//声明到此
#include "module_name.inlines.cc" //可选的内嵌结构//包含到此。//包含模块内嵌函数之后//没有其他声明。
#endif
//module_name.hh 结束
对包含保护符使用模块文件名。对模块名与操作符使用同一大小写。
使用"No_Inline
"条件编译符破坏内嵌编译
使用以下条件编译结构控制可内嵌函数的内嵌(相对于外部的)编译。
//在文件 module_name.inlines.hh 首部,
#if !defined(module_name_inlines)
#define module_name_inlines
#if defined(No_Inline)
#define inline //使内嵌关键词无效
#endif... //内嵌定义到此
#endif
//文件 module_name.inlines.hh 结束
//文件 module_name.hh 尾//
#if !defined(No_Inline)
#include "module_name.inlines.hh"
#endif
//包含module_name.hh之后
// module_name.cc 文件首//
#if defined(No_Inline)
#include "module_name.inlines.hh"
#endif
基本原理
以上技术允许内嵌函数在外部编译时的精简代码复制。使用条件编译,内嵌函数的简单复制编译成了定义模块;而当由编译器开关规约外部编译时,所复制的代码在每个使用的模块里编译成“静态”(内部链接)函数。
注意
条件编译增加了维护构建依赖关系的复杂性。对这种复杂性的管理通常是利用把头文件和内嵌函数定义视为一个逻辑单元:实施文件因此依赖于头文件以及内嵌函数定义文件。
代码风格
对嵌套语句使用小的、一致的缩进风格
示例
if (true){
//新程序块foo();
//程序块内语句 //缩进两个空格
}
else{
bar();
}
while (expression){statement();}
switch (i){
case 1:do_something();//相对 switch 语句本身 //缩进
break; //一层
case 2://...
default://...}
基本原理
能够轻易识别程序块,还要在不超出显示屏或打印页面界限的前提下有足够多的嵌套,使用两空格的缩进是一个折衷的方案。
相对于函数名或作用域名缩进函数参数
示例
void foo::function_decl( some_type first_parameter,
some_other_type second_parameter,
status_type and_subsequent);
如果按照以上的指南会导致换行,或参数缩进的太多,则以函数名或作用域名(类、名字空间)缩进所有的参数,每一参数独占一行:
示例
void foo::function_with_a_long_name( //函数名是//不易看到的
some_type first_parameter,
some_other_type second_parameter,
status_type and_subsequent);
请参照下面的布置规则。
使用能够适合标准打印纸大小的最大行宽
应限制程序行的宽度以防止在使用标准(信纸)或缺省打印纸张进行打印时丢失信息。
注意
如果缩进使深层的嵌套语句处于太右边,语句又太长以致超出了右边的空白,这时就应当考虑将代码分成更小的更易管理的函数。
使用一致换行
当函数声明、定义、调用或 enum 声明中枚举操作符的参数列表不能置于一行,则将每一个列表元素置于单独的一行(请参见“相对函数名和作用域缩进函数参数”)。
示例
enum color { red, orange, yellow, green,
//...violet
};
如果一个类或函数模板声明太长,则在模板的实参列表后进行连续换行。例如(标准迭代程序库的声明,[X3J16, 95]):
template <class InputIterator, class Distance>
void advance(InputIterator& i, Distance n);
第三章
注释
本章对代码注释的使用提供指导。
注释应当作为源代码的补充,而不是直译源代码:
- 它们应当帮助读者掌握背景中的概念、依赖性、特别是复杂的数据代码和算法。
- 它们应当解释不能直接从源代码看出东西;它们不应复制语言的语法或语义。
- 它们应当突出:与代码或设计标准的不同点、受限特性的使用、以及特殊的“技巧”。
对每一行注释,程序员都应能够轻松回答:这一注释有何价值?通常,合理选择名称就可不要注释。除非注释出现在正规的程序设计语言中 (PDL),否则编译器不对其进行编译;因此,根据单点维护原则,应当以源代码的方式表达设计决策,而不是以注释的方式,即使这样需要更多的声明。
使用 C++ 风格的注释而非 C 风格的注释
应使用 C++ 风格注释分界符"//
",而非 C 风格的"/*...*/
"。
基本原理
C++ 风格的注释更易理解,它减少了由于偶然缺少一个注释结束分隔符而造成大量代码被注释掉的风险。
反例
/*注释开始,但缺少注释结束分隔符
do_something();
do_something_else(); /*对 do_something_else 的注释*/
//注释到此结束。 // do_something 和
// do_something_else
//都意外的被注释掉了!Do_further();
注释与源代码尽可能靠拢
注释应紧贴它们所要注释的代码;它们使用相同的缩进,使用一个空注释行接于代码之后。
对多行连续语句的注释应置于语句的上方作为语句的介绍性说明。而对单个语句的注释应置于语句的下方。
示例
//置于语句前的注释
//它对下面多个语句进行注释
// ...void function();//
//语句之后的注释
//它对前一个语句进行注释。
避免行末注释
注释应避免与源结构处于同一行:否则会使注释与其所注释的源代码不对齐。但在描述长声明中的元素(如 enum 声明中的枚举操作符)时,也能容忍这种不好的注释方式。
避免注释头
使用空注释行分离注释段
示例
//在下一段中//需要继续这里的解释空注释行使得//同一注释块中//不同段间的界限分明。
避免冗余
编写自记录代码而非注释
时刻注意要编写自记录代码而非注释。这可通过选择合理的名称、使用特殊的临时变量或重新安排代码的结构来实施。注意注释中的风格、语法和拼写。使用自然语言注释而不是电报或加密格式。
示例
替换如下代码:do{...}
while (string_utility.locate(ch, str) != 0); //当找到时退出查找循环。
将以上代码改写成:do{...found_it = (string_utility.locate(ch, str) == 0);}
while (!found_it);
记录类与函数
虽然自记录代码比注释好;但通常还需要提供一些超出解释代码复杂部分以外的信息。至少需要记录以下信息:
- 每个类的目的;
- 函数名不能清楚说明它的目的时,则记录函数相应的目的;
- 返回值的含义;如不可预测函数布尔返回值的含义:如,ture 值是否表示函数执行成功;
- 出现异常的条件;
- 如果有的话,参数的前置或后置条件;
- 其他数据访问,特别是当数据被修改时:对有副作用的函数特别重要;
- 合理使用类或函数所需的限制或其他信息;
- 对类型和对象来说,语言无法表达的任何不变量或其他限制。
基本原理
与声明相结合的代码记录应当足以使客户使用代码;因为只使用 C++ 不能完全表达类、函数、类型和对象的完整语义,所以需要记录存档。
第四章
命名
本章为不同 C++ 实体命名指南。
概述
选择一个名称约定,使用它要前后一致
- 主机使用 UNIX 且不常使用商业库(如 X Window 库、X Toolkit Intrinsics 和 Motif)的项目可能倾向于使用全部为小写字符、以下划线分隔的约定:这是 UNIX 系统调用以及 C++ 草拟标准工作底稿使用的约定。
- 以商业库为中心的 UNIX 主机项目可能更倾向于使用大写风格,通常也称为 Smalltalk 风格 - 一种首字母大写,字间直接相连而无分隔符的书写风格。
- Microsoft 基于 Windows 的项目可能会推荐使用并不常用的 Microsoft® “匈牙利”命名法我们不推荐使用这种命名风格,因为它违背了本文指南的基本原则。
注意:
细心的读者会发现本文的示例现在并未遵循所有的指南。这部分是因为示例的来源有多个,还因为希望减少篇幅。因此,没有严格遵照格式化的指南来做。但需声明:照我说的去做,不要学我的实际做法。
永远不要声明以一个或多个下划线 ('_') 开头的名称
避免使用只靠字母大小写才能区分的名称
只通过大小写才能区分的类型名称,它们间的差异是很难记忆的,也就很容易造成混淆。
避免使用缩写
避免使用后缀来表明程序语言结构
选择清晰的、易辨认的、有意义的名称
从应用的角度选择名称,名词使用形容词修饰来提高局部(具体上下文)的含义。确保名称与其类型保持一致。
选择合适的名称,使以下结构:
object_name.function_name(...);
object_name->function_name(...);
示例
void set_color(color new_color){
...the_color = new_color;...}优于
void set_foreground_color(color fg)和:oid set_foreground_color(color foreground);
{...the_foreground_color = foreground;...}
第一个示例中名称的选择要优于其它两个:对 new_color
进行了限定使它与其类型一致;因此增强了函数的语义。
第二个示例中,读者直觉上会认为 fg
意指 foreground(前景);但是,任何一个好的编程风格都不应留给读者作直觉推导的余地。
第三个示例中,当使用参数 foreground
(远离其声明)时,读者会认为 foreground
实际上就是指 foreground 的颜色。可以想象,它能代表任何一种可暗中转化为 color
的类型。
注意:
使用名词和形容词构成名称、确保名称与类型相符、遵循自然语言以增强代码的可读性和语义。
使用名称的正确拼写
英语字符名称部分应正确的拼写,遵守项目要求的形式,如使用一致的英国英语或美国英语,但不能同时使用。这对注释也同样适用。
布尔值使用正值谓词从句
对布尔值类型的对象、函数及函数实参使用正值形式的判定句式,如 found_it
、is_available
,但不使用 is_not_available
。
基本原理
当否定谓词时,双重否定更难以理解。
名字空间
使用名字空间由子系统或库划分潜在全局名称
基本原理
使用名字空间包含可能的全局名称,这样有助于子项目团队或厂商独立开发代码时避免名称冲突。这必然导致只有名字空间是全局的。
类
类的名称使用名词或名词短语
使用简单形式的常用名词或名词短语,为类取名时要表达出它的抽象含义。基类使用更通用的名称,而对派生类使用更专用的名称。
typedef ... reference; //出自标准库
typedef ... pointer; //出自标准库
typedef ... iterator; //出自标准库
class bank_account {...};
class savings_account : public bank_account {...};
class checking_account : public bank_account {...};
当对象和类型的名称冲突或缺少合适的名称时,对象使用简单名称,类型名称添加后缀 mode、kind、code
等。
表明是对对象集合的抽象时使用复数形式。
typedef some_container<...>yellow_pages;
当需要对象集合外的其他语义时,使用以下标准库中的数据结构作为行为模式和名称后缀:
- vector(向量) - 一种可随机访问的顺序容器;
- list(表) - 一种有序的顺序容器;
- queue(队列) - 一种先入先出的顺序容器;
- deque(双端队列) - 一种两个端口都可进行操作的队列;
- stack(堆栈) - 一种后入先出的顺序容器;
- set(集合) - 一种关键词访问(关联关系)容器;
- map(图) - 一种关键词访问(关联关系)容器;
函数
过程类型的函数名称使用动词
示例
void insert(...);
void erase(...);
Name first_name();
bool has_first_name();
bool is_found();
bool is_available();
不要使用否定意义的名称,因为这会导致双重否定表达式出现(如 !is_not_found
);这使得代码更难以理解。有些情况下,通过使用反义词,如“is_invalid
”代替“is_not_valid
”,可以使否定谓词变成肯定的而不需改变其语义。
示例
bool is_not_valid(...);
void find_client(name with_the_name, bool& not_found);
Should be re-defined as:bool is_valid(...);
void find_client(name with_the_name, bool& found);
当需要使用同一通用含义时,使用函数的重载
对象与函数参数
实参名使用语法元素强调其含义
为表明其唯一性或表明本实体是活动的主要核心,在对象或参数名称前加前缀“the
”或“this
”。要表明次要、临时、辅助对象,则加前缀“a
”或“current
”:
示例
void change_name( subscriber& the_subscriber,const subscriber::name new_name)
{...the_subscriber.name = new_name;...}
void update(subscriber_list& the_list,
const subscriber::identification with_id,
structure& on_structure,
const value for_value);
void change( object& the_object,const object using_object);
异常
异常名选用否定的含义
只有在处理错误时才必须使用异常,故使用名词或名词短语来清楚的表达一个否定的含义:
overflow, threshold_exceeded, bad_initial_value
异常名使用项目定义过的形容词
其他事项
浮点指数和十六进制数使用大写字母。
浮点数中的字母“E”和十六进制数从“A”到“F”应当一直保持大写。
第五章
声明
本章为不同 C++ 声明类型的使用及形式提供指南。
名字空间
将全局声明限定在名字空间中
这意味着只有名字空间的名称可以是全局的;所有其他的声明都必须在某个名字空间的作用域内。
忽略了这一规则可能最终导致名称冲突。
使用名字空间划分非类功能
逻辑上划分非类功能类的类别,或作用域远大于类的功能(如库或子系统),使用名字空间逻辑上统一的声明(请参见“使用名字空间由系统或库划分潜在的全局名称”)。
名称表达了功能的逻辑划分。
示例
namespace transport_layer_interface { /* ...*/ };
namespace math_definitions { /* ...*/ };
尽量不使用全局和名字空间范围的数据。
类
C++ 中类是基本的设计实施单元。使用它们纪录域与设计的抽象,并作为实施抽象数据类型 (ADT) 的封装机制。
使用 class
而不是 struct
来实现抽象数据类型
基本原理
区分 class
与 struct
的的一致做法引入了以上的语义差别,并且还超出了语言规则的范围:class
是纪录抽象与封装最先考虑的结构;而 struct
代表一个纯数据结构,它可以在混合编程语言程序中交换使用。
以可访问权限逐次降低的顺序声明类的成员
类声明中的访问限定符应以顺序 public, protected, private 出现。
基本原理
以 public、protected、private 顺序排列的成员声明保证了类用户最感兴趣的信息置于最前列,因此减少了类用户访问不相关信息或实现细节。
抽象数据类型避免声明公共或保护数据成员
使用友元保留封装性
避免在类声明中定义函数
类的声明应当只包含函数的声明,永远不要进行函数定义(实现)。
基本原理
类声明中定义函数干扰了类对其实现细节的规约;使类的接口难以辨认并难以阅读;并增加了对编译的依赖。
类声明中的函数定义也减少了对内嵌函数的控制(请参照“使用 No_Inline
条件编译符禁止内嵌编译”)。
明确声明构造函数的类使用默认构造函数
为了在数组中或任何 STL 容器中使用类,类必须提供公共的默认构造函数或允许编译器生成一个构造函数。
注意:
当类有非静态引用类型的数据成员时是以上规则的一个例外,这种情况下通常不可能创建一个有意义的默认构造函数。因此,使用对象数据成员的引用就是不可靠的。
带有指针类型数据成员的类要声明其复制构造函数和赋值操作符
示例
//摘自 [Meyers, 92].void f(){String hello("Hello");
//假设 String 类型
//由指向 char 型
//数组的指针来实现。
{ //进入新的域(程序块)
String world("World");
world = hello; //赋值语句使 world 丢失了
//它最初指向的内存单元}
//从域中退出时 //解构 world;
//此时也间接的解构了 helloString hello2 = hello;
//将已解构过的 hello 赋给 // hello2}
以上代码中,储存字符串“World
”的内存单元在赋值语句之后丢失了。结束内部程序块时,销毁了world
;因此,由 hello
引用的内存单元也丢失了。已经解构的 hello
赋给了 hello2
。
示例
//摘自 [Meyers, 1992]。
void foo(String bar) {};
void f(){String lost = "String that will be lost!";
foo(lost);}
以上代码中,当调用函数 foo
使用实参 lost
时,利用编译器定义的复制构造函数将 lost
复制到 foo
中。因为复制 lost
时对指向“String that will be lost!
”的指针进行逐位复制,当从 foo
中退出时,对 lost
的复制(即形参 bar,译者注)将会被销毁(假设析构函数正确的释放了内存单元),同时存储字符串“String that will be lost!
”的内存单元也会被释放。
不要重复声明构造函数参数有默认值
示例
//示例摘自 [X3J16, 95; section 12.8]
class X {
public:X(const X&, int); // int 参数没有
//初始化
//没有用户声明的复制构造函数,因此
//编译器暗中声明了一个。
};
//int 参数滞后的初始化//将构造函数变成了复制构造函数。//
X::X(const X& x, int i = 0) { ...}
基本原理
编译器在类声明中没有发现“标准”的复制构造函数时会暗中声明一个复制构造函数。但是,默认参数滞后的初始化可能将构造函数改变成复制构造函数:使用复制构造函数时导致混淆。因为这种混淆,任何复制构造函数的使用都因此而变为病态的。[X3J16, 95; section 12.8].
将析构函数总是声明为 virtual 类型
除非明确将类的设计为不可继承的,否则应将析构函数声明为 virtual 类型。
基本原理
如果基类的析构函数没有声明为 virtual 类型,则通过基类的指针或引用删除派生类对象将导致不确定的行为。
示例
//为了简便起见,这里使用了不好的命名风格
class B {
public:
B(size_t size) {tp = new T[size]; }
~B() { delete [] tp; tp = 0; }//...
private:
T* tp;};
class D : public B {
public:
D(size_t size) : B(size) {}
~D() {}//... };
void f(){B* bp = new D(10);delete bp; //由于基类
//的析构函数没有定义成 virtual 类型,
//这里的行为是不确定的}
避免声明太多的转换操作符和单参数构造函数
使用限定符 explicit
可防止单参数构造函数用于隐式转换。
不要重定义非虚函数
示例
// Adapted from [Meyers, 92].
class A {
public:oid f(); //非虚函数:静态绑定
};
class B : public A {
public:void f(); //非虚函数:静态绑定
};
void g(){
B x;
A* pA = &x; //静态类型,指向 A 的指针
B* pB = &x; //静态类型,指向 B 的指针
pA->f(); //调用
A::fpB->f(); //调用
B::f
}
谨慎使用非虚函数
因为非虚函数通过限制特殊化(即重载,译者注)和多态的使用来限制子类,声明为非虚拟之前,必须注意确保操作对所有子类确实是固定不变的。
使用初始化构造函数而不是使用构造函数中的赋值语句
对象构造时其状态的初始化应由初始化构造函数(一个成员初始化列表)完成,而不是通过构造函数内的赋值操作符完成。
示例
代码如下:
class X {
public:
X();
privateY
the_y;
};
X::X() : the_y(some_y_expression) { } “the_y”由初始化构造函数进行初始化而不是如下进行初始化
X::X() { the_y = some_y_expression; } “the_y”由赋值操作符进行初始化
基本原理
对象的构造涉及到在执行构造函数体之前构造所有的基类以及数据成员。使用初始化构造函数只需要一个操作(由初值构造),而在构造函数体中初始化数据成员需要两个操作(构造以及赋值)。
对有多重嵌套的类(类包含类,所包含的类又包含其他的类),多个操作(构造+成员赋值)造成的性能的开支就十分重要了。
初始化构造函数不要调用成员函数
示例
class A {
public:
A(int an_int);
};
class B : public A{
public:
int f();
B();
};
B::B() : A(f()) {} //不确定:调用成员函数时A//尚未初始化[X3J16, 95].
基本原理
当所有基类的成员初始化完成之前如果初始化构造函数直接或间接的调用了成员函数,此操作的结果是不确定的。[X3J16, 95].
注意构造函数和析构函数调用时的情况
构造函数中调用成员函数时要格外注意;即使调用的是虚函数时也要注意。执行的将是在类或其基类的构造函数、析构函数中定义的操作。
整形类常量使用 static const
定义整形(整数)类常量时,使用 static const
数据成员,不要使用 #define
或全局常量。如果编译器不支持 static const
,则使用 enum
。
示例
代码如下:class X {
static const buffer_size = 100;
char buffer[buffer_size];
};
static const buffer_size;
或:
class C {
enum { buffer_size = 100 };
char buffer[buffer_size];
};
但不要使用:#define BUFFER_SIZE 100class C {char buffer[BUFFER_SIZE];};
函数
一定要明确声明函数的返回值类型
这可以防止编译器发现函数未声明返回值类型时所造成的模糊不清。
函数声明中要提供正规参数名称
函数的声明和定义中要使用相同的名称;这可使混淆程度降至最小。提供参数名称有利于提高代码的易注释性和易读性。
函数要尽量只有一个返回点
type_t foo(){if (this_condition)return this_value;elsereturn some_other_value;}
避免创建对全局有副作用的函数
以重要性和活跃性递减的顺序声明函数的参数
避免声明带有不定参数个数的函数
避免重复声明带有默认参数的函数
函数的进一步重复声明中应避免添加默认值:远离先前声明,一个函数只应声明一次。否则,如果读者没有意识到此后的声明,这将会造成混淆。
函数声明中尽可能使用 const
检查函数是否有常量行为(返回常数值;接受常量实参;或其操作不带有副作用),如果有则使用限定符 const
表明其行为。
示例
const T f(...); //函数返回一个常量对象。
T f(T* const arg); //函数接受一个常量指针
//为其参数。
//可以改变指针所指的对象,
//但不可以改变指针本身。
T f(const T* arg); //函数参数为指针类型T
f(const T& arg); // 函数参数为引用类型
//它们都指向一个常量对象。指针可以
//改变,但所指的对象
//不能改变。
T f(const T* const arg); //函数参数为
//常量指针,它指向常量对象。
//指针和它所指的对象
//都不改变。
T f(...) const; //函数没有副作用:
//不改变它对象的状态
//所以可应用于
//常量对象。
避免利用值传递对象
C::C(const C& aC);
C& C::operator=(const C& aC);
示例:
考虑以下例子:
the_class the_class::return_by_value(the_class a_copy){
return a_copy;
}
the_class an_object;
return_by_value(an_object);
调用函数 return_by_value
,其实参为 an_object
,此时调用 the_class
的复制构造函数将 an_object
复制到 a_copy
。再次调用 the_class
的复制构造函数将 a_copy
复制到函数返回的临时对象中。从函数返回时调用 the_class
的析构函数销毁 a_copy
。一段时间之后再次调用 the_class
的析构函数销毁 return_by_value
返回的对象。以上不完成任何事的函数的所有开支是两个构造函数和两个析构函数。
如果 the_class
是个派生类且包含其他类的成员数据,情况会更糟:会调用基类以及所包含类的构造函数和析构函数,因此函数调用所带来的构造函数和析构函数的调用就会随之急剧增长。
注意:
以上指南似乎旨在建议开发人员在传递和返回对象时总是使用引用类型,但应格外当心不要返回引用到局部对象上,当需要对象时,也不要返回引用。返回引用到局部对象会造成灾难性后果,因为函数返回时,所返回的引用绑定到了所销毁的对象上。
不能返回引用到局部对象
离开函数作用域时会销毁局部对象;使用销毁了的对象会造成灾难。
不可返回由 new 初始化,之后又已解除引用的指针
示例
class C {
public:
...friend C& operator+( const C& left,const C& right);
};
C& operator+(const C& left, const C& right){
C* new_c = new C(left..., right...);
return *new_c;
}
C a, b, c, d;
C sum;
sum = a + b + c + d;
因为在计算加和 sum 值时没有存储操作符“+”的中间结果,因此中间对象不能删除,否则会导致内存泄漏。
不要返回非常量引用或指针到成员数据上
宏扩展使用内嵌定义函数不使用 #define
示例
不要这样做:
#define MAX(a, b) ((a) > (b) ?(a) : (b))
而应这样做:
inline int max(int a, int b) { return a > b ? a : b; }
宏 MAX
有好几个问题:它不是安全的类型;它的行为不是确定的:
int a = 1, b = 0;
MAX(a++, b); // a 增 1 了两次
MAX(a++, b+10); // a 增 1 了一次
MAX(a, "Hello"); //比较整形和指针
使用默认参数而不使用函数重载
当只使用一个算法并且此算法可由少量几个参数进行参数化时,使用默认参数而不使用函数重载。
使用默认参数有利于减少重载函数的数量、提高可维护性、减少函数调用时所需的实参数量、以及提高代码的可读性。
使用函数重载表达常用语义
当同一语义操作需要多个实现时使用函数重载,但重载使用不同的实参类型。
重载操作符时保留其传统含义。不要忘记定义关系操作符,如操作符 operator==
和 operator!=
。
避免重载以指针和整形数为实参的函数
void f(char* p);
void f(int i);
令操作符 operator=
返回对 *this
的引用
String x, y, z;
x = y = z = "A string";
C& C::operator=(const C&);
只有左端的对象是可能的(rhs 为常量引用,lhs 为变量引用),因此应返回 *this
。详情请参见[Meyers, 1992]。
使 operator=
检查自赋值
尽可能减少复杂性
类型
定义项目范围的全局系统类型
大型项目中通常有整个系统中经常使用的类型的集合;这种情况下,将这些类型收集入一个或多个低级全局实用程序的名字空间中,这样做是明智的(请参见“避免使用基本类型”的示例)。
避免使用基本类型
示例
namespace system_types {
typedef unsigned char byte;
typedef short int integer16; //16 位有符号整数
typedef int integer32; //32 位有符号整数
typedef unsigned short int natural16; //16 位无符号整数
typedef unsigned int natural32; //32 位无符号整数...}
基本原理
基本类型的表示依赖于具体实施。
使用 typedef
创建同义词来加强局部含义
对现有名称使用 typedef
创建同义词,提供更有意义的局部名称并提高易读性(这样做不会增加运行时间)。
typedef
也可用来提供受限名称的速记法。
示例
//标准库的向量声明//
namespace std {template <class T, class Alloc = allocator>class vector {
public:
typedef typename Alloc::types<T>reference reference;
typedef typename Alloc::types<T>const_reference const_reference;
typedef typename Alloc::types<T>pointer iterator;
typedef typename Alloc::types<T>const_pointer const_iterator;
...}}
使用 typedef
创建的名称时,同一代码段中不要混合使用原名称和它的同义词。
常量与对象
定义常量时避免使用 #define 预处理指示符
#define LIGHT_SPEED 3E8而应这样做:const int light_speed = 3E8;或调整数组大小如下:
enum { small_buffer_size = 100, large_buffer_size = 1000 };
基本原理
因为由 #defines
引入的名称在编译预处理时会被替换掉,不出现在符号表中,因此调试就更加困难了。
对象声明靠近其第一次使用点
声明时总要初始化 const 对象
未声明为 extern
的 const
对象有内部链接,声明时初始化这些常量对象允许在编译时使用初始化函数。
永远不要忘记常量对象的“不变性”
定义时初始化对象
第六章
表达式和语句
本章对不同种类的 C++ 表达式和语句提供指南。
表达式
使用冗余的圆括号使复合表达式含义更加清晰
避免表达式的过深嵌套
表达式的嵌套层数定义为:忽略操作符优先级原则时,从左到右求表达式的值所需圆括号对的嵌套数。
嵌套层数过多会使表达式难以理解。
不要假定任何特殊的表达式计算次序
示例
foo(i, i++);array[i] = i--;
空指针使用 0 而不使用 NULL
char* cp = (void*)0; /* C 合法但 C++ 不合法*/
不要使用旧类型转换
dynamic_cast
- 类同一层次(子类型)成员间的的转换,使用运行类型信息(对带虚函数的类,运行类型信息是可用的)。这种类间的转换一定是安全的。static_cast
- 类同一层次成员间的的转换,不使用运行类型信息;所以这可能是不安全的。如果程序员不能保证类型的安全性,则使用dynamic_cast。
reinterpret_cast
- 不相关指针类型和整形(整数)间的转换;这是不安全的,只应在所提及的类型间使用。const_cast
- 将指定为const
型函数实参的“不变性”转换掉。注意:const_cast
不打算将确实定义为常量对象(它可能位于只读存储器中)的“不变性”转换掉。
不要使用 typeid
实现类型转换逻辑:使类型转换操作符完成类型检查和转换的操作不可再分,详情请参见 [Stroustrup, 1994]。
示例
不要这样做:
void foo (const base& b){
if (typeid(b) == typeid(derived1))
{
do_derived1_stuff();
else if (typeid(b) == typeid(derived2))
{do_derived2_stuff();
else if () {}}
基本原理
旧类型转换使类型系统失效,可能导致编译器难以察觉的缺陷:内存管理系统可能会崩溃,虚函数表可能遭严重错误修改,当对象作为派生类对象访问时无关对象可能会遭破坏。注意即使读访问也可能造成破坏,因为有可能引用了并不存在的指针或区域。
新类型转换操作符使类型转换更加安全(多数情况下)、更加明确。
Boolean(布尔)表达式使用新的 bool 类型
不要使用旧的 Boolean 宏或常数:没有标准的布尔值 true(真);代之以新的 bool
类型。
布尔值 true(真)之间不要直接比较
因为传统上 true 值 (1 或 !0) 没有标准值,非零表达式和真值的比较就可能无效。
代之以使用布尔表达式。
示例
不要这样做:
if (someNonZeroExpression == true) //可能不会解释为真值条件最好这样做:
if (someNonZeroExpression) //一定会解释为一个真值条件
指针不要与不在同一数组中的对象比较
已删除的对象指针要赋予空指针值
设置已删除对象的指针为空指针可避免灾难的发生:重复删除非空指针是有害的,但重复删除空指针是无害的。
即使在函数返回前,删除操作后也总要赋一个空指针,因为以后可能添加新的代码。
语句
当从布尔表达式分支时使用 if 语句
当从离散值分支时使用 switch 语句
当分支条件为离散值时使用 switch 语句而不使用一系列的“else if”。
一定为 switch 语句提供一个 default
分支以记录错误
当循环需要迭代前测试时使用 for 语句或 while 语句
当迭代和循环的结束是根据循环计数器的值时使用 for 语句不使用 while 语句。
当循环需要迭代后测试时使用 do while 语句
循环中避免使用 jump 语句
避免使用除循环结束条件以外的循环退出方式(使用 break、return
或 goto
),不要使用 continue
不成熟的跳转到下一迭代上。这减少了控制路径流程的数量,使代码更易理解。
不要使用 goto
语句
避免嵌套作用域内隐藏标识符
第七章
特殊主题
本章为内存管理与错误报告提供指南
内存管理
C 和 C++ 内存操作要避免混合使用
删除由 new 创建的数组对象时总使用 delete[]
使用 delete 删除数组对象时如果不使用空方括号(“[]”),则只删除数组的第一个元素,因此导致内存泄漏。
错误处理和异常
开发过程中多使用声明语句以发现错误
示例
template<class T, class Exception>inline void assert ( T a_boolean_expression, Exception the_exception){if (! NDEBUG)if (! a_boolean_expression) throw the_exception;}
只在真实的异常情况时使用异常
从标准异常中派生项目异常
给定抽象尽量少使用异常
将所有异常声明为已引发
产生异常(而不仅仅是传递异常)的函数应在它们的异常规约中将所有异常声明为已引发:它们不应平静的产生异常而不警告它们的用户。
在异常首次出现时就报告它
开发过程中,采用合适的记录机制尽早报告异常,包括在“异常引发点”。
按照从派生结构最底端到最顶端的顺序定义异常处理
示例
不要这样做:
class base { ...};
class derived : public base { ...};
...try {...throw derived(...);引发一个派生类异常}
catch (base& a_base_failure)但基类异常处理过程“捕获”了它,因为//基类处理过程首先匹配!
{...}
catch (derived& a_derived_failure) 这一处理过程是无法达到的!{ ...}
避免使用捕获所有异常的处理过程
避免使用捕获所有异常的处理过程(处理过程声明使用 “…”),除非重引发了异常只有在做本地内务管理时才使用捕获所有异常的处理过程,这时应重引发异常以避免掩盖了本层不能处理此异常的事实。
try {...}catch (...){ if (io.is_open(local_file)){io.close(local_file);}throw;}
确保函数状态代码有合适的值
当状态代码作为函数参数返回时,要赋值给参数作为程序体中的第一个可执行语句。系统的设置所有状态的默认值为成功或失败。考虑函数所有可能的出口,包括异常处理。
本地执行安全检查;不要希望您的客户会这样做
如果没有合适的输入,函数就会产生错误输出,在函数中装载代码以受控方式监测和报告非法输入。不要依赖于标注来告知客户传递合适的值。事实上标注迟早会被忽略掉,如果检测到非法参数就会导致难以调试的错误。
第八章
可移植性
本章论述先验不可移植的语言特征。
路径名
不要使用绝对代码文件路径名
各操作系统中的路径名不是以标准形式表示的。使用它们将会引入对平台的依赖。
示例
#include "somePath/filename.hh" // Unix
#include "somePath/filename.hh" // MSDOS
数据表示
类型的表示和排列高度依赖于机器的结构。对表示和排列的假设会导致混淆,降低可移植性。
不要假设类型的表示
特别的,不可以在 int
、长整形或任何其他数字类型中存储指针类型,因为这是高度不可移植的。
不要假设类型的排列
不要依赖于一个特殊的下溢或上溢行为
尽可能使用“可伸缩”常量
示例
const int all_ones = ~0;const int last_3_bits = ~0x7;
类型转换
不要从一个“短”类型转换成一个“长类型”
机器结构可能指定了某种类型的排列方式。从需要较松散排列的类型转换为需要较严密排列的类型可能会导致程序错误。
第九章
复用
本章为 C++ 代码复用提供指南。
尽可能使用标准库构件
如果没有标准库,则基于标准库接口创建类,这有利于将来的移植。
使用模板复用独立于数据的行为
使用公共继承复用类接口(子类型化)
使用 public
继承表达“is a”关系并复用基类接口,还可有选择的复用它们的实现。
使用包容而不是私有继承复用类的实现
当复用实现或建模“部分/整体”关系时,避免使用私有继承。复用未重新定义的实现最好由包容而非私有继承来实现。
当需要重新定义基类的操作时使用私有继承。
谨慎使用多继承
第十章
编译问题
本章为编译问题指南
尽量减少对编译的依赖
模块规约中不要包含只是此模块实施所需的其他头文件。
避免在只需要指针或引用可见时就为了能看到其他类而在规约中包含头文件;代之以预先声明。
示例
//模块 A 规约,包含于文件“A.hh”中#include "B.hh" //当只有实施需要时 //不要包含。#include "C.hh" //只有引用需要时不要包含; //代之以预先声明。class C;class A{C* a_c_by_reference; //有 a 的引用。};// “A.hh”结束
注意:
尽量减少对编译的依赖是某些设计代码模式或模式的基本原理,如不同的命名:Handle(句柄)或 Envelope(信包)[Meyers, 1992],或 Bridge(桥)[Gamma] 类。将类抽象的责任在两个关联类间分割,一个提供类接口,另一个提供类实现;因为任何实现(实现类)上的改变都不再需要客户重新编译,因此类与其客户间的依赖关系就最小化了。
示例
//模块 A 规约,包含于文件“A.hh”中
class A_implementation;class A{A_implementation* the_implementation;};
//“A.hh”结束
这一做法也允许接口类和实现类特殊化为两个单独的类层次结构。
用具体值定义 NDEBUG
指南总结
要求或限制
建议
提示
定义项目范围的全局系统类型
使用 typedef
创建同义词来加强局部含义
使用冗余的圆括号使复合表达式含义更加清晰
避免表达式的过深嵌套
空指针使用 0 而不使用 NULL
在异常首次出现时就报告它
参考文献
[Cargill, 92] | Cargill, Tom.1992. C++ Programming Styles Addison-Wesley. |
[Coplien, 92] | Coplien, James O. 1992. Advanced C++ Programming Styles and Idioms, Addison-Wesley. |
[Ellemtel, 93] | Ellemtel Telecommunications Systems Laboratories.June 1993. Programming in C++ Rules and Recommendations. |
[Ellis, 90] | Ellis, Margaret A. and Stroustrup, Bjarne.1990.The Annotated C++ Reference Manual, Addison-Wesley. |
[Kruchten, 94] | Kruchten, P. May 1994. Ada Programming Guidelines for the Canadian Automated Air Traffic System. |
[Lippman, 96] | Lippman, Stanley, B. 1996. Inside the C++ Object Model, Addison-Wesley. |
[Meyers, 92] | Meyers, Scott.1992. Effective C++, Addison-Wesley. |
[Meyers, 96] | Meyers, Scott.1996. More Effective C++, Addison-Wesley. |
[Plauger, 95] | Plauger, P.J. 1995. The Draft Standard C++ Library, Prentice Hall, Inc. |
[Plum, 91] | Plum, Thomas and Saks, Dan.1991. C++ Programming Guidelines, Plum Hall Inc. |
[Rational, 92] | Rational Software Corporation, December 1992. Rose C++ Programming Style Guidelines. |
[Stroustrup, 94] | Stroustrup, Bjarne.1994. The Design and Evolution of C++, Addison-Wesley. |
[X3J16, 95] | X3J16/95-0087 | WG21/N0687.April 1995. Working Paper for Draft Proposed International Standard for Information Systems-Programming Language C++. |
[Cargill, 92] | Cargill, Tom.1992. C++ Programming Styles Addison-Wesley. |
[Coplien, 92] | Coplien, James O. 1992. Advanced C++ Programming Styles and Idioms, Addison-Wesley. |
[Ellemtel, 93] | Ellemtel Telecommunications Systems Laboratories.June 1993. Programming in C++ Rules and Recommendations. |
[Ellis, 90] | Ellis, Margaret A. and Stroustrup, Bjarne.1990.The Annotated C++ Reference Manual, Addison-Wesley. |
[Kruchten, 94] | Kruchten, P. May 1994. Ada Programming Guidelines for the Canadian Automated Air Traffic System. |
[Lippman, 96] | Lippman, Stanley, B. 1996. Inside the C++ Object Model, Addison-Wesley. |
[Meyers, 92] | Meyers, Scott.1992. Effective C++, Addison-Wesley. |
[Meyers, 96] | Meyers, Scott.1996. More Effective C++, Addison-Wesley. |
[Plauger, 95] | Plauger, P.J. 1995. The Draft Standard C++ Library, Prentice Hall, Inc. |
[Plum, 91] | Plum, Thomas and Saks, Dan.1991. C++ Programming Guidelines, Plum Hall Inc. |
[Rational, 92] | Rational Software Corporation, December 1992. Rose C++ Programming Style Guidelines. |
[Stroustrup, 94] | Stroustrup, Bjarne.1994. The Design and Evolution of C++, Addison-Wesley. |
[X3J16, 95] |
X3J16/95-0087 | WG21/N0687.April 1995. Working Paper for Draft Proposed International Standard for Information Systems-Programming Language C++.http://blog.csdn.net/junnan321/article/details/1763465 |
C++大型项目开发约束相关推荐
- 大型项目开发,你准备好了吗?
大型项目开发,你准备好了吗? 大型项目开发,你准备好了吗?----网站开发人员应该知道的62件事,今天在chinaz上看到的,写的很全面,也很到位,这些问题若是都解决了,网站开发可谓完美... 一.界 ...
- iOS 大型项目开发漫谈
从http://www.cocoachina.com/ios/20150828/13170.html转载,谢谢写这篇文章的大神! 标题有些吓人请不要害怕,不过这确实不是扫盲贴,需要一定的iOS开发基础 ...
- iOS大型项目开发漫谈
标题有些吓人请不要害怕,不过这确实不是扫盲贴,需要一定的iOS开发基础.在我多年的码农生涯中绝大部分时间都是做的小项目,大一些的可能也就是百万行代码的样子,跟Windows系统几千万行源码比简直就是小 ...
- python赚钱项目开发大体流程咨询_大型项目开发的基本流程
万科四季花简介 万科四季花城总占地面积 217498.1 平方米 , 总建筑面积 407479 平方米 , 规划设计在万科城市 花园的基础上加以发展和延伸. 整个社区由多个围合式布局的小组团构成, 每 ...
- 【工具】PlatformIO终于能用了,arduino大型项目开发环境PIO,编辑器+编译器+调试全功能...
微信关注 "DLGG创客DIY" 设为"星标",重磅干货,第一时间送达. 经过漫长的等待PlatformIO终于能用了,今天来聊聊这货. 这货张什么样子? 下图 ...
- iOS大型项目开发架构
http://www.jianshu.com/p/921ab32c3c71# 首先说说编程语言的选择,Objecive-C还是Swift?我还没有在项目中使用Swift,因为我说服不了自己去用它,它的 ...
- arduino安装包_【工具】PlatformIO终于能用了,arduino大型项目开发环境PIO,编辑器+编译器+调试全功能...
微信关注 " DLGG创客DIY "设为" 星标",重磅干货,第一时间送达. 经过漫长的等待PlatformIO终于能用了,今天来聊聊这货. 这货张什么样子? ...
- python大型项目开发规范_大型项目CMakeLIsts.txt的编写规范
1.Very simple executable PROJECT( helloworld ) # 非必需 SET( hello_SRCS hello.cpp ) ADD_EXECUTABLE( h ...
- 基于J2EE架构的项目开发团队中的角色与职责
[声明] 1.2内容来源:<J2EE Architects Handbook>中文翻译<J2EE系统架构师参考手册>[翻译 Mellon] 1.角色 Technical arc ...
- db和model用哪个开发大型项目 thinkphp_Java程序员开发大型应用程序时,必须要掌握几个的技巧...
假如你是一名Java开发者,正在开发和维护包含2000个类并使用了很多框架的应用程序.你要如何理解这些代码呢?在典型的Java企业项目小组中,大部分能够帮你的高级工程师看起来都很忙,文档也很少.你需要 ...
最新文章
- 嫌Terminal终端太单调?快收下这几个有趣的改造工具!
- 网站内链为什么要做上下文链接?对网站优化有什么帮助?
- 汇编语言随笔(14)-直接定址表、检测点16和实验16
- python3 列表的增删改查
- 支架预压弹性变形值计算_复杂环境下大跨度箱梁整体支架法现浇安全要点
- DLL入门浅析(1)——如何建立DLL
- C# 委托:把方法组合到一个数组中使用
- python装饰器函数后执行_Python装饰器限制函数运行时间超时则退出执行
- 计算机科学家论文引用排名:LeCun终于晋身三巨头
- [转]刚成为程序员的你需要什么技能
- 那些年使用Android studio遇到的问题
- 【转】for循环中进行Promise异步操作的问题总结
- 零基础程序员如何自学编程
- C语言函数嵌套学习实例
- 地震数据.dat文件转.sgy文件
- 计算机科学与技术专业前沿技术相关论文,计算机科学与技术专业创新能力的培养途径论文...
- GMA Round 1 大吉大利,晚上吃鸡
- linux内核5.8.1,Linus Torvalds宣布大规模更新Linux内核5.8
- 【MOOC课程】浙大数据结构记录(下)
- sendmail收邮件