在C++编程中,智能指针和this指针的问题是一个常见但难以发现的问题。智能指针是一种自动管理内存的机制,而this指针则用于指向当前对象实例。当一个实例方法(即具有this的方法)清除了持有实际实例的智能指针时,当前对象将被释放,但this指针仍然可用,指向垃圾内存。因此,任何对"this"实例的其他成员的访问都将处理垃圾内存,可能导致许多问题。
在垃圾收集语言中,如C#或Java,这种情况不会发生(至少在没有与非托管对象交互或语言本身没有错误的情况下不会发生),因为即使是隐式的this变量也是一个垃圾收集引用(或者如果喜欢的话,一种智能指针)。这使得从垃圾收集语言移植代码到C++比第一眼看起来要困难得多。
让看一个基本的例子:
public class MyClass {
private readonly string Name;
public static MyClass GlobalInstance;
public MyClass() {
Name = "I only put a name here so we have a field to access";
}
public void MakeUnavailable() {
if (GlobalInstance == this) {
GlobalInstance = null;
Console.WriteLine("The instance \"" + this.Name + "\" is no more the GlobalInstance.");
}
}
}
在某个时刻,可以这样做:
MyClass.GlobalInstance = new MyClass();
MyClass.GlobalInstance.MakeUnavailable();
在C++中,代码非常相似:
class MyClass {
private:
string Name;
public:
static shared_ptr GlobalInstance;
MyClass() {
Name = "I only put a name here so we have a field to access";
}
void MakeUnavailable() {
if (GlobalInstance.get() == this) {
GlobalInstance.reset();
printf("The instance %s is no more the GlobalInstance.\n", this->Name);
}
}
};
再次,在另一个地方可以这样做:
MyClass::GlobalInstance = make_shared();
MyClass::GlobalInstance->MakeUnavailable();
请注意,静态变量和公共字段不是一个好的设计,但只想展示一个非常基本的例子,说明可能出错的情况。这段代码也不是线程安全的。
可以看到,C++和C#的代码非常相似。然而,C#代码将工作得很好。在将GlobalInstance设置为null之后,可以无问题地读取Name字段,因为this变量保证了当前对象保持活动。C++版本是不安全的。一旦GlobalInstance被重置,实例就会死亡,然后this->Name在printf()上将读取垃圾。有趣的是,许多这样的调用会工作,因为回收的内存不会立即清除或用于其他任何东西(特别是如果使用int变量而不是string),但当它实际失败时,就很难说出是什么问题了。
知道这个基本例子很天真。没有人写“好”的代码,将静态变量保持实例存活。然而,这个例子只是为了展示使用非常少量的代码可能出错的情况。
在大多数现实世界的情况下,错误发生在回调和使用像COM这样的引用计数对象时。代码可以像这样简单:
void InvokeCallback() {
if (m_InvokingCallback) return;
m_InvokingCallback = true;
InvokeCallback(this);
m_InvokingCallback = false;
}
类似于此的代码在许多通知中使用,以保护对象免受递归生成通知的影响。问题是对象可能在调用InvokeCallback()期间死亡,而粗体行执行m_InvokingCallback = false将访问垃圾。
想象一下InvokeCallback()实际上是一个MouseEntered事件。按钮是引用计数的,当在屏幕上时,计数至少为一。然而,回调可能会从屏幕上移除按钮,如果没有任何其他智能指针或AddRef()完成,其计数将减少到零,如果回调返回前没有其他引用,就会删除它。再次记住,this变量是一个基本指针,不是智能指针。它不会在按钮实例上执行AddRef()/Release()对,所以可能会在这里崩溃。
解决问题
知道问题存在是很好的。更好的是知道如何解决它。但这可能并不容易。考虑到没有控制/组件的源代码,可能有这个问题,可能需要寻找工作方式。
如果面对一个按钮不能在MoveMoved、Click或类似事件期间从屏幕上移除的情况,解决方法可能是向父控件、窗口或通过Dispatcher->BeginInvoke()发送消息,以便在控制不再执行回调时稍后执行该代码;
如果代码正在调用有问题的对象的方法,实际上是在调用过程中崩溃的,可以尝试保护该调用。也就是说,而不是写像这样的东西:
button->Click();
// Here the button may reach a refcount of zero and be deleted before the Click call returns
可以这样做:
ComPtr
或者,如果不关心异常,可以直接调用AddRef/Release:
button->AddRef();
button->Click();
button->Release();
在这两种替代方案中,即使在回调期间从屏幕上移除按钮,也会将按钮保留在内存中,避免在回调期间死亡;
当然,如果组件的所有者,可以尝试通过在调用任何回调之前执行AddRef/Release(带或不带智能指针)来保护组件,以防止在错误的时间死亡:
void InvokeCallback() {
if (m_InvokingCallback) return;
m_InvokingCallback = true;
ComPtr keepAlive(this);
InvokeCallback(this);
m_InvokingCallback = false;
}
重要的是要注意,对象不需要是COM对象才能具有AddRef和Release方法。此外,可以创建许多变体,甚至使用布尔值标记对象处于有问题的情况,并禁止在回调中间删除(如果用户尝试这样做可能会抛出异常),或者通过注册删除请求标志,然后在有问题的情况完成后删除对象。