cocos2d-x中的C++ 编码规范
- 声明务必要看
- 头文件
- define用法
- 前向声明
- 内联函数
- -inlh文件
- 函数参数顺序
- include的命名和顺序
- 作用域
- 命名空间
- 非命名的命名空间
- 命名空间的使用规则
- 嵌套类
- 非成员函数静态成员函数全局函数
- 局部变量
- 静态变量和全局变量
- 类
- 在构造函数里面完成工作
- 初始化
- 显式构造函数
- 拷贝构造函数
- 委派和继承构造函数
- 结构体 vs 类
- 继承
- 多重继承
- 接口
- 操作符重载
- 访问控制
- 声明顺序
- 编写短函数
- 其它C特性
- 所有权和智能指针
- 引用参数
- 右值引用
- 函数重载
- 缺省参数
- 变长数组和alloca
- 友元
- 异常
- 运行时类型识别
- 转换
- 流
- 前置自增和自减
- const用法
- constexpr用法
- Integer类型
- Unsigned Integers类型
- 64位移植性
- 预处理宏
- 0和nullptrNULL
- sizeof
- auto
- 大括号初始化
- Lambda表达式
- Boost
- C11
- 文件名
- 类型名
- 变量名
- 结构体变量
- 全局变量
- 常量名
- 一般函数
- 访问器和存储器
- 命名空间的名称
- 枚举器名称
- 宏命名
- 注释
- Doxygen
- 法律声明和作者
- 类注释
- 函数注释
- 函数声明
- 函数定义
- 变量注释
- 类成员
- 全局变量
- 实现注释
- 类数据成员
- 单行注释
- nullptrNULL truefalse 1 2 3
- Donts
- 标点拼写和语法
- TODO注释
- 弃用注释
- 格式化
- 行长度
- 非ASCII字符
- 空格还是制表位
- 函数声明与定义
- 函数调用
- 大括号初始化列表
- 条件语句
- 循环和选择语句
- 指针和引用表达式
- 布尔表达式
- 返回值
- 变量和数组初始化
- 预处理器指令
- 类格式
- 构造函数初始化列表
- 命名空间格式化
- 水平空白
- 一般
- 循环和条件
- 操作符
- 模版和类型转换
- 垂直空白
- 例外的规则
- 现存的不符合标准的代码
- Windows代码
- 赠言
此文档主要参考于cocos官网的帮助文档,但由于没有目录功能,所以自己结合《c++ primer》编写此文档。
在此,为了避免误人子弟特此声明,这并不是c++知识点总结的技能型干货文章,只是 编码规范。。。。。
(但是我开始天真的以为这是官网为我们准备的c++学习文档,在重整理的途中还加了很多书上和网上的经典文章(
所以,这并不太适合刚入门想学c++的新手看。。。。
在此,再分享一个教学视频:轻松理解Effective C++:
http://edu.9miao.com/course/50
为了确保各个文件中类的定义一致,类通常被定义在头文件中。一般情况下,每个.CPP文件应该有一个相关的.h文件,而且名字一致。有一些常见的例外,如单元测试代码和只包含一个main函数的cpp文件。
但是可能由于头文件和类文件同时包含其他类的头文件从而导致头文件多次被包含而不安全(会编译出错,库文件不会,有预编译命令)
确保多次包含而安全的常用技术是预处理器,当预处理器看到#include标记时,会自动用该头文件内容替换#include
还有一种功能是头文件保护符,接下来会讲到
所有头文件应该由#define防护,以避免多重包含。符号名称的格式应该是<PROJECT>_<PATH>_<FILE>_H_。
为了保证唯一性,它们应根据在项目的源代码树的完整路径。例如,在文件中FOO项目cocos2dx/sprites_nodes/CCSprite.h应具有以下防护:
这里写代码片
#ifndef COCOS2DX_SPRITE_NODES_CCSPRITE_H_
#define COCOS2DX_SPRITE_NODES_CCSPRITE_H_
...
#endif // COCOS2DX_SPRITE_NODES_CCSPRITE_H_
这里写代码片
// Pragma once is still open for debate (讨论)
#pragma once
我们在考虑是是否使用#pragma once,我们不确定他能支持所有平台。
ifndef: 当且仅当变量未定义时为真,一旦检查结果为真,则一直执行后续操作直到遇到#endif指令为止
前向声明普通类可以避免不必要的#includes。
定义: “前向声明”是类、函数或者模版的声明,没有定义。用前向声明来替代#include通常应用在客户端代码中。
优点:
不必要的#includes会强制编译器打开更多的文件并处理更多的输入。
不必要的#includes也会导致代码被更经常重新编译,因为头文件修改。
缺点:
不容易确定模版、typedefs、默认参数等的前向声明以及使用声明。
不容易判断对给定的代码该用前向声明还是#include,尤其是当有隐式转换时。极端情况下,用#include代替前向声明会悄悄的改变代码的含义。
在头文件中多个前向声明比#include啰嗦。
前向声明函数或者模版会阻止头文件对APIs做“否则兼容”(otherwise-compatible)修改;例如,扩展参数类型或者添加带有默认值的模版参数。
前向声明std命名空间的符号通常会产生不确定的行为。
为了前向声明而结构化代码(例如,适用指针成员,而不是对象成员)会使代码更慢更复杂。
前向声明的实际效率提升未经证实。
结论:
使用头文件中声明的函数,总是#include该头文件。
使用类模版,优先使用#include。
使用普通类,可以用前向声明,但是注意前向声明可能不够或者不正确的情况;如果有疑问,就用#include。
不应只是为了避免#include而用指针成员代替数据成员。
总是#include实际声明/定义的文件;不依赖非直接包含的头文件中间接引入的符号。例外是,Myfile.cpp可以依赖Myfile.h中的#include和前向声明。
讲了那么多最后还是说多用#include。。。看这个链接吧
http://blog.csdn.net/u012723995/article/details/47137275
定义:函数体很小——10行代码以内,用以将函数在调用点内联地展开,从而消除函数的运行时开销。在函数返回类型前面加inline
优点:内联短小精悍的函数可以生成更高效的对象码。推荐内联取值函数、设值函数以及其余性能关键的短函数。
缺点: 滥用内联可能导致程序更慢。内联可能让代码尺寸增加或者减少,这取决于函数的尺寸。内联一个非常小的取值函数通常会减少代码尺寸,而内联一个非常大的函数会显著增加代码尺寸。在现代处理器架构下,更小尺寸的代码因为可以更好的利用指令缓存,通常跑得更快。
结论:一个黄金法则是不要内联超过10行的函数。要小心析构函数,因为隐含成员和基类的析构函数,它们通常比看上去的要长。
另一个黄金法则:通常不建议内联带循环或者switch语句的函数(除非,大部分情况下,循环或者switch语句不会被执行)
需要注意的是,即便函数被声明为内联他们也不一定会真的内联;例如虚函数以及递归函数一般都不会被内联。通常递归函数不应该被内联。将虚函数内联的主要原因是为了方便或者文档需要,将其定义放在类中,例如取值函数以及设值函数。
如果有需要,可以用带-inl.h后缀的文件来定义复杂内联函数。
内联函数的定义必须放在头文件中,这样编译器在函数调用处内联展开时才有函数定义可用。但实现代码通常还是放在.cpp文件比较合适,因为除非会带来可读性或者性能上的好处,否则我们不希望在.h文件里堆放太多具体的代码。
如果一个内联函数的定义非常短,只含有少量逻辑,你可以把代码放在你的.h文件里。例如取值函数与设值函数都毫无疑问的应该放在类定义中。更复杂的内联函数为了实现者和调用者的方便,也要放在.h文件里,但是如果这样会让.h文件过于臃肿,你也可以将其放在一个单独的-inl.h文件里。这样可以将具体实现与类定义分开,同时又确保了实现在需要用到的时候是被包含的。
-inl.h文件还有一个用途是存放函数模板的定义。这样可以让你的模板定义更加易读。
不要忘记,就像其他的头文件一样,一个-inl.h文件也是需要#define防护的。
定义函数时,参数顺序应该为:输入,然后是输出。
C/C++函数的参数要么是对函数的输入,要么是函数给出的输出,要么两者兼是。输入参数通常是值或者常引用,而输出以及输入/输出参数是非const指针。 在给函数参数排序时,将所有仅输入用的参数放在一切输出参数的前面。特别需要注意的是,在加新参数时不要因为它们是新的就直接加到最后去;新的仅输入用参数仍然要放到输出参数前。
这不是一条不可动摇的铁律。那些既用于输入又用于输出的参数(通常是类/结构体)通常会把水搅浑,同时,为了保持相关函数的一致性,有时也会使你违背这条原则。
使用以下标准顺序以增加可读性,同时避免隐藏的依赖关系:C库,C++库,其他库的.h文件,你自己项目的.h文件。
所有本项目的头文件都应该包含从源代码根目录开始的完整路径,而不要使用UNIX的目录快捷方式.(当前目录)或者..(上层目录)。例如google-awesome-project/src/base/logging.h应写为以下方式
#include "base/logging.h"
例如有文件dir/foo.cpp或dir/foo_test.cpp,他们的主要用途是实现或者测试dir2/foo2.h头文件里的内容,那么include的顺序应该如下:
dir2/foo2.h (推荐位置——理由见后)
C system files.
C++ system files.
Other libraries’ .h files.
Your project’s .h files.
按照这个推荐顺序,如果dir2/foo2.h漏掉了什么必须的包含文件,dir/foo.cpp或者dir/foo_test.cpp就会编译失败。这样的规则就保证了工作在这些文件的人而不是在其他包工作的无辜的人最先发现问题。
dir/foo.cpp和dir2/foo2.h通常位于同一个目录(例如base/basictypes_test.cpp和base/basictypes.h),但是在不同目录也没问题。
在同一部分中包含文件应该按照字母顺序排列。注意如果老代码不符合这条规则,那就在方便的时候改过来。
例如cocos2dx/sprite_nodes/CCSprite.cpp的include部分可能如下:
#include "sprite_nodes/CCSprite.h" // Preferred location.
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
特例:有时候系统相关代码需要使用条件包含。这种情况下可以把条件包含放在最后。当然,要保持系统相关代码短小精悍并做好本地化。例如:
#include "foo/public/fooserver.h"
#include "base/port.h"
// For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
一段程序代码中限定这个名字的可用性的代码范围就是这个名字的作用域。(在哪儿被创建,在哪儿被销毁)
对于对象而言(其他也是一样的),在main函数中,对象的作用域为他所在的最近的一对花括号内。在后花括号处析构函数被调用;全局的对象的作用域为声明之后的整个文件,析构函数在最后被调用。另外,临时产生的对象在使用完后立即会被析构。
允许在内层作用域中重新定义外层作用域已有的名字
C/C++中作用域详解:http://blog.csdn.net/u012723995/article/details/47154289
在.cpp文件中,提倡使用未命名的命名空间(unnamed namespaces,注:未命名的命名空间就像未命名的类一样,似乎被介绍的很少:-()。使用命名的命名空间时,其名称可基于项目的路径名称。不要使用using指示符。不要使用内联命名空间。
using namespace 命名空间名称;
using 命名空间名称::成员;
第一种形式中的命名空间名称就是我们要访问的命名空间。该命名空间中的所有成员都会被引入到当前范围中。也就是说,他们都变成当前命名空间的一部分了,使用的时候不再需要使用范围限定符了。第二种形式只是让指定的命名空间中的指定成员在当前范围中变为可见。
定义: 命名空间将全局作用域细分为不同的、命名的作用域,可有效防止全局作用域的命名冲突。 基本形式:namespace 名称
优点: 命名空间提供了(层次化的)命名轴(name axis,注:将命名分割在不同命名空间内),当然,类也提供了(层次化的)的命名轴。(简单讲就是减少命名冲突)
举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时造成冲突。如果每个项目将代码置于不同命名空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突。
内联命令空间自动地将名字置于封闭作用域。例子如下:
namespace X {
inline namespace Y {
void foo();
}
}
X::Y::foo()和X::foo()是一样的。内联命名空间是为了兼容不同版本的ABI而做的扩展
缺点: 命名空间具有迷惑性,因为它们和类一样提供了额外的(层次化的)命名轴。
特别是内联命名空间,因为命名实际上并不局限于他们声明的命名空间。只有作为较大的版本控制策略的一部分时才有用。
结论: 根据下文将要提到的策略合理使用命名空间。如例子中那样结束命名空间时进行注释。
允许甚至鼓励在.cpp中使用未命名空间,以避免运行时的命名冲突:
定义:关键字namespace后直接跟有花括号括起来的一系列声明语句
未命名的命名空间中定义的变量拥有静态生命周期,在第一次使用前被创建,知道程序结束才销毁。听上去跟静态声明差不多,这里注意:
在文件中进行静态声明的做法已经被c++标准取消了,现在的做法是使用未命名的命名空间
namespace {
// This is in a .cpp file.
// The content of a namespace is not indented
enum { UNUSED, EOF, ERROR };
// Commonly used tokens.
bool atEof() { return _pos == EOF; }
// Uses our namespace"s EOF.
} // namespace
然而,与特定类关联的文件作用域声明在该类中被声明为类型、静态数据成员与静态成员函数,而不是未命名命名空间的成员。 不能在.h文件中使用未命名空间。
命名空间将除文件包含、全局标识的声明/定义以及类的前置声明外的整个源文件封装起来,以同其他命名空间相区分。
// .h文件
// 使用cocos2d命名空间
NS_CC_BEGIN
// 所有声明均在命名空间作用域内。
// 注意不用缩进。
class MyClass
{
public:
...
void foo();
};
NS_CC_END
// .h文件
// 不使用cocos2d命名空间
namespace mynamespace {
// 所有声明均在命名空间作用域中。
// 注意不用缩进。
class MyClass
{
public:
...
void foo();
};
} // namespace mynamespace
// .cpp文件
namespace mynamespace {
// 函数定义在命名空间作用域中。
void MyClass::foo()
{
...
}
} // namespace mynamespace
通常.cpp文件会包含更多、更复杂的细节,包括引用其他命名空间中的类等。
#include "a.h"
DEFINE_bool(someflag, false, "dummy flag");
class C; // 前向声明全局作用域中的类C。
namespace a { class A; } // 前向声明a::A。
namespace b {
...code for b... // 代码无缩进。
} // namespace b
不要声明std命名空间里的任何内容,包括标准库类的前置声明。声明std里的实体会导致不明确的行为,例如,不可移植。包含对应的头文件来声明标准库里的实体。 最好不要使用using指示符,以保证命名空间下的所有名称都可以正常使用。
// **禁止--污染了命名空间**。
using namespace foo;
在.h的函数、方法、类,.cpp的任何地方都可以使用using声明。
// 在.cpp中没有问题。
// 在.h中必须在函数、方法或者累中。
using ::foo::bar;
using声明能使名字空间中的一个变量或函数在名字空间外可见,而using指示符则使整个名字空间中的成员在名字空间外都可见,就像去掉名字空间一样.
在.h的函数、方法或包含整个.h的命名的命名空间中以及.cpp中,可以使用命名空间别名。
// .cpp中一些常用名的缩写
namespace fbz = ::foo::bar::baz;
// .h中一些常用名的缩写
namespace librarian {
// 包括该头文件(在librarian命名空间中)在内的所有文件都可以使用下面的别名:
// 因此同一个项目中的别名应该保持一致。
namespace pd_s = ::pipeline_diagnostics::sidetable;
inline void myInlineFunction() {
// 函数或者方法中的本地命名空间别名。
namespace fbz = ::foo::bar::baz;
...
}
} // namespace librarian
注意,.h文件中的别名对所有包含该文件的所有文件都可见,因此公共的头文件(在项目外仍可用)以及通过他们间接办好的头文件应避免定义别名,为了保持公共的APIs尽可能小。
不要用内联命名空间。
当公开嵌套类作为接口的一部分时,虽然可以直接将他们保持在全局作用域中,但将嵌套类的声明置于命名空间中是更好的选择。
定义: 可以在一个类中定义另一个类,嵌套类也称成员类(member class)。
class Foo
{
private:
// Bar是嵌套在Foo中的成员类
class Bar
{
...
};
};
优点: 当嵌套(成员)类只在被嵌套类(enclosing class)中使用时很有用,将其置于被嵌套类作用域作为被嵌套类的成员不会污染其他作用域同名类。可在被嵌套类中前置声明嵌套类,在.cpp文件中定义嵌套类,避免在被嵌套类声明中包含嵌套类的定义,因为嵌套类的定义通常只与实现相关。
缺点: 只能在被嵌套类的定义中才能前置声明嵌套类。因此,任何使用Foo::Bar*指针的头文件必须包含整个Foo类的声明。
结论: 不要将嵌套类定义为public,除非它们是接口的一部分,比如,某方法使用了这个类的一系列选项。
c++的嵌套类使用 http://blog.csdn.net/u012723995/article/details/47155719
优先使用命名空间中的非成员函数或者静态成员函数,尽可能不使用全局函数。
优点: 某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数置于命名空间中可避免污染全局命名空间。
缺点: 将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源戒具有重要依赖时更是如此。
结论: 有时,不把函数限定在类的实体中是有益的,甚至是必要的,要么作为静态成员,要么作为非成员函数。非成员函数不应依赖外部发量,并尽量置于某个命名空间中。相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间。
定义在同一编译单元的函数,可能会在被其他编译单元直接调用时引入不必要的耦合和链接依赖;静态成员函数对此尤其敏感。可以考虑提取到新类中,戒者将函数置于独立库的命名空间中。
如果你确实需要定义非成员函数,又只是在.cpp中使用,可使用未命名的命名空间或静态关联(如static int Foo() {…})限定其作用域。
定义:形参和函数体内部定义的变量
尽可能缩小函数变量的作用域,并在声明变量时将其初始化。
C++允许在函数的任何位置声明发量。我们提倡在尽可能小的作用域中声明变量,离第一次使用越近越好。这使得代码易于阅读,易于定位变量的声明位置、类型和初始值。特别是,应使用初始化代替声明+赋值的方式。
int i;
i = f(); // // 坏——初始化和声明分离
int j = g(); // // 好——声明时初始化
vector<int> v;
v.push_back(1); // 优先使用括号初始化。
v.push_back(2);
vector<int> v = {1, 2}; // 好-v有初始化。
注意:gcc可正确实现了for (int i = 0; i < 10; ++i)(i的作用域仅限for循环),因此其他for循环中可重用i。if和while等语句中,作用域声明(scope declaration)同样是正确的。
while (const char* p = strchr(str, "/")) str = p + 1;
注意:如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数。
// 低效实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // My ctor and dtor get called 1000000 times each.
f.doSomething(i);
}
//类似变量放到循环作用域外面声明要高效的多:
Foo f; // My ctor and dtor get called once each.
for (int i = 0; i < 1000000; ++i) {
f.doSomething(i);
}
类似变量放到循环作用域外面声明要高效的多:
class类型的全局变量是被禁止的:这导致隐藏很深的bugs,因为构造和析构的顺序不明确。然而,允许constexpr类型(常表达式)的静态或全局变量:他们没有动态的初始化或者析构。
class类型的全局变量:http://blog.csdn.net/u012723995/article/details/47184833
包含静态存储的对象,包括全局变量,静态变量,全局类成员变量,以及函数静态变量,必须是POD类型(Plain Old Data):只能是POD类型的整形(int)、字符(char)、浮点(float)或者指针或者数组/结构体
关于POD:http://www.cnblogs.com/tracylee/archive/2012/10/18/2730164.html
通俗简化版POD:http://blog.csdn.net/aqtata/article/details/35618709
对于静态变量,C++只定义了类构造和初始化的部分顺序,并且每次生成的结果可能不一样,这将导致隐藏很深的bugs。因此,除了禁用class类型的全局变量,也不允许使用函数的结果初始化静态POD变量,除非函数(如getenv(),getpid())本身不依赖任何其他的全局变量。
同样,全局变量和静态变量在程序终止时销毁,不管是因为main()返回还是调用了exit()。析构顺序和构造顺序刚好相反,因此析构顺序和构造顺序一样都是不明确的。例如,在程序结束时,静态变量可能已经销毁,但是仍在运行的代码-可能在另外一个线程-试图访问它,然后失败了。或者一个静态string变量先于另外一个包含该字符串的变量执行析构函数。
一个缓解析构函数问题的方法是调用quick_exit()而不是exit()来终止程序。区别是quick_exit()不调用析构函数,也不引入在atexit()中注册的任何句柄。如果程序终止时有句柄需要通过quick_exit()来运行(比如,刷新日志),在at_quick_exit()中注册它。(如果句柄需要在exit()和quick_exit()中都运行,那就在两个地方都注册)。
综上所述,只允许静态变量包含POD数据。禁用vector(用C数组代替),禁用string(用const char []代替)。
如果确实需要class类型的静态或者全局变量,考虑初始化一个指针(永不释放),要么在main()函数中,要么在pthread_once()中。注意指针必须是原始指针,不能是“智能”指针,因为智能指针的析构函数有我们一直在避免的析构函数顺序问题。
在构造函数里面避免复杂的初始化(特别是那些初始化的时候可能会失败或者需要调用虚拟函数的情况)
定义: 有可能在构造函数体内执行初始化
优点: 方便书写。不必要担心类是否已经被初始化。
缺点: 在构造函数里完成工作面临如下问题:
由于缺少异常处理(在构造函数中禁止使用),构造函数很难去定位错误。
如果初始化失败,接着我们继续使用一个初始化失败的对象,可能会出现不可以预知的状态。
如果初始化调用了虚拟函数,这些调用将不会正确的传至子类的实现。以后对该类的修改可能会悄悄的出现该问题,即使你的类当前并不是子类,也会引起混乱。
如果创建一个该类的全局变量(虽然违反规则,但是仍然有人会这样子做),构造函数代码会在main函数之前被调用,可能会破坏一些在构造函数代码里面隐含的假设,譬如,gflags还没有被初始化。
结论: 构造函数不应该调用虚函数,否则会引起非致命的错误。如果你的对象需要的初始化工作比较重要,你可以考虑使用工厂方法或者Init()方法。
cocos2dx的几种常见设计模式
http://blog.csdn.net/u012723995/article/details/47185175
如果你的类定义了成员变量,你必须在类里面为每一个成员变量提供初始化或者写一个默认的构造函数。如果你没有声明任何构造函数,编译器会为你生成一个默认的构造函数,这个默认构造函数可能没有初始化一些字段,也可能初始化为不恰当的值。
定义:当我们以无参数形式new一个类对象的时候会调用默认构造函数。当调用‘new[]’(用于创建数组)的时候默认构造函数总是会被调用。在类成员里面进行初始化是指声明一个成员变量的时候使用一个结构例如‘int _count = 17’或者‘string _name{“abc”}’来替代这样的形式‘int _count’或者‘string _name’
优点: 它能保证一个对象被创建后总是处于有效或者可用状态;它也能保证一个对象在最初被创建的时候处于一个明显不可能出现的状态来简化调试。 对类里面的成员进行初始化工作能保证一个成员变量正确的被初始化且不会出现在多个构造函数有同样的初始化代码。这样在你新增一个成员变量的时候就就可以减少出现bug的几率,因为你可能记得了在某一个构造函数里面初始化它了,却忘了在其他构造函数里面进行初始化。
缺点: 对于开发者,明确地定义一个默认构造函数是一个额外工作。 在对类成员进行初始化工作时如果一个成员变量在声明时初始化同时也在构造函数里面初始化,这可能会引起混乱,因为在构造函数里面的值会替换掉在声明时的值。
结论: 使用类成员初始化作为简单的初始化,特别是当一个成员变量在多个构造函数里面必须使用相同的方式初始化的时候。如果你的类定义了成员变量却没有在类里面进行初始化的,且如果没有其它构造函数,你必须定义一个无参数的默认构造函数。它应该使用保持内部状态一致和有效的方式来更好的初始化类对象。 原因是因为如果你没有其他构造函数且没有定义一个默认的构造函数,编译器会生成同一个默认的构造函数给你。编译器生成的构造函数对你的对象的初始化可能并不正确。 如果你的类继承自一个已经存在的类,但是你并没有添加新的成员变量,你就不需要默认构造函数了。
对只有一个参数的构造函数使用C++关键字explicit。
定义: 一般来说,如果一个构造函数只有一个参数,它可以当做转换函数使用。例如,如果你定义了Foo::Foo(string name),然后传进一个string类型给一个函数是需要Foo类型的,Foo的构造函数将会被调用并转换这个string类型为Foo类型,然后把这个Foo类型传递给这个函数。这能提供便利,但是这也是产生麻烦的根源:当一个对象被转换了,但是它却不是你想要的类型。显式地声明一个构造函数可以防止这种隐性转换。
优点: 避免出现不合需求的转换
缺点: 没有
结论: 所有的单个参数的构造函数都应该使用explicit显式声明。在定义类的时候,对于只有一个参数的构造函数时总是要在其前面使用explicit:explicit Foo(string name);
有一点例外的是拷贝构造函数,在一些比较少的情况我们允许它不使用explicit。还有一种例外的情况是,那些打算作为透明封装的类。这两种情况都应该明确的进行注释。
最后,构造函数中只有一个初始化列表的可以是非explicit。这是为了允许你的类型结构可以使用大括号初始列表的方式进行赋值。
当需要时应该提供一个拷贝构造函数和赋值操作符。否则,使用DISALLOW_COPY_AND_ASSIGN来禁用它们
定义: 拷贝构造函数和赋值操作符是用来创建一个对象的拷贝。拷贝构造函数是有编译器在某些情况下隐式调用的,例如,以传值方式传一个对象的时候。
优点: 拷贝构造函数使得拷贝对象变得简单。STL容器要求所有的内容都是可以拷贝和赋值的。拷贝构造函数比CopyFrom()方式这种替代方案更高效,在某些情况,编译器可以省去它们,它也避免了堆分配的开销。
缺点: 在C++中,隐式的拷贝对象可能会引起bugs和性能问题的。它也会减少可读性,因为它使得以传值方式的对象难以跟踪,相对于传引用来说,对象的改变会立刻得到反馈。
结论: 很少类需要能被拷贝。大部分类时不需要拷贝构造函数和赋值操作符的。在许多情况下,一个指针或者引用和一个被拷贝的值使用起来是差不多的,然而它们却更高效。例如,你可以以引用或者指针的方式传递函数参数来代替传值方式,你也可以用指针替代类对象来保存在STL容器里面。
如果你的类需要能被拷贝,与其提供一个拷贝构造函数,不如提供提供一个例如clone()的拷贝函数更好,因为这样的函数不能被隐式的调用。如果一个拷贝方法不满足你的需求情况(例如,考虑到性能原因,或者你的类需要以值方式保存在STL容器里面),可以也再提供拷贝构造函数和赋值操作符。
如果你的类不需要拷贝构造函数或者赋值操作符,你必须显式禁用它们。你可以这样子做,在类里面以私有方式为拷贝构造函数和赋值操作符添加声明,记得不要对它们提供任何对应的实现(这样会导致链接错误)。
为了方便,可以这样定义一个DISALLOW_COPY_AND_ASSIGN宏:
// A macro to disallow the copy constructor and operator= functions
// This should be used in the private: declarations for a class
#define DISALLOW_COPY_AND_ASSIGN(TypeName)
TypeName(const TypeName&);
void operator=(const TypeName&)
然后,在类Foo里面的实现就可以这样:
class Foo
{
public:
Foo(int f);
~Foo();
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};
可以减少重复代码时使用委派和继承构造函数。
定义:委派构造函数和继承构造函数是为了减少构造函数重复代码而在C++11中引入的两个不同的特性。委派构造函数允许类的一个构造函数通过特殊的初始化列表语法调用另外的构造函数。
委托构造函数
X::X(const string& name) : name_(name) {
...
}
X::X() : X("") { }
继承构造函数允许派生类可以直接使用基类的构造函数,就像使用基类的其他成员函数,而不需要重新声明这些构造函数。尤其当基类有多个构造函数时特别有用。使用时加一条using声明
class Base {
public:
Base();
Base(int n);
Base(const string& s);
...
};
class Derived : public Base {
public:
using Base::Base; // Base"s constructors are redeclared here.
};
当派生类构造函数仅仅只是调用基类构造函数时特别有用。
优点:
委派构造函数和继承构造函数可以减少冗余代码,从而提高代码可读性。
Java程序员对委派构造函数很熟悉。
缺点:
使用辅助函数可以预估委派构造函数的行为。
如果派生类引入了新的成员变量,那么继承构造函数会被迷惑,因为基类构造函数不知道这些新的成员变量。
结论:
当可以减少冗余、提高可读性的时候使用委派构造函数和继承构造函数。当派生类有新的成员变量的时候谨慎对待继承构造函数。如果派生类成员变量使用类内成员初始化(in-class member initialization),继承构造函数仍然是适用的。
仅当只有数据时使用struct,其它一概使用class。
唯一区别:两者的默认访问权限不同。struct,默认public;
class默认private。
在C++中,关键字struct和class几乎含义等同,我们为其人为添加语义,以便为定义的数据类型合理选择使用哪个关键字。
struct被用在仅包含数据的消极对象(passive objects)上,可能包括有关联的常量,但没有存取数据成员之外的函数功能,而存取功能通过直接访问实现而无需方法调用,这里提到的方法是指只用于处理数据成员的,如构造函数、析构函数、Initialize()、Reset()、Validate()。
如果需要更多的函数功能,class更适合,如果不确定的话,直接使用class。
为了与STL保持一直,仿函数(functors)和特性(traits)可以不用class而是使用struct。
注意:类和结构体的成员变量使用不同的命名规则。
使用组合(composition,注,这一点也是GoF在《Design Patterns》里反复强调的)通常比使用继承更适宜,如果使用继承的话,只使用公共继承。
定义: 当子类继承基类时,子类包含了父类所有数据及操作的定义。C++实践中,继承主要用于两种场合:实现继承(implementation inheritance),子类继承父类的实现代码;接口继承(interface inheritance),子类仅继承父类的方法名称。
优点: 实现继承通过原封不动的重用基类代码减少了代码量。由于继承是编译时声明(compile-time declaration),编码者和编译器都可以理解相应操作并发现错误。接口继承可用于程序上增强类的特定API的功能,在类没有定义API的必要实现时,编译器同样可以侦错。
缺点: 对于实现继承,由于实现子类的代码在父类和子类间延展,要理解其实现变得更加困难。子类不能重写父类的非虚函数,当然也就不能修改其实现。基类也可能定义了一些数据成员,用于区分基类的物理布局(physical layout)
结论: 所有继承必须是public的,如果想私有继承的话,应该采取包含基类实例作为成员的方式作为替代。
不要滥用实现继承,组合通常更加合适。努力做到只在“是一个”(“is-a”,译者注,其他”has-a”情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo,才令Bar是Foo的子类。
必要的话,令析构函数为virtual,这里必要是指该类具有虚函数。
限定仅在子类访问的成员函数为protected,需要注意的是数据成员应始终为私有。
当重定义派生的虚函数时,在派生类中明确声明其为virtual。根本原因:如果遗漏virtual,读者需要检索类的所有祖先以确定该函数是否为虚函数(注,虽然不影响其为虚函数的本质)。
真正需要用到多重实现继承(multiple implementation inheritance)的时候非常少,只有当最多一个基类中含有实现,其他基类都是以Interface为后缀的纯接口类时才会使用多重继承。
定义 多重继承允许子类拥有多个基类,要将作为纯接口的基类和具有实现的基类区别开来。
优点: 相比单继承,多重实现继承可令你重用更多代码(参考继承章节)。
缺点: 真正需要用到多重实现继承的时候非常少。当多重实现继承看上去是不错的解决方案时,通常可以找到更加明确、清晰的、不同的解决方案。
结论: 只有当所有超类(superclass)除第一个外都是纯接口类时才能使用多重继承。为确保它们是纯接口,类必须以Interface为后缀。
注意:关于此规则,Windows下有种例外情况(译者注,将在本译文最后一篇的例外规则中阐述)。
接口是指满足特定条件的类,这些类以Interface为后缀(非必需)。
定义:
当一个类满足以下要求时,称之为纯接口:
只有纯虚函数(”=0”)和静态函数(下文提到的虚析构函数除外);
没有非静态数据成员;
没有定义任何构造函数。如果有,也不含参数,并且为protected;
如果是子类,也只能继承满足上述条件并且后缀是Interface的类。
接口类不能被直接实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(作为第1条规则的例外,析构函数不能是纯虚函数)。具体细节可参考Stroustrup的《The C++ Programming Language, 3rd edition》第12.4节。
优点: 以Interface为后缀可令他人知道不能为该接口类增加实现函数或非静态数据成员,这一点对多重继承尤其重要。另外,对于Java程序员来说,接口的概念已经深入人心。
缺点: Interface后缀增加了类名长度,给阅诺和理解带来不便,同时,接口属性作为实现细节不应暴露给客户。
结论: 只有满足上述需要,类才可能以Interface结尾,但反过来,满足上述需要的类未必一定以Interface结尾。
除少数特定环境外,不要重载操作符。
定义: 一个类可以定义诸如+、/等操作符,使其可以像内建类型一样直接使用。重载操作符”“允许使用内置文本语法来创建类的对象。
优点: 操作符重载使代码看上去更加直观,就像内建类型(如int)那样。相比Equals()、Add()等黯淡无光的函数名,操作符重载有趣多了。
为了使一些模板函数正确工作,你可能需要定义操作符。
自定义的文本是一个非常简洁的符号,用来创建用户自定义类型的对象。
缺点: 虽然操作符重载令代码更加直观,但也有以下不足:
混淆直觉,让你误以为一些耗时的操作像内建操作那样轻巧;
查找重载操作符的调用处更加困难,查找Equals()显然比==容易的多;
有的操作符可以对指针进行操作,容易导致bugs。Foo + 4做的是一件事,而&Foo + 4可能做的是完全不同的另一件事,对于二者,编译器都不会报错,使其很难调试;
即便是对于老道的C++程序员,用户自定义文创建新的语法形式也是一件很陌生的事情。
重载还有令你吃惊的副作用,比如,前置声明重载操作符&的类很不安全。
结论: 一般不要重载操作符,尤其是赋值操作(operator=)暗藏杀机,应避免重载。如果需要的话,可以定义类似Equals()、CopyFrom()等函数。同样的,不惜一切代价避免重载一元操作符&,如果类有可能被前向声明的话。
不要重载操作符”“,比如,不要引入自定义文本。
然而,极少数情况下需要重载操作符以便与模板或“标准”C++类衔接(如operator<<(ostream&, const T&)),如果被充分证明则是可接受的,但你仍要尽可能避免这样做。尤其是不要仅仅为了在STL容器中作为key使用就重载operator==或operator<,取而代之,你应该在声明容器的时候,创建相等判断和大小比较的仿函数类型。
有些STL算法确实需要重载operator==时可以这么做,但是不要忘了提供文档说明原因。
亦可参考拷贝构造函数和函数重载章节。
将数据成员私有化,并提供相关访问函数(因技术原因,当使用Google测试时,允许test类中的数据成员是protected)。典型得,变量命名为_foo,取值函数为getFoo(),赋值函数为setFoo()。例外:静态常量数据成员(命名为FOO)不需要是private。
取值函数一般作为内联函数定义在头文件中。
亦可参考继承和函数名称章节。
在类中使用特定的声明顺序:public:在private:之前,成员函数在数据成员(变量)之前等等。
类的各部分定义顺序如下:首先是public:部分,然后是protected:部分,最后是private:部分。如果其中某部分没有,直接忽略即可。
在上述任何部分内,声明需要遵循以下顺序:
Typedefs和Enums
常量(static const类型的数据成员)
创建函数(createXXX方法)
构造函数
析构函数
成员方法,包括静态方法
重写方法(overridden methods,必须以override关键字作为后缀)
数据成员(static const数据成员除外)
友元声明必须放在private:部分,宏DISALLOW_COPY_AND_ASSIGN应该放在private:部分最后。这应该是类的最后一部分内容。亦可参考拷贝构造函数章节。
.cpp文件中函数的定义应尽可能和声明次序一致。
不要在类的定义中内联大型函数定义。通常,只有那些没有特别意义的或者性能要求高的,并且比较短小的函数才被定义为内联函数。更多细节参考内联函数章节。
示例:
class MyNode : public Node
{
// public first
public:
// "creator" methods first
static MyNode *create();
static MyNode *createWithParam(const something& arg);
// If applicable, then Constructors and the Destructor
MyNode();
virtual ~MyNode();
// Then the init methods
bool init();
bool initWithParam(const something& arg);
// Then methods of the instance
void addColor( const Color& color );
void addBackgroundImage( const char* filepath );
// Then the overrides
virtual void visit(void) override;
virtual void addChild(Node * child, int zOrder, int tag) override;
// then protected
protected:
Point absolutePosition();
// then private
private:
Point _lastPosition;
};
}
优先选择短小、精炼的函数。
关于所有权和智能指针,最好使得动态分配的对象有单一、固定的所有者。最好用智能指针来转移所有权。
定义:“所有权”是管理动态分配的内存(还有其它资源)的一种簿记(bookkeeping)技术。动态分配对象的所有者是一个对象或者函数,这个对象或者函数负责在动态分配对象不再需要时将其删除。所有权有时可以共享,在这种情况下,最后一个所有者通常负责将其删除。即使所有权不是共享的,它也可以通过代码段来转移。
“智能”指针看起来像指针,比如通过重载*和 - >运算符。一些智能指针类型可用于自动化簿记所有权,通过自动化来确保所有权的上述责任可以得到满足。std::unique_ptr是C++11中介绍的一种智能指针类型,它表达了动态分配对象的独占所有权,在std::unique_ptr指针超出范围时这个对象被删除。这个对象不能被复制,但是可以将所有权转移。shared_ptr也是一种智能指针类型,它表达了动态分配对象的共享所有权。shared_ptrs可以被复制,并且对象的所有权被所有副本共享,当最后一个shared_ptr被销毁时对象被删除。
优点:
在没有所有权逻辑的情况下不可能管理动态内存分配.
相比起复制来说,转移对象的所有权消耗的资源更小(在可以复制的情况下).
所有权转移比起’借用’(borrowing)指针或者引用更简单一些,因为不再需要在两个使用者之间协调对象的生命周期.
使所有权逻辑清晰、自文档化、引用明确可以提高智能指针的可读性.
智能指针可以消除所有权的主动簿记,简化代码,并且可以排除常见类型的错误.
对于常量对象,共享所有权是替代深拷贝的简单有效的方法.
智能指针详解:http://blog.csdn.net/u012723995/article/details/47261573
缺点:
所有权必须通过指针来表示和转移(无论智能或者普通指针)。指针语义比普通值的语义要更复杂,尤其在API中: 不仅需要考虑到所有权,而且还要考虑对象引用混淆(aliasing)、生命周期、可变性等其它的问题.
值语义(value semantics)的性能成本经常被高估,因此所有权转移的性能优点可能无法证明它的可读性和复杂性成本.
所有权转移操作相关的API会强制将它们的操作源引入一个单一内存管理的模式.
当资源释放发生的时候,使用智能指针的代码会有些不太明确.
std::unique_ptr在表示所有权转移时用的是C++11的操作语义,这在Google代码中是禁止的,这样使用的话可能会让一些程序员产生混淆.
共享所有权操作会比所有权的谨慎设计更有诱惑性,这样处理会模糊系统的设计.
所有权共享在系统运行时需要明确的簿记,这样的操作开销会比较大.
在某些情况下(比如循环引用),共享所有权的对象可能会永远得不到释放删除.
智能指针不是普通指针的完美替代.
结论:
如果必须要动态分配对象,那么最好让分配所有权的代码一直持有所有权。如果其它代码需要访问持有所有权的对象,可以考虑不传递所有权而是传递一个副本、或者一个指针或引用。最好使用std::unique_ptr使得所有权的传递更明确。比如:
std::unique_ptr FooFactory();
void FooConsumer(std::unique_ptr ptr);
在不是很必要的情况下不要在你的代码中使用所有权共享。其中一种情况是为了避免复制操作的高昂开销,但是你应该只在性能优势提高很显著的情况下使用,并且底层的对象是不可变的(即shared_ptr)。如果要使用所有权共享,最好使用shared_ptr.
除非是为了跟老版本的C++兼容,否在在新版本的C++代码中的不要使用scoped_ptr,永远不要使用linked_ptr 或者std::auto_ptr。在所有以上这三种情况下,用std::unique_ptr来代替.
所有按引用传递的参数必须加上const.
定义
在C语言中,如果函数需要修改变量的值,形参(parameter)必须为指针,比如 int foo(int *pval)。在C++中,函数还可以声明引用形参: int foo(int &val)。
优点:
定义形参为引用避免了像(*pval)++这样难看的代码,像拷贝构造函数这样的应用也是必须的。而且很清楚,不像指针那样不能使用空指针null。
缺点:
引用容易引起误解,因为引用在语法上是值但却有指针的语义。
结论:
函数形参表中,所有的引用必须是const:
void foo(const string &in, string *out);
这是一个硬性约定:输入参数是值或者常数引用,输出参数为指针。输入参数可以是常数指针,但不能使用非常数引用形参,除非是约定需要,比如swap() 。
不过,有些情况下选择输入形参时,const T*比const T&更好。
例如:
需要传递一个空指针。
函数保存了一个指针或者引用作为输入。
传递const char* 给字符串。
要记住,大多数情况下输入形参要被指定为const T&。使用 const T*会传达给读者这样一个信息:输入参数要以某种方式区别处理。因此有确切的理由时,再选择const T*而不是const T&作为形参输入,否则会误导读者去寻找有关这方面其实不存在的解释。
不要使用右值引用,std::forwad,std::move_iterator或std::move_if_noexcept。仅与不可拷贝参数一起使用std::move的单参数形式。
定义: 右值引用是引用的一种,只能绑定到临时对象。语法与传统的引用语法相似。例如void f(string&& s);声明了一个函数,其参数是一个字符串的右值引用。
左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。
一个便捷区分方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
优点:
定义移动构造函数(使用类类型右值引用的构造函数)可以用移动值来替代拷贝值。如果v1是vector,那么auto v2(std::move(v1))很可能只是一些简单的指针操作而不是拷贝大量的数据。有时这可以显著提升性能。
右值引用可以编写通用的函数封装来转发其参数到另外一个函数,无论其参数是否是临时对象。
右值引用实现了可移动不可拷贝的类型,这对那些拷贝没有意义,但是对作为函数参数传递或者塞到容器内等的类型非常有用。
std::move对高效使用某些标准库类型如std::unique_ptr来说是必须的。
缺点:
右值引用是一个相对较新的特性(C++11的一部分),还没有被广泛的理解。像引用坍塌(reference collapsing)、移动构造函数的自动推导等规则很复杂。
右值引用鼓励重度使用值语义的编程风格。对很多开发者来说,这种风格是陌生的,而且其性能特点也很难言明。
仅在输入参数类型不同、功能相同时使用重载函数(含构造函数)。
定义:
同名函数的参数形式(指参数的个数、类型或者顺序)不同,也就是说用同一个运算符完成不同的运算功能,这就是重载函数。
可以定义一个函数参数类型为const string&,并定义其重载函数类型为const char*。
class MyClass
{
public:
void analyze(const string &text);
void analyze(const char *text, size_t textlen);
};
优点: 通过重载不同参数的同名函数,令代码更加直观,模板化代码必须使用重载,同时为读者带来便利。
缺点: 限制使用重载的一个原因是在特定调用处很难确定到底调用的是哪个函数,另一个原因是当派生类只重载函数的部分变量会令很多人对继承语义产生困惑。
结论: 如果你想重载一个函数,考虑让函数名包含参数信息,例如,使用AppendString()、AppendInt()而不是Append()。
禁止使用缺省函数参数,除非是下述有限的几种情况之一。如果合适,用函数重载来替代。
定义:
就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数。
缺省参数使用主要规则:调用时你只能从最后一个参数开始进行省略,换句话说,如果你要省略一个参数,你必须省略它后面所有的参数,即:带缺省值的参数必须放在参数表的最后面。 缺省值必须是常量。显然,这限制了缺省参数的数据类型,例如动态数组和界面类型的缺省参数值只能是 nil;至于记录类型,则根本不能用作缺省参数。 缺省参数必须通过值参或常参传递。
优点: 经常一个函数带有缺省值,偶尔会重写一下这些值。缺省参数为极少的例外情况提供了少定义一些函数的方便。相比重载这个函数,缺省参数有更干净的语义,用更少的样板,并且更清晰的区分“必须”和“可选”的参数。
缺点: 缺省参数的存在使得函数指针产生迷惑,因为函数的签名与调用的签名经常不一致。往现有的函数中增加缺省参数会改变函数的类型,这会导致使用函数地址的代码出现问题。函数重载可以避免这些问题。而且,缺省参数会导致“笨重”的代码,因为他们在每个调用的地方都被重复,而重载的函数只有在定义的地方才出现“这些”缺省。
结论: 尽管上述的缺点并不是那么“繁重”,但是相比缺省参数带来的很小的好处,仍然是得不偿失。因此除了下述的例外,所有的参数都应该显式的指定。
一个特例是当函数是.cpp文件中静态函数(或者在一个未命名的命名空间里)。这种情况下,因为函数只在很小的作用域中使用,缺省参数的缺点就显得微不足道。
通常情况下,cocos2dx的createXXX和initXXX方法允许使用缺省参数。
另外一个特例是缺省参数用于变长参数列表。例如:
// Support up to 4 params by using a default empty AlphaNum.
string strCat(const AlphaNum &a,
const AlphaNum &b = gEmptyAlphaNum,
const AlphaNum &c = gEmptyAlphaNum,
const AlphaNum &d = gEmptyAlphaNum);
禁止使用变长数组和alloca()。
优点: 变长数组具有浑然天成的语法。变长数组和alloca()也都很高效。
缺点: 变长数组和alloca()不是标准C++的组成部分,更重要的是,它们在堆栈(stack)上根据数据分配大小可能导致难以发现的内存泄漏:“在我的机器上运行的好好的,到了产品中却莫名其妙的挂掉了”。
结论: 使用安全的分配器(allocator),如scoped_ptr/scoped_array。
允许合理使用友元类及友元函数。
通常将友元定义在同一文件下,避免读者跑到其他文件中查找其对某个类私有成员的使用。经常用到友元的一个地方是将FooBuilder声明为Foo的友元,FooBuilder以便可以正确构造Foo的内部状态,而无需将该状态暴露出来。某些情况下,将一个单元测试用类声明为待测类的友元会很方便。
友元延伸了(但没有打破)类的封装界线,当你希望只允许另一个类访问某个成员时,使用友元有时比将其声明为public要好得多。当然,大多数类应该只提供公共成员与其交互。
禁止使用C++异常。
优点:
异常允许应用的更上层代码决定如何处理在底层嵌套函数中发生的“不可能发生”的失败,不像出错代码的记录那么模糊费解;
应用于其他很多现代语言中,引入异常使得C++与Python、Java及其他和C++相近的语言更加兼容;
有些C++第三方库使用了异常,关闭异常会使继承更加困难;
异常是解决构造函数失败的唯一方案,虽然可以通过工厂函数(factory function)和Init()方法模拟异常,但他们分别需要堆分配或者新的“非法”状态;
在测试框架(testing framework)中,异常确实很好用。
缺点:
在现有函数中添加throw时,必须检查所有调用处,即使它们至少具有基本的异常安全保护,或者程序正常结束,永远不可能捕获该异常。例如:如果f()依次调用了g()和h(),h抛出被f捕获的异常,g就要当心了,避免出现错误清理;
通俗一点说,异常会导致无法通过查看代码确定程序控制流:函数有可能在不确定的地方返回,从而导致代码管理和调试困难,当然,你可以通过规定何时何地如何使用异常来最小化的降低开销,却给开发人员带来掌插这些规定的负担;
异常安全需要RAII和不同编码实践。轻松的编写异常安全(exception-safe)的代码需要大量的支持机制。进一步,为了避免需要读者去理解整个调用表,异常安全代码必须隔离将持久状态写入到“提交”阶段的逻辑。这样做有利有弊(或许你不得不为了隔离提交而混淆代码)。允许使用异常将会付出这些不值得的代价。
加入异常使二进制文件体积发大,增加了编译时长(或许影响不大),还可能增加地址空间压力;
异常的实用性可能会刺激开发人员在不恰当的时候抛出异常,或者在不安全的地方从异常中恢复,例如,非法用户输入不应该导致抛出异常。如果允许使用异常会使得这样一篇编程风格指南长出很多。
结论:
表面上看,使用异常利大于弊,尤其是在新项目中。然而,对于现有代码,引入异常会牵连到所有依赖的代码。如果允许异常在新项目中使用,在跟以前没有使用异常的代码集成时也是一个麻烦。因为Google现有的大多数C++代码都没有异常处理,引入带有异常处理的新代码相当困难。
鉴于Google现有代码不接受异常,在现有代码中使用异常比在新项目中使用
- 上一篇: C++ 运算符优先级和结合性(MSDN)
- 下一篇: 如何使PHP文件与HTML代码更好的分离