C言語の標準関数を自作する:文字列処理 strncat()編
目次
はじめに
C言語には、文字列の末尾に別の文字列から指定したバイト数まで追加する標準関数として strncat() が用意されています。
今回は、この strncat() を標準Cライブラリの strncat() を利用せずに自作します。
このプロジェクトでは、標準関数と同じインターフェースを提供するラッパー関数と、実際の処理を行うコア関数を分離する構成を採用しています。
今回作成する strncat() は、以下のような構成とします。
利用側
↓
my_strncat()
↓
core_strncat()
my_strncat() が利用側から呼び出され、実際の連結処理は core_strncat() が担当します。
なお、本プロジェクトでは、自作した標準関数模擬関数であるラッパー関数を、他の自作関数の実装から利用することを許容しています。
つまり、標準Cライブラリの関数を直接利用することは禁止しますが、自作した標準関数模擬関数(my_**()関数)については、必要に応じて他の自作関数から利用できます。
これは、自作した標準関数群を組み合わせて利用できるようにするためのプロジェクト上の設計方針です。
1. strncat()の仕様を確認
まず、標準関数としての strncat() の仕様を確認します。
Linux環境では、以下のコマンドでmanページを確認できます。
man 3 strncat
オンラインでは、Linux man-pages の strncat(3) を参照できます。
strncat() は、コピー元から最大 n バイトの非NUL文字をコピーして、コピー先の文字列の末尾に追加する関数です。
重要なのは、strncpy() とは異なり、strncat() は追加後に必ず終端の '\0' を書き込むことです。
例えば、以下の場合を考えます。
char lv_Buffer[32] = "Hello";
strncat(lv_Buffer, " World", 6);
結果は、
Hello World\0
となります。
ここで注意したいのは、n は「結果全体の最大長」ではないという点です。
n は、コピー元から追加する最大バイト数を指定します。
また、コピー先は呼び出し前から正しいC文字列である必要があり、連結後の文字列と終端 '\0' を格納できるだけの領域が必要です。
Linux man-pagesでも、必要なバッファサイズを概念的に strlen(dest) + strnlen(src, n) + 1 と説明しています。(man7.org)
1.1 関数プロトタイプ
strncat() のプロトタイプは以下です。
char *strncat(char *dest, const char *src, size_t n);
第1引数は連結先の文字列です。
第2引数は追加する文字列です。
第3引数は、コピー元から追加する最大バイト数です。
戻り値は、連結先 dest です。
例えば、
char lv_Buffer[32] = "Hello";
char *lp_Result = strncat(
lv_Buffer,
" World",
6);
とした場合、
lv_Buffer
↓
Hello World
となり、
lp_Result == lv_Buffer
です。
戻り値はエラーを表すものではなく、連結先へのポインタです。
POSIXの仕様でも、strncat() は s1 を返し、エラーを示す戻り値は予約されていません。(man7.org)
1.2 最大nバイトまで追加する
strncat() の特徴は、第3引数で指定した n バイトを上限として、コピー元から文字を追加することです。
例えば、
char lv_Buffer[32] = "Hello";
strncat(lv_Buffer, " World", 3);
とした場合、
Hello
↓
Hello Wo
となります。
コピー元 " World" の先頭3バイト、
' '
'W'
'o'
が追加されます。
その後、必ず終端の '\0' が追加されます。
したがって、
Hello Wo\0
となります。
ここで重要なのは、n はコピー先全体のサイズではないということです。
strncat(lv_Buffer, " World", 3);
の 3 は、
lv_Bufferに最大3バイト追加する
という意味です。
1.3 コピー元がnバイトより短い場合
コピー元が n バイトより短い場合は、コピー元の終端 '\0' に到達した時点でコピーを終了します。
例えば、
char lv_Buffer[32] = "Hello";
strncat(lv_Buffer, "ABC", 10);
とした場合、
HelloABC\0
となります。
コピー元は、
A B C \0
なので、n は10ですが、実際に追加される文字は3文字です。
'\0' 自体は追加対象の文字としてコピーされるのではなく、strncat() が結果の終端として '\0' を追加します。
つまり、
コピー元: ABC\0
n : 10
追加される文字: ABC
結果 : HelloABC\0
となります。
1.4 コピー元がnバイト以上ある場合
一方、コピー元の文字列が n バイト以上ある場合は、最大 n バイトの文字を追加します。
例えば、
char lv_Buffer[32] = "Hello";
strncat(lv_Buffer, "ABCDEFG", 3);
とした場合、
HelloABC\0
となります。
コピー元の先頭3文字、
A B C
だけが追加されます。
strncpy() と異なり、strncat() はこの場合でも結果の末尾に必ず '\0' を追加します。
つまり、
strncpy
↓
nバイトで終了すると'\0'が付かない場合がある
strncat
↓
最大nバイト追加した後、必ず'\0'を付ける
という違いがあります。
1.5 nが0の場合
n == 0 の場合、コピー元から追加する文字はありません。
例えば、
char lv_Buffer[32] = "Hello";
strncat(lv_Buffer, "World", 0);
とした場合、文字列の内容は、
Hello
のままです。
ただし、今回の自作関数では、プロジェクト独自の安全性要件としてNULLを処理します。
そのため、
NULL / "ABC" / 0
"ABC" / NULL / 0
についても、自作関数では NULL を返す仕様とします。
1.6 戻り値
strncat() は、連結後のコピー先へのポインタを返します。
例えば、
char lv_Buffer[32] = "Hello";
char *lp_Result = strncat(
lv_Buffer,
" World",
6);
の場合、
lp_Result
↓
+---+---+---+---+---+---+---+
| H | e | l | l | o | | W |
+---+---+---+---+---+---+---+
↑
lv_Buffer
となり、
lp_Result == lv_Buffer
です。
strncat() の戻り値はエラーを表すものではありません。
今回の my_strncat() でも、正常時にはコピー先へのポインタを返します。
1.7 コピー先とコピー元の重複
コピー元とコピー先の領域が重複している場合、strncat() の動作は未定義です。
POSIXの仕様でも、コピー元とコピー先のオブジェクトが重複している場合の動作は未定義とされています。(man7.org)
例えば、同じ配列の一部をコピー元とコピー先として使用するような処理は、正常な使用方法として扱うことはできません。
今回の自作関数でも、標準 strncat() と同様に、コピー元とコピー先が重複しないことを前提とします。
1.8 コピー先には十分な領域が必要
strncat() では、第3引数によって追加する文字数を制限できます。
しかし、コピー先のバッファサイズを自動的に確認してくれるわけではありません。
例えば、
char lv_Buffer[8] = "Hello";
strncat(lv_Buffer, "World", 5);
の場合、
Hello
+
World
=
HelloWorld
となるため、
HelloWorld\0
を格納できるだけの領域が必要です。
しかし、lv_Buffer は8バイトしかありません。
このように、コピー先の領域が不足している場合は、安全に処理できません。
必要なサイズは概念的には、
strlen(dest) + strnlen(src, n) + 1
以上です。Linux man-pagesでも、このサイズが必要なバッファサイズとして示されています。(man7.org)
つまり、
コピー先の現在の長さ
+
実際に追加する文字数
+
終端'\0'
の領域が必要です。
1.9 NULLポインタについて
標準 strncat() にNULLポインタを渡した場合の動作は未定義です。
例えば、
strncat(NULL, "ABC", 3);
や、
strncat(lv_Buffer, NULL, 3);
を標準 strncat() の正常系として扱うことはできません。
今回の自作関数では、これまでの自作文字列関数と同様に、プロジェクト独自の安全性要件として、
コピー先がNULL → NULLを返す
コピー元がNULL → NULLを返す
とします。
この仕様は標準 strncat() の仕様ではなく、今回の自作関数に追加した独自仕様です。
2. 実装
ここまでで、標準strncat()の基本的な仕様を確認しました。
では、先程確認した仕様を参考に、実際にstrncat()を自作してみます。
今回のプロジェクトでは、利用側から呼び出されるラッパー関数と、実際の処理を行うコア関数を分離しています。
実装はこちらです。
2.1 コア関数のヘッダーファイル
まず、コア関数のインターフェースをヘッダーファイルに定義します。
#ifndef CORE_STRING_CORE_STRNCAT_H
#define CORE_STRING_CORE_STRNCAT_H
#include <stddef.h>
#include "myc/myc_define.h"
EXTERN char *core_strncat(
char *ap_Destination,
const char *ap_Source,
size_t av_Count);
#endif
第1引数は連結先なので char * とします。
第2引数は連結元なので const char * とします。
第3引数は追加する最大バイト数なので size_t とします。
戻り値は標準 strncat() と同じく、コピー先を指す char * とします。
ヘッダーファイルには、プロジェクトのコーディングルールに従い、インクルードガードを付けています。
インクルードガードは、同じヘッダーファイルが複数回インクルードされた場合に、定義が重複することを防ぐためのものです。
また、EXTERN はプロジェクト共通で使用するマクロです。
2.2 コア関数の実装
コア関数では、実際の文字列の連結を行ないます。
#include "core/string/core_strncat.h"
#include "wrapper/string/my_strlen.h"
char *core_strncat(
char *ap_Destination,
const char *ap_Source,
size_t av_Count)
{
size_t lv_DestinationLength;
size_t lv_Index;
if (NULL == ap_Destination) {
return NULL;
}
if (NULL == ap_Source) {
return NULL;
}
if (0 == av_Count) {
return ap_Destination;
}
lv_DestinationLength = my_strlen(ap_Destination);
for (lv_Index = 0;
(lv_Index < av_Count) && ('\0' != ap_Source[lv_Index]);
lv_Index++) {
ap_Destination[lv_DestinationLength + lv_Index]
= ap_Source[lv_Index];
}
ap_Destination[lv_DestinationLength + lv_Index] = '\0';
return ap_Destination;
}
この実装では、コピー先の文字列長を my_strlen() で取得しています。
例えば、
dest = "ABC"
src = "DEF"
n = 3
の場合、
lv_DestinationLength = 3
となります。
その後、
A B C
↓
A B C D E F
と追加し、最後に '\0' を書き込みます。
結果は、
ABCDEF\0
となります。
一方、
dest = "ABC"
src = "DEFGHI"
n = 2
の場合は、
ABCDE\0
となります。
最大2文字だけ追加するためです。
2.3 ラッパー関数のヘッダーファイル
次に、利用側から呼び出されるラッパー関数のインターフェースを定義します。
#ifndef WRAPPER_STRING_MY_STRNCAT_H
#define WRAPPER_STRING_MY_STRNCAT_H
#include <stddef.h>
#include "myc/myc_define.h"
/*
* 標準関数: strncat.
*
* 文字列を指定したバイト数まで追加します.
*
* 引数:
* ap_Destination : 連結先の文字列.
* ap_Source : 追加する文字列.
* av_Count : 追加する最大バイト数.
*
* 戻り値:
* 正常 : 連結先へのポインタ.
* NULL入力 : NULL.
*
* 標準strncatとの相違点:
* 標準strncatではNULLポインタを渡した場合の動作は保証されません.
* 本関数ではNULLを独自に処理し、NULLを返します.
*/
EXTERN char *my_strncat(
char *ap_Destination,
const char *ap_Source,
size_t av_Count);
#endif
こちらのヘッダーファイルにもインクルードガードを付けています。
また、標準 strncat() と同じ引数の型を提供します。
2.4 ラッパー関数の実装
ラッパー関数では、利用側から呼び出されるmy_strncat()を実装します。
#include "core/string/core_strncat.h"
#include "wrapper/string/my_strncat.h"
char *my_strncat(
char *ap_Destination,
const char *ap_Source,
size_t av_Count)
{
return core_strncat(
ap_Destination,
ap_Source,
av_Count);
}
ラッパー自身は連結処理を行わず、コア関数へ処理を委譲しています。
今回の実装では、
my_strncat()
↓
core_strncat()
↓
my_strlen()
↓
連結処理
という役割分担になります。
ここまでで、実際に動作するmy_strncat()の実装を確認しました。
ただし、このコードには、本プロジェクト独自の仕様やコーディング規約が反映されています。
🔗 関連記事・関連リンク
この記事の最新アップデートや、このシリーズの関連記事は以下のリンクからご覧いただけます。
内容は基本的に同じですので、お好みのサイトでお読みください。
🟢 Zenn
-
この記事の最新版はこちら。
-
「標準関数自作」Topicの関連記事はこちら。
🔵 Qiita
-
「標準関数自作」タグの関連記事はこちら。
🧡 note
-
このシリーズの記事はこちら。
-
要件定義以降は、有料パートとしてこちらで公開。