C言語の大きな特徴であると共に、駆け出しプログラマの敵。ここではポインタをその正体から見ていくことで理解を深め、イメージできるようになることが目標です。
変数とメモリに関する記述が多く出てくるので、不安な方は変数についての簡単な解説を先に読んでもらえるとよいかと思います。
出来る限り丁寧に説明していますが、解りにくい箇所があれば連絡して下さい。(時間のあるときに)改良できるかとおもいます。
ポインタとは、「変数などのメモリアドレスを格納する変数」のことです。普通の変数と同じく、その値(=アドレス)を使うこともできますし、そのポインタが指すアドレスに格納されている値を使うこともできます。詳しい使い方等は後述します。
ポインタ変数を宣言するには、
型 *変数名;
とします。
また、ポインタ変数が指しているアドレスの中身を参照するには演算子「*」を。変数が格納されている場所のアドレスを参照するには演算子「&」を、それぞれ変数の前につけてやります。
言葉で説明してもイマイチよくわからないと思うので、例を見てみましょう。
int foo = 100; /*整数型変数*/ int *bar; /*整数に対するポインタ変数*/ bar = &foo; /*変数fooのアドレスをポインタ変数barに格納*/ printf("fooの中身 :%d\n",foo); printf("fooのアドレス:%p\n",&foo); printf("barの中身 :%p\n",bar); printf("barが指すアドレスの中身:%d\n",*bar);
「%p」というのはフォーマット指定子の一種です。整数を表示したいときに「%d」を使うように、アドレスを表示したいときにはこれを用います。
さて、上の例を実行すると…
fooの中身 :100 fooのアドレス:00123abc barの中身 :00123abc barが指すアドレスの中身:100
このような結果が表示されるはずです。「00123abc」は、アドレスを16進数で表示しているものです。
※勿論、アドレスは環境によって異なります。
このときのメモリの状態を図で表しておきましょう。(「0x」は後に続く数が16進数だという意味です)
因みに、「scanf」関数などで、よく変数の前に「&」をつけるのは、scanf関数には「格納したい変数のポインタ」を渡すことになっているからです。
ポインタ変数に何も代入しないままそのポインタが指す先を読み書きするとエラーの原因となります。必ず他の変数のアドレスを代入するか、後で解説するメモリの確保を行ってから操作するようにしましょう。
さて、ここまでは難なく理解している人も多いでしょう。しかしポインタが本領を発揮するのは、次の項で扱う関数との連携と、構造体を用いてさまざまなデータ構造を実現するときです。これで前置きは終わり。次項から、肝となる部分を説明していきます!
ポインタを使うと、そのポインタが指す変数の値を書き換えることができます。
先ずは簡単な例から。変数fooの中身を、ポインタ変数barを通して置き換えます。
int foo = 100; printf("foo = %d\n",foo); int *bar = &foo; /*fooのアドレスをbarに*/ *bar = 50; /*ここで値を代入*/ printf("foo = %d\n",foo);
実行結果は、
foo = 100 foo = 50
となります。4行目で、barが指すアドレスの値(つまりfooの値)を50に変更しています。「*」演算子はこのように、ポインタが指すアドレスの中身を書き換えたい時にも使えます。
さてもう少し理解を深めてみましょう。下図を見てください。ポインタを使う場合(緑)と使わない場合(赤)のアクセスの仕方の違いです。
図を見るとわかると思いますが、「変数とその値は1対1対応」しているわけではありません。変数に対応しているのはあくまでも「その変数に対応するメモリアドレス」であり、変数はそのアドレスへの案内役にすぎないのです。だからこそ、先の例(緑の矢印に相当)のように変数fooを全く使わずにfooの値を変更するなどということができたわけです。
さて、ここからが一つ目の山場となります。気合入れていきましょう!
早速ですが例を見てください。main関数のfooという変数の値を、hoge関数の中で書き換えています。
void hoge(int *bar){ *bar = 50; /*「bar」の指す先の値を50に*/ } int main(){ int foo=100; printf("%d\n",foo); hoge(&foo); /*「foo」のアドレスをhogeに渡す*/ printf("%d\n",foo); return 0; }
実行結果は
100 50
となります。fooはhoge関数のスコープ外にありますから、hoge関数内からは「foo」という名前でmain関数内のfooにアクセスすることはできません。
しかし、変数はスコープ外でもメモリの同じ位置にあり続けますから、hoge関数にfooのアドレスを渡せば、それを用いて値を書き換えられるというわけです。そしてこれは前項で行ったことを、関数をまたいで行っているだけのことです。
この項で紹介した事実が、ポインタを用いた有用なテクニックの基礎となっています。後ほど、これらを基にしたもうすこし具体的な技術を紹介します。
ポインタのもう一つの魅力は、配列と深い関係にあることです。配列を利用することで、より強力なプログラムを作ることができます。
配列に入る前に、もう一つ、基本事項の紹介です。
ポインタに対して加算を行うとどうなるでしょうか?例を見てみましょう。ポインタ変数barに1を足す前と後の値を表示するプログラムです。
int main(){
int foo;
int *bar = &foo;
printf("%p\n",bar);
bar++; /*bar = bar+1と同義*/
printf("%p\n",bar);
}
実行すると、
00123abc 00123ac0
ポインタの値は4増えています。これは、ポインタへの加算が、元となる型のサイズを基準としているからだと考えてください。int型はメモリの4バイト分の領域を使用するので、int型に対するポインタに対して整数を足すと、その4倍の数値が加算されるのです。
配列とは、メモリ中の連続した領域に配置される変数の群のことです。
int foo[5];
という宣言の場合、メモリ中で配列fooは下図のようになっています。
図に記してありますが、配列の添え字を取ったもの(ここでは「foo」)は、配列の先頭を指すポインタとなっています。
2次元以上の配列は後ほど。
さて、もうピンと来た人もいるかもしれませんが、これは、配列の先頭のアドレスが解れば、ポインタに加算を施すことで配列の各要素にアクセスできるということです。例えば、
int foo[5];
と宣言されているときには、
となります(下図も参照)。添え字と足す数が同じなのがうれしいところ。
また、
int foo[5];
int *bar = foo; /*配列の先頭アドレスをbarに*/
このようになっているとき、
foo[n] = *(foo+n) = *(bar+n)= bar[n]
が成り立ちます。ポインタは、配列名として扱うことができる。ということです。ポインタによるアクセスより、配列としてアクセスした方がわかりやすいですよね。
※2次元以上の配列になると、少し勝手が違ってきます。
これで、関数をまたいで配列を使用することができるようになります。例を見てみましょう。
#define ARRAY_LENGTH 3 void add_arr(int *arr,int arrlen){ /*arrlenは配列の長さ*/ for(int i=0;i<arrlen;i++){ arr[i] += 1; /*ポインタを配列として使用*/ } } int main(){ int array[ARRAY_LENGTH]={ 1,2,3, }; printf("add_arrの前\n"); for(int i=0;i<ARRAY_LENGTH;i++){ /*arrayの中身を全て表示*/ printf("array[%d] = %d\n",i,array[i]); } add_arr(array,ARRAY_LENGTH); /*配列arrayの先頭ポインタを渡す*/ printf("add_arrの後\n"); for(int i=0;i<ARRAY_LENGTH;i++){ /*arrayの中身を全て表示*/ printf("array[%d] = %d\n",i,array[i]); } return 0; }
実行結果は以下の通り。
add_arrの前 array[0] = 1 array[1] = 2 array[2] = 3 add_arrの後 array[0] = 2 array[1] = 3 array[2] = 4
ここでは先ず「メモリの動的割り当て」と呼ばれる技術を説明して、関数や配列を使った例を紹介します。
変数や配列を宣言してメモリの領域を確保するのは、言わば静的なメモリ割り当てです。ここではプログラム中で好きなときに、好きな分だけ(上限はありますが…)メモリを確保できる方法について説明します。
mallocという関数について説明します。この関数は、必要なメモリ領域を自動で見つけて確保し、その先頭アドレスを返す関数です。
#include <stdio.h> #include <stdlib.h> int main(){ int *foo; foo = (int *)malloc(sizeof(int)) ........ printf("%d",*foo); ........ free(foo); ........ return 0; }
malloc関数を使うにはstdlib.hのインクルードが必要です。
以下、太字部分について説明していきます。
foo = ...
として、返り値(確保した領域の先頭アドレス)をポインタに代入してやります。
(int *)malloc...
C++コンパイラを用いる場合、malloc関数は返す型をこちらで決めてやらなければいけません(これをキャストと言います)。「(int *)」は、そのために記述しており、今回の場合「intに対するポインタを返す」という意味です。
Cコンパイラを使う場合には、キャストは行わない方が良いようそうです(cf:malloc -Wikipedia)が、開発環境が変わって、C++コンパイラを使うことになった場合はエラーが出るようなので注意です。
malloc(sizeof(int))
sizeof関数は、その型に必要なメモリの容量を返します。mallocの引数は、確保したいメモリサイズであり、この場合「int型の大きさ分の領域」を確保します。
free(foo);
free関数はmalloc関数などで確保した領域を開放する関数で、引数はポインタです。通常、プログラム終了時に自動的に開放されますが、異常終了やメモリの容量なども考えて、使わない領域は開放してやるといいでしょう。
※通常の変数などにfreeを使ってはいけません。エラーが発生します。
さて、これでポインタfooはint型変数を格納できる領域を指すようになりました。つまり、先ほどまでと同じようにfooを使って値を読み書きできるということです。
メモリの確保は時々失敗し、そのときmalloc関数はNULL(無効)を返します。そして、このままプログラムを実行させ続けると多くの場合エラーが起きてしまいます。このエラーはパソコン自体に悪影響を及ぼしかねないので、普通はチェックを行って、強制終了させます。
int *foo; foo = (int *)malloc(sizeof(int)) if(!foo){ printf("エラー:メモリの割り当てに失敗しました。"); exit(-1); }
fooがNULL(=0)のときに強制終了させています。exit関数は、プログラムを終了させる関数です。異常終了の場合、引数に1以外の値を指定します。(私は-1が好きなだけです)
皆さんはユーザーに配列の長さを入力してもらうために下のようなコードを書いてエラーを起こしたことはありませんか?
int len; //配列の長さ
printf("配列の長さを入力してください:");
scanf("%d",&len);
int arr[len];
...
配列の宣言をするときにその長さを変数にしています。長さは静的でないといけないので、エラーになってしまう訳ですね。
ですが、このままでは配列の大きさをユーザーが決定することは出来ません。ではどうすればよいのかと言うと、メモリの動的確保を応用するわけです。
上のコードを少し書き換えます。(太字部分が変更部分です)
int len; //配列の長さ int *arr; //配列の先頭を指すポインタ printf("配列の長さを入力してください:"); scanf("%d",&len); arr = (int *)malloc((sizeof(int)) * len) ...
これで、長さがint型の変数「len」個分の連続領域つまりサイズがlenの配列を確保することができました。arrはそれらの領域の先頭ポインタですから、前項と同じように配列arrとして扱うことができるわけです。
では、ちょっとしたプログラムを組んでみましょう。全く実用的なプログラムではありませんが、仕様は、
としましょう。以下に実行結果の例を示します。
nの値を入力してください(半角自然数):10 stepの値を入力してください(半角整数):2 0,2,4,6,8,10,12,14,16,18 合計:90
別ページで詳しく解答をしています。解らなければ見てみてください。
ここからの内容は難解な部分が多いので、ザックリ説明していきます。余裕のある方だけどうぞ。
配列が多次元になると、扱いが一気に変わってきますので、補足しておきます。なお、3次元以上は容易に考えが拡張できるので、簡単のため2次元配列のみを扱います。(数学の教科書風に逃
いきなりですが、図を見てもらいましょう。
int foo[3][2];
と宣言されているとすると、メモリ中では
となっています。一応、このように規則正しく並んでいますが、ポインタでアクセスしようとすると、なかなか上手く行きません。
上の2次元配列は、foo[0]、foo[1]、foo[2]という長さ2の1次元配列3つからできています。そして、「foo」は配列foo(1次元ポインタ配列)の先頭つまり、&foo[0]を、またfoo+1は&foo[1]を…指しています。因みに、foo[0]は&foo[0][0]を、foo[0]+1は&foo[0][1]を…指しています。つまり、
※この図は厳密には正しくありません。興味のある人は&foo,&foo[0],&foo[0][0]辺りを調べてみるといいと思います。
このようなイメージなので、ポインタを使うと…
などという、非常にわかりにくいものとなってしまいます。
2次元配列は、マップ(表)として使われることが多く、関数を介して受け渡しできると非常に便利です。しかし、1次元のときと違い、単にポインタを渡すだけでは上手くいかないことが多いのです。例えば次のような場合には上手く動きません。
void hoge(int *foo){ /* 添え字の合計を代入。例:foo[1][2]=(1+2=)3 */
int i=0,j=0;
for (i=0;i<3;i++){
for(j=0;j<4;j++){
foo[i][j] = i+j;
}
}
}
int main(){
int foo[3][4];
hoge(foo);
...
return 0;
}
原因は、「hogeから見るとfooがどんな構造の配列なのかわからないから」です。見ると、確かにhogeにおけるfooは、単なるint型のポインタでしかありません。
これを解決するにはいくつか手段があります。
1つは、「この配列がどこで区切れるか」をhogeに教えることです。hogeの宣言を以下のようにします。
void hoge(int foo[][4])
前の括弧にも数値を入れて構いませんが、こちらの方が柔軟性があります。これで、fooが、「●●×4の配列」だとhogeに伝わるので、正しく動くようになります。
さて、次の方法です。今度は、引数として配列のサイズをhogeに与えてやります。
void hoge(int *foo,int m,int n){ /*配列はm×n*/ int i=0,j=0; for (i=0;i<m;i++){ for(j=0;j<n;j++){ *(foo + n*i + j) = i+j; } } } int main(){ int foo[3][4]; hoge(&foo[0][0],3,4); /*&foo[0][0]は「foo」でも大丈夫。*/ ... return 0; }
アクセスにはポインタを用いています。hogeからでは、fooはただのポインタにしか見えないので1次元配列のときと同じようにようにアクセスできるのです。この方法だと、配列のサイズが動的な場合にも対処できます。
しかしこの場合、"配列名[添え字][添え字]"とすることができないため、ソースが大変見づらくなってしまいます。
そこで、次のような方法を考えてみます。
void hoge(int **foo,int m,int n){ /*配列はm×n*/ int i=0,j=0; for (i=0;i<n;i++){ for(j=0;j<m;j++){ foo[i][j] = i+j; } } } int main(){ int *foo[3]; /*ポインタ配列の宣言*/ int i; for(i=0;i<3;i++){ /*fooの各要素に配列の先頭アドレスを代入。*/ if(!(foo[i] = (int *)malloc(sizeof(int)*4))){ printf("error:割り当てに失敗しました"); exit(-1); } } hoge(foo,3,4); ... return 0; }
長さ3のポインタ配列fooを確保。さらに長さ4のint型配列の先頭アドレスをfooの各要素に代入して、2次元配列を実装しています
強調してある部分ですが、fooは、ポインタのポインタと呼ばれるものです。fooが「ポインタ変数のアドレス」を指す場合に使います。先の図を見てもわかりますね。main関数のfooも、明示はされていませんが同様です。
因みにこの場合、通常の2次元配列のように全ての要素が連続した領域に配置されるわけではありません。例えばm×n配列であれば、長さnの配列をm個ばらばらに確保します。
C言語では文字列は配列として実装されています。が、実は文字列を保持する変数には2種類あるのです
一つは、文字列を、ごく普通に配列として確保する方法です。なじみが深いのはこちらの方法ではないでしょうか?
char str[] = "hoge";
宣言は上のような形です。
しかし、実はメモリでは意外な操作が行われています。下の図を見てみましょう。
プログラム中で文字列は一旦文字列専用の領域に書き込まれます。文字配列は、文字列を専用の領域からコピーすることで実装されているのです。
さて、こちらは先ほどとは少し様子が違って、文字列の先頭へのポインタを得ることで文字列を保持します。文字ポインタの宣言は次のようにします。
char *str = "hoge";
こちらも図を見てみましょう。
こちらは、文字列をコピーすることはなく、専用の領域にあるその文字列の先頭のポインタを直接得ています。
これら二つの方法は、普段はそこまで大きく違わないのですが、意識しておかないとプログラムが正常に動作してくれない場合があります。
特にソートなど、文字列をさらに配列にするときは特に注意しましょう。
隠蔽されてしまったポインタ達の事、時々d(ry