最近、Qt (しかも 4.8) を使ってるので、Qt のメモリ管理について調べてみました。
下準備
まず、実験用に MyObject クラスを作っておきます。
# include <QObject>
# include <QDebug>
class MyObject : public QObject
{
Q_OBJECT
public:
MyObject(QString name, QObject* parent = 0) :
QObject(parent), m_name(name) { qDebug() << "ctor:" << *this; }
~MyObject() { qDebug() << "dtor:" << *this; }
QString name() const { return m_name; }
void setName(QString name) { m_name = name; }
friend QDebug operator << (QDebug dbg, const MyObject& obj) {
return dbg.nospace() << "MyObject(" << obj.name() << ")";
}
private:
QString m_name;
};
ちなみに、private メンバにアクセスしないのに operator << を friend にしてるのは、最初の引数が QDebug だからメンバ演算子にはできなくて、コンストラクタなどのインライン実装で使うにはクラスの前または中に宣言する必要があって、宣言だけ前に書いて実装を後ろに書くのは嫌なので、クラス中に書きたいけどそのためには friend にする必要があるため、です。
QObject の親子関係
QObject のコンストラクタに親オブジェクトのポインタを渡したり、setParent() で指定すると、親オブジェクトの children リストに登録されて、親オブジェクトが破棄されるときに delete されます。
# include "MyObject.h"
int main()
{
MyObject parent("parent");
new MyObject("child 1", &parent);
MyObject *child2 = new MyObject("child 2");
child2->setParent(&parent);
qDebug() << "parent.children() =" << parent.children();
}
ctor: MyObject("parent")
ctor: MyObject("child 1")
ctor: MyObject("child 2")
parent.children() = (MyObject(0x8831720) , MyObject(0x8830998) )
dtor: MyObject("parent")
dtor: MyObject("child 1")
dtor: MyObject("child 2")
QObjectList (QList<QObject*>) を表示するときには MyObject の operator << を使ってくれなくて残念。
で、親オブジェクトを指定すると子オブジェクトたちも delete されるので、子オブジェクトをスタック上に置いてはいけないと思いがちですが、子オブジェクトが破棄されれば親オブジェクトの children から取り除かれるので、子オブジェクトが先に破棄される場合は大丈夫です。
# include "MyObject.h"
int main()
{
MyObject parent("parent");
{
MyObject child("child", &parent);
qDebug() << "parent.children() =" << parent.children();
}
qDebug() << "parent.children() =" << parent.children();
}
ctor: MyObject("parent")
ctor: MyObject("child")
parent.children() = (MyObject(0xbfeca854) )
dtor: MyObject("child")
parent.children() = ()
dtor: MyObject("parent")
もちろん、子オブジェクトよりも親オブジェクトが先に破棄される場合はクラッシュします。
# include "MyObject.h"
int main()
{
MyObject child("child ");
MyObject parent("parent");
child.setParent(&parent);
qDebug() << "parent.children() =" << parent.children();
// ここで child.setParent(0); すれば大丈夫。
}
ctor: MyObject("child ")
ctor: MyObject("parent")
parent.children() = (MyObject(0xbf910178) )
dtor: MyObject("parent")
dtor: MyObject("child ")
*** glibc detected *** ./a.out: double free or corruption (out): 0xbf910178 ***
(以下略)
また、親オブジェクトを複数持つことはないので、setParent() すれば元の親オブジェクトとは縁を切り、新しい親オブジェクトの子オブジェクトになります。
# include "MyObject.h"
int main()
{
MyObject *parent1 = new MyObject("parent 1");
MyObject *child = new MyObject("child", parent1);
qDebug() << "parent1->children() =" << parent1->children();
MyObject *parent2 = new MyObject("parent 2");
child->setParent(parent2);
qDebug() << "parent1->children() =" << parent1->children();
qDebug() << "parent2->children() =" << parent2->children();
delete parent1;
qDebug() << "child =" << child;
delete parent2;
}
ctor: MyObject("parent 1")
ctor: MyObject("child")
parent1->children() = (MyObject(0x8822730) )
ctor: MyObject("parent 2")
parent1->children() = ()
parent2->children() = (MyObject(0x8822730) )
dtor: MyObject("parent 1")
child = MyObject(0x8822730)
dtor: MyObject("parent 2")
dtor: MyObject("child")
QWidget の親子関係
QWidget や QLayout などの GUI 部品は QObject のサブクラスなので、当然 QObject の親子関係が適用されますが、いくつか注意すべきことがあります。
- setParent() は使わずに、addWidget() や addLayout() などを使う。
- GUI 部品の親オブジェクトは GUI の親子 (包含) 関係と連動する。
- GUI 部品の子オブジェクトは GUI 部品だけでなく、QButtonGroup や QTimer などの非 GUI オブジェクトを持つこともできる。
つまり、各 GUI 部品を new して作り、addWidget() や addLayout() などで親子関係を構築すれば、上位の GUI 部品を破棄した時に子オブジェクトはすべて自動的に delete されるということになります。
それでは、GUI 部品を new せずに作るにはどうすればよいでしょうか? まず、GUI 部品がひとつだけの場合。
# include <QApplication>
# include <QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel label("Hello, World!");
label.show();
return app.exec();
}
親オブジェクトがない GUI 部品を show すると、それ自身がウィンドウになります。そして、勝手に delete されることはないので、スタックに置くことができます。
次に、複数ある場合。
# include <QApplication>
# include <QHBoxLayout>
# include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget widget;
QHBoxLayout layout(&widget);
QPushButton button1("Hello");
QPushButton button2("World");
layout.addWidget(&button2);
layout.addWidget(&button1);
widget.show();
return app.exec();
}
スタック上に生成したオブジェクトは宣言の逆順に破棄されるので、親から子の順に宣言すれば、すべてスタックに置くことができます。
最後に、メンバ変数にする場合。
# include <QMainWindow>
# include <QHBoxLayout>
# include <QPushButton>
class MyMainWindow : public QMainWindow
{
Q_OBJECT
public:
MyMainWindow(QWidget* parent = 0) :
QMainWindow(parent), layout(&widget), button1("Hello"), button2("World")
{
layout.addWidget(&button1);
layout.addWidget(&button2);
setCentralWidget(&widget);
}
private:
QWidget widget;
QHBoxLayout layout;
QPushButton button1;
QPushButton button2;
};
# include <QApplication>
# include "MyMainWindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MyMainWindow mainWindow;
mainWindow.show();
return app.exec();
}
同じ private: セクションに宣言した変数は宣言の逆順に破棄されるので、親から子の順に宣言すれば、new を使わずに書くこともできます。
まぁ、普通は new (というか、ui ファイル) を使うよね。
おまけ
面倒くさくなったので QPointer とか例外安全とかは省略して、この記事を読んで気になった QList などのコンテナクラスのコピーオンライトについて検証してみます。
あらかじめ、MyObject にコピーコンストラクタを無理やり追加しておきます。
(本来、QObject はコピーコンストラクタを禁止しているので、QObject のサブクラスでやるのは良くないけど…)
class MyObject : public QObject
{
// ...
public:
MyObject(const MyObject& other, QObject* parent = 0) :
QObject(parent), m_name(other.m_name + " copy")
{
qDebug() << "copy ctor:" << *this;
}
// ...
};
まず、コピー先のリストに要素を追加した場合。
# include "MyObject.h"
# include <QList>
int main()
{
QList<MyObject> list1;
list1 << MyObject("abc") << MyObject("def");
qDebug() << "list1 =" << list1;
QList<MyObject> list2 = list1;
qDebug() << "list2 =" << list2;
list2 << MyObject("xyz");
qDebug() << "list1 =" << list1;
qDebug() << "list2 =" << list2;
}
ctor: MyObject("def")
ctor: MyObject("abc")
copy ctor: MyObject("abc copy")
copy ctor: MyObject("def copy")
dtor: MyObject("abc")
dtor: MyObject("def")
list1 = (MyObject("abc copy"), MyObject("def copy"))
list2 = (MyObject("abc copy"), MyObject("def copy"))
ctor: MyObject("xyz")
copy ctor: MyObject("abc copy copy")
copy ctor: MyObject("def copy copy")
copy ctor: MyObject("xyz copy")
dtor: MyObject("xyz")
list1 = (MyObject("abc copy"), MyObject("def copy"))
list2 = (MyObject("abc copy copy"), MyObject("def copy copy"), MyObject("xyz copy"))
dtor: MyObject("xyz copy")
dtor: MyObject("def copy copy")
dtor: MyObject("abc copy copy")
dtor: MyObject("def copy")
dtor: MyObject("abc copy")
リストに要素を追加するときにはコピーが行われますが、list1 を list2 にコピーするときには要素はコピーされずに、list2 に要素を追加した時に、他の要素もコピーされています。
次に、要素を書き換えた場合。
# include "MyObject.h"
# include <QList>
int main()
{
QList<MyObject> list1;
list1 << MyObject("abc") << MyObject("def");
qDebug() << "list1 =" << list1;
QList<MyObject> list2 = list1;
qDebug() << "list2 =" << list2;
list1[0].setName("ghi");
qDebug() << "list1 =" << list1;
qDebug() << "list2 =" << list2;
}
ctor: MyObject("def")
ctor: MyObject("abc")
copy ctor: MyObject("abc copy")
copy ctor: MyObject("def copy")
dtor: MyObject("abc")
dtor: MyObject("def")
list1 = (MyObject("abc copy"), MyObject("def copy"))
list2 = (MyObject("abc copy"), MyObject("def copy"))
copy ctor: MyObject("abc copy copy")
copy ctor: MyObject("def copy copy")
list1 = (MyObject("ghi"), MyObject("def copy copy"))
list2 = (MyObject("abc copy"), MyObject("def copy"))
dtor: MyObject("def copy")
dtor: MyObject("abc copy")
dtor: MyObject("def copy copy")
dtor: MyObject("ghi")
今度は list1[0] に代入するところで list1 の各要素がコピーされました。
代入しなくてもコピーされるのか、確認してみましょう。
# include "MyObject.h"
# include <QList>
int main()
{
QList<MyObject> list1;
list1 << MyObject("abc") << MyObject("def");
qDebug() << "list1 =" << list1;
QList<MyObject> list2 = list1;
qDebug() << "list2 =" << list2;
const MyObject& obj = list1[0];
qDebug() << "obj =" << obj;
qDebug() << "list1 =" << list1;
}
ctor: MyObject("def")
ctor: MyObject("abc")
copy ctor: MyObject("abc copy")
copy ctor: MyObject("def copy")
dtor: MyObject("abc")
dtor: MyObject("def")
list1 = (MyObject("abc copy"), MyObject("def copy"))
list2 = (MyObject("abc copy"), MyObject("def copy"))
copy ctor: MyObject("abc copy copy")
copy ctor: MyObject("def copy copy")
obj = MyObject("abc copy copy")
list1 = (MyObject("abc copy copy"), MyObject("def copy copy"))
dtor: MyObject("def copy")
dtor: MyObject("abc copy")
dtor: MyObject("def copy copy")
dtor: MyObject("abc copy copy")
list1[0] を変更せずに表示するだけでもコピーされてしまいました。
(Qt 4.8 は古い C++ を見捨ててないから、かなぁ。)
最後に、foreach を試してみます。
# include "MyObject.h"
# include <QList>
int main()
{
QList<MyObject> list1;
list1 << MyObject("abc") << MyObject("def");
qDebug() << "list1 =" << list1;
QList<MyObject> list2 = list1;
qDebug() << "list2 =" << list2;
foreach (const MyObject& obj, list1) {
qDebug() << "obj =" << obj;
}
qDebug() << "list1 =" << list1;
}
ctor: MyObject("def")
ctor: MyObject("abc")
copy ctor: MyObject("abc copy")
copy ctor: MyObject("def copy")
dtor: MyObject("abc")
dtor: MyObject("def")
list1 = (MyObject("abc copy"), MyObject("def copy"))
list2 = (MyObject("abc copy"), MyObject("def copy"))
obj = MyObject("abc copy")
obj = MyObject("def copy")
list1 = (MyObject("abc copy"), MyObject("def copy"))
dtor: MyObject("def copy")
dtor: MyObject("abc copy")
今度はコピーされませんでした。めでたしめでたし (?)。