こんにちは、ナナです。
「ポインタ」と「配列」は異なる機能です。
しかし、C言語の中ではポインタと配列の扱い方が、酷似している部分があります。何が同じで何が違うのかを学んでいきます。
ポインタの全貌を学びたい方は『C言語 ポインタを使いこなせ【身に付けるための9の極意】』の記事から順に読むことをお勧めします。
本記事では次の疑問点を解消する内容となっています。
では、ポインタと配列の関係性を学んでいきましょう。
配列を関数の引数へ渡すとポインタに変身する
はい、はーーい。配列といえば私、配列王子がやってまいりましたよっ!じゃじゃーん。
え?「ポインタ」がテーマ?しょぼん、ガックシです…。
ポインタと配列はね、違うようで似てるんだよ。配列王子を名乗るのであれば、この関係性は知っておくべきなんだよ。
まずは、関数と「配列」「ポインタ」の奇妙な関係性を学ぼうね。
配列を関数の引数で渡したいシーンは、実際の開発の中でもよく出てきます。
この時に注意しなければならないのは、引数へ渡された配列はポインタとなって受け渡しされるということです。
配列を渡される関数の引数はポインタ変数で定義せよ!
実際に配列変数を関数の引数へ渡す例を、プログラムで示しましょう。
main関数側のmoji配列を、subfunc関数の引数として渡しています。subfunc関数側の引数定義は、ポインタ変数として定義されているのがわかるでしょう。
このように、配列を受け取りたい関数の引数は、ポインタ変数として受けなければなりません。
この際に注意することは、subfunc関数側のポインタ変数pmojiを利用して参照先の値を書き換えると、moji配列の中身のデータが変わってしまうことです。
つまり、上記プログラムではprintf関数によるnum[0]の表示は「C」という文字が表示されることになります。
ポインタなので当然と言えば当然ですね。
配列の変数名は番地として扱われる
先ほどの例にて「配列」を「ポインタ変数」に受け渡しました。簡易的に書くと次になります。
// 配列定義
char moji[2] = {'A', 'B'};
// ポインタ変数定義
char * pmoji;
// ポインタに配列名を代入?
// &mojiとしなくてよいのか?
pmoji = moji;
このプログラムの書き方は正しいのですが、不思議に思いませんか?
ポインタ変数へ番地を設定するときは、変数名にアドレス演算子「&」を付けて番地を取り出していましたよね。しかし、このプログラムには「&」がそもそも出てきていません。
ここから導き出せる答えは「配列の変数名は番地として解釈される」ということです。
そのため「&」をわざわざ付ける必要がないのです。
「変数」と「配列変数」では番地の取り出し方が変わるんです。これはすごく大事なルールなんです。このことが「配列」と「ポインタ」の関係性を近くすることになるんです。
ポインタからの配列的アクセス方法
ほほほーい。「ポインタ」と「配列」が親戚と聞いて、俄然やる気が湧いてきましたよー。ポインタを使って配列を使いこなす。この配列王子が必ずマスターしてみせますっ!ででで、どうやって使うんですか?
ポインタに配列のように[]を付ければ使えるんだよ。配列と一緒でしょ。これが配列とポインタの類似性だよ。
配列を使ったプログラムでは、次のように[]を使って各配列要素にアクセスしていました。
char moji[2];
moji[0] = 'A';
moji[1] = 'B';
ポインタが配列を参照している場合、次のように配列要素にアクセスができます。
char moji[2];
// 配列をポインタから参照 pmoji --> moji
char * pmoji = moji;
// ポインタから配列へ書き込み
pmoji[0] = 'A';
pmoji[1] = 'B';
よく見てください。なんと、ポインタ変数に[]を使って配列を参照しています。
皆さんは、[]とは配列に使用するものではないのかと思っているかもしれませんが、こんなことができるのです。これには理由があります。
この添字演算子ですが、配列にもポインタ変数にも利用できます。それはなぜか?
演算子と書き方
番地[インデックス]
使用例
pmoji[0]
説明
番地からインデックスで指定した距離のメモリ場所へアクセスする。
添字演算子[]は配列に対して作用しているわけではなく、番地に作用しているのです。
この条件を満たせば「配列」でも「ポインタ」でも利用ができます。
配列とポインタ変数に対して添字演算子を使うことは最終的に同一の解釈に落ち着きます。
配列名に添字演算子を適用した場合
ポインタに添字演算子を適用した場合
「配列名は番地である」ということが、ここで活きてきます。これこそが配列にもポインタ変数にも、添字演算子が使用できる理由です。
添字演算子の[]は配列にもポインタ変数にも使える便利なヤツなんですよ!
関数の引数で指定できるポインタ定義のバリエーション
はい、はーい。配列を関数で受け取るときにはポインタ変数で定義する必要があるんですねー。もし、引数を配列として定義したらビルドエラーが出るんですか?
お、これはいい質問だね。実際に書いてみるとわかるけど、ビルドエラーは出ないんだよ。でもね、配列のように書くことに対して注意が必要だよ。
関数の引数で配列を受け取る場合には、ポインタ変数として定義します。
しかし、
このポインタの定義方法ですが、いくつかの記述方法が存在します。このバリエーションは配列とポインタの関係性が近いことにより認められている特殊な記法です。
次のように配列を定義し、func関数へ配列を渡すことを想定します。
#include <stdio.h>
int main(void)
{
long num[10];
// 関数に配列numの受け渡し
func(num);
return 0;
}
この時に定義するfunc関数の定義の引数は、次のいずれかの書き方が認められています。
①:標準的な表記
void func(long * pnum)
{
}
②:配列表記(要素無)
void func(long pnum[])
{
}
③:配列表記(要素有)
void func(long pnum[10])
{
}
注目すべきは②③ですが、引数の定義が配列っぽい表記ですよね。
しかし、この②③は配列ではありません。②③は見た目は配列のようですが、正体は①のポインタ変数と全く同じものです。
関数に対して配列を丸ごと受け渡すことはできません。すべてポインタに変えられてしまいます。
ですが、「配列を受け取ります」という意味を強調できるよう、関数の引数に限りこれらの特殊な記法が認められています。
一般的に配列を受け取るときでも①の記法を好む方が多いですが、配列を強調するため②の記法を使用する方もいます。③の書き方は使われません。
開発者は他の人が作成したプログラムを読むことも日常ですので、この特殊な記法のルールは知っておく必要があります。
このルールは関数の引数だけに適用されるルールです。すごく特殊なルールということになります。
ポインタから配列へのアクセス方法のバリエーション
ほーーい。配列とポインタは仲良しなんですね。では、僕もポインタと仲良しになろうと思います。ポインタと仲良くなれる方法を教えてください!
じゃあ、ポインタから配列にアクセスするための方法を教えるよ。いくつかの方法があるから、好きな方法を選んで仲良くなってね。
配列を参照しているポインタを扱う際に、メモリへのアクセス方法にはいくつかの書き方が存在します。
次のプログラムにおいて、subfunc関数は配列へのポインタを引数で管理しています。このポインタを使った配列へのアクセス方法として、パターン①~③が存在します。
#include <stdio.h>
void subfunc(long * pnum, int size);
int main(void)
{
long num[5] = { 10, 20, 30, 40, 50 };
// 配列を渡す時は配列要素数も一緒に渡す
subfunc(num, sizeof(num)/sizeof(num[0]));
return 0;
}
void subfunc(long * pnum, int size)
{
int i;
// ①:配列的参照
for (i=0 ; i < size ; i++)
{
printf("%d\n", pnum[i]);
}
// ②:ポインタ位置からの相対参照
for (i = 0; i < size; i++)
{
printf("%d\n", *(pnum + i));
}
// ③:ポインタ位置をずらして参照
for (i = 0; i < size; i++)
{
printf("%d\n", *pnum);
pnum++;
}
return;
}
よく利用されるのが①と③の参照方法です。それぞれの特徴は下記です。
パターン①
ポインタを配列的に参照するパターン。
あたかも配列のように参照できるため、直感的で使いやすい。
パターン②
ポインタの番地にインデックスを加え、ポインタの参照位置をずらすパターン。
パターン①と同様の意味であるが、①の方がわかりやすいため、あまり使われない。
パターン③
ポインタの参照位置を直接ずらしながら順番に参照していくパターン。
配列先頭の番地がわからなくなるため、使う時には注意が必要。
補足になりますが、配列を関数の引数に渡す際には配列要素数も一緒に渡すのがセオリーです。
これは配列を渡された側の関数にとって配列はポインタ変数としてしか認識できず、配列要素数を知ることができないためです。
関数には「配列へのポインタ」と「配列要素数」を一緒に渡すというのは、よくある実践的なパターンですね。覚えておくとよいですよ。
Q&A:ポインタと配列の関係に関するよくある質問
どんな質問でも、どしどしどうぞ!
Q:配列をポインタではなく、値渡しとして関数の引数で受け渡すことはできないの?
くくく、悔しいですっ!配列はなぜ関数にそのまま渡すことができないのですか?配列王子としての尊厳に関わる問題ですよ、これは。配列協議委員会に物申しますよ!
そんな委員会あるの?初めて知ったよ。配列を関数の引数でマルっと受け渡す方法はあるにはあるんだよ。だから物申さないでね。
基本的に配列は値渡しで渡すことはできず、ポインタ渡しになってしまいます。
しかし、構造体の中に配列を定義することで、構造体としての値渡しならば可能です。
#include <stdio.h>
// 配列を内包する構造体定義
typedef struct
{
long num[10]; // 配列10個
} S_PACK;
void func(S_PACK pack)
{
// 配列[0]を書き換え
pack.num[0] = 0x89ABCDEF;
return;
}
int main(void)
{
S_PACK data;
// 配列[0]に値を格納
data.num[0] = 0x01234567;
func(data);
// 結果変わらずは0x01234567
printf("0x%08x", data.num[0]);
return 0;
}
このようにfunc関数で構造体内の配列を書き換えても呼び出し元の配列には影響がありません。これは構造体のデータは値渡しが可能だからです。
課題:ポインタと配列の関係が学べたかを確認しよう
もしも、プログラムが上手く動かなくて困ったときは、答えを見るのではなく「デバッガ」の使い方を学びましょう。
この記事を見ると問題の解決技術が身に付きます。困ったときのオススメ記事です!
課題1
課題内容
次の関数を定義せよ。
次のプログラムに上記関数を追加し、ケース①~③を切り替えて出力期待結果が表示されることを確認せよ。
main.c
#include <stdio.h>
int main(void)
{
long num[] = {29, 9642, -3849, 628, 20};
long sum = 0;
int ret;
// ケース①
ret = sumArray(num, sizeof(num)/sizeof(num[0]), &sum);
// ケース②
// ret = sumArray(NULL, sizeof(num)/sizeof(num[0]), &sum);
// ケース③
// ret = sumArray(num, sizeof(num)/sizeof(num[0]), NULL);
if (ret == 0)
{
printf("sum:%d", sum);
}
else
{
printf("ERROR");
}
return 0;
}
出力期待結果
ケース①のとき
sum:6470
ケース②のとき
ERROR
ケース③のとき
ERROR
main.c
#include <stdio.h>
int sumArray(long * pArray, int arrayNum, long * pSum)
{
long sum = 0;
int i;
// NULLチェック
if (pArray == NULL || pSum == NULL)
{
return -1;
}
// ポインタから配列にアクセス
for (i = 0 ; i < arrayNum ; i++)
{
sum += pArray[i];
}
// 加算結果をポインタ先へ書き込み
*pSum = sum;
return 0;
}
int main(void)
{
long num[] = {29, 9642, -3849, 628, 20};
long sum = 0;
int ret;
// ケース①
ret = sumArray(num, sizeof(num)/sizeof(num[0]), &sum);
// ケース②
// ret = sumArray(NULL, sizeof(num)/sizeof(num[0]), &sum);
// ケース③
// ret = sumArray(num, sizeof(num)/sizeof(num[0]), NULL);
if (ret == 0)
{
printf("sum:%d", sum);
}
else
{
printf("ERROR");
}
return 0;
}
NULLチェックを実施し、引数の異常値を判定している。
前章のNULLチェックも入れてますね。このように関数の引数でNULLチェックはよく使われます。
ポインタを利用した添字演算子の使い方もしっかりとマスターしてください。
課題2
課題内容
次の関数を定義せよ。
次のプログラムに上記関数を定義し呼び出せ。出力期待結果が表示されることを入力ケース①と②の場合で確認せよ。
main.c
#include <stdio.h>
int main(void)
{
// 入力ケース①
long num[] = {10, -290, 3498, 28, -943};
// 入力ケース②
// long num[] = { 698, 1285, 0, -754, 9832, 1048 ,18};
int i;
// sortNumber関数を呼び出し配列を昇順に並び替えよ
for (i = 0 ; i < sizeof(num)/sizeof(num[0]) ; i++)
{
printf("%d\n", num[i]);
}
return 0;
}
出力期待結果
入力ケース①のとき
-943
-290
10
28
3498
入力ケース②のとき
-754
0
18
698
1048
1285
9832
main.c
#include <stdio.h>
int sortNumber(long * pNumber, int arrayNum)
{
int i, k;
long tmp;
// NULLチェック
if (pNumber == NULL)
{
return -1;
}
// バブルソート
for (i = 0 ; i < arrayNum ; i++)
{
for (k = i + 1 ; k < arrayNum ; k++)
{
if (pNumber[i] > pNumber[k])
{
tmp = pNumber[i];
pNumber[i] = pNumber[k];
pNumber[k] = tmp;
}
}
}
return 0;
}
int main(void)
{
// 入力ケース①
long num[] = {10, -290, 3498, 28, -943};
// 入力ケース②
// long num[] = { 698, 1285, 0, -754, 9832, 1048 ,18};
int i;
// sortNumber関数を呼び出し配列を昇順に並び替えよ
sortNumber(num, sizeof(num) / sizeof(num[0]));
for (i = 0 ; i < sizeof(num)/sizeof(num[0]) ; i++)
{
printf("%d\n", num[i]);
}
return 0;
}
データを昇順や降順で並べ替えることを「ソート」と呼びます。このプログラムは「バブルソート」と呼ばれる方法を利用しています。