1回目B

C言語の復習


復習1:ポインタ変数


今回はC言語のもっともC言語らしいところである,ポインタ変数について復習しよう.


参考ページ:情報処理1・情報処理演習1,Lecture9 ポインタ

ポインタの前に,前提知識として アドレスについて.

アドレスとは?

プログラム中の変数や配列などの値は,コンピュータのメインメモリ(Random Access Memory,RAM,
ラムと読む)上に記憶されている.
このメモリには,場所を表す「アドレス」という連続した通し番号(整数の値)がついており,変数ごとの
アドレスがOS(Operating System, ここではWindows)により管理されている.

たとえば,C言語プログラム中で int a; と整数の変数を1つ定義すると,整数の値1個を格納する
場所が確保され,a という名前を使ってこの場所に値を書き込んだり参照したりすることができる.
下の図ではアドレスは int型の変数 a が,4 バイト分のメモリを割り当てられていることを示す.
機械語ではこのアドレスを直接用いてプログラミングするが、C言語では変数名aによりプログラミング
できる。

では,ある変数に割り当てられた場所(メモリのアドレス,上の図では0x0000E008)を調べるには,
どうしたらよいか.また,アドレスを知ることができるという事にどのような利点があるのか.

C言語では,変数の名前の直前に「&」を付けると,変数のアドレスを取り出すことができる.

#include <stdio.h>

void main()
{
 int a = 10;

 printf("aの 値  は,%d です¥n", a); /* 普通に値を表示 */
 printf("aのアドレスは,%p です¥n", &a); /* %p はアドレスを16進数8桁で表示する */
}

この逆,ある「アドレス」に格納されている変数の「値」を調べるには,どうしたらよいだろうか.


ポインタ変数

変数の置かれているメモリの位置(アドレス)を格納するための変数のことを,ポインタ変数と呼ぶ.

「アドレス」自身はもちろん数値で,演習で用いているPC(32bitCPUと,32bitOSであるWindows XP)
では,0 から 232 までの値である.16進数で表すと,0x00000000から,0xFFFFFFFFまでである

参考:1 bit(ビット)= 0 か 1 の値をとる最小単位で,電気信号では電圧の有無に対応.
1 byte(バイト)= 8 bit,28 であるから,0から 255 までの値.
int型は 4 byteのメモリを占有.char型は 1 byte, float,double型はそれぞれ 4 byte, 8 byte.

「ポインタ変数」と「(intなど)普通の(整数型)変数」は,数値を格納する点では同じであるが,
その数値の解釈に差異がある


ポインタ変数の基本的な書式

int a=10, b;

これは普通の整数型変数a,bの宣言.

int *p;

ポインタ変数pの宣言.ポインタの指す型を指定し,変数名の前に(アスタリスク)をつける.

p = &a;

アドレスを格納するポインタ変数pに,int型変数aのアドレスを代入.(数値の代入ではない!)
aのアドレスを取り出すには,変数名の前に
&をつける.

b = *p;

ポインタ変数に格納されているアドレスの示す先に格納されているを取り出すには,ポインタ
変数の前に
*をつける.ここではポインタ変数 p の指す変数(ここでは変数 a )の値を,普通の
整数型変数 b に代入.b に10が代入される.

プログラム例:

#include <stdio.h>

void main()
{
 int a; /* 普通の変数の宣言 */
 int *p; /* ポインタ変数の宣言 */

 a = 3;
 printf("aのアドレスは,%p です¥n", &a);

 p = &a;
 printf("ポインタ変数pの値(=aのアドレス)は,%p です¥n", p);
 printf("ポインタ変数pの指している値は,%d です¥n", *p);

 p++;
 printf("ポインタ変数を一つ増やすと,%p です¥n", p);
}


配列とポインタの関係(重要!)

C言語において配列は,メモリの連続した区間を使うという約束になっている.

このことから,ポインタへの整数の足し算,ポインタ同士の引き算をすることが可能である.ここでは
ポインタを使って,配列の要素を扱ってみよう.

例:

#include <stdio.h>

void main()
{
 int a[5] = {10, 20, 30, 40, 50}; /* 配列 */
 int b;
 int *p; /* ポインタ変数 */

 p = &a[0]; /* 配列の先頭のアドレスを代入する */

 printf("配列 a の先頭のアドレスは,%p です¥n", p);

 printf("はじめに:p の指している値は,%d です¥n", *p);

 p += 2; /* ポインタ変数に2を足すと,pは配列の先頭から2つ先を指す=a[2]のアドレス! */

 printf("ここでは:p の指している値は,%d です¥n", *p);

 b = *(p-1); /* ポインタの指しているアドレスのひとつ前隣の値をbに代入 */
 printf("b の中身は,%d です¥n", b);
}

このように,ポインタ変数に足し算,引き算をすることにより,配列の要素に順次アクセスできる.
ポインタに1を足したときに何バイト先に進むかはポインタの指す変数の型により異なるが,ポインタ
変数を宣言する時の型指定から自動的に計算される.


いろいろな型の変数のアドレスを調べよう.


#include <stdio.h>
 
void main()
{
    int e[3];
    double f[3];
    char g[3];
 
    printf("int配列    :%p %p %p¥n", &e[0], &e[1], &e[2]);
    printf("double配列 :%p %p %p¥n", &f[0], &f[1], &f[2]);
    printf("char配列   :%p %p %p¥n", &g[0], &g[1], &g[2]);
}

実行結果の例(アドレスの値はプログラム実行時にOSが決めるため,必ずしも値がこうなるとは限らない)

0xbffffab4 0xbffffab8 0xbffffabc
0xbffffac0 0xbffffac8 0xbffffad0
0xbffffad8 0xbffffad9 0xbffffada

この結果から,int, double, char, 配列などの各型がメモリ上でどれくらいの大きさかがわかる.


演習課題

(1)整数型配列に初期値として,(10,20,30,40,50)を設定したとき,全ての要素の値を表示するプロ
グラムをポインタを用いて作成せよ.

ヒント:


#include <stdio.h>
 
void main()
{
    int a[5] = {10, 20, 30, 40, 50};
    int *p;
    int i;
 
    p = ???;
 
    for(i=0; i<5; i++) {
        printf(" 配列の %d 番目は???です¥n", ?????? );
    }
}

復習2:関数


参考ページ:情報処理1・情報処理演習1,Lecture4 関数

変数のスコープ(有効範囲)

今までに「関数には引数として様々な値を渡すことができる」ことを学んだ.
なぜ異なる関数においては,変数の受け渡しが必要なのだろうか.
それはint型,float型,char型などで宣言される変数は, 宣言する場所によってその有効範囲が
異なるためである.

今までの演習で用いて来た変数は,main()をはじめ,ある関数の中で宣言してきた.
このように関数の中で宣言する変数のことを
ローカル変数という.
ローカル変数を利用できる範囲は,変数を宣言した関数の中,即ち { から } までのブロック内に限られる.
この変数の有効範囲のことを変数の
スコープと呼ぶ.
したがって,異なる関数内に「同じ名前」の「異なる変数」を用いる事が可能である.

実際に以下のプログラムでローカル変数間の値が変わらないことを確認してみる.

#include <stdio.h>

int func(void)
{
 int x; /*この変数は main 内の x とは別物*/
 x = 3;

 x += 5;

 printf("xの値:%d¥n",x); /* func内のxの値の表示 */

 return 0;
}

void main()
{
 int x; /* この変数は func 内の x とは別物 */
 x = 3;

 printf("xの値:%d¥n",x); /* main内のxの値の表示 */

 func();

 printf("xの値:%d¥n",x); /* func関数実行後のxの値の表示 */
}

同じ名前の変数であっても,関数が違えば影響しないことが実行結果からわかる.


値参照とアドレス参照

変数のスコープのため,関数に引数として値を渡さなければ,他の関数の値を用いた計算が出来ない.
引数として用いる変数には,前回の演習で学んだ
通常の変数と,ポインタ変数がある.
これらをうまく使い分けることで,様々な関数の作成,利用が可能となる.

値参照(Call by value)

原則:関数の呼び出しでは,呼び出し元から関数へ変数の値がコピー,複製されて渡される.
従って,普通の変数を関数に渡して,関数内で変更しても,関数を呼びだした側の
元の変数は変化しない.

これを値参照(Call by value),値渡しと呼ぶ.

呼び出し元の関数に計算結果などの数値を戻したい場合は,return 文を用いる.
このため,
戻り値の個数は 0,または 1 のいずれかであった.

簡単なプログラムで確認してみよう.


#include <stdio.h>
                
int test(int a)
{
    a += 10;  /* この関数内で値を変更 */
    return a;
}
                
void main()
{
    int a, b;
                
    printf("aの値を入力してください:");
    scanf("%d", &a);
                
    printf("関数へ渡す前の a の値は,%dです¥n", a);
                
    b = test(a);
                
    printf("関数から戻ると,a の値は %d , 戻り値 b の値は %d ¥n", a, b);
}

例えば a に 2 を入力すると,

結果はどのように表示されるだろうか.



アドレス参照(Call by reference)

値参照では,最大でも 1 つの値しか関数から返すことができなかった.
また,呼び出した関数内で値を変更した場合も,呼び出し元(mainなど)では値は変化しない.
これは,関数には変数の中身が複製され,渡されることを考えると,当然である.

では,上記の値参照では実現できない

を実現するにはどうしたらよいか.
この場合,関数に変数の格納されている「場所」を伝え, その「場所」のデータを関数内で操作する
ようにすれば,実現できそうである.

変数の格納されている場所,即ちアドレスを扱うためには, ポインタ変数を利用する.

ポインタを用いて,変数の場所を関数に渡したプログラムの例.


#include <stdio.h>
                
int test2(int *p)        /*  変数のアドレスを受け取る(ポインタ変数)   */
{
    *p += 10;             /*  p の指す中身に変更を加えるから,main関数でも変化する  */
                
    return *p;            /*  値を返す  */
}
                
void main()
{
    int a, b;
                
    printf("aの値を入力してください:");
    scanf("%d", &a);
                
    printf("関数へ渡す前の a の値は,%dです¥n", a);
                
    b = test2(&a);        /* 変数aのアドレスを関数に渡す */
                
    printf("関数から戻ると,a の値は %d , 戻り値 b の値は %d ¥n", a, b);
}

上の例と同様,a に 2 を入力すると,今度はどのようになったか.



配列の受け渡し

ポインタを用いて,関数に変数のアドレスを渡せることがわかった.
では,同じ方法を使って,関数に配列を渡してみよう.

以下は,配列を受け渡しする典型的な例である.
配列名には,配列の先頭のアドレスが格納されているので,ポインタで受け取ることができる.

配列を渡す場合は配列の先頭アドレスだけではなく,配列の要素数を同時に渡すと,どのような
要素数の配列が渡されても,関数内でうまく処理ができる.
(アドレスのみだと,受け取った関数内で配列の個数が分からない.)


#include <stdio.h>
                
void zero_init(int *p, int n)       /*  要素数 n 個,配列の先頭のアドレス p を引数とする関数  */
{
    int i;
                
    for(i = 0; i < n; i++) {
        *(p+i) = 0;     /*  p[i] = 0; という書き方も可 */
        }
                
    return;
}
                
void main()
{
    int i;
    int  ary[] = {10, 20, 30, 40, 50};
                
    zero_init(ary, 5);
                
    for(i =0; i < 5; i++){
        printf("ary[%d] = %d¥n", i, ary[i]);
    }
}

このプログラムは,配列 ary の先頭から n 個の要素の値を 0 に設定する関数で,配列の初期化など
の際に用いる.
関数には,配列の先頭要素のアドレス(
ary)が渡されるので,関数側ではポインタ(int *p)で受け取る.
この例では関数に戻り値が無いが,関数でのデータ変更は,main 関数においても反映されている.


関数のまとめ

・関数への引数の渡し方として,「値参照」と「アドレス参照」がある.

・値参照では変数の値が(複製されて)渡されるので,関数内で値を変更しても,元の関数には影響が
及ばない.戻り値により値を1つだけ戻すことができる.

・アドレス参照では変数のアドレスが(複製されて)渡されるが,関数でポインタの指す値を変更すると,
呼び出し元の値も影響される.

・どちらも関数に数値を渡すという点では,本質的に同じ.(アドレス参照ではアドレスという整数値が
コピーされ,ポインタ変数に渡される.)


演習課題

(2)下のmain関数を参考に,変数 a と b の値を,入れ替えるswap()関数を追加し,プログラムを完成
させよ.□には記号が入る???


void main()
{
    float a=2.0, b=3.0;
		
    printf("a=%f, b=%f¥n", a, b);
		
    swap(□a, □b);
		
    printf("a=%f, b=%f¥n", a, b);
}

(3)下のmain()関数を用い,キーボードから10個の値を実数型配列に入力,それらの平均値,最大
最小値を算出する関数をそれぞれ作成せよ.さらに,大きい順に並び替える(ソート)関数,配列の
中身を画面に表示する計5つの関数を作成し,動作確認せよ.


void main()
{
    float data[10];	/* 配列 */
    float max, min;
		
    input(data, 10);				/* 10個の要素を入力する関数*/
		
    printf("平均値 = %f¥n", average(data,10));	/* 平均値を算出する関数 */
		
    max_min(data, 10, &max, &min);			/* 最大値と最小値を算出する関数 */
		
    printf("最大値 = %f,最小値 = %f¥n",max,min);
		
    sort(data, 10);					/* 大きい順に並び替える関数 */
		
    output(data, 10);				/* 配列の中身を出力する関数 */
}

課題1 http://www.isc.meiji.ac.jp/~re00079/EX2.2010/2010_24.html の中の課題3
課題2 http://www.isc.meiji.ac.jp/~re00079/EX2.2010/2010_26.html の中の課題1と4

1回目のヒント


ポインタが良く分からない人のための解説