C言語の標準関数を自作する:文字列処理 strncpy()編
目次
はじめに
C言語には、文字列をコピーするための標準関数として strcpy() が用意されています。また、コピーする最大バイト数を指定できる strncpy() も存在します。
今回は、この strncpy() を標準Cライブラリの strncpy() を利用せずに自作します。
このプロジェクトでは、標準関数と同じインターフェースを提供するラッパー関数と、実際の処理を行うコア関数を分離する構成を採用しています。
今回作成する strncpy() は、以下のような構成とします。
利用側
↓
my_strncpy()
↓
core_strncpy()
my_strncpy() が利用側から呼び出され、実際の文字列コピー処理は core_strncpy() が担当します。
なお、本プロジェクトでは、自作した標準関数模擬関数であるラッパー関数を、他の自作関数の実装から利用することを許容しています。
つまり、標準Cライブラリの関数を直接利用することは禁止しますが、自作した標準関数模擬関数(my_**()関数)については、必要に応じて他の自作関数から利用することができます。
1. strncpy()の仕様を確認
まず、標準関数としての strncpy() の仕様を確認します。
Linux環境では、以下のコマンドでmanページを確認できます。
man strncpy
オンラインでは、Linux man-pages の strncpy(3) を参照できます。
1.1 関数プロトタイプ
strncpy() のプロトタイプは以下です。
char *strncpy(char *restrict dest, const char *restrict src, size_t n);
第1引数の dest は、コピー先の領域です。
第2引数の src は、コピーする文字列です。
第3引数の n は、コピーする最大バイト数です。
restrict は、ポインタを介したアクセスについて、他のポインタとの別名関係がないことをコンパイラに示すために使用されています。
strncpy() は、src から最大 n バイトを dest にコピーします。
例えば、
char lv_Dest[32] = "";
const char *lp_Source = "Hello";
strncpy(lv_Dest, lp_Source, 3);
とした場合、
実行前
(空)
実行後
Hel(3バイトのみコピー)
となります。
戻り値として、コピー先である dest の先頭アドレスが返されます。
1.2 strcpy()とstrncpy()の違い
strcpy() と strncpy() は、どちらもコピー元からコピー先へ文字列の内容をコピーする関数ですが、コピー方法が異なります。
strcpy() は、コピー元の終端 '\0' までコピーします。
char lv_Dest[32] = "";
strcpy(lv_Dest, "Hello");
の場合、
Hello\0
となります。
一方、strncpy() では、第3引数によってコピーする最大バイト数を指定します。
char lv_Dest[32] = "";
strncpy(lv_Dest, "Hello", 3);
の場合、
Hel
となります。
ただし、strncpy() は単純に「安全な strcpy()」というわけではありません。
コピーする最大バイト数を指定できますが、コピー先の領域がそのサイズ以上あることを自動的に確認するわけではありません。
また、コピー元が n バイト以上ある場合、終端 '\0' はコピーされません。
そのため、strncpy() は「コピーする最大バイト数を指定できる関数」であり、一般的な意味での安全な文字列コピー関数とは限らないことを理解しておく必要があります。
1.3 戻り値
strncpy() は、コピー先の dest の先頭へのポインタを返します。
例えば、
char lv_Dest[32] = "";
char *lp_Result = strncpy(lv_Dest, "Hello", 5);
とした場合、
lp_Result == lv_Dest
となります。
1.4 エッジケース:コピー数がゼロの場合
n がゼロの場合、文字のコピーは行われません。
例えば、
char lv_Dest[32] = "World";
strncpy(lv_Dest, "Hello", 0);
とした場合、dest の内容は変化せず、
World
のままです。
戻り値は dest の先頭アドレスです。
1.5 文字列終端について
strncpy() は、src の長さが n より小さい場合、残りの領域を '\0' で埋めます。
例えば、
char lv_Dest[32] = "";
strncpy(lv_Dest, "Hi", 5);
の場合、
H i \0 \0 \0
となります。
一方、src の長さが n 以上の場合、'\0' は追加されません。
例えば、
char lv_Dest[32] = "";
strncpy(lv_Dest, "Hello", 5);
では、コピーされる5バイトは、
H e l l o
です。
この場合、strncpy() 自身は終端 '\0' を追加しません。
ここで重要なのは、strncpy() が常に「文字列」を作るとは限らないという点です。
コピー元の長さが n 以上の場合、コピー先には終端 '\0' が存在しない可能性があります。
1.6 NULLポインタについて
標準 strncpy() に NULL ポインタを渡した場合、その動作は未定義です。
そのため、
strncpy(NULL, "Hello", 5);
strncpy(lv_Dest, NULL, 5);
のような呼び出しを、標準 strncpy() の正常な仕様として扱うことはできません。
今回の自作関数では、この標準仕様とは別に、独自の安全性要件として NULL ポインタを処理します。
2. 実装
ここまでで、標準 strncpy() の基本的な仕様を確認しました。
では、先ほど確認した仕様を参考に、実際に strncpy() を自作してみます。
今回のプロジェクトでは、利用側から呼び出されるラッパー関数と、実際の処理を行うコア関数を分離しています。
実装はこちらです。
2.1 コア関数のヘッダーファイル
まず、コア関数のインターフェースをヘッダーファイルに定義します。
#ifndef CORE_STRING_CORE_STRNCPY_H
#define CORE_STRING_CORE_STRNCPY_H
#include <stddef.h>
#include "myc/myc_define.h"
EXTERN char *core_strncpy(char *restrict ap_Destination,
const char *restrict ap_Source,
size_t av_Size);
#endif
第1引数はコピー先の領域なので char * とします。
第2引数は読み取り専用の文字列なので const char * とします。
また、標準 strncpy() のインターフェースに合わせて、これらのポインタには restrict を付けています。
第3引数はコピーする最大バイト数なので size_t とします。
戻り値はコピー後の第1引数の先頭アドレスなので char * とします。
ヘッダーファイルには、プロジェクトのコーディングルールに従い、インクルードガードを付けています。
インクルードガードは、同じヘッダーファイルが複数回インクルードされた場合に、定義が重複することを防ぐためのものです。
また、EXTERN はプロジェクト共通で使用するマクロです。
2.2 コア関数の実装
コア関数では、実際の文字列コピーを行います。
#include "core/string/core_strncpy.h"
char *core_strncpy(char *restrict ap_Destination,
const char *restrict ap_Source,
size_t av_Size)
{
size_t lv_Index = 0;
if (NULL == ap_Destination) {
return NULL;
}
if (NULL == ap_Source) {
return NULL;
}
if (0 == av_Size) {
return ap_Destination;
}
while ((lv_Index < av_Size) && ('\0' != ap_Source[lv_Index])) {
ap_Destination[lv_Index] = ap_Source[lv_Index];
lv_Index++;
}
while (lv_Index < av_Size) {
ap_Destination[lv_Index] = '\0';
lv_Index++;
}
return ap_Destination;
}
この実装では、まず NULL チェックとサイズゼロチェックを行います。
その後、第2引数の文字を第1引数にコピーし、コピー元が '\0' に到達してコピーが終了した場合は、不足分を '\0' で埋めています。
コピー元が av_Size バイト以上ある場合は、av_Size バイトまでコピーした時点で終了するため、終端 '\0' は追加されません。
2.3 ラッパー関数のヘッダーファイル
次に、利用側から呼び出されるラッパー関数のインターフェースを定義します。
#ifndef WRAPPER_STRING_MY_STRNCPY_H
#define WRAPPER_STRING_MY_STRNCPY_H
#include <stddef.h>
#include "myc/myc_define.h"
/*
* 標準関数: strncpy.
*
* 指定した文字列から、最大指定バイト数までをコピーします.
*
* 引数:
* ap_Destination : コピー先の文字列.
* ap_Source : コピー元の文字列.
* av_Size : コピーする最大バイト数.
*
* 戻り値:
* コピー成功 : コピー先文字列の先頭アドレス.
* NULL入力 : NULL.
*
* 標準strncpyとの相違点:
* 標準strncpyではNULLポインタを渡した場合の動作は保証されません.
* 本関数ではNULLを独自に処理し、NULLを返します.
*/
EXTERN char *my_strncpy(char *restrict ap_Destination,
const char *restrict ap_Source,
size_t av_Size);
#endif
標準 strncpy() の関数プロトタイプに合わせ、ポインタ型の引数には restrict を付けています。
2.4 ラッパー関数の実装
ラッパー関数では、利用側から呼び出される my_strncpy() を実装します。
#include "core/string/core_strncpy.h"
#include "wrapper/string/my_strncpy.h"
char *my_strncpy(char *restrict ap_Destination,
const char *restrict ap_Source,
size_t av_Size)
{
return core_strncpy(ap_Destination, ap_Source, av_Size);
}
ラッパー自身は文字列コピー処理を行わず、コア関数へ処理を委譲しています。
また、標準 strncpy() と同様に、ポインタ型の引数には restrict を付けています。
ここまでで、実際に動作する strncpy() の実装を確認しました。
ただし、このコードには、本プロジェクト独自の仕様やコーディング規約が反映されています。
🔗 関連記事・関連リンク
この記事の最新アップデートや、このシリーズの関連記事は以下のリンクからご覧いただけます。
内容は基本的に同じですので、お好みのサイトでお読みください。
🟢 Zenn
-
この記事の最新版はこちら。
-
「標準関数自作」Topicの関連記事はこちら。
🔵 Qiita
-
「標準関数自作」タグの関連記事はこちら。
🧡 note
-
このシリーズの記事はこちら。
-
要件定義以降は、有料パートとしてこちらで公開。