最近我遇到了我的应用程序的一些奇怪的行为。它主要是在C#中开发的,但CLI / C ++也用于实现更好的性能。我在TimeSpan比较中以一种非常简单的方法获得了System.NullReferenceException:
TimeSpan _timestamp;
void UpdateFrame(TimeSpan timestamp)
{
if(TimeSpan::Equals(_timestamp, timestamp) == false)
很明显,此表达式中使用的唯一引用是隐式this(this._timestamp)。我添加了一个断言语句,结果证明这实际上是空的。经过短暂的调查,我设法准备了一个简短的程它是C ++ / CLI。
using namespace System;
using namespace System::Reflection;
public class Unmanaged
{
public:
int value;
};
public ref class Managed
{
public:
int value;
Unmanaged* GetUnmanaged()
{
SampleMethod();
return new Unmanaged();
}
void SampleMethod()
{
System::Diagnostics::Debug::Assert(this != nullptr);
this->value = 0;
}
};
public ref class ManagedAccessor
{
public:
property Managed^ m;
};
int main(array<System::String ^> ^args)
{
ManagedAccessor^ ma = gcnew ManagedAccessor();
// Confirm that ma->m == null
System::Diagnostics::Debug::Assert(ma->m == nullptr);
// Invoke method on the null reference
delete ma->m->GetUnmanaged();
return 0;
}
有人知道怎么可能吗?这是编译器中的错误吗?
答案 0 :(得分:19)
在C ++中(可能在C ++ / CLI中)没有什么能阻止你尝试在NULL指针上调用方法。在大多数实现中,虚拟方法调用将在调用时崩溃,因为运行时将无法读取虚方法表。但是,非虚拟方法调用只是一个带有一些参数的函数调用,其中一个参数是this
指针。如果它为null,那么就是传递给函数的内容。
我相信在NULL
(或nullptr
)指针上调用任何成员函数的结果是正式的“未定义行为”。
答案 1 :(得分:12)
谢谢Greg的回答,就像你描述的那样。但是,我对这种情况不满意,因为这意味着我必须放置
if(this == nullptr) throw gcnew ArgumentException("this");
在每个方法的开头。只有这样才能保证我的方法不会在没有参数验证的情况下作为错误的代码段出现在堆栈跟踪的顶部。
当我用C#编写时,我从未遇到过(这= = null)。因此,我决定了解它与C ++ / CLI有何不同。我在C ++ / CLI中创建了一个示例应用程序:
namespace ThisEqualsNull{
public ref class A
{
public:
void SampleMethod()
{
System::Diagnostics::Debug::Assert(this != nullptr);
}
};
public ref class Program{
public:
static void Main(array<System::String ^> ^args)
{
A^ a = nullptr;
a->SampleMethod();
}
};
}
C#中的一个小程序,它使用具有相同Main方法的C ++ / CLI类:
class Program
{
static void Main(string[] args)
{
A a = null;
a.SampleMethod();
}
}
然后我用Red Gate的.NET Reflector反汇编它们:
C++/CLI
.method public hidebysig static void Main(string[] args) cil managed
{
.maxstack 1
.locals ( [0] class ThisEqualsNull.A a)
L_0000: ldnull
L_0001: stloc.0
L_0002: ldnull
L_0003: stloc.0
L_0004: ldloc.0
L_0005: call instance void ThisEqualsNull.A::SampleMethod()
L_000a: ret
}
C#
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init ( [0] class [ThisEqualsNull]ThisEqualsNull.A a)
L_0000: nop
L_0001: ldnull
L_0002: stloc.0
L_0003: ldloc.0
L_0004: callvirt instance void [ThisEqualsNull]ThisEqualsNull.A::SampleMethod()
L_0009: nop
L_000a: ret
}
重要的部分是:
C++/CLI
L_0005: call instance void ThisEqualsNull.A::SampleMethod()
C#
L_0004: callvirt instance void [ThisEqualsNull]ThisEqualsNull.A::SampleMethod()
其中:
现在最后的结论是:
VS 2008中的C#编译器将每个方法视为虚拟,因此可以安全地假设(this!= null)。在C ++ / CLI中,每个方法都应该被调用,因此有必要注意非虚方法调用。