この記事の要点
- 配列とは、同じデータ型がいくつか並んだもの
- 配列を確保したら、初期化子リストで全ての要素を初期化する
- 配列の要素数を求めるには、sizeof(配列) / sizeof(任意の配列の要素) を使う
C言語の配列は、ポインタと混同されやすい、コピーにひと手間かかるなど、初心者がつまずきやすいポイントの1つです。
当記事では、ポインタとの違い、初期化、コピーの方法、要素数の取得、引数への渡し方、多次元配列といった、初心者必修の基礎テクニックをわかりやすく解説します。
目次
C言語の配列とは?ポインタとの違い
C言語の配列とポインタは混同されやすいですが、まったくの別物です。
それぞれについて簡単に説明します。
配列とは?
配列とは、同じデータ型がいくつか並んだもののことです。
以下のように使います。
int array[3] = {10 , 20, 30};
この場合、メモリ上にint型3つ分の領域が確保されます。
ポインタとは?
ポインタは、メモリ上の位置情報(アドレス)を取り扱うための概念です。
int num = 10; int *pNum; // ポインタ変数pNumを宣言 pNum = # // ポインタ変数pNumに、変数numのアドレス(&numで取得)を代入する
初心者がつまずきやすいポインタについて、理解するためのコツをこちらでも解説しています。
配列とポインタを混同してしまう理由
配列とポインタの図を見比べていただくと、両者が別物であることは一目瞭然だと思います。
ではなぜ混同してしまうかというと、次のようなややこしい言語仕様になっているからです。
- 配列は式の中では先頭要素へのポインタとして扱われる
- ポインタが配列を指している場合、[]を使うことで配列の各要素にアクセスできる
例えば、次のコードはコンパイル、実行ともに成功します。
int array[] = {10 , 20, 30}; int *pArray = array; // arrayは式の中では、array[0]へのポインタとして扱われる printf("%d", pArray[1]); // 「20」が出力される
このように、配列とポインタはC言語の仕様上、紛らわしい使い方をします。
逆に言うと、紛らわしいだけであって、配列=ポインタではありません。
しっかり整理して理解しましょう。
配列の初期化
C言語では、確保したメモリは基本的に、毎回初期化する必要があります。
なぜなら、初期化されていないメモリには何が入っているかわからないため、そのままにしておくと思いがけない動作の原因になりうるからです。
配列の場合も同様で、確保したすべての要素を初期化するようにしましょう。
初期化子リストの使い方
配列は、初期化子リスト({})を利用することですっきり簡単に初期化できます。
特定の値で初期化したいとき
{}の中に任意の値を指定することで、その値で要素を初期化することができます。
int array[3] = {10, 20, 30}; for(int i = 0; i < 3; i++){ printf("%d ", array[i]); // 「10 20 30」と出力される }
とりあえずデフォルト値で初期化したいとき
また、値の指定を省略した場合、省略された要素はデフォルトの値(intなどの算術型なら0、ポインタならNULL)で初期化されます。
例えば以下のように記述すると、全ての要素を0で初期化することができます。
int array[3] = {}; for(int i = 0; i < 3; i++){ printf("%d ", array[i]); // 「0 0 0」と出力される }
配列のコピー
C言語では、代入で配列をコピーすることはできません。
例えば、次のような記述はコンパイルエラーになります。
int arrayA[3] = {10, 20, 30}; int arrayB[3] = {}; arrayB = arrayA; // コンパイルエラー
今回は、配列をコピーするための手段として次の2通りの方法をご紹介します。
- for文で全ての要素を1つずつコピーする(初心者向け)
- memcpy関数を使って、メモリごとコピーする(おすすめ)
for文で全ての要素を1つずつコピーする
こちらの方法では、次のようにコピーを行います。
int arrayA[3] = {10, 20, 30}; int arrayB[3] = {}; for(int i = 0; i < 3; i++){ arrayB[i] = arrayA[i]; }
簡単に書けて、初心者の方にも扱いやすい方法です。
しかし、次のようなデメリットがあります。
- コード記述量が増える
- 配列の要素数が増えれば増えるほど、処理に時間がかかる
そのため、できれば次に紹介する方法でコピーできるように練習しましょう。
memcpy関数を使って、メモリごとコピーする
こちらの方法では、memcpy関数を使用します。
memcpy関数は、配列に限らず、あるメモリに格納されているデータを、別のメモリにコピーするための関数です。
memcpy関数の使い方は次の通りです。
第一引数 | コピー先のポインタ |
---|---|
第二引数 | コピー元のポインタ |
第三引数 | コピーするサイズ |
戻り値 | コピー先のポインタ |
コード例は次のようになります。
int arrayA[3] = {10, 20, 30}; int arrayB[3] = {}; memcpy(arrayB, arrayA, sizeof(arrayB)); for(int i = 0; i < size; i++){ printf("%d ", arrayB[i]); // 「10 20 30 」と出力される }
上記コードは、「arrayB(のポインタが指すメモリ)に、arrayA(のポインタが指すメモリ)の内容を、arrayBの大きさ分だけコピーする」という処理です。
for文を使うコード例と比べて、記述量が減ってすっきり書けています。
また、処理スピードもfor文を使う場合より早いです。
ただし、ポインタを扱う必要があったり、確保した配列のサイズを超えてコピーしてしまう危険があったりと、慣れるまでは扱いが難しいです。
まだポインタを理解しきれていない方は、まずはそちらの学習を完了させてから、改めてmemcpyを学習すると理解が進みやすいでしょう。
配列の要素数の取得方法
配列の要素数は、その配列の型がなんであっても求められる、万能の計算方法があります。
その計算式が、こちらです。
配列全体のサイズ ÷ 要素1つ分のサイズ
コードにすると sizeof(配列) / sizeof(任意の配列の要素) となります。
int array[3] = {10, 20, 30}; int size = sizeof(array) / sizeof(array[0]); // 要素数を求める printf("%d", size); // 「3」が出力される
よく使うテクニックで、これ1つ覚えておけばOKなので、しっかり理解しておきましょう。
引数に配列を指定する方法
C言語では、関数の引数に配列をそのまま渡すことはできない仕様になっています。
そこで、引数に配列を渡したい場合はポインタを使うことになります。
void printArray(int *array){ printf("%d", array[0]); } int main(){ int array[3] = {10, 20, 30}; printArray(array); // 「10」が出力される return 0; }
ポインタを使うと、関数側からは配列全体の大きさや要素数を知ることができません。
そのため、配列を関数に渡す際は、以下のように、一緒に要素数を渡すことが多いです。
void printArray(int *array, int size){ for(int i = 0; i < size; i++){ printf("%d ", array[i]); } }
多次元配列の使い方
多次元配列とは、配列の中に配列があるようなイメージです。
実際には、配列の中に「配列へのポインタ」が入っているのですが、ややこしいので慣れないうちはシンプルに考えましょう。
多次元配列は以下のように利用します。
int array[2][3] = {{10, 20, 30}, {40, 50, 60}}; printf("%d", array[0][1]); // 「20」が出力される printf("%d", array[1][2]); // 「60」が出力される
こちらのコードを図にすると、次のようなイメージです。
まとめ
今回の記事では、C言語の配列に関するテクニックについてご説明しました。
配列自体は難しい概念ではないのですが、C言語の配列は、正しく初期化しないとバグの原因になる、代入でコピーができない、関数に渡すにはポインタを利用する必要があるなど、慣れるまでは扱いにくい側面もあります。
しかし、システム開発に携わる場合、これらの配列の操作はほぼ必須といえます。
この記事の内容を参考に、実際に自分でサンプルプログラムを作成すると、より理解が深まります。
ぜひ、配列を自由に操作できるよう練習してみてください!