string.hライブラリの主要関数の実装方法
組み込みソフトウェア開発の面接において、プログラミング問題は候補者の基礎能力と実践的なスキルを評価する重要な要素です。多くの面接問題の中でも、標準ライブラリの一般的な関数を再実装することは、候補者のプログラミング技術と基本概念の理解度を両方示すことができます。文字列操作に関連する一般的な関数を実装することは、面接でよくある筆記試験問題です。この記事では、主に文字列のコピー、連結、比較、検索、長さの計算、およびメモリ操作といったstring.hライブラリの主要な関数の実装方法に焦点を当てます。これらの実装例を通じて、読者が面接準備をより良く行うだけでなく、標準ライブラリ関数の内部実装に対する理解を深めるのに役立ちます。
[!注意]
この記事ではstring.hのすべての関数を説明するわけではなく、比較的一般的な面接でよく出題される関数のみを紹介します。
一、文字列のコピー
1. strcpy(NULL終了文字までの文字列コピー)
strcpyはstring copy(文字列コピー)の略称で、srcアドレスから始まりNULL終了文字を含む文字列をdestから始まるアドレス空間にコピーする関数です。関数のプロトタイプ宣言は以下の通りです:
char *strcpy(char *dest, const char *src);
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
char *duplicateString(char *target, const char *source)
{
if (NULL == target || NULL == source)
return target;
char *result = target;
char *current = source;
while (*current != '\0') {
*target = *current;
target++;
current++;
}
*target = '\0';
return result;
}
上記のコードでは、まずsrcとdestがNULLポインタでないか確認します。次に、一時ポインタresultを使ってdestポインタの代わりに走査し、戻り値の際に直接destポインタを使用できるようにします。
[!注意]
なぜ
do......whileではなくwhileを使用するのか?
strcpy関数の機能によると、すべての文字列をコピーした後、さらに'\0'(空文字)をコピーします。whileループを使用すると、srcポインタがすでに空文字を指しているとすぐに走査が終了し、空文字が宛先アドレスにコピーされません。そのため、do......whileを使用することで先にコピーしてから判断し、空文字がコピーされることを保証します。
面接で上記のコードを即席で書くことができれば問題ありません。もちろん、以下のような簡略版も使用できます:
char *copyString(char *destination, const char *source)
{
if (NULL == destination || NULL == source)
return destination;
char *ptr = destination;
while ((*ptr++ = *source++));
return destination;
}
while ((*ptr++ = *source++));という行は、ポインタ操作を使用して文字を走査およびコピーしています。ここで、*destination++ = *source++文は以下の操作を行います:
*sourceでソース文字列の現在の文字を取得します。*destinationでその文字を宛先文字列の現在の文字位置に保存します。- その後、2つのポインタをそれぞれインクリメントして次の文字位置を指します。
[!注意]
なぜここで
whileを使用するのか?
srcが空文字を指す場合、まずdestにコピーし、その後whileが*destの値が空文字かどうかを判断するからです。
2. strncpy(固定長の文字列コピー)
strncpy関数(string copy with n)も文字列をコピーする関数ですが、長さを指定できる点が異なります。srcが指す文字列の先頭nバイトをdestが指す配列にコピーし、コピー後のdestを返します。関数のプロトタイプ宣言は以下の通りです:
char *strncpy(char *dest, const char *src, size_t n)
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
char *copyLimitedString(char *target, const char *source, size_t limit)
{
if (NULL == target || NULL == source || 0 == limit)
return target;
char *result = target;
size_t copied = 0;
while (copied < limit && *source != '\0') {
*target = *source;
target++;
source++;
copied++;
}
while (copied < limit) {
*target = '\0';
target++;
copied++;
}
return result;
}
コードの前半部分はstrcpyと同じですが、後半部分では最初のwhileループで、*sourceが空文字でなく、コピーする文字数が指定された文字数以内であるかを判断します。2つ目のwhileループは最初のループが早期に終了した場合(つまりsourceの文字数がnより小さい場合)に実行され、宛先文字列の残りの部分を空文字で埋めます。
[!注意]
なぜ最初の
whileをwhile (copied < limit && *source != '\0')と書かず、もっと簡潔な形にしないのか?
sourceが指す文字列の長さがnより大きい場合を考慮する必要があります。while (copied-- && (*target++ = *source++))のような書き方をすると、copiedが0になった後にもデクリメントが続けられ、負の値になってしまいます。これにより、targetが境界を越え、メモリ破壊を引き起こす可能性があります。
二、文字列の連結
1. strcat(文字列の末尾に別の文字列を連結)
strcatはstring concatenationの略称です。この関数は、1つの文字列を別の文字列の末尾に連結するために使用されます。具体的には、strcat関数はソース文字列の内容を宛先文字列の末尾にコピーし、新しい結合された文字列を終了させるためのnull終了文字を自動的に追加します。関数のプロトタイプ宣言は以下の通りです:
char *strcat(char *dest, const char *src);
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
char *appendString(char *target, const char *source)
{
if (NULL == target || NULL == source)
return target;
char *ptr = target;
while (*ptr != '\0')
ptr++;
while (*source != '\0') {
*ptr = *source;
ptr++;
source++;
}
*ptr = '\0';
return target;
}
最初のwhileループの目的は、ptrポインタを宛先文字列の末尾(つまり空文字の位置)に移動させることです。その後、ソース文字列の内容を宛先文字列の末尾に1文字ずつコピーします。大部分のコードの機能は文字列のコピーと同じなので、ここでは詳しく説明しません。
2. strncat(指定長の文字列を別の文字列の末尾に連結)
strncatは"string concatenate with n"の略称で、"string concatenate with length limit"の全称です。この関数は、ソース文字列の指定長の文字を宛先文字列の末尾に追加し、新しい結合された文字列を終了させるためのnull終了文字を自動的に追加します。関数のプロトタイプ宣言は以下の通りです:
char *strncat(char *dest, const char *src, size_t n);
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
char *appendLimitedString(char *target, const char *source, size_t limit)
{
if (NULL == target || NULL == source || 0 == limit)
return target;
char *ptr = target;
size_t appended = 0;
while (*ptr != '\0')
ptr++;
while (appended < limit && *source != '\0') {
*ptr = *source;
ptr++;
source++;
appended++;
}
*ptr = '\0';
return target;
}
コードの具体的な説明は前のcopyLimitedStringとappendStringと同じなので、ここでは展開しません。
三、文字列の比較
1. strcmp(2つの文字列の内容を比較)
strcmpは"string compare"の略称です。この関数は2つの文字列の辞書順を比較するために使用されます。2つの文字列の文字を1つずつ比較し、異なる文字が見つかるか終了のnull文字に遭遇するまで比較を続けます。関数のプロトタイプ宣言は以下の通りです:
int compareStrings(const char *str1, const char *str2);
strcmpの戻り値は整数型で、3つのケースがあります。-1、0、1の3つのケースは、いずれも異なる文字が比較されたときに2つの文字を直接差し引いて得られる結果です。具体的な状況は以下の通りです:
str1がstr2より前にある場合、戻り値は-1です。str1がstr2と等しい場合、戻り値は0です。str1がstr2より後にある場合、戻り値は1です。
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
int stringCompare(const char *first, const char *second)
{
if (first == second)
return 0;
while (*first != '\0' && *first == *second) {
first++;
second++;
}
if (*first == '\0' && *second == '\0')
return 0;
else if (*first > *second)
return 1;
else
return -1;
}
上記のコードでは、まず2つのアドレスが同じかどうかを判断します。同じであれば、文字列も同じなので0を返します。whileループでは、firstが現在指す文字が空文字でないこと、そしてfirstとsecondがそれぞれ指す文字が同じであることの2つの条件を同時に満たしている場合、2つのポインタをそれぞれ増分してオフセットします。
最後の戻り値では、三項演算子内に別の三項演算子をネストして差値判断を行い、0かどうかを先に判断し、0であれば0を返し、そうでなければ正数か負数かを判断して、正数なら1を、負数なら-1を返します。
[!注意]
2つのパラメータの1つにNULLが渡された場合、セグメンテーションエラーが発生しますが、なぜこのコードには解決策が示されていないのか?
strcmpも同様で、パラメータにNULLが1つしかない場合、セグメンテーションエラーが発生します。2つのNULLが渡された場合は0を返すため、私は元のstrcmpが持つべき機能を保つためにこのstringCompareを書きました。
2. strncmp(指定長の2つの文字列の内容を比較)
strncmpはstrcmpに長さの比較を追加したもので、2つの文字列の先頭n文字の辞書順を比較するために使用されます。2つの文字列の文字を1つずつ比較し、異なる文字が見つかるか、指定されたn文字が比較されたか、空文字に遭遇するまで比較を続けます。関数のプロトタイプ宣言は以下の通りです:
int compareLimitedStrings(const char *str1, const char *str2, size_t n);
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
int limitedStringCompare(const char *first, const char *second, size_t limit)
{
if (first == second)
return 0;
size_t compared = 0;
while (compared < limit && *first != '\0' && *first == *second) {
first++;
second++;
compared++;
}
if (compared == limit || (*first == '\0' && *second == '\0'))
return 0;
else if (*first > *second)
return 1;
else
return -1;
}
四、文字列の検索
1. strchr(文字列で最初に現れる文字を検索)
strchrは"string character"の略称で、"string character search"の全称です。この関数は文字列で最初に現れる指定された文字を検索し、その文字を指すポインタを返します。関数のプロトタイプ宣言は以下の通りです:
char *findCharacter(const char *str, int c);
検索対象の文字列strで、cで指定された文字を検索します。渡される型はintですが、実際の比較時にはcharに変換されます。文字列strでcが見つかれば、文字列で最初に現れる文字cを指すポインタを返します。見つからなければNULLを返します。
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
char *searchChar(const char *text, int character)
{
const char *current = text;
while (*current != '\0' && *current != (char)character)
current++;
if (*current == (char)character)
return (char *)current;
return NULL;
}
上記のコードでは、whileループは*currentを判断すると同時に、*currentが(char)characterと一致するかどうかも判断します。したがって、ループを抜けるには2つの場合があります:1つはstrの走査が終了した場合で、cが空文字(cが'\0'の可能性がある)の場合はstrの現在のアドレスを返し、それ以外の場合はNULLを返します。もう1つはstrの走査が終了する前に、cと一致する文字が見つかった場合で、strの現在のアドレスを返します。
[!注意]
なぜ
strを返す際にchar *型にキャストする必要があるのか?直接返せないのか?関数の戻り値の型は
char *ですが、strはconst char *型です。直接返すことに問題はありませんが、コンパイル時に型の不一致に関する警告が出るため、ここでは警告を消すためにキャストしています。
2. strrchr(文字列で最後に現れる文字を検索)
strrchrは"string reverse character"の略称で、"string reverse character search"の全称です。この関数は文字列で最後に現れる指定された文字を検索し、その文字を指すポインタを返します。関数のプロトタイプ宣言は以下の通りです:
char *findLastCharacter(const char *str, int c);
strchr関数と同様に、文字列strでcが見つかれば、文字列で最後に現れる文字cを指すポインタを返します。見つからなければNULLを返します。
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
char *findLastOccurrence(const char *text, int character)
{
const char *lastPosition = NULL;
const char *current = text;
while (*current != '\0') {
if (*current == (char)character) {
lastPosition = current;
}
current++;
}
if (*current == (char)character)
return (char *)current;
return (char *)lastPosition;
}
上記のコードでは依然としてwhileループを使用した走査方法を用いています。走査の過程で一致する文字を探し、文字が見つかると最新のアドレスをlastPositionポインタに保存して続け、再度指定された文字が見つかるとすぐにlastPositionポインタを更新し、文字列全体の走査が終了するまで続けます。
3. strstr(文字列で部分文字列を検索)
strstrは"string substring"の略称で、"string substring search"の全称です。この関数は文字列で最初に現れる部分文字列を検索し、その部分文字列の開始位置を指すポインタを返します。関数のプロトタイプ宣言は以下の通りです:
char *findSubstring(const char *haystack, const char *needle);
ここで、haystackは検索する主文字列で、needleは検索する部分文字列です。needleがhaystackの部分文字列であれば、主文字列で最初に現れる部分文字列を指すポインタを返し、そうでなければNULLを返します。
char *locateSubstring(const char *mainText, const char *subText)
{
if (*subText == '\0')
return (char *)mainText;
const char *mainPtr = mainText;
const char *subPtr = subText;
const char *matchStart = NULL;
while (*mainPtr != '\0') {
if (*mainPtr == *subText) {
matchStart = mainPtr;
subPtr = subText;
while (*mainPtr != '\0' && *subPtr != '\0' && *mainPtr == *subPtr) {
mainPtr++;
subPtr++;
}
if (*subPtr == '\0')
return (char *)matchStart;
mainPtr = matchStart + 1;
} else {
mainPtr++;
}
}
return NULL;
}
strstr関数のコードは、これまでに述べたすべての関数よりもはるかに複雑であることがわかります。以下にコードの分析を示します:
- 空文字列チェック:まず
subTextが空文字列かどうかをチェックします(元のstrstr関数も同様)、空文字列であればmainTextのポインタを直接返します。 - 逐文字比較:
- 外層の
while (*mainPtr != '\0')ループでmainText文字列を走査します。 - 最初の一致する文字が現れた後、内層の
whileループでmainTextとsubTextを1文字ずつ比較し、文字が一致しないかsubTextの末尾に達するまで続けます。
- 一致チェック:
subTextのすべての文字が一致した場合、つまり*subPtr == '\0'であれば、matchStartの位置を返します。 - 検続続行:現在の文字が一致しない場合、
mainPtrを次の文字に移動して検索を続けます。 - 見つからない場合は
NULLを返す:mainTextを走査してもsubTextが見つからない場合、NULLを返します。
五、文字列の長さ計算
1. strlen(文字列の長さを計算)
strlenは"string length"の略称で、文字列の長さ、つまり文字列内の文字の数(終了空文字を含まない)を計算するために使用されます。関数のプロトタイプ宣言は以下の通りです:
size_t calculateStringLength(const char *str);
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
size_t getStringLength(const char *text)
{
const char *current = text;
size_t length = 0;
while (*current != '\0') {
length++;
current++;
}
return length;
}
コードは非常に短いため、ここでは詳しく説明しません。
2. strnlen(指定数の範囲内で文字列の最大長を計算)
strnlenは"string length with n"の略称で、"string length with length limit"の全称です。この関数は文字列の長さを計算しますが、指定された最大文字数までしかチェックしません。関数のプロトタイプ宣言は以下の通りです:
size_t getLimitedStringLength(const char *str, size_t maxlen);
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
size_t getSafeStringLength(const char *text, size_t maxLen)
{
const char *current = text;
size_t count = 0;
while (count < maxLen && *current != '\0') {
current++;
count++;
}
return count;
}
六、メモリ操作
1. memcpy(メモリブロックの内容をコピー)
memcpyは"memory copy"の略称で、この関数はソースアドレスから指定されたバイト数を宛先アドレスにコピーするために使用されます。関数のプロトタイプ宣言は以下の通りです:
void *copyMemory(void *dest, const void *src, size_t n);
destは宛先メモリブロックを指すポインタ、srcはソースメモリブロックを指すポインタ、nはコピーするバイト数です。最後に宛先メモリブロックdestのポインタを返します。
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
void *duplicateMemory(void *target, const void *source, size_t size)
{
unsigned char *dest = (unsigned char *)target;
const unsigned char *src = (const unsigned char *)source;
size_t i;
for (i = 0; i < size; i++) {
dest[i] = src[i];
}
return target;
}
ここで注意すべきは型の問題で、メモリコピーを行うためにはunsigned char型にキャストする必要があります。
2. memset(指定文字でメモリブロックを埋める)
memsetは"memory set"の略称で、この関数は指定値をメモリブロックに埋めるために使用され、通常初期化やリセットに使用されます。この関数は「埋め込み」関数とも呼ばれます。関数のプロトタイプ宣言は以下の通りです:
void *fillMemory(void *str, int c, size_t n);
strは埋めるメモリブロックを指すポインタです。cは設定する値で、int型で渡されますが、埋め込み時にはunsigned char型に変換されます。nは埋めるバイト数です。最後にメモリブロックstrを指すポインタを返します。
これと同じ機能を持つ関数を自作する場合、以下のように実装できます:
void *setMemory(void *block, int value, size_t length)
{
unsigned char *ptr = (unsigned char *)block;
size_t i;
for (i = 0; i < length; i++) {
ptr[i] = (unsigned char)value;
}
return block;
}
付録
1. 英語の一般的な略語
関数のパラメータリストには2つの高頻度で出現するパラメータがあります。srcとdestです。その中でsrcはソースオペランドを指し、sourceの略語です。destは宛先オペランドを指し、destinationの略語です。
2. size_t
size_tはオブジェクトのサイズまたは配列のインデックスを表すための符号なし整数型です。CおよびC++標準ライブラリでは、size_tの定義が異なります。異なるコンパイラおよびプラットフォームでは、size_tはいくつかの中間ファイルを介して間接的に定義されることがありますが、主なヘッダーファイルは通常stddef.h(C)およびcstddef(C++)です。
C言語では、stddef.hには以下の内容が含まれることがあります:
#ifndef _STDDEF_H
#define _STDDEF_H
typedef unsigned long size_t;
#endif // _STDDEF_H
C++では、cstddefには以下の内容が含まれることがあります:
#ifndef _CSTDDEF_
#define _CSTDDEF_
#include <stddef.h>
namespace std {
using ::size_t;
}
#endif // _CSTDDEF_
3. なぜ多くの文字列関数は戻り値を必要とするのか?
文字列関数の戻り値の設計には、いくつかの実用的な理由があります。戻り値の設計は、関数の使用をより柔軟で便利にするいくつかの追加の利点を提供します。以下にstrcpy関数を例に説明します。
- チェイン呼び出し:
strcpyは宛先文字列のポインタを返すため、チェイン呼び出しが可能になります。チェイン呼び出しにより、複数の文字列操作関数を連続して使用でき、コードを簡素化できます。
char dest[100];
strcpy(dest, "Hello, ");
strcat(dest, "world!");
// チェイン呼び出しを使用
strcat(strcpy(dest, "Hello, "), "world!");
- より良い関数の組み合わせ:戻り値が宛先文字列のポインタであるため、
strcpyは文字列ポインタを入力として必要とする他の関数とより良く組み合わせることができます。例えば、多くの文字列処理関数(strlenなど)は文字列ポインタをパラメータとして必要とします。
char dest[100];
size_t len = strlen(strcpy(dest, "Hello, world!"));
- コードの一貫性と利便性:C標準ライブラリでは、多くの文字列操作関数が結果へのポインタを返します。例えば、
strcatとstrtokは両方とも結果文字列へのポインタを返します。strcpyもこの設計パターンに従っており、ライブラリ全体の設計をより一貫性があり直感的にします。 - デバッグとエラー処理:
strcpy自体はエラー処理メカニズムを提供していませんが、戻り値は特定の状況下でデバッグ時に便利です。例えば、チェイン呼び出しの中間結果を確認するために、戻り値を出力してコピー操作が成功したかどうかを検証できます。
char dest[100];
printf("Copied string: %s\n", strcpy(dest, "Hello, world!"));
- 歴史的な習慣との互換性:多くの初期のC関数、
strcpyを含むものは、UNIX関数設計の習慣に従っています。この習慣は通常、結果へのポインタを返します。この習慣に従うことで、ライブラリ関数がより広く受け入れられやすくなります。