ここではC言語のポインタの基礎を,デバッガを用いながら学習します. 使用するサンプルは以下のような簡単なものです.サンプルのプロジェクトは,pointerサブフォルダに配置してありますので,pointer.sln ファイルをダブルクリックしてプロジェクトを開きます.
プロジェクトを開いたら,ビルドをして実行ファイルを作成してください.
debugger フォルダ内に演習で使用するPDFファイル(debugger2024.pdf)も置いてありますので開いておいてください.
※注意) コピーしたプロジェクトを Visual C++ から開く際には,必ず Z ドライブからのパスを指定すること.マイドキュメントからのパスでは,デバッガが正しく動作しない.
#include/* arrayの各要素に iの階乗を代入 */ int setFactorial(int array[], int n) { if (n == 0) { return 1; } else { return array[n-1] = n * setFactorial(array, n - 1); } } /* 配列の n 個分の int を表示 */ void printArray(int* p, int n) { while (n--) { printf("%d ", *p++); } puts(""); } #define NUM 10 int main() { int* p; char* pc; double* pd; int array[NUM]; char c = '\0'; double d = 0.0; int x, y; x = 10; y = 20; /* 変数 x y のアドレスを表示 */ printf("&x = %p, &y = %p\n", &x, &y); p = &x; pc = &c; pd = &d; *p = 0; // <--- ここにブレークポイントを置く setFactorial(array, NUM); printArray(array, NUM); //以下、文字列演習用 char msg1[] = "hello"; char* pmsg = msg1; //ここにブレークポイントを設定 print(msg1); //配列先頭アドレスの引き渡し print(pmsg); //ポインタを媒介したアドレスの引き渡し print("konnichiwa"); //文字列リテラルの引き渡し print(NULL); return 0; }
プログラムをデバッガ上で動作させたときに,プログラムの動作を一時的に停止させる部分をブレークポイントと呼びます.
ブレークポイントの設定は,エディタ左側の灰色の帯の部分をクリックすることで行います.
ここにブレークポイントを置く のコメント行にブレークポイントを設定します.ブレークポイントが設定された行には,●印が行の左端に表示されます.
ブレークポイントを設定したプログラムをデバッガ上で実行するには,[デバッグ]メニューから[デバッグの開始] を選択します.(F5 キーがショートカットとなっているので,覚えておくと便利です.)
プログラムが開始され,ブレークポイントで一時停止すと,一時停止した位置が黄色い矢印で示されます.
プログラムのトレース
デバッグが開始されると、以下の[デバッグ]ツールバーが表示されます.
プログラムが停止した状態からは、[デバッグ]ツールバーのステップ実行ボタンで一行ずつ実行を進めることができ ます。以下にデバッグツールバーと、 各ボタンの動作を示します.
ステップ実行 (ステップイン)[F11] 実行を一行すすめる。関数呼出の場合には、関数内部に入る。
関数単位で実行(ステップオーバー)[F10] 実行を関数単位で実行する。
- 関数から抜ける (ステップアウト)(Shift+F11) 現在実行している関数から抜ける。
ブレークポイントを任意の位置に設定し,デバッグ実行をします.デバッグツールバーの動作を確認すること.
変数ウインドウでは,デバッガ上でプログラムが停止している状態で,変数の値を確認することができます.
変数ウインドウの[ローカル]タブでは,実行している行のスコープ内にある変数の値を確認することができます.変更のあった変数の値は赤色で表示されます.
また,ウォッチウインドウでは,シンボル名フィールドに任意の変数や式を入力して,その値がどのように評価されるかを調べることができます.
[呼び出し履歴]ウインドウでは、関数の呼び出し順を参照することができます。 デバッグを、setFactorial 関数内に進め、[呼び出し履歴]の表示の様子を観察すること。
[デバッグ]メニューの[デバッグの停止]をクリックすると,デバッグを終了します.
プロジェクトをコンパイルし,示された位置にブレークポインタを設定後にデバッガを起動する .ブレークポイントで停止すると[変数]ウインドウの[ローカル]タブの表示は以下のようになっています.
変数は,プログラミング言語レベルで見ると, x, y のようなシンボルで,これらの記号を用いて書き込みや読み出しを行います.これら変数の値が書き込まれる場所は,実際にはコンパイラが割り当てた(あるいは実行時に割り当てられるようにした) 特定のメモリ領域です.
C言語処理系において,それぞれの変数が何バイトを占めるかはその変数の型によって異なります.変数の占める大きさ(バイト数)はsizeof 演算子によって知ることができます.sizeof 演算子は,sizeof (型名) の書式で使われます.その型の変数を格納するのに必要なメモリ量(変数のサイズ)のバイト数がコンパイル時に評価されます.
また,sizeof (変数名) の書式で,その変数のサイズを知ることが出来ます..
たとえば,以下のように printf 関数とともに使用すると,int 型のサイズを表示することができます.
printf("%d\n", sizeof(int));
今回の演習では,プログラムを実行してこれらの値を表示させるかわりに,デバッガのウォッチウインドウの機能を用いて,それぞれの値がどのように評価されるかを確かめます.
ウォッチウインドウに sizeof(int),sizeof(char), sizeof(x),sizeof(array) などと入力し,その値がいくつになるか確認してみよう.
構造体のサイズも,sizeof 演算子を用いて取得することができます.このため,メモリ領域を動的に(プログラム実行時に)確保するプログラムで頻繁に使用されます.(動的メモリ管理については,演習II 課題2 で学習する.)
変数は,メモリ上に割り当てられた領域であるため,そのアドレス(メモリ番地)を持ちます.
変数のアドレスを得るにはアドレス演算子(&) を使用します. &x と記述すると,それは変数 x のアドレスを表します.デバッガのウオッチウインドウに &x, &y を入力して,その値がどのようになっているかを確かめなさい.
また,sizeof 演算子を用いてポインタ変数 p のサイズを調べ,なぜそのような値になるのか考察しなさい.
( ローカル変数の格納には,スタックと呼ばれる領域が仕様されている.C言語のメモリ領域については 演習I 課題2の資料 を参照すること.ヒープと呼ばれる領域の扱いについては,演習II 課題2で行う.)
int型変数には,1,2,3・・のような整数を格納します.float型には実数を格納します.このようにC言語では,変数に書き込む値の「型」が厳密に決められています.
ポインタとは,これら変数のアドレスを格納するための変数です.ポインタ変数 p に int 型変数 i のアドレスが格納されているときに,「p は i を指している」という言い方をします.
int 型の変数を指すポインタは int *p; のように宣言します.
プログラムでは p = &x; の行で,p には x のアドレスを代入しています.
変数ウインドウで,p の値が x のアドレスに等しいことを確かめなさい.
ポインタに格納されているアドレスの変数の値を参照するには,間接参照演算子(*)を用い ます.
ウオッチウインドウに *p と入力し,その値がいくつになっているかを確認しなさい.次に変数ウインドウで,変数 x の値を変更し,*p の値がどのようになるか確認しなさい.
ポインタは変数なのでさまざまな値を代入することができます.プログラム上で p = &y という文を実行したら *p の値はどのようになるかをデバッガ上で確認してみましょう.
変数ウインドウに表示されている p の値に &y と入力する.確定すると p の値は y のアドレスになっていることを確認.
この時に *p の値はどのようになるか?また,この状態で変数 y の値を100に変更したときに *p の値がどのようになるか?
2.4 間接参照による変数の書き換え
ポインタによる間接参照から,その参照先の値を変更することも可能です.プログラム中で *p = 1000; を実行した時のことをデバッガで確認してみましょう.
まず p の値を確認し,どの変数のアドレスが格納されているか確認する.次に ウォッチウインドウで *p の値を変更してみる. p の値を &x や &y に変更しながら *p の値を変更してみよう.
このようにポインタを通じてポインタが指している先の変数を変更することができます.C言語はポインタが指している先が有効な変数であるか否かは全くチェックしない のでポインタの扱いを誤るとプログラムを 誤動作させてしまうことになります.これをデバッガ上で確認してみましょう.
p の値に 0 を入力する.このとき,*p の値はどのようになるか?また,この状態で実行ボタンを押してプログラムの *p = 0 の行を実行してみよう.どのようになるか?
0 というアドレスは有効な変数が格納されている領域ではなく,そのメモリの値を読み取ることもできません.ポインタがこのような領域を指している状態で *p = 0 の行を実行すると,OSがそれを検知して例外を発生させ,プログラムが強制終了されます.
先の例ではポインタにメモリ保護違反が生じるようなアドレスを設定してみたので,プログラムの動作が正常でないことがすぐにわかる例ですが, 他にも自分の意図しない変数を書き換えてしまって,なんとなく動いているけど計算結果がおかしいというような気づきにくく,また原因もつかみにくいバグが発生することもあります.
配列は添え字を使ってアクセスできる変数の集まりです.C言語では,配列には連続したメモリが割り当てられ ます .変数ウインドウで array[10] と宣言したときの array というシンボルがどのように評価されているか見てみましょう.
ウォッチウインドウに以下のシンボルを入力して,どのような値になっているか確認する.
- &array[0]
- array
- &array
上記はどれも同じ値に評価されていることが確認できます .どれも配列の0番目のアドレス(配列の先頭アドレス)です.
では,配列のほかの要素のアドレスも確認してみて,それらがどのように配置されているかを確認しましょう.
ウォッチウインドウに以下のように入力して,配列の各要素のアドレスを確認する.
- &array[1]
- &array[2]
- &array[3]
アドレスはいくつおきに配置されているか確認する.
この配列のアドレスをポインタに代入してみましょう.プログラムでは p = array; を実行します.これと同じ動作をデバッガ上で確認します.
ポインタ p の値に array と入力する.p には配列の先頭アドレスが入る.この時に以下のシンボルがどのように評価されるかをウォッチウインドウに入力し確認する.
- *p
- p[3]
- p[9]
このように,ポインタとして宣言した変数に添え字を使って配列のようにアクセスできることができます..では,ポインタと配列とは同じなので しょうか?以下のことを試してみましょう.
ウォッチウインドウから array の値を &x に変更してみなさい.(プログラムで array = &x; と書いた場合の操作となる.)
array の値の編集は出来なかったと思います.p はその値を格納する場所を持つ変数ですが,array の値はコンパイル時に決定する定数なのです.( 10 = 3; のように定数に代入することができないのと同様に,array に値を代入することはできないのです.)
ウオッチウインドウで以下の式がどのように評価されるか調べなさい.
- x + 1
- array + 1
- array + 2
- p + 1
- p + 2
期待された結果となったでしょうか? x は int型であるために,x + 1 は x の値に1を加えたものとなります.これに対し int 型へのポインタである p に1を加えると,アドレスが4加算されていることに気がつきます.4というのは ,この処理系上で int 型が4バイトを占めていることによります.ポインタに1を加えると,配列の一つとなりの要素を指すようになります.
p + 1, array + 1 などの値を,&array[1] と比較してみよ.
変数の宣言を以下のように char 型に変更してコンパイルし直し,デバッガで同様の操作を行い,挙動がどのように変化するかを調べよ.また,なぜそうなるかを考察せよ.
char x,y;
char* p;
char array[10];
array[3] と示した時に参照される部分はどこ でしょう? array という配列の※4番目の要素ですね .(※C言語の配列の添え字はゼロから始まるので)
これをポインタの加算とそのアドレスへの間接参照でも表現してみましょう.
3番目の要素のアドレス -> array + 3
上記アドレスを間接参照-> *(array + 3)このように, array[3] と *(array + 3) は同じ意味となります.
C言語では,A[B] という構文は *(A + B) と解釈されコンパイルされています.
また,C言語は配列のアクセスにおける境界チェックを全く行いません .それゆえに, int array[10] と宣言した array に対して 10以上の添字でアクセスしいように注意してプログラムを書く必要があります.
ウォッチウインドウで array[12] の値がどのようになっているか調べよ.また,その値を変更したときに,なにが生じるかを調べよ.
メモリウインドウを用いて,array の格納されているメモリ領域の様子を確認せよ.
2.8 配列の関数へのひきわたし
サンプルコードの,setFactorial 関数,および printArray 関数の引数を比較してみてください.一方は配列のように,一方はポインタを受け取るように記述されていますが,どちらの関数も配列の各要素について代入や印刷などの処理を行っています.
結論を書きますと,C言語では関数の引数宣言に int a[] と書いても int* p と書いても同一の意味となります.
setFactoral 関数の第1引数を int *array に.printArray 関数の第1引数を int p[] に書き換えてビルド->実行して動作を確認する.
printArray 関数の第1引数を,int p[3] のように要素数を指定して,動作を確認せよ.
配列形式で宣言した関数内にブレイクポイントを置き,変数の書き換えが可能か試してみよ.
2.9 32ビットアプリケーションと64ビットアプリケーション
現在の Windows は64ビットのオペレーティングシステムであり、32ビットオペレーティングシステムよりも多くの物理メモリを扱えるようになっています。
64ビット Windows オペレーティングシステム上では、32ビットアプリケーションも64ビットアプリケーションも動作させることができます。アプリケーションを作成する際には、自分がターゲットとしているアプリケーションが32ビットアプリケーションなのか64ビットアプリケーションなのかを、しっかりと把握しておく必要があります。
Visual Studio では、 同一のソースコードからビルドの種類を変更することで、生成するアプリケーションの種類を変更することができます。
x86 が 32ビットアプリケーション、x64 が 64ビットアプリケーション用のビルドです。
pointer アプリは32ビットアプリケーションとなっています。
ビルドの種類を x64 に変更して再度ビルドする。デバッガ―を起動し、以下の型 の変数のサイズを調べなさい。
int, int*, flaot, float*, double, double*, char , char*
脚注1
int 型のポインタを一行で宣言するには以下のように書く.
int *p, *q;
これを int *p, q; のように書くと,int型のポインタ p と,int 型の q を宣言したことになるので注意.メモリ保護
Windows NT では,OSはプロセスごとのメモリの領域(セクション)に読み取り,書き込み,の属性を設定している .このために読み取り属性の無い領域をリードしたり,書き込み属性の無い領域にライトしようとすると例外が生じる.例外
プログラムの実行が正常でないことをOSが検知したときに発生させられる.通常例外が発生するとプログラムは強制終了させられる .例外にはメモリ保護違反や,0による除算などがある.ちょっとびっくり
9[array] == array[9]
"Hello World"[6] == 6["Hello World"]== 'W'