C言語の標準関数を自作する:文字列処理 strcat()編
目次
はじめに
C言語には、2つの文字列を連結するための標準関数として strcat() が用意されています。
今回は、この strcat() を標準Cライブラリの strcat() を利用せずに自作します。
このプロジェクトでは、標準関数と同じインターフェースを提供するラッパー関数と、実際の処理を行うコア関数を分離する構成を採用しています。
今回作成する strcat() は、以下のような構成とします。
利用側
↓
my_strcat()
↓
core_strcat()
my_strcat() が利用側から呼び出され、実際の文字列連結処理は core_strcat() が担当します。
なお、本プロジェクトでは、自作した標準関数模擬関数であるラッパー関数を、他の自作関数の実装から利用することを許容しています。
つまり、標準Cライブラリの関数を直接利用することは禁止しますが、自作した標準関数模擬関数(my_**()関数)については、必要に応じて利用することができます。
これは、自作した標準関数群を組み合わせて利用できるようにするためのプロジェクト上の設計方針です。
今回の strcat() では、文字列の長さを取得する処理に my_strlen() を利用します。
my_strlen() がすでに作成済みであり、今回の処理上も問題がないため、標準Cライブラリの strlen() を直接利用するのではなく、自作関数である my_strlen() を利用します。
1. strcat()の仕様を確認
まず、標準関数としての strcat() の仕様を確認します。
Linux環境では、以下のコマンドでmanページを確認できます。
man strcat
オンラインでは、Linux man-pages の strcat(3) を参照できます。
1.1 関数プロトタイプ
strcat() のプロトタイプは以下です。
char *strcat(char *restrict dest, const char *restrict src);
第1引数の dest は、連結先となる文字列です。
第2引数の src は、連結する文字列です。
strcat() は、dest の末尾にある文字列終端 '\0' の位置から src の内容をコピーし、その後に '\0' を設定します。
例えば、
char lv_Destination[32] = "Hello";
const char *lp_Source = " World";
strcat(lv_Destination, lp_Source);
とした場合、
実行前
Hello\0
実行後
Hello World\0
となります。
戻り値として、連結後の dest の先頭アドレスが返されます。
restrict は、ポインタを介したアクセスについて、他のポインタとの別名関係がないことをコンパイラに示すために使用されています。
そのため、標準 strcat() では、連結先と連結元の領域が重複しないことが前提となります。
1.2 文字列を連結する基本的な動作
strcat() の基本的な動作は、以下の通りです。
-
destの末尾を探す。 -
destの終端'\0'の位置からsrcをコピーする。 -
srcの終端'\0'も含めてコピーする。
例えば、
dest
Hello\0
src
World\0
の場合、
Hello
↑
ここから src をコピー
となり、
HelloWorld\0
になります。
重要なのは、src の文字列終端 '\0' も含めて連結後の文字列へ設定することです。
'\0' の設定に失敗すると、連結後の dest をC文字列として正しく扱えなくなります。
1.3 戻り値
strcat() は、連結後の dest の先頭へのポインタを返します。
例えば、
char lv_Destination[32] = "Hello";
char *lp_Result = strcat(lv_Destination, " World");
とした場合、
lv_Destination
Hello World
↑
lp_Result
となります。
つまり、
lp_Result == lv_Destination
となります。
今回の自作関数でも、標準 strcat() と同じように、連結先文字列の先頭アドレスを返します。
1.4 エッジケース:空文字列との連結
src が空文字列の場合、
char lv_Destination[32] = "Hello";
strcat(lv_Destination, "");
としても、dest の内容は変化しません。
Hello\0
のままです。
一方、dest が空文字列の場合、
char lv_Destination[32] = "";
strcat(lv_Destination, "Hello");
とすると、
Hello\0
になります。
このように、空文字列は特別なエラーとして扱う必要はありません。
1.5 strcat()とstrcpy()の違い
strcpy() と strcat() はどちらも文字列を扱う関数ですが、目的が異なります。
strcpy() は、第2引数の文字列を第1引数へ上書きコピーします。
例えば、
char lv_Destination[32] = "Hello";
strcpy(lv_Destination, "World");
とすると、
World
になります。
一方、strcat() は第1引数の末尾に第2引数を追加します。
char lv_Destination[32] = "Hello";
strcat(lv_Destination, "World");
とすると、
HelloWorld
になります。
つまり、
strcpy → 第1引数を第2引数で置き換える
strcat → 第1引数の末尾に第2引数を追加する
という違いがあります。
この違いを理解することが、strcat() を正しく利用する上での基本となります。
1.6 NULLポインタについて
標準 strcat() に NULL ポインタを渡した場合の動作は未定義です。
例えば、
strcat(NULL, "Hello");
や、
strcat(lv_Destination, NULL);
のような呼び出しを、標準 strcat() の正常な仕様として扱うことはできません。
今回の自作関数では、プロジェクト独自の安全性要件として、第1引数または第2引数が NULL の場合には NULL を返すようにします。
1.7 連結先の領域について
strcat() は dest の内容を書き換えます。
そのため、dest には連結後の文字列全体を格納できるだけの十分な領域が必要です。
例えば、
char lv_Destination[16] = "Hello";
strcat(lv_Destination, " World");
であれば、連結後の文字列と終端 '\0' を格納できる領域が必要です。
一方、
char lv_Destination[6] = "Hello";
strcat(lv_Destination, " World");
のように、連結後の文字列を格納できない領域を指定すると、領域外へ書き込むことになります。
このような場合の動作は未定義です。
strcat() 自体には、連結先のバッファサイズを確認する仕組みはありません。
そのため、連結後の文字列を格納できる十分な領域を利用側で確保する必要があります。
1.8 連結先と連結元の領域について
標準 strcat() では、連結先と連結元の領域が重複している場合の動作は未定義です。
例えば、同じ配列の一部を連結先と連結元として指定するような使い方は、今回の正常系の対象外とします。
これは、標準 strcat() のプロトタイプに restrict が指定されていることからも分かります。
今回の自作関数でも、連結先と連結元の領域が重複しないことを前提とします。
重複する領域を安全に扱う必要がある場合は、strcat() とは異なる設計が必要になります。
2. 実装
ここまでで、標準 strcat() の基本的な仕様を確認しました。
では、先程確認した仕様を参考に、実際に strcat() を自作してみます。
今回のプロジェクトでは、利用側から呼び出されるラッパー関数と、実際の処理を行うコア関数を分離しています。
実装はこちらです。
2.1 コア関数のヘッダーファイル
まず、コア関数のインターフェースをヘッダーファイルに定義します。
#ifndef CORE_STRING_CORE_STRCAT_H
#define CORE_STRING_CORE_STRCAT_H
#include "myc/myc_define.h"
EXTERN char *core_strcat(char *ap_Destination, const char *ap_Source);
#endif
第1引数は連結先文字列なので char * とします。
第2引数は読み取り専用の文字列なので const char * とします。
戻り値は連結後の第1引数の先頭アドレスなので char * とします。
標準 strcat() のプロトタイプには restrict が付いていますが、本プロジェクトでは自作関数のインターフェースとして restrict を付けずに定義しています。
ただし、標準 strcat() と同様に、連結先と連結元の領域が重複しないことを前提とします。
ヘッダーファイルには、プロジェクトのコーディングルールに従い、インクルードガードを付けています。
インクルードガードは、同じヘッダーファイルが複数回インクルードされた場合に、定義が重複することを防ぐためのものです。
また、EXTERN はプロジェクト共通で使用するマクロです。
2.2 コア関数の実装
コア関数では、実際の文字列の連結を行ないます。
#include <stddef.h>
#include "core/string/core_strcat.h"
#include "wrapper/string/my_strlen.h"
char *core_strcat(char *ap_Destination, const char *ap_Source)
{
size_t lv_DestinationLength;
size_t lv_Index = 0;
if (NULL == ap_Destination) {
return NULL;
}
if (NULL == ap_Source) {
return NULL;
}
lv_DestinationLength = my_strlen(ap_Destination);
while ('\0' != ap_Source[lv_Index]) {
ap_Destination[lv_DestinationLength + lv_Index] =
ap_Source[lv_Index];
lv_Index++;
}
ap_Destination[lv_DestinationLength + lv_Index] = '\0';
return ap_Destination;
}
この実装では、まず my_strlen() を利用して連結先文字列の長さを取得しています。
例えば、
dest = "Hello"
の場合、
my_strlen(dest)
↓
5
となります。
C文字列では、文字列の末尾に '\0' が存在するため、
H e l l o \0
0 1 2 3 4 5
↑
連結開始位置
となります。
そのため、
ap_Destination[lv_DestinationLength]
から ap_Source の内容をコピーします。
例えば、
dest = "Hello"
src = " World"
の場合、
H e l l o \0
↑
コピー開始
となり、
H e l l o W o r l d \0
が生成されます。
ループでは ap_Source の通常文字をコピーし、'\0' に到達したところで終了します。
その後、
ap_Destination[lv_DestinationLength + lv_Index] = '\0';
によって、連結後の文字列を終端します。
この処理によって、src の '\0' も連結後の dest に設定されます。
また、NULL を使用しているため、<stddef.h> を明示的にインクルードしています。
2.3 ラッパー関数のヘッダーファイル
次に、利用側から呼び出されるラッパー関数のインターフェースを定義します。
#ifndef WRAPPER_STRING_MY_STRCAT_H
#define WRAPPER_STRING_MY_STRCAT_H
#include "myc/myc_define.h"
/*
* 標準関数: strcat.
*
* 連結先文字列の末尾に、指定した文字列を連結します.
*
* 引数:
* ap_Destination : 連結先の文字列.
* ap_Source : 連結する文字列.
*
* 戻り値:
* 連結成功 : 連結先文字列の先頭アドレス.
* NULL入力 : NULL.
*
* 標準strcatとの相違点:
* 標準strcatではNULLポインタを渡した場合の動作は未定義です.
* 本関数ではNULLを独自に処理し、NULLを返します.
*/
EXTERN char *my_strcat(char *ap_Destination, const char *ap_Source);
#endif
こちらのヘッダーファイルにもインクルードガードを付けています。
また、利用側に対して標準 strcat() と同じ引数の型を提供します。
2.4 ラッパー関数の実装
ラッパー関数では、利用側から呼び出される my_strcat() を実装します。
#include "core/string/core_strcat.h"
#include "wrapper/string/my_strcat.h"
char *my_strcat(char *ap_Destination, const char *ap_Source)
{
return core_strcat(ap_Destination, ap_Source);
}
ラッパー自身は文字列連結処理を行わず、コア関数へ処理を委譲しています。
このように、今回の実装では、
my_strcat()
↓
core_strcat()
↓
my_strlen()
↓
文字列を連結
という役割分担になっています。
ここまでで、実際に動作する my_strcat() の実装を確認しました。
ただし、このコードには、本プロジェクト独自の仕様やコーディング規約が反映されています。
🔗 関連記事・関連リンク
この記事の最新アップデートや、このシリーズの関連記事は以下のリンクからご覧いただけます。
内容は基本的に同じですので、お好みのサイトでお読みください。
🟢 Zenn
-
この記事の最新版はこちら。
-
「標準関数自作」Topicの関連記事はこちら。
🔵 Qiita
-
「標準関数自作」タグの関連記事はこちら。
🧡 note
-
このシリーズの記事はこちら。
-
要件定義以降は、有料パートとしてこちらで公開。