C言語入門:strstr関数で文字列検索をマスターしよう
はじめに
C言語は、その実行速度の速さとハードウェアに近いレベルでの制御が可能であることから、オペレーティングシステムや組み込みシステム、高性能なアプリケーション開発など、多岐にわたる分野で今なお使われ続けている強力なプログラミング言語です。しかし、C言語を学び始めると、多くの初学者が「文字列操作」という壁にぶつかります。
JavaやPythonのような現代的な言語には、文字列を扱うための豊富な機能が組み込まれた強力なstringクラスが用意されています。しかし、C言語における文字列は、単なる「文字の配列」であり、その終端が特別な文字である「NULL文字 (\0)」で示されるという規約に基づいています。このため、文字列の長さを調べたり、連結したり、そして今回テーマとする「検索」を行ったりするには、専用の関数を正しく理解して使う必要があります。
文字列検索は、プログラミングにおける最も基本的かつ頻繁に行われるタスクの一つです。例えば、
* ユーザーが入力したテキストから特定のキーワードを探す
* ログファイルから「ERROR」という文字列を含む行を抽出する
* URLから特定のパラメータを抜き出す
など、その応用範囲は無限大です。
この重要なタスクをC言語で実現するための強力な武器となるのが、標準ライブラリに含まれる strstr 関数です。この関数は、ある文字列の中から、指定した部分文字列がどこにあるかを見つけ出してくれます。
この記事では、C言語の初学者から一歩進んだ学習者までを対象に、strstr関数を徹底的に解説します。基本的な使い方から、その内部で何が起こっているのかという動作原理、実用的な応用例、そして安全に使うための注意点や関連関数との比較まで、strstrをマスターするために必要な知識を網羅的に提供します。
この記事を読み終える頃には、あなたはstrstr関数を自信を持って使いこなし、C言語における文字列操作の能力を一段階引き上げることができるでしょう。さあ、一緒にstrstrの世界を探求していきましょう。
1. strstr関数とは? – 基本の「き」
まず、strstr関数が何者なのか、その正体を明らかにしましょう。
strstrは、C言語の標準ライブラリの一部であり、「string search(文字列検索)」 を行うための関数です。その名前は “string in string”(文字列の中の文字列)を探す、と覚えると分かりやすいかもしれません。
この関数を利用するには、まずおまじないとして、プログラムの先頭でstring.hヘッダファイルをインクルードする必要があります。
“`c
include // strstr関数を使うために必要
include // printf関数などを使うために一般的によく使われる
“`
関数のプロトタイプ(宣言)
string.hヘッダファイルの中には、strstr関数がどのように定義されているかを示す「プロトタイプ宣言」が記述されています。これを見ることで、関数の使い方を正確に理解することができます。
c
char *strstr(const char *haystack, const char *needle);
この一行には、重要な情報が詰まっています。一つずつ分解して見ていきましょう。
char *strstr(...): 関数の名前はstrstrで、戻り値の型がchar *(char型へのポインタ)であることを示します。ポインタとは、メモリ上のアドレスを指し示す変数です。ここでは、「見つかった文字列の先頭アドレス」を返します。const char *haystack: 最初の引数です。haystackという名前は「藁山」を意味し、検索の対象となる大きな文字列を指します。constというキーワードは、この関数の中でhaystackが指す文字列の内容が変更されないことを保証するものです。char *なので、文字列の先頭アドレスを渡します。const char *needle: 2番目の引数です。needleは「針」を意味し、haystackの中から探し出したい部分文字列を指します。こちらもconstが付いているため、関数内で内容が変更されることはありません。
戻り値の意味
strstr関数の最も重要な特徴は、その戻り値です。
- 見つかった場合:
haystack文字列の中で、needle文字列が最初に出現した位置へのポインタを返します。これは、haystackの途中を指すポインタになります。 - 見つからなかった場合:
haystackの中にneedleが見つからなかった場合、NULLポインタを返します。NULLは、どこも指し示していないことを示す特別なポインタの値です。
この戻り値の性質を理解することが、strstrを正しく使うための鍵となります。特に、戻り値がNULLである可能性を常に考慮し、NULLチェックを行うことが、安全で堅牢なプログラムを書く上で不可欠です。
2. 基本的な使い方 – 最初のステップ
それでは、実際にstrstr関数を使った簡単なプログラムを見てみましょう。このコードを通じて、基本的な使い方をマスターします。
サンプルコード1: 基本的な検索
“`c
include
include
int main(void) {
// 検索対象の文字列 (haystack)
const char *haystack = “C programming is a powerful language.”;
// 検索したい部分文字列 (needle)
const char *needle = "powerful";
// strstr関数を呼び出して検索を実行
char *result = strstr(haystack, needle);
// 戻り値をチェックして結果を表示
if (result != NULL) {
// 見つかった場合
printf("'%s' が見つかりました。\n", needle);
printf("見つかった位置から先の文字列: \"%s\"\n", result);
// 見つかった位置のインデックスを計算
// (見つかった位置のアドレス) - (元の文字列の先頭アドレス)
long index = result - haystack;
printf("見つかった位置のインデックス: %ld\n", index);
} else {
// 見つからなかった場合
printf("'%s' は見つかりませんでした。\n", needle);
}
// 見つからない場合の例
const char *needle_not_found = "Python";
result = strstr(haystack, needle_not_found);
if (result == NULL) {
printf("\n'%s' は見つかりませんでした。正常な動作です。\n", needle_not_found);
}
return 0;
}
“`
実行結果:
'powerful' が見つかりました。
見つかった位置から先の文字列: "powerful language."
見つかった位置のインデックス: 19
'Python' は見つかりませんでした。正常な動作です。
コードの解説
-
準備:
haystackに検索対象の文章を、needleに探したい単語 “powerful” を設定します。const char *として宣言することで、これらの文字列がプログラム中で変更されないことを意図しています。 -
検索の実行:
char *result = strstr(haystack, needle);の行で、strstr関数を呼び出しています。haystackのアドレスとneedleのアドレスが関数に渡されます。strstrはhaystackの中からneedleを探し、見つかればその場所のアドレスを、見つからなければNULLをresultポインタに返します。
-
NULLチェック:if (result != NULL)は、このプログラムで最も重要な部分です。- もし
strstrが見つけられなかった場合、resultにはNULLが入ります。NULLポインタに対してprintfで内容を表示しようとしたりすると、セグメンテーション違反 (Segmentation Fault) と呼ばれるエラーでプログラムが強制終了してしまいます。 - そのため、
resultがNULLでないことを必ず確認してから、そのポインタを使用する必要があります。
- もし
-
見つかった場合の処理:
printf("見つかった位置から先の文字列: \"%s\"\n", result);
resultは “powerful” の ‘p’ の文字を指すポインタです。printfの%s書式指定子は、与えられたポインタからNULL文字\0に出会うまで文字を出力し続けるため、結果として “powerful language.” という文字列が表示されます。long index = result - haystack;
これは非常に便利なポインタ演算のテクニックです。同じ配列(文字列)を指す2つのポインタ同士を引き算すると、その間の要素数を計算できます。ここでは、result(見つかった場所のアドレス)からhaystack(文字列の先頭アドレス)を引くことで、先頭から何文字目にneedleが見つかったか(0から始まるインデックス)を求めています。
-
見つからなかった場合の処理:
elseブロックでは、見つからなかった旨のメッセージを表示します。needle_not_found(“Python”) を検索した例では、こちらが実行されます。
この基本的な使い方を理解すれば、strstrの第一関門は突破です。重要なのは、「ポインタを返す」 ということと、「NULLチェックが必須」 という2点です。
3. strstr関数の動作原理 – 内部を覗いてみよう
strstrはどのようにして文字列を見つけているのでしょうか?その内部実装は処理系(コンパイラやライブラリ)によって最適化されている可能性がありますが、基本となるアルゴリズムは理解しやすいものです。ここでは、最も単純な「ブルートフォース(総当たり)法」を元に、その動作をシミュレーションしてみましょう。
haystack = "abcabdabe"
needle = "abd"
ステップ1: haystackの先頭から比較開始
haystackのインデックス0 (a) とneedleのインデックス0 (a) を比較。→ 一致。haystackのインデックス1 (b) とneedleのインデックス1 (b) を比較。→ 一致。haystackのインデックス2 (c) とneedleのインデックス2 (d) を比較。→ 不一致!- 比較を中断。
ステップ2: haystackの次の文字から比較再開
haystackの開始位置をインデックス1 (b) にずらす。haystackのインデックス1 (b) とneedleのインデックス0 (a) を比較。→ 不一致!- 比較を中断。
ステップ3: haystackのさらに次の文字から比較再開
haystackの開始位置をインデックス2 (c) にずらす。haystackのインデックス2 (c) とneedleのインデックス0 (a) を比較。→ 不一致!- 比較を中断。
ステップ4: haystackのインデックス3から比較再開
haystackの開始位置をインデックス3 (a) にずらす。haystackのインデックス3 (a) とneedleのインデックス0 (a) を比較。→ 一致。haystackのインデックス4 (b) とneedleのインデックス1 (b) を比較。→ 一致。haystackのインデックス5 (d) とneedleのインデックス2 (d) を比較。→ 一致。needleの最後まで全て一致した!- 成功!
haystackのインデックス3を指すポインタを返す。
もし見つからなかったら…
haystackの終端(NULL文字)に到達するまで上記のプロセスを繰り返し、それでもneedleが最後まで一致することがなければ、最終的にNULLを返します。
疑似コードによる表現
このアルゴリズムを疑似コードで書くと、以下のようになります。
“`
function my_strstr(haystack, needle):
// needleが空文字列なら、haystackの先頭を返す (標準の仕様)
if needle is empty:
return haystack
h_len = length of haystack
n_len = length of needle
// haystackの各文字を開始点としてループ
for i from 0 to h_len – n_len:
// needleがhaystackのこの部分に一致するかチェック
match = true
for j from 0 to n_len – 1:
if haystack[i+j] != needle[j]:
match = false
break // 内部ループを抜ける
if match is true:
// 全て一致したので、その位置のポインタを返す
return address of haystack[i]
// ループが終了しても見つからなかった
return NULL
“`
実際のstrstrの実装は、これよりも遥かに効率的なアルゴリズム(例えば、Boyer-Moore法やKMP法など)が採用されていることが多いです。これらのアルゴリズムは、比較が不一致だった場合に、次の比較開始位置を賢くスキップすることで、検索速度を大幅に向上させます。
しかし、基本的な動作原理としてこのブルートフォース法を理解しておけば、strstrが何をしているのか、なぜ見つかった位置の「先頭」ポインタを返すのか、といった挙動を直感的に把握する助けになります。
4. strstr関数の応用例 – 実践力を高める
strstrの基本的な使い方がわかったところで、次にもっと実践的な応用例を見ていきましょう。strstrを他の関数やロジックと組み合わせることで、様々な問題が解決できます。
応用例1: 文字列の置換
strstrは文字列を「見つける」だけですが、これを使って「置換」する関数を自作することができます。
シナリオ: 文章中の特定の単語を、別の単語に置き換える。
例: “I love cats.” -> “I love dogs.”
実装のポイント:
1. 置換後の文字列を格納するための、十分な大きさの新しいメモリ領域(バッファ)を用意する。
2. strstrで置換対象の文字列(old_word)を探す。
3. 見つかったら、それより前の部分を新しいバッファにコピーする。
4. 新しい単語(new_word)をバッファに連結する。
5. 検索の開始位置を、置換したold_wordの末尾の次に更新して、2からの処理を繰り返す。
6. 最後に、残りの文字列をバッファに連結する。
サンプルコード2: 文字列置換関数
“`c
include
include
include // malloc, freeを使うため
// 文字列を置換する関数
// 戻り値: 新しく確保された置換後の文字列。呼び出し側でfree()する必要がある。
char replace_str(const char src, const char old_word, const char new_word) {
char result;
char p;
int old_len = strlen(old_word);
int new_len = strlen(new_word);
int count = 0;
// 1. old_wordが何回出現するか数える
p = (char *)src;
while ((p = strstr(p, old_word)) != NULL) {
count++;
p += old_len;
}
// 2. 置換後の文字列の長さを計算し、メモリを確保
// 元の長さ + (新旧の長さの差) * 出現回数 + 1 (NULL文字)
int result_len = strlen(src) + (new_len - old_len) * count + 1;
result = (char *)malloc(result_len);
if (result == NULL) {
printf("メモリ確保に失敗しました。\n");
return NULL;
}
result[0] = '// 1. old_wordが何回出現するか数える
p = (char *)src;
while ((p = strstr(p, old_word)) != NULL) {
count++;
p += old_len;
}
// 2. 置換後の文字列の長さを計算し、メモリを確保
// 元の長さ + (新旧の長さの差) * 出現回数 + 1 (NULL文字)
int result_len = strlen(src) + (new_len - old_len) * count + 1;
result = (char *)malloc(result_len);
if (result == NULL) {
printf("メモリ確保に失敗しました。\n");
return NULL;
}
result[0] = '\0'; // 空文字列で初期化
// 3. 実際に置換処理を行う
p = (char *)src;
char *current_pos = result;
while (1) {
char *found_pos = strstr(p, old_word);
if (found_pos == NULL) {
// もうold_wordはないので、残りを全てコピーして終了
strcat(result, p);
break;
}
// old_wordの手前までをコピー
int head_len = found_pos - p;
strncat(result, p, head_len);
// new_wordを連結
strcat(result, new_word);
// 検索開始位置を更新
p = found_pos + old_len;
}
return result;
'; // 空文字列で初期化
// 3. 実際に置換処理を行う
p = (char *)src;
char *current_pos = result;
while (1) {
char *found_pos = strstr(p, old_word);
if (found_pos == NULL) {
// もうold_wordはないので、残りを全てコピーして終了
strcat(result, p);
break;
}
// old_wordの手前までをコピー
int head_len = found_pos - p;
strncat(result, p, head_len);
// new_wordを連結
strcat(result, new_word);
// 検索開始位置を更新
p = found_pos + old_len;
}
return result;
}
int main(void) {
const char original_text = “This is a test. This test is simple.”;
const char word_to_find = “test”;
const char *word_to_replace = “exam”;
printf("Original: %s\n", original_text);
char *replaced_text = replace_str(original_text, word_to_find, word_to_replace);
if (replaced_text != NULL) {
printf("Replaced: %s\n", replaced_text);
// 動的に確保したメモリは必ず解放する
free(replaced_text);
}
return 0;
}
“`
実行結果:
Original: This is a test. This test is simple.
Replaced: This is a exam. This exam is simple.
この例では、mallocで動的にメモリを確保し、strncatやstrcatを駆使して新しい文字列を組み立てています。strstrが置換処理の中核を担っていることがわかります。
応用例2: 特定文字列の出現回数を数える
strstrをループで使うことで、部分文字列が何回出現するかを簡単に数えられます。
シナリオ: 長い文章の中に、特定のキーワードが何回出てくるか調べる。
実装のポイント:
1. whileループの中でstrstrを呼び出す。
2. 見つかったら(戻り値がNULLでなければ)、カウンターを1増やす。
3. 次の検索に備えて、検索開始ポインタを「見つかった場所 + needleの長さ」に更新する。これにより、同じ場所を何度もカウントすることを防ぐ。
4. strstrがNULLを返したら、ループを終了する。
サンプルコード3: 出現回数カウント
“`c
include
include
int main(void) {
const char haystack = “ababab, a baba, and abab.”;
const char needle = “ab”;
int count = 0;
// 検索開始位置を保持するポインタ
const char *p = haystack;
printf("'%s'の中の'%s'の出現回数を数えます。\n", haystack, needle);
while ((p = strstr(p, needle)) != NULL) {
count++; // 見つかったのでカウント
// 次の検索は、見つかった位置の1文字後ろから開始する
// p += strlen(needle); とすると、"ababab"が2回ではなく3回とカウントされてしまう
// "ab"を見つけたら、次の検索は"b"から始める
p++;
}
printf("出現回数: %d回\n", count);
// 重複を許さない場合のカウント
count = 0;
p = haystack;
while ((p = strstr(p, needle)) != NULL) {
count++;
// 次の検索は、見つかった単語の直後から開始する
p += strlen(needle);
}
printf("重複を許さない場合の出現回数: %d回\n", count);
return 0;
}
“`
実行結果:
'ababab, a baba, and abab.'の中の'ab'の出現回数を数えます。
出現回数: 5回
重複を許さない場合の出現回数: 3回
このコードは、p++とするかp += strlen(needle)とするかで結果が変わることを示しています。前者は”ababab”を”ab”, “ab”, “ab”と3回カウントするのに対し、後者は最初の”ab”を見つけた後、次の検索を”ab”の直後から始めるため”ab”, “ab”と2回カウントします(コード例では”ababab”は1つの塊として扱われるため、”abab”では2回になる)。どのような仕様でカウントしたいかによって、ポインタの進め方を調整する必要があります。
応用例3: CSV形式のデータの解析
strstrを使って、カンマ区切り(CSV)のような単純な形式のデータを解析することもできます。
シナリオ: “name,age,city” のような形式の文字列から、各フィールドを抜き出す。
実装のポイント:
1. strstrで区切り文字(例: ,)を探す。
2. 見つかったポインタと、前の区切り文字の位置(または文字列の先頭)を使って、フィールドの長さを計算する。
3. その長さ分だけ、フィールドの内容を別のバッファにコピーする。
サンプルコード4: CSVパーサーもどき
“`c
include
include
void print_csv_fields(const char csv_line) {
const char start = csv_line;
const char *end;
char field[128];
printf("CSVデータ: \"%s\"\n", csv_line);
printf("--- 解析結果 ---\n");
while (1) {
// 次のカンマを探す
end = strstr(start, ",");
if (end == NULL) {
// カンマが見つからない = 最後のフィールド
printf("Field: %s\n", start);
break;
}
// フィールドの長さを計算
int len = end - start;
// フィールドをコピーして出力 (バッファオーバーフローに注意)
if (len < sizeof(field)) {
strncpy(field, start, len);
field[len] = 'printf("CSVデータ: \"%s\"\n", csv_line);
printf("--- 解析結果 ---\n");
while (1) {
// 次のカンマを探す
end = strstr(start, ",");
if (end == NULL) {
// カンマが見つからない = 最後のフィールド
printf("Field: %s\n", start);
break;
}
// フィールドの長さを計算
int len = end - start;
// フィールドをコピーして出力 (バッファオーバーフローに注意)
if (len < sizeof(field)) {
strncpy(field, start, len);
field[len] = '\0'; // NULL終端
printf("Field: %s\n", field);
}
// 次の検索開始位置をカンマの1つ後ろに設定
start = end + 1;
}
printf("---------------\n");
'; // NULL終端
printf("Field: %s\n", field);
}
// 次の検索開始位置をカンマの1つ後ろに設定
start = end + 1;
}
printf("---------------\n");
}
int main(void) {
print_csv_fields(“Taro Yamada,30,Tokyo”);
print_csv_fields(“Hanako Suzuki,25,Osaka,Japan”);
return 0;
}
“`
実行結果:
“`
CSVデータ: “Taro Yamada,30,Tokyo”
— 解析結果 —
Field: Taro Yamada
Field: 30
Field: Tokyo
CSVデータ: “Hanako Suzuki,25,Osaka,Japan”
— 解析結果 —
Field: Hanako Suzuki
Field: 25
Field: Osaka
Field: Japan
``strtok
この方法は、元の文字列を破壊しないという利点があります。文字列を分割する標準関数は便利ですが、元の文字列をNULL文字で上書きしてしまうため、元のデータを保持したい場合にはstrstr`を使ったこのようなアプローチが有効です。
5. strstr関数を使う上での注意点
strstrは非常に便利ですが、誤った使い方をすると予期せぬバグやセキュリティ脆弱性の原因となります。安全に使うために、以下の点に注意してください。
注意点1: 大文字と小文字の区別
strstrは、大文字と小文字を厳密に区別します。
strstr("Hello World", "world") は NULL を返します。なぜなら、’W’と’w’は異なる文字として扱われるからです。
大文字・小文字を区別せずに検索したい場合は、いくつかの方法があります。
-
方法A: 検索前に文字列を変換する
検索対象の文字列と検索したい文字列の両方を、一時的なバッファにコピーし、toupperやtolower関数(ctype.hヘッダ)を使って全て大文字または小文字に変換してからstrstrで比較します。欠点:
* 追加のメモリ確保が必要。
* 文字列をコピーして変換する手間がかかる。 -
方法B:
strcasestr関数を使う(非標準)
一部の環境(主にGNU/Linux)では、大文字・小文字を区別しないバージョンのstrcasestrという関数が提供されています。これは非常に便利ですが、標準C言語の関数ではないため、Windows (MSVC)など、他の環境では使えません。移植性を重視するプログラムでは使用を避けるべきです。 -
方法C: 自前で実装する
移植性を保ちつつこの機能を実現するには、大文字・小文字を無視する比較ロジックを自分で実装するのが最も確実です。
注意点2: 戻り値のポインタとconstの罠
strstrのプロトタイプを再確認しましょう。
char *strstr(const char *haystack, const char *needle);
引数のhaystackは const char * であり、中身を変更しないと約束されています。しかし、戻り値は char * であり、constがついていません。これは歴史的な経緯によるもので、C言語の標準化以前の古いコードとの互換性を保つためと言われています。
この仕様は、非常に危険な状況を生み出す可能性があります。
“`c
include
include
int main(void) {
// 文字列リテラルは書き込み禁止領域に配置されることが多い
const char *text = “You can change me!”;
char *found = strstr(text, "change");
if (found != NULL) {
// これは絶対にやってはいけない!
// foundは書き込み禁止領域を指している可能性がある
// 未定義動作を引き起こし、クラッシュの原因となる
// *found = 'C'; // Segmentation Fault!
printf("見つかりました: %s\n", found);
}
return 0;
}
``*found = ‘C’;
上記の例で、のコメントアウトを外してコンパイル・実行すると、多くの場合セグメンテーション違反でプログラムがクラッシュします。なぜなら、textは文字列リテラルであり、そのデータは読み取り専用のメモリセグメントに配置されることが一般的だからです。strstrが返すポインタfound`も、その読み取り専用領域を指しているため、書き込もうとするとOSがそれを検知してプログラムを強制終了させます。
教訓: strstrの戻り値のポインタは、位置情報としてのみ利用し、そのポインタを通じて元の文字列を書き換えるべきではありません。 もし文字列を変更したい場合は、応用例1で示したように、新しいメモリ領域に文字列全体をコピーしてから操作を行うのが安全です。
注意点3: 空文字列の扱い
needle(検索する文字列)が空文字列 ("") だった場合、strstrはどのような動作をするでしょうか?
これは少し直感的でないかもしれませんが、C言語の標準規格では、needleが空文字列の場合、strstrはhaystackの先頭ポインタを返すと定められています。
“`c
include
include
int main(void) {
const char haystack = “Any string”;
const char empty_needle = “”;
char *result = strstr(haystack, empty_needle);
if (result == haystack) {
printf("仕様通り、空文字列を検索するとhaystackの先頭が返されました。\n");
}
return 0;
}
``strstr
これは、「空の文字列は、どんな文字列のどの位置にも(特に先頭に)存在する」という論理的な解釈に基づいています。この挙動を知らないと、ユーザー入力などで空文字列が渡された場合に予期せぬ動作を引き起こす可能性があるため、注意が必要です。必要であれば、を呼び出す前にstrlen(needle) == 0`のようなチェックを入れると良いでしょう。
注意点4: バッファオーバーフローへの警戒
strstr関数自体が直接バッファオーバーフローを引き起こすことはありません。しかし、その戻り値を使った後続の処理でバッファオーバーフローが発生する危険性が常に伴います。
例えば、strstrで見つけた部分以降を、固定長のバッファにstrcpyでコピーしようとする場合です。
“`c
// 悪い例: バッファオーバーフローの危険性
char buffer[10];
const char long_string = “Find the keyword here and it is long.”;
char found = strstr(long_string, “keyword”);
if (found != NULL) {
// foundが指す先の文字列 “keyword here and it is long.” は
// bufferのサイズ(10)より遥かに長い。
// strcpyは境界チェックをしないため、バッファオーバーフローが発生する!
strcpy(buffer, found);
}
“`
このようなコードは絶対に書いてはいけません。必ずstrncpyやsnprintfのような、書き込むサイズを指定できる安全な関数を使用してください。
“`c
// 良い例: 安全なstrncpyの使用
char buffer[10];
const char long_string = “Find the keyword here and it is long.”;
char found = strstr(long_string, “keyword”);
if (found != NULL) {
// コピーするサイズをバッファサイズ-1に制限
strncpy(buffer, found, sizeof(buffer) – 1);
// strncpyは必ずしもNULL終端しないので、手動で保証する
buffer[sizeof(buffer) – 1] = ‘\0’;
printf(“Copied safely: %s\n”, buffer);
}
“`
6. 関連する関数との比較
C言語の文字列ライブラリには、strstrと似た目的を持つ、あるいは混同しやすい関数がいくつか存在します。適切に使い分けるために、それぞれの違いを理解しておきましょう。
| 関数 | 検索対象 | 動作 | 元の文字列の変更 |
|---|---|---|---|
strstr |
文字列 | 指定した文字列が最初に出現する位置のポインタを返す。 | しない |
strchr |
1文字 | 指定した文字が最初に出現する位置のポインタを返す。 | しない |
strrchr |
1文字 | 指定した文字が最後に出現する位置のポインタを返す。 | しない |
strtok |
区切り文字 | 文字列を区切り文字で分割(トークン化)する。 | する |
memchr |
バイト | メモリ領域から指定したバイトが最初に出現する位置のポインタを返す。 | しない |
memmem |
バイト列 | メモリ領域から指定したバイト列が最初に出現する位置のポインタを返す。(非標準) | しない |
-
strchr/strrchrvsstrstr
strchrは特定の1文字を探すのに使います。ファイルパスから最後の/を探してファイル名を取り出す、といった用途に便利です。一方、strstrは文字列(単語など)を探します。 -
strtokvsstrstr
strtokは文字列を分割するのに特化していますが、元の文字列を破壊的に変更します(区切り文字を\0に置き換える)。また、内部に静的な状態を持つためスレッドセーフではなく、扱いに注意が必要です。応用例3で見たようにstrstrを使えば、元の文字列を変更せずに同様の分割処理を実装できます。 -
memchr/memmemvsstrstr
strstrはNULL終端文字列(\0)を前提として動作します。文字列の途中に\0があると、そこで検索は終了してしまいます。一方、memchrやmemmemは、検索するメモリ領域の長さを明示的に指定するため、\0を含むバイナリデータの中から特定のバイトやバイト列を探すことができます。memmemはstrstrのバイナリデータ版と言えますが、strcasestr同様、GNU拡張であり標準Cではありません。
7. 実践的なQ&A
Q1: strstrの戻り値がNULLでないことを確認せずに使うと、具体的に何が起こりますか?
A1: 戻り値がNULLのポインタ(resultとします)を、printf("%s", result); や strcpy(dst, result); のように参照(デリファレンス)しようとすると、プログラムはNULLポインタが指す無効なメモリアドレス(通常はアドレス0)にアクセスしようとします。これはOSによって保護された領域であるため、OSがメモリアクセス違反を検知し、プログラムを強制的に終了させます。これが「セグメンテーション違反」や「アクセス違反」と呼ばれるランタイムエラーの正体です。このエラーは非常に一般的で、C言語プログラミングにおける最も頻繁に遭遇するバグの一つです。常にNULLチェックを怠らない習慣をつけましょう。
Q2: 文字列の末尾から逆方向に検索したいのですが、strrstrのような関数はありますか?
A2: 残念ながら、標準Cライブラリにはstrrstr(文字列を後ろから検索する)関数は存在しません。strrchr(文字を後ろから検索する)はありますが、文字列には対応していません。この機能が必要な場合は、自前で実装する必要があります。一つの実装方法としては、haystackの末尾から一文字ずつポインタをずらし、各位置からstrncmpを使ってneedleと前方一致するかどうかを比較していく方法が考えられます。
Q3: strstrはマルチバイト文字(日本語のUTF-8など)を正しく扱えますか?
A3: strstrは、あくまでバイト単位で動作します。マルチバイト文字セットであるUTF-8では、1つの文字が1〜4バイトで表現されます。例えば、UTF-8の「あ」は0xE3 0x81 0x82という3バイトのシーケンスです。
strstrを使ってUTF-8文字列から「あ」を探す場合、needleとしてその3バイトのシーケンスを指定すれば、正しく見つけることができます。
しかし、strstrは「文字」という概念を理解していません。例えば、「”α”と”β”」という文字列から「”β”」を探すのは問題ありませんが、「”α”と”β”」を2文字とカウントすることはできません(UTF-8では4バイト+ASCII文字)。あくまでバイト列としての一致を見ているだけ、という点を理解しておく必要があります。より高度なロケールやUnicodeのルール(正規化など)を考慮した文字列検索が必要な場合は、ICU (International Components for Unicode) のような専門のライブラリを使用する必要があります。
Q4: strstrのパフォーマンスは気にする必要がありますか?
A4: 一般的なアプリケーションでは、strstrのパフォーマンスがボトルネックになることは稀です。現代の標準ライブラリの実装は十分に高速に最適化されています。しかし、何ギガバイトもあるような巨大なファイル全体に対して、何万回も繰り返し検索を行うような、極端に性能が要求されるシナリオでは、パフォーマンスが問題になる可能性があります。そのような場合は、より高度な検索アルゴリズム(Boyer-Moore法など)を自前で実装するか、高速なテキスト検索に特化したライブラリの利用を検討する価値があります。
まとめ
この記事では、C言語の文字列検索関数strstrについて、包括的に解説してきました。最後に、重要なポイントを振り返りましょう。
-
strstrの基本:string.hをインクルードし、char *strstr(const char *haystack, const char *needle)の形で使用します。haystackの中からneedleを探し、見つかればその位置へのポインタを、見つからなければNULLを返します。 -
NULLチェックの徹底:strstrの戻り値は、使用前に必ずNULLでないかチェックしてください。これを怠ると、プログラムクラッシュの直接的な原因となります。 -
ポインタ演算の活用: 戻り値のポインタと元の文字列の先頭ポインタの差分を取ることで、見つかった位置のインデックスを簡単に計算できます。
-
多彩な応用:
strstrは単なる検索に留まらず、ループと組み合わせることで出現回数のカウントや、他の文字列関数と連携させることで文字列の置換、簡易的なパーサーの実装など、様々な処理に応用できます。 -
安全な使い方:
strstrは大文字と小文字を区別すること、戻り値のポインタ経由で元の文字列を変更してはいけないこと、空文字列の挙動、そしてstrcpyなど危険な関数と組み合わせる際のバッファオーバーフローのリスクを常に意識してください。
strstrは、C言語における文字列操作の基本の「き」でありながら、その応用範囲は非常に広い、強力なツールです。この関数を深く理解し、安全に使いこなすことは、あなたのC言語プログラミングのスキルを確実に向上させます。
文字列操作は、メモリ管理やポインタの理解と密接に結びついています。strstrの学習をきっかけに、ぜひ他の文字列関数や、メモリを安全に扱うためのテクニックについても学びを深めていってください。地道な学習の積み重ねが、堅牢で効率的なCプログラムを書くための礎となるでしょう。