C言語基礎|配列の受渡しとconst

前回の「配列の受渡し」でつかんだ一番大事な感覚はこれでしたね。

関数が受け取った配列は、呼び出し側の配列そのものを参照しているように振る舞う

つまり、関数の中で配列要素を書き換えると、呼び出し側の配列も変わる可能性があります。
ここで多くの人が不安になります。

「えっ…関数に配列を渡すだけで、勝手に中身を書き換えられたら困るんだけど?」

その不安をスッと解消するために登場するのが const(型修飾子) です。
読み取り専用で使う関数には const を付ける—これがC言語の“安心設計”の基本ルールになります。

サンプルプログラム

ここでは 温度データを加工する関数と、温度データを表示する関数を作ります。

  • 関数 adjust_all:配列の先頭 n 個の要素に、補正値を加える(書き換える)
  • 関数 show_array:配列の先頭 n 個の要素を表示する(読むだけ=書き換えない)

サンプル:配列を書き換える関数と、読むだけの関数

プロジェクト名:chap6-15-1 ソースファイル名:chap6-15-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

// 温度配列を補正し、表示する(constの有無を確認)

#include <stdio.h>

//--- 要素数nの配列vの先頭n個にoffsetを加える ---//
void adjust_all(int v[], int n, int offset)
{
    for (int i = 0; i < n; i++)
        v[i] += offset;
}

//--- 要素数nの配列vの先頭n個を表示して改行 ---//
void show_array(const int v[], int n)
{
    printf("[ ");
    for (int i = 0; i < n; i++)
        printf("%d ", v[i]);
    printf("]\n");

    // もしここで v[0] = 999; などを書いたらコンパイルエラーになる(constだから)
}

int main(void)
{
    int tokyo[] = {10, 12, 11, 9, 8};
    int osaka[] = {14, 15, 13};

    printf("補正前 Tokyo = ");  show_array(tokyo, 5);
    printf("補正前 Osaka = ");  show_array(osaka, 3);

    adjust_all(tokyo, 5, 2);   // Tokyoの先頭5個を+2補正
    adjust_all(osaka, 2, -1);  // Osakaの先頭2個だけを-1補正(わざと一部だけ)

    puts("補正後(配列の中身が更新されました)");
    printf("補正後 Tokyo = ");  show_array(tokyo, 5);
    printf("補正後 Osaka = ");  show_array(osaka, 3);

    return 0;
}

実行例

補正前 Tokyo = [ 10 12 11 9 8 ]
補正前 Osaka = [ 14 15 13 ]
補正後(配列の中身が更新されました)
補正後 Tokyo = [ 12 14 13 11 10 ]
補正後 Osaka = [ 13 14 13 ]

配列を渡すと「同じ実体」を見ている(図で理解)

配列名を渡すと、関数側の仮引数 v は 呼び出し側配列の先頭を指すように扱われます。
だから、関数内で v[i] を更新すると、呼び出し側の配列も更新されます。

const を付けると「読むだけ」をコンパイラが保証してくれる

ここが今回の主役です。

const を付けるべきかの判断

その関数は配列要素を書き換える?仮引数の宣言例意味
書き換えるint v[]呼び出し側の配列を更新する可能性がある
書き換えない(読むだけ)const int v[]関数内で v[i] に代入できない(安全)

つまり、show_array は読むだけなので const を付ける。
adjust_all は書き換えるので const を付けない。
このルールが分かれば、呼び出し側も安心して使えます。

命令(構文)の書式と「何をする命令か」

➀ const(型修飾子)

  • 書式の例:const int v[]
  • 何をする?
    その変数(ここでは配列要素)を 書き換え禁止 にする指定です。
    読み込みはOK、代入はNGになります。

➁ 配列を受け取る関数の仮引数

  • 書式の例:int v[]、const int v[]
  • 何をする?
    「int型の配列(の先頭を指す情報)を受け取る」という宣言になります。
    要素数は別引数 n で受け取るのが基本です。

➂ for 文

  • 書式:for (初期化; 条件; 更新) 文
  • 何をする?
    指定回数ぶん、繰り返し処理を行います。配列処理の定番です。

➃ puts / printf

  • puts:文字列を表示して改行
  • printf:書式を指定して表示(数値などの整形ができる)

const を付けると起きる「エラー」の意味

たとえば show_array の中で、うっかり次のように書くと…

void show_array(const int v[], int n)
{
    v[0] = 999;   // 代入しようとした
}

これは コンパイルエラーになります。
エラーの意味はシンプルで、

const で宣言された配列要素は変更できません

ということです。
つまり const を付けることで、間違いが実行前に止まるんです。強い。

n は「配列の要素数」ではなく「処理対象の個数」と考えるとスッキリする

今回の例では、わざとこうしました。

adjust_all(osaka, 2, -1);

osaka は3要素あるのに、先頭2個だけ補正しています。
ここから分かる大事なポイントはこれです。

n の正体

n の意味何が起きる?
配列の要素数そのものshow_array(tokyo, 5)5個すべて表示
処理したい先頭の個数adjust_all(osaka, 2, -1)先頭2個だけ変更

なので説明としては、

  • 「配列の全要素を…」という言い方もよくする(慣習)
  • でも正確には「先頭 n 個の要素を…」という意味

この2つを頭の中で切り替えられると、配列関数が一気に読みやすくなります。

重要ポイント(ここだけは覚えておく)

  • 配列を渡すと、関数側は呼び出し側配列を直接扱っているように見える。
  • 書き換える関数は int v[]
  • 読むだけの関数は const int v[](安心・安全・再利用しやすい)
  • n は「処理対象の個数」と考えると応用が効く。