cocos2d-x中的C++ 编码规范

声明(务必要看)

此文档主要参考于cocos官网的帮助文档,但由于没有目录功能,所以自己结合《c++ primer》编写此文档。
在此,为了避免误人子弟特此声明,这并不是c++知识点总结的技能型干货文章,只是    编码规范。。。。。

(但是我开始天真的以为这是官网为我们准备的c++学习文档,在重整理的途中还加了很多书上和网上的经典文章(

所以,这并不太适合刚入门想学c++的新手看。。。。
在此,再分享一个教学视频:轻松理解Effective C++:

http://edu.9miao.com/course/50

头文件

为了确保各个文件中类的定义一致,类通常被定义在头文件中。一般情况下,每个.CPP文件应该有一个相关的.h文件,而且名字一致。有一些常见的例外,如单元测试代码和只包含一个main函数的cpp文件。
但是可能由于头文件和类文件同时包含其他类的头文件从而导致头文件多次被包含而不安全(会编译出错,库文件不会,有预编译命令)
确保多次包含而安全的常用技术是预处理器,当预处理器看到#include标记时,会自动用该头文件内容替换#include
还有一种功能是头文件保护符,接下来会讲到

define用法

所有头文件应该由#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文件

如果有需要,可以用带-inl.h后缀的文件来定义复杂内联函数

内联函数的定义必须放在头文件中,这样编译器在函数调用处内联展开时才有函数定义可用。但实现代码通常还是放在.cpp文件比较合适,因为除非会带来可读性或者性能上的好处,否则我们不希望在.h文件里堆放太多具体的代码。

如果一个内联函数的定义非常短,只含有少量逻辑,你可以把代码放在你的.h文件里。例如取值函数与设值函数都毫无疑问的应该放在类定义中。更复杂的内联函数为了实现者和调用者的方便,也要放在.h文件里,但是如果这样会让.h文件过于臃肿,你也可以将其放在一个单独的-inl.h文件里。这样可以将具体实现与类定义分开,同时又确保了实现在需要用到的时候是被包含的。

-inl.h文件还有一个用途是存放函数模板的定义。这样可以让你的模板定义更加易读。

不要忘记,就像其他的头文件一样,一个-inl.h文件也是需要#define防护的。

函数参数顺序

定义函数时,参数顺序应该为:输入,然后是输出

C/C++函数的参数要么是对函数的输入,要么是函数给出的输出,要么两者兼是。输入参数通常是值或者常引用,而输出以及输入/输出参数是非const指针。 在给函数参数排序时,将所有仅输入用的参数放在一切输出参数的前面。特别需要注意的是,在加新参数时不要因为它们是新的就直接加到最后去;新的仅输入用参数仍然要放到输出参数前。

这不是一条不可动摇的铁律。那些既用于输入又用于输出的参数(通常是类/结构体)通常会把水搅浑,同时,为了保持相关函数的一致性,有时也会使你违背这条原则。

include的命名和顺序

使用以下标准顺序以增加可读性,同时避免隐藏的依赖关系: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类型传递给这个函数。这能提供便利,但是这也是产

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值