C言語を学び始めて多くの人が最初に直面する壁の一つが、配列の代入です。

PythonやJavaScriptといったモダンな言語では、配列(リスト)を変数間で単純に「=」で結ぶだけでデータがコピーされたり、参照が渡されたりします。

しかし、C言語における配列は、メモリ上の連続した領域を指す特殊な存在であり、他の変数のように一筋縄ではいきません。

本記事では、C言語において配列を正しく代入・コピーする方法について、基礎的なループ処理から標準ライブラリの活用、さらにはポインタや構造体を用いた高度なテクニックまで、詳細に解説します。

プログラミングの実践において、なぜ直接代入ができないのかという原理を理解することは、メモリ管理能力の向上に直結します。

2026年現在の開発環境においても変わらない、C言語の本質的な仕様を紐解いていきましょう。

配列の直接代入が不可能な理由

C言語において、配列名はその配列の先頭要素を指すポインタ定数として扱われます。

この性質が、配列同士の直接代入を不可能にしている最大の要因です。

例えば、以下のコードを考えてみましょう。

C言語
int a[5] = {1, 2, 3, 4, 5};
int b[5];

b = a; // コンパイルエラー

このコードをコンパイルしようとすると、多くのコンパイラで「代入式の左辺値が配列型である」といった内容のエラーが発生します。

これは、配列名 b がメモリ上の固定されたアドレスを指しており、そのアドレス自体を書き換えることができないためです。

つまり、配列変数は「値を格納する箱」であると同時に、「場所を示すラベル」としての性質が強く、ラベルそのものを別の場所へ張り替えることは許されていないのです。

配列の内容を別の配列にコピーしたい場合は、メモリ上のデータを物理的に転送する手続きが必要になります。

初期化時における代入の特殊性

配列の「代入」と「初期化」は、C言語において明確に区別される必要があります。

宣言と同時に値を割り当てる初期化のタイミングに限っては、波括弧 {} を用いた一括代入のような記述が可能です。

宣言時の初期化

C言語
#include <stdio.h>

int main(void) {
    // 宣言と同時に初期化
    int numbers[] = {10, 20, 30, 40, 50};
    
    for (int i = 0; i < 5; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    
    return 0;
}
実行結果
numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
numbers[3] = 40
numbers[4] = 50

このように、宣言時であれば複数の要素を一度に流し込むことができます。

しかし、一度宣言が終わった配列に対して、後から numbers = {1, 2, 3}; のように記述することは構文ルール違反となります。

指定初期化子(C99以降)

モダンなC言語(C99以降およびC23規格を含む)では、特定のインデックスを指定して初期化することも可能です。

C言語
int arr[10] = {[2] = 100, [5] = 200};

これにより、特定の要素以外を0で埋めつつ、必要な箇所だけを効率的に初期化できます。

ただし、これもあくまで「初期化」の範疇であり、実行中の「代入」には使えません。

ループ処理による要素ごとの代入

最も基本的かつ汎用的な配列コピーの方法は、forループを使用して要素を一つずつコピーする方法です。

この方法は、配列の型に関わらず利用でき、コピーの過程で値を加工することも可能なため、非常に柔軟です。

基本的な実装例

C言語
#include <stdio.h>

#define ARRAY_SIZE 5

int main(void) {
    int source[ARRAY_SIZE] = {1, 2, 3, 4, 5};
    int destination[ARRAY_SIZE];

    // forループによる一つずつの代入
    for (int i = 0; i < ARRAY_SIZE; i++) {
        destination[i] = source[i];
    }

    // 結果の確認
    for (int i = 0; i < ARRAY_SIZE; i++) {
        printf("destination[%d] : %d\n", i, destination[i]);
    }

    return 0;
}
実行結果
destination[0] : 1
destination[1] : 2
destination[2] : 3
destination[3] : 4
destination[4] : 5

この方法のメリットは、型安全性が高いことと、デバッグが容易なことです。

また、コピー元とコピー先の配列サイズが異なる場合に、特定の条件でコピーを中断するといった制御も容易に行えます。

欠点としては、要素数が多い場合に記述が冗長になり、大規模なデータ転送では後述する memcpy よりも実行速度がわずかに劣る可能性がある点です。

memcpy関数による高速な一括コピー

大量のデータを一括でコピーしたい場合、標準ライブラリ string.h に定義されている memcpy 関数を使用するのが一般的です。

この関数は、メモリブロックをバイト単位で直接コピーするため、非常に高速です。

memcpyの基本構文

C言語
void *memcpy(void *buf1, const void *buf2, size_t n);
  • buf1:コピー先のメモリブロックへのポインタ
  • buf2:コピー元のメモリブロックへのポインタ
  • n:コピーするバイト数

実装例

C言語
#include <stdio.h>
#include <string.h>

int main(void) {
    double src[3] = {1.1, 2.2, 3.3};
    double dest[3];

    // memcpyによる一括コピー
    // sizeof(src) は配列全体のバイトサイズを返す
    memcpy(dest, src, sizeof(src));

    for (int i = 0; i < 3; i++) {
        printf("dest[%d] = %.1f\n", i, dest[i]);
    }

    return 0;
}
実行結果
dest[0] = 1.1
dest[1] = 2.2
dest[2] = 3.3

memcpy を使用する際の注意点は、コピーするサイズの指定を誤らないことです。

第3引数には要素数ではなく「バイト数」を渡す必要があります。

そのため、sizeof(配列名) を使用するか、要素数 * sizeof(型) と計算して渡すのが定石です。

また、コピー元とコピー先の領域がメモリ上で重複している場合、memcpy の動作は未定義となります。

そのようなケースでは、重複を考慮して安全にコピーを行う memmove 関数を使用してください。

ポインタを用いた配列の「代入」と扱い

C言語において、配列を別のポインタ変数に代入することは可能です。

しかし、これはデータのコピーではなく、メモリアドレスの共有(参照のコピー)であることを理解しなければなりません。

ポインタへの代入

C言語
#include <stdio.h>

int main(void) {
    int original[3] = {100, 200, 300};
    int *ptr;

    // 配列の先頭アドレスをポインタに代入
    ptr = original;

    printf("ptr[1]の値: %d\n", ptr[1]);

    // ポインタ経由で値を書き換える
    ptr[1] = 999;

    printf("original[1]の値: %d\n", original[1]);

    return 0;
}
実行結果
ptr[1]の値: 200
original[1]の値: 999

上記の例では、ptroriginal と同じメモリ領域を指しています。

そのため、ptr を通じて行った変更は、そのまま original に反映されます。

これは「浅いコピー(Shallow Copy)」と呼ばれ、大きなデータをコピーせずに扱えるため効率的ですが、意図しない書き換えが発生するリスクも孕んでいます。

実務においては、関数に配列を渡す際にこの仕組みが利用されます。

C言語では配列そのものを引数として渡すことはできず、常に先頭要素へのポインタが渡されます。

構造体を利用した配列の一括代入テクニック

C言語の制限を回避する面白いテクニックとして、配列を構造体のメンバーに含めるという方法があります。

驚くべきことに、C言語では構造体同士の代入は「=」演算子で許可されており、その際にメンバーである配列も丸ごとコピーされます。

構造体によるコピーの例

C言語
#include <stdio.h>

// 配列を内包する構造体の定義
typedef struct {
    int data[5];
} ArrayWrapper;

int main(void) {
    ArrayWrapper a = {{1, 2, 3, 4, 5}};
    ArrayWrapper b;

    // 構造体同士なら「=」で代入可能
    b = a;

    // コピー先の値を確認
    for (int i = 0; i < 5; i++) {
        printf("b.data[%d] = %d\n", i, b.data[i]);
    }

    // 独立性の確認(bを書き換えてもaには影響しない)
    b.data[0] = 99;
    printf("a.data[0] = %d, b.data[0] = %d\n", a.data[0], b.data[0]);

    return 0;
}
実行結果
b.data[0] = 1
b.data[1] = 2
b.data[2] = 3
b.data[3] = 4
b.data[4] = 5
a.data[0] = 1, b.data[0] = 99

この方法は、コードがシンプルになるという利点があります。

コンパイラ内部では memcpy 相当の処理が行われるため、パフォーマンスも良好です。

ただし、配列のサイズが固定されている場合に限られるため、動的な配列には向きません。

文字列(char配列)のコピーにおける注意点

C言語において、文字列は文字の配列(char 型の配列)として扱われますが、代入に関してはさらに注意が必要です。

文字列の代入には、通常 strcpy や、より安全な strncpy、あるいは環境によって提供される strcpy_s を使用します。

関数名特徴2026年時点の推奨度
strcpy単純なコピー。バッファサイズをチェックしない。非推奨(脆弱性の原因)
strncpy指定した文字数分コピーする。終端ヌル文字に注意が必要。一般的
strcpy_sサイズ指定を必須とし、エラーチェックを行う(MSVC等)。推奨(環境依存あり)

文字列の末尾には必ずヌル文字(\0)が含まれている必要があり、これを忘れるとメモリを越えて読み取りを続けるバグの原因となります。

配列のコピーを扱う際は、常にデータの終端を意識してください。

多次元配列のコピー方法

多次元配列(例:int matrix[3][3])も、メモリ上では一列に並んだ連続した領域として確保されます。

そのため、要素ごとのループ処理のほかに、memcpy による一括コピーも可能です。

多次元配列の一括コピー例

C言語
#include <stdio.h>
#include <string.h>

int main(void) {
    int matrix1[2][2] = {{1, 2}, {3, 4}};
    int matrix2[2][2];

    // 全体を一括コピー
    memcpy(matrix2, matrix1, sizeof(matrix1));

    printf("matrix2[1][0] = %d\n", matrix2[1][0]);

    return 0;
}
実行結果
matrix2[1][0] = 3

ただし、多次元配列が「ポインタの配列」として動的に確保されている場合は、memcpy 一回では正しくコピーできません。

各ポインタの先にある実データを個別にコピーする深いコピー(Deep Copy)が必要になります。

これは複雑なデータ構造を扱う際の必須知識です。

2026年のベストプラクティス:安全性と効率の両立

現代のC言語プログラミング(C23以降を視野に入れた開発)では、単にコピーができるだけでなく、セキュリティ上の安全性が強く求められます。

  1. バッファオーバーランの防止:コピー先のサイズがコピー元よりも小さい場合、メモリ破壊が発生します。必ずサイズチェックを行うか、動的なメモリ割り当て(malloc)と組み合わせて適切なサイズを確保してください。
  2. 静的解析ツールの活用:2026年現在の開発環境では、コンパイラの警告レベルを上げたり、静的解析ツールを使用したりすることで、配列代入のミスを事前に検知することが標準となっています。
  3. 定数性の活用:書き換える必要のないコピー元配列には const 修飾子を付与し、意図しない変更を防止してください。
C言語
// より安全なコピー関数のイメージ
void safe_copy(int *dest, size_t dest_size, const int *src, size_t src_count) {
    if (dest_size < src_count * sizeof(int)) {
        // エラー処理
        return;
    }
    memcpy(dest, src, src_count * sizeof(int));
}

まとめ

C言語における配列の代入は、他のプログラミング言語に慣れた人ほど戸惑うポイントです。

しかし、その背後にある「配列名はアドレスを指すポインタ定数である」という基本原則を理解すれば、なぜ特別な手続きが必要なのかが見えてきます。

  • 宣言時以外の直接代入(=)は不可能
  • 柔軟な操作が必要ならforループによる個別代入。
  • パフォーマンスを重視するならmemcpyによる一括コピー。
  • 参照だけで良いならポインタによる共有。
  • 構文上の利便性を求めるなら構造体の活用。

それぞれの方法にはメリットとリスクが存在します。

開発しているシステムの要件やデータの規模に応じて、最適な手法を選択してください。

メモリを直接制御するC言語の配列操作をマスターすることは、コンピュータの仕組みを深く理解することと同義です。

この記事が、より堅牢で効率的なC言語プログラムを書くための一助となれば幸いです。