プログラミングおよび演習 NO.10
Last-Modified: 2014.11.03


ポインタ

今日は、ポインタというC言語の中で比較的むずかしい事柄について勉強します。と言っても、以下の説明を良く読んで貰えば、そんなに難しいことではありません。ポインタを理解するためには、コンピュータがどのようなメモリ操作をして、プログラムを実行しているかということを考える必要があります。ポインタ操作は、自由自在にプログラムを作る(コンピュータが実際に行っていることに即した処理を行う)ために重要です。是非とも頑張って身につけてください。

ポインタを説明する前に、コンピュータが変数のデータをメモリに読み書きするしくみを頭にいれる必要があります。

コンピュータは、変数のデータを読み書きするためのメモリを用意します。ここで、複数ある変数の一つ一つを区別するために、メモリに番号を付ける必要があります。その番号を、アドレス(番地)といいます。
例えば、変数名aを

 int a;

のように型宣言すると、コンピュータは変数aのためのメモリを用意します。すなわち、適当なアドレス(番地)のメモリを変数aのために確保します(例えば、変数aのメモリとして100番地を割り当てる)。このとき、

 a=50;

のように、aに50の数値を代入するプログラムを実行すると、コンピュータは、変数aのメモリに50の数を書き込みます。同様に、

 c=a+3;

のように、変数aを参照する式を実行すると、コンピュータは、変数aのメモリに書き込まれているデータを読み出します。上の例では、読み出したaのデータに3を足すという演算を行い、結果をcのメモリに書き込みます。すなわち、変数aの参照は次のようなことを意味します。

 a :変数aのメモリに書き込まれているデータ

プログラムを作る人にとって、変数aのメモリアドレスが何番地であるか、普段は気にかける必要はありません。
しかし、変数名を指定した上記のデータの読み書き(メモリ操作)に代わって、変数のアドレスを指定してメモリ操作を行ったほうが便利なことがあります。
 そのことは後で説明しますが、C言語ではアドレスを指定した変数の操作、すなわちポインタ操作を可能にするために、&と*の2つの記号を、次のように約束します。

 &a :aのメモリアドレス
 *a :aをポインタ型変数で宣言する。aをアドレスと解釈して、アドレスa番地に書き込まれているデータ(aが指している(ポイントしている)アドレスのデータ)

上記の約束のもとに、(1)普通の変数と(2)ポインタ変数を、各々次のような意味で使います。

(1)普通の変数

 int a : 整数型変数aの宣言(普通の宣言) 
 a   : 変数aの中身、aには整数型のデータ(普通の数値データ)を代入する。
 &a  : 整数型変数aのメモリアドレス

(2)ポインタ変数

 int *a : 整数型ポインタ変数aの宣言
 a   : 変数aの中身、aには整数型のアドレスデータ(ポインタデータ)を代入する。
 *a   : アドレスaが指しているメモリに書き込まれている整数型データ(a番地のメモリのデータ、aをアドレスと解釈する)



ポインタの動作を確認するためのプログラムを以下に示します。(このプログラムは実際の役にはたちません)

例 10-1
 #include<stdio.h>
    
 int main(void)
 {
    int a,*c; /* a:普通の変数、c:ポインタ型の変数 の宣言*/
    
    a=10;     /* 変数aに10を代入 */
    c=&a;     /*変数aのアドレスをポインタ変数に代入*/
    
    printf("a=%d &a=%d\n",a,&a);
    printf("c=%d *c=%d &c=%d\n",c,*c,&c);
    
    return(0);
}


補足:

例題10-1では、ポインタ型変数cに

 c=&a;

として、変数aのアドレスを書き込んでいます。
このとき、変数aとポインタ型変数cの関係は下図のようになります。
cが指しているメモリ(cのメモリの中身をアドレスとみなしたとき、そのアドレスメモリの中身)のデータ *c は、aになります。



演習問題10-1  (Revised : 2013/10/28)

例10-1のプログラムの動作を説明してください。また、このプログラムの、c=&aの後に、

 *c=100;

のような代入文を挿入した場合の結果を確かめてください。
変数aに対する代入演算が、ポインタ変数cを用いても実現できることを理解してください(a=100*c=100のメモリ操作は、まったく同じ働きをする)。


ポインタの応用(関数の引数)

はじめに、先週習ったように、2つの変数x、yの足し算をする関数sum(x,y)を使ったプログラムは例10-2のようになります。ここでは、足し算の結果x+yは、関数のreturn()の戻り値を使って、結果を受け取りました。

以下の例10-2は、先週の例題と同様に、戻り値を使った足し算の関数プログラムです。

例10-2
#include<stdio.h>

int main(void)
{
    int x=1,y=3,z=0;
    int add(int,int);

    z=add(x,y); /*xとyの足し算結果をzに入れる*/

    printf("次は関数mainの出力\n");
    printf("%d+%d=%d\n",x,y,z);

    return(0);
}

int add(int a, int b)
{
    int c;

    c=a+b;
    printf("次は関数addの出力\n");
    printf("%d+%d=%d\n",a,b,c);

    return(c);
}

ここでは、呼び出し側と関数本体とのデータの受け渡しの様子を分かりやすく説明するために、 箱(コンピュータのメモリ)を使ったモデルに置き換えてみます。ここで、メモリにデータを書き込む操作を、 データを書いた紙(変数データ)を箱の中に入れる作業におきかえて考えます。また、メモリからデータを 読み出す操作を、箱から紙を取り出して読む作業におきかえて考えます。どの箱にデータを入れたか分からないと 困りますので、箱に番号をつけます。これをまとめると、次のようになります。

  箱の名前:  変数名
  箱の番号:  メモリのアドレス
  箱の中の紙: メモリに書き込まれているデータ

上記例題の関数呼び出しにおけるデータの受け渡しを、箱モデルを用いて示したのが以下の図です。


さて、上記のように戻り値で結果を受け取る代わりに、次の例題のように3番目の引数zを使って結果が受け取ることができるかどうか試してみます?。まず、期待した結果になるかどうかプログラムを動かして確かめください。

例10-3
#include<stdio.h>

int main(void)
{
    int x=1,y=3,z=0;

    void add(int,int,int);

    add(x,y,z); /*xとyの足し算結果をzに入れる*/
    printf("次は関数mainの出力\n");
    printf("%d+%d=%d\n",x,y,z);
    
    return(0);
}

/* aとbの値を足し算結果をcに入れるプログラム*/
void add(int a, int b, int c)
{
    c=a+b;
    printf("次は関数addの出力\n");
    printf("%d+%d=%d\n",a,b,c);
}

上の例10-3の問題の、関数呼び出しにおける、 呼び出し側と関数側の変数と引数を、箱のモデルに表すと下図のようになります。

例10-3の関数呼び出し

ここで、引数の箱a,b,cは、呼び出し側の変数の箱x、y、zと、別の箱であることに注意してください。

例10-3のプログラムの処理手順を詳しくみてみると次のようになります。

①呼び出し側(main関数)

   変数xの箱には1、変数yには2、変数zには0が、入っている。   
   関数add()の引数aの箱に、変数xの箱の中身をコピーして入れる。
       〃  引数bの箱に、変数yの箱の中身をコピーして入れる。
       〃  引数cの箱に、変数zの箱の中身をコピーして入れる。
   関数add()を呼びだす

②関数側(add関数)

   引数a,bの各々の箱の中身を取り出して、両者を足す。
   その結果を、引数cの箱に入れる。
   a,b,cの箱の中身を表示する。
   呼び出し側に戻る。

③呼び出し側(main関数)

   x,y,zの箱の中身を表示する。

上記の説明で、zの箱の中身は書き換わらないことに注意してください。要するに、呼び出し側では、関数の引数にデータをコピーして渡しているだけです(値渡し)。 引数のデータを呼び出し側で受け取ることは出来きないのです。すなわち、C言語 では値渡しの方法で関数にデータを渡すため、データのやり取りは基本的には,次のように一方通行しかできません。

  引数: 呼び出し側-->関数
  戻り値:関数-->呼び出し側

このため、上記の場合、関数の中で 引数cに新しい値を代入しても、呼び出し側の変数zは元のままです(mainで結果を受け取ることができません)。

例10-3のプログラムを修正して、呼び出し側の引数zに足し算の結果を正しく受け取れるようにするには、以下のようにします。

呼出し側:

  void add(int,int,int*); /* *印はポインタ型変数(或いは関数)であることを意味する*/
  add(x,y,&z);       /* 3番目の引数は、変数zの番地を渡している */

関数側:

  void add(int a, int b, int *c)  /* 引数cに番地データを受け取る。そのために、cをポインタ変数で宣言している */
  {
     *c=a+b;  /* c番の番号の箱(すなわちzの箱)に、a+bの結果を代入する */
  }

ポインタ変数を引数に用いた関数呼び出し

以上のように引数に変数zのデータを直接渡すかわりに、zのメモリアドレス&zを渡します。 こうすれば、return文を用いなくても、アドレス情報を介して間接的にデータを受け取ることができます (この方法を参照渡しと言います。C言語の関数にデータを渡す方法は、基本的に値渡しを前提にしています。このため、 引数を介して、関数側のデータを貰うためには、上記のようにポインタ変数を用いる必要があります)。

演習問題 10-2  (Revised : 2011/10/31)

 前回の演習問題9-1では、return文の戻り値を使って、関数の結果を持ち帰りま した。上記の説明に従って、ポインタ型の引数を使って、値を持ち帰る格好に全ての関数を書き換えてみてください。
また、scanf文でscanf(“%e”,&a)のように 変数aの頭に&を付ける理由についてもレポートに示してください。


受け取りたい結果が複数ある場合の関数の作り方

上記の例では、一つの変数の値を受け取っただけです。この場合は、先週の例題のように戻り値を使って結果を受け取るのに比べて、都合の良いことは何もありません。しかし、受け取りたい変数の数が2つ以上ある場合は、ポインタ型の引数を使うと便利です。

次の例題は、複素数z=x+jyの実数部xと虚数部yを入力すると、絶対値|z|と位相角∠zを、計算してくれる関数henkan()を使ったプログラムです。

例 10-4(受け取りたいデータが2つある関数)
#include<stdio.h>
#include<math.h>

int main(void)
{
    void henkan(float, float,float *, float *);
    float a,b,amp,phase;
    
    printf("Input real part="); fflush(0); scanf("%e",&a); 
    printf("Input imag part="); fflush(0); scanf("%e",&b);
    
    henkan(a,b,&amp,&phase);
    printf("%e+j%e=|%e|∠%e\n",a,b, amp,phase);

    return(0);
 }

 void henkan(float x, float y, float *r, float *theta) 
 {
    *r=sqrt(x*x+y*y);
    *theta=180.0*atan2(y,x)/3.141592;
 }


演習問題 10-3 (Revised : 2014/11/03)

税抜きで一個あたり120円のハンバーグをn個、一杯あたり100円のジュースをm杯、一袋あたり250円のポテトをk袋を買った時の、税抜き価格C、消費税(8%)Tax、及び税込み価格Sを求める関数を作成して下さい。

ただし、ハンバーグ、ジュース、ポテトの3点をセットで買った場合は、各々を単品で買った場合の合計金額(税抜き価格)から、40円×セット数の値引きがあることに注意してください(セット数は、n,m,kの最小値の数とする)。

また、消費税は値引き後の総計に対して計算し、一円未満は切り捨てて下さい。

ここでは、上で示した全ての変数(n, m, k, C, Tax, S)を引数とし、n, m, k をキーボードから入力すると、合計額の税抜き価格C、消費税(8%)Tax、及び税込み価格Sを画面に表示するプログラムとしてください。

また、メイン文と関数間の値の受け渡し方法に注目して、プログラムの動作を説明してください。



プリプロセッサ

プログラムの先頭のところに、#記号のあとに続けて、以下のような構文があると、コンパイラはある定まった約束の処理を行います。その幾つかを説明します。

インクルード文

   #include "ファイル名"
または

   #include <ファイル名>

これは以前に説明しましたように、ファイル名のファイルをinclude文の位置に挿入してコンパイルすることを意味しています。

ここで、ファイル名を""で囲むと、コンパイラはカレントディレクトリから先にファイルを探します。一方、<>で囲んだときは、コンパイラのシステムディレクトリから先に捜す約束を表しています。そのため、標準関数のヘッダファイル(stdio.h やmath.h)は<>で囲む約束であると理解してください。

define文

   #define 文字列1 文字列2

上記のようなdefine文をプログラムの先頭に用意します。このとき、プログラムのどこかに、文字列1が含まれていると、それを文字列2で置き換えて、コンパイルします。

例えば、プログラムのなかで円周率πを用いる計算はよくあります。そのために、

   #define PI 3.14159265

とします。上記のdefine文を用意した場合、本文中で

   sin(30.0/180.0*3.141593) /* sin(30°)の計算 */

と書く代わりに、

   sin(30.0/180.0*PI) 

のように、PIを記号のように使ってプログラムを書くことができます。

マクロ文
define文の手の込んだ使い方として、マクロ文があります。マクロ文は関数と良く似た目的のために使うことができます。
マクロ文は、

   #define マクロ名(x,y‥‥)  マクロ定義文

のように定義します。このとき、プログラム本体の中で、

   マクロ名(a,b‥‥)

のように書いてマクロ文を使います。マクロ名が表れるとコンパイラは、通常のdefine文と同じように、まずマクロ名を定義式の文字列に置き換えます。その際、マクロ文の引数に対しては、さらに、 定義文の中の引数文字列を、呼び出し側の引数文字列に置き換えます。上記の例では、、コンパイラは、マクロ定義文の引数文字列 "x""y"の各々を、プログラム中のマクロ文の引数文字列、 "a","b"に置き換えます。

その結果、あたかもマクロ名()の名前の関数を用意したのと同じ働きをさせることができます。

例をあげて説明します。

#define PI 3.141593
#define VOLUME(r) 4.0*PI*r*r*r/3.0   /*半径rの球の体積を求めるマクロ*/

のようにdefine文を用意します。そして、本文の中で、

  VOLUME(2.0) ;

のようにマクロ文を呼び出したとします。このように、PIやVOLUMEという文字列がプログラム中に表れると、コンパイラは、各々をdefine定義文の文字列3.141593および4.0*PI*r*r*r/3.0で置き換えます。その際、VOLUMEはマクロ文であるので(引数(カッコの中の文字列)があるので)、定義文中の引数文字列 "r"を、呼び出し側の引数文字列 "2.0"に置き換えます。すなわち、VOLUME(2.0) を、

  4.0*3.141593*2.0*2.0*2.0/3.0;

の文字列に置き換えます。

 以上のように、プログラムを作る人にとって、見かけ上、VOLUMEという名前の関数を呼び出したかのように使うことができました(ただし、マクロ文は、文字列の置換処理によって実現している点で、関数呼び出し(サブルーチンの実行)とは本質的に異なることに注意してください)。


演習問題 10-4(マクロ文の練習) (Revised : 2011/11/3)

(1) 以下は、マクロ文COMPUTE(a,b) を用いた a*10+b の計算プログラムです。
プログラムを実行し、キーボードからa,bに適当な数値を入力した際の計算結果を確認してください。
次に、2*(a*10+b) を計算する目的で、

  ans=2*COMPUTE(a,b);

のように変更してみてください。この場合、期待通りの答えになりません。
その理由を考察し、正しい答えが出るように、マクロ文を訂正してください。
#include<stdio.h>

#define COMPUTE(a,b) a*10+b  /* aの10倍にbを加算した結果を返す */

int main(void)
{

    float a,b,ans;
    printf("input a="); fflush(0); scanf("%e",&a);
    printf("input b="); fflush(0); scanf("%e",&b);
    ans=COMPUTE(a,b);
    printf("\nCOMPUTE(%5.3f,%5.3f)=%f\n",a,b, ans);

    return(0);
}
(2) 次のプログラムは、球の半径を指定して、球の体積を求めるためのプログラムです。xxxの部分には球の半径を指定します。xxxを適当な数値に書きかえた上で、プログラムを実行してみてください。
xxxのところを、数値でなく、例えば 3+2 のような、足し算を含んだ式にすると、正しい答えがでてきません。
その理由を考察し、正しい答えが出るように、マクロ文を訂正してください。
#include<stdio.h>

#define PI 3.14159
#define VOLUME(r) 4.0/3.0*PI*r*r*r /*半径rの球の体積 */

int main(void)
{
    printf("\n半径%fの球の体積=%f\n",xxx,VOLUME(xxx));

    return(0);
}
(3) 正十二面体の体積Vを指定して、一辺の長さaを求めるマクロを作成して下さい。
なお、Vとaの関係は以下の図の下に示しました。



(4)関数を使っても同じプログラムが作れます。マクロと関数は、どんな違いがありますか?
 各々のメリット、デメリットについて考察してレポートに書いてください。