C++でクラスのサイズを調べた際に面白いことがあったので、クラスを調べて想像していたサイズと違った人に向けて助けになればと思う。
#実行環境
- visualstudio2017
- win10
#ポインタのサイズ
C++で構造体を書いたとき、サイズを調べたいとする。多くの数字や、文字の型は以下のようにサイズが決まっている。今回は代表的なものだけ挙げる。ただし、処理系によってサイズが違う場合もあるので、ぜひ自分の環境でも調べてみてほしい。
|型 |サイズ(byte) |
|---|---|---|
|int型 |4 |
|double型 |8 |
|char型|1|
ポインタはどうだろう?
私がネットで探した時は初心者向けの詳しい説明はあまり載っていなくて混乱した
ポインタのサイズは実はコンパイラの環境によって変わる。
VisualStudio2017(以下VS)での確認の方法は以下の通り。
1. VSを開く。
2. サイズを調べたいクラスなどの入ったプロジェクトを開く
3. 画面上のほうにある×(数字)となったところの数字を調べる。
この数字が、86の時は32bitコンパイラ、64の時は64ビットコンパイラを使っているのだ。
先ほど言ったポインタのサイズだが、32bitコンパイラを用いると4byte、64ビットコンパイラを用いると8byteとなる。
試しにコードをたたいて確認する。
確認の仕方はこんな感じ
#include <iostream>
using namespace std;
class A {
A*a_p;
};
int main(void) {
cout <<"size of A: "<<sizeof(A) << endl;
return 0;
}
つまり、ポインタはコンパイラによって値が異なるのだ。32bitコンパイラでポインタのサイズは4byte、64bitコンパイラでポインタのサイズは8byte。
余談だが、sizeof()はbyteで値を返す。1byte=8bitなので、ポインタ一つのサイズはそれぞれコンパイラの処理系の名前のbit数と同じになる。当たり前のことのはずなのだが、私は賢くないのでここで少し驚いた。
(例:32bitコンパイラ下でのポインタ1個分のサイズは4byte=4×8bit=32bit)
#仮想関数の隠れたポインタvtpr
さて、ちょっとクラスAに要素を変更してみる。
試しに以下のように、引数戻り値がない関数を設定しよう。
class A {
void a();
};
sizeを調べてみると、これは1byteになる。コンパイラ二種類共でこの値は変わらない。
voidは型がないという意味だし、サイズも存在しない。クラス自体のサイズを表示してくれちゃうのだ。
はて、この関数を仮想関数にするとどうなるか。
class A {
virtual void a();
};
先ほどの値と違って大きく値が出た。コンパイラ間でも違う。
ポインタのサイズの説明を思い出してほしい、
つまり、ポインタはコンパイラによって値が異なるのだ。32bitコンパイラでポインタのサイズは4byte、64bitコンパイラでポインタのサイズは8byte。
この値、先ほどの結果によく似ている。
驚きなことに、このクラスには勝手に不可視なポインタが追加されているのだ。
オブジェクトが生成される際、vtable に対するポインタ、仮想テーブルポインタ, vpointer, vptr がオブジェクトの不可視のメンバーとして追加される(通常は最初のメンバーとなる)。コンパイラはコンストラクタ内に"隠れた"コードを生成し、クラスのオブジェクトの vpointer が、対応する vtable のアドレスで初期化されるようにする。
「仮想テーブル」『フリー百科事典 ウィキペディア日本語版』。更新日時: 2017年1月11日 08:00 (UTC)
つまり、仮想関数を含むクラスのサイズはポインタが追加されている分少し大きい。
仮想関数を一つ増やすとどうなるか?
実は変わらない。一度vptrを持てば他の仮想関数にもそのポインタを流用できるからだ。
#構造体アライアメントとパディングの仕組み
最後に、先ほどのクラスAのメンバ変数として他の変数を組み合わせて以下のように構成してみる。
ここから先の実行環境は32bitコンパイラに限る。よって、ポインタのサイズは4byte、仮想関数の値は不可視なポインタvptrを含むので4byteとなる。
class A {
int x;
virtual void a();
double b;
char c;
};
intが4byte、仮想関数a()が4byte、doubleが8byte、charが1byte
合計、17byteになるはずだ。
あれ…なんだかめちゃくちゃ大きい。
17byteを引くと、ほかに15byte分の何かが詰め込まれている。
これは、メモリ(主記憶)への書き込みの際に生じるメモリ上の位置調節が原因である。
クラスAの中で最も大きなサイズの変数はdoubleの8byteである。
メモリからcpuへの読み出しの際に、より効率的に読みだすには個々のサイズに合わせて読みだすよりも、すべての大きさがそろっていたほうが都合がいいのだ。
たとえば以下のようにメモリが整列していたとする。1行あたり8byte格納できる箱が何行も並んでいる。
□□□□□□□□
□□□□□□□□
□□□□□□□□
□□□□□□□□
…
クラスAのサイズ分それぞれの大きさを埋めてみる。
■■■■□□□□
■■■■□□□□
■■■■■■■■
■□□□□□□□
…
黒い四角がデータが埋まっているメモリだと考えると、かなりいびつな形をしている。
このままだと読み出しの際に処理が遅くなってしまう。
この、空白の穴を勝手に埋めてしまおう。
というのがパディングの仕組みである。
大きなサイズの変数に合わせてパディングしているかを確認するためにクラスAのdouble変数を消してみる。もし大きなサイズの変数に合わせてパディングしているならクラスAのサイズは12byte、そうでなくて8byteに固定して長さを合わせているなら24byteになるはずである。
class A {
int x;
virtual void a();
//double b;
char c;
};
やはり、大きなサイズの変数に合わせてパディングしているのである。
#おまけ
ちなみに、先ほどの箱の横幅。大きなサイズの変数に合わせて作られているといったが、これを無理やり任意の幅にすることができる。
メモリを食いたくないときに使う。
以下のように#pragma pack(n:任意の数)という文を付け加えればいい。
#include <iostream>
#pragma pack(1)
using namespace std;
class A {
int x;
virtual void a();
//double b;
char c;
};
int main(void) {
cout <<"size of A: "<<sizeof(A) << endl;
return 0;
}
1行に1byteしか入らない。つまり、パディングする必要のある中途半端な断面、空白部分はなくなったのだ。よって、メンバ変数のサイズのみでクラスのサイズが決まることになる。
使う機会はあまりないかもしれないがレポートのネタに最適である。
(追記:2020/7/21)
ご指摘をいただいたので、最後の内容についてちょっと付け加えます。
#pragma pack(n)
のnは1,2,4,8が有効です。あと、packするとデータのサイズは小さくなるけれど動きは遅くなることや不具合が生じる可能性もあります。不具合は自己責任です。実際にお使いになる際はご自身でリファレンスを読んでいただけると幸いです。