显式调用构造函数

9
我知道我们可以显式和隐式地调用构造函数的概念,并且我已经测试了这两种情况(通常情况下,直接调用构造函数就能满足我的所有目的),但我想知道当我们创建对象时构造函数会自动被调用,那么为什么要显式地调用构造函数呢?在显式调用构造函数和隐式调用构造函数之间有什么优缺点呢?
例子:
class integer
{
   int m ,n;
 public:
   integer (int x , int y); 
};
integer :: integer (int x , int y )
{
   m=x; n = y;
}

现在,如果我这样调用:
integer int1 = integer( 0 , 100); //  Explicit Call
integer int1(1,100); // implicit call

2
首先,具有两个或更多参数的构造函数只能显式调用。 - aschepler
1
你能展示一些只使用隐式构造函数的代码吗?我感觉这个问题缺少一些上下文。 - Useless
好的,你的例子标题为“显式调用”的实际上是对两个参数构造函数的显式调用,然后是对复制构造函数的隐式调用,而你的例子标题为“隐式调用”的实际上是一个显式调用。因此,在询问任何优点之前,你应该先对定义进行排序,因为并非所有可以通过显式构造函数调用完成的操作都可以通过隐式构造函数调用完成(正如aschepler已经指出的那样),这将是一个明显的优点。 - Christian Rau
@Christian Rau 我的代码只是一个示例代码,用于展示每个调用,我只想问一下,我可以通过两种类型来调用构造函数,但为什么要使用显式调用? - Tejendra
@Tejendra Ok,暂且不考虑显式调用的真正定义,考虑你自己的定义,那么在integer int1 = integer(0, 100)integer int1(1, 100)之间选择确实没有意义。但请记住,显式构造函数调用的实际定义是完全不同的(而你的第二个示例实际上是显式构造函数调用),这使得大多数现有答案根本没有回答你的实际问题。并且显式构造函数调用具有许多优点,因为你大多数情况下使用它们来做一些隐式无法完成的事情。 - Christian Rau
@Christian Rau...根据您的陈述,“而且显式构造函数调用还具有许多优点,因为您大多数时候会将它们用于无法使用隐式构造函数完成的任务。” 您能否以答案的形式写下这些优点? - Tejendra
5个回答

20

这里有两个不同的问题,因为你对于“显式(explicit)”和“隐式(implicit)”的定义与标准定义不同(现有大部分答案都是基于标准定义,因为它们是在你添加包含自己定义的" 显式(explicit)"和" 隐式(implicit)"示例之前撰写的)。

好的,让我们首先考虑你所定义的" 显式(explicit)",你的定义是什么(我猜你称之为" 显式(explicit)"是因为你明确地写出了类型名称?):

integer int1 = integer(0, 100);

相对于你对隐式的定义:

integer int1(1, 100);
在这种情况下,第一个“explicit”调用与第二个“implicit”调用没有任何优势。但仍然有区别。第一个实际上使用两个参数的构造函数创建了一个临时对象,然后使用复制构造函数来创建int1。尽管在实践中编译器通常会优化掉这个额外的复制,但如果您的复制构造函数是私有的,它仍然不起作用,而第二个只需要两个参数的构造函数(您甚至可以将其视为缺点)。
但现在到实际的关于“explicit”和“implicit”的标准定义。显式构造函数调用是你明确调用的任何构造函数。实际上,每当您使用圆括号语法()创建对象时,您都会显式调用构造函数;否则就是隐式构造函数调用(也就是由编译器在幕后完成)。
integer int1;                   // implicit default constructor
integer int1(1, 100);           // explicit two-arg constructor
integer int1 = integer(0, 100); // explicit two-arg constructor, implicit copy constructor

void func(integer);             // function taking by-value
func(int1);                     // implicit copy constructor

因此,只有默认构造函数和任何一个参数的构造函数(包括复制和移动构造函数)可以被隐式调用。在这方面特别的问题是,不是复制/移动构造函数的单参数构造函数:

struct integer
{
    integer(int);
};

这允许编译器隐式地调用构造函数以转换类型,因此任何int都可以隐式转换为integer

void func(integer);
func(42);             // implicit call to int-constructor
为了禁止这种行为,您需要将构造函数标记为explicit
struct integer
{
    explicit integer(int);
};
explicit关键字只允许显式调用构造函数(例如func(integer(42))),这样做的好处是它不会在后台引入未被注意或不必要的转换,从而导致各种难以发现的问题和重载解析的模糊性。因此,通常的做法是将任何转换构造函数(一个参数的非复制/移动构造函数)标记为explicit,并且很可能也是C++11最终引入explicit转换运算符的原因。
总之,根据您的定义和示例,使用integer int1 = integer(1, 100);而不是integer int1(1, 100);并没有任何优势,虽然它有一些(通常无关紧要)的差别。
但根据标准定义,explicit构造函数调用比隐式构造函数调用有很多优势,因为实际上只有通过显式构造函数调用才能明确地构造对象,而隐式构造函数调用只在某些情况下在后台执行,并且仅适用于零个和一个参数的构造函数(正如aschepler已经指出的那样)。显式标记转换构造函数为explicit具有防止在后台进行不需要的隐式转换的优点。

我猜它在一定程度上解决了我的问题...我有关于显式构造函数(通过在构造函数名称后添加explicit关键字)的优点的想法。顺便说一下,谢谢你的详细解释。 - Tejendra
但是我没有理解你在这行代码中所说的一点:“隐式构造函数调用只在某些情况下在幕后进行,并且仅适用于单参数构造函数(对于零参数,我明白了)”。请编辑您的答案并包含此内容。 - Tejendra
@Tejendra 我增加了一个讨论,关于明确标记构造函数 explicit 的优点。 - Christian Rau
@Tejendra,我不知道还有什么可说的了。每当你写(...)来创建一个对象时,根据“显式”这个词的标准定义,这被视为显式构造函数调用(与你示例代码中使用的定义相反)。只有在编译器在幕后自动执行构造函数调用而无需你显式创建新对象时,才会按照标准定义将构造函数调用称为“隐式”。因此,integer int1(whatever);始终被称为“显式构造函数调用”,与你自己的定义相反。 - Christian Rau
例如,编译器可以(并且将会)在按值传递对象或从函数返回对象时隐式复制对象(使用复制构造函数)。或者它可以隐式地将一个类型的对象转换为另一个类型的对象(使用相应的构造函数,如果它没有标记为“显式”)。这些都是单参数构造函数。 - Christian Rau
@Tejendra,它不能隐式地使用两个(或更多)参数构造函数,因为没有办法表示应该转换为一个值的两个值(使用某些构造函数)。例如,您如何调用void func(integer)以使(int,int)构造函数隐式调用?您不能只调用它func(1,100),您只能(隐式地)将某种类型的一个对象转换为另一种类型的一个对象。 - Christian Rau

2

显式调用构造函数允许您使用参数构造对象,而不是使用默认构造函数。

class Foo
{
  public:
    Foo() {}
    Foo(int bar) : mBar(bar) {}
  private:
    int mBar;
}

Foo f;    // Implicitly constructed with default constructor.
Foo f(7); // Explicitly constructed with argument for 'bar'

2

构造函数有三种调用方式:

  • 隐式地,通过声明一个未初始化的类型实例
  • 同样是隐式地,通过使用=初始化实例或导致参数类型向您的类进行隐式转换。
  • 显式调用构造函数,传递参数。

在特定上下文中使用哪种取决于您正在调用的构造函数。

class Foo 
{
    Foo();                                  // 1
    Foo(int a);                             // 2
    explicit foo(const std::string& f);     // 3
    Foo(int c, int d);                      // 4
};
  1. 在声明Foo f;时,将隐式调用此构造函数。绝不要尝试显式地调用没有参数的构造函数,因为Foo f();声明一个函数!
  2. 可以使用Foo f = 42;Foo f(42)来调用这个函数。
  3. explicit关键字禁止通过Foo f = std::string("abc");function_taking_foo(function_returning_string());进行隐式转换。
  4. 由于有多个参数,只适用于显式版本。

1

我很不愿意这么说,因为这太反常了,但还有一种显式调用构造函数的方法。

class integer
{
   int m ,n;
 public:
   integer (int x , int y); 
};
integer :: integer (int x , int y )
{
   m=x; n = y;
}

构造函数可以在已经构建的对象上被显式调用。
integer i(1,100);
i.~integer();
i.integer::integer(2,200);

这里我显式地构造了一个整数实例。然后我显式地调用了它的析构函数。然后我再次显式地调用了构造函数。我认为你可以在测试中使用这种习惯用法。我不知道标准中是否禁止使用它。它在Visual Studio 2010中有效。我还没有测试过很多编译器。

对于“显式”的大值,这些调用是显式的。


我在 g++ 编译器中尝试了,但出现了这个错误.. ---> 错误:不能直接调用构造函数 'integer::integer' - chaitanya lakkundi
我认为instance.Class::Class(...)不是有效的语法,但你仍然可以这样做:new (&i) integer(2,200)。另外请注意,您不需要先调用析构函数;C++对象模型要求调用析构函数。虽然在实践中,类本身可能会隐含地依赖于它发生,因此在有意义的情况下这样做是一个好主意。 - sfink

0
如果您创建一个函数,该函数接受对您的类对象的引用,并且您传递的是除您的对象之外的其他类型,则您的类的构造函数将将该类型转换为您的类的对象。任何一个参数的构造函数都被视为转换构造函数。如果您声明该构造函数为显式构造函数,则将不会将传递给该函数的不同类型转换为您的对象,并且编译器将返回错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接