ポインタ
C言語の学習をされている方で、つまづきやすいところの一つに「ポインタ」があると思います。私は学生の頃、ポインタが理解できずにC言語を一度挫折した経験があります。
しかし以前受講した組み込みソフトの研修の中で、ある関数に出会うことであっさり理解できました。
その関数は「malloc」と「free」となります。
どうしてあっさり理解できたのか、私自身の考え方となりますがその経験をお伝えできればと思います。
「C言語での戻り値は一つだけなので、配列を戻り値にできない」という考えから「配列を返してもらうには参照渡しをする」という考え方に至るストーリーをお伝えしていきます。
私個人の考え方ですので少し回りくどく感じるかもしれませんが、ストーリーを辿ることでご理解のお役に立てればと思います。
「ポインタとは何か?」を一言で言ってしまうと次の通りとなります。
結論:ポインタとはC言語で変数を参照渡しするときに使うもの
こちらの記事も参考にして頂ければと思います。「C言語のポインタとは結局何なのか? 〜ポインタがある理由〜」
配列を戻り値で返す
C言語の配列は複数の値で構成されますので、配列をそのまま戻り値として返すことができません。
しかし戻り値としては一つだけなら値を返すことができますので、「配列の先頭アドレスを返せば良いのではないか」という考えが浮かびます。
配列の先頭アドレスが分かれば、そのアドレスを起点にして配列の値を順次読み出すことができます。
この考え方でコードを作成すると下記のようになります。(3つの値を入力して昇順にソートする「sort関数」を例にしております。)
int *sort(int x, int y, int z){ // 昇順にソートする関数:戻り値はアドレス
int number[3];
int tmp;
number[0]=x;
number[1]=y;
number[2]=z;
/* 数値を昇順にソート */
for (i=0; i<3; ++i) {
for (j=i+1; j<3; ++j) {
if (number[i] > number[j]) {
tmp = number[i];
number[i] = number[j];
number[j] = tmp;
}
}
}
return(&number[0]); // 先頭のアドレスを返す。
}
void main(void){
int x,y,z;
int *num_sort; // アドレスが返ってくるのでポインタを宣言
x=3;
y=1;
z=2;
num_sort = sort(x,y,z); // 返ってきたアドレスをポインタに代入
/* 配列の要素を出力 */
printf("%d\n", num_sort[0]);
printf("%d\n", num_sort[1]);
printf("%d\n", num_sort[2]);
// →「1,2,3」と意図した結果にならない。
}
上記コードはwarningが表示されるかもしれませんが、シンタックスエラーは発生しません。従いまして一応動作はします。
しかし動作はするのですが期待した出力「1, 2, 3」は表示されずに、よく分からない数値が出力されるかと思います。
これはメモリの動作が関係しており、main関数側で意図しない値が読み出されているからです。
ここで注目するべきことは宣言した変数が「自動変数」であることです。次は自動変数についてご説明していきます。
自動変数
変数を宣言する時に自動変数かどうかを意識していないと思いますが、変数を宣言するときはデフォルトで自動変数となっているので気にしなくて大丈夫です。
「int x;」と宣言すると勝手に「auto int x;」となっているとご理解ください。
ここで「何が自動なのか?」ということですが、これは「メモリ操作が自動である」ということになります。
メモリ操作とは変数が宣言された時のメモリ領域の確保と、return時のメモリ領域の開放となります。具体的には「malloc」と「free」という命令が使用されることになります。
- 変数が宣言 → メモリ領域の確保 : mallocが実行される
- return実行 → メモリ領域の開放 :freeが実行される
C言語で変数を使う時はmallocとfreeを使ってメモリ操作をする必要があるのですが、自動変数はその操作を自動で行ってくれています。
先程のコードで変数「number」に注目して、どういうメモリ操作が行われているかをイメージすると、下記の通りとなります。
int *sort(int x, int y, int z){
int number[3];
/* メモリ領域の確保:int型を3つ分確保する
「number = (int*)malloc(sizeof(int) * 3);」が勝手に実行されている。*/
int tmp;
number[0]=x;
number[1]=y;
number[2]=z;
/* 数値を昇順にソート */
for (i=0; i<3; ++i) {
for (j=i+1; j<3; ++j) {
if (number[i] > number[j]) {
tmp = number[i];
number[i] = number[j];
number[j] = tmp;
}
}
}
/* メモリ領域の開放
「free(number);」が勝手に実行されている。*/
return(&number[0]); // 先頭のアドレスを返す。
}
自動変数は「mallocとfreeを勝手にやってくれている」くらいのイメージをつかんで頂けるとありがたいです。
ここで重要なことは自動変数は「return時に勝手にメモリが開放されている」ということです。
戻り値でアドレスを返したのに、main関数側で意図しない値が読み出されている理由は、「開放されたメモリを読みにいっているから」となります。
静的変数による解決
returnを実行すると勝手にメモリが開放されるのであれば、開放されないようにすれば良いということになります。
その方法として静的変数で宣言するというやり方があります。静的変数は変数宣言時に「static」とつけるだけでOKです。 (例: static int x;)
この方法で先程のコードを修正すると下記の通りとなります。
int *sort(int x, int y, int z){ // 昇順にソートする関数:戻り値はアドレス
static int number[3]; //静的変数で宣言
int tmp;
number[0]=x;
number[1]=y;
number[2]=z;
/* 数値を昇順にソート */
for (i=0; i<3; ++i) {
for (j=i+1; j<3; ++j) {
if (number[i] > number[j]) {
tmp = number[i];
number[i] = number[j];
number[j] = tmp;
}
}
}
return(&number[0]); // 先頭のアドレスを返す。
}
void main(void){
int x,y,z;
int *num_sort; // アドレスが返ってくるのでポインタを宣言
x=3;
y=1;
z=2;
num_sort = sort(x,y,z);
/* 配列の要素を出力 */
printf("%d\n", num_sort[0]);
printf("%d\n", num_sort[1]);
printf("%d\n", num_sort[2]);
// →「1,2,3」と表示される。
}
これでreturn時に変数「number」はメモリが開放されることがないので、main関数側で問題なく読み出すことができます。
しかし「教科書に書かれているコードと違う」、「このコードは本当に正しいの?」と思われた方もいらっしゃるかと思います。
上記コードでも問題なく動作はしますが、もう少し工夫できますので次はそこをご説明していきます。
参照渡し
ここで発想を逆転させます。
main関数側から見て「アドレスをもらって読みに行く」ではなく、「アドレスを渡して書いてもらう」と考え方を変えてみます。
「アドレスをもらって読みに行く」 → 「アドレスを渡して書いてもらう」
先程のコードですが、「アドレスをもらって読みに行く」ということはmain関数はsort関数内の変数「number」に直接アクセスしていることになります。
ということは逆にsort関数側からmain関数内の変数に直接アクセスすることもできると考えることができます。
つまりmain関数側で変数を宣言して、そのアドレスをsort関数に渡してあげれば、sort関数側からmain関数内の変数に直接アクセスすることができるということです。
こうすればmain関数が呼び出し元となりますので、メモリの開放は気にしなくて良くなります。
この考え方でコードを修正すると下記の通りとなります。(x,y,zはあえて残してあります)
void sort(int x, int y, int z, int *number){ // ポインタでアドレスをもらう
int tmp;
number[0]=x;
number[1]=y;
number[2]=z;
/* 数値を昇順にソート */
// main関数の変数num_sortに直接アクセスして書き換えている
for (i=0; i<3; ++i) {
for (j=i+1; j<3; ++j) {
if (number[i] > number[j]) {
tmp = number[i];
number[i] = number[j];
number[j] = tmp;
}
}
}
return; // 戻り値はなし
}
void main(void){
int x,y,z;
int num_sort[3];
x=3;
y=1;
z=2;
sort(x,y,z,&num_sort[0]); // sort関数にnum_sortのアドレスを渡す
// sort関数を実行することで変数「num_sort」の内容が書き換わっている
/* 配列の要素を出力 */
printf("%d\n", num_sort[0]);
printf("%d\n", num_sort[1]);
printf("%d\n", num_sort[2]);
// →「1,2,3」と表示される。
}
アドレスを返してもらう必要はないので、戻り値はなしとなります。これで教科書に書かれているようなコードになったと思います。
長々と説明してきましたが、ポインタの動作は「アドレスを渡して書いてもらう」ということになります。これを言い換えると「呼び出し元から参照渡しする」ということになります。
ポインタの動作:アドレスを渡して書いてもらう → 呼び出し元から参照渡しする
従いまして、「ポインタとは参照渡しをするときに使うもの」という考えになります。
まとめ
「C言語のポインタとは結局何なのか?〜ポインタの使い方〜」についてまとめてみました。
少し長くなりましたが、私の考え方をトレースして頂くことでご理解のお役に立てれば幸いです。
よろしければこちらの記事も参照して頂ければと思います。「C言語のポインタとは結局何なのか? 〜ポインタがある理由〜」