C言語の基本|sizeofで配列要素数を求める

配列の長さは手で数えない。sizeofで要素数を求めれば、変更に強く安全な繰り返し処理が書けます。

配列をfor文で処理するときは、配列に何個の要素があるのかを正しく把握する必要があります。

要素数を少なく指定すれば、一部のデータが処理されません。反対に多く指定すると、配列の範囲外へアクセスする危険があります。初期化子を増減するたびにループ回数を手作業で修正する方法では、書き換え忘れも起こりやすくなります。

この問題を解決するときに便利なのがsizeof演算子です。

sizeofを使うと、配列全体が使用するバイト数と、要素1個が使用するバイト数を調べられます。この2つの値を利用すれば、配列の実際の要素数を計算できます。

この記事では、sizeofの基本的な役割、要素数を求める式、size_tを使う理由、for文との組み合わせ、マクロとの使い分け、関数の引数で使用するときの注意点まで、順番に見ていきます。

配列の要素数が必要になる理由

配列は、同じ型のデータをまとめて保存するための仕組みです。たとえば、商品の価格を配列へ保存すると、for文を使って順番に表示したり合計したりできます。

int prices[] = {180, 250, 120, 300, 210};

この配列には5個の要素があります。for文ですべての要素を処理するなら、5回繰り返す必要があります。

for (int i = 0; i < 5; i++) {
    printf("%d\n", prices[i]);
}

ただし、配列の値をあとから増やした場合、ループ条件の5も修正しなければなりません。

int prices[] = {180, 250, 120, 300, 210, 160, 280};

配列は7要素になりましたが、for文がi < 5のままでは、後ろの2要素が処理されません。

要素数の指定を間違えると、次のような問題が起こります。

状況起こる問題
実際より小さい要素数を使う配列の後半が処理されない
実際より大きい要素数を使う範囲外アクセスが発生する可能性がある
配列だけ変更するループ条件との不一致が起こる
同じ要素数を複数箇所へ書く修正漏れが起こりやすくなる

配列そのものから要素数を計算すれば、このような不一致を防ぎやすくなります。

sizeof演算子とは

sizeofは、データ型や変数がメモリ上で使用する大きさを、バイト単位で求める演算子です。

sizeof(int)
sizeof(double)
sizeof(prices)
sizeof(prices[0])

一般的な環境では、各型のサイズは次のようになることが多いです。

対象記述例よくある結果
char型sizeof(char)1バイト
int型sizeof(int)4バイト
double型sizeof(double)8バイト
配列sizeof(prices)配列全体のバイト数
配列の要素sizeof(prices[0])要素1個分のバイト数

ただし、intやdoubleのサイズは処理系によって異なる可能性があります。特定のバイト数を決めつけるのではなく、sizeofで実際のサイズを調べることが大切です。

charについては、C言語の規則によりsizeof(char)は必ず1になります。

図1:sizeofが調べる2種類のサイズ

この図から分かること

配列に対するsizeofは、配列全体のバイト数を求めます。一方、配列の1要素に対するsizeofは、その要素1個分のバイト数を求めます。

同じ型の要素は同じ大きさなので、配列全体のサイズを要素1個分のサイズで割れば、配列に含まれる要素の個数が分かります。

sizeofで配列要素数を求める基本式

配列の要素数は、次の式で求められます。

sizeof(配列名) / sizeof(配列名[0])

それぞれの式には、次の意味があります。

求められるもの
sizeof(配列名)配列全体のバイト数
sizeof(配列名[0])先頭要素1個分のバイト数
全体のサイズ ÷ 1要素のサイズ配列の要素数

たとえば、int型の要素を5個持つ配列を考えてみましょう。

int prices[] = {180, 250, 120, 300, 210};

仮にint型が4バイトなら、計算は次のようになります。

調べるもの計算結果
配列全体4バイト × 5要素20バイト
先頭要素4バイト × 1要素4バイト
要素数20 ÷ 45要素

したがって、次の式の結果は5になります。

sizeof(prices) / sizeof(prices[0])

重要なのは、int型が必ず4バイトだから計算できるわけではないという点です。sizeofが配列全体と1要素の実際のサイズを調べるため、要素の型や環境に合った計算ができます。

なぜ先頭要素を使うのか

基本式では、1要素分のサイズを求めるために配列名[0]を使います。

sizeof(prices[0])

配列はすべての要素が同じ型なので、どの要素を使ってもサイズは同じです。

sizeof(prices[0])
sizeof(prices[1])
sizeof(prices[2])

これらはすべてint型1個分のサイズになります。

それでも配列名[0]がよく使われるのは、先頭要素を表すため分かりやすく、添字が必ず有効だからです。要素数が1個以上ある配列なら、配列名[0]は必ず存在します。

要素の型を直接指定する書き方もできます。

sizeof(prices) / sizeof(int)

ただし、配列の型をあとからdoubleなどへ変更した場合、sizeof(int)も修正しなければなりません。

double prices[] = {180.5, 250.0, 120.8};

配列名[0]を使っておけば、要素型を変更しても式を書き換えずに済みます。

sizeof(prices) / sizeof(prices[0])

この書き方なら、配列自身の型に合わせて1要素分のサイズが決まります。

size_tで要素数を受け取る

sizeofの計算結果はsize_t型になります。そのため、配列の要素数を保存する変数にもsize_tを使うのが自然です。

size_t item_count = sizeof(prices) / sizeof(prices[0]);

size_tは、メモリのサイズやデータの個数を表すために用意された符号なし整数型です。サイズは負の値にならないため、この用途に適しています。

size_t型の値をprintfで表示するときは、変換指定子%zuを使います。

printf("要素数は%zu個です。\n", item_count);
内容使用するもの
sizeofの結果を保存する型size_t
size_tをprintfで表示する指定子%zu
for文のインデックス変数size_tを使用可能

for文の変数にもsize_tを使えば、要素数を保存した変数と同じ型で比較できます。

for (size_t i = 0; i < item_count; i++) {
    printf("%d\n", prices[i]);
}

配列の要素数と内容を表示する

6回分の荷物重量を順番に表示するプログラム

ファイル名:10_5_1.c

#include <stdio.h>

int main(void)
{
    /* 6回分の荷物重量を保存する配列 */
    int weights[] = {8, 12, 5, 15, 9, 11};

    /* 配列全体のサイズを1要素分のサイズで割る */
    size_t weight_count = sizeof(weights) / sizeof(weights[0]);

    printf("登録されている荷物は%zu個です。\n", weight_count);

    for (size_t i = 0; i < weight_count; i++) {
        printf("%zu個目の重量は%dkgです。\n", i + 1, weights[i]);
    }

    return 0;
}
実行結果の例
登録されている荷物は6個です。
1個目の重量は8kgです。
2個目の重量は12kgです。
3個目の重量は5kgです。
4個目の重量は15kgです。
5個目の重量は9kgです。
6個目の重量は11kgです。

プログラムの流れを詳しく見てみよう

最初に、荷物の重量を保存する配列weightsを初期化しています。

int weights[] = {8, 12, 5, 15, 9, 11};

要素数は省略されていますが、初期化子が6個あるため、コンパイラによって6要素の配列として扱われます。

次の行で、配列の要素数を求めています。

size_t weight_count = sizeof(weights) / sizeof(weights[0]);

仮にint型が4バイトの場合、それぞれの結果は次のようになります。

sizeof(weights)    → 24
sizeof(weights[0]) → 4
24 ÷ 4             → 6

計算結果の6がweight_countへ代入されます。

要素数の表示では、size_t用の%zuを使用しています。

printf("登録されている荷物は%zu個です。\n", weight_count);

for文では、weight_countを継続条件に利用しています。

for (size_t i = 0; i < weight_count; i++) {
    printf("%zu個目の重量は%dkgです。\n", i + 1, weights[i]);
}

iは0から始まり、5まで変化します。これにより、weights[0]からweights[5]までの全要素が1回ずつ処理されます。

画面に表示する個数は1から始めたほうが自然なので、表示用にはi + 1を使っています。配列の添字には、そのままiを使います。

iの値表示する番号処理する要素
01個目weights[0]
12個目weights[1]
23個目weights[2]
34個目weights[3]
45個目weights[4]
56個目weights[5]

図2:配列のバイト数から要素数を計算する流れ

この図から分かること

配列weightsにはint型の値が6個入っています。int型が4バイトの環境では、配列全体のサイズは24バイトです。

1要素分の4バイトで割ると、24 ÷ 4によって6という要素数が求められます。実際のプログラムでは、バイト数を手で決めず、両方をsizeofで調べることがポイントです。

配列を変更してもfor文を直さずに済む

sizeofで求めた要素数をfor文で使用すると、配列の初期化子を変更したときにもループ回数が自動的に合います。

たとえば、先ほどの配列へ2個の値を追加してみます。

int weights[] = {8, 12, 5, 15, 9, 11, 7, 14};

初期化子が8個になったため、weightsは8要素の配列になります。

size_t weight_count = sizeof(weights) / sizeof(weights[0]);

この式の計算結果も自動的に8になります。for文はweight_countを使っているため、継続条件を修正する必要はありません。

for (size_t i = 0; i < weight_count; i++) {
    printf("%d\n", weights[i]);
}

この書き方には、次の利点があります。

  • 初期化子を追加してもループ回数が自動で増える
  • 初期化子を減らしてもループ回数が自動で減る
  • 要素数の数え間違いを防ぎやすい
  • 配列とfor文の不一致が起こりにくい
  • 範囲外アクセスを防ぎやすくなる

配列を変更したときに修正する場所が少なくなるため、保守しやすいコードになります。

int以外の配列でも同じ式を使える

基本式は、int型の配列だけに使うものではありません。

sizeof(配列名) / sizeof(配列名[0])

double型の配列でも、同じ形で要素数を求められます。

double temperatures[] = {21.5, 23.0, 19.8, 22.6};

size_t count =
    sizeof(temperatures) / sizeof(temperatures[0]);

char型の配列でも考え方は同じです。

char grades[] = {'A', 'B', 'A', 'C'};

size_t count = sizeof(grades) / sizeof(grades[0]);

型によって1要素のバイト数は異なりますが、全体を1要素分で割るという関係は変わりません。

配列の型配列全体1要素分計算結果
int型5要素sizeof(numbers)sizeof(numbers[0])5
double型4要素sizeof(values)sizeof(values[0])4
char型6要素sizeof(codes)sizeof(codes[0])6

配列の要素型を直接式へ書かないため、型の変更にも対応しやすい書き方です。

マクロで要素数を管理する方法との違い

配列の要素数は、マクロを使って管理することもできます。

#define WEIGHT_COUNT 6

int weights[WEIGHT_COUNT] = {8, 12, 5, 15, 9, 11};

sizeofを使う方法とマクロを使う方法には、それぞれ向いている場面があります。

比較項目sizeofで計算マクロで管理
要素数の決まり方実際の配列から計算する定義した固定値を使う
初期化子の増減自動的に対応するマクロも確認する必要がある
配列サイズの共有配列が見える場所で使うプログラム内の複数箇所で使いやすい
向いている場面初期値の個数から長さを決める仕様として個数が固定されている
主な注意点関数引数では使えない場合がある配列定義との不一致に注意する

初期化子の個数に合わせて配列の長さを決めたい場合は、sizeofによる計算が便利です。

一方、入力を必ず10件受け付けるなど、配列サイズが設計上の決まりとして存在する場合は、マクロで管理する方法も分かりやすい書き方です。

配列本体に対して使うことが重要

要素数を求める式は、配列そのものに対してsizeofを使える場所で有効です。

int values[] = {10, 20, 30, 40};

size_t count = sizeof(values) / sizeof(values[0]);

このvaluesは配列本体なので、sizeof(values)は配列全体のサイズになります。

ところが、配列を関数の引数として受け取ると、引数は実質的に先頭要素を指すポインタとして扱われます。

void show_values(int values[])
{
    /* ここでsizeof(values)を使っても、
       配列全体のサイズにはならない */
}

この場所にあるvaluesは、配列全体ではなくポインタとして扱われます。そのため、次の式から元の配列の要素数を正しく求めることはできません。

sizeof(values) / sizeof(values[0])

関数へ配列を渡す場合は、要素数も別の引数として渡す方法が基本です。

void show_values(const int values[], size_t count)

ただし、この関数を使った完全なプログラムは今回の範囲には含めず、ここでは注意点として考え方だけ押さえておきましょう。

図3:配列本体と関数引数で異なるsizeofの結果

この図から分かること

sizeofによる要素数の計算が正しく働くのは、配列本体に対してsizeofを使える場合です。

関数の配列引数はポインタとして扱われるため、sizeofで得られるのは配列全体ではなく、ポインタのサイズです。関数へ配列を渡すときは、要素数も一緒に渡す必要があります。

sizeofを使うときの確認ポイント

確認することポイント
対象は配列本体かポインタでは配列全体のサイズを求められない
分母は1要素のサイズかsizeof(array[0])を使う
結果を保存する型は適切かsize_tを使う
printfの指定子は適切かsize_tには%zuを使う
for文の条件は正しいかi < 要素数とする
配列の型を直接書いていないかsizeof(array[0])なら型変更に対応しやすい

覚えておきたい基本形は、次の2行です。

size_t count = sizeof(array) / sizeof(array[0]);

for (size_t i = 0; i < count; i++) {
    /* array[i]を処理する */
}

配列本体から実際の要素数を求め、その結果をfor文の条件に使うことで、配列の増減に自然に対応できます。要素数を人が数えて管理する箇所を減らせるため、修正漏れや範囲外アクセスを防ぎやすいコードになります。