C言語Day 22

sizeof演算子にまつわるアレコレ

本記事は C言語 Advent Calendar 2016 22日目(2週間ぶり3度目) にエントリしています。

当初、12/1で参加した時は4エントリしか参加表明がなかったのに、自転車操業のごとく2度目の参加につながり、気が付いたら25日分全てのエントリが埋まったようです。おめでとうございます。みなさんC言語好きなんですね(?)


プログラミング言語Cの sizeof演算子 に関するネタを、基本の"キ"から重箱の隅つつきまで揃えました。1

各話題にはマニアック度(=どうでも良い度合い)を独断と偏見で3段階評価してあります。★が少ないほど広くC言語プログラマに知っておいて欲しい事項(=重要)です。...普通のランキングと逆じゃねーか。

なお本記事の内容は、特に断りがない限りISO/IEC 9899言語仕様(C90/C99/C11)に準じます。また用語の日本語訳はJIS X 3010規格票に合わせます。


基礎編

マニアック度★1つ。C言語プログラマなら全員知ってる(ハズ)の情報です。


sizeof演算子とsize_t型 [★]

sizeof演算子を適用した結果の型は、処理系定義(implementaion-defined)の 符号無し整数型size_t です。int型やunsigned int型ではないため、結果を格納するときは正しくsize_t型を使うべきです。

size_t size = sizeof(int);

size_tは 符号無し(unsigned) の整数型ですから、減算やデクリメント演算に気をつけてください。size_t型は絶対に負値をとりません。値0との大小比較に要注意。

size_t n = /*...*/

while (0 <= n--) { // NG: 恒真のため無限ループに陥る可能性あり
//...
}


sizeof(char) == 1 [★]

sizeof(char)必ず1を返します。例外はありません。

assert(sizeof(char) == 1);

一般的な1バイト==8ビットのアーキテクチャに限らず、1バイトが何ビットで構成されていようとも、C言語仕様は sizeof(char) == 1を保証 します。また、unsigned char型とsigned char型に対しても必ず値1を返します。

malloc(length * sizeof(char))のようなコードは、局所的観点からは"* sizeof(char)"部分は冗長で無駄な処理です。ただし、周辺コードとの一貫性(例:他で* sizeof(int)計算など)もありますから、可否判断はケース・バイ・ケースかと思います。不毛なレビューコメント戦争は避けましょう。


sizeofと配列 [★]

sizeof演算子を 配列(array) に適用した場合、その結果は sizeof(要素型) * 要素数 に等しくなります。

int a[10];

assert(sizeof(a) == sizeof(int) * 10);

これを利用した、配列の要素数を計算させるテクニックが有名ですね。

int a[10];

const size_t nelem = sizeof(a) / sizeof(a[0]); // 要素数10

sizeof演算子を可変長配列(VLA; Variable Length Array)(C99以降)に適用した場合も、通常の配列と同様に sizeof(要素型) * 要素数 を返します。

unsigned int n;

scanf("%u", &n);
if (0 < n) {
int vla[n];
assert(sizeof(vla) == sizeof(int) * n);
}


注意:関数パラメータとして配列型を記述した場合、上記テクニックは期待通り動作しません。詳細はJPCERT ARR01-C解説を参照ください。

void func(int a[10]) {

size_t n = sizeof(a) / sizeof(a[0]); // 注意: 期待通り10とはならない
}


sizeofと構造体 [★]

sizeof演算子を 構造体(struct) に適用した場合、パディング領域も含む構造体型全体のサイズを返します。言い換えると、構造体のサイズは常に全メンバ型サイズの合計以上となります。(両者が等しくなる、つまりパディング領域が存在しないこともあります。)

struct S {

char c;
int i;
};
assert(sizeof(struct S) >= sizeof(char) + sizeof(int);

パディング領域はコンパイラによって、構造体メンバ同士の間や構造体末尾に配置されます。C言語仕様ではこれ以上の詳細を規定しませんが、多くの処理系ではプラグマ#pragma packによりパディング量を制御できます。

また、sizeof演算子に指定する構造体型は完全な定義が必要です。宣言のみの構造体型に対してはsizeof演算子を適用できません。

struct X;

sizeof(struct X); // NG: コンパイルエラー


size_tとC標準ライブラリヘッダ [★]

sizeof演算子の適用結果であるsize_t型は、C標準ライブラリヘッダ<stddef.h>にて宣言されます。

#include <stddef.h>  // size_t型を宣言


size_t n = sizeof(int);

ちなみにsize_t型はC標準ライブラリヘッダ<stdio.h>, <stdlib.h>, <string.h>, <time.h>, <wchar.h>(C99以降), <uchar.h>(C11以降)でも宣言されます。ソースコード中でいずれかのヘッダをincludeすればsize_t型を利用できます。


中級編

マニアック度★2つ。熟練C言語プログラマなら大抵知ってそうな情報です。


sizeofと共有体 [★★]

sizeof演算子を 共有体(union) に適用した場合、共有体メンバのうち最も大きい型のサイズ以上の値を返します。

union U {

char c;
short s;
long l;
};
assert(sizeof(union U) >= sizeof(long));
// 事実上は sizeof(union U) == sizeof(long) となるはず

構造体と同様に、共有体の末尾にもパディング領域が存在することがあります。これは共用体の配列を考えたとき、全ての共用体メンバでアライメント要件を満足させるための措置です。

union U2 {

char a[sizeof(double) + 1];
double d;
};
assert(sizeof(union U2) >= sizeof(double) + 1);

また、sizeof演算子に指定する共用体型は完全な定義が必要です。宣言のみの共用体型に対してはsizeof演算子を適用できません。

union Y;

sizeof(union Y); // NG: コンパイルエラー


sizeofと文字列リテラル [★★]

sizeof演算子を 文字列リテラル(string literal) に適用した場合、その適用結果は 文字列リテラル中の文字数 + 1 となります。文字列リテラルの型はchar配列であり、コンパイラによって末尾NUL文字('\0')が追加されるためです。

assert(sizeof("Hello") == 6);   // sizeof(char[6])相当

assert(sizeof("X\0Z\0") == 5); // sizeof(char[5])相当

ワイド文字列リテラル(C99以降)では、プレフィクスL/u/Uに応じて配列要素型が変化(wchar_t/char16_t/char32_t)します。UTF-8文字列リテラル(C11以降)では、UTF-8文字エンコーディングに従った要素値をもつchar配列型となります。

#include <stddef.h>  // wchar_t型の宣言

assert(sizeof(L"wide") == 5 * sizeof(wchar_t)); // sizeof(wchar_t[5])相当
assert(sizeof(u8"utf-8") == 6); // sizeof(char[6])相当

#include <uchar.h> // char16_t型, char32_t型の宣言
assert(sizeof(u"utf-16") == 7 * sizeof(char16_t)); // sizeof(char16_t[7])相当
assert(sizeof(U"utf-32") == 7 * sizeof(char32_t)); // sizeof(char32_t[7])相当


sizeofの適用対象 [★★]

sizeof演算子は、「関数型(function type)」や「不完全型(incomplete type)」の式または型名 には適用できません。さらに「ビットフィールド(bit field)メンバ」にも適用できません。

int func(int);  // 関数の宣言

sizeof(func); // NG: 関数型int(int)には適用できない
sizeof(&func); // OK: 関数ポインタ型int(*)(int)には適用可

int a[]; // 要素数を指定しない配列型
struct X; // 宣言のみの構造体
sizeof(void); // NG: voidは不完全型
sizeof(a); // NG: int[]は不完全型
sizeof(struct X); // NG: struct Xは不完全型

struct S { int bf : 4; } s;
sizeof(s.bf); // NG: ビットフィールドには適用できない

関数型と関数ポインタ型で、振る舞いが異なることに注意してください。普段はあまり意識しない点ですが、両者は明確に異なる型なのです。


処理系依存:GCCでは独自の拡張仕様によって「関数型」と「void型」を許容し、それぞれ値1を返します。この独自拡張は-Wpointer-arith-pedanticオプションを使うことで、警告またはエラーとして検知できます。



sizeof('C') [★★]

C言語では 文字定数(character constant) の型はintです。よって常に sizeof('C') == sizeof(int) となります。


C++:C++言語では 文字リテラル(character literal) の型はcharとなるため、sizeof('C') == sizeof(char)つまりsizeof('C') == 1となります。この違いを利用して「C/C++判定トリック」として紹介されることもあります。実用性はありませんが、話のネタ程度に。



sizeofと列挙体 [★★]

sizeof演算子を 列挙体型(enum) に適用した場合、処理系定義(implementation-defined)のサイズを返します。一方、列挙子(enumerator) はint型の定数と定められているため、そのサイズはsizeof(int)と等しくなります。

enum E {

A, B, C
};

assert(sizeof(enum E) <= sizeof(int)); // int型以下のサイズを持つ整数型が選択される
assert(sizeof(A) == sizeof(int)); // int型に等しい


処理系依存:GCC既定動作では列挙体サイズはint型に等しくなります。-fshort-enumオプションにより列挙体のサイズを、全ての列挙定数を表現可能な最も小さい整数型へと変更できます。前掲コードでは、char型で全列挙定数を表現できるためsizeof(enum E) == sizeof(char)となります。



sizeofと記憶域クラス指定子 [★★]

sizeof演算子は 型名 に適用できますが、C言語の構文上は型名(type-name)には 記憶域クラス指定子(storage-class-specifier) を含みません。つまりキーワードstatic, register, _Thread_local(C11以降)などはsizeof演算子のオペランドに記述できません。

sizeof(static void*);  // NG: 構文エラー

sizeof(register int); // NG: 構文エラー

sizeof演算子を 式(expression) に適用する場合、変数宣言時の記憶域クラスは式の型には影響しないため、下記コードは期待通りに動作します。

static void* p;

register int r;

sizeof(p); // OK: sizeof(void*)と等価
sizeof(r); // OK: sizeof(int)と等価


sizeofと整数定数 [★★]

C90のsizeof演算子は、コンパイル時に値が定まる 整数定数(integer constant) を返します。古き良き時代のお話です。

C99以降のsizeof演算子は、オペランドが可変長配列(VLA)である場合を除いて 整数定数(integer constant) を返します。sizeof演算子のオペランドに可変長配列(VLA)を指定した場合、そのオペランドはプログラム実行時に評価されます。

int main()

{
char sa[10]; // (普通の)配列を宣言
sizeof(sa); // コンパイル時に整数定数 10 となる

int n;
scanf("%d", &n);
char vla[n]; // 可変長配列(VLA)を宣言
sizeof(vla); // 実行時に評価され、結果は値nに等しい
}

C99以降に準拠したコードで、VLAを多用する場合は少し気にかけてください。VLAで遊ぶのは止めましょう。


C++:C++言語仕様には可変長配列が存在しません。このためsizeof演算子の適用結果は必ず 定数(constant) となり、C++ソースコードのコンパイル時に処理されます。



上級編

マニアック度★3つ。"ISO/IEC 9899"や"JIS X 3010"という単語でピンとくる人向けです。


sizeofと単項演算子の関係 [★★★]

sizeofキーワードによる sizeof演算子(sizeof operator) は、C言語の構文上は 単項式(unary-expression) を構成します。単項式となる演算子は、このほかに 前置インクリメント/デクリメント演算子++/-- や、各種の単項演算子(unary operators)&/*/+/-/~/! も単項式を構成します。

int a = 0;

sizeof(a) // 式aのサイズを返す 単項式
++a // 変数aの値に1を加算しその値を返す 単項式
&a // 変数aのアドレスを返す 単項式

上記定義に従うと、厳密には sizeof演算子 $∉$ 単項演算子 と言えます。だから何なんだという話ですが。


sizeofと括弧() [★★★]

sizeof演算子には、そのオペランドに 式(expression) をとるものと、型名(type-name) をとるものの2種類が存在します。


  • オペランドに 式 をとる場合、括弧は任意です。

  • オペランドに 型名 をとる場合、括弧は必須です。

// オペランドに式をとる場合

int a = 0;
sizeof(a) // OK: 式 (a) がオペランド
sizeof(42) // OK: 式 (42) がオペランド
sizeof a // OK: 式 a がオペランド
sizeof 42 // OK: 式 42 がオペランド

// オペランドに型名をとる場合
sizeof(int) // OK: 型名 int がオペランド
sizeof(char*) // OK: 型名 char* がオペランド
sizeof int // NG: コンパイルエラー
sizeof char* // NG: コンパイルエラー

sizeof演算子の構文規則を引用します;


unary-expression:

  sizeof unary-expression

  sizeof ( type-name )


ただし慣例的には、sizeof演算子のオペランドでは常に括弧を使うべきでしょう。いちいち覚えていられませんし。


sizeof(void*)と関数ポインタ型のサイズ [★★★]

C言語仕様の範囲では、関数ポインタ型のサイズとsizeof(void*)に相関はありません。つまりvoid*型に関数ポインタ型の値を格納できる保証がありません。C言語仕様としては「データ型ポインタ」と「関数ポインタ」の互換性を要求しないため、両者の相互運用が不可能なハーバード・アーキテクチャもカバーしています。


処理系依存:一般的なPC環境(ノイマン・アーキテクチャ)では、データ型ポインタと関数ポインタ間の相互変換が可能です。例えばPOSIX標準ではvoid*→関数ポインタへの型変換を許容します。



sizeofとオペランドの評価 [★★★]

前節の通り、C99以降のVLAを除いて、sizeof演算子の適用結果はコンパイル時定数(整数定数)となります。sizeof演算子のオペランドに指定した 式(expression) は、プログラム実行時に評価されません。C言語仕様での「評価されない(is not evaluated)」という言い回しは、厳密な意味を持っています。

int x = 0;

sizeof(++x); // 式++xは評価されない
assert(x == 0); // 変数xの値は0のまま

副作用を伴う式が「評価されない」とき、該当処理を実行しないという意味になります。IOCCC出場時でなければ、このようなコード記述は避けるべきです。JPCERT EXP44-Cも参照してください。

なお、sizeof演算子オペランドの式は評価されませんが、適用結果は必要に応じて 整数拡張(integer promotion) 適用後の型になります。

short s = 0;

assert(sizeof(s) == sizeof(short));
assert(sizeof(+s) == sizeof(int)); // 整数拡張によりshort→int

一方、sizeof演算子オペランドが評価されないことを利用したテクニックも存在します。

struct S { /*...*/ };

int main() {
struct S *p; // 未初期化の変数p
//...
p = malloc(sizeof(*p)); // OK: 式*pは評価されず、型情報のみを扱う
}

未初期化のポインタ変数pに対して、式*pが評価されてしまうと未定義の動作(undefined behavior)を引き起こします。しかしsizeof演算子のオペランドは評価されないため、型情報のみがsizeof(struct S)と解釈されプログラムは正常に実行されます。型struct Sを変数pから導出できるメリットはありますが、普及したイディオムでもない(たぶん)ため、実践はちょっと微妙かもしれません。


エクストリーム・sizeof

冒頭で「3段階評価」とは言ったが、★3つがMAXだとは言っていない。ここからがほんとうの誰得情報だ...


sizeofとVLAサイズ式の評価 [★★★★]

sizeof演算子をVLA型を含む式に適用するとき、sizeof演算子の結果に影響しないVLAサイズ式が評価されるか否かは未規定(unspecified)となります。なおsizeof演算子の適用結果に直接影響する場合は、VLAサイズ式は必ず評価されます。

int i = 5;

size_t n = sizeof(char(*)[++i]); // VLA(char[++i])へのポインタ型
assert(i == 5 || i == 6); // ++iが評価されるか否かは未規定
assert(n == sizeof(void*));

int j = 5;
size_t m = sizeof(char[++j]); // VLA(char[++j])型
assert(j == 6); // ++jは必ず評価される
assert(m == 6);

ふーん、そう。(普通は)関係ないね。

C11 §6.7.6.2, paragraph 5(C99 §6.7.5.2)より引用:


[...] Where a size expression is part of the operand of a sizeof operator and changing the value of the size expression would not affect the result of the operator, it is unspecified whether or not the size expression is evaluated.



sizeofとregister配列 [★★★★★]

C言語仕様では、register記憶域クラスをもつオブジェクトのアドレスを取れません。このためregister記憶域を持つ配列は、要素アクセスが不可能となるため全く使い道がありません。ただし、われらがsizeof演算子のオペランドへは指定可能です。

register int a[5];

a[0]; // NG: *(a+0)と等価; 配列型→ポインタ型へ変換不可
sizeof(a); // OK: sizeof(int[5])と等価

そう、よかったね。だから何なんだつーの。

C11 §6.7.1 Note121(C99 同§)より引用。


[...] Thus, the only operators that can be applied to an array declared with storage-class specifier register are sizeof and _Alignof.






  1. 本記事を書くに当たり網羅性はそれほど重視していませんでした。Internet上で見つけたネタや、仕様書をうろうろして再発見した内容をかき集めたというのが実情です。