strncpy

char* strncpy(char* dst, const char* src, size_t n);
(1) nバイト コピーする.
(2) srcの先頭からnバイトの部分にNUL文字(0x00)がひとつもない場合, dstは終端がNUL文字にならないことに注意が必要.
(3) 文字列srcの長さがnより短い場合,dst残りの部分はNUL文字で塗りつぶされる. したがって,dstの長さは少なくともn以上でなければならない.

strcpyとは根本的に違う動作になる

strncpystrcpyと違って srcがNUL文字に到達しても終了せず,残りの部分の全バイトをNUL文字で塗りつぶすという仕様になっていることを覚えておこう. 普通のプログラマの認識ではstrcpystrncpyの違いは単にコピーする最大バイト数を意識するかどうかだけだと思われている ---したがって,srcのサイズがdstのサイズより十分に小さい場合は同じ動作になると思われているが,実はそうではない. strncpysrc側でNUL文字に到達したあとも dst側の残りの領域をすべて0x00にするという余分な作業をやってくれている(図1).

図1 strcpystrncpyの動作の違い
16進値
dst[10] "ABCDEFGHI" 41 42 43 44 45 46 47 48 49 00
src "123" 31 32 33 00
strcpy(dst, src);の結果 31 32 33 00 45 46 47 48 49 00
strncpy(dst, src, 10);の結果 31 32 33 00 00 00 00 00 00 00

この動作の違いがどう影響するかだけど,まぁ,普通は違いが問題になることはないと思うけど…

(A1) 性能要件がシビアなプログラムでstrcpystrncpyにかえると 余計な動作のために性能要件をクリアできなくなるかも. たとえば図1の例と図2のような実装で考えると, strncpyに変更すると比較回数が4回から4+10=14回に増加,メモリ転送回数が4回から10回に増加する. 比較命令を1クロック,メモリ転送命令を3クロックと仮定すると,処理時間は(14×1+10×3)÷(4×1+4×3)=2.75倍に増加することになり, この劣化は簡単には受け入れられないと思う.
(A2) strncpyが余計な部分もゼロクリアしてくれることを前提にしている箇所では strncpystrcpyは交換できない. バイナリフォーマットでデータをやり取りするプログラムの場合(たとえばオブジェクトファイルを読み書きするようなプログラムとか), フィールド長が何バイトでフィールド長より短い文字列を指定する場合は余った部分を0x00で埋めることというような規則になっている場合が多く, あながちトリッキーともいえない.
(A3) 隠れてたバグが見つかるかも. dstsrcのサイズは同じつもりだったのに,実はdstの方が短かったというような場合に, strcpyからstrncpyに変えて サイズとしてsrcのサイズを与えたらセグメンテーション フォルトを起こしてしまったとか. でも,これって別にstrcpy/strncpyの問題じゃないか.
図2 strcpyとstrncpyの実装イメージ
strcpystrncpy
while (*dst++ = *src++)
    /* no body */;


return dst;
while (n-- > 0 && *dst++ = *src++)
    /* no body */;
while (n-- > 0)
    *dst++ = 0x00;
return dst;

文字列のサイズをMAX+1のようにするのは誤解を生じやすいかも

文字列を保持する配列を用意する場合,次の(D1)(D2)の2種類の流儀があるが, (D1)の流儀に統一しておくのがよさそうだ.

(D1)(D2)
#define BUFSIZE 10
char buf[BUFSIZE];
#define BUFSIZE 9
char buf[BUFSIZE + 1];

(D2)の意図は“文字列は最後にNUL文字をいれるスペースが必要だから その分+1する”というもの. ただ,こうすると,C言語の配列の慣習の枠組みから外れてしまって間違いが起こりやすい. また,実際のプログラム例を検証してみると(表3),定義部でわざわざ+1としておいても その後 手続き部を書くにあたって特にプログラムが読みやすくなるわけでも,間違いが防ぎやすいわけでもなさそう. どうしても+1を明示したいなら,次の(D1.5)のような書き方がいいかもしれない. [しかし10バイトの配列だからといって,必ず9文字+NUL文字になってるわけではないんだよね….]

(D1.5)
#define BUFSIZE (9 + 1)
char buf[BUFSIZE];
表3 よくある操作と書き方
操作書き方
末尾要素へのアクセス buf[BUFSIZE - 1]
forループ for (i = 0; i < BUFSIZE; ++i) { ... }
strncpy strncpy(buf, src, BUFSIZE);
fgets fgets(buf, BUFSIZE, fp);

C言語では文字列は単に文字の配列(char型の配列)であり,いくつかの関数が文字0x00を特別視するという約束事があるだけなので, 他の配列と同様に考えて,配列a[n],先頭a[0]〜末尾a[n-1], アクセスするときの慣例はfor (i = 0; i < n; ++i) ...のように統一するほうがすっきりするように思う. 要するに文字列だけを特別視するのが混乱のものなんじゃないかな. といいつつ,文字列は終端に0x00をおくという規約がある以上,やっぱり特例では?という意見もあるだろうけど.

参考

[1]文字配列char str[x]の初期化

はたいたかし
2007-04-16
トップ > 開発ツール > C/C++