C言語の初心者の人がよく躓く中の一つに、配列の要素を別の配列にコピーするということがあるのではないでしょうか?
何事もそうですが、慣れてしまえば、何も難しくありません。
用意されている標準関数を使うか自分でコピーするプログラムを作るかのいずれかです。
まずは、自分で配列の要素を別の配列にコピーするプログラムを作ってみましょう。
配列の要素を一つずつコピーする
一番わかりやすいし、データを加工してから代入したり、ずらして代入したり、いろんな融通が利く方法ですね。for文で順番に代入していきます。
<stdio.h> void main(void) { short data1[10] = {5,7,2,8,1,0,3,9,6,4}; short data2[10]; short i; for (i = 0; i < 10; i++) { data2[i] = data1[i]; } }
2次元配列でも理屈は同じです。次のようになります。
<stdio.h> void main(void) { short data1[2][10] = {{5,7,2,8,1,0,3,9,6,4}, {-5,-7,-2,-8,-1,0,-3,-9,-6,-4}}; short data2[2][10]; short i, j; for (i = 0; i < 2; i++){ for (j = 0; j < 10; j++) { data2[i][j] = data1[i][j]; } } }
for文を2重に回すことで、コピーができます。ものすごく簡単ですね。
しかし、間違っても data2 = data1 としてはいけません。
コンパイルエラーになると思いますが、こういう記述はできません。
できたらいいのにと、個人的には思いますが(笑)。
配列の要素を別の配列へコピーするプログラムを毎回、for文を記述するは面倒だと思われる場合は、関数化して使用すればいいと思います。その際、なるべく汎用的に関数化することが重要です。
そうでなければ、使用できる場面が限られてしまい、結局、同じようなプログラムを記述しなくてはならなくなります。そうなっては関数化した意味が薄れてしまいます。
ちょっとややこしいですが、こんな感じです。
<stdio.h> short array_copy(short* array1, short* array2, short size) { short i; /* 引数の配列先頭アドレスがNULL、コピーするサイズが0以下の場合、 何もせずに0を返す。 */ if ((array1 == NULL) || (array2 == NULL) || (size <= 0 ) ) { return 0; } /* array2のデータをarray1にコピーする */ for (i = 0; i < size; i++) { array1[i] = array2[i]; } return 1; } void main(void) { short data1[10] = {5,7,2,8,1,0,3,9,6,4}; short data2[10]; array_copy(data2, data1, 10); }
関数化する際、配列自身を引数で持てないため、配列の先頭アドレスをポインタで渡すようにしています。
また、コピーするサイズが不明なので、それも関数の引数でもらうようにしています。
NULLはNULLポインタのことで0を指します。渡された配列の先頭アドレスが、万が一、不明なものであった場合、コピーすることができません。もし、コピーしようとしたら、プログラムが落ちるか暴走します。
なので、ここで、引数のエラーチェックをしているわけです。
渡されたサイズも同じです。0以下ではコピーできないですよね。
short型の配列のコピーなら、これでいいのではないかと思います。
配列の要素のコピー関数だけでなく、いろんな場面で関数化するべき内容はあるはずなので、それを意識できるようになると、プログラマーとして1歩成長したと言えます。
配列の要素のコピーに関しては標準的に関数がありますので、それを利用する方法を次に紹介します。
memcpy関数を使用する
先ほど、自分で関数化しましたが、もっと汎用的な関数が用意されています。それは、memcpyです。この関数を使用するにはstring.hをインクルードする必要があります。
配列をコピーするためというものではなく、メモリにあるデータを別のメモリに指定したサイズ分コピーするイメージです。使い方は簡単です。
<stdio.h> <string.h> void main(void) { short data1[10] = {5,7,2,8,1,0,3,9,6,4}; short data2[10]; memcpy((void *)data2, (void *)data1, sizeof(data1)); }
第1引数にコピーされる配列の先頭アドレス、第2引数にコピーする配列の先頭アドレス、第3引数にコピーするサイズです。
配列の[]を省略した場合、その配列の先頭アドレスを指すポインタになります。
サイズですが、配列の要素数×short型のサイズになりますので、この例では20ということになります。
それを求めているのが、sizeof(data1)です。10 * sizeof(short)としても結果は同じになります。
これを、printf文で表示させてみれば、コピーできているか確認できますので、試してみてください。
memmove関数を使用する
先ほど、memcpyを紹介しました。似たようなmemmoveというのも標準関数で用意されています。
memcpyとの違いは、処理速度は多少落ちますが、メモリ重複があった場合でも、関数実行前のデータをそのまま、コピーしてくれるものと考えてもらえればいいです。
どういうことかというと、memcpyで配列の要素を別の配列へコピーしようとしたとき、2つの配列のメモリの割り当てに重複があった場合、コピーされた配列の要素が元の配列の要素と同じにならない可能性があります。
組み込みマイコンの場合、プログラマーが変数や配列にアドレスを割り付けたりします。
そのとき、配列の使用領域の演算を間違えてたり、配列の要素数を増やしたりして、使用領域が増えたとき、コピー元の配列の領域がコピー先の配列の領域の一部まで浸食している場合、コピーしたときの結果が保証されません。
もちろん、領域が重なっているのを修正するのが本来のあるべき姿なのですが、それに気づかない場合もあります。そのようなときに有効なのが、memmove関数です。
通常は領域が重なっているとプログラムが正常に動作しないでしょうが、配列は大きめにとったが実際は半分しか使っておらず、重なっている領域は4分の1であった場合は、特に支障なく動いてしまうでしょう。
memcpyの場合、なぜ、領域が重なっていると結果が不定になってしまうかということを少し考えてみましょう。
記述自体は1行で書けますね。
しかし、実行させると、順番にコピーしていくわけです。コピー先の配列の先頭とコピー元の配列の中間位置が同じアドレスだった場合、先頭のデータをコピーしたとき、コピー元の中間位置のデータを変更することになりますよね?
以下にイメージ図を示します。
アドレス: 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 コピー元: 0 1 2 3 4 5 6 7 8 9 コピー先: 0 1 2 3 4 5 6 7 8 9
コピー元はアドレス101番から110番に配列の領域が確保されている状態で、コピー先はアドレス106番から115番に配列の領域が確保されている状態であるとします。
このときに、アドレス101番の値をアドレス106番にコピーされるということがイメージできましたか?
そうするとどうでしょう。コピー元の配列INDEX 5 の場所でもあるため、このデータも書き換えられてしまうのです。
そうなったら、いざ、INDEX 5の値をコピーしようとしたとき、元の値が書き換えられてしまっているため、期待していた値になりません。
しかし、memmoveではそうならないように配慮されています。したがって、組み込みマイコンではmemmoveを使うようにしましょう。
先ほどもいいましたが、本来、こうならないように配列の領域割り当ては行わなくてはいけません。
まとめ
配列の要素のコピーの仕方について、解説しました。
・自分でプログラムを記述し1つずつ要素をコピーする。
・標準関数memcpy、memmoveを使用する。
ということでしたね。構造体配列のコピーはここでは解説しませんでした。
memcpyやmemmoveを使用すれば、構造体配列でもコピー領域さえ間違えなければ、問題なくコピーされます。
構造体に関しては、別途解説しようと思います。
配列の要素のコピーは配列の初期化にも応用できると思いますので、ぜひ活用ください。