通常,每一个cpp 文件(C++的源文件)都有一个对应的.h 文件(头文件).也有一些常见例外, 如单元测试代码和只包含 main() 函数的 .cc 文件。正确使用头文件可令代码在可读性、文件大小和性能上大为改观.
下面的规则将引导你规避使用头文件时的各种陷阱.
Tip: 头文件应该能够自给自足(self-contained, 也就是可以作为第一个头文件被引入),以 .h 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法.
所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有1.2. #define 保护,统统包含它所需要的其它头文件,也不要求定义任何特别 symbols。不过有一个例外,即一个文件并不是 self-contained 的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用 .inc 文件扩展名。
如果 .h 文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 .cc 文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的 -inl.h 文件里
(译者注:过去该规范曾提倡把定义放到 -inl.h 里过)。
有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的 .cc 文件里。
所有头文件都应该使用#define 防止头文件被多重包含,命名格式当是:
H#ifndef <标识> #define <标识> ...... ...... #endif
<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef STDIO_H
#define STDIO_H
…
#endif
否则,比如你有两个C文件,这两个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件,于是问题来了,大量的声明冲突。为保证唯一性,头文件的命名应基于其所在项目源代码树的全路径。例如,项目foo 中的头文件foo/src/bar/baz.h 按如下方式保护: #ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_
尽可能地避免使用前置声明,倾向于使用 #include包含需要的头文件。
前置声明的主要作用:
1.解决两个class的相互依赖问题,也就是两个头文件相互include;
2.降低文件之间的“编译依存关系”;
使用前置声明的陷阱:
1.前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
2.必须用指针或引用。
3.不能在前置声明类定义前使用前置声明类中的析构函数。
4.用前置声明代替 #include 甚至都会暗暗地改变代码的含义:
// b.h:
struct B {};
struct D : B {};
// good_user.cpp:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); } // calls f(B*)
若把#include换成前置声明,由于声明时不知道D是B的子类,test()中f(x)就会导致f(void*)被调用,而不是f(B*)。
1.不要内联超过 10 行的函数,只有当函数只有 10 行甚至更少时才将其定义为内联函数.
2.inline函数体中,不能有循环语句,if语句或者switch语句,否则,即使函数定义时使用了inline关键字,编译器也会将其当成普通函数来处理
3.虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.
A.包含头文件的顺序规则如下:
1.相关头文件,
2.C 库,
3.C++ 库,
4.其他库的 .h,
5.本项目内的 .h.
例如:dir/foo.cpp 主要作用是实现或测试 dir2/foo2.h 的功能,foo.cpp 中包含头文件的次序如下:
1.dir2/foo2.h (优先位置)
2.C系统文件
3.C++系统文件
4.其他库的 .h 文件
5.本项目内 .h 文件
B.项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 . (当前目录) 或 .. (上级目录).
例如, google-awesome-project/src/base/logging.h 应该按如下方式包含:
#include "base/logging.h"
// .h 文件
namespace mynamespace {
// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// .cpp 文件
namespace mynamespace {
// 函数定义都置于命名空间中
void MyClass::Foo() {
...
}
} // namespace mynamespace
// 禁止 —— 污染命名空间
using namespace foo;
namespace X {
inline namespace Y {
void foo();
} // namespace Y
} // namespac
X::Y::foo() 与 X::foo() 彼此可代替
// 在 .cpp 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
namespace impl { // 仅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl
inline void my_inline_function() {
// 限制在一个函数中的命名空间别名
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian
在 .cpp 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 static 。
匿名命名空间的声明和具名的格式相同,在最后注释上 namespace 。
namespace {
...
} // namespace
匿名命名空间中声明的名称具有internal链接属性这和声明为static的全局名称的链接属性是相同的,即名称的作用域被限制在当前文件中,无法通过在另外的文件中使用extern声明来进行链接。
非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内.
类的静态方法应当和类的实例或静态数据紧密相关.不要单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 应该使用命名空间 。举例而言,对于头文件 myproject/foo_bar.h , 应当使用:
namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
} // namespace foo_bar
} // namespace myproject
而非
namespace myproject {
class FooBar {
public:
static void Function1();
static void Function2();
};
} // namespace myproject
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:
int i;
i = f(); // 坏——初始化和声明分离
int j = g(); // 好——初始化时声明
vector v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
vector v = {1, 2}; // 好——v 一开始就初始化
属于 if, while 和 for 语句的变量应当在这些语句中声明,这样这些变量的作用域就被限制在这些语句中了,举例而言:
while (const char* p = strchr(str, '/')) str = p + 1;
在循环语句中如果变量是一个对象, 需要注意该对象变量每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低. 在循环作用域外面声明这类变量要高效的多。
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。
禁用类类型的全局变量,因为静态变量的构造函数、析构函数和初始化的顺序在是不确定的,同一个编译单元内是确定的,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序是不确定的,甚至随着构建变化而变化,导致难以发现的 bug.
例如
禁用全局vector (使用 C 数组替代) 和 string (使用 const char [])。
函数命名、变量命名、文件命名要有描述性,少用缩写.
注意, 一些特定的广为人知的缩写是允许的, 例如用 i 表示迭代变量和用 T 表示模板参数.
int price_count_reader; // 无缩写
int num_errors; // "num" 是一个常见的写法
int num_dns_connections; // 人人都知道 "DNS" 是什么
int n; // 毫无意义.
int nerr; // 含糊不清的缩写.
int n_comp_conns; // 含糊不清的缩写.
int wgc_connections; // 只有贵团队知道是什么意思.
int pc_reader; // "pc" 有太多可能的解释了.
int cstmr_id; // 删减了若干字母.
好的变量名或是函数名,我认为应该有以下的规则:
1) 直观并且可以拼读,可望文知意,不必“解码”。
2) 名字的长度应该即要最短的长度,也要能最大限度的表达其含义。
3) 不要全部大写,也不要全部小写,应该大小写都有,如:GetLocalHostName 或是 UserAccount。
4) 可以简写,但简写得要让人明白,如:ErrorCode -> ErrCode, ServerListener -> ServLisner,UserAccount -> UsrAcct 等。
5) 为了避免全局函数和变量名字冲突,可以加上一些前缀,一般以模块简称做为前缀。
6) 全局变量统一加一个前缀或是后缀,让人一看到这个变量就知道是全局的。
7) 用匈牙利命名法命名函数参数,局部变量。但还是要坚持“望文生意”的原则。
8) 与标准库(如:STL)或开发库(如:MFC)的命名风格保持一致。
文件名要全部小写, 可以包含下划线 ‘_’, C++文件要以 .cpp 结尾, 头文件以 .h 结尾.
可接受的文件命名示例:
my_useful_class.cpp
my-useful-class.cpp
myusefulclass.cpp
myusefulclass_test.cpp
通常应尽量让文件名更加明确. http_server_logs.h 就比 logs.h 要好. 定义类时文件名一般成对出现, 如 foo_bar.h 和 foo_bar.cpp, 对应于类 FooBar.
类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.
所有类型命名 —— 类, 结构体, 类型定义 (typedef), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线.
例如:
// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// 类型定义
typedef hash_map PropertiesMap;
// using 别名
using PropertiesMap = hash_map;
// 枚举
enum UrlTableErrors { ...
变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.
普通变量 (包括函数参数) 一律小写,单词之间用下划线连接.
举例:
string table_name; // 好 - 用下划线.
string tableName; // 差 - 混合大小写
数据成员名一律小写,单词之间用下划线连接.并以下划线结尾
class TableInfo {
...
private:
string table_name_; // 好 - 后加下划线.
string tablename_; // 好.
static Pool* pool_; // 好.
};
和普通变量一样一律小写,单词之间用下划线连接. 结构体数据成不用像类那样后接下划线:
struct UrlTableProperties {
string name;
int num_entries;
static Pool* pool;
};
声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. 例如:
const int kDaysInAWeek = 7;
所有具有静态存储类型的变量 (例如静态变量或全局变量) 都应当以此方式命名. 对于其他存储类型的变量, 如自动变量等, 这条规则是可选的. 如果不采用这条规则, 就按照一般的变量命名规则.
常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配:
MyExcitingFunction(),
MyExcitingMethod(),
my_exciting_member_variable(),
set_my_exciting_member_variable().
一般来说, 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线. 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 StartRpc() 而非 StartRPC()).
示例:
AddTableEntry()
DeleteUrl()
OpenFileOrDie()
命名空间以小写字母命名,不要使用缩写. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.
顶级命名空间的名称应当是项目名或者是该命名空间中的代码所属的团队的名字. 命名空间中的代码, 应当存放于和命名空间的名字匹配的文件夹或其子文件夹中.
枚举的命名应全部大写, 使用下划线分割单词如 ENUM_NAME.
示例
enum AlternateUrlTableErrors {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};
通常不应该使用宏. 如果不得不用, 其命名全部大写, 使用下划线:
#define ROUND(x) ...
#define PI_ROUNDED 3.0
注释虽然写起来很痛苦, 但对保证代码可读性至关重要. 最好的代码应当本身就是文档. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字.
对于行注释使用 // 或 块注释/* */, 统一就好.一些老版本的C编译器并不支持行注释,所以为了你的程序的移植性,请你还是尽量使用块注释。
你也许会为块注释的不能嵌套而不爽,那么你可以用预编译来完成这个功能。使用“#if 0”和“#endif”括起来的代码,将不被编译,而且还可以嵌套。
在每一个文件开头加入版权公告.
文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释.
每个文件都应该包含许可证引用. 为项目选择合适的许可证版本.(比如, Apache 2.0, BSD, LGPL, GPL)
如果你对原始作者的文件做了重大修改, 请考虑删除原作者信息.
如果一个 .h 文件声明了多个概念, 则文件注释应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系. 一个一到两行的文件注释就足够了, 对于每个概念的详细文档应当放在各个概念中, 而不是文件注释中.
-------
好的程序员会给自己的每个函数,每个文件,都注上版权和版本。
对于C/C++的文件,文件头应该有类似这样的注释:
/************************************************************************
*
* 文件名:network.c
*
* 文件描述:网络通讯函数集
*
* 创建人: Hao Chen, 2003年2月3日
*
* 版本号:1.0
*
* 修改记录:
*
************************************************************************/
每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显
示例
// Iterates over the contents of a GargantuanTable.
// Example:
// GargantuanTableIterator* iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
// process(iter->key(), iter->value());
// }
// delete iter;
class GargantuanTableIterator {
...
};
类注释应当为读者理解如何使用与何时使用类提供足够的信息, 同时应当提醒读者在正确使用此类时应当考虑的因素. 如果类有任何同步前提, 请用文档说明. 如果该类的实例可被多线程访问, 要特别注意文档说明多线程环境下相关的规则和常量使用.
如果你想用一小段代码演示这个类的基本用法或通常用法, 放在类注释里也非常合适.
如果类的声明和定义分开了(例如分别放在了 .h 和 .cpp 文件中), 此时, 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.
函数声明处的注释描述函数功能; 定义处的注释描述函数实现.
基本上每个函数声明处前都应当加上注释, 描述函数的功能和用途. 只有在函数的功能简单而明显时才能省略这些注释(例如, 简单的取值和设值函数). 注释使用叙述式 (“Opens the file”) 而非指令式 (“Open the file”); 注释只是为了描述函数, 而不是命令函数做什么. 通常, 注释不会描述函数如何工作. 那是函数定义部分的事情.
函数声明处注释的内容:
函数的输入输出.
对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
函数是否分配了必须由调用者释放的空间.
参数是否可以为空指针.
是否存在函数使用上的性能隐患.
如果函数是可重入的, 其同步前提是什么?
示例
// Returns an iterator for this table. It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginning of the table.
//
// This method is equivalent to:
// Iterator* iter = table->NewIterator();
// iter->Seek("");
// return iter;
// If you are going to immediately seek to another place in the
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;
但也要避免罗罗嗦嗦, 或者对显而易见的内容进行说明. 下面的注释就没有必要加上 “否则返回 false”, 因为已经暗含其中了:
// Returns true if the table cannot hold any more entries.
bool IsTableFull();
注释函数重载时, 注释的重点应该是函数中被重载的部分, 而不是简单的重复被重载的函数的注释. 多数情况下, 函数重载不需要额外的文档, 因此也没有必要加上注释.
注释构造/析构函数时, 切记读代码的人知道构造/析构函数的功能, 所以 “销毁这一对象” 这样的注释是没有意义的. 你应当注明的是注明构造函数对参数做了什么 (例如, 是否取得指针所有权) 以及析构函数清理了什么. 如果都是些无关紧要的内容, 直接省掉注释. 析构函数前没有注释是很正常的.
函数声明注释规范:
最基础的简单注释:
/*!
* \brief ,简介: 开始侦听连接请求.
* \etails ,详细信息: 这将在收到SIGTERM、SIGINT信号时发生.
* \param[in] ,参数[in]: config_file: 配置文件的路径.
* \return ,返回值: 创建的实例.
* \error ,错误: SecurityErrorDomain::kEmptyContainer(如果容器为空)
*/
所有注释项,根据需要使用:
/*!
* \brief ,简介——开始侦听连接请求。
* \etails ,详细信息——这将在收到SIGTERM、SIGINT信号时发生。
* \param[in] ,参数——config_file: 配置文件的路径。
* \return ,返回值——创建的实例。
* \error ,错误——SecurityErrorDomain::kEmptyContainer (如果容器为空)
* \note ,注意事项——用户提供的上下文可用于以下算法:Ed25519ctx、Ed25519ph、Ed448ph。
* \warning ,警告——InitializeComponent将生成一个线程。
* \remarks ,备注——将生成函数体。
* \code ,代码
* int main(int argc, char* argv[])
* {
* PerformSignalHandling();
* ara::core::Result init_result{amsr::log::internal::InitializeComponent()};
* }
* \endcode ,结束代码
* \pre ,在什么之前——上下文ID必须包含4个或更少的字符
* \throw ,投——crypto::softwareprovider::CryptoRuntimeError 侦听接收器失败。
* \exceptionsafety ,例外安全——BASIC 接收器内部可能会发生变化。
* \context ,上下文——初始化阶段,在创建其他线程之前。
* \reentrant ,可重入的——FALSE
* \synchronous ,同步——TRUE
* \threadsafe ,线程安全——FALSE
* \spec ,规格
* - requires要求 true;
* \endspec ,端规格
* \trace ,跟踪——CREQ日志区分消息组
* \vprivate ,Vector私有——Vector产品内部API
* \vpublic ,Vector公有
* \internal ,内部
* - 检查加密对象是否与预期类型匹配
* - 从给定容器中检索加密对象
* - 如果检索加密对象成功
* - 将加密对象指针强制转换为指向预期类型的指针
* - 返回加密对象的唯一指针结果
* - 否则,失败并出现错误
* \endinternal ,结束内部
*/
如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释. 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由. 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要.
不要 从 .h 文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上.
通常变量名本身足以很好说明变量用途. 某些情况下, 也需要额外的注释说明.
每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果有非变量的参数(例如特殊值, 数据成员之间的关系, 生命周期等)不能够用类型与变量名明确表达, 则应当加上注释. 然而, 如果变量类型与变量名已经足以描述一个变量, 那么就不再需要加上注释.
特别地, 如果变量可以接受 NULL 或 -1 等警戒值, 须加以说明. 比如:
private:
// Used to bounds-check table acppesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;
和数据成员一样, 所有全局变量也要注释说明含义及用途, 以及作为全局变量的原因. 比如:
// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;
对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释.
巧妙或复杂的代码段前要加注释. 比如:
// Divide result by two, taking into acppount that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
}
比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释. 比如:
// If we have enough memory, mmap the data portion too.
mmap_budget = max(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
return; // Error already logged.
注意, 这里用了两段注释分别描述这段代码的作用, 和提示函数返回时错误已经被记入日志.
如果你需要连续进行多行注释, 可以使之对齐获得更好的可读性:
DoSomething(); // Comment here so the comments line up.
DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment.
{ // One space before comment when opening a new scope is allowed,
// thus the comment lines up with the following comments and code.
DoSomethingElse(); // Two spaces before line comments normally.
}
std::vector list{
// Comments in braced lists describe the next element...
"First item",
// .. and should be aligned appropriately.
"Second item"};
DoSomething(); /* For trailing block comments, one space is fine. */
对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释.
TODO 注释要使用全大写的字符串 TODO, 在随后的圆括号里写上你的名字, 邮件地址, bug ID, 或其它身份标识和与这一 TODO 相关的 issue. 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 TODO 格式进行查找. 添加 TODO 注释并不意味着你要自己来修正, 因此当你加上带有姓名的 TODO 时, 一般都是写上自己的名字.
// TODO([email protected]): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
// TODO(bug 12345): remove the "Last visitors" feature
通过弃用注释(DEPRECATED comments)以标记某接口点已弃用.
您可以写上包含全大写的 DEPRECATED 的注释, 以标记某接口为弃用状态. 注释可以放在接口声明前, 或者同一行.
在 DEPRECATED 一词后, 在括号中留下您的名字, 邮箱地址以及其他身份标识.
每个人都可能有自己的代码风格和格式, 但如果一个项目中的所有人都遵循同一风格的话, 这个项目就能更顺利地进行. 每个人未必能同意下述的每一处格式规则, 而且其中的不少规则需要一定时间的适应, 但整个项目服从统一的编程风格是很重要的, 只有这样才能让所有人轻松地阅读和理解代码.
每一行代码字符数不超过 80.
尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.
缩进应该是每个程序都会做的,一个缩进一般是一个TAB键或是4个空格。(最好不用TAB键)
只使用空格, 每次缩进 2 个空格.
我们使用空格缩进. 不要在代码中使用制表符. 你应该设置编辑器将制表符转为空格.
返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致.
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // 4 space indent
Type par_name2,
Type par_name3) {
DoSomething(); // 2 space indent
...
}
注意以下几点:
使用好的参数名.
只有在参数未被使用或者其用途非常明显时, 才能省略参数名.
如果返回类型和函数名在一行放不下, 分行.
如果返回类型与函数声明或定义分行了, 不要缩进.
左圆括号总是和函数名在同一行.
函数名和左圆括号间永远没有空格.
圆括号与参数间没有空格.
左大括号总在最后一个参数同一行的末尾处, 不另起新行.
右大括号总是单独位于函数最后一行, 或者与左大括号同一行.
右圆括号和左大括号间总是有一个空格.
所有形参应尽可能对齐.
缺省缩进为 2 个空格.
换行后的参数保持 4 个空格的缩进.
未被使用的参数, 或者根据上下文很容易看出其用途的参数, 可以省略参数名:
class Foo {
public:
Foo(Foo&&);
Foo(const Foo&);
Foo& operator=(Foo&&);
Foo& operator=(const Foo&);
};
未被使用的参数如果其用途不明显的话, 在函数定义处将参数名注释起来:
class Shape {
public:
virtual void Rotate(double radians) = 0;
};
class Circle : public Shape {
public:
void Rotate(double radians) override;
};
void Circle::Rotate(double /*radians*/) {}
// 差 - 如果将来有人要实现, 很难猜出变量的作用.
void Circle::Rotate(double) {}
属性, 和展开为属性的宏, 写在函数声明或定义的最前面, 即返回类型之前:
MUST_USE_RESULT bool IsOK();
不要在 return 表达式里加上非必须的圆括号.
只有在写 x = expr 要加上括号的时候才在 return expr; 里使用括号.
return result; // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
another_condition);
Lambda 表达式对形参和函数体的格式和其他函数一致; 捕获列表同理, 表项用逗号隔开.
若用引用捕获, 在变量名和 & 之间不留空格.
int x = 0;
auto add_to_x = [&x](int n) { x += n; };
短 lambda 就写得和内联函数一样.
std::set blacklist = {7, 8, 9};
std::vector digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
return blacklist.find(i) != blacklist.end();
}),
digits.end());
要么一行写完函数调用, 要么在圆括号里对参数分行, 要么参数另起一行且缩进四格. 如果没有其它顾虑的话, 尽可能精简行数, 比如把多个参数适当地放在同一行里.
函数调用遵循如下形式:
bool retval = DoSomething(argument1, argument2, argument3);
如果同一行放不下, 可断为多行, 后面每一行都和第一个实参对齐, 左圆括号后和右圆括号前不要留空格:
bool retval = DoSomething(averyveryveryverylongargument1,
argument2, argument3);
参数也可以放在次行, 缩进四格:
if (...) {
...
...
if (...) {
DoSomething(
argument1, argument2, // 4 空格缩进
argument3, argument4);
}
如果一些参数本身就是略复杂的表达式, 且降低了可读性, 那么可以直接创建临时变量描述该表达式, 并传递给函数:
int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);
或者放着不管, 补充上注释:
ool retval = DoSomething(scores[x] * y + bases[x], // Score heuristic.
x, y, z);
如果一系列参数本身就有一定的结构, 可以酌情地按其结构来决定参数格式
// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
在圆括号和条件之间没有空格if 和左圆括号间都有个空格. 右圆括号和左大括号之间也要有个空格:
if (condition) { // 圆括号里没有空格.
... // 2 空格缩进.
} else if (...) { // else 与 if 的右括号同一行.
...
} else {
...
}
if(condition) // 差 - IF 后面没空格.
if (condition){ // 差 - { 前面没空格.
if(condition){ // 变本加厉地差.
if (condition) { // 好 - IF 和 { 都与空格紧邻.
只有当语句简单并且没有使用 else 子句条件语句允许写在同一行.
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();
如果语句有 else 分支则不允许写在同一行:
// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
if (x) DoThis();
else DoThat();
if 必须总是使用大括号, 单行语句也需要使用大括号
if (condition) {
DoSomething(); // 2 空格缩进.
}
switch 语句可以使用大括号分段, 以表明 cases 之间不是连在一起的.
switch 语句中的 case 块可以使用大括号括起来
如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning).
如果 default 应该永远执行不到, 简单的加条 assert:
switch (var) {
case 0: { // 2 空格缩进
... // 4 空格缩进
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}
在单语句循环里, 括号可用可不用.
for (int i = 0; i < kSomeNumber; ++i)
printf("I love you\n");
for (int i = 0; i < kSomeNumber; ++i) {
printf("I take it back\n");
}
空循环体应使用 {} 或 continue.
while (condition) {
// 反复循环直到条件失效.
}
for (int i = 0; i < kSomeNumber; ++i) {} // 可 - 空循环体.
while (condition) continue; // 可 - contunue 表明没有逻辑.
while (condition); // 差 - 看起来仅仅只是 while/loop 的部分之一.
句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.
下面是指针和引用表达式的正确使用范例:
x = *p;
p = &x;
x = r.y;
x = r->y;
在声明指针变量或参数时, 星号与类型或变量名紧挨都可以:
// 好, 空格前置.
char *c;
const string &str;
// 好, 空格后置.
char* c;
const string& str;
int x, *y; // 不允许 - 在多重声明中不能使用 & 或 *
char * c; // 差 - * 两边都有空格
const string & str; // 差 - & 两边都有空格.
如果一个布尔表达式超过标准行宽, 断行方式要统一一下.
下例中, 逻辑与 (&&) 操作符总位于行尾:
if (this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one) {
...
}
此外, 直接用符号形式的操作符, 比如 && 和 ~, 不要用词语形式的 and 和 compl.
用 =, () 和 {} 均可. 以下的例子都是正确的:
int x = 3;
int x(3);
int x{3};
string name("Some Name");
string name = "Some Name";
string name{"Some Name"};
非空列表初始化就会优先调用 std::initializer_list, 空列表初始化, 原则上会调用默认构造函数. 为了强制禁用 std::initializer_list 构造函数, 请改用括号.
vector v(100, 1); // 内容为 100 个 1 的向量.
vector v{100, 1}; // 内容为 100 和 1 的向量.
列表初始化不允许整型类型的四舍五入, 否则编译报错
int pi(3.14); // 好 - pi == 3.
int pi{3.14}; // 编译错误: 缩窄转换.
预处理指令不要缩进, 从行首开始, 即使预处理指令位于缩进代码块中, 指令也应从行首开始.
// 好 - 指令从行首开始
if (lopsided_score) {
#if DISASTER_PENDING // 正确 - 从行首开始
DropEverything();
# if NOTIFY // 非必要 - # 后跟空格
NotifyClient();
# endif
#endif
BackToNormal();
}
// 差 - 指令缩进
if (lopsided_score) {
#if DISASTER_PENDING // 差 - "#if" 应该放在行开头
DropEverything();
#endif // 差 - "#endif" 不要缩进
BackToNormal();
}
访问控制块的声明依次序是 public:, protected:, private:, 每个都缩进 1 个空格.,类声明基本格式如下:
class MyClass : public OtherClass {
public: // 注意有一个空格的缩进
MyClass(); // 标准的两空格缩进
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
void SomeFunctionThatDoesNothing() {
}
void set_some_var(int var) { some_var_ = var; }
int some_var() const { return some_var_; }
private:
bool SomeInternalFunction();
int some_var_;
int some_other_var_;
};
注意事项:
所有基类名应在 80 列限制下尽量与子类名放在同一行.
关键词 public:, protected:, private: 要缩进 1 个空格.
除第一个关键词 (一般是 public) 外, 其他关键词前要空一行. 如果类比较小的话也可以不空.
这些关键词后不要保留空行.
public 放在最前面, 然后是 protected, 最后是 private.
构造函数初始化列表放在同一行或按四格缩进并排多行.
下面两种初始值列表方式都可以接受:
// 如果所有变量能放在同一行:
MyClass::MyClass(int var) : some_var_(var) {
DoSomething();
}
// 如果不能放在同一行,
// 必须置于冒号后, 并缩进 4 个空格
MyClass::MyClass(int var)
: some_var_(var), some_other_var_(var + 1) {
DoSomething();
}
// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(int var)
: some_var_(var), // 4 space indent
some_other_var_(var + 1) { // lined up
DoSomething();
}
// 右大括号 } 可以和左大括号 { 放在同一行
// 如果这样做合适的话
MyClass::MyClass(int var)
: some_var_(var) {}
命名空间内容不缩进, 例如:
namespace {
void foo() { // 正确. 命名空间内没有额外的缩进.
...
}
} // namespace
不要在命名空间内缩进:
namespace {
// 错, 缩进多余了.
void foo() {
...
}
} // namespace
声明嵌套命名空间时, 每个命名空间都独立成行.
namespace foo {
namespace bar {
水平留白的使用根据在代码中的位置决定. 不要在行尾添加没意义的留白.
void f(bool b) { // 左大括号前总是有空格.
...
int i = 0; // 分号前不加空格.
// 列表初始化中大括号内的空格是可选的.
// 如果加了空格, 那么两边都要加上.
int x[] = { 0 };
int x[] = {0};
// 继承与初始化列表中的冒号前后恒有空格.
class Foo : public Bar {
public:
// 对于单行函数的实现, 在大括号内加上空格
// 然后是函数实现
Foo(int b) : Bar(), baz_(b) {} // 大括号里面是空的话, 不加空格.
void Reset() { baz_ = 0; } // 用空格把大括号与实现分开.
添加冗余的留白会给其他人编辑时造成额外负担. 因此, 行尾不要留空格. 如果确定一行代码已经修改完毕, 将多余的空格去掉; 或者在专门清理空格时去掉(尤其是在没有其他人在处理这件事的时候). (注: 现在大部分代码编辑器稍加设置后, 都支持自动删除行首/行尾空格, 如果不支持, 考虑换一款编辑器或 IDE)
if (b) { // if 条件语句和循环语句关键字后均有空格.
} else { // else 前后有空格.
}
while (test) {} // 圆括号内部不紧邻空格.
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) { // 循环和条件语句的圆括号里可以与空格紧邻.
if ( test ) { // 圆括号, 但这很少见. 总之要一致.
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) { // 循环里内 ; 后恒有空格, ; 前可以加个空格.
switch (i) {
case 1: // switch case 的冒号前无空格.
...
case 2: break; // 如果冒号有代码, 加个空格.
// 赋值运算符前后总是有空格.
x = 0;
// 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格.
// 圆括号内部没有紧邻空格.
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);
// 在参数和一元操作符之间不加空格.
x = -5;
++x;
if (x && !y)
...
// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector x;
y = static_cast(x);
// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector x;
垂直留白越少越好.
不在万不得已, 不要使用空行. 尤其是: 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行.这样同一屏可以显示的代码越多, 越容易理解程序的控制流. 当然, 过于密集的代码块和过于疏松的代码块同样难看, 这取决于你的判断. 但通常是垂直留白越少越好.
空格。有效的利用空格可以让你的程序读进来更加心旷神怡。
ha=(ha*128+*key++)%tabPtr->size;
ha = ( ha * 128 + *key++ ) % tabPtr->size;
有空格和没有空格的感觉不一样吧。一般来说,语句中要在各个操作符间加空格,函数调用时,要以各个参数间加空格。
不要把语句都写在一行上,这样很不好。
for(i=0;i´9´)&&(a[i]<´a´a[i]>´z´)) break;
这种即无空格,又无换行的程序在写什么啊?加上空格和换行吧。
for ( i=0; i ´9´ ) &&
( a[i] < ´a´ a[i] > ´z´ ) ) {
break;
}
}
函数参数多的时候,最好也换行,如:
CreateProcess(
NULL,
cmdbuf,
NULL,
NULL,
bInhH,
dwCrtFlags,
envbuf,
NULL,
&siStartInfo,
&prInfo
);
条件语句也应该在必要时换行:
if ( ch >= ´0´ ch <= ´9´
ch >= ´a´ ch <= ´z´
ch >= ´A´ ch <= ´Z´ )
空行。不要不加空行,空行可以区分不同的程序块,程序块间,最好加上空行。如:
HANDLE hProcess;
PROCESS_T procInfo;
/* open the process handle */
if((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)) == NULL)
{
return LSE_MISC_SYS;
}
memset(&procInfo, 0, sizeof(procInfo));
procInfo.idProc = pid;
procInfo.hdProc = hProcess;
procInfo.misc = MSCAVA_PROC;
return(0);
用TAB键对齐你的一些变量的声明或注释,一样会让你的程序好看一些。如:
typedef strUCt _pt_man_t_ {
int numProc; /* Number of processes */
int maXProc; /* Max Number of processes */
int numEvnt; /* Number of events */
int maxEvnt; /* Max Number of events */
HANDLE* pHndEvnt; /* Array of events */
DWord timeout; /* Time out interval */
HANDLE hPipe; /* Namedpipe */
TCHAR usr[MAXUSR];/* User name of the process */
int numMsg; /* Number of Message */
int Msg[MAXMSG];/* Space for intro process communicate */
} PT_MAN_T;
怎么样?感觉不错吧。
不要在构造函数中调用虚函数
不要在构造函数中尝试报告一个非致命错误,如果代码允许, 直接终止程序是一个合适的处理错误的方式. 否则, 考虑用 Init() 方法或使用工厂模式。
不要定义隐式类型转换. 对于转换运算符和单参数构造函数, 要使用 explicit 关键字
拷贝和移动构造函数不应当被标记为 explicit, 因为它们并不执行类型转换。
接受一个 std::initializer_list 作为参数的构造函数也不应当标记 explicit,以便支持拷贝初始化 (例如 MyType m = {1, 2};).
如果你的类型需要就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用
由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现.
如果你的类不需要拷贝 / 移动操作, 请显式地通过在 public 域中使用 = delete 或其他手段禁用之
// MyClass is neither copyable nor movable.
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
仅当只有数据成员时使用 struct, 其它一概使用 class.
为了和 STL 保持一致, 对于仿函数等特性可以不用 class 而是使用 struct.
所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象。
基类析构函数声明为 virtual. 如果你的类有虚函数, 则析构函数也应该为虚函数.
在派生类中被重写虚函数或虚析构函数, 要使用 override关键字进行标记,这样如果不是对基类虚函数的重写的话, 编译会报错, 这有助于捕获常见的错误。
真正需要用到多重实现继承的情况很少. 只在以下情况才允许多重继承: 最多只有一个基类是非抽象类;,其它基类都是以 Interface 为后缀的纯接口类。
当一个类满足以下要求时, 称之为纯接口:
1.只有纯虚函数 (”=0”) 和静态函数
2.没有定义任何构造函数,如果有,也不能带有参数,并且必须为 protected
接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数
接口类命名应以 Interface 为后缀
除少数特定环境外, 不要重载运算符。
将所有数据成员声明为 private, 除非是 static const 类型成员
类定义一般应以 public,开始, 后跟 protected,最后是 private。
禁止使用 RTTI。
RTTI 允许程序员在运行时识别 C++ 类对象的类型. 它通过使用 typeid 或者 dynamic_cast 完成
示例
bool Base::Equal(Base* other) = 0;
bool Derived::Equal(Base* other) {
Derived* that = dynamic_cast(other);
if (that == NULL)
return false;
...
}
if (typeid(*data) == typeid(D1)) {
...
} else if (typeid(*data) == typeid(D2)) {
...
} else if (typeid(*data) == typeid(D3)) {
...
一旦在类层级中加入新的子类, 像上面这样的代码往往会崩溃. 而且, 一旦某个子类的属性改变了, 很难找到并修改所有受影响的代码块.
如果需要在运行期间确定一个对象的类型, 这通常说明需要考虑重新设计你的类.
不要使用 C 风格类型转换. 而应该使用 C++ 风格.
用 static_cast 替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时.
用 const_cast 去掉 const 限定符.
用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换。
不要使用流, 除非是日志接口需要. 使用 printf 、fprintf之类的代替.
原因:
流最大的优势是在输出时不需要关心打印对象的类型. 这是一个亮点. 同时, 也是一个不足: 你很容易用错类型, 而编译器不会报警. 使用流时容易造成的这类错误:
cout << this; // 输出地址
cout << *this; // 输出值
由于 << 被重载, 编译器不会报错
有人说 printf 的格式化丑陋不堪, 易读性差, 但流也好不到哪儿去. 看看下面两段代码吧, 实现相同的功能, 哪个更清晰?
cerr << "Error connecting to '" << foo->bar()->hostname.first
<< ":" << foo->bar()->hostname.second << ": " << strerror(errno);
fprintf(stderr, "Error connecting to '%s:%u: %s",
foo->bar()->hostname.first, foo->bar()->hostname.second,
strerror(errno));
对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符,对简单数值 (非对象), (++i,i++)两种都无所谓。
定义了 int16_t, uint32_t, int64_t 等整型, 在需要确保整型大小时可以使用它们代替 short, unsigned long long 等. 在 C 整型中, 只使用 int. 在合适的情况下, 推荐使用标准类型如 size_t
如果已知整数不会太大, 我们常常会使用 int, 如循环计数. 在类似的情况下使用原生类型 int,如果需要 64 位整型, 用 int64_t 或 uint64_t.
创建 64 位常量时使用 LL 或 ULL 作为后缀
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
尽可能用 sizeof(varname) 代替 sizeof(type),使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新。
Struct data;
Struct data; memset(&data, 0, sizeof(data));
auto 只能用在局部变量里。不要用在文件作用域变量,命名空间作用域变量和类数据成员里。
永远别列表初始化 auto 变量,示例
auto x(3); // 圆括号。
auto y{3}; // 大括号。
它们的类型是不一样的x 是 int, y 则是 std::initializer_list.
C++11 中任何对象类型都可以被列表初始化,示例如下
// Vector 接收了一个初始化列表。
vector v{"foo", "bar"};
// 不考虑细节上的微妙差别,大致上相同。
// 您可以任选其一。
vector v = {"foo", "bar"};
// 可以配合 new 一起用。
auto p = new vector{"foo", "bar"};
// map 接收了一些 pair, 列表初始化大显神威。
map m = {{1, "one"}, {2, "2"}};
// 初始化列表也可以用在返回类型上的隐式转换。
vector test_function() { return {1, 2, 3}; }
// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}
// 在函数调用里用列表初始化。
void TestFunction2(vector v) {}
TestFunction2({1, 2, 3});
用户自定义类型也可以定义接收 std::initializer_list 的构造函数和赋值运算符,以自动列表初始化:
class MyType {
public:
// std::initializer_list 专门接收 init 列表。
// 得以值传递。
MyType(std::initializer_list init_list) {
for (int i : init_list) append(i);
}
MyType& operator=(std::initializer_list init_list) {
clear();
for (int i : init_list) append(i);
}
};
MyType m{2, 3, 5, 7};
千万别直接列表初始化 auto 变量,看下一句,估计没人看得懂:
auto d = {1.23}; // d 是 std::initializer_list
auto d = double{1.23}; //d 为 double, 并非 std::initializer_list.
禁用默认捕获,捕获都要显式写出来。打比方,比起 [=](int x) {return x + n;}, 您该写成 [n](int x) {return x + n;} 才对,这样读者也好一眼看出 n 是被捕获的值。
匿名函数始终要简短,如果函数体超过了五行,那么还不如起名(即把 lambda 表达式赋值给对象)或改用函数。
不要使用复杂的模板编程,模板编程有时候能够实现更简洁更易用的接口, 但是更多的时候却适得其反. 因此模板编程最好只用在少量的基础组件, 基础数据结构上, 因为模板带来的额外的维护成本会被大量的使用给分担掉
最好按值返回, 否则按引用返回, 避免返回指针 ,除非它可以为空.。
我们倾向于编写简短, 凝练的函数.
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug.使函数尽量简短, 以便于他人阅读和修改代码.
在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码: 如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数.
所有按引用传递的参数必须加上 const.
如果打算重载一个函数, 最好在函数名里加上参数信息. 例如, 用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append().
如果重载函数的目的是为了支持不同数量的同一
类型参数, 则优先考虑使用 std::vector 以便使用者可以用列表初始化 指定参数.
对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作. 如果在每个调用点缺省参数的值都有可能不同, 在这种情况下缺省函数也不允许使用.
例如, 不要写像 void f(int n = counter++); 这样的代码.
只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前. 只有在必需的时候(如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法. 但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中, 而多数情况下不鼓励写这样复杂的模板代码.
动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权.
如果必须使用动态分配, 那么更倾向于将所有权保持在分配者手中. 如果其他地方要使用这个对象, 最好传递它的拷贝, 或者传递一个不用改变所有权的指针或引用. 倾向于使用 std::unique_ptr 来明确所有权传递, 例如:
std::unique_ptr FooFactory();
void FooConsumer(std::unique_ptr ptr);
如果没有很好的理由, 则不要使用共享所有权. 这里的理由可以是为了避免开销昂贵的拷贝操作, 但是只有当性能提升非常明显, 并且操作的对象是不可变的(比如说 std::shared_ptr )时候, 才能这么做. 如果确实要使用共享所有权, 建议于使用 std::shared_ptr .
不要使用 std::auto_ptr, 使用 std::unique_ptr 代替它.
使用 cpplint.py 检查风格错误.
cpplint.py 是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报,但它仍然是一个非常有用的工具. 在行尾加 // NOLINT, 或在上一行加 // NOLINTNEXTLINE, 可以忽略报错.
某些项目会指导你如何使用他们的项目工具运行 cpplint.py. 如果你参与的项目没有提供, 你可以单独下载 cpplint.py.
http://github.com/google/styleguide/blob/gh-pages/cpplint/cpplint.py
只在定义移动构造函数与移动赋值操作时使用右值引用. 不要使用 std::forward 功能函数..
定义:
右值引用是一种只能绑定到临时对象的引用的一种, 其语法与传统的引用语法相似. 例如,
void f(string&& s); 声明了一个其参数是一个字符串的右值引用的函数.
你可能会使用 std::move 来表示将值从一个对象移动而不是复制到另一个对象.
我们不允许使用变长数组和 alloca().
优点:
变长数组具有浑然天成的语法. 变长数组和 alloca() 也都很高效.
缺点:
变长数组和 alloca() 不是标准 C++ 的组成部分. 更重要的是, 它们根据数据大小动态分配堆栈内存, 会引起难以发现的内存越界 bugs: “在我的机器上运行的好好的, 发布后却莫名其妙的挂掉了”.
改用更安全的分配器(allocator),就像 std::vector 或 std::unique_ptr.
Tip: 我们允许合理的使用友元类及友元函数.
通常友元应该定义在同一文件内, 避免代码读者跑到其它文件查找使用该私有成员的类. 经常用到友元的一个地方是将 FooBuilder 声明为 Foo 的友元, 以便 FooBuilder 正确构造 Foo 的内部状态, 而无需将该状态暴露出来. 某些情况下, 将一个单元测试类声明成待测类的友元会很方便.
友元扩大了 (但没有打破) 类的封装边界. 某些情况下, 相对于将类成员声明为 public, 使用友元是更好的选择, 尤其是如果你只允许另一个类访问该类的私有成员时. 当然, 大多数类都只应该通过其提供的公有成员进行互操作.
我们不使用 C++ 异常.
从表面上看来,使用异常利大于弊, 尤其是在新项目中. 但是对于现有代码, 引入异常会牵连到所有相关代码. 如果新项目允许异常向外扩散, 在跟以前未使用异常的代码整合时也将是个麻烦.
我们强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的 constexpr 更好。
const 变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障; 便于尽早发现错误. 因此, 我们强烈建议在任何可能的情况下使用 const:
• 如果函数不会修改你传入的引用或指针类型参数, 该参数应声明为 const.
• 尽可能将函数声明为 const. 访问函数应该总是 const. 其他不会修改任何数据成员, 未调用非 const 函数, 不会返回数据成员非 const 指针或引用的函数也应该声明成 const.
• 如果数据成员在对象构造之后不再发生变化, 可将其定义为 const.
不要在一些地方把 const 写在类型前面, 在其他地方又写在后面, 确定一种写法, 然后保持一致。
在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。
靠 constexpr 特性,方才实现了 C++ 在接口上打造真正常量机制的可能。好好用 constexpr来定义真·常量以及支持常量的函数。避免复杂的函数定义,以使其能够与 constexpr 一起使用。千万别痴心妄想地想靠 constexpr 来强制代码「内联」。
代码应该对 64 位和 32 位系统友好. 处理打印, 比较, 结构体对齐时应切记:
• 记住 sizeof(void *) != sizeof(int). 如果需要一个指针大小的整数要用 intptr_t.
• 你要非常小心的对待结构体对齐,在 64 位系统中, 任何含有 int64_t/uint64_t 成员的类/结构体, 缺省都以 8 字节在结尾对齐. 如果 32 位和 64 位代码要共用持久化的结构体, 需要确保两种体系结构下的结构体对齐一致. 大多数编译器都允许调整结构体对齐. gcc 中可使用
attribute((packed)). MSVC 则提供了 #pragma pack() 和 __declspec(align())。
• 如果你确实需要 32 位和 64 位系统具有不同代码, 可以使用 #ifdef _LP64 指令来切分 32/64 位代码. (尽量不要这么做, 如果非用不可, 尽量使修改局部化。
使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.
宏意味着你和编译器看到的代码是不同的. 这可能会导致异常行为, 尤其因为宏具有全局作用域.
值得庆幸的是, C++ 中, 宏不像在 C 中那么必不可少. 以往用宏展开性能关键的代码, 现在可以用内联函数替代. 用宏表示常量可被 const 变量代替. 用宏“缩写”长变量名可被引用代替. 用宏进行条件编译…这个, 千万别这么做, 会令测试更加痛苦 (#define 防止头文件重包含当然是个特例).
整数用 0, 实数用 0.0, 指针用 nullptr 或 NULL, 字符 (串) 用 '\0'.
整数用 0, 实数用 0.0, 这一点是毫无争议的.
对于指针 (地址值), 到底是用 0, NULL 还是 nullptr. C++11 项目用 nullptr; C++03 项目则用 NULL,毕竟它看起来像指针。实际上,一些 C++ 编译器对 NULL 的定义比较特殊,可以输出有用的警告,特别是 sizeof(NULL) 就和 sizeof(0) 不一样。
字符 (串) 用 '\0', 不仅类型正确而且可读性好.
只使用 Boost 中被认可的库.
定义:
Boost 库集 是一个广受欢迎, 经过同行鉴定, 免费开源的 C++ 库集.
优点:
Boost 代码质量普遍较高, 可移植性好, 填补了 C++ 标准库很多空白, 如型别的特性, 更完善的绑定器, 更好的智能指针。
缺点:
某些 Boost 库提倡的编程实践可读性差, 比如元编程和其他高级模板技术, 以及过度“函数化”的编程风格.
为了向阅读和维护代码的人员提供更好的可读性, 我们只允许使用 Boost 一部分经认可的特性子集.
适当用 C++11(前身是 C++0x)的库和语言扩展,在贵项目用 C++11 特性前三思可移植性。
写有参数的函数时,首要工作,就是要对传进来的所有参数进行合法性检查。而对于传出的参数也应该进行检查,这个动作当然应该在函数的外部,也就是说,调用完一个函数后,应该对其传出的值进行检查。
当然,检查会浪费一点时间,但为了整个系统不至于出现“非法操作”或是“Core Dump”的系统级的错误,多花这点时间还是很值得的。
经常看到这样的错误的程序:
FuncName(char* str)
{
int len = strlen(str);
.....
}
char*
GetUserName(struct user* pUser)
{
return pUser->name;
}
不!请不要这样做。
你应该先判定一下传进来的那个指针是不是为空。假如传进来的指针为空的话,那么,你的一个大的系统就会因为这一个小的函数而崩溃。一种更好的技术是使用断言(assert)。当然,假如是在C++中,引用要比指针好得多,但你也需要对各个参数进行检查。
对于一些系统调用,比如打开文件,我经常看到,许多程序员对fopen返回的指针不做任何判定,就直接使用了。然后发现文件的内容怎么也读出不出,或是怎么也写不进去。还是判定一下吧:
fp = fopen("log.txt", "a");
if ( fp == NULL ){
printf("Error: open file error ");
return FALSE;
}
其它还有许多啦,比如:socket返回的socket号,malloc返回的内存。请对这些系统调用返回的东西进行判定。
对 if 出错语句的处理。
if ( ch < ´0´ ch > ´9´ ){
/* 输出错误信息 */
printf("error ...... ");
return ( FALSE );
}
/* 正常处理代码 */
…
这种结构很不好,如果“正常处理代码”很长时,对于这种情况,最好不要用else。如:
if ( ch >= ´0´ && ch <= ´9´ ){
/* 正常处理代码 */
}else{
/* 输出错误信息 */
printf("error ...... ");
return ( FALSE );
}
stack栈上分配的内存系统自动释放,
heap堆上分配的内存,系统不释放,哪怕程序退出,那一块内存还是在那里。
stack一般是静态分配内存,heap上一般是动态分配内存。
由malloc系统函数分配的内存就是从堆上分配内存。从堆上分配的内存一定要自己释放。用free释放,不然就是术语--“内存泄露”(或是“内存漏洞”)于是,系统的可分配内存会随malloc越来越少,直到系统崩溃。在使用malloc系统函数(包括calloc,realloc)时千万要小心。
栈内存分配
-----
char*
AllocStrFromStack()
{
char pstr[100];
return pstr;
}
那块pstr的内存在函数返回时就被系统释放了。于是所返回的char*什么也没有。
堆内存分配
-----
char*
AllocStrFromHeap(int len)
{
char *pstr;
if ( len <= 0 ) return NULL;
return ( char* ) malloc( len );
}
是从堆上分配内存,所以哪怕是程序退出时,也不释放,所以第二个函数的返回的内存没有问题,可以被使用。但一定要调用free释放,不然就是Memory Leak!
1) 配对使用,有一个malloc,就应该有一个free。(C++中对应为new和delete)
2) 尽量在同一层上使用,不要像上面那种,malloc在函数中,而free在函数外。最好在同一调用层上使用这两个函数。
3) malloc分配的内存一定要初始化。free后的指针一定要设置为NULL。
注:虽然现在的操作系统(如:UNIX和Win2k/NT)都有进程内存跟踪机制,也就是假如你有没有释放的内存,操作系统会帮你释放。但操作系统依然不会释放你程序中所有产生了Memory Leak的内存,所以,最好还是你自己来做这个工作。(有的时候不知不觉就出现Memory Leak了,而且在几百万行的代码中找无异于海底捞针,Rational有一个工具叫Purify,可能很好的帮你检查程序中的Memory Leak)
1) 对malloc分配的内存进行memset清零操作。(可以使用calloc分配一块全零的内存)
2) 对一些栈上分配的struct或数组进行初始化。(最好也是清零)
char pstr; / 一个字符串 /
pstr = ( char ) malloc( 50 );
if ( pstr == NULL ) exit(0);
strcpy( pstr, “Hello Wrold” );
但假如是下面一种情况,最好进行内存初始化。(指针是一个危险的东西,一定要初始化)
char pstr; / 一个字符串数组 /
pstr = ( char ) malloc( 50 );
if ( pstr == NULL ) exit(0);
/* 让数组中的指针都指向NULL /
memset( pstr, 0, 50sizeof(char*) );
Links *plnk = NULL; /* 对于全局变量plnk初始化为NULL */
---------
H文件和C文件怎么用呢?一般来说,H文件中是declare(声明),C文件中是define(定义)。
因为C文件要编译成库文件(Windows下是.obj/.lib,UNIX下是.o/.a)。
假如别人要使用你的函数,那么就要引用你的H文件,所以,H文件中一般是变量、宏定义、枚举、结构和函数接口的声明,就像一个接口说明文件一样。而C文件则是实现细节。
H文件和C文件最大的用处就是声明和实现分开。这个特性应该是公认的了,但我仍然看到有些人喜欢把函数写在H文件中,这种习惯很不好。(假如是C++话,对于其模板函数,在VC中只有把实现和声明都写在一个文件中,因为VC不支持export关键字)。而且,假如在H文件中写上函数的实现,你还得在makefile中把头文件的依靠关系也加上去,这个就会让你的makefile很不规范。
最后,有一个最需要注重的地方就是:带初始化的全局变量不要放在H文件中!
例如有一个处理错误信息的结构:
char* errmsg[] = {
/* 0 */ "No error",
/* 1 */ "Open file error",
/* 2 */ "Failed in sending/receiving a message",
/* 3 */ "Bad arguments",
/* 4 */ "Memeroy is not enough",
/* 5 */ "Service is down; try later",
/* 6 */ "Unknow information",
/* 7 */ "A socket operation has failed",
/* 8 */ "Permission denied",
/* 9 */ "Bad configuration file format",
/* 10 */ "Communication time out",
......
......
};
请不要把这个东西放在头文件中,因为假如你的这个头文件被5个函数库(.lib或是.a)所用到,于是他就被链接在这5个.lib或.a中,而假如你的一个程序用到了这5个函数库中的函数,并且这些函数都用到了这个出错信息数组。那么这份信息将有5个副本存在于你的执行文件中。假如你的这个errmsg很大的话,而且你用到的函数库更多的话,你的执行文件也会变得很大。
正确的写法应该把它写到C文件中,然后在各个需要用到errmsg的C文件头上加上 extern char* errmsg[]; 的外部声明,让编译器在链接时才去管他,这样一来,就只会有一个errmsg存在于执行文件中,而且,这样做很利于封装。
〔备注〕
-----
有朋友对我说,这个只是一个特例,因为,假如errmsg在执行文件中存在多个副本时,可以加快程序运行速度,理由是errmsg的多个复本会让系统的内存换页降低,达到效率提升。像我们这里所说的errmsg只有一份,当某函数要用errmsg时,假如内存隔得比较远,会产生换页,反而效率不高。
这个说法不无道理,但是一般而言,对于一个比较大的系统,errmsg是比较大的,所以产生副本导致执行文件尺寸变大,不仅增加了系统装载时间,也会让一个程序在内存中占更多的页面。而对于errmsg这样数据,一般来说,在系统运行时不会经常用到,所以还是产生的内存换页也就不算频繁。权衡之下,还是只有一份errmsg的效率高。即便是像logmsg这样频繁使用的的数据,操作系统的内存调度算法会让这样的频繁使用的页面常驻于内存,所以也就不会出现内存换页问题了。
---------
你会处理出错信息吗?哦,它并不是简单的输出。看下面的示例:
if ( p == NULL ){
printf ( "ERR: The pointer is NULL " );
}
离别学生时代的编程吧。这种编程很不利于维护和治理,出错信息或是提示信息,应该统一处理,而不是像上面这样,写成一个“硬编码”。第10条对这方面的处理做了一部分说明。假如要治理错误信息,那就要有以下的处理:
/* 声明出错代码 */
#define ERR_NO_ERROR 0 /* No error */
#define ERR_OPEN_FILE 1 /* Open file error */
#define ERR_SEND_MESG 2 /* sending a message error */
#define ERR_BAD_ARGS 3 /* Bad arguments */
#define ERR_MEM_NONE 4 /* Memeroy is not enough */
#define ERR_SERV_DOWN 5 /* Service down try later */
#define ERR_UNKNOW_INFO 6 /* Unknow information */
#define ERR_SOCKET_ERR 7 /* Socket operation failed */
#define ERR_PERMISSION 8 /* Permission denied */
#define ERR_BAD_FORMAT 9 /* Bad configuration file */
#define ERR_TIME_OUT 10 /* Communication time out */
/* 声明出错信息 */
char* errmsg[] = {
/* 0 */ "No error",
/* 1 */ "Open file error",
/* 2 */ "Failed in sending/receiving a message",
/* 3 */ "Bad arguments",
/* 4 */ "Memeroy is not enough",
/* 5 */ "Service is down; try later",
/* 6 */ "Unknow information",
/* 7 */ "A socket operation has failed",
/* 8 */ "Permission denied",
/* 9 */ "Bad configuration file format",
/* 10 */ "Communication time out",
};
/* 声明错误代码全局变量 */
long errno = 0;
/* 打印出错信息函数 */
void perror( char* info)
{
if ( info ){
printf("%s: %s ", info, errmsg[errno] );
return;
}
printf("Error: %s ", errmsg[errno] );
}
这个基本上是ANSI的错误处理实现细节了,于是当你程序中有错误时你就可以这样处理:
bool CheckPermission( char* userName )
{
if ( strcpy(userName, "root") != 0 ){
errno = ERR_PERMISSION_DENIED;
return (FALSE);
}
...
}
main()
{
...
if (! CheckPermission( username ) ){
perror("main()");
}
...
}
一个即有共性,也有个性的错误信息处理,这样做有利同种错误出一样的信息,统一用户界面,而不会因为文件打开失败,A程序员出一个信息,B程序员又出一个信息。而且这样做,非常轻易维护。代码也易读。
当然,物极必反,也没有必要把所有的输出都放到errmsg中,抽取比较重要的出错信息或是提示信息是其要害,但即使这样,这也包括了大多数的信息。
-----------------
看一下下面这个例子:
for( i=0; i<1000; i++ ){
GetLocalHostName( hostname );
...
}
GetLocalHostName的意思是取得当前计算机名,在循环体中,它会被调用1000次啊。这是多么的没有效率的事啊。应该把这个函数拿到循环体外,这样只调用一次,效率得到了很大的提高。虽然,我们的编译器会进行优化,会把循环体内的不变的东西拿到循环外面,但是,你相信所有编译器会知道哪些是不变的吗?我觉得编译器不可靠。最好还是自己动手吧。
同样,对于常用函数中的不变量,如:
GetLocalHostName(char* name)
{
char funcName[] = "GetLocalHostName";
sys_log( "%s begin......", funcName );
...
sys_log( "%s end......", funcName );
}
假如这是一个经常调用的函数,每次调用时都要对funcName进行分配内存,这个开销很大啊。把这个变量声明成static吧,当函数再次被调用时,就会省去了分配内存的开销,执行效率也很好。
------------
向函数传参数时,一般而言,传入非const的指针时,就表示,在函数中要修改这个指针指向的内存中的数据。假如是传值,那么无论在函数内部怎么修改这个值,也影响不到传过来的值,因为传值是只内存拷贝。
什么?你说这个特性你明白了,好吧,让我们看看下面的这个例程:
void
GetVersion(char* pStr)
{
pStr = malloc(10);
strcpy ( pStr, "2.0" );
}
main()
{
char* ver = NULL;
GetVersion ( ver );
...
...
free ( ver );
}
我保证,类似这样的问题是一个新手最轻易犯的错误。程序中妄图通过函数GetVersion给指针ver分配空间,但这种方法根本没有什么作用,原因就是--这是传值,不是传指针。你或许会和我争论,我分明传的时指针啊?再仔细看看,其实,你传的是指针其实是在传值。
-----------
当你维护别人的程序时,请不要非常主观臆断的把已有的程序删除或是修改。我经常看到有的程序员直接在别人的程序上修改表达式或是语句。修改别人的程序时,请不要删除别人的程序,假如你觉得别人的程序有所不妥,请注释掉,然后添加自己的处理程序,必竟,你不可能100%的知道别人的意图,所以为了可以恢复,请不依靠于CVS或是SourceSafe这种版本控制软件,还是要在源码上给别人看到你修改程序的意图和步骤。这是程序维护时,一个有修养的程序员所应该做的。
如下所示,这就是一种比较好的修改方法:
/*
* ----- commented by haoel 2003/04/12 ------
*
* char* p = ( char* ) malloc( 10 );
* memset( p, 0, 10 );
*/
/* ------ Added by haoel 2003/04/12 ----- */
char* p = ( char* )calloc( 10, sizeof char );
/* ---------------------------------------- */
...
当然,这种方法是在软件维护时使用的,这样的方法,可以让再维护的人很轻易知道以前的代码更改的动作和意图,而且这也是对原作者的一种尊敬。
以“注释 - 添加”方式修改别人的程序,要好于直接删除别人的程序。
---------------------
假如你有一些程序的代码片段很相似,或直接就是一样的,请把他们放在一个函数中。而假如这段代码不多,而且会被经常使用,你还想避免函数调用的开销,那么就把他写成宏吧。
千万不要让同一份代码或是功能相似的代码在多个地方存在,不然假如功能一变,你就要修改好几处地方,这种会给维护带来巨大的麻烦,所以,做到“一改百改”,还是要形成函数或是宏。
---------
假如一个比较复杂的表达式中,你并不是很清楚各个操作符的忧先级,即使是你很清楚优先级,也请加上括号,不然,别人或是自己下一次读程序时,一不小心就看走眼理解错了,为了避免这种“误解”,还有让自己的程序更为清淅,还是加上括号吧。
比如,对一个结构的成员取地址:
GetUserAge( &( UserInfo->age ) );
虽然,&UserInfo->age中,->操作符的优先级最高,但加上一个括号,会让人一眼就看明白你的代码是什么意思。
再比如,一个很长的条件判定:
if ( ( ch[0] >= ´0´ ch[0] <= ´9´ ) &&
( ch[1] >= ´a´ ch[1] <= ´z´ ) &&
( ch[2] >= ´A´ ch[2] <= ´Z´ ) )
括号,再加上空格和换行,你的代码是不是很轻易读懂了?
-----------
对于一些函数中的指针参数,假如在函数中只读,请将其用const修饰,这样,别人一读到你的函数接口时,就会知道你的意图是这个参数是[in],假如没有const时,参数表示[in/out],注重函数接口中的const使用,利于程序的维护和避免犯一些错误。
虽然,const 修饰的指针,如:const char* p,在C中一点用也没有,因为不管你的声明是不是 const,指针的内容照样能改,因为编译器会强制转换,但是加上这样一个说明,有利于程序的阅读和编译。因为在C中,修改一个 const 指针所指向的内存时,会报一个 Warning。这会引起程序员的注重。
C++中对 const 定义的就很严格了,所以C++中要多多的使用 const,const 的成员函数,const 的变量,这样会对让你的代码和你的程序更加完整和易读。(关于C++的 const 我就不多说了)
-----------------
函数的参数个数最好不要太多,一般来说6个左右就可以了,众多的函数参数会让读代码的人一眼看上去就很头昏,而且也不利于维护。假如参数众多,还请使用结构来传递参数。这样做有利于数据的封装和程序的简洁性。
也利于使用函数的人,因为假如你的函数个数很多,比如12个,调用者很轻易搞错参数的顺序和个数,而使用结构struct来传递参数,就可以不管参数的顺序。
而且,函数很轻易被修改,假如需要给函数增加参数,不需要更改函数接口,只需更改结构体和函数内部处理,而对于调用函数的程序来说,这个动作是透明的。
--------------
我看到很多程序写函数时,在函数的返回类型方面不太注重。假如一个函数没有返回值,也请在函数前面加上 void 的修饰。而有的程序员偷懒,在返回 int 的函数则什么不修饰(因为假如不修饰,则默认返回 int),这种习惯很不好,还是为了原代码的易读性,加上 int 吧。
所以函数的返回值类型,请不要省略。
另外,对于 void 的函数,我们往往会忘了 return,由于某些C/C++的编译器比较敏感,会报一些警告,所以即使是void的函数,我们在内部最好也要加上return的语句,这有助于代码的编译。
---------
N年前,软件开发的一代宗师--迪杰斯特拉(Dijkstra)说过:“goto statment is harmful !!”,并建议取消goto语句。因为goto语句不利于程序代码的维护性。
这里我也强烈建议不要使用goto语句,除非下面的这种情况:
#define FREE§ if§ {
free§;
p = NULL;
}
main()
{
char fname=NULL, lname=NULL, mname=NULL;
fname = ( char ) calloc ( 20, sizeof(char) );
if ( fname == NULL ){
goto ErrHandle;
}
lname = ( char ) calloc ( 20, sizeof(char) );
if ( lname == NULL ){
goto ErrHandle;
}
mname = ( char ) calloc ( 20, sizeof(char) );
if ( mname == NULL ){
goto ErrHandle;
}
…
ErrHandle:
FREE(fname);
FREE(lname);
FREE(mname);
ReportError(ERR_NO_MEMOEY);
}
也只有在这种情况下,goto语句会让你的程序更易读,更轻易维护。(在用嵌C来对数据库设置游标操作时,或是对数据库建立链接时,也会碰到这种结构)
------
很多程序员不知道C中的“宏”到底是什么意思?非凡是当宏有参数的时候,经常把宏和函数混淆。我想在这里我还是先讲讲“宏”,宏只是一种定义,他定义了一个语句块,当程序编译时,编译器首先要执行一个“替换”源程序的动作,把宏引用的地方替换成宏定义的语句块,就像文本文件替换一样。这个动作术语叫“宏的展开”
使用宏是比较“危险”的,因为你不知道宏展开后会是什么一个样子。例如下面这个宏:
#define MAX(a, b) a>b?a:b
当我们这样使用宏时,没有什么问题: MAX( num1, num2 ); 因为宏展开后变成 num1>num2?num1:num2;。但是,假如是这样调用的,MAX( 17+32, 25+21 ); 呢,编译时出现错误,原因是,宏展开后变成:17+32>25+21?17+32:25+21,哇,这是什么啊?
所以,宏在使用时,参数一定要加上括号,上述的那个例子改成如下所示就能解决问题了。
#define MAX( (a), (b) ) (a)>(b)?(a):(b)
即使是这样,也不这个宏也还是有Bug,因为假如我这样调用 MAX(i++, j++); ,经过这个宏以后,i和j都被累加了两次,这绝不是我们想要的。
所以,在宏的使用上还是要谨慎考虑,因为宏展开是的结果是很难让人预料的。而且虽然,宏的执行很快(因为没有函数调用的开销),但宏会让源代码澎涨,使目标文件尺寸变大,(如:一个50行的宏,程序中有1000个地方用到,宏展开后会很不得了),相反不能让程序执行得更快(因为执行文件变大,运行时系统换页频繁)。
因此,在决定是用函数,还是用宏时得要小心。
--------
static 关键字,表示了“静态”,一般来说,他会被经常用于变量和函数。一个 static 的变量,其实就是全局变量,只不过他是有作用域的全局变量。比如一个函数中的 static 变量:
char*
getConsumerName()
{
static int cnt = 0;
....
cnt++;
....
}
cnt变量的值会跟随着函数的调用次而递增,函数退出后,cnt的值还存在,只是cnt只能在函数中才能被访问。而cnt的内存也只会在函数第一次被调用时才会被分配和初始化,以后每次进入函数,都不为 static 分配了,而直接使用上一次的值。
对于一些被经常调用的函数内的常量,最好也声明成 static(参见第12条)
但 static 的最多的用处却不在这里,其最大的作用的控制访问,在C中假如一个函数或是一个全局变量被声明为 static,那么,这个函数和这个全局变量,将只能在这个C文件中被访问,假如别的C文件中调用这个C文件中的函数,或是使用其中的全局(用extern要害字),将会发生链接时错误。这个特性可以用于数据和程序保密。
----------
一个函数完成一个具体的功能,一般来说,一个函数中的代码最好不要超过600行左右,越少越好,最好的函数一般在100行以内,300行左右的孙函数就差不多了。有证据表明,一个函数中的代码假如超过500行,就会有和别的函数相同或是相近的代码,也就是说,就可以再写另一个函数。
另外,函数一般是完成一个特定的功能,千万忌讳在一个函数中做许多件不同的事。函数的功能越单一越好,一方面有利于函数的易读性,另一方面更有利于代码的维护和重用,功能越单一表示这个函数就越可能给更多的程序提供服务,也就是说共性就越多。
虽然函数的调用会有一定的开销,但比起软件后期维护来说,增加一些运行时的开销而换来更好的可维护性和代码重用性,是很值得的一件事。
---------
typedef 是一个给类型起别名的要害字。不要小看了它,它对于你代码的维护会有很好的作用。比如C中没有bool,于是在一个软件中,一些程序员使用 int,一些程序员使用 short,会比较混乱,最好就是用一个 typedef 来定义,如:
typedef char bool;
一般来说,一个C的工程中一定要做一些这方面的工作,因为你会涉及到跨平台,不同的平台会有不同的字长,所以利用预编译和typedef可以让你最有效的维护你的代码,如下所示:
#ifdef SOLARIS2_5
typedef boolean_t BOOL_T;
#else
typedef int BOOL_T;
#endif
typedef short INT16_T;
typedef unsigned short UINT16_T;
typedef int INT32_T;
typedef unsigned int UINT32_T;
#ifdef WIN32
typedef _int64 INT64_T;
#else
typedef long long INT64_T;
#endif
typedef float FLOAT32_T;
typedef char* STRING_T;
typedef unsigned char BYTE_T;
typedef time_t TIME_T;
typedef INT32_T PID_T;
使用 typedef 的其它规范是,在结构和函数指针时,也最好用 typedef,这也有利于程序的易读和可维护性。如:
typedef struct _hostinfo {
HOSTID_T host;
INT32_T hostId;
STRING_T hostType;
STRING_T hostModel;
FLOAT32_T cpuFactor;
INT32_T numCPUs;
INT32_T nDisks;
INT32_T memory;
INT32_T swap;
} HostInfo;
typedef INT32_T (*RsrcReqHandler)(
void *info,
JobArray *jobs,
AllocInfo *allocInfo,
AllocList *allocList);
C++中这样也是很让人易读的:
typedef CArray HostInfoArray;
于是,当我们用其定义变量时,会显得十分易读。如:
HostInfo* phinfo;
RsrcReqHandler* pRsrcHand;
这种方式的易读性,在函数的参数中十分明显。
要害是在程序种使用 typedef 后,几乎所有的程序中的类型声明都显得那么简洁和清淅,而且易于维护,这才是 typedef 的要害。
--------
最好不要在程序中出现数字式的“硬编码”,如:
int user[120];
为这个 120 声明一个宏吧。为所有出现在程序中的这样的常量都声明一个宏吧。比如TimeOut的时间,最大的用户数量,还有其它,只要是常量就应该声明成宏。假如,忽然在程序中出现下面一段代码,
for ( i=0; i<120; i++){
....
}
120 是什么?为什么会是120?这种“硬编码”不仅让程序很读,而且也让程序很不好维护,假如要改变这个数字,得同时对所有程序中这个120都要做修改,这对修改程序的人来说是一个很大的痛苦。所以还是把常量声明成宏,这样,一改百改,而且也很利于程序阅读。
#define MAX_USR_CNT 120
for ( i=0; i
}
这样就很轻易了解这段程序的意图了。
有的程序员喜欢为这种变量声明全局变量,其实,全局变量应该尽量的少用,全局变量不利于封装,也不利于维护,而且对程序执行空间有一定的开销,一不小心就造成系统换页,造成程序执行速度效率等问题。所以声明成宏,即可以免去全局变量的开销,也会有速度上的优势。
-----------
有许多程序员不知道在宏定义时是否要加分号,有时,他们以为宏是一条语句,应该要加分号,这就错了。当你知道了宏的原理,你会赞同我为会么不要为宏定义加分号的。看一个例子:
#define MAXNUM 1024;
这是一个有分号的宏,假如我们这样使用:
half = MAXNUM/2;
if ( num < MAXNUM )
等等,都会造成程序的编译错误,因为,当宏展开后,他会是这个样子的:
half = 1024;/2;
if ( num < 1024; )
是的,分号也被展进去了,所以造成了程序的错误。请相信我,有时候,一个分号会让你的程序出现成百个错误。所以还是不要为宏加最后一个分号,哪怕是这样:
#define LINE “=================================”
#define PRINT_LINE printf(LINE)
#define PRINT_NLINE(n) while ( n-- >0 ) { PRINT_LINE; }
都不要在最后加上分号,当我们在程序中使用时,为之加上分号,
main()
{
char *p = LINE;
PRINT_LINE;
}
这一点非常符合习惯,而且,假如忘加了分号,编译器给出的错误提示,也会让我们很轻易看懂的。
------------
条件语句中的这两个“与 && ”和“或 || ”操作符一定要小心,它们的表现可能和你想像的不一样,这里条件语句中的有些行为需要和说一下:
express1 express2
先执行表达式express1假如为“真”,express2将不被执行,express2仅在express1为“假”时才被执行。因为第一个表达式为真了,整个表达式都为真,所以没有必要再去执行第二个表达式了。
express1 && express2
先执行表达式express1假如为“假”,express2将不被执行,express2仅在express1为“真”时才被执行。因为第一个表达式为假了,整个表达式都为假了,所以没有必要再去执行第二个表达式了。
于是,他并不是你所想像的所有的表达式都会去执行,这点一定要明白,不然你的程序会出现一些莫明的运行时错误。
例如,下面的程序:
if ( sum > 100 &&
( ( fp=fopen( filename,"a" ) ) != NULL ) {
fprintf(fp, "Warring: it beyond one hundred ");
......
}
fprintf( fp, " sum is %id ", sum );
fclose( fp );
本来的意图是,假如sum > 100 ,向文件中写一条出错信息,为了方便,把两个条件判定写在一起,于是,假如sum<=100时,打开文件的操作将不会做,最后,fprintf和fclose就会发现未知的结果。
再比如,假如我想判定一个字符是不是有内容,我得判定这个字符串指针是不为空(NULL)并且其内容不能为空(Empty),一个是空指针,一个是空内容。我也许会这样写:
if ( ( p != NULL ) && ( strlen(p) != 0 ))
于是,假如p为NULL,那么strlen(p)就不会被执行,于是,strlen也就不会因为一个空指针而“非法操作”或是一个“Core Dump”了。
记住一点,条件语句中,并非所有的语句都会执行,当你的条件语句非常多时,这点要尤其注重。
---------------
基本上来说,for 可以完成 while 的功能,我是建议尽量使用 for 语句,而不要使用 while 语句,非凡是当循环体很大时,for 的优点一下就体现出来了。
因为在 for 中,循环的初始、结束条件、循环的推进,都在一起,一眼看上去就知道这是一个什么样的循环。刚出学校的程序一般对于链接喜欢这样来:
p = pHead;
while ( p ){
...
...
p = p->next;
}
当 while 的语句块变大后,你的程序将很难读,用for就好得多:
for ( p=pHead; p; p=p->next ){
..
}
一眼就知道这个循环的开始条件,结束条件,和循环的推进。大约就能明白这个循环要做个什么事?而且,程序维护进来很轻易,不必像while一样,在一个编辑器中上上下下的捣腾。
-------------
许多程序员在使用sizeof中,喜欢sizeof变量名,例如:
int score[100];
char filename[20];
struct UserInfo usr[100];
在 sizeof 这三个的变量名时,都会返回正确的结果,于是许多程序员就开始sizeof变量名。这个习惯很虽然没有什么不好,但我还是建议 sizeof 类型。
我看到过这个的程序:
pScore = (int*) malloc( SUBJECT_CNT );
memset( pScore, 0, sizeof(pScore) );
...
此时,sizeof(pScore)返回的就是4(指针的长度),不会是整个数组,于是,memset就不能对这块内存进行初始化。为了程序的易读和易维护,我强烈建议使用类型而不是变量,如:
对于 int score: sizeof(int) * 100 /* 100个int /
对于 char filename: sizeof(char) * 20 / 20个char /
对于 struct UserInfo usr: sizeof(struct UserInfo) * 100 / 100个UserInfo */
这样的代码是不是很易读?一眼看上去就知道什么意思了。
另外一点,sizeof一般用于分配内存,这个特性非凡在多维数组时,就能体现出其优点了。如,给一个字符串数组分配内存,
/*
* 分配一个有20个字符串,
* 每个字符串长100的内存
*/
char* *p;
/*
* 错误的分配方法
*/
p = (char**)calloc( 20*100, sizeof(char) );
/*
* 正确的分配方法
*/
p = (char**) calloc ( 20, sizeof(char*) );
for ( i=0; i<20; i++){
/*p = (char*) calloc ( 100, sizeof(char) );*/
p[i] = (char*) calloc ( 100, sizeof(char) );
}
(注:上述语句被注释掉的是原来的,是错误的,由dasherest朋友指正,谢谢)
为了代码的易读,省去了一些判定,请注重这两种分配的方法,有本质上的差别。
----------
对于一些编译时的警告信息,请不要忽视它们。虽然,这些Warning不会妨碍目标代码的生成,但这并不意味着你的程序就是好的。必竟,并不是编译成功的程序才是正确的,编译成功只是万里长征的第一步,后面还有大风大浪在等着你。从编译程序开始,不但要改正每个error,还要修正每个warning。这是一个有修养的程序员该做的事。
一般来说,一面的一些警告信息是常见的:
1)声明了未使用的变量。(虽然编译器不会编译这种变量,但还是把它从源程序中注释或是删除吧)
2)使用了隐晦声明的函数。(也许这个函数在别的C文件中,编译时会出现这种警告,你应该这使用之前使用extern要害字声明这个函数)
3)没有转换一个指针。(例如malloc返回的指针是void的,你没有把之转成你实际类型而报警,还是手动的在之前明显的转换一下吧)
4)类型向下转换。(例如:float f = 2.0; 这种语句是会报警告的,编译会告诉你正试图把一个double转成float,你正在阉割一个变量,你真的要这样做吗?还是在2.0后面加个f吧,不然,2.0就是一个double,而不是float了)
不 管怎么说,编译器的 Warning 不要小视,最好不要忽略,一个程序都做得出来,何况几个小小的 Warning 呢?
----------------
程序在开发过程中必然有许多程序员加的调试信息。我见过许多项目组,当程序开发结束时,发动群众删除程序中的调试信息,何必呢?为什么不像VC++那样建立两个版本的目标代码?一个是debug版本的,一个是Release版的。那些调试信息是那么的宝贵,在日后的维护过程中也是很宝贵的东西,怎么能说删除就删除呢?
利用预编译技术吧,如下所示声明调试函数:
#ifdef DEBUG
void TRACE(char* fmt, …)
{
…
}
#else
#define TRACE(char* fmt, …)
#endif
于是,让所有的程序都用TRACE输出调试信息,只需要在在编译时加上一个参数“-DDEBUG”,如:
gcc -DDEBUG -o target target.c
于是,预编译器发现DEBUG变量被定义了,就会使用TRACE函数。而假如要发布给用户了,那么只需要把取消“-DDEBUG”的参数,于是所有用到TRACE宏,这个宏什么都没有,所以源程序中的所有TRACE语言全部被替换成了空。一举两得,一箭双雕,何乐而不为呢?
顺便提一下,两个很有用的系统宏,一个是“__FILE__”,一个是“__LINE__”,分别表示,所在的源文件和行号,当你调试信息或是输出错误时,可以使用这两个宏,让你一眼就能看出你的错误,出现在哪个文件的第几行中。这对于用C/C++做的大工程非常的管用。
综上所述,都是为了三大目的–
1、程序代码的易读性。
2、程序代码的可维护性,
3、程序代码的稳定可靠性。
有修养的程序员,就应该要学会写出这样的代码!这是任何一个想做编程高手所必需面对的细小的问题,编程高手不仅技术要强,基础要好,而且最重要的是要有“修养”!
好的软件产品绝不仅仅是技术,而更多的是整个软件的易维护和可靠性。
软件的维护有大量的工作量花在代码的维护上,软件的Upgrade,也有大量的工作花在代码的组织上,所以好的代码,清淅的,易读的代码,将给大大减少软件的维护和升级成本。