
Jo Stichbury开放本代码诊所是为了提供更多有关Symbian C++的建议。
在本月的诊所文章中,Jo Stichbury解释了在混合继承的类中使用清理栈出现两个严重错误的原因。
新的代码诊所文章将在每个月的第一个星期五发表。你有Symbian C++方面的问题?请将问题发送至位于 sdn@symbian.com的代码医生。
如果问题被接收,你会获得Symbian出版社出版的 Symbian OS软件开发(第2版)。
亲爱的诊所医生
我在调试编译模式下从清理栈弹出指针时收到E32USER-CBase 90严重错误 。 Symbian开发者资料库文档 显示"当弹出项与预期项不符时" 会出现这种错误,但是我无法获知这是如何发生的。我已经附加上我的代码,请问你能帮我解释一下哪里错了吗?
迷惑的用户 (Neil, 曼彻斯特)
亲爱的Neil,
感谢你发送来的问题和代码。我会在这里粘贴部分代码并进一步解释问题所在,以及如何解决该问题。
首先,先为你解释一下这个严重错误的含义。当你使用CleanupStack::Pop()或CleanupStack::PopAndDestroy()的某一种检查重载方式将一个指针压入清理栈并在随后试图将其弹出时,可能发生E32USER-CBase 90严重错误。这些函数重载需要接收一个参数来指出你期望移除的指针。在调试编译时,如果你传入的指针并不是位于清理栈栈顶的指针,则会发生这种严重错误。
文档建议最好的操作是使用检查重载,因为它们允许你保证对清理栈的使用是按照预期方式进行的。例如,在大量使用清理栈的复杂函数中,可以很方便的在函数返回之前使用检查重载以确保你已经计算了压入清理栈的每个指针。但是你遇到的情况下,该检查似乎无故失败了。这里是导致该问题的代码:
CTestClass* testClass = CTestClass::NewLC(); CleanupStack::Pop(testClass); // E32USER-CBase 90 严重错误!
好,这很直截了当。一个工厂函数将该指针压入清理栈然后返回。下一行代码将该指针弹出...并且发生严重错误。不妙!
那么在CTestClass::NewLC()里发生了什么呢?
/*static*/ CTestClass* CTestClass::NewLC()
{
CTestClass* me = new(ELeave) CTestClass();
CleanupStack::PushL(me);
return (me);
}
这部分代码非常简单。它是用于 二阶段构造 的静态工厂函数,但是你实际上并没有调用第二阶段构造函数。更重要的是,我查看了CTestClass()的构造函数代码,非常正确,它并没有将任何对象压入清理栈。到目前为止,你的代码都是没有问题的,你确实是弹出自己压入的对象。那么为什么会发生严重错误呢?
让我们来看看你的类:
class MCallback
{
public:
virtual void OnCallback() = 0;
};
class CTestClass : public MCallback, public CBase
{
public:
static CTestClass* NewLC();
~CTestClass() {};
public: // 继承自MCallback
virtual void OnCallback();
protected:
CTestClass() {};
};
啊,这就是问题所在。看看你声明继承的地方:
class CTestClass : public MCallback, public CBase
继承时的顺序将混合类放在CBase类之前。Symbian OS中与C类混合继承时,需要将C类放在基类列表的首位。该问题的解决方法很简单,将你的代码修改如下:
class CTestClass : public CBase, public MCallback
但是为什么呢? 正如 Symbian开发者资料库文档 给出的解释,确保首先声明CBase类意味着在C类和TAny*指针之间的安全转换时可能的。在实际中,这意味着它允许你避免由于Symbian OS内部代码假设TAny*和C类指针能够转换而造成的严重错误。为了保持如下解释的易懂性,我会避免使用一些术语,但是如果你有兴趣了解更多,我建议你参考[R1]。
当你在CTestClass::NewLC()中调用CleanupStack::PushL()时,你将你在堆上创建CTestClass对象返回的指针作为参数传入。然而,由于CleanupStack::PushL()具有接收CBase*指针参数的重载,并且CTestClass继承自CBase(声明为第二个基类),就会出现隐式转换。这意味着传入PushL()的内存地址并不是CTestClass对象的起始处,而是增加了4字节,位于CBase“子对象” 的内存地址。图1对此进行说明:
图 1:CTestClass布局
PushL() 函数在清理栈保存CBase*指针(以TCleanupItem的形式,如参考文献[R2]所述)。在如上的示例中,它保存内存位置0x377a2234。
/*static*/ CTestClass* CTestClass::NewLC()
{
CTestClass* me = new(ELeave) CTestClass();
// 例如, *me位于0x377a2230
CleanupStack::PushL(me);
// PushL()接收内存位置0x377a2234
return (me);
}
当你稍后调用CleanupStack::Pop()从清理栈移除该指针时,你调用如下的函数(来自e32base.inl):
// CleanupStack类
#ifdef _DEBUG
#ifdef _DEBUG
inline void CleanupStack::Pop(TAny* aExpectedItem)
{ CleanupStack::Check(aExpectedItem); CleanupStack::Pop(); }
注意该函数以TAny*作为参数,并将其传递给Check()。Check()函数简单地将存于清理栈栈顶的内存位置和作为预期项的传入参数进行比较。如果两者相同,则检查通过,否则发生E32USER-CBase 90严重错误。
你调用Check()函数错误的原因是Pop()函数将TAny*而不是CBase*作为参数。当你调用Pop()时,没有发生改变指针指向CBase子对象的隐式转换。取而代之传入的是CTestClass对象起始处的地址(0x377A2230)。Check()函数在相同的对象中比较两个不同的内存位置,因此失败并发生严重错误。图2对此进行说明:
图 2: CTestClass布局,显示传入CleanupStack函数的不同内存位置
如果你调用不执行检查的Pop()函数,或者你只在发布编译模式下运行代码,那么你肯定不会看到该严重错误。
正如Symbian开发者资料库指出的那样,C++标准并不会强制要求对象布局遵循基类声明的顺序,但是在实际中这是许多编译器,包括Symbian OS使用的编译器采用的方法。Symbian OS中与C类使用混合继承时,首先从CBase或CBase的派生类继承,然后从混合类继承,以便促进CBase*和TAny*之间的安全转换。
亲爱的诊所医生
通过阅读,我知道不应该使用CleanupStack::PushL()将一个指向C类对象的混合类指针压入清理栈。那么如果我希望让该指针安全退出,我应该如何使用清理栈呢?
谨慎使用清理栈 (Nicky, 圣克鲁斯)
亲爱的Nicky,
你没有提到你阅读的资料,但是这种做法是正确的,你不应该使用CleanupStack::PushL()将指向C类的混合类指针压入清理栈。设想一下如果你这样做会发生什么,CleanupStack::PushL()的哪一种重载会被调用?
static void PushL(TAny* aPtr); static void PushL(CBase* aPtr); // 不是这个 static void PushL(TCleanupItem anItem); // 也不是这个
记住,尽管你的类可能也是从CBase继承而来,但是你拥有一个M类指针指向该对象。你的指针不会使用CBase* 重载压入清理栈——因为它不是一个CBase* 指针——不同的是,该指针是以TAny*类型压入清理栈的。
如果清理栈要求销毁对象,这可以通过调用CleanupStack::PopAndDestroy()或异常退出事件实现,那么PushL()采用的重载形式就关系重大。当一个TAny*对象被压入清理栈,清理工作是通过对该指针简单调用User::Free()实现的。不需要调用析构函数。
但是,对于你的对象,上面的操作会导致User 42严重错误。这是因为M类指针并不位于该对象的起始处(参见图3),同时User::Free()无法找到它释放内存还给系统所需要的堆信息。即使可以,C类对象的析构函数也不会得到调用,这会造成潜在的内存泄漏。这可能也会造成严重错误以突出该问题,所以我们能够解决它。
让我们来看一个相当简单的例子。一个C类,CSquareClass,多重继承自CBase和一个M类接口,MPolygon。CSquareClass对象被实例化,M类接口由CleanupDeletePushL()(位于e32base.h) 压入清理栈。该模板函数将该指针压入清理栈,并且保证当清理该指针时调用delete。清理栈通过创建TCleanupItem项并让其对指针调用delete来实现该清理过程。你可以参考 [R2]或在线的Symbian开发者资料库获得与该模板函数相关,以及将R类压入清理栈的更多信息。
class MPolygon
{
public:
virtual TInt NumberOfVertices() =0;
virtual ~MPolygon(){};
};
class CSquareClass : public CBase, public MPolygon
{
public:
static MPolygon* NewL();
virtual TInt NumberOfVertices(); // 继承自MPolygon
virtual ~CSquareClass(){};
private:
CSquareClass (){};
};
/*static*/ MPolygon* CSquareClass::NewL()
{// 这里不需要二阶段构造。
CSquareClass* me = new(ELeave) CSquareClass();
return (me);
}
TInt CSquareClass::NumberOfVertices()
{// 返回顶点数目
return (4);
}
MPolygon* polygon = CSquareClass::NewL(); CleanupDeletePushL(polygon); ... // 调用可能发生异常退出的代码 CleanupStack::PopAndDestroy(polygon); // OK
图 3:MPolygon* 指向M类子对象,并不位于CSquareClass对象的起始处。该对象不能通过对该指针调用User::Free()来进行清理
如图3所示,如果使用CleanupStack::PushL()简单地将polygon压入清理栈,会导致USER 42严重错误。使用CleanupDeletePushL()保证清理成功,并且C类对象的析构函数得以调用。最后,注意混合类拥有空的虚析构函数,允许M类指针指向的对象可以被释放。
[R1] 深度探索C++对象模型, Stanley B Lippman,Addison-Wesley,1996年。
[R2] Symbian OS C++高效编程,Jo Stichbury,John Wiley & Sons,2004年。
我要感谢Vladimir Marko,他为这篇文章的许多方面提供颇有价值的反馈,感谢Symbian开发者网站的Hamish Willee和Tanzim Husain,还要感谢Neil和Nicky提交问题。
代码诊所将于每月的第一个星期五在Symbian开发者网站定期更新。你有Symbian C++方面的问题?请将它发送至位于sdn@symbian.com的代码医生。如果问题被采用,你会获得Symbian出版社出版的 Symbian OS软件开发(第2版)。T