文字列の基礎

Cの文字列は透明なビーズで終わるネックレスのようなものです——各ビーズが文字で、最後の目に見えない終端子\0が「ネックレスはここで終わり」と合図します。

文字配列と文字列

Cには専用の文字列型がありません。文字列は本質的に\0(null文字)で終端された文字配列です。\0のASCII値は0で、文字列の終わりを示します。

C
char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

この配列は6要素です。5つの可視文字と1つの\0です。\0がなければ、それはただの文字配列であり、文字列としては使えません。

文字列リテラル(二重引用符)は末尾に自動的に\0が付きます。

C
char str[] = "Hello";

配列の長さは6(5文字+1つの\0)で、コンパイラが自動的に計算します。より大きい長さを指定することもでき、余った位置は\0で埋められます。

C
char str[10] = "Hello";

str[5]からstr[9]まではすべて\0です。

💡 ヒント: 単一引用符'H'は文字(1バイト)を表し、二重引用符"Hello"は文字列(暗黙の\0付き)を表します。両者は同じではありません。

文字列の宣言と初期化

文字配列方式

C
char s1[] = "abc";
char s2[10] = "abc";
char s3[] = {'a', 'b', 'c', '\0'};

3つとも文字列"abc"と等価です。s3には手動で\0を追加する必要があることに注意してください。

文字ポインタ方式

C
const char *s4 = "abc";

s4は文字列リテラルの最初の文字を指します。文字列リテラルは読み取り専用領域に格納されており、変更しようとすると未定義動作になります。

C
s4[0] = 'A';

これは誤りです。しかし文字配列は変更可能です。

C
s1[0] = 'A';

有効です。s1の内容は"Abc"になります。

⚠️ 注意: ポインタで文字列リテラルを指す際はconstを使ってください。変更しようとするとコンパイラが警告してくれます。

ポインタと配列の違い

C
char arr[] = "hello";
const char *ptr = "hello";

文字列の入出力

printfとscanf

文字列の入出力には%s書式指定子を使います。

C
char name[20];
scanf("%s", name);
printf("Hello, %s!\n", name);

scanfは空白文字(スペース、改行、タブ)で読み取りを停止するため、スペースを含む行全体は読めません。

scanfはバッファサイズをチェックしないため、配列の長さを超える入力はオーバーフローを引き起こします。読み取り長を制限できます。

C
scanf("%19s", name);

これは最大19文字を読み取り、1つ分の位置を\0のために残します。

putsとgets

putsは文字列を出力し、自動的に改行を追加します。

C
char msg[] = "Hello";
puts(msg);

これはprintf("%s\n", msg);と等価です。

getsは改行に達するまで1行全体(スペースを含む)を読み取ります。

C
char line[100];
gets(line);
🔥 よくある間違い: getsは絶対に使わないでください! バッファサイズをチェックしないため、入力が配列の容量を超えると境界外書き込みが発生します。最も危険なC関数の一つであり、C11で完全に削除されました。

fgets

fgetsgetsの安全な代替です。

C
char line[100];
fgets(line, sizeof(line), stdin);

仮引数は、格納バッファ、読み取り最大バイト数(\0を含む)、入力ストリームです。fgetsは(改行が読み取られ、まだ余裕があれば)文字列末尾の改行を保持します。

末尾の改行を削除する一般的なパターン:

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

int main(void) {
    char line[100];
    fgets(line, sizeof(line), stdin);

    int len = strlen(line);
    if (len > 0 && line[len - 1] == '\n') {
        line[len - 1] = '\0';
    }

    printf("[%s]\n", line);
    return 0;
}

ユーザーの名前と自己紹介を1行ずつ読み取ります。

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

void trim_newline(char *s) {
    int len = strlen(s);
    if (len > 0 && s[len - 1] == '\n') {
        s[len - 1] = '\0';
    }
}

int main(void) {
    char name[30];
    char intro[200];

    printf("Enter your name: ");
    fgets(name, sizeof(name), stdin);
    trim_newline(name);

    printf("Introduce yourself: ");
    fgets(intro, sizeof(intro), stdin);
    trim_newline(intro);

    printf("\n--- Personal Info ---\n");
    printf("Name: %s\n", name);
    printf("Intro: %s\n", intro);

    return 0;
}
▶ 試してみよう
TEXT
Enter your name: Alice
Introduce yourself: I am a C beginner

--- Personal Info ---
Name: Alice
Intro: I am a C beginner

strlen関数

strlen<string.h>で宣言されており、文字列の長さ(\0を含まない)を返します。

C
#include <string.h>

char s[] = "Hello";
printf("%zu\n", strlen(s));

出力:5。sizeof(s)は6(\0を含む)ですが、strlen(s)は5(\0を含まない)です。

strlenの仕組み:先頭から\0に遭遇するまで文字を数えます。

C
size_t my_strlen(const char *s) {
    size_t len = 0;
    while (s[len] != '\0') {
        len++;
    }
    return len;
}

文字配列が\0で終端されていない場合、strlenは偶然ゼロバイトに出会うまで読み続け、非常に危険です。

文字ポインタの入門

文字ポインタは文字配列にも文字列リテラルにも指し示すことができます。

C
char arr[] = "world";
const char *ptr = "world";

ptr = arr;
printf("%s\n", ptr);

ポインタは別の文字列を指すよう再代入できますが、配列名は定数であり変更できません。

C
ptr = "another string";

有効。しかし:

C
arr = "another string";

不正。配列名には代入できません。

ポインタで文字列を走査:

C
const char *p = "Hello";
while (*p != '\0') {
    printf("%c", *p);
    p++;
}
printf("\n");

*pで間接参照して現在の文字を取得し、p++で次の文字位置に進みます。ポインタの歩幅は指している型で決まり、char *は1ステップで1バイト進みます。

文字ポインタで文字列を逆順に出力します。

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

void print_reverse(const char *s) {
    const char *p = s + strlen(s) - 1;
    while (p >= s) {
        putchar(*p);
        p--;
    }
    putchar('\n');
}

int main(void) {
    print_reverse("Hello");
    print_reverse("ABCDEF");
    return 0;
}
▶ 試してみよう
TEXT
olleH
FEDCBA

まずポインタpを文字列の最後の文字(\0の1つ前)に移動し、そこから前へ向かって文字を出力します。

❓ よくある質問

Q 'a'"a"の違いは何ですか?
A 'a'は1バイトを占める文字定数です。"a"は2バイト('a'と'\0')を占める文字列定数で、型はchar[]です。
Q 文字列でsizeofstrlenの結果が異なるのはなぜですか?
A sizeofは配列が占有する総メモリ(\0を含む)を返し、strlenは\0より前の文字数を返します。
Q fgetsが改行を読み取った場合どうなりますか?
A fgetsは改行をバッファに格納します。通常、手動で確認して\0に置き換えることで削除する必要があります。
Q 文字ポインタを通じて文字列リテラルの内容を変更できますか?
A いいえ。文字列リテラルは読み取り専用セグメントに格納されており、変更は未定義動作です。内容を変更する必要がある場合は文字配列を使ってください。

📖 まとめ

📝 練習問題

  1. fgetsで1行のテキストを読み取り、文字、数字、その他の文字の数を数えるプログラムを書いてください。
  2. 文字列s中のすべてのoldnew_chに置換する関数void str_replace_char(char *s, char old, char new_ch)を書いてください。
  3. strlenを使わずに文字ポインタで文字列の長さを計算する関数を書いてください。
100%