More Effective C++----(4)避免无用的缺省构造函数 & (5)谨慎定义类型转换函数

Item M4:避免无用的缺省构造函数


缺省构造函数(指没有参数的构造函数)在C++语言中是一种让你无中生有的方法。构造函数能初始化对象,而缺省构造函数则可以不利用任何在建立对象时的外部数据就能初始化对象。有时这样的方法是不错的。例如一些行为特性与数字相仿的对象被初始化为空值或不确定的值也是合理的,还有比如链表、哈希表、图等等数据结构也可以被初始化为空容器。


但不是所有的对象都属于上述类型,对于很多对象来说,不利用外部数据进行完全的初始化是不合理的。比如一个没有输入姓名的地址簿对象,就没有任何意义。在一些公司里,所有的设备都必须标有一个公司ID号码,所以在建立对象以模型化一个设备时,不提供一个合适的ID号码,所建立的对象就根本没有意义。

在一个完美的世界里,无需任何数据即可建立对象的类可以包含缺省构造函数,而需要数据来建立对象的类则不能包含缺省构造函数。唉!可是我们的现实世界不是完美的,所以我们必须考虑更多的因素。特别是如果一个类没有缺省构造函数,就会存在一些使用上的限制。

请考虑一下有这样一个类,它表示公司的设备,这个类包含一个公司的ID代码,这个ID代码被强制做为构造函数的参数:
class EquipmentPiece {
public:
  EquipmentPiece(int IDNumber);
  ...
};
因为EquipmentPiece类没有一个缺省构造函数,所以在三种情况下使用它,就会遇到问题。第一中情况是 建立数组时。 一般来说 ,没有一种办法能在建立对象数组时给构造函数传递参数。所以在通常情况下,不可能建立EquipmentPiece对象数组:

EquipmentPiece bestPieces[10];           // 错误!没有正确调用
                                         // EquipmentPiece 构造函数
 EquipmentPiece *bestPieces =
  new EquipmentPiece[10];                // 错误!与上面的问题一样
不过还是有三种方法能回避开这个限制。对于使用 非堆数组(non-heap arrays)(即不在堆中给数组分配内存。译者注)的一种解决方法是在数组定义时提供必要的参数:
int ID1, ID2, ID3, ..., ID10;            // 存储设备ID号的
                                         // 变量
... 
EquipmentPiece bestPieces[] = {          // 正确, 提供了构造
  EquipmentPiece(ID1),                   // 函数的参数
  EquipmentPiece(ID2),
  EquipmentPiece(ID3),
  ...,
  EquipmentPiece(ID10)
};

不过很遗憾,这种方法 不能用在堆数组(heap arrays)的定义上。

一个更通用的解决方法是 利用指针数组来代替一个对象数组
typedef EquipmentPiece* PEP;             //  PEP 指针指向
                                         //一个EquipmentPiece对象
 
PEP bestPieces[10];                      // 正确, 没有调用构造函数
PEP *bestPieces = new PEP[10];           // 也正确

指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象:
for (int i = 0; i < 10; ++i)
  bestPieces[i] = new EquipmentPiece( ID Number );

不过这中方法有两个缺点, 第一你必须删除数组里每个指针所指向的对象。如果你忘了,就会发生内存泄漏。第二 增加了内存分配量,因为正如你需要空间来容纳EquipmentPiece对象一样,你也需要空间来容纳指针。

如果你 为数组分配raw memory,你就可以避免浪费内存。使用placement new方法(参见条款M8)在内存中构造EquipmentPiece对象:

// 为大小为10的数组 分配足够的内存
// EquipmentPiece 对象; 详细情况请参见条款M8
// operator new[] 函数
void *rawMemory =
  operator new[](10*sizeof(EquipmentPiece));
// make bestPieces point to it so it can be treated as an
// EquipmentPiece array
EquipmentPiece *bestPieces =
  static_cast<EquipmentPiece*>(rawMemory);
// construct the EquipmentPiece objects in the memory
// 使用"placement new" (参见条款M8)
for (int i = 0; i < 10; ++i)
  new (&bestPieces[i]) EquipmentPiece( ID Number );

注意你仍旧得为每一个EquipmentPiece对象提供构造函数参数。这个技术(和指针数组的主意一样)允许你在没有缺省构造函数的情况下建立一个对象数组。 它没有绕过对构造函数参数的需求,实际上也做不到。如果能做到的话,就不能保证对象被正确初始化。

使用placement new的缺点除了是大多数程序员对它不熟悉外(能使用它就更难了),还有就是当你不想让它继续存在使用时,必须手动调用数组对象的析构函数,然后调用操作符delete[]来释放raw memory(请再参见条款M8):( WQ加注,已经有placement delete/delete []操作符了,它会自动调用析构函数。)

// 以与构造bestPieces对象相反的顺序
// 解构它。
for (int i = 9; i >= 0; --i)
  bestPieces[i].~EquipmentPiece(); 
// deallocate the raw memory
operator delete[](rawMemory);

如果你忘记了这个要求而使用了普通的数组删除方法,那么你程序的运行将是 不可预测的。这是因为: 直接删除一个不是用new操作符来分配的内存指针,其结果没有被定义。
delete [] bestPieces;                    // 没有定义! bestPieces
                                    //不是用new操作符分配的。

有关new、placement new和它们如何与构造函数、析构函数一起使用的更多信息,请见条款M8。

对于类里没有定义缺省构造函数所造成的 第二个问题是它们无法在许多基于模板(template-based)的容器类里使用。 因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数,这是一个常见的要求。这个要求总是来自于模板内部,被建立的模板参数类型数组里。例如一个数组模板类:

template<class T>
class Array {
public:
  Array(int size);
  ... 
private:
  T *data;
}; 
template<class T>
Array<T>::Array(int size)
{
  data = new T[size];                    // 为每个数组元素 
  ...                                    //依次调用 T::T()
}

在多数情况下,通过仔细设计模板可以杜绝对缺省构造函数的需求。例如标准的vector模板(生成一个类似于可扩展数组的类)对它的类型参数没有必须有缺省构造函数的要求。不幸的是,很多模板类没有以仔细的态度去设计。这样,没有缺省构造函数的类就不能与许多模板兼容。当C++程序员深入领会了模板设计以后,这样的问题应该不再那么突出了。这会花多长时间,完全在于个人的造化。

最后讲一下在设计虚基类时所面临的要提供缺省构造函数还是不提供缺省构造函数的两难决策。 不提供缺省构造函数的虚基类,很难与其进行合作。因为几乎所有的派生类在实例化时都必须给虚基类构造函数提供参数。这就要求所有由没有缺省构造函数的虚基类继承下来的派生类(无论有多远)都必须知道并理解提供给虚基类构造函数的参数的含义。派生类的作者是不会企盼和喜欢这种规定的。

因为这些强加于没有缺省构造函数的类上的种种限制,一些人认为所有的类都应该有缺省构造函数,即使缺省构造函数没有足够的数据来完整初始化一个对象。比如这个原则的拥护者会这样修改EquipmentPiece类:

class EquipmentPiece {
public:
  EquipmentPiece(  int IDNumber = UNSPECIFIED);
  ...
private:
  static const int   UNSPECIFIED;        // 其值代表ID值不确定。
};  

这允许这样建立EquipmentPiece对象
EquipmentPiece e;                         //这样合法

这样的修改 使得其他成员函数变得复杂,因为不再能确保EquipmentPiece对象进行了有意义的初始化。假设它建立一个因没有ID而没有意义的EquipmentPiece对象,那么大多数成员函数必须检测ID是否存在。如果不存在ID,它们将必须指出怎么犯的错误。不过通常不明确应该怎么去做,很多代码的实现什么也没有提供:只是抛出一个异常或调用一个函数终止程序。当这种情形发生时,很难说提供缺省构造函数而放弃了一种保证机制的做法是否能提高软件的总体质量。

提供无意义的缺省构造函数也会 影响类的工作效率。如果成员函数必须测试所有的部分是否都被正确地初始化,那么这些函数的调用者就得为此付出更多的时间。而且还得付出更多的代码,因为这使得可执行文件或库变得更大。它们也得在测试失败的地方放置代码来处理错误。如果一个类的构造函数能够确保所有的部分被正确初始化,所有这些弊病都能够避免。 缺省构造函数一般不会提供这种保证,所以在它们可能使类变得没有意义时,尽量去避免使用它们。 使用这种(没有缺省构造函数的)类的确有一些限制,但是当你使用它时,它也给你提供了一种保证:你能相信这个类被正确地建立和高效地实现。

Item M5:谨慎定义类型转换函数

C++编译器能够在两种数据类型之间进行 隐式转换(implicit conversions),它继承了C语言的转换方法,例如允许把char隐式转换为int和从short隐式转换为double。因此当你把一个short值传递给准备接受double参数值的函数时,依然可以成功运行。 C中许多这种可怕的转换可能会导致数据的丢失,它们在C++中依然存在,包括int到short的转换和double到char的转换。

你对这些类型转换是无能为力的,因为它们是 语言本身的特性。不过当你增加自己的类型时,你就可以有更多的控制力,因为你能选择是否提供函数让编译器进行隐式类型转换。

有两种函数允许编译器进行这些的转换: 单参数构造函数(single-argument constructors)和隐式类型转换运算符单参数构造函数是指只用一个参数即可以调用的构造函数。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的所有参数都有缺省值。以下有两个例子:

class Name {                                 // for names of things
public:
  Name(const string& s);                     // 转换 string 到
                                             // Name
  ...  
}; 
class Rational {                             // 有理数类
public:
  Rational(int numerator = 0,                // 转换int到
           int denominator = 1);             // 有理数类
  ... 
};

注:关于转换构造函数和隐式类型转换运算符,请参考< http://blog.csdn.net/qianqin_2014/article/details/51316461>

隐式类型转换运算符只是一个样子奇怪的成员函数: operator 关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。例如为了允许Rational(有理数)类隐式地转换为double类型(在用有理数进行混合类型运算时,可能有用),你可以如此声明Rational类:

class Rational {
public:
  ...
  operator double() const;                   // 转换Rational类成
};                                           // double类型

在下面这种情况下,这个函数会被自动调用:
Rational r(1, 2);                            // r 的值是1/2
 
double d = 0.5 * r;                          // 转换 r 到double,
                                         // 然后做乘法

以上这些说明只是一个复习,我真正想说的是为什么你不需要定义各种类型转换函数

根本问题是当你在不需要使用转换函数时,这些函数却会被调用运行。结果,这些不正确的程序会做出一些令人恼火的事情,而你又很难判断出原因。

让我们首先分析一下隐式类型转换运算符,它们是最容易处理的。假设你有一个如上所述的Rational类,你想让该类拥有打印有理数对象的功能,就好像它是一个内置类型。因此,你可能会这么写:

Rational r(1, 2); 
cout << r;                                    // 应该打印出"1/2"

再假设你忘了为Rational对象定义operator<<。你可能想打印操作将失败,因为没有合适的的operator<<被调用。但是你错了。 当编译器调用operator<<时,会发现没有这样的函数存在,但是它会试图找到一个合适的隐式类型转换顺序以使得函数调用正常运行。类型转换顺序的规则定义是复杂的,但是在现在这种情况下,编译器会发现它们能调用Rational::operator double函数来把r转换为double类型。所以上述代码打印的结果是一个浮点数,而不是一个有理数。这简直是一个灾难,但是它表明了隐式类型转换的缺点:它们的存在将导致错误的发生。

解决方法是用不使用语法关键字的等同的函数来替代转换运算符。例如为了把Rational对象转换为double,用asDouble函数代替operator double函数:

class Rational {
public:
  ...
  double asDouble() const;                   //转变 Rational
};                                       // 成double

这个成员函数能被显式调用:

Rational r(1, 2);
 
cout << r;                             // 错误! Rationa对象没有
                                     // operator<< 
cout << r.asDouble();                   // 正确, 用double类型 
                                    //打印r

在多数情况下, 这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再会发生,这点损失是值得的。 一般来说,越有经验的C++程序员就越喜欢避开类型转换运算符。例如在C++标准库(参见Effective C++条款49和M35)委员会工作的人员是在此领域最有经验的,他们加在库函数中的string类型没有包括隐式地从string转换成C风格的char*的功能,而是定义了一个成员函数c_str用来完成这个转换,这是巧合么?我看不是。

通过单参数构造函数进行隐式类型转换更难消除。而且在很多情况下这些函数所导致的问题要甚于隐式类型转换运算符。

举一个例子,一个array类模板,这些数组需要调用者确定边界的上限与下限:
template<class T>
class Array {
public:
  Array(int lowBound, int highBound);
  Array(int size); 
  T& operator[](int index); 
  ... 
};

第一个构造函数允许调用者确定数组索引的范围,例如从10到20。它是一个两参数构造函数,所以不能做为类型转换函数。第二个构造函数让调用者仅仅定义数组元素的个数(使用方法与内置数组的使用相似), 不过不同的是它能做为类型转换函数使用,能导致无穷的痛苦。

例如比较Array<int>对象,部分代码如下:
bool operator==( const Array<int>& lhs,
                 const Array<int>& rhs); 
Array<int> a(10);
Array<int> b(10); 
... 
for (int i = 0; i < 10; ++i)
  if (a == b[i]) {               // 哎呦! "a" 应该是 "a[i]"
    do something for when
    a[i] and b[i] are equal;
  }
  else {
    do something for when they're not;
  }

我们想用a的每个元素与b的每个元素相比较,但是当录入a时,我们偶然忘记了数组下标。当然我们希望编译器能报出各种各样的警告信息,但是它根本没有。 因为它把这个调用看成用Array<int>参数(对于a)和int(对于b[i])参数调用operator==函数,然而没有operator==函数是这样的参数类型,我们的编译器注意到它能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int类型的参数。然后编译器如此去编译,生成的代码就象这样:
for (int i = 0; i < 10; ++i)
  if (a == static_cast< Array<int> >(b[i]))   ...

每一次循环都把a的内容与一个大小为b[i]的临时数组(内容是未定义的)比较。这 不仅不可能以正确的方法运行,而且还是效率低下的。因为每一次循环我们都必须建立和释放Array<int>对象(见条款M19)。

通过不声明运算符(operator)的方法,可以克服隐式类型转换运算符的缺点,但是单参数构造函数没有那么简单。毕竟,你确实想给调用者提供一个单参数构造函数。同时你也希望防止编译器不加鉴别地调用这个构造函数。幸运的是,有一个方法可以让你鱼肉与熊掌兼得。事实上是两个方法:一是容易的方法,二是当你的编译器不支持容易的方法时所必须使用的方法。

容易的方法是利用一个最新编译器的特性 ,explicit关键字。为了解决隐式类型转换而特别引入的这个特性,它的使用方法很好理解。构造函数用explicit声明,如果这样做,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法:
template<class T>
class Array {
public:
  ...
  explicit Array(int size);           // 注意使用"explicit"
  ...
}; 
Array<int> a(10);                 // 正确, explicit 构造函数
                               // 在建立对象时能正常使用
Array<int> b(10);                // 也正确 
if (a == b[i]) ...                   // 错误! 没有办法
                               // 隐式转换
                               // int 到 Array<int> 
if (a == Array<int>(b[i])) ...        // 正确,显式从int到
                               // Array<int>转换
                               // (但是代码的逻辑
                               // 不合理) 
if (a == static_cast< Array<int> >(b[i])) ...
                               // 同样正确,同样
                               // 不合理 
if (a == (Array<int>)b[i]) ...        //C风格的转换也正确,
                               // 但是逻辑
                               // 依旧不合理

在例子里使用了static_cast(参见条款M2), 两个“>”字符间的空格不能漏掉,如果这样写语句:

if (a == static_cast<Array<int>>(b[i])) ...

这是一个不同的含义的语句。 因为C++编译器把“>>”做为一个符号来解释。在两个“>”间没有空格,语句会产生语法错误。

如果你的编译器不支持explicit,你不得不回到不使用成为隐式类型转换函数的单参数构造函数。

我前面说过复杂的规则决定哪一个隐式类型转换是合法的,哪一个是不合法的。这些规则中没有一个转换能够包含用户自定义类型(调用单参数构造函数或隐式类型转换运算符)。你 能利用这个规则来正确构造你的类,使得对象能够正常构造,同时去掉你不想要的隐式类型转换。

再来想一下数组模板,你 需要用整形变量做为构造函数参数来确定数组大小,但是同时又必须防止从整数类型到临时数组对象的隐式类型转换。你要达到这个目的,先要建立一个新类ArraySize。这个对象只有一个目的就是表示将要建立数组的大小。你必须修改Array的单参数构造函数,用一个ArraySize对象来代替int。代码如下:

template<class T>
class Array {
public: 
  class ArraySize {                    // 这个类是新的
  public:
    ArraySize(int numElements): theSize(numElements) {}
    int size() const { return theSize; } 
  private:
    int theSize;
  }; 
Array(int lowBound, int highBound);
  Array(ArraySize size);                  // 注意新的声明 
... 
};

这里把ArraySize嵌套入Array中,为了 强调它总是与Array一起使用。你也必须声明ArraySize为公有,为了让任何人都能使用它。

想一下,当通过单参数构造函数定义Array对象,会发生什么样的事情:
Array<int> a(10);

你的编译器要求用int参数调用Array<int>里的构造函数,但是没有这样的构造函数。 编译器意识到它能从int参数转换成一个临时ArraySize对象,ArraySize对象只是Array<int>构造函数所需要的,这样编译器进行了转换。函数调用(及其后的对象建立)也就成功了。

事实上你仍旧能够安心地构造Array对象,不过这样做能够使你避免类型转换。考虑一下以下代码:

bool operator==( const Array<int>& lhs,
                 const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i)
  if (a == b[i]) ...                 // 哎呦! "a" 应该是 "a[i]";
                               // 现在是一个错误。

为了调用operator==函数,编译器要求Array<int>对象在”==”右侧,但是 不存在一个参数为int的单参数构造函数而且编译器无法把int转换成一个临时ArraySize对象然后通过这个临时对象建立必须的Array<int>对象,因为这将调用两个用户定义(user-defined)的类型转换,一个从int到ArraySize,一个从ArraySize到Array<int>。这种转换顺序被禁止的,所以当试图进行比较时编译器肯定会产生错误。

ArraySize类的使用有些象一个有目的的帮手,这是一个更通用技术的应用实例。类似于ArraySize的类经常被称为 proxy classes(代理类),因为这样类的每一个对象都为了支持其他对象的工作。 ArraySize对象实际是一个整数类型的替代者,用来在建立Array对象时确定数组大小。Proxy对象能帮你更好地控制软件的在某些方面的行为,否则你就不能控制这些行为,比如在上面的情况里,这种行为是指隐式类型转换,所以它值得你去学习和使用。你可能会问你如何去学习它呢?一种方法是转向条款M30;它专门讨论proxy classes。

在你跳到条款M30之前,再仔细考虑一下本条款的内容。 让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值