C++のmemcpy関数の使い方について

AI実装検定のご案内

memcpyは、指定したバイト数だけ、コピー元のメモリ領域からコピー先のメモリ領域へデータを複製する関数です。

通常の代入とは異なり、変数やオブジェクトの型に応じて値をコピーするのではなく、メモリ上のバイト列をそのままコピーします。

そのため、連続したメモリ領域やバイナリバッファーを扱う処理で使用されます。

C++では、<cstring>ヘッダーをインクルードし、一般的にstd::memcpyとして使用します。

#include <cstring>

基本的な構文は次のとおりです。

std::memcpy(
    コピー先,
    コピー元,
    コピーするバイト数
);

memcpyは低レベルなメモリ操作を行う関数であるため、コピー範囲や対象となる型を正しく理解して使用する必要があります。

通常の変数やクラスのコピーでは、代入やコピーコンストラクタ、std::copyなどを優先するのが基本です。

目次

memcpyの基本構文

関数の形式

std::memcpyは、概念的には次のような形式で使用します。

void* std::memcpy(
    void* destination,
    const void* source,
    std::size_t count
);

各引数の意味は次のとおりです。

引数意味
destinationコピー先のメモリアドレス
sourceコピー元のメモリアドレス
countコピーするバイト数
戻り値コピー先のポインタ

重要なのは、第3引数が要素数ではなく、バイト数である点です。

例えば、int型の要素を3個コピーしたい場合は、次のように記述します。

std::memcpy(
    destination,
    source,
    3 * sizeof(int)
);

より型の変更に強い書き方として、次のように要素からサイズを取得する方法もあります。

std::memcpy(
    destination,
    source,
    3 * sizeof(*source)
);

基本的な使い方

整数変数をコピーする例

int型の変数をmemcpyでコピーする例は次のとおりです。

#include <cstring>
#include <iostream>

int main()
{
    int source = 100;
    int destination = 0;

    std::memcpy(
        &destination,
        &source,
        sizeof(source)
    );

    std::cout << destination << '\n';

    return 0;
}

実行結果は次のとおりです。

100

&destinationはコピー先のアドレス、&sourceはコピー元のアドレスです。

sizeof(source)によって、sourceが占めるバイト数を指定しています。

ただし、単純な整数のコピーでは、通常は次のように代入するほうが分かりやすく安全です。

destination = source;

memcpyは、通常の代入ではなく、明示的にバイト列をコピーしたい場面で使用します。

配列をmemcpyでコピーする方法

配列全体をコピーする例

整数配列全体をコピーする場合は、次のように記述できます。

#include <cstring>
#include <iostream>

int main()
{
    int source[] = {
        10, 20, 30, 40, 50
    };

    int destination[5] = {};

    std::memcpy(
        destination,
        source,
        sizeof(source)
    );

    for (int value : destination)
    {
        std::cout << value << ' ';
    }

    return 0;
}

実行結果は次のとおりです。

10 20 30 40 50

この例におけるsizeof(source)は、配列全体のバイト数を返します。

例えば、intが4バイトの環境で要素数が5個なら、コピーされるサイズは20バイトです。

ただし、intのサイズは処理系によって異なる可能性があります。

そのため、バイト数を固定値で直接指定するのではなく、sizeofを使って計算するのが基本です。

一部の要素だけをコピーする例

配列の先頭から3要素だけをコピーする場合は、次のように記述します。

#include <cstring>
#include <iostream>

int main()
{
    int source[] = {
        10, 20, 30, 40, 50
    };

    int destination[5] = {};

    std::memcpy(
        destination,
        source,
        3 * sizeof(source[0])
    );

    for (int value : destination)
    {
        std::cout << value << ' ';
    }

    return 0;
}

実行結果は次のとおりです。

10 20 30 0 0

次の式は、配列要素3個分のバイト数を意味します。

3 * sizeof(source[0])

次のように書くこともできます。

3 * sizeof(*source)

配列とポインタに対するsizeofの違い

配列にsizeofを使う場合

配列変数に直接sizeofを使うと、配列全体のサイズを取得できます。

int values[10];

std::size_t size = sizeof(values);

intが4バイトなら、sizeof(values)は40バイトになります。

ポインタにsizeofを使う場合

ポインタにsizeofを使うと、ポインタ自体のサイズが返されます。

int* values = nullptr;

std::size_t size = sizeof(values);

この結果は、ポインタが指している配列のサイズではありません。

一般的な64ビット環境では、ポインタのサイズは8バイトになることが多いですが、処理系によって異なります。

関数の引数では配列がポインタとして扱われる

次の関数の引数は、一見すると配列のように見えます。

void copy_array(int source[])

しかし、関数の仮引数では、実質的に次と同じ扱いになります。

void copy_array(int* source)

そのため、関数内で次のように書いても、配列全体のサイズは取得できません。

sizeof(source)

要素数を別の引数として受け取り、バイト数を計算する必要があります。

#include <cstddef>
#include <cstring>

void copy_array(
    int* destination,
    const int* source,
    std::size_t count
)
{
    std::memcpy(
        destination,
        source,
        count * sizeof(*source)
    );
}

呼び出し側は次のようになります。

int source[] = {10, 20, 30};
int destination[3] = {};

copy_array(
    destination,
    source,
    3
);

文字列をmemcpyでコピーする方法

C形式文字列をコピーする例

文字配列をコピーする場合は、終端のヌル文字にも注意が必要です。

#include <cstring>
#include <iostream>

int main()
{
    const char source[] = "Hello";
    char destination[20] = {};

    std::memcpy(
        destination,
        source,
        sizeof(source)
    );

    std::cout << destination << '\n';

    return 0;
}

実行結果は次のとおりです。

Hello

sourceには、文字列の末尾を示すヌル文字も含まれています。

'H' 'e' 'l' 'l' 'o' '\0'

そのため、sizeof(source)は6バイトになります。

strlenを使う場合の注意点

std::strlenは、終端のヌル文字を含まない文字数を返します。

std::strlen(source)

そのため、次のコードではヌル文字がコピーされません。

std::memcpy(
    destination,
    source,
    std::strlen(source)
);

コピー先をC形式文字列として使用する場合は、ヌル文字分の1バイトを加える必要があります。

std::memcpy(
    destination,
    source,
    std::strlen(source) + 1
);

ただし、コピー先に十分な容量があることが前提です。

コピー先の容量不足に注意する

コピー先が小さい場合

次のコードでは、コピー先の容量が不足しています。

#include <cstring>

int main()
{
    char source[] = "Hello";
    char destination[3];

    std::memcpy(
        destination,
        source,
        sizeof(source)
    );
}

sourceはヌル文字を含めて6バイトですが、destinationは3バイトしかありません。

この状態で6バイトを書き込むと、配列の範囲外へ書き込むことになります。

これは未定義動作です。

コピー前に容量を確認する

コピー先には、コピーするバイト数以上の有効な領域が必要です。

#include <cstring>
#include <iostream>

int main()
{
    char source[] = "Hello";
    char destination[10] = {};

    static_assert(
        sizeof(destination) >= sizeof(source)
    );

    std::memcpy(
        destination,
        source,
        sizeof(source)
    );

    std::cout << destination << '\n';

    return 0;
}

実行時にサイズが決まる場合は、明示的に比較します。

if (source_size <= destination_size)
{
    std::memcpy(
        destination,
        source,
        source_size
    );
}

コピー元にも十分な領域が必要

コピー元の範囲外を読み取る例

コピー先だけでなく、コピー元にも指定したバイト数以上の有効な領域が必要です。

int source[2] = {10, 20};
int destination[4] = {};

// 誤り
std::memcpy(
    destination,
    source,
    4 * sizeof(int)
);

destinationには4要素分の容量がありますが、sourceには2要素分しかありません。

そのため、コピー元の範囲外を読み取ることになり、未定義動作が発生します。

memcpyを使うときは、コピー元とコピー先の両方に必要なサイズの有効領域があることを確認する必要があります。

コピー元とコピー先が重なる場合

重複した領域にmemcpyを使うと未定義動作になる

memcpyでは、コピー元とコピー先のメモリ領域が重なっていてはいけません。

#include <cstring>

int main()
{
    char text[] = "abcdef";

    std::memcpy(
        text + 1,
        text,
        5
    );
}

この例では、コピー元とコピー先の範囲が重複しています。

コピー元は次の範囲です。

text[0] ~ text[4]

コピー先は次の範囲です。

text[1] ~ text[5]

このような重複領域にmemcpyを使うと、未定義動作になります。

重複する場合はmemmoveを使う

コピー元とコピー先が重なる可能性がある場合は、std::memmoveを使用します。

#include <cstring>
#include <iostream>

int main()
{
    char text[] = "abcdef";

    std::memmove(
        text + 1,
        text,
        5
    );

    std::cout << text << '\n';

    return 0;
}

memmoveは、コピー元の内容を一時的に退避したかのように処理するため、重複した領域でも正しくコピーできます。

無効なポインタを渡さない

ヌルポインタを渡す例

コピーするバイト数が0より大きい場合、コピー元やコピー先にヌルポインタや無効なポインタを渡してはいけません。

#include <cstring>

int main()
{
    int* source = nullptr;
    int destination = 0;

    std::memcpy(
        &destination,
        source,
        sizeof(destination)
    );
}

このコードでは、sourceが有効なメモリ領域を指していません。

そのため、未定義動作になります。

memcpyを呼び出す前に、コピー元とコピー先が有効な領域を指していることを確認する必要があります。

オブジェクト全体をmemcpyでコピーできる条件

トリビアルコピー可能型とは

オブジェクト全体を、その値を保ったままバイト単位でコピーする用途では、原則としてトリビアルコピー可能型を対象にします。

トリビアルコピー可能型であるかどうかは、std::is_trivially_copyable_vで確認できます。

#include <type_traits>

static_assert(
    std::is_trivially_copyable_v<int>
);

構造体についても確認できます。

#include <type_traits>

struct Point
{
    int x;
    int y;
};

static_assert(
    std::is_trivially_copyable_v<Point>
);

「単純な構造体」だけでは判断できない

メンバーがintなどの基本型だけで構成されている構造体は、トリビアルコピー可能になることが多いです。

ただし、「見た目が単純だから安全」と判断するべきではありません。

ユーザー定義のコピー処理やデストラクター、仮想関数、継承関係などによって、トリビアルコピー可能ではなくなることがあります。

最終的には、次のように型特性で確認するのが確実です。

static_assert(
    std::is_trivially_copyable_v<Point>
);

構造体をmemcpyでコピーする例

トリビアルコピー可能な構造体

次の構造体は、一般的にトリビアルコピー可能です。

struct Point
{
    int x;
    int y;
};

コピー例は次のとおりです。

#include <cstring>
#include <iostream>
#include <type_traits>

struct Point
{
    int x;
    int y;
};

int main()
{
    static_assert(
        std::is_trivially_copyable_v<Point>
    );

    Point source{10, 20};
    Point destination{};

    std::memcpy(
        &destination,
        &source,
        sizeof(Point)
    );

    std::cout << destination.x << '\n';
    std::cout << destination.y << '\n';

    return 0;
}

実行結果は次のとおりです。

10
20

ただし、このケースでも通常は代入を使うほうが自然です。

destination = source;

通常の代入はサイズ指定が不要であり、コードの意図も明確になります。

memcpyを使ってはいけない代表例

std::stringを含むクラス

次のような型のオブジェクト全体をmemcpyでコピーしてはいけません。

#include <string>

struct Person
{
    std::string name;
    int age;
};

誤った例は次のとおりです。

Person source{"Alice", 30};
Person destination;

std::memcpy(
    &destination,
    &source,
    sizeof(Person)
);

std::stringは、文字列データを管理するための内部状態を持っています。

内部ポインタやサイズ情報などを単純に複製すると、コピー元とコピー先の状態が破壊される可能性があります。

正しくは通常の代入を使用します。

destination = source;

std::vectorを含むクラス

std::vectorも、動的メモリや要素数、容量などを管理しています。

#include <vector>

struct Data
{
    std::vector<int> values;
};

このようなオブジェクト全体をmemcpyでコピーしてはいけません。

正しくは代入を使います。

Data destination = source;

仮想関数を持つクラス

仮想関数を持つクラスは、一般にトリビアルコピー可能型ではありません。

class Base
{
public:
    virtual ~Base() = default;

    int value{};
};

このようなオブジェクト全体をmemcpyでコピーすると、実装内部の情報まで上書きする可能性があります。

コピーコンストラクタやコピー代入演算子を使用してください。

ポインタをmemcpyする場合の注意点

コピーされるのはアドレス値だけ

ポインタ型はトリビアルコピー可能型に含まれます。

ただし、ポインタをコピーしても、ポインタが指しているオブジェクト自体が複製されるわけではありません。

#include <cstring>
#include <iostream>

int main()
{
    int value = 100;

    int* source = &value;
    int* destination = nullptr;

    std::memcpy(
        &destination,
        &source,
        sizeof(source)
    );

    std::cout << *destination << '\n';

    return 0;
}

コピー後、sourcedestinationは同じvalueを指します。

これはディープコピーではなく、ポインタ値のコピーです。

通常は、次のように代入すれば十分です。

destination = source;

std::arrayの要素をコピーする方法

std::arraymemcpyを使う例

要素型がトリビアルコピー可能であれば、std::arrayの内部データをコピーできます。

#include <array>
#include <cstring>
#include <type_traits>

int main()
{
    using Element = int;

    static_assert(
        std::is_trivially_copyable_v<Element>
    );

    std::array<Element, 3> source{
        10, 20, 30
    };

    std::array<Element, 3> destination{};

    std::memcpy(
        destination.data(),
        source.data(),
        source.size() * sizeof(Element)
    );

    return 0;
}

ただし、std::array同士なら通常の代入のほうが簡潔です。

destination = source;

std::vectorの要素をコピーする方法

std::vector本体はコピーしない

std::vectorオブジェクトそのものをmemcpyでコピーしてはいけません。

ただし、要素型がトリビアルコピー可能であり、コピー先に必要な数の要素が存在する場合は、要素が格納されている領域をコピーできます。

#include <cstring>
#include <type_traits>
#include <vector>

int main()
{
    static_assert(
        std::is_trivially_copyable_v<int>
    );

    std::vector<int> source{
        10, 20, 30, 40
    };

    std::vector<int> destination(
        source.size()
    );

    std::memcpy(
        destination.data(),
        source.data(),
        source.size() * sizeof(source[0])
    );

    return 0;
}

コピー先には、事前に必要な要素数を確保しておく必要があります。

std::vector<int> destination(
    source.size()
);

空のstd::vectorへ書き込んではいけない

次のコードは誤りです。

std::vector<int> destination;

std::memcpy(
    destination.data(),
    source.data(),
    source.size() * sizeof(source[0])
);

空のdestinationには、要素を書き込むための有効な領域がありません。

通常の用途では、次の代入を使用するのが最も安全です。

destination = source;

std::vector<std::string>には使用できない

要素型が非トリビアルコピー可能型の場合、要素領域にもmemcpyを使うべきではありません。

std::vector<std::string> source{
    "A", "B", "C"
};

std::vector<std::string> destination(
    source.size()
);

// 使用してはいけない
std::memcpy(
    destination.data(),
    source.data(),
    source.size() * sizeof(source[0])
);

std::stringは内部リソースを管理するため、バイトコピーでは正しい複製になりません。

配列やバッファーの途中からコピーする方法

オフセットを指定する例

配列の途中からコピーする場合は、ポインタ演算を使えます。

#include <cstring>
#include <iostream>

int main()
{
    int source[] = {
        10, 20, 30, 40, 50
    };

    int destination[2] = {};

    std::memcpy(
        destination,
        source + 2,
        2 * sizeof(source[0])
    );

    std::cout << destination[0] << '\n';
    std::cout << destination[1] << '\n';

    return 0;
}

実行結果は次のとおりです。

30
40

source + 2は、source[2]の位置を指しています。

そこから2要素分をコピーしています。

std::byteを使ったバッファー操作

バイト単位のバッファーを扱う場合は、std::byteを使用できます。

#include <cstddef>
#include <cstring>

int main()
{
    std::byte source[100]{};
    std::byte destination[20]{};

    std::memcpy(
        destination,
        source + 10,
        sizeof(destination)
    );

    return 0;
}

オブジェクトをバイト列へコピーする方法

トリビアルコピー可能型をバイト配列へコピーする

トリビアルコピー可能型のオブジェクトは、charunsigned charstd::byteなどのバイト列へコピーできます。

#include <array>
#include <cstddef>
#include <cstring>
#include <type_traits>

struct Point
{
    int x;
    int y;
};

int main()
{
    static_assert(
        std::is_trivially_copyable_v<Point>
    );

    Point point{10, 20};

    std::array<std::byte, sizeof(Point)> bytes{};

    std::memcpy(
        bytes.data(),
        &point,
        sizeof(point)
    );

    return 0;
}

同じ型の有効なオブジェクトへ戻すこともできます。

Point restored{};

std::memcpy(
    &restored,
    bytes.data(),
    sizeof(restored)
);

ファイル保存や通信にそのまま使う場合の注意点

構造体のバイト列をそのままファイルやネットワークへ保存すると、別の環境で正しく復元できない可能性があります。

主な原因は次のとおりです。

  • 構造体のパディング
  • 整数のバイト順
  • 型のサイズ
  • 浮動小数点数の表現
  • コンパイラーやABIの違い
  • コンパイル設定の違い

異なる環境間でデータを交換する場合は、構造体全体をそのままコピーするのではなく、各フィールドを明確な形式へ変換するシリアライズ処理が必要です。

異なる型の間でコピーする場合の注意点

コピー先で有効な値になるとは限らない

memcpyは型を検査しないため、異なる型の間でもバイト列をコピーできます。

しかし、コピーしたバイト列がコピー先の型にとって有効な値表現になるとは限りません。

例えば、次のような処理は避けるべきです。

char source = 42;
bool destination = false;

std::memcpy(
    &destination,
    &source,
    sizeof(destination)
);

コピーされたビット列がbool型の有効な値表現でなければ、その値を読み出したときに問題が発生する可能性があります。

型変換を目的とする場合は、通常のキャストや、用途によってはC++20以降のstd::bit_castを検討します。

#include <bit>
#include <cstdint>

double value = 0.1;

std::uint64_t bits =
    std::bit_cast<std::uint64_t>(value);

std::bit_castは、ビット表現を別の型として取得したい意図を明確にできます。

memcpyと通常の代入の違い

通常の代入

通常の代入は、型のルールに従って値をコピーします。

destination = source;

コピー代入演算子が定義されているクラスでは、その処理が呼び出されます。

memcpy

memcpyは、指定したバイト数だけメモリ内容をコピーします。

std::memcpy(
    &destination,
    &source,
    sizeof(source)
);

主な違いは次のとおりです。

項目通常の代入memcpy
コピー単位値やオブジェクトバイト
型情報使用する基本的に使用しない
サイズ指定不要必要
クラスのコピー処理呼び出される呼び出されない
非トリビアル型対応できる原則として不適切
重複領域通常は問題にならない使用不可
コードの安全性比較的高い手動確認が必要

通常のオブジェクトコピーでは、まず代入を検討してください。

memcpystd::copyの違い

std::copyは要素単位でコピーする

std::copyは、要素単位でコピーするC++標準ライブラリのアルゴリズムです。

#include <algorithm>

std::copy(
    source_begin,
    source_end,
    destination_begin
);

配列をコピーする例は次のとおりです。

#include <algorithm>
#include <iostream>
#include <iterator>

int main()
{
    int source[] = {
        10, 20, 30
    };

    int destination[3] = {};

    std::copy(
        std::begin(source),
        std::end(source),
        std::begin(destination)
    );

    for (int value : destination)
    {
        std::cout << value << ' ';
    }

    return 0;
}

std::copyは、要素の代入処理を利用します。

そのため、ユーザー定義型や非トリビアルな型にも利用できます。

使い分けの目安

用途適した方法
通常の変数やオブジェクト代入
配列やコンテナの要素std::copy、代入
生のバイナリデータstd::memcpy
重複するメモリ領域std::memmove
C形式文字列std::stringや適切な文字列処理
非トリビアルなクラスコピーコンストラクタ、代入

memcpymemmoveの違い

重複領域への対応が異なる

memcpymemmoveは、どちらも指定したバイト数をコピーする関数です。

std::memcpy(
    destination,
    source,
    size
);

std::memmove(
    destination,
    source,
    size
);

最大の違いは、コピー元とコピー先の重複に対応しているかどうかです。

関数重複領域
std::memcpy対応しない
std::memmove対応する

コピー元とコピー先が重ならないことが明確な場合は、memcpyを使用できます。

重なる可能性がある場合は、memmoveを使用します。

memcpyは必ず高速とは限らない

代入やstd::copyより常に速いわけではない

memcpyは、連続したメモリ領域を効率よくコピーするために使用されることがあります。

ただし、代入やstd::copyより必ず高速になるわけではありません。

最適化コンパイラーは、次のような処理を内部的に効率的なメモリコピーへ変換することがあります。

std::copy(
    source.begin(),
    source.end(),
    destination.begin()
);

構造体の代入についても、最適化によって効率的な命令へ変換される可能性があります。

memcpyを選ぶ理由は、単純な速度ではなく、処理の意味が「バイト列のコピー」であるかどうかです。

安全なラッパー関数を作る方法

トリビアルコピー可能型だけに制限する

同じ型のオブジェクトをmemcpyでコピーする関数を作る場合は、トリビアルコピー可能型だけに制限すると安全性を高められます。

#include <cstring>
#include <type_traits>

template<class T>
void copy_trivial(
    T& destination,
    const T& source
)
{
    static_assert(
        std::is_trivially_copyable_v<T>,
        "T must be trivially copyable"
    );

    std::memcpy(
        &destination,
        &source,
        sizeof(T)
    );
}

ただし、同じ型の通常のコピーであれば、基本的には代入を使うほうが簡潔です。

destination = source;

C++20のコンセプトを使う例

C++20以降では、requiresを使って制約を記述できます。

#include <cstring>
#include <type_traits>

template<class T>
requires std::is_trivially_copyable_v<T>
void copy_trivial(
    T& destination,
    const T& source
)
{
    std::memcpy(
        &destination,
        &source,
        sizeof(T)
    );
}

memcpyを安全に使うための確認事項

コピーするバイト数は正しいか

第3引数は要素数ではなくバイト数です。

count * sizeof(*source)

のように計算します。

コピー先に十分な容量があるか

コピーするバイト数が、コピー先の有効領域を超えてはいけません。

コピー元に十分なデータがあるか

コピー元からも、指定したバイト数を安全に読み取れる必要があります。

コピー元とコピー先が重なっていないか

重なる可能性がある場合は、std::memmoveを使用します。

ポインタは有効か

ヌルポインタや、寿命が終了したオブジェクトを指すポインタを渡してはいけません。

対象の型は適切か

オブジェクト全体をコピーする場合は、原則としてトリビアルコピー可能型を対象にします。

std::is_trivially_copyable_v<T>

で確認できます。

通常の代入で十分ではないか

変数やクラスの通常のコピーでは、代入やコピーコンストラクタを優先してください。

まとめ

std::memcpyは、コピー元のメモリ領域からコピー先のメモリ領域へ、指定したバイト数をコピーする関数です。

基本構文は次のとおりです。

std::memcpy(
    コピー先,
    コピー元,
    コピーするバイト数
);

配列全体をコピーする場合は、次のように記述できます。

std::memcpy(
    destination,
    source,
    sizeof(source)
);

一部の要素をコピーする場合は、要素数をバイト数へ変換します。

std::memcpy(
    destination,
    source,
    count * sizeof(*source)
);

安全に使うためには、次の点を確認する必要があります。

  • コピー先に十分な容量がある
  • コピー元に十分な有効領域がある
  • コピー元とコピー先が重複していない
  • ポインタが有効である
  • オブジェクト全体をコピーする場合は型が適切である
  • 第3引数が正しいバイト数になっている

std::stringstd::vectorなど、内部リソースを管理するオブジェクト全体をmemcpyでコピーしてはいけません。

通常の変数やクラスのコピーでは、代入、コピーコンストラクタ、std::copyなどを優先します。

memcpyは、生のバイナリデータや連続したメモリ領域を、明示的にバイト単位でコピーする必要がある場面で使用するのが基本です。

以上、C++のmemcpy関数の使い方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次