SymbianOS異常三步曲之一:異常退出(leave)

Symbian的異常處理有別於標準C++的異常處理機制,主要原因是最初在設計Symbian的異常處理機制時,C++還沒有引入異常處理,但是從Symbian OS 9.1開始,Symbian開始支持標準C++的try—catch異常處理機制,不過考慮到系統開銷以及兼容性的因素,我們提倡使用Symbian特有的異常處理機制即異常退出。
一、異常退出函數
當調用異常退出函數或顯式調用系統函數時可能會發生異常退出。如果一旦異常退出發生,就會拋出一個異常,並同時產生一個錯誤碼,這個錯誤碼會沿着調用棧傳遞,直到被最近的一個異常捕獲模塊所捕獲。
注意:一旦發生異常退出,異常退出點後的代碼不再執行,而是轉入最近的捕獲模塊,捕獲完畢接着執行捕獲模塊後的代碼。這樣做,並不會中止程序流,這和偵測程序錯誤的斷言assert是不同的(Symbian中常用的斷言宏有_ASSERT_ALWAYS_ASSERT_DEBUG),斷言會中止程序繼續執行。
異常退出函數是執行了不能保證一定成功的操作(例如在低內存容量下分配內存)的函數。
關於異常退出函數的返回值:除非需要將函數中所分配資源的指針或引用作爲返回值,否則異常退出函數應該是沒有返回值的。關鍵是我們將一個函數做成一個異常退出函數好還是做成一個返回錯誤代碼的普通函數合適。後面我們會做詳細分析。
舉幾個異常退出函數聲明的例子:
void InitializeL( );
static CTestClass* NewL( );
RClangerHandle& CloneHandleL( );
      
從上面3個例子可以看到:異常退出函數的名字都是以“L”結尾的,這是必須的,如果不用L表明異常退出函數的話,別人在調用你的函數時,可能由於沒有捕獲異常,導致潛在的內存泄漏發生。
       函數在下列3種情況下可能發生異常退出:
(1)       調用了可能異常退出的代碼,並且在調用代碼的周圍沒有使用異常捕獲模塊。
(2)       調用了一個會產生異常退出的系統函數,如User::Leave( )或User::LeaveIfError( )
(3)       使用了以ELeave爲參數的new操作符重載形式。
 
下面看一下這幾個系統異常退出函數的理解:
(1)User::LeaveIfError( ):會測試傳入其中的一個整數參數值,如果該值小於零,譬如在e32std.h中定義的某個KErrXXX錯誤常量,則產生一個異常退出。這個函數可用來將返回標準Symbian OS錯誤碼的無異常退出函數轉化成一個以對應值作爲異常退出碼的異常退出函數。
(2)User::Leave( )不作任何參數值的檢查,而只是簡單的異常退出,並以傳入的整數值爲異常退出碼。
(3)User::LeaveNoMemory( ):只是簡單的異常退出,但異常退出碼被硬編碼成KerrNoMemory,它的效果等同於調用User::Leave(KErrNoMemory)。
(4)User::LeaveIfNull( ):接受一個指針作爲參數,如果該指針爲NULL,則以KerrNoMemory爲異常退出碼發生異常退出。
 
後綴“L”在編譯時不被檢查,所以由於我們忘記添加“L”或給原來的無異常退出函數添加了異常退出代碼等原因,爲此可以使用Symbian OS爲我們提供的工具LeaveScan
 
二、使用new(ELeave)進行基於堆的內存分配
使用new(ELeave)來爲對象在堆上分配內存,當內存不足時將會發生異常退出。因此我們可以直接使用其返回的指針,而無需做進一步的測試來確定內存是否分配成功。實質是,new(ELeave)中已經對指針是否爲NULL進行了if判斷。
       舉例:
       CClanger* InitializeClangerL( )
{
              CClanger* clanger = new(ELeave)CClanger();
              if(clanger == NULL)//這條語句多餘,可以省略
{
                     CleanupStack::PushL(clanger);
                     clanger->InitializeL();
                     CleanupStack::Pop(clanger);
}
return clanger;
}
上面的例子中對clanger進行了是否分配內存成功的測試,實際是多餘的,完全可以省略。
 
三、構造函數和析構函數
這兩個函數是絕對不允許發生異常退出的。因爲如果構造時發生異常,那麼可能會因爲缺乏足夠的資源而無法創建或初始化對象,但對象空間已分配,導致內存泄漏;如果析構時發生異常,將導致對象的不完全析構,這就可能造成資源的泄漏。
       解決上面問題的辦法其實很簡單:將構造函數中可能異常的代碼提取出來,放在一個單獨的異常退出函數ConstructL()中,利用二階段完成對象的構造。也可將析構函數中可能異常的代碼提取出來,放在CommitL()FreeResource()中,在析構之前調用它,給調用者一個機會來處理可能發生的問題。
 
四、使用異常退出函數
例1:
void FunctionMayLeaveL()
{
       CTestClass* ironChicken = CTestClass::NewL();
       ironChicken->FunctionDoesNotLeave();
       delete ironChicken;
}
例2:
void UnsafeFunctionL()
{
       CTestClass* test = CTestClass::NewL();
       test->FunctionMayLeaveL();
       delete test;
}
 
比較上面兩個例子:
(1) 兩個函數都是異常退出函數,都可能異常退出,並且ironChickentest都是局部變量。
(2) 我們在調用兩個函數中的類CTestClass的NewL方法產生對象指針以後,無需對指針進行是否NULL的判斷。因爲構建成功的話,指針絕對不會爲NULL;而構建失敗的話,NewL方法將異常退出。
(3) 由於採用二階段構造,即使NewL方法構建對象失敗,仍然能夠保證ironChicken和test的堆內存安全釋放。
(4) 但是例1中,ironChicken調用不會異常的函數,因此delete ironChicken可以使ironChicken的堆內存安全釋放;而例2中,test調用可能異常的函數,一旦異常發生,那麼delete test語句將會執行不到,從而導致test所指的堆內存不能得到釋放,從而導致內存泄漏,這是需要特別注意的。
 
針對於例2,再看下面這個例3:
void CTestClass::SaftFunctionL()
{
       iMember = CclangerClass::NewL();//iMember是類成員
       FunctionMayLeaveL();
}
在這個例子中,我們首先構造了iMember,然後同樣是調用一個異常退出函數FunctionMayLeaveL(),但是這裏即使發生異常,iMember所指的堆內存仍然可以安全釋放,因爲在這裏iMember是類CTestClass的成員,它的堆內存釋放是由析構函數來完成的,而不是在FunctionMayLeaveL()語句後執行delete釋放的,所以即使這裏發生異常,只要能夠調用析構函數,也不會發生堆內存泄漏。
 
五、用TRAP和TRAPD捕獲異常退出
1、使用格式
Symbian OS提供了TRAP和TRAPD這兩個宏來捕獲異常。它們使用區別,僅僅在於使用TRAP之前,需要事先聲明保存異常錯誤碼的變量,而TRAPD不需要,可以直接使用。並且在捕獲異常之後往往會有一個if的異常結果判斷語句。
       TRAPD(result,MayLeaveL());
       If ( KerrNone != result )
{
              //錯誤處理
}
 
TInt result;
TRAP( result,MayLeaveL( ) );
If(KerrNone != result)
{
       //錯誤處理
}
 
       2、宏TRAPD的嵌套和同名變量的使用
      
       (1)同名
TRAPD(result , MayLeaveL( ));
       If ( KErrNOne == result )
       {
              TRAPD ( result , MayAlsoLeaveL( ));
}
User::LeaveIfError(result);
 
這個例子的本意是隻要這裏的兩個異常函數MayLeaveL()和MayAlsoLeave()有一個發生異常,都會導致User::LeaveIfError( )發生。但事實上,由於錯誤碼重名,導致User::LeaveIfError()僅能看到第一個result,而第二個result實際上被第一個result屏蔽了,因此爲了看到第二個異常的結果,我們應該使用另一個異常錯誤代碼名字,可以在if語句外面定義一個新的錯誤碼變量,然後使用TRAP捕獲第二個異常。
實際上,User::LeaveIfError()永遠不能捕獲到第二個異常的錯誤碼,因爲一旦MayLeaveL()發生,就不會進入if語句,因此,第二個異常函數也就根本運行不到了。
 
(2)嵌套
爲了系統開銷上面的考慮,我們儘量不要使用TRAPD嵌套,儘量考慮使用其他的辦法來替代。
譬如我們可以將如下代碼:
TInt MyNonLeavingFunction( )
{
       TRAPD( result , FunctionMayLeaveL( ) );
       if(KErrNone == result )
              TRAPD(result , AnotherFunctionWhichMayLeaveL( ) );
       if(KErrNone == result )
       TRAPD(result , PotenialLeaveL( ) );
return result;
}
爲了避免嵌套,將可能產生異常的函數集中到一個函數中,將上面代碼改爲:
MyNonLeavingFunction()
{
       TRAPD(result , MyLeavingFunctionL( ) );
       return result;
}
void MyLeavingFunctionL()
{
       FunctionMayLeaveL();
       AnotherFunctionWhichMayLeaveL();
       PotentialLeaveL();
}
 
3、不應該寫這樣的函數:將錯誤碼作爲返回值返回,同時又有可能發生異常退出。
TInt OpenFileObjectL(CFileObject* aFileObject)
{
       Rfile file;
       TInt error = file.open(…..);
       If(KErrNone == error)
{
              CleanupClosePushL(file);
              aFileObject = CFileObject::NewL(file);
              CleanupStack::Pop(&file);
}
return error;
}
上面這個函數就寫的不太好,這個函數的返回值是錯誤碼,但是在使用這個函數時還可能產生異常退出,從而傳出異常退出碼。
我們一般這樣使用上面這個函數:
void ClientFunctionL()
{
       CFileObject* fileObject = NULL;
       TInt errorCode = OpenFileObjectL();
       If ( KErrNone != errorCode)
{
              User::Leave(errorCode);
}
}
上面的使用僅檢測了OpenFileObjectL方法的錯誤碼,而沒有捕獲異常,不妥。
或者這樣:
TInt ClientFunction()
{
       CFileObject* fileObject = NULL;
       TInt errorCode;
       TRAPD(r,errorCode = OpenFileObjectL() );
If(KErrNone!=r)
              return r;
       if(KErrNone!=errorCode)
              return errorCode;
}
這裏既分析了錯誤碼,又捕獲了異常,但總感覺放在一起判斷時,有點不倫不類,也不妥。
那麼應該怎麼辦呢?
有兩種辦法:
(1)       用異常錯誤碼取代錯誤碼,也就是統一爲異常退出。
即將原來的TInt OpenFileObjectL(CFileObject* aFileObject)方法做如下修改:
CFileObject* LeavingExampleL() //CFileObject對象指針通過函數返回值獲得,
//而不是之前那樣通過引用形參獲得。
{
  RFile file;
  User::LeaveIfError(file.Open(…..));//錯誤碼轉化爲異常拋出
  return CFileObject::NewL(file);
}
然後可以這樣使用:
void ClientFunctionL()
{
  CFileObject* fileObject = NULL;
  TRAPD(r,fileObject = LeavingExample());
  switch(r)
{
case(KErrNoMemory)
…….//釋放一些內存
break;
……….
default:
User::Leave(err);
    break;
}
}
這樣進行處理時就比較統一了,僅對異常進行switch進行討論即可,風格比較統一了。
(2)       用錯誤碼取代異常錯誤碼,也就是統一爲錯誤碼。
TInt Example(CFileObject*& aFileObject)//函數返回值爲錯誤碼
{
  RFile file;
  TInt error = file.open(…..);
  If(error == KErrNone)
{
  TRAP(error,aFileObject = CFileObject::NewL(file));//將異常轉換成錯誤碼
}
return error;
}
這樣進行使用:
CFileObject* fileObject = NULL;
User::LeaveIfError(Example(fileObject));
這樣進行處理時,風格就比較統一了,僅需處理錯誤代碼即可。
      
       相比較而言,將錯誤代碼轉化爲異常退出進行統一處理的系統開銷較小,也更簡單,推薦使用這個辦法。 
發佈了8 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章