C言語基礎|文字列のコピー

ポインタを1文字ずつ進めるだけ!―C流の高速・美しい文字列コピーを体に覚えよう。

文字列を扱っていると、「別の配列へコピーしたい」場面が必ず出てきます。
たとえば、入力を一旦バッファに受けてから本体へ反映したり、加工用の作業領域へ移したりですね。

ここで大事なのは、C言語の文字列は**'\0'(ナル文字)で終わる char の並び**だということ。
コピーも結局は、

  • 先頭から1文字ずつ
  • '\0' が出るまで
  • コピー先へ代入しながら進む

というシンプルな繰り返しになります。
この「1文字ずつ進む」を、添字ではなくポインタの増分(++)で書くのが、C熟練者っぽい書き方です。

サンプルプログラム

ご指定どおり、元の ABC やメッセージを使わず、別の日本語メッセージに置き換えたシンプル例です。

プロジェクト名:chap11-5-1 ソースファイル名:chap11-5-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

// 文字列srcをdstにコピーする(dstを返す)
char *str_copy(char *dst, const char *src)
{
    char *top = dst;

    while (*dst++ = *src++) {
        // 代入が0('\0')になるまで続く
    }

    return top;
}

int main(void)
{
    char text[128] = "はじめの文字列";
    char buf[128];

    printf("現在の text:%s\n", text);

    printf("新しく入れたい言葉:");
    scanf("%127s", buf);

    str_copy(text, buf);

    printf("更新後の text:%s\n", text);

    return 0;
}

まず押さえる:文字列コピーがやっていること

コピー処理の全体像

要素内容
コピー元src が指す文字列(先頭から '\0' まで)
コピー先dst が指す配列領域(十分なサイズが必要)
繰り返し1文字コピー → ポインタを進める
終了条件'\0' をコピーした瞬間に止まる(終端もコピーする)

表の説明

  • 「終端の '\0' もコピーする」のが超重要です。これがないとコピー先が文字列になりません。

関数 str_copy の引数と返り値

関数の書式

  • 書式:char *str_copy(char *dst, const char *src);

引数の意味

引数意味変更される?
dstchar *コピー先の先頭を指すポインタ中身(配列の内容)が書き換わる
srcconst char *コピー元の先頭を指すポインタ中身は変更しない約束

表の説明

  • const char * は「コピー元は読むだけだよ」という安全宣言です。
  • dst の中身を書き換えるので dst 側は const を付けません。

返り値 char * が返すもの

この関数は top(コピー先の先頭アドレス) を返します。
途中で dst はどんどん進んでしまうので、先頭を覚えておくために top を使います。

while (*dst++ = *src++) の“2段階”を分解する

ここがこの記事の核心です。短い式に見えますが、やっていることは筋が通っています。

*dst++ = *src++ の評価の流れ

段階起きること重要ポイント
① 代入*dst = *src が実行されるsrc が指す1文字を dst へコピー
② 増分dst と src がそれぞれ1つ進む次の文字へ移動する
③ 条件判定代入式の結果(代入した文字)で while を判定代入した文字が '\0' なら終了

表の説明

  • while の条件は「代入式そのもの」です。
  • Cでは代入式の値は「代入後の左辺の値」になります。
    つまり、コピーした文字が '\0'(数値として0)になった瞬間に while が終わります。

図でつかむ:コピーが進む様子

例として src が "NEKO" のときの流れです。

1文字ずつコピーし、'\0' で止まる

src:  'N'  'E'  'K'  'O'  '\0'
       ↑
      src

dst:  [  ] [  ] [  ] [  ] [  ]
       ↑
      dst

1回目: 'N' をコピー → 両方進む
2回目: 'E' をコピー → 両方進む
3回目: 'K' をコピー → 両方進む
4回目: 'O' をコピー → 両方進む
5回目: '\0' をコピー → 条件が0 → ループ終了

図の説明

  • コピーは「見える文字」だけじゃなく、最後の '\0' も含めて完成します。
  • '\0' をコピーした瞬間に終了するので、ちょうどよく止まります。

添字で書く別解との比較(なぜポインタ版が好まれる?)

添字版(わかりやすい形)だと、だいたいこうなります。

int i = 0;
while (dst[i] = src[i]) {
    i++;
}

添字版とポインタ版の違い

観点添字版ポインタ版
追加変数i が必要不要
参照の形dst[i], src[i]*dst, *src
進め方i を増やすポインタを増やす
書き味明快だけど少し長い短くてCらしい

表の説明

  • 添字版は読みやすいです。最初はこれでもOK。
  • ポインタ版は「加算して参照外し」を毎回書かずに済む分、コードが締まります。
    (実際の最適化はコンパイラがやるので、必ず速いと断言はしないけど、意図としては効率的になりやすい、という理解でOKです。)

登場する命令(関数)の書式と役割

scanf

  • 書式:scanf(書式文字列, 格納先...);
  • 何をする命令?:標準入力から読み取り、指定した場所へ格納する
  • 今回のポイント:%127s
    ・buf は 128 文字分の配列
    ・最大127文字を読み、最後に '\0' を付ける余裕を確保(安全策)

printf

  • 書式:printf(書式文字列, 引数...);
  • 何をする命令?:指定した形式で表示する

注意点(実務で事故りやすいところ)

よくある落とし穴

落とし穴何が起きる?対策
dst の領域が小さいはみ出してメモリ破壊コピー先サイズを確保する
src と dst が重なる途中で壊れたコピーになることがある重なる可能性があるなら別手段(memmove系の発想)
文字列リテラルへコピー書き込み不可領域で落ちる可能性dst は必ず配列など書き込み可能領域にする

表の説明

  • 文字列コピーは「コピー先のサイズ」が命です。ここだけは口酸っぱく言いたいポイントです。