C言語の while 文が思った通りにループしない

[code]

    a[0] = ABC
    a[1] = DE
    a[2] = FGHI
    
[/code]

のようにコマンドプロンプト上に表示できるようにしたつもりだったのですが、

[code]

    a[0] = ABC
    a[1] = DE
    a[2] = FGHI
    a[3] = 
    a[4] = a
    a[5] = +
    
[/code]

と表示されてしまいました。

なぜ while文は a[2] = FGHI の表示で終わらなかったのか、かといって無限に続くわけでもなく a[5] = +
で止まったのか、分かる方いましたら教えていただきたいです。

[code]

    int i = 0;
    char a[][5] = {"ABC","DE","FGHI"};
    
    while (a[i][0] != 0) {
        printf("a[%d] = %s\n", i, a[i]);
        i++;
    }
    
[/code]




`a`の初期化部分は次のコードとほぼ同じです。

[code]

    char a[3][5] = {
        {'A', 'B', 'C', '\0', '\0'},
        {'D', 'E', '\0', '\0', '\0'},
        {'F', 'G', 'H', 'I', '\0'}
    };
    
[/code]

`a`の5x3の二次元配列です。`sizeof(a)`が15になることからもこれがわかります。5の部分は明示されていますが、3の部分は初期化子によって決定されます。

さて、問題は`while`の条件式である`a[i][0] != 0`で`i`が3以上の時です。`a[3]`からは`a`の配列のサイズを越えているため
**未定義の動作**
になります。つまり、何が起きてもおかしくないということです。`a[5]`で止まったのは偶然であり、`[2]`で止まっても、逆にプログラムが落ちても不思議ではありません。理由云々の前に、Cとしては
**間違ったコード(書いてはいけないコード)** となります。

※
未定義の動作とはCの規格上、どのような動作になるのかを定義していない動作のことです。コンパイル時にエラーになる、何もしないでそのまま素通りする、ゴミデータを出力する、エラーになる、プログラムが落ちる、任意コードを実行可能にする等、何が起きてもおかしくないことを意味します。状況によっては脆弱性の原因となりますので、そのような動作がおきないようにコードを書かなければなりません。

* * *

では、どのように修正すべきかです。`a`という配列には終端を示すものはありませんので、終端判定はできません。ですので、ちゃんと`a`のサイズを計算して、その分だけ`i`を回すようにします。

[code]

    #include <stdio.h>
    int main(void)
    {
        char a[][5] = {"ABC","DE","FGHI"};
    
        for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
            printf("a[%d] = %s\n", i, a[i]);
        }
    }
    
[/code]

または、kunifさんの回答のように終端を表す`""`を追加すると言う方法でもいいでしょう。

* * *

未定義の動作であっても、どういう動作をしているのか知りたい…という人もいるかも知れません。ただ、これらの動作は環境や実行状況、コンパイラ、その他色々な要因によって動作が変わるので、よくある動作の一つとして考えてください。

`a`はスタックに積まれることになります。ほとんどのアーキテクチャではスタックは後ろから前に詰まれます。つまり、(たいていの場合は)`a`はスタックの最前線であり、その後ろにも何らかのデータがあり、最適化などがないとたいていの場合は`i`です。さらにその後も呼び出し元でスタックに積まれたデータが入っています(`main`であっても、その`main`を呼び出す前の前処理の何かが入っています)。もし、初期化していない変数とかアライメントによって使われていない領域があっても何からのデータが入っています。それは0かもしれないですし、前に実行した値かも知れませんし、実行時の状況に変わります。なので、その何かが入っている領域に対して、律儀にアクセスし、たまたま`a[6][0]`に相当するところが0だったので止まったと言うだけです。

だいたいこんな感じです。より細かく知りたいのであれば、逆アセンブルして分析をする必要があるでしょう。




`a`の初期化部分は次のコードとほぼ同じです。

[code]

    char a[3][5] = {
        {'A', 'B', 'C', '\0', '\0'},
        {'D', 'E', '\0', '\0', '\0'},
        {'F', 'G', 'H', 'I', '\0'}
    };
    
[/code]

`a`の5x3の二次元配列です。`sizeof(a)`が15になることからもこれがわかります。5の部分は明示されていますが、3の部分は初期化子によって決定されます。

さて、問題は`while`の条件式である`a[i][0] != 0`で`i`が3以上の時です。`a[3]`からは`a`の配列のサイズを越えているため
**未定義の動作**
になります。つまり、何が起きてもおかしくないということです。`a[5]`で止まったのは偶然であり、`[2]`で止まっても、逆にプログラムが落ちても不思議ではありません。理由云々の前に、Cとしては
**間違ったコード(書いてはいけないコード)** となります。

※
未定義の動作とはCの規格上、どのような動作になるのかを定義していない動作のことです。コンパイル時にエラーになる、何もしないでそのまま素通りする、ゴミデータを出力する、エラーになる、プログラムが落ちる、任意コードを実行可能にする等、何が起きてもおかしくないことを意味します。状況によっては脆弱性の原因となりますので、そのような動作がおきないようにコードを書かなければなりません。

* * *

では、どのように修正すべきかです。`a`という配列には終端を示すものはありませんので、終端判定はできません。ですので、ちゃんと`a`のサイズを計算して、その分だけ`i`を回すようにします。

[code]

    #include <stdio.h>
    int main(void)
    {
        char a[][5] = {"ABC","DE","FGHI"};
    
        for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
            printf("a[%d] = %s\n", i, a[i]);
        }
    }
    
[/code]

または、kunifさんの回答のように終端を表す`""`を追加すると言う方法でもいいでしょう。

* * *

未定義の動作であっても、どういう動作をしているのか知りたい…という人もいるかも知れません。ただ、これらの動作は環境や実行状況、コンパイラ、その他色々な要因によって動作が変わるので、よくある動作の一つとして考えてください。

`a`はスタックに積まれることになります。ほとんどのアーキテクチャではスタックは後ろから前に詰まれます。つまり、(たいていの場合は)`a`はスタックの最前線であり、その後ろにも何らかのデータがあり、最適化などがないとたいていの場合は`i`です。さらにその後も呼び出し元でスタックに積まれたデータが入っています(`main`であっても、その`main`を呼び出す前の前処理の何かが入っています)。もし、初期化していない変数とかアライメントによって使われていない領域があっても何からのデータが入っています。それは0かもしれないですし、前に実行した値かも知れませんし、実行時の状況に変わります。なので、その何かが入っている領域に対して、律儀にアクセスし、たまたま`a[6][0]`に相当するところが0だったので止まったと言うだけです。

だいたいこんな感じです。より細かく知りたいのであれば、逆アセンブルして分析をする必要があるでしょう。

