搜尋此網誌

工商服務

2009年4月15日 星期三

[Qt密技][新手當心]在衍生類別時,別忘了將其基礎類別初始化!



不知道是腦殘還是怎麼來著的,最近在開發衍生自QWidget類別的GUI應用程式的時候,功能不僅好像沒動作,還在關閉視窗時遭遇了詭異的執行時期錯誤,而且程式本來都還執行得好好的,直到後來類別越寫越多時問題才浮現出來,囧。

這個問題耗費了我不少時間,信心也為之動搖,甚至同樣的程式碼邏輯還更換了好幾種不同的寫法,最後才發現原來是因為:在衍生QWidget類別時,沒有傳遞適當的參數給基礎類別QWidget的建構式去進行正確的初始化。

我是無意間才發現自己在衍生類別的建構式當中遺漏了這個步驟,信手翻閱《C++ GUI Programming with Qt 4,2/e》和《C++ Primer中文版,3/e》,果然在裡頭找到了我想知道的理論部分,前者在第16頁,後者在第48頁。

一般而言,我們在開發GUI應用程式時,比方說撰寫一個繼承自QWidget的簡單畫面,其類別定義會長得像這樣:

class Window : public QWidget
{
  Q_OBJECT
public:
  Window(QWidget *parent = 0);
  ~Window();
private:
  bool isActive;
  int count;
};

然後在實作其建構式的時候,會在成員初始化序列(member initialization list),也就是以冒號(:)引導的那個段落裡面,將parent參數傳遞給基礎類別(此處即QWidget)的建構式,透過此機制來進行基礎類別的初始化。像這樣:

Window::Window(QWidget *parent)
  : QWidget(parent)
{
}

附帶說明,成員初始化序列的作用顧名思義就是可以透過小刮號((...))來對類別的資料成員進行初始化,並且以逗號(,)分隔開來,例如:

Window::Window(QWidget *parent)
  : QWidget(parent), isActive(false), count(0)

我就是因為少做了這個「在衍生類別的建構式的成員初始化序列裡面,將相關資訊傳遞給其基礎類別以便順利進行初始化」的步驟,才會導致遭遇執行時期的錯誤。理由是:基礎類別的建構式並不會被衍生類別繼承下來,因此必須透過衍生類別的成員初始化序列來獲得參數,以便進行所需的初始化動作(如果有的話)。換言之,針對本例,基礎類別QWidget未被適當呼叫建構式來進行初始化,是導致程式遭遇執行時期錯誤的原因所在。

檢視Qt 4.5裡面的QWidget和QObject類別原始碼,都可以發現這個步驟是很重要的,因為它們的建構式確實需要一些參數來協助完成其初始化。摘錄如下:

QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)
  : QObject(*new QWidgetPrivate, 0), QPaintDevice()
{
  d_func()->init(parent, f);
}

QObject::QObject(QObject *parent)
  : d_ptr(new QObjectPrivate)
{
  Q_D(QObject);
  qt_addObject(d_ptr->q_ptr = this);
  d->threadData = (parent && !parent->thread()) ?
    parent->d_func()->threadData : QThreadData::current();
  d->threadData->ref();
  if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0,
    d->threadData))
    parent = 0;
  setParent(parent);
}

所以這裡可以歸納出一個非常重要的結論,就是:在某些類別裡面,即使衍生類別似乎並不需要建構式來做些特別的初始化工作,但是最好還是別忘了撰寫其建構式,因為可能至少還是要在成員初始化序列裡面,將必要參數(如果有的話)傳遞給基礎類別的建構式,否則在某些情況下,可能將會因此而導致程式發生錯誤。相反地,解構式就沒這樣的考量了,如果沒有特別的資料成員需要被釋放的話就可以不寫,反正衍生類別的物件在解構時會自動去呼叫基礎類別的解構式來歸還系統資源。

哇,C++與Qt應用程式框架裡面的類別庫真是博大精深呀。

3 則留言:

  1. 我在開發的時候也遇到一些問題, (Qt 4.5 VS2008), 再跳出Qt的時候(連到SLOT(quit()) 或者是按視窗右上角的x) 都會跑出runtime error, 如果是用debugger mode, 會出現中斷點之類的, 感覺是destructor出了問題, 但我不知道該怎麼下手, 請問有何建議.

    回覆刪除
  2. To Mingyu:我的猜測是,應用程式在關閉的時候(比方說按了視窗右上角的[X]),有些背景執行的工作尚未結束,比方說QHttp的下載動作,然而應用程式在結束時會將記憶體資源釋放掉,導致這些尚在執行中的工作產生非法的存取動作,解決方式就是不直接呼叫quit(),而是呼叫另外一個函式,這個函式會先把執行中的工作停掉,最後才呼叫quit()。我之前有遇過這種情況下發生的Runtime Error,提供給你參考。祝你好運,^^

    回覆刪除
  3. 您好!我在搜尋相關初始化序列的資料時,無意間發現您的網站。您上面寫說:基礎類別的建構式並不會被衍生類別繼承下來,因此必須透過衍生類別的成員初始化序列來獲得參數,以便進行所需的初始化動作(如果有的話)。
    這句話寫得有點含糊,我的解讀是:如果Call子類別,一開始進入建構子的時候,並不會執行父類別的建構子。
    實際上我們在實做繼承時,在進入子類別的建構子之前,會先呼叫父類別的建構子,才進入子類別的建構子。因此,如果希望一開始建立子類別時,先執行父類別帶有參數的建構子,可以使用初始化序列來達到建立基底類別的目的。

    回覆刪除