C言語基礎|ナル文字を持たない配列

その配列、本当に文字列? ナル文字がないだけで、扱いは別モノになる!

ナル文字がないと「文字列」としては終われない

C言語の文字列は、見た目が文字の並びでも 末尾にナル文字(値が0の文字) があることで「ここまでが文字列です」と分かる仕組みでした。

でも、配列のサイズがギリギリで ナル文字が入らない と、そこには「終わりの合図」がありません。
この状態は《文字列》ではなく、《文字が並んだ配列》として扱う必要があります。

まず結論:文字列と、文字配列の違い

文字列 vs ナル文字を持たない配列

種類末尾のナル文字表示方法長さの求め方ありがちな事故
文字列あるprintf の %s でそのまま表示できる'\0' まで数える安全に扱いやすい
ナル文字を持たない配列ない1文字ずつ、要素数ぶん表示する要素数で管理する%s で表示すると暴走しやすい

表の説明

  • 文字列は「終端が分かる」ので、%s や puts で扱えます
  • ナル文字がない配列は「終端が分からない」ので、配列の要素数が必須になります

なぜ char str[4] = "ABCD"; は危険なの?

次の宣言を見てみましょう。

char str[4] = "ABCD";

"ABCD" は見た目4文字ですが、文字列リテラルは末尾にナル文字が付きます。つまり必要なのは 5文字分 です。
でも配列は4文字分しかないので、末尾のナル文字が入りません。

その結果、この宣言は次と同じ扱いになります。

char str[4] = {'A', 'B', 'C', 'D'};

メモリ上のイメージ

説明

  • 文字列なら最後に 0 が入って終端になります
  • この配列は 0 が無いので、「どこまで表示してよいか」が分かりません

サンプルプログラム

ナル文字を持たない配列を安全に表示するプログラム例です。

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

// ナル文字を持たない配列を安全に表示する例
#include <stdio.h>

// 配列 a を n 個ぶん表示(文字列としては扱わない)
void put_chars(const char a[], int n)
{
    for (int i = 0; i < n; i++)
        putchar(a[i]);
}

int main(void)
{
    char code[4] = "ABCD";  // 終端のナル文字は入らない

    printf("これは文字列ではなく、4文字の配列です:");
    put_chars(code, 4);
    putchar('\n');

    return 0;
}

実行例

これは文字列ではなく、4文字の配列です:ABCD

よくある事故:%s で表示すると何が起きる?

もし次のように書いたらどうなるでしょう。

printf("%s\n", code);

これは 文字列として終端の 0 を探しにいく 表示です。
ところが code には終端がないので、メモリ上でたまたま 0 が出てくるまで読み続けます。

暴走のイメージ

図の説明

  • 配列の外まで読んでしまい、意味不明な文字が出たり、クラッシュしたりします。
  • これは「終端を持たない配列」を「文字列」として扱ったのが原因です。」

文字列の配列を 1文字ずつ走査する話(2次元配列の復習も兼ねる)

文字列の配列(2次元配列)を 各文字を1文字ずつ走査して表示 します。
これは「文字列の終端ナル文字まで読む」考え方の練習としてとても良いです。

プロジェクト名:chap9-11-2 ソースファイル名:chap9-11-2.c

// 文字列の配列を 1文字ずつ走査して表示(別メッセージ)
#include <stdio.h>

void put_strary_bychar(const char s[][6], int n)
{
    for (int i = 0; i < n; i++) {
        int j = 0;

        printf("項目%d:", i);

        while (s[i][j]) {
            putchar(s[i][j]);
            j++;
        }
        putchar('\n');
    }
}

int main(void)
{
    char words[][6] = {"Turbo", "NA", "DOHC"};

    puts("1文字ずつ順番に表示します。");
    put_strary_bychar(words, 3);

    return 0;
}

実行例

1文字ずつ順番に表示します。
項目0:Turbo
項目1:NA
項目2:DOHC

添字が1つ増えると何が変わる?

1次元の走査と2次元の走査(イメージ)

1次元(文字列):
s[i] を i で進める

2次元(文字列の配列):
s[i][j]
 i: 何番目の文字列か
 j: その文字列の何文字目か

説明

  • 文字列は char の並びなので添字は1つ(i)で走査できます。
  • 文字列の配列は「文字列が複数ある」ので、行(i)と列(j)の2つの添字でアクセスします。

命令の書式と「何をする命令か」

for の書式と役割

  • 書式
    for (初期化; 継続条件; 更新) 文;
  • 何をする?
    回数が決まっている繰り返しに向いています。
    今回は「n個の要素を表示する」「n個の文字列を表示する」で使っています。

while の書式と役割

  • 書式
    while (条件式) 文;
  • 何をする?
    条件式が真の間、繰り返します。
    文字列走査では s[i][j] が 0 になるまで進めるのが定番です。

putchar の書式と役割

  • 書式
    int putchar(int c);
  • 何をする?
    1文字を標準出力に出します。戻り値は出力した文字(エラー時は EOF)です。
    文字列を1文字ずつ表示したいときに活躍します。

printf の書式と役割(注意つき)

  • 書式
    int printf(const char *format, ...);
  • 何をする?
    書式付きで表示します。%s は「ナル文字で終わる文字列」を表示します。
    だから、ナル文字がない配列に %s を使うのは危険です。

演習問題

演習9-11:$$$$$ で入力終了する「文字列の配列」読み込み&表示

文字列の個数と最大長をオブジェクト形式マクロで定義する。
文字列を読み込み、$$$$$ が入力されたら終了する。
$$$$$ より前に入力された文字列だけ表示する。

解答例

プロジェクト名:chap9-11-3 ソースファイル名:chap9-11-3.c

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

#include <stdio.h>
#include <string.h>

#define MAX_N 5
#define MAX_LEN 128

int read_strings(char s[][MAX_LEN], int n)
{
    int count = 0;

    for (int i = 0; i < n; i++) {
        printf("入力%d:", i);
        scanf("%127s", s[i]);

        if (strcmp(s[i], "$$$$$") == 0)
            break;

        count++;
    }
    return count;
}

void print_strings(const char s[][MAX_LEN], int n)
{
    for (int i = 0; i < n; i++)
        printf("s[%d] = %s\n", i, s[i]);
}

int main(void)
{
    char s[MAX_N][MAX_LEN];

    puts("$$$$$ で入力終了です。");
    int used = read_strings(s, MAX_N);

    puts("入力された文字列一覧:");
    print_strings(s, used);

    return 0;
}

解説

  • MAX_N と MAX_LEN をマクロにすると、配列サイズ変更が簡単になります。
  • read_strings は「何個入力できたか」を返すのがコツです。
  • strcmp を使って $$$$$ かどうかを判定します。

演習9-12:文字列の配列に入った各文字列を反転する

void rev_strings(char s[][128], int n);
を作成し、各文字列をその場で反転させる。

解答例

プロジェクト名:chap9-11-4 ソースファイル名:chap9-11-4.c

#include <stdio.h>

int str_length(const char s[])
{
    int len = 0;
    while (s[len])
        len++;
    return len;
}

void rev_string(char s[])
{
    int left = 0;
    int right = str_length(s) - 1;

    while (left < right) {
        char tmp = s[left];
        s[left] = s[right];
        s[right] = tmp;
        left++;
        right--;
    }
}

void rev_strings(char s[][128], int n)
{
    for (int i = 0; i < n; i++)
        rev_string(s[i]);
}

int main(void)
{
    char s[2][128] = {"SEC", "ABC"};

    rev_strings(s, 2);

    printf("%s\n", s[0]);
    printf("%s\n", s[1]);

    return 0;
}

解説

  • 反転は左右から交換していくのが定番です。
  • rev_strings は「各行の文字列 s[i] を rev_string に渡す」だけでOKです。
  • ここでも文字列なので、終端ナル文字までが対象になります。