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() について紹介しました。場合によっては結構便利な関数ではないかと思います。