C言語入門|メモリ操作の基本関数

ここまで本当によく頑張りました。
配列、ポインタ、添え字、アドレス……
正直、C言語の中でも一番ややこしいところを通り抜けてきましたね。

そして今、ようやく
「ポインタを引数に取る標準関数」
を、安全に使うための準備が整いました。

C言語の標準ライブラリには、
メモリを直接扱うための便利な関数がいくつも用意されています。
今回はその中でも、実務で最重要クラスの関数を入口として紹介します。

なぜメモリ操作用の関数が必要なのか

C言語では、配列や文字列について次のような大原則がありました。

  • 配列は = で代入してはいけない。
  • 文字列も丸ごと代入してはいけない。
  • 関数の引数や戻り値で直接やり取りしない。

これは不便に見えますが、理由はとてもシンプルです。

= は「値のコピー」ではない

int a[4];
int b[4];

b = a;   // これはできない

この書き方が禁止されているのは、

  • 配列名はアドレスに化ける。
  • b = a は「アドレスの代入」を意味してしまう。

からです。

でも現実のプログラムでは、

  • 配列を丸ごとコピーしたい。
  • 構造体の中身をまとめて複製したい。

という場面が頻繁にあります。

そこで登場するのが、メモリ操作の標準関数です。

メモリ操作関数を使う準備

これから紹介する関数を使うときは、
必ずソースコードの先頭に次を記述します。

#include <string.h>

string.h という名前ですが、
文字列だけでなく メモリ操作全般 を扱う関数が含まれています。

memcpy ― メモリ領域をまるごとコピーする

最初に紹介するのは、もっとも基本で、もっとも重要な関数です。

memcpy の役割

あるメモリ領域の中身を
別のメモリ領域に、指定したバイト数だけコピーする。

配列・構造体・バッファなど、
形や型を問わず使える汎用コピー関数です。

memcpy の書式

void* memcpy(void* dest, const void* src, size_t len);

引数の意味

引数内容
destコピー先の先頭アドレス
srcコピー元の先頭アドレス
lenコピーするバイト数

戻り値

  • dest と同じアドレス

戻り値はあまり使われませんが、
関数の連結などで利用できます。

size_t とは何か

len に指定する size_t 型は、

  • 0以上の整数
  • メモリサイズや配列サイズを表すための型

です。

感覚的には、

サイズ専用の int

と思っておけば問題ありません。

const void* の意味(軽く)

src が const void* になっているのは、

  • コピー元のメモリ内容を
  • 関数内で変更しない

という約束を示しています。

安全のための仕組みなので、
「そういうもの」と受け取ってOKです。

配列を memcpy でコピーしてみよう

次の例では、int型配列を丸ごとコピーします。

サンプルプログラム

プロジェクト名:10-7-1 ソースファイル名: sample10-7-1.c

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

int main(void)
{
    int src[3] = {8, 16, 24};
    int dst[3];

    memcpy(dst, src, sizeof(src));

    printf("src[1]=%d (%p)\n", src[1], &src[1]);
    printf("dst[1]=%d (%p)\n", dst[1], &dst[1]);

    return 0;
}

実行結果(例)

src[1]=16 (0x7ffee2c1a4c4)
dst[1]=16 (0x7ffee2c1a4b4)

何が起きているのか

  • src と dst は 別々の配列
  • アドレスも完全に異なる。
  • 中身だけがコピーされている。

これが、

正しい配列コピー

です。

配列名を引数に渡していますが、
実際には「先頭アドレス」が渡されており、
memcpy はその番地から指定バイト数分を
そのまま複製しています。

浅いコピーと深いコピーの落とし穴

ここで、重要な注意点があります。

浅いコピーとは

  • ポインタを含む配列や構造体を
  • memcpy でそのままコピーすること

この場合、

  • 外側の構造は複製される。
  • 中のポインタが指す先は共有される。

という状態になります。

浅いコピーが引き起こす問題

  • 片方で free すると、もう片方が壊れる。
  • 意図せず同じデータを操作してしまう。
  • バグの原因が非常に分かりにくい。

このようなコピーは 浅いコピー(shallow copy) と呼ばれます。

深いコピーという考え方

深いコピーでは、

  • 構造体や配列を複製
  • 内部のポインタが指す先も
  • 新しい領域を確保してコピー

します。

手間は増えますが、

安全で、意図どおりの動作

を保証できます。

メモリ操作関数を使う心構え

メモリ操作系の関数は、とても強力です。

その一方で、

  • サイズ指定を間違える。
  • コピー先が足りない。
  • 重なった領域を扱う。

と、簡単にバグや未定義動作を引き起こします。

覚えておきたい鉄則

  • バイト数は必ず意識する。
  • sizeof を積極的に使う。
  • 「とりあえず memcpy」はしない。

C言語では、

便利さと危険さは常にセット