类是c++代码的基本单元,自然使用就很广泛。本章列出了一些在写类时需要注意的事项。

构造函数的职责

   不要在构造函数里做复杂的初始化。

定义:

在构造函数里做初始化操作。

优点:

方便排版,无需担心类是否已经初始化。

缺点:

1、  不容易报告错误,不能使用异常。

2、  如果操作失败,我们得到的是一个初始化是吧的对象,它有着不确定的状态。

3、  如果调用了虚函数,这些调用不会被派发到子类的实现中。将来对类的修改也好引入这个问题,即使现在类没有子类,但是还是会引发很多问题。

4、  如果有点人不小心定义了该类的全局变量,构造函数就会在main函数之前被调用,有可能打破构造函数中暗含的假定条件。例如,flag标志可能还没被初始化。

决定:

   构造函数不能调用虚函数或者引发非致命的失败。如果你的对象需要一些重要的初始化,考虑用工厂函数或者init()函数。

 

默认构造函数

你必须定义一个默认构造函数,如果你的类定义了一个成员变量而且没有其他构造函数。否则编译器讲会生成一个默认构造函数。

 

定义:

当你new一个类对象而且不带参数时,默认构造函数将被调用。如果用new[]生成对象数组总是调用默认构造函数。

优点:

默认初始化结构为不可能的值,使得更容易调试。

缺点:

对于写代码的人来说,这是多余的工作。

决定:

如果你的类定义了一个成员变量而且有没有定义其他构造函数,那就必须定义一个默认构造函数。它能较好的初始化对象,使得它的状态一致有效。

这样做的理由是,如果你没有其他构造函数,也不定义默认构造函数,编译器将会为你生成一个默认构造函数。编译器生成默认构造函数可能不会那么好的初始化对象。

如果你的类继承只一个已经存在的类,而且添加了新的成员变量,你并不需要一定定义默认构造函数。

 

显示构造函数

   用explicit关键字声明带有一个参数的构造函数。

定义:

   通常,带有一个参数的构造函数可能用作类型转换。

例如,定义一个Foo::Foo(stringname),然后传递一个string给一个期待得到Foo的函数。那么这个构造函数将会被调用,把string转换为Foo,再把Foo传递给你调用的函数。这个转换可以进行,但也是麻烦的源泉,对象被转换和新对象生成超出了你的意图。用explicit 声明一个构造函数可以防止构造函数在隐式转换的情况下被调用。


优点:

避免了不良的转换

缺点:无

决定:

所有带有一个参数的构造函数都必须是显示声明的。在一个参数的构造函数定义前面加explicit 。如explicit Foo(string name);

复制构造函数例外,如果我们允许,在很少情况它们是非显示的。被其他类透明包装的类也例外。这些例外都应该写明清楚的注释。

 

复制构造函数

有必要的时候提复制构造函数和赋值操作。否则的话用DISALLOW_COPY_AND_ASSIGN.进行禁止。

 

定义:

复制构造函数和赋值操作用于赋值对象的副本。复制构造函数有时会被编译器隐式调用,比如传值的方式传递对象。

优点:

复制构造函数能容易复制对象。STL容器需要它的内容是可以复制和能赋值的。复制构造函数比CopyFrom()函数更有效率,它结合了构造和拷贝操作,在某些环境下编译器可以省掉,很容易避免了堆内存的申请。

缺点:

隐式对象拷贝容易引发bug和性能的问题。同时也降低了代码的可读性,因为当对象被进行值传递而不是通过引用时很难进行跟踪,对象修改的地方也很难体现。

决定:

少部分的类需要可以拷贝。大部分不需要复制构造函数和赋值函数。在很多情况下,指针或者引用能跟对象的副本一样很好的工作。例如,你可以给一个函数传引用或者指针替代传值,也可以在STL容器里存指针而不是对象。

如果你的类需要可拷贝,提供一个实习复制的方法,如CopyFrom()  Clone()会更好,因为这样的方法不会被隐式调用。如果一个复制的方法不足以实现功能,那就同时提供复制构造函数和赋值构造函数。

如果你的类不需要复制 函数或赋值构造函数,必须要显示禁止掉。

// A macro to disallow the copy constructorand 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&)

class Foo {

 public:

 Foo(int f);

 ~Foo();

 private:

 DISALLOW_COPY_AND_ASSIGN(Foo);

};

 

结构体和类

   当只有数据成员时用struct,其他情况都用class

StructclassC++中基本上是一样的。我们为每个关键字添加了语意,所以你应该在定义数据类型时使用恰当的关键字。

   Struct在只包含数据成员的消极对象使用。有时可能跟常量有关。但是不存在访问数据成员的任何函数。对其中数据的访问是直接访问而不是通过方法调用。方法不提供任何行为,仅仅是用来设置数据成员,例如构造函数,析构函数和Initialize()Reset()Validate().等。

如果需要更多的方法,那就用class。如果有疑问就直接用class。如果与STL结合,在仿函数和特性中可以用struct替代class

注意:成员变量在structclass中有不同的命名规则。

 

继承:

   组合经常比继承更好。当使用继承时,请使用public 继承。

定义:

当子类继承自基类,它继承了父类定义的所有数据成员和操作。实际上,在C++中主要有两种继承方式:实现继承,即子类继承了父类的实现代码;接口继承,即只有方法名被继承。

优点:

     实现继承通过重用基类的代码特化已存在的类型来减少了代码量。由于继承是编译时声明,你和编译器都能明白操作和发现错误。接口继承用来加强程序编程公开类特定的API。在这种情况下,如果一个类没有定义实现一个API,编译器就能发现错误。

 

缺点:

对于实现继承,由于实现代码在基类和子类之间实现了延伸,使得更加难以理解实现。子类不能重写非虚函数,所以子类不能改变实现代码。基类可能定义一下数据成员,因此还需指明基类物理布局。

决定:

所有的继承都应该是public继承。如果你想private继承,应该包含一个基类的实例成员。

不要过度使用实现继承。只用组合会更合适。尽量做到在“IS-A”情况下才使用继承。如果Bar可以说是一个Foo时,Bar才能是Foo的子类。

使用虚析构函数。如果你的类存在虚函数,析构函数应该是虚函数。

限定需要在子类中访问的成员函数为protect,数据成员应该为private

当重定义一个虚函数时,应该在子类的声明里显示的声明为虚函数。原因是:如果虚函数被遗漏,读者需要检查所有的祖先类来确定一个函数是否是虚函数。

多继承

  很少有多实现继承是真正有用的。当最多只有一个基类有实现,其他基类都是纯接口类时我们允许多继承

定义:

   多继承允许子类有多于一个基类。我们必须区别纯接口基类和带实现的基类。

优点:

    多实现继承可以让你重用比单继承更多的代码。

缺点:

    只有很少的多实现继承是真正有用的。当多实现继承看起来好像是个解决方案时,其实你可以找到一个不同的更好的解决方案。

决定:

    我们允许多继承,仅仅是当除了第一个类以外的所有超类都是纯接口时。为了确保他们保持纯接口,需要加上Interface 

接口

   接口是满足特定条件的类,但不是必须加Interface 后缀。

定义:

   当一个类满足下列条件就是一个接口:

1、  只有public的纯虚函数和静态方法

2、 没有非静态的数据成员

3、 不需要定义任何构造函数。。如果提供构造函数,那么它必须是不带参数而且是protect的。

4、 如果是子类,它只能继承自满足这些条件的类和标记Interface 后缀的类。

接口不能被直接实例化,因为它声明了纯虚函数。为了确保所有接口的实现能被正确的销毁,接口必须声明一个虚析构函数。

 

优点:

Interface标记类使其他人知道这些类不能添加实现方法或者非静态数据成员。这点在多继承时尤其重要。此外,接口这个概念已经被Java程序员所熟知。

缺点:

    Interface后缀加长了类名,从而使之难读和理解。接口特性作为实现细节不应该暴露给客户。

决定:

   当满足以上条件时一个类才是一个接口。但是满足以上条件的类不需要一定要Interface后缀。

操作符重载

   不要重载操作符,除非特殊情况。

定义:

    类可以定义操作符如+/操作,就像是内置类型一样。

优点:

    可以使代码更直观,类可以有像内置类型一样的行为。重载操作符函数有着有趣的名字,相比之下,Equals()  Add()函数就逊色多了。为了某些模版很好的工作,你可能需要定义操作符。

缺点:

重载操作符使代码直观的同时也存在一些不足:

1、 它可能使直观的你误认为昂贵的操作就像内置类型那样容易实现。

2、 重载操作符很难找到它的调用处。找Equals()的调用比找==的相关调用容易得多。

3、 一些操作符对指针也起作用,从而容易引起bug&Foo + 4 Foo + 4 做完全不同的操作。编译器不会报错,所以也很难调试。

重载还要惊人的影响,例如,一个类重载了一元操作符&,它就很难被安全的前向声明。

 

决定:

通常不要重载操作符。赋值操作尤其危险,应该避免。如果需要你可以定义像Equals() CopyFrom()的方法。同样,如果一个类有可能被前向声明,物流如何都要避免重载操作符&

很少有为了与模版或标准C++类交互而需要重载操作符的情况。如果合情合理这些都是可接受的,如果可能你应该避免这样做。不要为了在STL容器中做key使用就重载操作符==<。取而代之,你应该写比较和等值比较函数。

有的STL算法并不需要你重载==,如果你那么做最好提供文档说明。

 

访问控制

     把数据成员声明为private,如果需要则提供存取函数对它们进行访问。例如你定义变量Foo_和取值函数foo(),那应该定义赋值函数set_foo().

例外:静态常量数据成员不需要是private

取值函数的定义一般是在头文件的内联函数。

声明顺序

     在类里的用特定的声明次序,publicprivate前面,方法在数据成员前面。

  类定义开始应该是public:节,接着是protected:然后才是private。如果其中是空的节则省略掉。

在每节中声明的顺序如下:

1、 Typedefs Enums

2、 Const常量

3、 构造函数

4、 析构函数

5、 方法,包括静态方法

6、 数据成员,除了静态常量数据成员


友元声明应该放在privateDISALLOW_COPY_AND_ASSIGN 宏应该放在private的末尾。

cpp文件中的方法定义应该进可能的跟声明的顺序一样。

编写短小的函数

  写小而集中的函数。

长的函数有时候也是可以的,并没有硬性规定函数的长度。如果一个函数超过40行,在不影响程序结构的情况下考虑是否可以对其进行分解。

即使现在你的长函数运行得很好,将来别人对其进行修改可能添加新的功能。由此可能引发很难发现的bug。保持你的函数小而简单,这样其他人可以更容易读懂和修改。

你会发现有的长而复杂的函数使用某些代码。不要害怕修改这些已经存在的代码,如果证明使用这样的函数很困难,很难调试或你需要在不同的地方使用其中相同的代码段,可以考虑分解函数为小而易管理的代码。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值