C言語の文字列の切り出し関数を作る: strncpy, trim

354, 2021-11-29

目次

C言語の文字列の切り出し関数を作る

C言語には他の言語のように文字列の部分文字列を切り出すという関数はありません。
strncpy()で代用することも可能ですが、strncpy()はこのような用途を目的とした関数ではないため色々ハマりやすい罠があります。
この記事では文字列の部分文字列を切り出すtrim()という自作関数を作ります
テストも書いてあるので興味がある方は使ってみてください。

文字列の切り出しは色々なシーンで使われます。
特に文字列処理では、文字列の加工で必要になる時があります。
文字列の部分文字列を切り出せる関数をストックしておくのはC言語のプログラミングで有用と言えるでしょう。

この記事では具体的に↓のコンテンツを解説していきます。

  • 文字列の切り出しとは?

  • C言語の文字列の切り出し方法

  • strncpy()を使った切り出し方法

  • strncpy()の問題点

  • 自作関数trim()を作る

関連記事
C言語の文字列の配列の使い方
C言語で文字列の長さを取得する: strlen, wcslen
C言語の文字列の切り出し関数を作る: strncpy, trim
C言語で文字列を比較する方法: strcmp, strncmp, streq
C言語の文字列を初期化する方法: 文字配列、文字列ポインタの初期化

文字列の切り出しとは?

文字列の切り出しとは何でしょうか?
文字列の切り出しとは、文字列から部分文字列を抽出することを言います

つまり「abcdef」という文字列から「abc」という部分文字列を抽出するのが切り出しです。
文字列の切り出しは色々なシーンで使われます。
たとえばURLからプロトコル部分を切り出したい場合があります。
https://yu-nix.com」というURLがあり、これの「https」部分を切り出すわけです。
またはURLのドメイン部分を切り出すというのも考えられます。
その場合は切り出した文字列は「yu-nix.com」になります。

文字列を切り出す関数は、モダンなプログラミング言語では標準で備わっていることが多いです。
しかし歴史あるC言語にはそういった標準関数は備わっていません
「無ければ作ろう」の精神で、C言語ではこういった場合は外部ライブラリを探すか、自分で制作するのが一般的です。

C言語の文字列の切り出し方法

C言語の文字列の切り出し方法です
切り出し対象の文字列が文字列定数で、保存先がchar型の文字配列である場合です。

その場合はまず「切り出しの開始点」と「切り出す長さ」が必要になります。
この開始点と切り出す長さは、切り出し対象の文字列の長さの範囲に収まっていないといけません
切り出し範囲が文字列の長さの範囲外、あるいは保存先文字列のサイズを超えている場合は、これを考慮する必要があります。
その場合はたとえば切り出しを中途で終わらせたり、保存先を空文字列にするなどの工夫が必要になります。

対象の文字列のポインタと開始点を演算して、開始点のポインタを取り出します。
ここは今回はポインタで実装してますが、添え字を使っても良いです。
同時に対象の文字列のポインタと開始点、切り出しの長さを演算して、終了点のポインタを取り出します。

保存先の文字配列に開始点のポインタを実体化して文字を代入していきます。
それと同時に保存先の文字配列の添え字と開始点のポインタをインクリメントしていきます。
ループは開始点のポインタが終了点のポインタに達したら終了になります。

最後に保存先の文字配列にナル文字を代入して、文字配列が文字列として機能するようにセットします。

これで文字列から部分文字列を保存先の文字配列に保存できました。
あとは保存先文字配列をプログラムの中で活用して処理を書いていきます。

strncpy()を使った切り出し方法

strncpy()は第1引数のバッファに第2引数の文字列を第3引数の数だけコピーする関数です。
この関数を使うと部分文字列の抽出処理を代用することができます。

↓のように行います。

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "abcdef";

    char d1[100];
    strncpy(d1, s, 3);
    d1[3] = '\0';
    printf("d1[%s]\n", d1);  // d1[abc]

    char d2[100];
    strncpy(d2, s + 3, 3);
    d2[3] = '\0';
    printf("d2[%s]\n", d2);  // d2[def]

    char d3[100];
    strncpy(d3, s + 6, 0);
    d3[0] = '\0';
    printf("d3[%s]\n", d3);  // d3[]

    return 0;
}

↑の場合「abcdef」という文字列sstrncpy()で切り出しています。
保存先のバッファはd1, d2, d3がそうです。

strncpy()の第1引数には保存先のバッファを指定します。
保存先バッファは十分なサイズを確保するようにします。

そしてstrncpy()の第2引数には文字列のアドレスを渡します。
文字列の先頭から切り出したい場合はsをそのまま渡します。
sの3文字目から切り出しを始めたい場合はs + 3のように演算した結果を渡します。

第3引数には切り出したい長さを渡します。
これが3の場合は3文字分の切り出しになります。

strncpy()を使い終わったら、保存先バッファにナル文字をセットします。
これはコピーした長さの位置にナル文字をセットします。

strncpy()の問題点

便利なstrncpy()ですが、文字列から部分文字列を切り出す場合、いろいろな面倒な仕様が存在します。
それはおもに↓の2つになります。

  • ナル文字が自動で付加されない

  • バッファーオーバーフローする可能性がある

ナル文字が自動で付加されない

strncpy()は第2引数の文字列の長さが第3引数の値以上だった場合、ナル文字を自動で付加しません
そのため文字列の切り出しにstrncpy()を使いたい場合は、ナル文字を手動で付加する必要があります。

第2引数の文字列の長さが第3引数の値より小さかった場合は保存先バッファの一部分はナル文字で埋められます。
しかし、この仕様は文字列の切り出しという点においてハマりやすい罠になりがちです。

なぜstrncpy()がこのような仕様になっているかというと、こうしたほうがstrncpy()の本来の用途には合っているからです。
つまり↓のようなコードが書けるわけです。

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "Goood";
    char dst[100] = "Hello, World!";

    strncpy(dst, s, 5);
    printf("dst[%s]\n", dst);  // dst[Goood, World!]

    return 0;
}

↑のように保存先にナル文字を付加しなければ、保存先の文字配列を部分的に置き換えることができます。
strncpy()は本来こういった用途の関数なので、文字列の切り出しに使う場合は注意が必要です。

バッファーオーバーフローする可能性がある

それからstrncpy()保存先バッファのサイズを指定できません
そのためバッファーオーバーフローになる可能性があります

たとえば保存先バッファのサイズが10で、文字列の長さが100で、切り出す長さが50だった場合です。
この場合はバッファのサイズが切り出す長さ未満のためバッファーオーバーフローになります。
なぜそうなるのかというと、strncpy()保存先のバッファのサイズを知るすべがないからです。
strncpy()の第1引数には保存先バッファを指定できますが、保存先バッファのサイズは指定できません。
そのためstrncpy()は保存先バッファのサイズを知らずにコピーを実行します。
そのため場合によってはバッファーオーバーフローが起こります。
バッファーオーバーフローはプログラムの脆弱性になります。

自作関数trim()を作る

文字列の部分文字列を抽出する関数がないのであれば自分で作ろうというのが一般的なC言語脳です。

いや、外部ライブラリ使おうよ

ほんまやで

ここでは自作関数trim()を紹介します。
trim()のコードは↓になります。

#include <stdio.h>
#include <string.h>
#include <assert.h>

/**
 * 文字列sの部分文字列を切り取りdstに保存する
 * LICENSE: MIT
 * 
 * @param[out] dst   保存先バッファ
 * @param[in]  dstsz dstのサイズ
 * @param[in]  beg   切り取りの開始点
 * @param[in]  len   切り取りの長さ
 * @param[in]  s     対象の文字列
 *
 * @return dstのアドレス
 */
char *trim(char *dst, size_t dstsz, int beg, int len, const char *s) {
    // 引数のチェック
    if (!dst || !dstsz || beg < 0 || len < 0 || !s) {
        return NULL;
    }

    size_t slen = strlen(s);
    const char *b = s + beg;  // 切り取りの開始点
    const char *e = b + len;  // 切り取りの終了点
    const char *send = s + slen;  // 文字列の終了点
    size_t i;

    // ループを回してsの部分文字列を抽出する
    for (i = 0; i < dstsz - 1 && b < e && b < send; b += 1, i += 1) {
        dst[i] = *b;
    }

    dst[i] = '\0';  // ナル文字を保存

    return dst;
}

int main(void) {
    // 異常系テスト
    char dst[100];
    assert(trim(NULL, sizeof dst, 0, 3, "abcdef") == NULL);
    assert(trim(dst, 0, 0, 3, "abcdef") == NULL);
    assert(trim(dst, sizeof dst, -1, 3, "abcdef") == NULL);
    assert(trim(dst, sizeof dst, 0, -1, "abcdef") == NULL);
    assert(trim(dst, sizeof dst, 0, 3, NULL) == NULL);

    // 正常系テスト
    assert(!strcmp(trim(dst, sizeof dst, 0, 3, "abcdef"), "abc"));
    assert(!strcmp(trim(dst, sizeof dst, 3, 3, "abcdef"), "def"));
    assert(!strcmp(trim(dst, sizeof dst, 4, 10, "abcdef"), "ef"));
    assert(!strcmp(trim(dst, sizeof dst, 10, 3, "abcdef"), ""));
    assert(!strcmp(trim(dst, sizeof dst, 0, 6, "abcdef"), "abcdef"));
    assert(!strcmp(trim(dst, sizeof dst, 0, 0, ""), ""));
    assert(!strcmp(trim(dst, sizeof dst, 3, 0, "abcdef"), ""));

    char shrt[3];
    assert(!strcmp(trim(shrt, sizeof shrt, 0, 3, "abcdef"), "ab"));

    // 使用例
    char buf[100];
    const char *s = "abcdef";

    trim(buf, sizeof buf, 2, 3, s);  // 2文字目から3文字分切り出す
    printf("buf[%s]\n", buf);  // buf[cde]

    return 0;
}

trim()関数は第1引数に保存先バッファ、第2引数に保存先バッファのサイズをとります。
第3引数に切り出し位置の開始点、第4引数に切り出す長さをとります。
第5引数には切り出す対象文字列を取ります。

trim()は↓のように使うことができます。

    // 使用例
    char buf[100];
    const char *s = "abcdef";

    trim(buf, sizeof buf, 2, 3, s);  // 2文字目から3文字分切り出す
    printf("buf[%s]\n", buf);  // buf[cde]

↑の場合、trim()を使って「abcdef」という文字列から「cde」を切り出しています。

trim()は引数が不正だった場合はNULLポインタを返します。
そのためちゃんと使う場合はtrim()の返り値をチェックします。

trim()は保存先バッファのサイズを指定できるので、strncpy()を使うよりは安全に使えます。
しかし第2引数の保存先バッファのサイズが間違っていた場合はstrncpy()と同様にバッファーオーバフローになる可能性があります。

おわりに

今回はC言語の文字列の部分文字列を抽出する関数trim()を作ってみました
trim()はライセンスはMITなのでライセンスの範囲で好きに使ってください。

部分文字列を切り出す

シャープにやるぜ