2006年6月24日

memccpy() で文字列をコピーする

詳解Unixプログラミングを読んでいたところ、標準入出力ライブラリの章に「この例からわかることは、行単位の関数は memccpy(3) を用いて実装されていることである」という記述がありました。

一瞬、memcpy(3) の誤植かと思いましたが、調べてみると 4.3 BSD で追加された関数ということがわかりました。 glibc に入っているので Linux でも使えます。

 

マニュアル によると memccpy() は次のような関数です。

書式
void *memccpy(void *dest, const void *src, int c, size_t n);
説明
memccpy() はメモリ領域src からメモリ領域dest に最大でnバイトコピーする。nバイトコピーする前に文字 c が見つかると、そこでコピーを中止する。
返り値
memccpy() は、dest 中に見つかったc の次にあるキャラクター型の変数を指すポインタを返す。見つからなかった場合、 NULL を返す。

これは確かに fgets() などの行単位の関数を実装するのに便利そうな関数です。memccpy(dest, src, '\n', n) のように使えば、長さ n を超えない範囲で、src から改行まで (改行を含む) を dest に読み込むことができます。ただし、'\0' による終端はやってくれないので、memccpy() の返り値を見て呼び出し側で処理する必要があります。

memccpy() は fgets() の実装以外にも、文字列を安全にコピーしたいときに使えそうです。C における文字列のコピーは意外にも難しい問題です。

  • strcpy(dest, src) は src が dest に収まりきらないときにバッファオーバーランが起きるので危険
  • strncpy(dest, src, n) は n バイトを超えない範囲でコピーを行うが、src から nバイト以内に '\0' がない場合、コピー結果は '\0' で終端されない。また、nバイト以内に '\0' が見つかった場合、残りのバイト数分だけ dest の末尾になぜか \0' が埋められる。きわめて使いにくい関数
  • OpenBSD で採用されているstrlcpy(src, dest, n) は上記の問題を解決しているが、移植性はそれほど高くない。たとえば、glibc には今のところ入っていない
  • glib (glibc ではなく) には strlcpy() 互換の g_strlcpy() が入っているので、 glib を使う場合はこれを使うといい
  • ヒープ上のメモリを使っていいなら dest = strdup(src) が楽だが、free() が必要
  • len = MIN(strlen(src), dest_size - 1); memcpy(dest, src, len); dest[len] = '\0'; のようにすれば大体安全に dest にコピーできるが (dest_size が 0 だと非常にまずい)、境界条件で間違いやすい上に、strlen(), memcpy() と2重に文字列を走査するので非効率

memccpy() を使えば文字列のコピーは次のように書けます。

assert(dest_size > 0);  // 0以下のサイズのバッファを許さない
if (memccpy(dest, src, '\0', dest_size - 1) == NULL) {
    // dest に入りきらない場合は自力で '\0' で終端
    dest[dest_size - 1] = '\0';
}

境界条件は - 1 が入っていて難しいままですが、 strlen() + memcpy() のときよりは多少ましです。

まとめ

memccpy() について紹介しました。場合によっては結構便利な関数ではないかと思います。