Google C++ 编程风格指南(中文翻译)-2

来源:互联网 发布:淘宝直播卖衣服货源 编辑:程序博客网 时间:2024/06/16 02:03

 

4       

C++中类是构成代码的基础。平时,我们非常广泛的使用它们。这部分列出了当写一个类时你应该遵循的重要的那些该做的和不该做的。

4.1    在构造函数中做事情

一般来说,构造函数应该仅仅是设置成员变量的初始值。任何复杂的初始化应该放在显式的Init()方法里。

Ø  定义

在构造函数中进行初始化是合理的。

Ø  正面的

便于打字。不用再担心类是否被初始化了。

Ø  反面的

在构造函数中做事情的问题是:

l  没有从构造函数中通知错误的简单办法,出了异常外(只是被禁止的)

l  如果构造函数中的工作失败了,那么我们就有了一个对象,而它的初始化代码失败了,那么它可能出于未定义状态。

l  如果这些事情调用了虚函数,这些调用将不会分配到子类的实现。你对类的进一步的改变可能悄悄引入这一问题,即使你的类现在还没有子类,这会造成很大混乱。

l  如果有人用这个类型生成了一个全局的变量(这违反了规则,但是仍然这么做了),构造函数将会在main()前被调用,这可能打破一些在构造函数代码中隐藏的假定。比如gflags还没有被初始化过。

Ø  结论

如果你的对象需要一些重大的初始化,考虑一下使用一个显式的Init()方法。特别地,构造函数不应该调用虚函数,试图报出错误,访问可能没有初始化的全局变量,等等。

4.2    默认构造函数

如果你的类定义了成员变量,并且没有其他的构造函数,那么你必须定义默认构造函数。否则编译器会为你生成一个非常糟糕的。

Ø  定义

当我们new了一个对象,并且没有参数,那么默认构造函数将会被调用。在调用new[](数组)它也总会被调用。

Ø  正面的

使用默认值厨师化结构体,设置一些不可能的值,这样会使debug更容易。

Ø  反面的

这会为你(代码机器)带来一些额外的工作。

Ø  结论

如果你的类定义了成员变量,而且也没有其它的构造函数,那么你必须定义一个默认构造函数(默认构造函数是无参数的)。这个默认构造函数应该把对象初始化,使它的内部状态一致,并有效。

这样做的原因是,如果你没有其它的构造函数,而且也没有定义一个默认构造函数,那么编译器就会为你生成一个。编译器生成的构造函数对你的对象的初始化可没那么智能。

如果你的类从一个现有类继承,而且你也没有添加新的成员变量,那你就不用再定义默认构造函数了。

4.3    显示构造函数

如果构造函数只有一个参数,给他加上C++关键字explicit

Ø  定义

一般情况下,如果一个构造函数只接受一个参数,它是可能被用来做转换的。比如,如果你定义了Foo::Foo(string name),然后你把一个string给了一个函数,而这个函数要的是Foo,这个构造函数就会被调用把string转换为Foo,并为你把Foo传给你的函数。有时候这是很方便的,但是这也是麻烦的制造者转换发生了,而生成的新对象不是你想要的。把构造函数声明为explicit能阻止它被作为转换进行隐式调用。

Ø  正面的

避免不良的转换。

Ø  反面的

无。

Ø  结论

我们要求所有的单参数构造函数都要是显式的。在类的定义中,总是把explicit放在单参数构造函数前:explicit Foo(string name)

拷贝构造函数是个例外,在少数情况下,我们允许它们不用explicit。还有一种例外就是,有些类就是为了透明的封装其它的类为目的的。这些例外情况都应该以注释注明。

4.4    拷贝构造函数

 

只在有必要的时候提供拷贝构造函数和赋值操作符。否则,用DISALLOW_COPY_AND_ASSIGN宏禁用它们。

Ø  定义

拷贝构造函数和赋值操作符是用来通过拷贝生成对象的。在某些情况下,拷贝构造函数被编译器隐式调用,比如以传值方式来传递对象。

Ø  正面的

拷贝构造函数是对象拷贝操作更容易。STL容器要求他的内容都支持拷贝和赋值。拷贝构造函数会比CopyFrom()的变通方式更有效率,因为它把构造和拷贝融合在了一起,编译器可根据上下文忽略它,而且它也使避免堆分配变得更简单。

Ø  反面的

C++中,隐式的对象拷贝是bug和性能问题的丰富源泉。同时,它也降低了可读性,因为与引用传递相比,很难跟踪哪些对象被值传递,对一个对象的哪些改变其作用了。

Ø  结论

很少类需要支持拷贝。绝大多数既不需要拷贝构造,也不需要赋值操作符。在许多情况下,指针或引用可以工作得和值拷贝一样好,而且具有更好的性能。例如,你可以对函数参数使用引用传递或用指针而不是值传递,而且你可以在STL容器中存储指针而不是对象。

如果你的泪需要支持拷贝,最好提供专门的拷贝方法,比如像CopyFrom() Clone(),而不是拷贝构造函数,因为这些方法不会被隐式调用。如果拷贝方法满足不了你的需求(比如,由于性能原因,或者因为你的类需要在STL容器中以值存储),那么就同时提供拷贝构造和赋值操作符。

如果你的类不需要拷贝构造函数或者赋值操作符,你必须显式的禁掉它们。你应该这样做,在你的类的private:区域声明假的拷贝构造函数和赋值操作符,而不提供相应的定义(因此,所以试图使用它们的都会造成链接错误)。

例如,可以用一个DISALLOW_COPY_AND_ASSIGN宏:

// 一个禁止拷贝构造函数和赋值操作符的宏
// 它应该放在类声明的private:私有区
#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);
};

 

 

4.5    结构体vs

 对那些用来存放数据的被动对象使用struct;其它的都用class

C++structclass关键字基本上是一样的。我们给这两个关键字加上我们的语义,所以你应该为你定义的数据类型使用正确的关键字。

Structs用来表示那些存放数据的被动对象,而且可能有相关常数,但是除了用来访问,设置数据字段外,没有任何其它功能。对字段的访问/设置是通过对字段的直接访问完成的,而不是通过方法调用。方法不应该提供行为,而应该只是建立数据成员,例如,构造,析构,Initialize()Reset() Validate()

如果你还需要更多的功能,用class会更恰当。如果不确定,那就用class吧。

注:structclass的成员变量使用不同的命名规则。

4.6     继承

组合往往比继承更合适。使用public继承。

Ø  定义

当子类从基类继承是,它引入了父基类定义的所有数据和操作的所有定义。实践中,继承在C++中的使用主要有两种方式:实现继承,也就是子类完全继承所有代码,另一种是接口继承,只继承方法名。

Ø  正面的

实现继承能减少代码量,它重用基类的代码,这些代码特化了现有的类型。因为继承是编译期声明的,你和编译器可以理解这种操作,并探测错误。借口继承能迫使类暴露特定借口API。同时,在这种情况下,如果这个类没有定义API的必要的方法,编译器能发现错误。

Ø  反面的

对于实现继承,因为子类的实现代码是在基类和子类间传播的,所以,可能会很难理解某个实现。子类不能重载非虚函数,所以子类不能改变实现。子类可能也定一了一些成员数据,因而指定了基类的物理边界。

Ø  结论

继承都应该用public。如果你想要私有继承(private),你可以通过包含一个基类的实例的成员来实现。 

不要过渡使用实现继承。组合往往更合适。试着限制使用继承”is-a”的情况:Bar继承自Foo,除非Bar 是一种Foo看起来很合理。 

尽量把析构函数设为virtual 如果你的类里有虚函数,那么析构函数就应该是virtual。注,数据成员应该是private

限制protected的使用,对那些可能需要从子类中访问的成员函数。 

重新定义继承来的虚函数时,在派生类的声明中,显式声明virtual。原理:如果virtual被忽略了,读程序的人就不得不察看所有的祖先类,来确定这个函数到底是不是virtual的疑惑。

4.7     多重继承

只有很少的多重实现继承是真正有用的。我们只在一种情况下允许多重继承:基类中只有一个有实现,其它的基类都是接口,并被标上了interface的后缀。 

Ø  定义:

多重继承允许子类有多个基类。我们把基类分为纯接口的基类和有实现的基类。

Ø  正面的:

多重实现继承可以让你重用比单继承要来的多的代码(请看继承章节)。

Ø  反面的:

只有很少的多重实现继承是真正有用的。当多重实现继承看起来像是解决方案时,你往往可以找到另一种,更直率的,更清楚地解决方案。

Ø  结论:

只在这种情况下用多重继承,所有的超类(除了一个例外可能有一个是实现)都是纯接口。为了确保它们都是接口,它们的名字都应该以interface的后缀结束。

注:在Windows上有一个此规则的例外

4.8    接口

满足了一定条件的类就可以,但不是必须的,就可以具有interface后缀。

Ø  定义:

如果一个类满足了以下条件,那就是纯接口:

l  它只有public的纯虚方法(“=0”)和警惕方法(对析构函数请往下看)。

l  它可能没有非静态(non-static)的数据成员。

l  它不需要定义任何构造函数。如果提供了一个构造函数,必须是没有参数的,并且必须是protected

l  如果是一个子类,它只能从满足这些条件的,并标记有interface后缀的类派生。

一个接口类永远不能直接实例化,这是由于它定义的纯接口方法。为了确保接口的所有实现们都能被正确的销毁,它们必须声明一个virtual析构函数(对第一个条件的例外,这个不应该是纯虚方法)。细节请参考Stroustrup的《C++编程语言第三版,12.4章节》

Ø  正面的:

把一个类标记上interface的后缀,能让别人知道他们不能添加实现方法和非静态的数据成员。这在多重继承的情况下是相当重要的。另外,接口语义在Java语言中早已是众所周知的了。

Ø  反面的:

Interface后缀加长了类名,有可能会造成阅读和理解的困难。同时,接口属性可能会考虑到那些不应该暴露给客户的实现细节。

Ø  结论:

仅当一个类满足了上面的那些条件的时候,才给它加上interface的后缀。然而,满足上面调解的类也不是必须加上interface后缀。

4.9    操作符重载

不要重载操作符,除了在极少的,特殊的情况下。

Ø  定义:

类可以在类上定义像+/这样的操作符,就像那些内建类型一样。

Ø  正面的:

能使代码更直观,因为这样的类将像具有跟内建类型(如int)一样的行为。重载的操作符往往是对于那些不具有丰富命名的函数的更顽皮的名字,比如,Eqauals()或者Add()。对于有些模板,为了让它们正确运行,你可能就需要定义一些操作符。

Ø  反面的:

虽然操作符重载能使代码看起来更直观,但它也有一些缺点:

l  它可能愚弄我们的直觉,使我们觉得代价高昂的操作是低廉的,内建的操作。

l  为重载的操作符找到调用点是非常困难的。而找Equals()要比找==操作符的相关调用方便的多。

l  有些操作符还能对指针工作,这就更容易引入bug了。Foo + 4可能做一件事,而&Foo + 4 做的却是完全不同的事。编译器不会对这两种情况有什么抱怨,这就使debug变得非常困难。

重载也有令人惊奇的衍生物。比如,如果一个类重载了非数组的operator&,那它就不能被安全的前置声明。

Ø  结论:

一般情况下,不要重载操作符。特别是复制操作符(operator=),非常隐蔽,应该被避免使用。如果需要,你可以定义像Equals()CopyFrom()的函数。同样地,如果这个类有可能被前置声明,那就极力避免非常危险的非数组的operator&

然而,可能有一些少数情况,为了能跟模板或者标准C++类正确工作(比如为了记日志operator(ostream&, const T& )),你需要重载操作符。如果证明合法,这些是可接受的,但你最好尽可能避免它们。特别是,不要只是为了使你的类能作为STL容器的键值,去重载operator==或者operator<;你可以在声明容器时,创建等值和比较的函数对象。

有些STL的算法确实要求你必须重载operator==,当这种情况下,你不得不这么做时,在文档中记录下原因。

你还可以在看看拷贝构造函数函数重载章节。

4.10    访问控制

把数据成员都设成private,为它们提供访问函数(因为技术上的原因,在使用Google Test时,我们允许测试装置的类的数据成员为protected)。典型情况下,一个变量可能叫foo_,访问函数就叫foo(),你可能需要一个设值函数set_foo()。例外:static const(静态常量)数据成员不用是private

访问函数往往在头文件中定义成内联的(inline)。

你也可以查看继承函数名的相关内容。

4.12    声明顺序

在类内使用特定的声明顺序:public:private:之前,方法在数据成员之前,等等。

你的类定义应该以它的public:部分开始,后面跟着是protected:部分,然后是private:部分。如果某个部分是空的,那就省略它。

在每个部分内的声明一般应该遵循以下的顺序:

l  TypedefEnum

l  常量(static const静态常量数据成员)

l  构造函数

l  析构函数

l  方法,包括静态方法

l  数据成员(除了static const静态常量数据成员)

友元声明总是应该放在private部分,DISALLOW_COPY_AND_ASSIGN宏应该放在private:部分的最后。它应该是类的最后的东西。请看拷贝构造函数章节。

在相应的.cc文件中,方法定义应该跟声明的顺序尽可能一样。

不要在类的定义中放入非常大的内联方法的定义。通常,只有那些不重要的,性能要求高的,非常短的方法才可能会定义成内联的。更多详情请看内联函数章节。

4.13    写短函数

选择小的并且功能集中的函数。

我们发现,有时候很长的函数是合适的,所以在函数长短上没有固定的限制。如果函数超过了40行,考虑一下它是否能被分成小函数,同时又不影响程序的结构。

即使你的长函数现在工作的非常好,别人在未来的什么时候修改它,可能添加一些新行为。这可能会带来一些很难发现的bug。保持你的函数短小精悍,能帮助别人更容易的去阅读和修改你的代码。

你可以在你工作的代码中找到一些很长的,并且很复杂的函数。不要试图去修改已有的代码:如果使用这样的函数是非常困难的,或者你发现很难去debug错误,或者你想在几个不同的上下文中使用它的一个片段,考虑把这样的函数分割成小的,更有意义的片段。

5      Google 特有的魅力

我们有许多各种各样的技巧和工具来使我们的代码更健壮,还有许多我们使用C++的方法可能跟你在其它地方看到的很不一样。

5.1    智能指针

如果你真的需要指针语义,scoped_ptr是非常棒的。在所有的特定情况下,你都应该只适用std::tr1::shared_ptr,比如当你需要把对象放在STL容器中存储时。永远不要使用auto_ptr

智能指针是一些对象,它们看起来像指针,但被加入了一些其它语义。当一个scoped_ptr被销毁时,比如,它删除它指向的对象。shared_ptr也这样,但它实现了引用计数,所以只有最后一个指向对象的指针去删除那个对象。

一般来讲,我们倾向于代码设计的具有非常清晰的所有权。对象所有权最清晰的是,直接使用对象作为字段或者局部变量,而根本不用指针。在另一种极端情况下,由于他们的每个定义,具有引用计数的指针不被任何人拥有。这种设计的问题是,很容易生成环形引用,或者另一种奇怪的情形造成一个对象永远不被销毁。它也使每次进行的值拷贝或赋值的原子操作变慢。

虽然,并不是推荐的,但是具有引用计数的指针有时往往是最简单,最幽雅的解决问题的方法。

5.2    cpplint

使用cplint.py探测类型错误。

cpplint.py是一个工具,它可以读源文件,并且辨识许多类型错误。它虽然不是完美的,并且还有误报(错误的判定正确的和错误的),但它仍然是一个非常有意义的工具。可以在行尾加上//NOLINT的注释来忽略正确的误报。

有些项目有怎么从项目工具中运行cpplint.py的说明。如果你出力的项目没有,你可以单独下载cpplint.py

6      其他的C++特征

6.1    引用参数

 

6.2    函数重载

6.3    默认参数

6.4    变长数组和alloca()

6.5    友元

6.6    异常

6.7    运行时类型信息(RTTI

6.8    截断

6.9    

6.10       前置递增和前置递减

6.11       const的用法

6.12       整数类型

6.13       64可位移植性

6.14       预处理宏

6.15       0NULL

6.16       sizeof

6.17       Boost

6.18       C++0x

7      命名

7.1    通用命名规则

7.2    文件名

7.3    类型名

7.4    变量名

7.5    常量名

7.6    函数名

7.7    名字空间的名字

7.8    枚举名

7.9    宏名

7.10   命名规则的例外

原创粉丝点击