翻译《有关编程、重构及其他的终极问题?》——19.如何合理的从一个构造函数中调用另外一个构造函数

时间:2021-12-21 19:28:34

翻译《有关编程、重构及其他的终极问题?》——19.如何合理的从一个构造函数中调用另外一个构造函数

标签(空格分隔): 翻译 技术 C/C++
作者:Andrey Karpov
翻译者:顾笑群 - Rafael Gu
最后更新:2017年02月22日


19.如何合理的从一个构造函数中调用另外一个构造函数

这个问题是从LibreOffice项目中发现的。PVS-Studio诊断的错误描述为:V603 The object was created but it is not being used. If you wish to call constructor, ‘this->Guess::Guess(….)’ should be used(译者注:大意为有个创建了但没有被使用的对象,如果想调用构造函数,应该使用‘this->Guess::Guess(….)’这样)。

Guess::Guess()
{
  language_str = DEFAULT_LANGUAGE;
  country_str = DEFAULT_COUNTRY;
  encoding_str = DEFAULT_ENCODING;
}

Guess::Guess(const char * guess_str)
{
  Guess();
  ....
}

解释
好的程序员讨厌写模棱两可的代码,很棒。但在处理多个构造函数时,想尽量让代码简单和干净,就有些搬起石头砸自己的脚了。

你知道的,一个构造函数式不能被像普通函数那样调用的。如果我们写了“A::A(int x) { A(); }”这样的代码,运行时就会导致创建一个A类型的临时匿名对象,而不是调用那个没有参数的构造函数。

所以上面问题代码运行后,实际发生的事情是这样:一个Guess类型的临时匿名对象被创建出来后,马上就被销毁,导致其language_str等成员变量未初始化。

正确的代码
通常有三种方式在构造函数中避免模棱两可的代码。让我们看下把。

第一种方式是实现一个专门用来初始化的函数,然后在所有的构造函数中调用它。因为这个方式显而易见,我这里就不写示例代码了。

这种方式是一种不错的、可靠的、干净的并且安全的技术。然而一些差劲的程序员企图让他们的代码看起来更短。所以我不得不再说说另外两种方式。

这另外的两种方式是非常危险的,并且需要你充分理解它们是如何工作的,而且你要知道你必须面对的后果是什么。

第二种方式:

Guess::Guess(const char * guess_str)
{
  new (this) Guess();
  ....
}

第三种方式:

Guess::Guess(const char * guess_str)
{
  this->Guess();
  ....
}

这第二种以及第三种方式是相当危险的,因为类实例化了两次。这种代码会引起一些微妙的bug,所以坏处会多于好处。大家可以考虑一下类似这种构造函数在那些情况下是合适的,那些情况下不太合适。

这里有一个例子是合适的情况:

class SomeClass
{
  int x, y;
public:
  SomeClass() { new (this) SomeClass(0,0); }
  SomeClass(int xx, int yy) : x(xx), y(yy) {}
};

上面这段代码是很安全的,因为其类中占有简单的数据类型,而且没有任何基类。所以两次的构造函数调用不会有任何危险。

然后,下面�有另外一段代码,当调用构造函数时会引起错误:

class Base 
{ 
public: 
 char *ptr; 
 std::vector vect; 
 Base() { ptr = new char[1000]; } 
 ~Base() { delete [] ptr; } 
}; 

class Derived : Base 
{ 
  Derived(Foo foo) { } 
  Derived(Bar bar) { 
     new (this) Derived(bar.foo); 
  }
  Derived(Bar bar, int) { 
     this->Derived(bar.foo); 
  }
}

我们使用了语句“new (this)Derived(bar.foo);”或者“this->Derived(bar.foo)”来调用构造函数。

基类的对象已经被创建了,而且其成员变量也被初始化了。再次调用构造函数会导致初始化两次。结果指针ptr被再次新分配的内存地址给替换了,导致内存泄露。至于对std::vector类型的两次初始化,其结果就更难预测了。只有一件事情是肯定的:像这样的代码不应被允许。

难道我们要处理这么多头疼的事情才能解决这个问题吗?如果你不使用C++11的特性,那么就是用第一种方式吧(创建一个专门的初始化函数)。其实,一个明确的调用构造函数的需求是很少见的。

建议
现在我们还有最后一个特性要介绍,用来帮助我们解决调用构造函数的问题!

C++11允许我们的构造函数调用其他构造函数(被称之为代理)。这种方式可以让构造函数用最少的代码使用其他构造函数。

比如:

Guess::Guess(const char * guess_str) : Guess()
{
  ....
}

要像学习更多有关代理构造函数的知识,请看如下链接:
1. Wikipedia,C++11:对象构造函数的改进
2. C++11 FAQ:代理构造函数
3. MSDN:统一初始化和代理构造函数