C言語の構造体をコピーする

382, 2022-01-04

目次

C言語の構造体をコピーする

C言語では構造体を扱うことができます。
構造体はメンバ変数で構成されたデータのまとまりです。
今回はこの構造体をコピーする方法について具体的に解説します。

以下を見ていきます。

  • 構造体変数への代入によるコピー

  • memcpy()を使ったコピー

  • memmove()を使ったコピー

  • memcpyとmemmove, どちらが速い?

  • deepcopy()関数を定義してコピーする

まずは構造体変数への代入によるコピーからです。

構造体変数への代入によるコピー

構造体で定義する変数を構造体変数と言います。
この構造体変数は変数ですので、別の変数に代入することができます。
たとえば↓のようにです。

#include <stdio.h>

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[40];  // 名前
};

int main(void) {
    struct Animal cat = { 20, "Tama" };  // 構造体変数catを定義する
    struct Animal dog = cat;  // 代入してcatをdogにコピー

    // dogのメンバを出力
    printf("dog age[%d] name[%s]\n", dog.age, dog.name);
    // 出力結果↓
    // dog age[20] name[Tama]

    return 0;
}

代入先の変数が代入元の変数と同じ構造体であれば、このように代入式で構造体変数をコピーできます
このコピーは構造体のメンバがすべてコピーされます。

ただし動的なメモリ確保をされた変数は別です
動的なメモリ確保で得られたアドレスを保存してるポインタ変数は、そのアドレス値が単純にコピーされるだけです。
これはどういうことかと言うと、メモリが持つ実体としてのデータはコピーされないということです。
あくまでもコピーされるのはポインタ変数だけになります。
↓のコードをご覧ください。

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

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char *name;  // 名前(動的にメモリ確保される)
};

int main(void) {
    // cat変数を初期化する
    struct Animal cat;
    cat.age = 20;
    cat.name = malloc(sizeof(char) * 40);  // 動的メモリ確保
    strcpy(cat.name, "Tama");

    struct Animal dog = cat;  // 代入してcatをdogにコピー

    // dogのメンバを出力
    printf("dog age[%d] name[%s]\n", dog.age, dog.name);
    // 出力結果↓
    // dog age[20] name[Tama]

    // catとdogのnameのアドレス値を比較
    printf("cat name address[%p]\n", cat.name);
    printf("dog name address[%p]\n", dog.name);
    // 出力結果↓
    // cat name address[0xb37010]
    // dog name address[0xb37010]

    free(cat.name);
    return 0;
}

dog変数にcatを代入文でコピーしているのは変わりません。
catnamemalloc()関数で動的にメモリが確保されています。
dognameを参照すると「Tama」と出力されますので、文字列がコピーされているように見えます。
しかしこれはポインタ変数がコピーされているだけです。
その証拠にcatnamedognameのアドレス値は同じものになります。

ですのでメモリの解放もcat.nameの方だけにしています。
仮にdog.namefree()で解放すると、これは同じメモリを2回解放することになります。
いわゆるダブルフリーというバグになります。

つまり動的なメモリ確保を行っているメンバが構造体に存在してる場合、普通の代入文のコピーではうまくいかないということになります。
これを解決するには後述するdeepcopy()などのコピー用の関数を定義する必要があります。

memcpy()を使ったコピー

string.hをインクルードすると使えるmemcpy()関数でも構造体をコピーすることができます。

#include <string.h>

// dest ... コピー先のポインタ
// src  ... コピー元のポインタ
// n    ... コピーするバイト数
// 返り値 ... destへのポインタ
void *memcpy(void *dest, const void *src, size_t n);

memcpy()関数の特徴は、memmove()と違い、コピー元とコピー先のポインタが同じだった場合に正常に動作しない点です
memcpy()は内部でコピー元のコピーを作らないので、ポインタが指すメモリが同じだった場合に期待しない動作をすることがあります。

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

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[40];  // 名前
};

int main(void) {
    struct Animal cat = { 20, "Tama" };  // 構造体変数catを定義
    struct Animal dog;  // コピー先のdog変数

    // catをdogにコピーする
    memcpy(&dog, &cat, sizeof(cat));

    // dogのメンバを出力
    printf("dog age[%d] name[%s]\n", dog.age, dog.name);
    // 出力結果↓
    // dog age[20] name[Tama]

    return 0;
}

↓のコードの部分でコピーを実行しています。

    // catをdogにコピーする
    memcpy(&dog, &cat, sizeof(cat));

memcpy()の第1引数にdog変数のアドレスを渡しています。
第2引数にはcat変数のアドレスを渡します。
第3引数にはcat変数のバイト数をsizeof演算子で求めて渡しています。
こうするとdog変数にcat変数がcat変数のバイト数だけコピーされます。
catdogは同じAnimal構造体の変数なので、そのバイト数は同じです。

それからこれはmemcpy()memmove()の両方に言える点ですが、これらの関数はコピー先のサイズを気にしません。
仮にコピー先のサイズがコピーするバイト数よりも小さかった場合は、意図しないメモリ上のデータを上書きする場合があります。
この辺は注意が必要です。

あと動的確保されたポインタ変数はポインタ変数だけがコピーされるので注意が必要です。
ポインタの先のメモリのデータはコピーされません。

memmove()を使ったコピー

string.hをインクルードすると使えるmemmove()関数も構造体をコピーすることができます。

#include <string.h>

// dest ... コピー先のポインタ
// src  ... コピー元のポインタ
// n    ... コピーするバイト数
// 返り値 ... destへのポインタ
void *memmove(void *dest, const void *src, size_t n);

memmove()memcpy()と使い方は同じです。

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

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[40];  // 名前
};

int main(void) {
    struct Animal cat = { 20, "Tama" };  // 構造体変数catを定義
    struct Animal dog;  // コピー先のdog変数

    // catをdogにコピーする
    memmove(&dog, &cat, sizeof(cat));

    // dogのメンバを出力
    printf("dog age[%d] name[%s]\n", dog.age, dog.name);
    // 出力結果↓
    // dog age[20] name[Tama]

    return 0;
}

memmove()memcpy()と違って、コピー元の領域を一旦コピーしておきます。
そのためdestとsrcの領域が重なっていても期待した動作になります

そのためセキュアなプログラムを作りたい場合は理屈の上ではmemcpy()を使うよりもmemmove()を使ったほうが良いということになります。

memcpyとmemmove, どちらが速い?

memcpy()memmove()と比べてコピー領域が重なっていた場合に意図しない動作をする分、危険な関数と言えますが、その分余計なことをしないためはやく動作しそうです。
果たしてこの仮説は本当なんでしょうか?
↓のコードでmemcpy()memmove()の処理速度を比較できます。

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

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[3000];  // 名前
};

int main(void) {
    struct Animal cat = { 20, "Tama" };  // 構造体変数catを定義
    struct Animal dog;  // コピー先のdog変数
    const long n = 1000000000;  // 試行回数

    // memcpy()の処理速度の計測
    {
        clock_t start,end;
        start = clock();

        for (long i = 0; i < n; i += 1) {
            memcpy(&dog, &cat, sizeof(cat));
        }

        end = clock();

        printf("memcpy: %.2f秒かかりました\n", (double) (end - start) / CLOCKS_PER_SEC);
    }

    // memmove()の処理速度の計測
    {
        clock_t start,end;
        start = clock();

        for (long i = 0; i < n; i += 1) {
            memmove(&dog, &cat, sizeof(cat));
        }

        end = clock();

        printf("memmove: %.2f秒かかりました\n", (double) (end - start) / CLOCKS_PER_SEC);
    }

    return 0;
}

上記のコードを筆者の環境で実行する(最適化なし)と以下のような結果になります。

memcpy: 29.58秒かかりました
memmove: 29.54秒かかりました

なんとビックリ仰天玉手箱、memmove()の方が速いという結果になりました。

(^ _ ^)

memcpy()良いとこないやん

(・ v ・)

memcpy()はいらない子・・・?

しかしこのテスト結果は不思議な結果です。
理屈ではmemcpy()のほうが速いはずですが……。

deepcopy()関数を定義してコピーする

構造体のメンバに動的にメモリが確保されたアドレスを持つポインタ変数があった場合、代入文やmemcpy(), memmove()では期待した動作、つまり全体的なデータのコピーが実現できません。
これを解決するにはコピー用の関数を独自に定義する必要があります。
たとえば↓を見てください。

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

struct Animal {
    int age;
    char *name;
};

// ディープコピーを行う関数
void deepcopy(struct Animal *dst, struct Animal *src) {
    // 年齢のコピー
    dst->age = src->age;

    // 名前のコピー
    size_t byte = sizeof(char);
    size_t size = byte * strlen(src->name) + byte;
    dst->name = malloc(size);  // エラー処理は省略
    strcpy(dst->name, src->name);
}

int main(void) {
    // catの初期化
    struct Animal cat;
    cat.age = 20;
    cat.name = malloc(sizeof(char) * 40);  // エラー処理は省略
    strcpy(cat.name, "Tama");

    // dogにcatをコピー
    struct Animal dog;
    deepcopy(&dog, &cat);

    // dogのメンバを出力
    printf("dog age[%d] name[%s]\n", dog.age, dog.name);

    // メモリを解放
    free(cat.name);
    free(dog.name);
    return 0;
}

↑のdeepcopy()関数は構造体Animalをコピーする関数です。
内部ではsrcnamedstに動的にコピーしています。

このようなコピー用の関数を定義しておくと、構造体のメンバの動的メモリ確保にも対応できます。
ただしこのようなディープコピーを行う関数はけっこう遅いので、頻繁に使う場合は注意してください。
プログラムのボトルネックになる可能性も持っています。

おわりに

今回はC言語の構造体のコピーについて解説しました。
一番簡単なのは代入文によるコピーですが、臨機応変にmemcpy()memmove()も使えます。
動的なメンバはdeepcopy()などのコピー用関数を定義して対応しましょう。

(^ _ ^)

コピーは奥が深いね

(・ v ・)

パソコンの基礎機能だしね



この記事のアンケートを送信する