C言語の標準関数を自作する:文字列処理 strlen()編
目次
はじめに
C言語には、文字列の長さを取得するための標準関数として strlen() が用意されています。
今回は、この strlen() を標準Cライブラリの strlen() を利用せずに自作します。
このプロジェクトでは、標準関数と同じインターフェースを提供するラッパー関数と、実際の処理を行うコア関数を分離する構成を採用しています。
今回作成する strlen() は、以下のような構成とします。
利用側
↓
my_strlen()
↓
core_strlen()
my_strlen() が利用側から呼び出され、実際の文字列長の計算は core_strlen() が担当します。
1. strlen() の仕様を確認
まず、標準関数としての strlen() の仕様を確認します。
Linux環境では、以下のコマンドで manページを確認できます。
man strlen
オンラインでは、Linux man-pagesの strlen(3) を参照できます。
strlen(3) — Linux manual page
1.1 関数プロトタイプ
strlen() のプロトタイプは以下です。
size_t strlen(const char *s);
strlen() は、文字列へのポインタを引数として受け取り、文字列の長さを size_t 型で返します。(man7.org)
1.2 文字列の長さ
strlen() が返す長さには、文字列終端のNULL文字('\0')は含まれません。(man7.org)
例えば、
"Hello"
という文字列は、概念的には、
H e l l o \0
という並びになっています。
この場合、strlen("Hello") の結果は 5 です。
空文字列の場合は、文字列中に通常の文字が存在しないため、結果は 0 になります。
1.3 NULLポインタについて
標準 strlen() にNULLポインタを渡した場合の動作は、未定義です。
1.4 strlen() の読み取り範囲について
strlen() は、文字列の終端である '\0' が見つかるまで文字列を読み取ります。
そのため、引数が指す文字列にヌル終端文字列 '\0' がない場合、読み取り可能な領域を超えてアクセスする可能性があります。このような場合の動作は未定義です。
このように、読み取り可能な最大範囲を指定できないことが strlen() の性質上の制約の一つと言えます。
そのためPOSIX環境では、 strnlen() が用意されています。strnlen() は最大読み取り長を指定できるため、入力が適切にヌル終端されていることを前提としにくい場面で選択肢になります。
2. 実装
ここまでで、標準 strlen() の基本的な仕様を確認しました。
では、先程確認した仕様を参考に、実際に strlen() を自作してみます。
今回のプロジェクトでは、利用側から呼び出されるラッパー関数と、実際の処理を行うコア関数を分離しています。
実装はこちらです。
2.1 コア関数のヘッダーファイル
まず、コア関数のインターフェースをヘッダーファイルに定義します。
#ifndef CORE_STRING_CORE_STRLEN_H
#define CORE_STRING_CORE_STRLEN_H
#include <stddef.h>
#include "myc/myc_define.h"
EXTERN size_t core_strlen(const char *ap_String);
#endif
size_t および NULL を使用するため、標準ヘッダーの をインクルードしています。
size_t は、strlen() の戻り値として使用されており、オブジェクトのサイズや配列の要素数などを表すために使用される符号なし整数型です。具体的な基本型は処理系によって異なります。
また、NULL は、ヌルポインタを表すために使用します。今回の実装では、引数が NULL かどうかを確認するために使用しています。
ヘッダーファイルには、プロジェクトのコーディングルールに従い、インクルードガードを付けています。
インクルードガードは、同じヘッダーファイルが複数回インクルードされた場合に、定義が重複することを防ぐためのものです。
また、EXTERN はプロジェクト共通で使用するマクロです。今回は空マクロとして展開されるため、strlen() の実装には特に影響しません。
2.2 コア関数の実装
コア関数では、実際の文字列長の計算を行います。
#include <stddef.h>
#include "core/string/core_strlen.h"
size_t core_strlen(const char *ap_String)
{
size_t lv_Length = 0;
if (NULL == ap_String) {
return 0;
}
while ('\0' != ap_String[lv_Length]) {
lv_Length++;
}
return lv_Length;
}
2.3 ラッパー関数のヘッダーファイル
次に、利用側から呼び出されるラッパー関数のインターフェースをヘッダーファイルに定義します。
#ifndef WRAPPER_STRING_MY_STRLEN_H
#define WRAPPER_STRING_MY_STRLEN_H
#include <stddef.h>
#include "myc/myc_define.h"
/*
* 標準関数: strlen.
*
* 文字列の長さを取得します.
* 終端文字 '\0' は文字列の長さに含めません.
*
* 引数:
* ap_String : 長さを取得する文字列.
*
* 戻り値:
* 文字列の長さを size_t 型で返します.
*
* 標準strlenとの相違点:
* 標準strlenではNULLポインタを渡した場合の動作は未定義です.
* 本関数ではNULLポインタを独自に処理し、0を返します.
*/
EXTERN size_t my_strlen(const char *ap_String);
#endif
こちらのヘッダーファイルにも、インクルードガードを付けています。
2.4 ラッパー関数の実装
ラッパー関数では、利用側から呼び出される my_strlen() を実装します。
#include <stddef.h>
#include "core/string/core_strlen.h"
#include "wrapper/string/my_strlen.h"
size_t my_strlen(const char *ap_String)
{
return core_strlen(ap_String);
}
ラッパー自身は文字列長を計算せず、コア関数へ処理を委譲しています。
このように、今回の実装では、
my_strlen()
↓
core_strlen()
↓
文字列長を計算
という役割分担になっています。
ここまでで、実際に動作する my_strlen() の実装を確認しました。
ただし、このコードには、本プロジェクト独自の仕様やコーディング規約が反映されています。
ここからは、なぜこのような実装になっているのかを、要件定義や設計の観点から説明します。
3. 自作 strlen()関数の要件定義
標準 strlen() の仕様を確認し、実装も確認したところで、今回作成する my_strlen() にどのような要件を設定したのかを整理します。
3.1 正常系
以下を正常系の要件とします。
- '\0' までに存在する文字数を返す。
- '\0' 自体は文字数に含めない。
- 空文字列の場合は 0 を返す。
- 戻り値の型は size_t とする。
- 通常の文字列について、標準
strlen()と同じ結果を返す。
3.2 安全性
標準 strlen() の仕様とは別に、自作関数独自の安全性要件を追加します。
- 引数が NULL の場合は 0 を返す。
これは標準 strlen() との完全な互換性を目的とした仕様ではなく、本プロジェクトにおける自作関数の安全性を高めるための要件です。
4. 基本設計
strlen() の基本的な処理は、文字列の先頭から終端の '\0' を探し、それまでに存在する文字数を数えることです。
処理の流れは以下のようになります。
-
引数がNULLか確認する。
-
NULLの場合は0を返す。
-
文字数を0で初期化する。
-
現在位置の文字が'\0'になるまで繰り返す。
-
1文字進むごとに文字数を1増加させる。
-
'\0'に到達したら文字数を返す。
5. 詳細設計
5.1 NULLチェック
最初に引数が NULL か確認します。
if (NULL == ap_String) {
return 0;
}
NULLの場合は、今回設定した安全性要件に従って 0 を返します。
5.2 文字数のカウント
文字列の先頭から順番に文字を確認します。
while ('\0' != ap_String[lv_Length])
現在位置の文字が '\0' でなければ、文字数を1増加させます。
lv_Length++;
終端の '\0' に到達した時点でループを終了し、それまでに数えた文字数を返します。
6. コーディング規約
本プロジェクトでは、独自のコーディング規約を定めています。
今回のコア関数では、特に以下の規約を適用します。
6.1 引数とローカル変数の命名
引数には a、ローカル変数には l のプレフィックスを付けます。
また、ポインタ系と値系を区別します。
例えば、
const char *ap_String;
は引数であるポインタ、
size_t lv_Length;
はローカル変数である値として扱います。
6.2 ポインタのNULL判定
ポインタのNULL判定は暗黙的な真偽値判定を行わず、明示的に NULL と比較します。
if (NULL == ap_String) {
return 0;
}
6.3 比較演算子の記述
== および != による比較では、右辺に変数を記述するというルールを採用しています。
例えば、
if (NULL == ap_String)
や、
if (NULL != ap_String)
のように記述します。
6.4 コメント
// によるコメントを記述する場合は、必ず半角のピリオド . で終了します。
6.5 インクルードガード
ヘッダーファイルには、必ずインクルードガードを付けます。
インクルードガードには、ヘッダーファイルのパスをもとにした一意なマクロ名を使用します。
#ifndef CORE_STRING_CORE_STRLEN_H
#define CORE_STRING_CORE_STRLEN_H
/* ヘッダーの内容. */
#endif
同じヘッダーファイルが複数回インクルードされた場合でも、内容が重複して展開されることを防ぐために使用します。
7. テスト
今回は、主に以下の2種類の試験を実施します。
7.1 要件達成試験
標準 strlen() と自作 my_strlen()の結果を比較します。
これは、内部実装を意識せず、入力に対する出力を比較するブラックボックス試験です。
以下のような入力を使用します。
- 空文字列。
- 1文字の文字列。
- 複数文字の文字列。
- 一般的な文字列。
- 比較的長い文字列。
7.2 安全性試験
NULL入力については、標準 strlen() と比較せず、自作関数が安全性要件を満たしているかを確認します。
今回の要件では、NULL入力時に 0 を返すことを確認します。
8. テストコード
テストコードでは、要件達成試験として標準 strlen() と自作 my_strlen() の結果を比較します。
また、安全性試験では my_strlen(NULL) の結果を確認します。
テスト結果は、単にPASS/FAILだけを表示するのではなく、試験内容、入力、標準関数の結果、自作関数の結果を表示するようにします。
#include <stdio.h>
#include <stddef.h>
#include <string.h>
#include "wrapper/string/my_strlen.h"
static int test_string(const char *ap_String)
{
size_t lv_Expected = strlen(ap_String);
size_t lv_Actual = my_strlen(ap_String);
printf("[要件達成試験] \"%s\"\n", ap_String);
printf(" 標準 strlen : %zu\n", lv_Expected);
printf(" 自作 my_strlen : %zu\n", lv_Actual);
if (lv_Expected != lv_Actual) {
printf(" 結果 : FAIL\n\n");
return 1;
}
printf(" 結果 : PASS\n\n");
return 0;
}
static int test_null(void)
{
size_t lv_Actual = my_strlen(NULL);
printf("[安全性試験] NULL入力\n");
printf(" 自作 my_strlen(NULL) : %zu\n", lv_Actual);
if (0 != lv_Actual) {
printf(" 結果 : FAIL\n\n");
return 1;
}
printf(" 結果 : PASS\n\n");
return 0;
}
int main(void)
{
int lv_Result = 0;
lv_Result |= test_string("Hello");
lv_Result |= test_string("A");
lv_Result |= test_string("");
lv_Result |= test_string("Hello, World!");
lv_Result |= test_string("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
lv_Result |= test_null();
if (0 == lv_Result) {
printf("=== 全試験 PASS ===\n");
} else {
printf("=== 試験 FAIL ===\n");
}
return lv_Result;
}
このテストコードは、標準関数との比較を行うため、標準ライブラリの strlen() を使用しています。
テストコードはコーディング規約の適用対象外としているため、コア関数とは異なり、テストの目的に合わせて標準ライブラリ等を利用しています。
9. テスト結果
実際にテストを実行した結果、以下のようになりました。
[要件達成試験] "Hello"
標準 strlen : 5
自作 my_strlen : 5
結果 : PASS
[要件達成試験] "A"
標準 strlen : 1
自作 my_strlen : 1
結果 : PASS
[要件達成試験] ""
標準 strlen : 0
自作 my_strlen : 0
結果 : PASS
[要件達成試験] "Hello, World!"
標準 strlen : 13
自作 my_strlen : 13
結果 : PASS
[要件達成試験] "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
標準 strlen : 26
自作 my_strlen : 26
結果 : PASS
[安全性試験] NULL入力
自作 my_strlen(NULL) : 0
結果 : PASS
=== 全試験 PASS ===
通常の文字列について、標準 strlen() と自作 my_strlen() の結果が一致しました。
また、NULL入力についても、自作関数が設定した安全性要件を満たしていることを確認できました。
10. 単体試験について
単体試験では、関数内部の処理を意識してテストケースを設計します。
今回の core_strlen() は処理が単純であるため、以下のようなケースを用意することで、主要な処理を確認できます。
| 入力 | 確認内容 |
|---|---|
| NULL | NULLチェックによる早期終了。 |
| "" | ループを実行せず終了するケース。 |
| "A" | 1回ループするケース。 |
| "Hello" | 複数回ループするケース。 |
今回はカバレッジ計測自体は実施していません。
単純な関数についてカバレッジ計測を必須とすると、関数の実装やテストそのものよりも、計測環境の構築にかかる負担が大きくなるためです。
ただし、単体試験ではC1程度の分岐を意識してテストケースを設計しています。
今後、実装が複雑になり、テストケースだけでは処理の網羅性を確認しにくい関数については、必要に応じてカバレッジ計測を実施します。
11. 安全性について
今回の my_strlen() では、標準 strlen() にはないNULLチェックを追加しています。
if (NULL == ap_String) {
return 0;
}
これにより、NULLポインタが渡された場合でも、本実装では文字列へのアクセスを行わずに処理を終了できます。
ただし、この動作は標準 strlen() の仕様そのものではありません。
そのため、
- 標準関数としての仕様。
- 自作関数に追加した安全性要件。
を分けて考える必要があります。
今回のNULLチェックは、自作関数側で安全性を高めるために追加した仕様です。
12. まとめ
今回は、標準Cライブラリの strlen() を参考に、自作の my_strlen() を実装しました。
要件定義から設計、実装、試験まで一通り行い、想定した要件を満たしていることを確認しました。
今回の実装を通して、C言語における文字列処理やポインタの基本についても確認できました。
追記・NULLを渡した場合の設計について
今回の my_strlen() では、NULL が渡された場合にNULLアクセスを起こさず、0 を返す仕様としています。
これは、「異常な入力ならプログラムをクラッシュさせればよい」と考えているためではありません。
異常な入力を検知した場合でも、必ずしもプログラム全体を停止させる必要があるとは限りません。システムによっては、異常な入力を受けた処理だけを中断し、エラーを通知したうえでプログラム全体は動作を継続する方が望ましい場合もあります。
今回の my_strlen() では、NULL が渡された場合にNULLアクセスを発生させず、処理を終了するという設計にしています。
ただし、この設計には一つ注意点があります。
my_strlen(NULL) → 0
my_strlen("") → 0
となるため、戻り値だけでは NULL が渡されたのか、単に空文字列だったのかを区別できません。
これは、今回の戻り値の仕様による設計上の制約です。
そのため、NULL と空文字列を区別して扱う必要がある場合は、my_strlen() の戻り値を見て判断するのではなく、呼び出し側で my_strlen() を呼び出す前に引数を確認する必要があります。
例えば、
if (NULL == lp_Message) {
/* NULL入力 */
} else {
size_t lv_Length = my_strlen(lp_Message);
if (0 == lv_Length) {
/* 空文字列 */
}
}
のように、呼び出し側でNULLを確認してから my_strlen() を呼び出します。
このようにすることで、呼び出し側では、
-
NULLが渡された場合。 - 空文字列が渡された場合。
- 1文字以上の文字列が渡された場合。
を区別して扱うことができます。
そもそも標準 strlen() に NULL を渡すことは想定されていません。そのため、正常な利用において NULL を渡さないことは呼び出し側の責務と考えています。
一方で、今回の my_strlen() では、万一呼び出し側から NULL が渡された場合でも、NULLポインタを参照してしまうことを防ぐため、コア関数側にもNULLチェックを設けています。
if (NULL == ap_String) {
return 0;
}
つまり、今回の設計では、
NULLを渡さないことは呼び出し側の責務としつつ、関数自身もNULLアクセスを防ぐための防御的なチェックを行う
という二段構えになっています。
このNULLチェックは、標準 strlen() の仕様を再現するためのものではなく、本プロジェクト独自の安全性要件として追加したものです。
errno を使う方法は?
NULL が渡されたことを戻り値とは別に明確に通知したいのであれば、本プロジェクト独自の仕様として errno などを利用する方法も考えられます。
例えば、
my_strlen(NULL);
を呼び出した場合に、戻り値とは別にエラー情報を設定する方法です。
しかし、今回はこの方式を採用していません。
strlen() は、文字列の長さを取得する非常に単純な関数です。
ここに独自のエラー通知機構を追加すると、標準 strlen() と同じ size_t を返すという単純なインターフェースに対して、別途エラー状態を確認する必要が生じます。
例えば、
size_t lv_Length = my_strlen(lp_Message);
/* エラー状態を確認する処理 */
のように、単純な文字列長の取得に対して呼び出し側の処理が増えることになります。
また、今回の設計では、正常な利用において NULL を渡さないことを呼び出し側の責務としています。
そのため、正常な利用時に毎回エラー状態を確認するような仕組みを strlen() 相当の単純な関数に追加する必要性は低いと判断しました。
さらに、errno を利用する方式は、標準 strlen() が持っている仕様ではありません。そのため、標準関数と同じインターフェースを提供するラッパー関数という今回の設計方針からも、今回は採用しないこととしました。
そのため今回は、
-
NULLを渡さないことは呼び出し側の責務とする。 -
my_strlen()自身にもNULLチェックを設け、NULLアクセスを防ぐ。 -
NULLが渡された場合は0を返す。 -
NULLと空文字列を区別する必要がある場合は、my_strlen()を呼び出す前に呼び出し側で引数を確認する。 - 独自のエラー通知機構や
errnoは導入しない。
という仕様にしています。
もちろん、これはあくまで今回の my_strlen() における設計判断です。
システムによっては、
- 異常な引数を受け取ったら即座にプログラムを停止させる。
- エラーコードを返して異常を通知する。
-
errnoなどでエラー状態を通知する。 - 戻り値とは別にエラー情報を受け取るための引数を設ける。
といった設計も考えられます。
重要なのは、NULLアクセスを防ぐことだけを「安全」と捉えるのではなく、異常な入力をどのように検知し、どこで責任を持って処理するのかまで含めて設計することです。
今回の my_strlen() では、標準 strlen() のインターフェースを大きく変更することなく、関数自身ではNULLアクセスを防ぎ、NULLと空文字列を区別する必要がある場合には呼び出し側で事前に判定する、という方針を採用しています。
🔗 関連記事・関連リンク
この記事の最新アップデートや、このシリーズの関連記事は以下のリンクからご覧いただけます。
内容は基本的に同じですので、お好みのサイトでお読みください。
🟢 Zenn
-
この記事の最新版はこちら。
-
「標準関数自作」Topicの関連記事はこちら。
🔵 Qiita
-
「標準関数自作」タグの関連記事はこちら。
🧡 note
-
このシリーズの記事はこちら。