C言語の文字列の使い方: 文字配列と文字列定数

327, 2021-09-21

目次

C言語における文字列とは?

C言語では文字列を扱うことが出来ます。
文字列はメッセージやファイルの内容などを書くのに使われます。
C言語の文字列には2種類あり、これらの違いを踏まえて使うのがコツです。

またこの記事では具体的な文字列の使い方とあわせて、文字列関数の使い方も解説します。

文字配列と文字列定数

C言語の文字列とは具体的に見ると↓の2つがそうです。

  • 文字配列

  • 文字列定数

これら2つのことを指して文字列と表現します。

文字配列は変更可能な文字列です。
変更可能なので文字列の要素を変更することができます。
これはデータセグメントと言うメモリ領域に保存されます。

いっぽう文字列定数は変更不可能な文字列です。
この文字列を無理に変更するとプログラムがクラッシュします。
これはテキストセグメントというメモリ領域に保存されています。

C言語の文字列の扱いではこれら2つの文字配列と文字列定数を混ぜ合わせて使うことが多いです。
この2つの違いを認識して使い分けるようにしましょう。

文字列の構造と仕組み

文字配列と文字列定数の構造は両方とも共通しています。
それは文字列が複数の文字の集まりによって構成されていることです。
また文字列はその終端にナル文字(ヌル文字)が存在します。
逆に言うと、このナル文字が存在しないとC言語では文字列として機能しません。

ナル文字は文字列の番兵として機能します。
つまり文字列をfor文などで回す時に文字列の終端を表す文字としてナル文字が存在しているわけです。
このナル文字がないとfor文などが無限ループになってしまいます。
そのためC言語で文字列(文字配列、文字列定数)を使う場合はこのナル文字を意識するようにしましょう。

文字列を扱う関数

C言語の文字列を扱う関数は標準ライブラリのstring.hに多く宣言されています。
代表的な関数は↓のようなものです。

  • strlen ... 文字列の長さを取得する関数

  • strcmp ... 文字列同氏を比較する関数

  • strcat ... 文字列を連結する関数

  • strcpy ... 文字列をコピーする関数

またstdio.hには文字列を読み込む関数も宣言されています。

  • fgets ... 標準入力から文字列を読み込む関数

  • scanf ... 同上

実際のC言語の文字列ではこれらの関数を使って文字列を処理します。

文字列の定義方法

文字列の定義方法はどのようにおこなうのでしょうか?
文字配列と文字列定数の定義方法は?
具体的に見ていきます。

char型の配列で文字配列を定義する

まず文字配列の定義方法です。
文字配列はchar型の配列を使って↓のように定義します。

    char s1[20];  // 20要素ある文字配列

これはただのchar型の配列ですが、値は初期化されていません。
文字配列を初期化するには↓のように書きます。

    char s2[20] = "Hello, World!";  // 初期化済みの文字配列

また、↑のように文字列定数(ダブルクオートで囲われた文字列)で文字配列を初期化する場合、↓のように配列の要素を省略できます。

    char s3[] = "Hello, World!";  // 初期化済みの文字配列

↑の場合、文字配列の要素は文字列定数の長さだけ自動で確保されます。
このように文字配列は文字列定数で初期化することが可能です。

ポインタ変数で文字列定数を定義する

ポインタ変数で文字列定数を定義します。
その場合は↓のように書きます。

    const char *s = "Hello, World!";  // 文字列定数へのポインタ

ダブルクオートで囲まれた文字列が文字列定数で、それに対するポインタがsです。
文字列定数は変更することが出来ません。
そのため↑のようにsにはconstを付けておきます。
文字列定数へのポインタを定義する場合はかならずconstを付ける習慣を付けておきましょう。

文字列定数とconstはお友達

文字列の参照方法

文字列を参照するにはどうしたらいいでしょうか?
文字列をprintf()で出力するには?
文字列の文字を参照するには?
具体的に見ていきます。

printf()で文字列を出力する

文字列をprintf()関数で出力するには%sという書式指定子を使います。
↓のようにすると文字配列と文字列定数を出力することができます。

#include <stdio.h>

int main(void) {
    char s1[] = "Good";
    const char *s2 = "Nice";

    printf("%s\n", s1);  // Good
    printf("%s\n", s2);  // Nice
    return 0;
}

インデックスで文字を参照する

文字列の要素は添え字で参照できます。
そのため文字列の要素を参照するには↓のように書きます。

#include <stdio.h>

int main(void) {
    char s1[] = "Good";
    const char *s2 = "Nice";

    printf("%c\n", s1[0]);  // G
    printf("%c\n", s2[1]);  // i
    return 0;
}

範囲外の添え字のアクセスはプログラムが落ちる場合があります。
しかしそのまま動作する場合もあるので注意が必要です。
その場合は非常にわかりづらいバグになります。

ナル文字の参照

文字列のナル文字はこっそり保存されています。
この存在を確認するコードは↓になります。

#include <stdio.h>

int main(void) {
    char s[] = "01";
    printf("%d\n", s[2] == '\0');  // 1
    return 0;
}

sは文字列で01という文字列が保存されています。
つまり文字列の長さは2です。これは添え字で言うと01までです。
ナル文字はこの場合、添え字の2の要素に保存されています。
↑のように「s[2] == '\0'」とやってナル文字が保存されているか確認します。結果は真になります。
\0」というのがナル文字を表す文字です。この値は整数では0で表現されます。

文字列の更新方法

文字列定数は更新できませんが、文字配列は更新することができます。
文字配列の要素を添え字で参照して上書きすることで更新可能です。

#include <stdio.h>

int main(void) {
    char s[] = "Good";
    s[0] = 'F';
    printf("%s\n", s);  // Food
    return 0;
}

↑の場合、Goodという文字配列がFoodという文字配列に更新されてます。

文字列定数を変更する場合の注意

文字列定数はテキストセグメントという変更不可なメモリ領域に保存されています。
そのため変更はできません。
変更しようとするとプログラムがクラッシュします。

そのため文字列定数へのポインタも変更不可にしておく必要があります。
constを付けるのを忘れないようにしてください。

文字列に関するポインタの操作あれこれ

文字列とポインタの関わりはどんなものでしょうか?
文字配列をポインタで扱うには?
具体的に解説します。

文字列をポインタに保存する

文字配列はポインタ変数に代入することができます。
↓の場合、sが文字配列でpがポインタです。
ポインタの参照先は文字配列なので出力される文字列は文字配列の場合と同じです。

#include <stdio.h>

int main(void) {
    char s[] = "Good";
    char *p = s;
    printf("%s\n", p);  // Good
    return 0;
}

その文字列、変更可能ですか?変更不可能ですか?

文字列のポインタでやっかいなのが、そのポインタが文字配列なのか文字列定数なのか判断できないところです。
うっかり文字列定数のポインタを変更してしまったらプログラムがクラッシュする場合があります。
そのため文字列定数のポインタにはconstを付けて、変更不可能なことを明示しておく必要があります。
文字列定数にconstを付けておけば、constがついていないポインタは文字配列と判断できます。

文字列の長さを取得: strlenの使い方

では文字列の長さを得るstrlen()関数の使い方を見てみます。
strlen()の第1引数に文字列を渡すと、strlen()はその文字列の長さをsize_tで返します。

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

int main(void) {
    const char *s = "Good";
    size_t len = strlen(s);  // 文字列sの長さを得る
    printf("%d\n", len);  // 4
    return 0;
}

Goodという文字列の長さは全体で4文字です。
そのためstrlen()の結果も4になります。
じっさいは文字列にはナル文字が含まれるため5要素あるのですが、strlen()は4文字でカウントします。
実際の文字列の要素数と文字列数は違うということに注意してください。

文字列同士を比較: strcmpの使い方

文字列同士が等しいかどうか比較する関数がstrcmp()です。
strcmp()は引数の文字列が等しければ0を返し、等しくなければ0以外を返します。

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

int main(void) {
    char s1[] = "Good";
    const char *s2 = "Good";
    int result = strcmp(s1, s2);  // s1とs2を比較する
    printf("%d\n", result);  // 0

    char s3[] = "Good";
    const char *s4 = "Nice";
    result = strcmp(s3, s4);  // s3とs4を比較する
    printf("%d\n", result);  // -7
    return 0;
}

if文でstrcmpを使う場合は↓のように書きます。

if (strcmp(s1, s2) == 0) {
    puts("同じ");
} else {
    puts("違う");
}

文字列を連結する: strcatの使い方

文字配列の末尾に別の文字列を連結するにはstrcatを使います。
strcatの第1引数に連結先の文字配列、第2引数に連結する文字列を指定します。

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

int main(void) {
    char s1[100] = "Good";
    strcat(s1, " Night");  // s1に"Night"を連結する
    printf("%s\n", s1);  // Good Night
    return 0;
}

strcatは連結先の文字配列の要素数を考慮しません。
そのためバッファーオーバフローを起こす可能性があります。
要素数が十分に確保できている場合にのみ使うか、代用としてsnprintf()を使うことをおすすめします。

文字列をコピーする: strcpyの使い方

文字配列に別の文字列をコピーする関数がstrcpyです。
strcpy()の第1引数にコピー先の文字配列、第2引数にコピーする文字列を渡します。

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

int main(void) {
    char s[100];;
    strcpy(s, "Good");  // sに"Good"をコピーする
    printf("%s\n", s);  // Good
    return 0;
}

strcpy()もコピー先の文字配列の要素数を考慮しません。
そのためバッファーオーバフローを起こす可能性があります。
そのため十分に要素数を確保できている状況で使うか、あるいはsnprintf()で代用してください。

文字列をフォーマットで作成する: snprintfの使い方

snprintfはセキュアで使い勝手のいいフォーマット関数です。
第1引数に文字配列、第2引数に文字配列のバイト数、第3引数にフォーマット、第4引数以降にフォーマットの引数を渡します。
snprintf()は第1引数の文字配列に指定のフォーマットで文字列をコピーします。

#include <stdio.h>

int main(void) {
    char s[100];
    snprintf(s, sizeof s, "%s and %s", "Cat", "Dog");
    printf("%s\n", s);  // Cat and Dog
    return 0;
}

前述のstrcat()strcpy()はこのsnprintf()で代用することができます。
安全なプログラムを作りたい場合はこのようにsnprintf()の使用を検討してください。
ただしsnprintf()も第2引数のバイト数が間違っていた場合はバッファーオーバーフローを起こす可能性があります。
その点は注意が必要です。

文字列を読み込む: scanf, fgetsの使い方

標準入力から文字列を読み込む関数がscanf()fgets()です。

scanf()関数の使い方

scanf()は↓のようにフォーマットを指定して文字列を配列に読み込みます。
scanf()はエラーや入力がEOFに達した場合はEOFを返します。

#include <stdio.h>

int main(void) {
    char s[100];
    scanf("%s", s);  // 入力を読み込みsに保存
    printf("%s\n", s);
    return 0;
}

scanf関数は扱いが難しい関数として知られています。
そのためバグやセキュリティホールの温床になりやすい関数です。
うまく扱うには神経を使う必要があるため、安全なプログラムを作りたい場合はfgets()の使用を検討してください。

しかし競技プログラミングではよく使われているようです。
フォーマットを指定すれば色々なデータを入力から読み取れるため、便利だからです。
安全性と利便性を考えてお使いください。

fgets()関数の使い方

fgets()は標準入力から文字列を1行読み込み文字配列に保存する関数です。
第1引数に文字配列、第2引数に文字配列のバイト数、第3引数にファイルポインタを渡します。
fgets()はファイルポインタがEOFに達した場合はNULLを返します。
EOFに達していない場合は第1引数の文字配列へのポインタを返します。

#include <stdio.h>

int main(void) {
    char s[100];
    fgets(s, sizeof s, stdin);  // 標準入力(stdin)から一行読み込み
    printf("[%s]\n", s);
    return 0;
}

fgets()は保存する文字配列のバイト数を指定できるため安全性が高い関数です。
fgets()の読み込みは一行単位で行われますが、そのとき文字配列には改行も一緒に保存されます。

おわりに

今回はC言語の文字列の扱いについて見てみました。
C言語の文字列は文字配列と文字列定数に分かれています。
これらの特徴を把握して使い分けることが大事と言えます。

また文字列関数も複数ありますが、これらに中には安全性の低い関数も含まれているため、シーンに応じて使い分ける必要があります。
文字列を習得して快適なC言語ライフを送りましょう。

文字列を制する者は

C言語を制する