【C言語】文字列比較をマスター!strcmp関数の完全ガイド
C言語でプログラミングをしていると、必ずと言って良いほど文字列を扱う機会があります。そして、文字列を扱う上で非常に重要になるのが「文字列の比較」です。ファイル名が一致するか、コマンドの引数が正しいか、ユーザーの入力したパスワードが登録されたものと一致するか――これらすべてに文字列比較が必要です。
C言語の標準ライブラリには、この文字列比較を行うための強力な関数が用意されています。それが今回ご紹介するstrcmp
関数です。
「文字列比較なんて簡単でしょう?」と思うかもしれませんが、C言語における文字列は少し特殊です。JavaやPythonのような言語では、==
演算子を使って簡単に文字列の内容を比較できますが、C言語ではそうはいきません。C言語の文字列は文字の配列であり、変数に格納されているのはその配列の先頭アドレス(ポインタ)であることが多いため、==
演算子で比較すると、比較しているのは文字列の内容そのものではなく、文字列が格納されているメモリのアドレスになってしまうからです。
つまり、たとえ全く同じ内容の文字列であっても、それぞれ異なるメモリ領域に格納されていれば、==
演算子での比較は「偽(false)」となります。
“`c
include
include
int main() {
char str1[] = “hello”; // 配列として定義
char str2[] = “hello”; // 別の配列として定義
char str3 = “hello”; // 文字列リテラル(多くの場合、読み取り専用領域)
char str4 = “hello”; // str3と同じ文字列リテラルを指す可能性(実装依存)
printf("str1: %p\n", (void*)str1);
printf("str2: %p\n", (void*)str2);
printf("str3: %p\n", (void*)str3);
printf("str4: %p\n", (void*)str4);
// == 演算子での比較(アドレス比較になる)
if (str1 == str2) {
printf("str1 == str2 (アドレスが同じ)\n"); // これは通常偽
} else {
printf("str1 != str2 (アドレスが異なる)\n"); // これは通常真
}
if (str3 == str4) {
printf("str3 == str4 (アドレスが同じ)\n"); // これは真になる可能性が高い(コンパイラの最適化による)
} else {
printf("str3 != str4 (アドレスが異なる)\n"); // これは偽になる可能性も(稀だが)
}
// strcmp 関数での比較(内容比較になる)
if (strcmp(str1, str2) == 0) {
printf("strcmp(str1, str2) == 0 (内容が同じ)\n"); // これは真
} else {
printf("strcmp(str1, str2) != 0 (内容が異なる)\n");
}
if (strcmp(str3, str4) == 0) {
printf("strcmp(str3, str4) == 0 (内容が同じ)\n"); // これは真
} else {
printf("strcmp(str3, str4) != 0 (内容が異なる)\n");
}
return 0;
}
**実行例:**
(アドレスの値は環境によって異なります)
str1: 0x7ffee51c8980
str2: 0x7ffee51c8986
str3: 0x103114f8b
str4: 0x103114f8b
str1 != str2 (アドレスが異なる)
str3 == str4 (アドレスが同じ)
strcmp(str1, str2) == 0 (内容が同じ)
strcmp(str3, str4) == 0 (内容が同じ)
“`
この例からわかるように、C言語で文字列の「内容」を比較するには、専用の関数を使う必要があるのです。そのための最も基本的で重要な関数がstrcmp
なのです。
この記事では、C言語初心者の方から、さらに深く理解したい中級者の方までを対象に、strcmp
関数について徹底的に解説します。
strcmp
関数の基本的な使い方と戻り値の意味strcmp
関数が内部で行っていること(比較のメカニズム)- 具体的な使用例とサンプルコード
- 使用上の注意点とよくある落とし穴
- 関連する文字列比較関数(
strncmp
など) strcmp
関数の簡単な実装例(理解を深めるために)- 実践的な応用例
この記事を読み終える頃には、あなたはC言語の文字列比較、特にstrcmp
関数を完全にマスターしていることでしょう。それでは、さっそく始めましょう!
1. strcmp
関数の基本
まずは、strcmp
関数がどのようなもので、どう使うのかを見ていきます。
1.1. 関数のプロトタイプ
strcmp
関数は、標準Cライブラリの <string.h>
ヘッダーファイルに含まれています。そのプロトタイプ(関数宣言)は以下のようになっています。
c
int strcmp(const char *s1, const char *s2);
int
: この関数が返す値の型です。比較結果を示す整数値が返されます。strcmp
: 関数の名前です。 “String Compare” の略ですね。const char *s1
: 最初の文字列へのポインタです。const
キーワードが付いているのは、関数がこの文字列の内容を変更しないことを示しています。const char *s2
: 2番目の文字列へのポインタです。こちらも内容が変更されないことを示しています。
このプロトタイプから、strcmp
関数は2つの文字列(正確には、文字列の先頭を指すポインタ)を引数にとり、比較結果を整数値で返す関数であることがわかります。
1.2. ヘッダーファイル
strcmp
関数を使うためには、プログラムの冒頭で <string.h>
ヘッダーファイルをインクルードする必要があります。
“`c
include
“`
これを忘れると、コンパイル時に「strcmp
関数が定義されていません」といったエラーが発生します。
1.3. 戻り値の意味
strcmp
関数の戻り値は整数値ですが、この値が比較結果を示します。具体的には以下の3つの可能性があります。
s1
<s2
の場合: 負の値- 最初の文字列
s1
が、2番目の文字列s2
より辞書順で前にある場合です。
- 最初の文字列
s1
==s2
の場合: ゼロ- 2つの文字列
s1
とs2
が、バイト単位で完全に一致する場合です。
- 2つの文字列
s1
>s2
の場合: 正の値- 最初の文字列
s1
が、2番目の文字列s2
より辞書順で後ろにある場合です。
- 最初の文字列
ここで言う「辞書順」とは、各文字の文字コードの値に基づいた順序のことです。例えば、ASCIIコードでは ‘A’ (65) < ‘B’ (66) < ‘a’ (97) < ‘b’ (98) という順序になります。文字列の比較は、先頭から1文字ずつこの文字コードに基づいて行われ、最初に異なる文字が見つかった時点でその文字の大小関係が文字列全体の大小関係となります。
重要なのは、戻り値が具体的に -1
や 1
になるとは限らないということです。標準では単に「負の値」「ゼロ」「正の値」と規定されています。多くの実装では、最初に異なる文字が見つかったときの s1
の文字コードと s2
の文字コードの差(s1
の文字コード – s2
の文字コード)が返されることが多いですが、これに依存したコードを書くべきではありません。戻り値がゼロかどうか、あるいはゼロより小さいか大きいか、だけで判定するようにしましょう。
1.4. 基本的な使い方(サンプルコード)
それでは、実際のコードでstrcmp
関数の使い方を見てみましょう。
“`c
include
include // strcmp 関数を使うために必要
int main() {
char str1[] = “apple”;
char str2[] = “apple”;
char str3[] = “banana”;
char str4[] = “Apple”; // 大文字を含む
int result;
// 例 1: 完全に一致する場合
result = strcmp(str1, str2);
if (result == 0) {
printf("'%s' と '%s' は一致します。\n", str1, str2);
} else if (result < 0) {
printf("'%s' は '%s' より辞書順で前です。\n", str1, str2);
} else { // result > 0
printf("'%s' は '%s' より辞書順で後ろです。\n", str1, str2);
}
// 出力: 'apple' と 'apple' は一致します。
// 例 2: 最初の文字列が辞書順で前の場合
result = strcmp(str1, str3);
if (result == 0) {
printf("'%s' と '%s' は一致します。\n", str1, str3);
} else if (result < 0) {
printf("'%s' は '%s' より辞書順で前です。\n", str1, str3);
} else { // result > 0
printf("'%s' は '%s' より辞書順で後ろです。\n", str1, str3);
}
// 出力: 'apple' は 'banana' より辞書順で前です。
// 例 3: 最初の文字列が辞書順で後ろの場合
result = strcmp(str3, str1);
if (result == 0) {
printf("'%s' と '%s' は一致します。\n", str3, str1);
} else if (result < 0) {
printf("'%s' は '%s' より辞書順で前です。\n", str3, str1);
} else { // result > 0
printf("'%s' は '%s' より辞書順で後ろです。\n", str3, str1);
}
// 出力: 'banana' は 'apple' より辞書順で後ろです。
// 例 4: 大文字・小文字の違い
// ASCIIでは 'A' < 'a' なので 'Apple' は 'apple' より前になる
result = strcmp(str4, str1);
if (result == 0) {
printf("'%s' と '%s' は一致します。\n", str4, str1);
} else if (result < 0) {
printf("'%s' は '%s' より辞書順で前です。\n", str4, str1);
} else { // result > 0
printf("'%s' は '%s' より辞書順で後ろです。\n", str4, str1);
}
// 出力: 'Apple' は 'apple' より辞書順で前です。
return 0;
}
“`
このコードを実行すると、それぞれの比較結果に応じたメッセージが表示されます。特に重要なのは、文字列が一致するかどうかの判定には strcmp(s1, s2) == 0
を使うという点です。また、辞書順での大小関係を判定するには、戻り値が負か正かを確認します。
2. strcmp
関数の詳細な挙動
strcmp
関数がどのように文字列を比較しているのか、その内部の挙動を詳しく見ていきましょう。
2.1. 比較のメカニズム:バイト単位の辞書順比較
strcmp
関数は、与えられた2つの文字列を、先頭の文字から順番に1バイトずつ比較していきます。比較は、対応する文字の文字コード(バイト値)に基づいて行われます。
例えば、ASCIIコードを使用している環境で “apple” と “apricot” を比較する場合を考えてみましょう。
- 1文字目: ‘a’ と ‘a’ -> ASCIIコードはどちらも 97。一致。
- 2文字目: ‘p’ と ‘p’ -> ASCIIコードはどちらも 112。一致。
- 3文字目: ‘p’ と ‘r’ -> ‘p’ の ASCIIコードは 112、’r’ の ASCIIコードは 114。ここで異なる文字が見つかりました。
- 112 < 114 なので、’p’ は ‘r’ より小さい(辞書順で前)と判断されます。
ここで比較は終了し、strcmp("apple", "apricot")
の戻り値は負の値となります。これは “apple” が “apricot” より辞書順で前であることを示しています。
このように、strcmp
関数は文字列全体を一度に見るのではなく、先頭から順に異なる文字が現れるまで、あるいは文字列の終端に達するまで、1バイトずつ比較を続けます。
2.2. ヌル終端文字 (\0
) の重要性
C言語の文字列は、特別な文字であるヌル終端文字 (\0
) で終わるという規則があります。strcmp
関数は、このヌル終端文字を文字列の終わりを示すマーカーとして利用します。
比較は、どちらかの文字列のヌル終端文字に達するまで続けられます。ヌル終端文字のバイト値は 0 です。この 0 という値は、どの印字可能文字や一般的な制御文字よりも小さいため、比較において重要な役割を果たします。
例: “apple” と “app” を比較する場合
- 1文字目: ‘a’ と ‘a’ -> 一致 (97 == 97)
- 2文字目: ‘p’ と ‘p’ -> 一致 (112 == 112)
- 3文字目: ‘p’ と ‘p’ -> 一致 (112 == 112)
- 4文字目: “apple” は ‘l’、”app” は
\0
。ここで異なる文字が見つかりました。- ‘l’ の ASCIIコードは 108、
\0
のバイト値は 0。 - 108 > 0 なので、’l’ は
\0
より大きいと判断されます。
- ‘l’ の ASCIIコードは 108、
したがって、strcmp("apple", "app")
の戻り値は正の値となります。これは “apple” が “app” より辞書順で後ろであることを意味します。辞書順では、短い方の文字列が長い方の文字列と完全に一致する場合、短い方が前になります。strcmp
はこの規則に則って動作します。
比較が終了する条件は以下のいずれかです。
- 異なる文字が見つかった場合: その文字のバイト値の大小で結果が決まります。
- どちらかの文字列のヌル終端文字に達した場合:
- 両方の文字列が同時にヌル終端に達した場合、文字列は完全に一致しており、戻り値はゼロです。
- 片方の文字列がヌル終端に達し、もう一方がまだヌル終端でない(つまり、もう一方の文字列がより長い)場合、ヌル終端に達した側の文字列の方が辞書順で前と判断され、戻り値は負の値になります。(例: “app” と “apple” を比較する場合、”app” は先に
\0
に達するので、strcmp("app", "apple")
は負の値になります。)
2.3. 戻り値の具体的な値について
前述の通り、strcmp
関数の戻り値は「負の値」「ゼロ」「正の値」のいずれかです。多くのC標準ライブラリの実装では、最初に異なるバイトが見つかったときに s1
のバイト値から s2
のバイト値を引いた差が返されます。例えば、”apple” と “apricot” の比較では、3文字目で ‘p’ (112) と ‘r’ (114) が異なり、112 – 114 = -2 となるため、-2 が返されることが多いです。また、”apple” と “app” の比較では、4文字目で ‘l’ (108) と \0
(0) が異なり、108 – 0 = 108 となるため、108 が返されることが多いです。
しかし、これは標準で保証された動作ではありません。異なる実装では、単に負の値であれば -1
を、正の値であれば 1
を返すように実装されている可能性もあります。
したがって、strcmp
の戻り値を利用する際は、決して具体的な数値(-1, 1, -2, 108 など)に依存せず、必ず以下のように判定するようにしてください。
- 一致:
strcmp(s1, s2) == 0
- s1 が s2 より辞書順で前:
strcmp(s1, s2) < 0
- s1 が s2 より辞書順で後ろ:
strcmp(s1, s2) > 0
- s1 が s2 と一致しない:
strcmp(s1, s2) != 0
この原則を守ることが、移植性の高い(様々な環境で正しく動作する)コードを書く上で非常に重要です。
3. サンプルコード集
strcmp
関数の様々な使い方を、具体的なコード例で確認しましょう。
3.1. 基本的な一致・不一致の判定
これは最もよくある使い方です。
“`c
include
include
int main() {
char command[100];
printf("コマンドを入力してください (例: start, stop, status): ");
scanf("%s", command); // 注意: バッファオーバーフローの可能性あり。本来はfgetsなどを使うべき。
if (strcmp(command, "start") == 0) {
printf("サービスを開始します。\n");
} else if (strcmp(command, "stop") == 0) {
printf("サービスを停止します。\n");
} else if (strcmp(command, "status") == 0) {
printf("サービスのステータスを表示します。\n");
} else {
printf("不明なコマンドです: %s\n", command);
}
return 0;
}
``
strcmp(command, “…”) == 0` という形が、文字列の一致判定の標準的なイディオムです。
このコードは、ユーザーが入力した文字列が特定のコマンド文字列と一致するかどうかを判定しています。
3.2. 辞書順での大小比較
文字列を辞書順で並べ替える(ソートする)際などに、大小比較の結果を利用します。
“`c
include
include
int main() {
char word1[] = “zebra”;
char word2[] = “apple”;
char word3[] = “banana”;
// 3つの単語を辞書順に並べ替える(簡易版)
// 実際にはソートアルゴリズムを使います
// word1 と word2 を比較
if (strcmp(word1, word2) < 0) {
printf("'%s' は '%s' より前です。\n", word1, word2);
} else if (strcmp(word1, word2) > 0) {
printf("'%s' は '%s' より後ろです。\n", word1, word2);
} else {
printf("'%s' と '%s' は同じです。\n", word1, word2);
}
// 出力: 'zebra' は 'apple' より後ろです。
// word2 と word3 を比較
if (strcmp(word2, word3) < 0) {
printf("'%s' は '%s' より前です。\n", word2, word3);
} else if (strcmp(word2, word3) > 0) {
printf("'%s' は '%s' より後ろです。\n", word2, word3);
} else {
printf("'%s' と '%s' は同じです。\n", word2, word3);
}
// 出力: 'apple' は 'banana' より前です。
return 0;
}
``
strcmp`の戻り値が負か正かを利用することで、文字列の辞書順での前後関係を判定できます。
このように、
3.3. 配列とポインタを使った比較
strcmp
関数は const char *
型の引数をとるため、char
配列でも char
ポインタでも、ヌル終端された文字列であればそのまま渡して比較できます。
“`c
include
include
int main() {
char array_str[] = “example”;
char *pointer_str = “example”; // 文字列リテラルはポインタで受け取るのが一般的
// 配列とポインタの比較
if (strcmp(array_str, pointer_str) == 0) {
printf("配列で定義した文字列とポインタで指す文字列は一致します。\n");
} else {
printf("一致しません。\n");
}
// 出力: 配列で定義した文字列とポインタで指す文字列は一致します。
// 文字列リテラル同士の比較
if (strcmp("test", "test") == 0) {
printf("文字列リテラル同士は一致します。\n");
} else {
printf("一致しません。\n");
}
// 出力: 文字列リテラル同士は一致します。
return 0;
}
``
strcmp`関数にとって重要なのは、引数がヌル終端された文字列の先頭を指しているかどうかだけです。文字列が配列として確保されているか、ポインタとして文字列リテラルを指しているか、動的に確保されたメモリ領域を指しているか、といった違いは関数の動作に影響しません。
3.4. ユーザー入力との比較
scanf
やfgets
などで読み込んだユーザー入力を特定の文字列と比較する例です。scanf
は安全ではない場合があるため、ここではより安全なfgets
を使った例を示します。fgets
は改行文字も読み込むため、比較前に改行文字を取り除く処理が必要になることが多い点に注意が必要です。
“`c
include
include
int main() {
char input[100];
printf("合言葉を入力してください: ");
// fgetsで標準入力から最大99文字+ヌル終端を読み込む
if (fgets(input, sizeof(input), stdin) != NULL) {
// fgetsは改行文字も読み込むため、末尾の改行があれば削除する
size_t len = strlen(input);
if (len > 0 && input[len - 1] == '\n') {
input[len - 1] = '\0';
}
if (strcmp(input, "secret_password") == 0) {
printf("認証成功!\n");
} else {
printf("認証失敗!\n");
}
} else {
// エラー処理(fgetsが失敗した場合)
perror("入力読み込みエラー");
}
return 0;
}
``
fgets
この例では、で入力を受け取り、末尾の改行文字を削除してから
strcmp`で比較しています。実際のアプリケーションでは、ユーザー入力の長さ制限や改行文字の処理などが重要になります。
4. strcmp
関数を使う上での注意点
strcmp
関数は便利ですが、いくつか注意すべき点があります。これらの点を理解していないと、予期せぬバグやセキュリティ上の問題につながる可能性があります。
4.1. NULLポインタを渡した場合の挙動 (未定義動作)
strcmp
関数に NULL
ポインタを渡すと、未定義動作 (Undefined Behavior) となります。これは、C標準がその場合の動作を規定していないことを意味します。プログラムがクラッシュしたり、予期しない結果になったり、あるいは表面上は問題なく動作するように見えても、将来的に問題を引き起こす可能性があります。
“`c
include
include
int main() {
char str1 = “hello”;
char str2 = NULL; // NULLポインタ
// この呼び出しは未定義動作を引き起こします!
// コンパイラによっては警告が出ることもありますが、必須ではありません。
int result = strcmp(str1, str2);
// 未定義動作なので、ここから先の実行は保証されません。
// 安全なコードでは、引数がNULLでないことを確認する必要があります。
if (str1 != NULL && str2 != NULL) {
result = strcmp(str1, str2);
// result の値を使って比較結果を判定
} else {
// NULLポインタが含まれている場合の適切な処理
fprintf(stderr, "エラー: 文字列ポインタがNULLです。\n");
// NULLポインタが含まれている場合は、一致しないとみなすなど、
// アプリケーションの仕様に応じた処理を行う
}
return 0;
}
``
strcmp関数を呼び出す前に、引数として渡すポインタが有効な文字列を指している(つまり、
NULL`でない)ことを必ず確認してください。
4.2. ヌル終端されていない文字列
strcmp
関数は、文字列がヌル終端されていることを前提としています。もしヌル終端されていない文字配列を渡すと、strcmp
関数はメモリ上のどこかにあるヌル終端文字を見つけるまで、指定されたメモリ領域を越えて比較を続けようとします。これはバッファオーバーリード (Buffer Over-read) と呼ばれる問題を引き起こし、プログラムのクラッシュやセキュリティ上の脆弱性につながる可能性があります。
“`c
include
include
int main() {
char buffer[5] = {‘h’, ‘e’, ‘l’, ‘l’, ‘o’}; // <– ヌル終端されていない!
char *str = “hello”;
// 未定義動作(バッファオーバーリード)を引き起こす可能性が高い
// strcmp は buffer の後続メモリも読み込もうとする
// result は何になるか不明、クラッシュする可能性も
int result = strcmp(buffer, str);
printf("Result (unpredictable): %d\n", result);
return 0;
}
``
strncpy
C言語で文字列を扱う際は、常に文字列がヌル終端されていることを保証する必要があります。特に、他のライブラリや外部からの入力(ファイルやネットワークなど)から文字列を受け取る場合は、長さを指定して安全にコピーする (や
snprintf`などを使用) と同時に、明示的にヌル終端を追加するなどの対策が必要です。
ヌル終端されていない文字列を、長さを指定して安全に比較したい場合は、後述する strncmp
関数を使用します。
4.3. 大文字・小文字を区別する (case-sensitive)
前述の例でも触れましたが、strcmp
関数はバイト値に基づいて比較を行うため、大文字と小文字を区別します。例えば、”Apple” と “apple” は異なる文字列と判断されます(ASCIIでは ‘A’ < ‘a’ なので、”Apple” が “apple” より辞書順で前になります)。
大文字・小文字を区別せずに文字列を比較したい場合は、strcmp
関数は適していません。このような場合は、後述する strcasecmp
(あるいは stricmp
) といった関数を使用する必要があります。これらの関数は標準Cライブラリには含まれていませんが、多くのシステム(POSIXシステム、Windowsなど)で提供されています。標準Cだけで実現する場合は、比較する前に両方の文字列を全て大文字または小文字に変換する処理を自前で行う必要があります。
4.4. ロケールに依存しない(バイト値に基づいた比較)
strcmp
関数は、文字のバイト値(文字コード)に基づいて比較を行います。これは、プログラムが実行されているシステムの設定(ロケール)に依存しません。
ほとんどの場合、ASCII文字や基本的な英数字を比較する分には問題ありません。しかし、日本語のようなマルチバイト文字や、ウムラウトなどのアクセント記号が付いた文字を比較する場合、strcmp
の結果が人間の感覚による辞書順と一致しないことがあります。例えば、ドイツ語の「ä」と「a」の比較順序は、ロケールによって異なることがありますが、strcmp
はバイト値で機械的に比較するだけです。
ロケールを考慮した、人間が期待するような文字列の比較を行うには、標準ライブラリの strcoll
関数や、より高度なライブラリ(ICUなど)を使用する必要があります。
5. 関連する文字列比較関数
strcmp
以外にも、C言語には文字列比較に関連する便利な関数がいくつかあります。目的に応じてこれらの関数を使い分けることが重要です。
5.1. strncmp
: 指定した長さだけ比較
strncmp
関数は、strcmp
関数と似ていますが、比較する文字列の最大バイト数を指定できる点が異なります。
- プロトタイプ:
int strncmp(const char *s1, const char *s2, size_t n);
- ヘッダーファイル:
<string.h>
- 引数:
s1
,s2
: 比較するヌル終端文字列へのポインタ。n
: 比較する最大バイト数。
- 戻り値:
strcmp
と同じ(負の値、ゼロ、正の値)ですが、比較が最大n
バイト行われた時点で結果が決まります。
strncmp
は、以下の条件のいずれかが満たされた時点で比較を終了し、結果を返します。
s1
とs2
の中で、比較中の位置で異なる文字が見つかった場合。- 比較が
n
バイト目に達した場合。 - どちらかの文字列のヌル終端文字 (
\0
) に達した場合。
strncmp
の利点は、固定長のバッファに入った文字列や、文字列の一部分だけを比較したい場合に安全に使用できることです。特に、ヌル終端されているか不明なバッファを扱う際には、バッファサイズを n
として指定することで、バッファオーバーリードを防ぐことができます。
使いどころ:
- 入力バッファのサイズが決まっている場合(最大
n
バイトだけ比較する)。 - 文字列の先頭部分が特定のプレフィックスと一致するか確認したい場合。
- ヌル終端されていない可能性があるバッファを安全に比較したい場合。
サンプルコード:
“`c
include
include
int main() {
char str1[] = “hello world”;
char str2[] = “hello universe”;
char str3[] = “hell”;
char buffer[5] = {‘h’, ‘e’, ‘l’, ‘l’, ‘o’}; // ヌル終端なし
// str1 と str2 を最初の5文字だけ比較
if (strncmp(str1, str2, 5) == 0) {
printf("'%s' と '%s' の最初の5文字は一致します。\n", str1, str2);
} else {
printf("'%s' と '%s' の最初の5文字は一致しません。\n", str1, str2);
}
// 出力: 'hello world' と 'hello universe' の最初の5文字は一致します。
// str1 と str3 を比較
// strncmp(str1, str3, strlen(str3)) は strcmp(str1, str3) と同じ挙動になりがち
// str3 が短いので、str3のヌル終端で比較が終わる
if (strncmp(str1, str3, strlen(str3)) == 0) { // strlen("hell") == 4
printf("'%s' は '%s' で始まります。\n", str1, str3);
} else {
printf("'%s' は '%s' で始まりません。\n", str1, str3);
}
// 出力: 'hello world' は 'hell' で始まります。
// strncmp を使ってヌル終端されていないバッファを安全に比較
// strncmp(buffer, str3, 4) で比較
if (strncmp(buffer, str3, 4) == 0) {
printf("bufferの最初の4バイトと'%s'は一致します。\n", str3);
} else {
printf("bufferの最初の4バイトと'%s'は一致しません。\n", str3);
}
// 出力: bufferの最初の4バイトと'hell'は一致します。
// buffer と str1 を5バイト比較 - bufferは5バイト目がヌル終端でない
// str1は5バイト目が'o' (ASCII 111)、bufferは5バイト目が'o' (ASCII 111)
// 6バイト目でstr1は' ' (ASCII 32)、bufferはヌル終端文字がないためメモリを読み進む
// これは strncmp の安全な使い方ではない(bufferの長さより大きいnを指定している)
// この例は strncmp が n バイトまで比較しようとする性質を示す
if (strncmp(buffer, str1, 5) == 0) {
printf("bufferとstr1の最初の5バイトは一致します(ただしbufferはヌル終端なし)。\n");
} else {
printf("bufferとstr1の最初の5バイトは一致しません。\n");
}
// bufferの最初の5バイトは 'h', 'e', 'l', 'l', 'o'。 str1の最初の5バイトも 'h', 'e', 'l', 'l', 'o'。
// なので一致と判定される。しかしこれは buffer の定義の仕方が悪い例。
// 安全な strncmp の使い方: n は常に比較したい文字列の短い方の長さ「以下」または「安全なバッファサイズ」とする。
// char safe_buffer[5] = {'h', 'e', 'l', 'l', 'o'}; // ヌル終端なし
// char safe_str[] = "hello"; // ヌル終端あり
// strncmp(safe_buffer, safe_str, sizeof(safe_buffer)); // safe_bufferのサイズまで比較
// -> ヌル終端がないので、'o'と'\0'が比較される。'o' > '\0' なので正の値。
return 0;
}
``
strncmpを使用する際は、第3引数
nの値に注意が必要です。
nは「最大バイト数」であり、そのバイト数に到達する前にどちらかの文字列がヌル終端された場合は、比較はそこで終了します。したがって、
strncmp(s1, s2, n)は、
s1の先頭から
min(strlen(s1), strlen(s2), n)` バイトまでを比較すると考えることができます。
5.2. strcasecmp
, strncasecmp
(POSIX/GNU拡張など)
大文字・小文字を区別せずに文字列を比較したい場合は、strcasecmp
関数(ヌル終端文字列全体を比較)や strncasecmp
関数(指定した長さだけ比較)が便利です。
- プロトタイプ:
int strcasecmp(const char *s1, const char *s2);
int strncasecmp(const char *s1, const char *s2, size_t n);
- ヘッダーファイル: 多くの場合
<strings.h>
または<string.h>
- 注意点: これらは標準Cライブラリ関数ではありません。POSIX標準 (Unix系システム) やGNU拡張として提供されていることが多く、Windowsでは
_stricmp
,_strnicmp
といった名前で提供されています。移植性を考慮する場合は、これらの関数の存在を#ifdef
などでチェックするか、自前で実装する必要があります。 - 挙動: 基本的には
strcmp
やstrncmp
と同じですが、比較の際に各文字を一時的に同じケース(通常は小文字)に変換してからバイト値比較を行います。
サンプルコード (POSIX準拠システムの場合):
“`c
include
include
include // strcasecmp のために必要(システムによる)
int main() {
char str1[] = “Apple”;
char str2[] = “apple”;
char str3[] = “APRICOT”;
// strcasecmp による大文字・小文字を区別しない比較
if (strcasecmp(str1, str2) == 0) {
printf("'%s' と '%s' は大文字・小文字を区別しなければ一致します。\n", str1, str2);
} else {
printf("'%s' と '%s' は大文字・小文字を区別しても一致しません。\n", str1, str2);
}
// 出力: 'Apple' と 'apple' は大文字・小文字を区別しなければ一致します。
// strncasecmp による大文字・小文字を区別しない、長さ指定比較
if (strncasecmp(str1, str3, 5) == 0) {
printf("'%s' と '%s' の最初の5文字は、大文字・小文字を区別しなければ一致します。\n", str1, str3);
} else {
printf("'%s' と '%s' の最初の5文字は、大文字・小文字を区別しても一致しません。\n", str1, str3);
}
// str1: "Apple", str3: "APRICOT"
// 最初の5文字: "Apple" vs "APRAC" (変換後) -> 'p' と 'r' で異なる
// 'p'(112) < 'r'(114) なので負の値が返る
// 正しい期待値: 'Apple' と 'APRIC' の最初の5文字を大文字小文字区別せず比較
// "apple" vs "apric" -> 'p' vs 'r' -> 'p' < 'r' なので負の値
// 出力例: 'Apple' と 'APRICOT' の最初の5文字は、大文字・小文字を区別しても一致しません。
// 正しいstrncasecmpの例
char str4[] = "APPLICATION";
if (strncasecmp(str1, str4, 3) == 0) { // "App" vs "APP" -> 大文字小文字区別せず一致
printf("'%s' と '%s' の最初の3文字は、大文字・小文字を区別しなければ一致します。\n", str1, str4);
} else {
printf("'%s' と '%s' の最初の3文字は、大文字・小文字を区別しても一致しません。\n", str1, str4);
}
// 出力: 'Apple' と 'APPLICATION' の最初の3文字は、大文字・小文字を区別しなければ一致します。
return 0;
}
“`
これらの関数は便利ですが、標準Cではないため、クロスプラットフォーム開発では注意が必要です。
5.3. memcmp
: 任意のバイト列を比較
memcmp
関数は、文字列に限らず、任意のメモリ領域のバイト列を比較します。
- プロトタイプ:
int memcmp(const void *s1, const void *s2, size_t n);
- ヘッダーファイル:
<string.h>
- 引数:
s1
,s2
: 比較するメモリ領域へのポインタ (void*
なのであらゆる型のポインタを受け取れる)。n
: 比較するバイト数。
- 戻り値:
strcmp
と同じ(負の値、ゼロ、正の値)ですが、これは指定されたn
バイトの領域のバイト値比較の結果です。ヌル終端は特別な意味を持ちません。
memcmp
は、バイナリデータの比較や、文字列のヌル終端を無視して特定のバイト数だけを比較したい場合に有効です。
strcmp
との違い:
strcmp
はヌル終端文字 (\0
) を文字列の終わりとして認識し、その文字を含む(あるいはその前までの)比較を行います。memcmp
はn
バイトを厳密に比較し、ヌル終端文字が現れても比較を止めません。strcmp
はconst char *
型のポインタを受け取ります。memcmp
はconst void *
型のポインタを受け取るため、文字列以外のデータ(例えば構造体や整数配列など)も比較できます。
サンプルコード:
“`c
include
include
int main() {
char str1[] = “abc”;
char str2[] = “abcd”;
char str3[] = “xbc”;
// strcmp vs memcmp
printf("strcmp(\"abc\", \"abcd\"): %d\n", strcmp(str1, str2)); // 'a', 'b', 'c', '\0' vs 'a', 'b', 'c', 'd', '\0' -> '\0' < 'd' なので負
printf("memcmp(\"abc\", \"abcd\", 3): %d\n", memcmp(str1, str2, 3)); // 'a', 'b', 'c' vs 'a', 'b', 'c' -> 一致なので 0
printf("memcmp(\"abc\", \"abcd\", 4): %d\n", memcmp(str1, str2, 4)); // 'a', 'b', 'c', '\0' vs 'a', 'b', 'c', 'd' -> '\0' < 'd' なので負 (strcmpと同じ結果)
printf("strcmp(\"abc\", \"xbc\"): %d\n", strcmp(str1, str3)); // 'a' vs 'x' -> 'a' < 'x' なので負
printf("memcmp(\"abc\", \"xbc\", 3): %d\n", memcmp(str1, str3, 3)); // 'a' vs 'x' -> 'a' < 'x' なので負 (strcmpと同じ結果)
// バイナリデータの比較
unsigned char data1[] = { 0x01, 0x02, 0xFF, 0x10 };
unsigned char data2[] = { 0x01, 0x02, 0xFF, 0x10 };
unsigned char data3[] = { 0x01, 0x02, 0xFE, 0x10 };
if (memcmp(data1, data2, sizeof(data1)) == 0) {
printf("data1 と data2 (バイト列) は一致します。\n");
} else {
printf("data1 と data2 (バイト列) は一致しません。\n");
} // 出力: data1 と data2 (バイト列) は一致します。
if (memcmp(data1, data3, sizeof(data1)) == 0) {
printf("data1 と data3 (バイト列) は一致します。\n");
} else {
printf("data1 と data3 (バイト列) は一致しません。\n");
} // 出力: data1 と data3 (バイト列) は一致しません。
return 0;
}
``
memcmpは文字列以外の比較に非常に便利ですが、文字列として扱う場合は
strcmpや
strncmpの方が意図が明確になりやすいでしょう。また、
memcmp`はヌル終端を無視して指定バイト数まで比較するため、ヌル文字が含まれる「バイナリ文字列」のようなものを特定の長さで比較するのにも使えます。
6. strcmp
関数の実装例(理解を深めるために)
strcmp
関数が内部でどのように動作しているかを理解するために、簡単な実装例を見てみましょう。これは標準ライブラリの実際のコードそのままではありませんが、そのコアとなるロジックを示しています。
6.1. シンプルなループによる実装
最も分かりやすいのは、先頭から順番に文字を比較していくループ構造です。
“`c
include // デバッグ出力用
include // size_t 用 (本来strcmpはsize_t不要だが、一般的な実装構造を示すため)
// strncmp のシンプルな実装例 (strcmp の理解にも役立つ)
// n バイトまで比較するか、ヌル終端か、異なる文字が見つかるまで
int my_strncmp(const char s1, const char s2, size_t n) {
size_t i = 0;
while (i < n) {
unsigned char c1 = (unsigned char)s1[i]; // 文字コードを取得 (負の値にならないようにunsigned charにキャスト)
unsigned char c2 = (unsigned char)s2[i]; // 文字コードを取得
if (c1 != c2) {
// 異なる文字が見つかったら、その差を返す
return c1 - c2;
}
if (c1 == '\0') {
// 片方または両方がヌル終端に達したら比較終了
// ここに来るのは c1 == c2 == '\0' の場合のみ
return 0; // 一致
}
i++; // 次の文字へ
}
// n バイトまで比較し終えた場合、それまでのバイトは全て一致
return 0; // n バイトの範囲では一致
}
// strcmp のシンプルな実装例
// strncmp(s1, s2, 無限大) のようなもの
int my_strcmp(const char s1, const char s2) {
while (1) { // 無限ループ
unsigned char c1 = (unsigned char)s1; // ポインタが指す文字のバイト値
unsigned char c2 = (unsigned char)s2; // ポインタが指す文字のバイト値
if (c1 != c2) {
// 異なる文字が見つかったら、その差を返す
return c1 - c2;
}
if (c1 == '\0') {
// ヌル終端文字に達したら比較終了
// ここに来るのは c1 == c2 == '\0' の場合のみ
return 0; // 完全に一致
}
s1++; // ポインタを進める
s2++; // ポインタを進める
}
// ここには到達しない(return 文で必ず終了するため)
}
int main() {
char str1[] = “hello”;
char str2[] = “hello”;
char str3[] = “world”;
char str4[] = “hell”;
printf("my_strcmp(\"%s\", \"%s\"): %d\n", str1, str2, my_strcmp(str1, str2)); // 0 を返す
printf("my_strcmp(\"%s\", \"%s\"): %d\n", str1, str3, my_strcmp(str1, str3)); // 'h' vs 'w' -> 'h' < 'w' なので負を返す
printf("my_strcmp(\"%s\", \"%s\"): %d\n", str3, str1, my_strcmp(str3, str1)); // 'w' vs 'h' -> 'w' > 'h' なので正を返す
printf("my_strcmp(\"%s\", \"%s\"): %d\n", str1, str4, my_strcmp(str1, str4)); // 'o' vs '\0' -> 'o' > '\0' なので正を返す
printf("my_strcmp(\"%s\", \"%s\"): %d\n", str4, str1, my_strcmp(str4, str1)); // '\0' vs 'o' -> '\0' < 'o' なので負を返す
printf("my_strncmp(\"%s\", \"%s\", 4): %d\n", str1, str3, my_strncmp(str1, str3, 4)); // "hell" vs "worl" -> 'h' vs 'w' -> 負
printf("my_strncmp(\"%s\", \"%s\", 5): %d\n", str1, str3, my_strncmp(str1, str3, 5)); // "hello" vs "world" -> 'h' vs 'w' -> 負
printf("my_strncmp(\"%s\", \"%s\", 4): %d\n", str1, str4, my_strncmp(str1, str4, 4)); // "hell" vs "hell" -> 一致 -> 0
printf("my_strncmp(\"%s\", \"%s\", 5): %d\n", str1, str4, my_strncmp(str1, str4, 5)); // "hello" vs "hell\0" -> 'o' vs '\0' -> 正
return 0;
}
``
my_strcmp
この関数は、2つのポインタ
s1と
s2を同時に進めながら、それぞれのポインタが指す文字を比較しています。
\0`) であれば、文字列全体が一致したと判断して 0 を返します。
* 文字が一致しない場合は、その文字のバイト値の差を計算して返します。
* 文字が一致し、かつその文字がヌル終端文字 (
* 文字が一致し、かつヌル終端文字でなければ、次の文字に進んで比較を続けます。
unsigned char
にキャストしているのは、char
型が符号付きか符号なしかが環境によって異なる場合があるためです。バイト値を数値として正確に比較するために、符号なし整数型である unsigned char
にキャストするのが一般的です。
6.2. ポインタ演算を使った慣用的な実装
C言語の標準ライブラリの実装では、より効率的なポインタ演算を使ったループがよく見られます。
“`c
include
include // strlen のために必要
int my_strcmp_ptr(const char s1, const char s2) {
while (s1 != ‘\0’ && s1 == s2) {
s1++;
s2++;
}
// ループを抜けたのは、s1 が ‘\0’ であるか、s1 != s2 のいずれかの場合
// 比較結果は、ループを抜けた時点の文字のバイト値の差
// s1 または s2 が’\0’の場合も、そのバイト値(0)が差の計算に使われる
return (unsigned char )s1 – (unsigned char )s2;
}
int my_strncmp_ptr(const char s1, const char s2, size_t n) {
size_t i = 0;
// i < n かつ、両方の文字列がヌル終端に達しておらず、かつ現在の文字が一致している間ループ
while (i < n && s1 != ‘\0’ && s1 == s2) {
s1++;
s2++;
i++;
}
// ループを抜けたのは、i == n、または s1 == ‘\0’、または s1 != s2 のいずれかの場合
if (i == n) {
// n バイトまで比較し終えた場合、そこまで一致していれば 0 を返す
// これは my_strncmp と slightly 異なる挙動。my_strncmp は n バイト目の文字も比較するが、
// このptr版は n バイト目に到達したら比較を終える。標準の strncmp は n バイト目まで比較する。
// なのでこの my_strncmp_ptr は strncmp の厳密な実装ではない。
// strncmp の標準的な実装は最初の my_strncmp 例に近い。
// 正しい strncmp のポインタ版に近い実装は以下のような形になる
// (実際には最適化で様々な形になる)
/
while (n– > 0) {
unsigned char c1 = (unsigned char)s1++;
unsigned char c2 = (unsigned char)s2++;
if (c1 != c2) {
return c1 – c2;
}
if (c1 == ‘\0’) {
break; // 片方または両方がヌル終端なら終了
}
}
return 0; // n バイト比較し終えたか、ヌル終端で終了した場合
/
// 簡単な my_strncmp_ptr では i==n の場合は一致とみなすことにする
return 0; // n バイト比較し終えた場合は一致とみなす (ただしこれは厳密には strcmp(s1, s2) とは違う)
// この実装例は理解のための簡易版であり、標準関数の厳密な再現ではないことに注意
} else {
// ヌル終端に達したか、異なる文字が見つかった場合
return *(unsigned char *)s1 - *(unsigned char *)s2;
}
}
int main() {
char str1[] = “hello”;
char str2[] = “hello”;
char str3[] = “world”;
char str4[] = “hell”;
printf("my_strcmp_ptr(\"%s\", \"%s\"): %d\n", str1, str2, my_strcmp_ptr(str1, str2)); // 0
printf("my_strcmp_ptr(\"%s\", \"%s\"): %d\n", str1, str3, my_strcmp_ptr(str1, str3)); // 負
printf("my_strcmp_ptr(\"%s\", \"%s\"): %d\n", str3, str1, my_strcmp_ptr(str3, str1)); // 正
printf("my_strcmp_ptr(\"%s\", \"%s\"): %d\n", str1, str4, my_strcmp_ptr(str1, str4)); // 正
printf("my_strcmp_ptr(\"%s\", \"%s\"): %d\n", str4, str1, my_strcmp_ptr(str4, str1)); // 負
// my_strncmp_ptr の挙動は my_strncmp と少し異なる可能性あり
printf("my_strncmp_ptr(\"%s\", \"%s\", 4): %d\n", str1, str3, my_strncmp_ptr(str1, str3, 4)); // 負
printf("my_strncmp_ptr(\"%s\", \"%s\", 5): %d\n", str1, str3, my_strncmp_ptr(str1, str3, 5)); // 負
printf("my_strncmp_ptr(\"%s\", \"%s\", 4): %d\n", str1, str4, my_strncmp_ptr(str1, str4, 4)); // 0
printf("my_strncmp_ptr(\"%s\", \"%s\", 5): %d\n", str1, str4, my_strncmp_ptr(str1, str4, 5)); // 正
return 0;
}
“`
この my_strcmp_ptr
実装では、ループ条件 *s1 != '\0' && *s1 == *s2
が満たされている間、ポインタ s1
と s2
を1バイトずつ進めています。
* ループは、s1
がヌル終端に達するか (*s1 == '\0'
)、または s1
と s2
が指す文字が一致しなくなった (*s1 != *s2
) 時点で終了します。
* ループを抜けた時点の s1
と s2
が指す文字のバイト値の差が、比較結果となります。もし両方ともヌル終端でループを抜けた場合は、'\0' - '\0'
で 0 が返され、正しく一致と判定されます。
このポインタ演算を使った実装は、インデックス変数 (i
) を使うよりもシンプルで、しばしば効率的とされます。多くのC標準ライブラリは、このようなポインタ演算を駆使したり、さらに低レベルなアセンブリ言語や、特定のCPUアーキテクチャに最適化された命令(SIMD命令など)を利用して、非常に高速な文字列操作関数を実現しています。
これらの実装例を通して、strcmp
関数が文字コードに基づいたバイト単位の比較を、ヌル終端文字が現れるか、あるいは異なる文字が見つかるまで地道に行っていることが理解できたかと思います。
7. strcmp
関数の応用例
strcmp
関数は非常に汎用性が高く、様々な場面で応用できます。ここではいくつかの応用例を紹介します。
7.1. コマンドライン引数の解析
C言語で書かれたプログラムが、実行時に受け取るコマンドライン引数(argc
, argv
)を処理する際に、特定の引数が指定されているか確認するためによく strcmp
が使われます。
“`c
include
include
int main(int argc, char *argv[]) {
int show_help = 0;
int verbose_mode = 0;
// プログラム名を除く引数をループ処理
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
show_help = 1;
} else if (strcmp(argv[i], "--verbose") == 0 || strcmp(argv[i], "-v") == 0) {
verbose_mode = 1;
} else {
printf("不明なオプション: %s\n", argv[i]);
// エラー処理やヘルプ表示など
}
}
if (show_help) {
printf("使用方法: myprogram [オプション]\n");
printf(" -h, --help ヘルプを表示\n");
printf(" -v, --verbose 詳細出力を有効に\n");
}
if (verbose_mode) {
printf("詳細モードが有効です。\n");
}
if (!show_help) {
// 通常のプログラム処理
printf("通常のプログラム処理を実行します。\n");
}
return 0;
}
``
argv
このコードでは、配列の各要素(文字列)を
–helpや
–verboseといった既知のオプション文字列と比較しています。
strcmp`の戻り値が 0 であれば、文字列が一致したと判断できます。
7.2. 文字列のソート
複数の文字列を辞書順に並べ替える(ソートする)際、比較関数として strcmp
が利用できます。例えば、C言語の標準ライブラリ関数 qsort
は、要素間の比較を行うための関数ポインタを引数にとります。この比較関数として strcmp
(またはそれに類するもの) を渡すことで、文字列配列をソートできます。
“`c
include
include // qsort のために必要
include // strcmp のために必要
// qsort に渡す比較関数
// void 型のポインタを受け取り、比較結果を整数で返す必要がある
int compare_strings(const void a, const void b) {
// qsort から渡されるのは、比較対象の要素(この場合は char 型)へのポインタ
// つまり、char 型のポインタとして受け取る
const char s1 = (const char )a;
const char s2 = (const char **)b;
// 実際の文字列比較は strcmp に任せる
return strcmp(s1, s2);
}
int main() {
char *fruits[] = {
“banana”,
“apple”,
“cherry”,
“date”,
“grape”
};
int num_fruits = sizeof(fruits) / sizeof(fruits[0]);
printf("ソート前:\n");
for (int i = 0; i < num_fruits; i++) {
printf("%s\n", fruits[i]);
}
// 文字列配列を qsort でソート
qsort(fruits, num_fruits, sizeof(fruits[0]), compare_strings);
printf("\nソート後:\n");
for (int i = 0; i < num_fruits; i++) {
printf("%s\n", fruits[i]);
}
return 0;
}
**実行例:**
ソート前:
banana
apple
cherry
date
grape
ソート後:
apple
banana
cherry
date
grape
``
compare_strings
この例では、という関数内で
strcmpを呼び出しています。
qsortは要素へのポインタ(ここでは
char*型のポインタ)のポインタを渡してくるため、内部でキャストとデリファレンスを行ってから
strcmp` に渡しています。
7.3. 簡単な辞書検索
文字列のリストから特定の文字列を検索する場合にも strcmp
は必須です。線形探索や、ソート済みのリストに対する二分探索などに利用できます。
“`c
include
include
int main() {
char *words[] = {
“apple”,
“banana”,
“cherry”,
“date”,
“grape”,
“lemon”,
“mango”,
“orange”,
“pear”,
“quince”,
“raspberry”,
“strawberry”,
“watermelon”
};
int num_words = sizeof(words) / sizeof(words[0]);
char search_word[50];
printf("検索する単語を入力してください: ");
if (fgets(search_word, sizeof(search_word), stdin) != NULL) {
// 改行文字の削除
search_word[strcspn(search_word, "\n")] = '\0';
int found = 0;
// 線形探索
for (int i = 0; i < num_words; i++) {
if (strcmp(search_word, words[i]) == 0) {
printf("単語 '%s' がリストに見つかりました (インデックス %d)。\n", search_word, i);
found = 1;
break; // 見つかったら終了
}
}
if (!found) {
printf("単語 '%s' はリストに見つかりませんでした。\n", search_word);
}
}
return 0;
}
``
strcmp
この線形探索の例では、リスト内の各単語と検索対象の単語をで比較し、一致するかどうかを確認しています。もしリストがソート済みであれば、
bsearch関数と
strcmp` を組み合わせることで、より高速な二分探索を行うことも可能です。
8. よくある質問 (FAQ)
strcmp
関数やC言語の文字列比較に関して、よくある質問とその回答をまとめました。
8.1. ==
演算子で文字列比較できないのはなぜ?
再三触れてきましたが、C言語において文字列を char
配列や char
ポインタとして扱う場合、変数そのものには文字列の内容ではなく、文字列の先頭のメモリアドレスが格納されています。
==
演算子は、オペランドが組み込み型(整数、浮動小数点数など)やポインタ型の場合、その値そのものを比較します。したがって、文字列のポインタに対して ==
を使うと、比較されるのはポインタが持つメモリアドレスの値です。
“`c
char str1[] = “hello”; // 配列。コンパイル時にメモリ領域が確保され、そこに”hello”がコピーされる。str1は配列の先頭アドレス。
char str2[] = “hello”; // 別の配列。str1とは別のメモリ領域が確保される。str2はその先頭アドレス。
char str3 = “hello”; // 文字列リテラル。コンパイラが文字列リテラルを格納する領域を用意し、str3はそのアドレスを指す。
char str4 = “hello”; // 同上。str3と同じアドレスを指す場合もあれば、別の同じ内容の領域を指す場合もある(実装依存)。
// アドレス比較
str1 == str2; // 常に偽 (異なる配列だから)
str3 == str4; // 真になる可能性が高い (コンパイラが同じ文字列リテラルを重複させない最適化を行うため)
``
==
このように、演算子は文字列の内容ではなく、それらを保持するメモリ領域が「同一であるか」を判定します。文字列の「内容が等しいか」を判定するには、文字列の各文字を先頭から終端まで順番に比較する必要があり、そのための関数が
strcmp` なのです。
8.2. strcmp
の戻り値が具体的にいくらになるか重要ですか?
いいえ、重要ではありませんし、依存すべきではありません。
標準Cでは、strcmp(s1, s2)
の戻り値は以下の通り規定されています。
s1
がs2
より辞書順で前 → 負の値s1
とs2
が一致 → ゼロs1
がs2
より辞書順で後ろ → 正の値
多くの実装では、最初に異なる文字のバイト値の差(s1
のバイト値 – s2
のバイト値)を返しますが、これは実装の詳細であり、標準によって保証されている動作ではありません。異なるシステムやコンパイラでコンパイルした場合、例えば “a” と “b” の比較で -1
を返す実装もあれば、-10
を返す実装もあるかもしれません。
コードの移植性を保つためには、戻り値がゼロか、負か、正かだけを見て判定するようにしてください。
- 一致の判定:
strcmp(s1, s2) == 0
- 大小関係の判定:
strcmp(s1, s2) < 0
またはstrcmp(s1, s2) > 0
このように書けば、どのC標準準拠環境でも正しく動作することが保証されます。
8.3. 日本語のようなマルチバイト文字の比較はどうなりますか?
strcmp
関数はバイト単位で比較を行います。UTF-8のような可変長のマルチバイト文字エンコーディングの場合、1つの文字が複数のバイトで表現されます。strcmp
はこれらのバイト列を先頭から順番に、文字コードの知識なしに、単なる数値として比較します。
これにより、以下のような問題が発生する可能性があります。
- バイト列が同じなら一致と判断される: 全く同じ文字の並びであれば、バイト列も同じになるので
strcmp
は 0 を返します。 - バイト列が異なれば不一致と判断される: 同じ意味でも異なるバイト列で表現される文字(例: 全角スペースの複数の表現方法など、稀ですが)があれば、
strcmp
は不一致と判断します。 - 辞書順が人間の感覚と一致しないことがある: UTF-8などでは、文字コードの並び順が必ずしも言語の辞書順と一致しません。例えば、濁点や半濁点が付く文字が、付かない文字の直後に来るとは限りません。また、ひらがな、カタカナ、漢字の間にも文字コードに基づいた順序関係があり、これが日本語の辞書順とは異なる可能性があります。
- 部分一致判定が意図通りにならない:
strncmp
でマルチバイト文字の途中までを比較した場合、その結果は無意味になることが多いです。
ロケールに応じた、人間が期待するような文字列比較(特にソート順など)が必要な場合は、標準Cライブラリの strcoll
関数や strxfrm
関数、あるいはより高機能な国際化ライブラリ(ICU: International Components for Unicode など)を使用することを検討してください。
8.4. セキュリティ上の注意点はありますか?
strcmp
関数そのものが直接的なセキュリティ脆弱性の原因となることは稀ですが、strcmp
を使う前後の文字列処理において脆弱性が生じる可能性があります。特に注意すべきは以下の点です。
- NULLポインタ: 前述の通り、
strcmp
にNULLポインタを渡すと未定義動作となり、クラッシュや予期せぬ動作を引き起こす可能性があります。ユーザー入力などを文字列ポインタとして扱う場合は、必ずNULLチェックを行うべきです。 - ヌル終端されていない文字列 (非NULL終端文字列):
strcmp
にヌル終端されていない文字配列のポインタを渡すと、バッファオーバーリードが発生し、プログラムがクラッシュしたり、攻撃者にメモリ内容を読み取られたりするリスクがあります。ユーザーからの入力や、ファイル・ネットワークから読み込んだデータを扱う場合は、安全な関数 (fgets
,strncpy
,snprintf
など) を使用し、常にヌル終端されていることを保証するか、長さを指定できるstrncmp
を使用すべきです。 - タイミング攻撃 (Timing Attacks): 非常に機密性の高い文字列(パスワードや秘密鍵など)を比較する際に
strcmp
を使うと、比較に要する時間が一致するバイト数に依存するため、攻撃者がタイミングを測定することでパスワードの一部を推測できる可能性があります。このような機密データには、比較結果にかかわらず常に一定の時間で処理が完了する、タイミング攻撃耐性のある専用の比較関数(例: OpenSSLのCRYPTO_memcmp
や独自の安全な実装)を使用すべきです。一般的な文字列比較ではstrcmp
で問題ありません。
これらの点に注意することで、より安全なコードを書くことができます。
9. まとめ
この記事では、C言語における文字列比較の基本中の基本である strcmp
関数について、その使い方から詳細な挙動、注意点、関連関数、応用例、そして実装例まで、網羅的に解説しました。
- C言語では
==
演算子で文字列の内容を比較できないため、strcmp
関数が必要であること。 strcmp(s1, s2)
は、s1
とs2
を辞書順に比較し、負の値、ゼロ、正の値を返すこと。- 戻り値が具体的に何になるかは保証されないため、ゼロか、負か、正かのみを見て判定すべきであること。
strcmp
はバイト単位の比較であり、ヌル終端文字 (\0
) を文字列の終端として利用すること。strcmp
は大文字・小文字を区別すること、ロケールには依存しないこと。- NULLポインタやヌル終端されていない文字列を渡すと、未定義動作やバッファオーバーリードといった危険があること。
- 長さ制限付きの比較には
strncmp
、大文字・小文字を区別しない比較にはstrcasecmp
/stricmp
、任意のバイト列比較にはmemcmp
があること。 strcmp
はコマンドライン引数解析、文字列ソート、検索など、様々な場面で応用できること。
strcmp
関数はC言語の標準ライブラリの中でも非常に頻繁に利用される関数の一つです。その正しい使い方と注意点を理解することは、堅牢で安全なC言語プログラムを書く上で不可欠です。
この記事を通して、あなたがstrcmp
関数を完全にマスターし、C言語での文字列処理に自信を持てるようになれば幸いです。
文字列操作関数は他にもたくさんあります(コピー、連結、検索、分割など)。ぜひ他の関数についても学びを深め、C言語での文字列処理のスキルをさらに向上させていってください。
これで、strcmp
関数の完全ガイドは終わりです。最後までお読みいただき、ありがとうございました!