#include <iostream>
using namespace std;
class Foo
{
public:
Foo(): initialised(0)
{
cout << "Foo() gets called AFTER test() ?!" << endl;
};
Foo test()
{
cout << "initialised= " << initialised << " ?! - ";
cout << "but I expect it to be 0 from the 'initialised(0)' initialiser on Foo()" << endl;
cout << "this method test() is clearly working on an uninitialised object ?!" << endl;
return Foo();
}
~Foo()
{};
private:
int initialised;
};
int main()
{
//SURE this is bad coding but it compiles and runs
//I want my class to DETECT and THROW an error to prevent this type of coding
//in other words how to catch it at run time and throw "not initialised" or something
Foo foo=foo.test();
}
4 回答
是的,它在尚未构造的对象上调用函数,这是未定义的行为。您无法检测到它是否可靠。我认为您也不应该尝试检测它。与例如在已删除的对象上调用函数相比,这不是偶然发生的事情。试图抓住所有可能的错误几乎是不可能的。声明的名称在其初始化程序中已经可见,用于其他有用的目的。考虑一下:
Type *t = (Type*)malloc(sizeof(*t));
这是 C 编程中的常见习语,在 C++ 中仍然有效。
就个人而言,我喜欢Herb Sutter 关于空引用(同样无效)的这个故事。要点是,不要试图避免语言明确禁止的情况,尤其是在一般情况下无法可靠诊断的情况。随着时间的流逝,您将获得虚假的安全性,这变得非常危险。相反,训练你对语言和设计界面的理解(避免原始指针,......),以减少犯错的机会。
在 C++ 和 C 中,许多情况并未明确禁止,而是未定义。部分是因为有些事情很难有效地诊断,部分是因为未定义的行为允许实现为它设计替代行为而不是完全忽略它——现有编译器经常使用它。
例如,在上述情况下,任何实现都可以随意抛出异常。还有其他情况同样是未定义的行为,对于实现来说更难有效诊断:在构造之前访问不同翻译单元中的对象就是这样的例子——这就是所谓的静态初始化顺序惨败。
构造函数是您想要的方法(不在初始化之前运行,而是在初始化时运行,但这应该没问题)。它在您的情况下不起作用的原因是您在这里有未定义的行为。
特别是,您使用尚不存在的 foo 对象来初始化自身(例如foo
infoo.Test()
尚不存在)。您可以通过显式创建对象来解决它:
Foo foo=Foo().test()
您无法在程序中检查它,但也许 valgrind 可以找到这种类型的错误(就像任何其他未初始化的内存访问一样)。
你不能阻止人们编码不好,真的。它就像“应该”一样工作:
- 为 Foo 分配内存(这是“this”指针的值)
- 通过执行以下操作进入 Foo::test: Foo::test(this),其中,
- 它通过this->initialized获取值,这是随机垃圾,然后它
- 调用 Foo 的默认构造函数(因为 return Foo();),然后
- 调用 Foo 的复制构造函数,复制右手的 Foo()。
就像它应该的那样。你不能阻止人们不知道使用 C++ 的正确方法。
你能做的最好的就是有一个神奇的数字:
class A
{
public:
A(void) :
_magicFlag(1337)
{
}
void some_method(void)
{
assert (_magicFlag == 1337); /* make sure the constructor has been called */
}
private:
unsigned _magicFlag;
}
这“有效”是因为 _magicFlag 在值已经是 1337 的地方被分配的机会很低。
但真的,不要这样做。
您收到很多回复,基本上是说“您不应该指望编译器会帮助您解决这个问题”。但是,我同意你的观点,编译器应该通过某种诊断来帮助解决这个问题。不幸的是(正如其他答案所指出的那样),语言规范在这里没有帮助 - 一旦你到达声明的初始化部分,新声明的标识符就在范围内。
不久前,DDJ 有一篇关于名为“DogTag”的简单调试类的文章,可以用作调试辅助工具来帮助:
- 删除后使用对象
- 用垃圾覆盖对象的内存
- 在初始化对象之前使用它
我没有使用太多 - 但它确实在遇到一些内存覆盖错误的嵌入式项目中派上用场。
它基本上是对GMan 所描述的“MagicFlag”技术的阐述。