【发布时间】:2009-05-06 00:35:50
【问题描述】:
我知道我们可以在 C++ 中使用范围解析运算符显式调用类的构造函数,即className::className()。我想知道我到底需要在哪里打这样的电话。
【问题讨论】:
-
说可以直接调用构造函数是不对的。该标准明确具有 (12.1/1):“构造函数没有名称。”您只能通过其他构造调用构造函数,例如函数样式转换或放置 new。
标签: c++ constructor
我知道我们可以在 C++ 中使用范围解析运算符显式调用类的构造函数,即className::className()。我想知道我到底需要在哪里打这样的电话。
【问题讨论】:
标签: c++ constructor
您有时还显式使用构造函数来构建临时对象。例如,如果您有一些带有构造函数的类:
class Foo
{
Foo(char* c, int i);
};
还有一个函数
void Bar(Foo foo);
但你身边没有 Foo,你可以这样做
Bar(Foo("hello", 5));
这就像演员表。事实上,如果你有一个只接受一个参数的构造函数,C++ 编译器将使用该构造函数来执行隐式转换。
在已经存在的对象上调用构造函数是不合法的。也就是说,你不能这样做
Foo foo;
foo.Foo(); // compile error!
不管你做什么。但是您可以在不分配内存的情况下调用构造函数 - 这就是 placement new 的用途。
char buffer[sizeof(Foo)]; // a bit of memory
Foo* foo = new(buffer) Foo(); // construct a Foo inside buffer
你给 new 一些内存,它会在那个地方构造对象而不是分配新的内存。这种用法被认为是邪恶的,在大多数类型的代码中很少见,但在嵌入式和数据结构代码中很常见。
例如,std::vector::push_back 使用此技术来调用复制构造函数。这样,它只需要做一个副本,而不是创建一个空对象并使用赋值运算符。
【讨论】:
<typename>(ctor-arg list),要学究起来和<typename>::<typename>(ctor-arg list)不一样。语法只是让它看起来像你在调用构造函数。事实上,你从不像函数一样“调用”构造函数。
<typename>::<typename> <var-name>(ctor-arg list) 时实际发生了什么?
大多数情况下,在需要一些参数的子类构造函数中:
class BaseClass
{
public:
BaseClass( const std::string& name ) : m_name( name ) { }
const std::string& getName() const { return m_name; }
private:
const std::string m_name;
//...
};
class DerivedClass : public BaseClass
{
public:
DerivedClass( const std::string& name ) : BaseClass( name ) { }
// ...
};
class TestClass :
{
public:
TestClass( int testValue ); //...
};
class UniqueTestClass
: public BaseClass
, public TestClass
{
public:
UniqueTestClass()
: BaseClass( "UniqueTest" )
, TestClass( 42 )
{ }
// ...
};
...例如。
除此之外,我没有看到该实用程序。我只是在我太年轻不知道我真正在做什么的时候才在其他代码中调用了构造函数......
【讨论】:
我认为编译器错误 C2585 的错误消息给出了您需要在构造函数上实际使用范围解析运算符的最佳理由,并且它与 Charlie 的回答相符:
从基于多重继承的类或结构类型转换。如果该类型多次继承同一个基类,则转换函数或运算符必须使用范围解析 (::) 来指定要在转换中使用哪些继承的类。
假设你有 BaseClass,BaseClassA 和 BaseClassB 都继承 BaseClass,然后 DerivedClass 继承 BaseClassA 和 BaseClassB。
如果您正在执行转换或运算符重载以将 DerivedClass 转换为 BaseClassA 或 BaseClassB,则需要确定在转换中使用哪个构造函数(我在想像复制构造函数,IIRC)。
【讨论】:
通常你不会直接调用构造函数。 new 运算符为您调用它,或者子类调用父类的构造函数。在 C++ 中,保证基类在派生类的构造函数启动之前完全构造。
您唯一一次直接调用构造函数是在极少数情况下您无需使用 new 来管理内存。即使那样,你也不应该这样做。相反,您应该使用 operator new 的放置形式。
【讨论】:
我认为您通常不会将其用于构造函数,至少不会以您描述的方式使用。但是,如果您在不同的命名空间中有两个类,您将需要它。例如,要指定这两个组成的类之间的区别,Xml::Element 和 Chemistry::Element。
通常,类的名称与作用域解析运算符一起使用,以调用继承类的父类上的函数。因此,如果您有一个继承自 Animal 的 Dog 类,并且这两个类都以不同的方式定义了函数 Eat(),那么您可能希望在名为“someDog”的 Dog 对象上使用 Animal 版本的 eat。我的 C++ 语法有点生疏,但我认为在这种情况下你会说 someDog.Animal::Eat()。
【讨论】:
在一些有效的用例中,您希望公开类的构造函数。例如,如果您希望使用 arena 分配器进行自己的内存管理,您将需要一个由分配和对象初始化组成的两阶段构造。
我采用的方法与许多其他语言的方法相似。我只是将我的构造代码放在众所周知的公共方法中(Construct()、init() 等),并在需要时直接调用它们。
您可以创建与您的构造函数匹配的这些方法的重载;您的常规构造函数只是调用它们。在代码中放入大的 cmets 以警告其他人您正在这样做,这样他们就不会在错误的地方添加重要的构造代码。
请记住,无论使用哪种构造重载,都只有一种析构方法,因此请使您的析构函数对未初始化的成员具有健壮性。
我建议不要尝试编写可以重新初始化的初始化程序。由于未初始化的内存与实际保存真实数据,很难判断您正在查看的对象中是否包含垃圾。
最困难的问题来自具有虚拟方法的类。在这种情况下,编译器通常会在类的开头插入 vtable 函数表指针作为隐藏字段。您可以手动初始化此指针,但您基本上取决于编译器特定的行为,并且可能会让您的同事看着您很有趣。
安置新在许多方面都被打破了;在数组的构造/销毁中是一种情况,所以我倾向于不使用它。
【讨论】:
考虑以下程序。
template<class T>
double GetAverage(T tArray[], int nElements)
{
T tSum = T(); // tSum = 0
for (int nIndex = 0; nIndex < nElements; ++nIndex)
{
tSum += tArray[nIndex];
}
// Whatever type of T is, convert to double
return double(tSum) / nElements;
}
这将显式调用默认构造函数来初始化变量。
【讨论】: