UP | HOME

构造函数

Table of Contents

摘抄自C++ FAQ Lite的构造函数笔记:

转载说明

本文摘抄自C++ FAQ Lite中文版

版权声明: Part of C++ FAQ Lite, Copyright © 1991-2001, Marshall Cline, cline@parashift.com

[10.7] 可以在构造函数中使用 this 指针吗?UPDATED!

[Recently rewrote because of a suggestion from Perry Rapp (on 4/01). Click here to go to the next FAQ in the "chain" of recent changes.]

某些人认为不应该在构造函数中使用this指针,因为这时this对象还没有完全形成。然后,只要你小心,是可以在构造函数(在函数体甚至在初始化列表中)使用this的。

以下是始终可行的:构造函数的函数体(或构造函数所调用的函数)能可靠地访问基类中声明的数据成员和/或构造函数所属类声明的数据成员。这是因为所有这些数据成员被保证在构造函数函数体开始执行时已经被完整的建立。

以下是始终不可行的:构造函数的函数体(或构造函数所调用的函数)不能向下调用被派生类重定义的虚函数。如果你的目的是得到派生类重定义的函数,那么你将无功而返。注意,无论你如何调用虚成员函数:显式使用this指针(如,this->method()),隐式的使用this指针(如,method()),或甚至在this对象上调用其他函数来调用该虚成员函数,你都不会得到派生类的重写函数。这是底线:即使调用者正在构建一个派生类的对象,在基类的构造函数执行期间,对象还不是一个派生类的对象。

以下是有时可行的:如果传递 this 对象的任何一个数据成员给另一个数据成员的初始化程序,你必须确保该数据成员已经被初始化。好消息是你能使用一些不依赖于你所使用的编译器的显著的语言规则,来确定那个数据成员是否已经(或者还没有)被初始化。坏消息是你必须知道这些语言规则(例如,基类子对象首先被初始化(如果有多重和/或虚继承,则查询这个次序!),然后类中定义的数据成员根据在类中声明的次序被初始化)。如果你不知道这些规则,则不要从this对象传递任何数据成员(不论是否显式的使用了this关键字)给任何其他数据成员的初始化程序!如果你知道这些规则,则需要小心。

[10.8] 什么是“命名的构造函数法(Named Constructor Idiom)”? UPDATED!

[Recently fixed a typo (Fred vs. Point) in the prose thanks to Roy LeCates (on 7/00). Click here to go to the next FAQ in the "chain" of recent changes.]

为你的类的用户提供的一种更直觉的和/或更安全的构造操作技巧。

问题在于构造函数总是有和类相同的名字。因此,区分类的不同的构造函数是通过参数列表。但如果有许多构造函数,它们之间的区别有时就会很敏感并且有错误倾向。

使用命名的构造函数法(Named Constructor Idiom),在private:节和protected:节中声明所有类的构造函数,并提供返回一个对象的public static 方法。这些方法由此称为“命名的构造函数(Named Constructors)”。一般,每种不同的构造对象的方法都有一个这样的静态方法。

例如,假设我们正在建立一个描绘X-Y平面的Point类。通常有两种方法指定一个二维空间坐标:矩形坐标(X+Y),极坐标(Radius+Angle)(半径+角度)。(不必担心已经忘了这些;重点不在于坐标系统的析解;重点在于有几种方法来创建一个Point对象。)不幸的是,这两种坐标系统的参数是相同的:两个 float。这将在重载构造函数中导致一个“重载不明确”的错误:

 
 class Point {
 public:
   Point(float x, float y);     // 矩形坐标
   Point(float r, float a);     // 极坐标 (半径和角度)
   // 错误:重载不明确:Point::Point(float,float)
 };

 int main()
 {
   Point p = Point(5.7, 1.2);   // 不明确:哪个坐标系统?
 }

解决这个不明确错误的一种方法是使用命名的构造函数法(Named Constructor Idiom):

#include <cmath>               // To get sin() and cos()

class Point {
public:
  static Point rectangular(float x, float y);      // 矩形坐标
  static Point polar(float radius, float angle);   // 极坐标
  // 这些 static 方法称为“命名的构造函数(named constructors)”
  // ...
private:
  Point(float x, float y);     // 矩形坐标
  float x_, y_;
};

inline Point::Point(float x, float y)
: x_(x), y_(y) { }

inline Point Point::rectangular(float x, float y)
{ return Point(x, y); }

inline Point Point::polar(float radius, float angle)
{ return Point(radius*cos(angle), radius*sin(angle)); }

现在,Point的用户有了一个清晰的和明确的语法在任何一个坐标系统中创建Point对象:

int main()
{
  Point p1 = Point::rectangular(5.7, 1.2);   // 显然是矩形坐标
  Point p2 = Point::polar(5.7, 1.2);         // 显然是极坐标
}

如果期望Point有派生类,则确保你的构造函数在protected:节中。

命名的构造函数法也能用于总是通过new来创建对象。

[10.9] 为何不能在构造函数的初始化列表中初始化静态成员数据?UPDATED!

[Recently added a "," in the initialization list thanks to Yaroslav Mironov (on 4/01). Click here to go to the next FAQ in the "chain" of recent changes.]

因为必须显式定义类的静态数据成员。

Fred.h:

class Fred {
public:
  Fred();
  // ...
private:
  int i_;
  static int j_;
};

Fred.cpp (或 Fred.C 或其他):

Fred::Fred()
  : i_(10)  // 正确:能够(而且应该)这样初始化成员数据
  , j_(42)  // 错误:不能象这样初始化静态成员数据
{
  // ...
}

// 必须这样定义静态数据成员:
int Fred::j_ = 42; 

[10.10] 为何有静态数据成员的类得到了链接错误?

因为静态数据成员必须被显式定义在一个编辑单元中。如果不这样做,你就可能得到"undefined external"链接错误。例如: Fred.h

class Fred {
public:
  // ...
private:
  static int j_;   // 声明静态数据成员:Fred::j_
  // ...
};

链接器会向你抱怨("Fred::j_ is not defined"),除非你在一个源文件中定义(而不仅仅是声明)Fred::j

// Fred.cpp

#include "Fred.h"

int Fred::j_ = some_expression_evaluating_to_an_int;

// Alternatively, if you wish to use the implicit 0 value for static ints:
// int Fred::j_;

通常定义Fred类的静态数据成员的地方是Fred.cpp文件(或者Fred.C或者你使用的其他扩展名)。

[10.11] 什么是“static initialization order fiasco”?

你的项目的微妙杀手。

static initialization order fiasco是对C++的一个非常微妙的并且常见的误解。不幸的是,错误发生在main()开始之前,很难检测到。

简而言之,假设你有存在于不同的源文件x.cpp 和y.cpp的两个静态对象x 和 y。再假定y对象的构造函数会调用x对象的某些方法。

就是这些。就这么简单。

结局是你完蛋不完蛋的机会是50%-50%。如果碰巧x.cpp的编辑单元先被初始化,这很好。但如果y.cpp的编辑单元先被初始化,然后y的构造函数比x的构造函数先运行。也就是说,y的构造函数会调用x对象的方法,而x对象还没有被构造。

我听说有些人受雇于麦当劳,享受他们的切碎肉的新工作去了。

如果你觉得不用工作,在卧室的一角玩俄罗斯方块是令人兴奋的,你可以到此为止。相反,如果你想通过用一种系统的方法防止灾难,来提高自己继续工作而存活的机会,你可能想阅读下一个 FAQ。

注意:static initialization order fiasco不作用于内建的/固有的类型,象int 或 char*。例如,如果创建一个static float对象,不会有静态初始化次序的问题。静态初始化次序真正会崩溃的时机只有在你的static或全局对象有构造函数时。

[10.13] 对于静态数据成员,如何防止“static initialization order fiasco”?

使用与描述过的相同的技巧,但这次使用静态成员函数而不是全局函数而已。

假设类 X 有一个static Fred对象:

// File X.hpp

class X {
public:
  // ...

private:
  static Fred x_;
};

自然的,该静态成员被分开初始化:

// File X.cpp

#include "X.hpp"

Fred X::x_;

自然的,Fred对象会在 X 的一个或多个方法中被使用:

void X::someMethod()
{
  x_.goBowling();
}

但现在“灾难情景”就是如果某人在某处不知何故在Fred对象被构造前调用这个方法。例如,如果某人在静态初始化期间创建一个静态的 X 对象并调用它的someMethod()方法,然后你就受制于编译器是在someMethod()被调用之前或之后构造 X::x。(ANSI/ISO C++委员会正在设法解决这个问题,但诸多的编译器对处理这些更改一般还没有完成;关注此处将来的更新。)

无论何种结果,将X::x_ 静态数据成员改为静态成员函数总是最简便和安全的:

// File X.hpp

class X {
public:
  // ...

private:
  static Fred& x();
};

自然的,该静态成员被分开初始化:

// File X.cpp

#include "X.hpp"

Fred& X::x()
{
  static Fred* ans = new Fred();
  return *ans;
}

然后,简单地将 x_ 改为 x():

void X::someMethod()
{
  x().goBowling();
}

如果你对性能敏感并且关心每次调用X::someMethod()的额外的函数调用的开销,你可以设置一个static Fred&来取代。正如你所记得的,静态局部对象仅被初始化一次(控制流程首次越过它们的声明处时),因此,将只调用X::x()一次:X::someMethod()首次被调用时:

void X::someMethod()
{
  static Fred& x = X::x();
  x.goBowling();
}

注意:对于内建/固有类型,象int 或 char*,不必这样做。例如,如果创建一个静态的或全局的float对象,不需要将它包裹于函数之中。静态初始化次序真正会崩溃的时机只有在你的static或全局对象有构造函数时。

[10.15] 什么是“命名参数法(Named Parameter Idiom)”?NEW!

[Recently created (on 4/01). Click here to go to the next FAQ in the "chain" of recent changes.]

发掘方法链的非常有用的方法。

命名参数法(Named Parameter Idiom)解决的最基本问题是C++仅支持位置相关的参数。例如,函数调用者不能说“这个值给形参xyz,另一个值给形参pqr”。在C++(和C 和Java)中只能说“这是第一个参数,这是第二个参数等”。Ada语言提出并实现的命名参数,对于带有大量的可缺省参数的函数尤其有用。

多年来,人们构造了很多方案来弥补C 和 C++缺乏的命名参数。其中包括将参数值隐藏于一个字符串参数,然后在运行时解析这个字符串。例如,这就是fopen()的第二个参数的做法。另一种方案是将所有的布尔参数联合成一个位映射,然后调用者将这堆转换成位的常量共同产生一个实际的参数。例如,这就是open()的第二个参数的做法。这些方法可以工作,但下面的技术产生的调用者的代码更明显,更容易写,更容易读,而且一般来说更雅致。

这个想法,称为命名参数法(Named Parameter Idiom),它是将函数的参数变为以新的方式创建的类的方法,这些方法通过引用返回*this。然后你只要将主要的函数改名为那个类中的无参数的“随意”方法。

举一个例子来解释上面那段。

这个例子实现“打开一个文件”的概念。该概念逻辑上需要一个文件名的参数,和一些允许选择的参数,文件是否被只读或可读写或只写的方式打开;如果文件不存在,是否创建它;是从末尾写(添加"append")还是从起始处写(覆盖"overwrite");如果文件被创建,指定块大小; I/O是否有缓冲区,缓冲区大小;文件是被共享还是独占访问;以及其他可能的选项。如果我们用常规的位置相关的参数的函数实现这个概念,那么调用者的代码会非常难读:有8个可选的参数,并且调用者很可能犯错误。因此我们使用命名参数用法来取代。

在实现它之前,假如你想接受函数的所有默认参数,看一下调用者的代码是什么样子:

  File f = OpenFile("foo.txt");

那是简单的情况。现在看一下如果你想改变一大堆的参数:

File f = OpenFile("foo.txt").
           readonly().
           createIfNotExist().
           appendWhenWriting().
           blockSize(1024).
           unbuffered().
           exclusiveAccess();

注意这些“参数”,被公平的以随机的顺序(位置无关的)调用并且都有名字。因此,程序员不必记住参数的顺序,而且这些名字是(正如所希望的)意义明显的。

以下是如何实现:首先创建一个新的类(OpenFile),该类包含了所有的参数值作为 private: 数据成员。然后所有的方法(readonly(), blockSize(unsigned), 等)返回*this(也就是返回一个OpenFile对象的引用,以允许方法被链状调用)。最后完成一个带有必要参数(在这里,就是文件名)的常规的,参数位置相关的OpenFile的构造函数。

class File;

class OpenFile {
public:
  OpenFile(const string& filename);
    // 为每个数据成员设置默认值
  OpenFile& readonly();  // 将 readonly_ 变为 true
  OpenFile& createIfNotExist();
  OpenFile& blockSize(unsigned nbytes);
  // ...
private:
  friend File;
  bool readonly_;       // 默认为 false [举例]
  // ...
  unsigned blockSize_;  // 默认为 4096 [举例]
  // ...
};

要做的另外一件事就是使得 File的构造函数带一个OpenFile对象:

class File {
public:
  File(const OpenFile& params);
    // vacuums the actual params out of the OpenFile object

  // ...
};

注意OpenFile 将 File 声明为友元,这样OpenFile 就不需要一大堆的(而且没用的)public: get 方法。

Author: Le Cao

Date: 2010-10-12 16:35:58 CST

HTML generated by org-mode TAG=7.01g in emacs 23