第2週 ポインタとアドレス
INDEX

本日の目標

  • ポインタとは何か知り、理解することができる。
  • ポインタを使ったプログラムを作成できる。
  • 関数の引数にポインタを使ってデータの受け渡しができる。

予習・復習

 以下のスライドを利用して、予習と復習をしよう。復習では、自分の理解度を確認するために、実際にプログラムを作成し、意図する結果が得られるか確認しよう。
  1. ポインタ

本日の講義・演習予定

  1. ポインタ
  2. ポインタ変数を引数とする関数
  3. 演習問題
  4. 提出課題
内 容
  1. ポインタ
  2. ポインタ変数を引数とする関数

ポインタ

変数とメモリアドレス

 例えば、次のようにchar型、int型、double型の変数を宣言します。この時、メインメモリには、型に応じた応じたサイズの領域が確保されます。下図のように、char型は1バイト、int型は4バイト、double型は8バイトです。
	char ch;
	int a;
	double x;
 プログラム中では各変数への値の代入や参照は宣言時に付けられた変数名を利用しますが、実際にはメモリアドレスを使ってメモリ領域への書き込み・読み出しが行われます。複数バイト領域からなるint型やdouble型の場合は、その先頭アドレスが使われます。

pointer1
Fig2-1.変数のサイズとメモリ割り当て
図中のアドレスは実行時の一例で、実際に配置されるアドレスはオペレーティングシステムなどの環境によって異なります。

アドレス演算子

 変数のメモリアドレスは、その変数名の前にアドレス演算子&を記述することで求めることができます。
[アドレス演算子] &変数名
 
関数scanf()では、アドレス演算子&を使って、入力された値を変数ではなく、変数のアドレス値を受け取るように引数が定義されていました。これは、引数では関数にその値のコピーを渡すだけで、変数の値は更新されないからです。アドレス値を渡すことで、その変数のメモリ領域への書き込みを実現していたのです。

ポインタ型変数の宣言

 ポインタ型変数はメモリアドレスを格納するための変数です。別の変数のアドレス値を格納して、そのアドレス値を使ってその変数を指し示します。ポインタ型変数を利用するためには、他の変数と同様に変数宣言をしなければなりません。ただし、ポインタ型であることを示すために変数名の前に宣言子*を付加します。
[ポインタ型変数の宣言] データ型 *ポインタ変数名
 ポインタ型変数の宣言では、ポインタ型ではない変数の宣言とデータ型の指定方法が異なります。文字ならばchar型(1Byte)、整数型ならばint型(4Byte)、浮動小数点型ならばdouble型(8Byte)、と格納するデータのタイプとサイズに合わせてデータ型を指定します。ポインタ型ならばアドレスのサイズ32bit(4Byte)に合わせてint型を指定するところですが、実際にはint型とは限りません。ポインタ型変数では、指し示す変数のデータ型と同じデータ型を記述します。例えば、char型で宣言された変数を指し示すためのポインタならば、データ型にはchar型を指定します。これはコンパイラに対してポインタが指し示す変数の型のサイズを知らせるためです。
ポインタ型変数の宣言時のデータ型は、指し示す変数の型を書く

ポインタ型の読み方
 例えば、int *aと宣言したポインタaの型は、int *型「int型のポインタ」と呼びます。

ポインタ型変数の使い方

 ポインタは他の変数のアドレスを持つことで、初めてポインタとして利用することができます。変数にアドレス演算子&を使ってそのメモリアドレスを取得して、ポインタに代入します。
ポインタ変数名 = &変数名

例えば、int型の変数aをポインタ型変数pで指し示すためには以下の例ように記述します。
	int a;
	int *p;		//ポインタの宣言
	
	a = 1000;	// 変数aの値を確定
	p = &a;		// 変数aのアドレスをポインタpに代入
	・・・

 そうすると、下図のようにポインタpは変数aを指し示すことになります。
pointer2


間接参照演算子

 間接参照演算子*を使うとポインタが指し示す変数の実体(値)を参照することができます。ポインタを使って間接参照できる実体と変数の実体が同一になることから、間接参照演算子*を付与したポインタを変数のエイリアス(別名)として利用することができます。
[間接参照] *ポインタ変数
 ポインタ型変数の宣言をするときの宣言子*とは異なります。
*ポインタ変数名は、そのポインタが指し示す変数のエイリアス(別名)

ポインタを理解するための実験

  1. [実験1] 変数のメモリアドレスを取得する(1)
  2.  次のソースコードの実行結果を予測してメモリ配置図として描きなさい。次に実際に実行結果を確認して、変数が格納されているメモリのアドレス値を記入しなさい。完成したメモリ配置図が事前の予想と同じになったか確認しなさい。もし、イメージと違っていた場合はその理由について調べて変数とアドレスの関係について納得しなさい。
    adressop1.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a,b,c;
    	
    	a = 1000;
    	b = 2000;
    	c = 3000;
    	printf("a = %d (%p)\n", a, &a);	//変数aのアドレス
    	printf("b = %d (%p)\n", b, &b);	//変数bのアドレス
    	printf("c = %d (%p)\n", c, &c);	//変数cのアドレス
    
    	return 0;
    }
    
    [メモリ配置図の例]
    exp1

  3. [実験2] 変数のメモリアドレスを取得する(2)
  4.  次のソースコードを実行すると、char型、int型、double型の各型のメモリアドレスとそのサイズを調べることができます。実行結果をメモリ配置図として描き、型ごとのメモリ配置について確認しなさい。もし、イメージと違っていた場合はその理由について調べて納得しなさい。変数のサイズの取得にはsizeof演算子を、またその書式指定の変換指定子には%luを使います。
    adressop2.c
    #include <stdio.h>
    
    int main(void)
    {
    	char c;
    	int a;
    	double x;
    
    	c = 'A';
    	a = 1000;
    	x = 0.124;
    	printf("c = %c (%p) %luByte\n",c,&c,sizeof c);	//変数cのアドレスとサイズ
    	printf("a = %d (%p) %luByte\n",a,&a,sizeof a);	//変数aのアドレスとサイズ
    	printf("x = %f (%p) %luByte\n",x,&x,sizeof x);	//変数xのアドレスとサイズ
    
    	return 0;
    }
    
    [メモリ配置図の例]
    exp2

  5. [実験3]ポインタ変数の宣言と使い方
  6.  アドレス演算子&を使って取得したアドレスを格納するための変数がポインタです。ポインタの基本的な使い方とその書き方について確認しなさい。また、実行結果から変数とポインタのメモリ配置図を描いて実行前のイメージと同じ結果が得られたか確認しなさい。もし、イメージと違っていた場合はその理由について調べて納得しなさい。
    pointer1.c
    #include <stdio.h>
    
    int main(void)
    {
        int a;
        int *p;		//ポインタ変数の宣言
    
        a = 1000;
        p = &a;	//変数aのメモリアドレスをポインタ変数に代入
    
        printf("変数aの値\t\t: %d\n", a);
        printf("変数aのアドレス\t\t: %p\n", &a);
        printf("ポインタpの値\t\t: %p\n", p);
        printf("ポインタpのアドレス\t: %p\n", &p);
        printf("ポインタpの指す値\t: %d\n", *p);
    
        return 0;
    }
    
    プログラムの説明
    • [6行目] ポインタの宣言です。このポインタはその前で宣言しているint型の変数aのアドレスを格納するために使われることから、「int型変数aへのポインタ」と呼びます。型をintにしたのは、指し示す先の変数aの型がint型だからです。
    • [9行目] 変数aのアドレスをポインタへ代入します。変数宣言の際の*が付いていませんが、ポインタに格納されているアドレス値を扱うときには*は付けません。
    • [15行目] ポインタpに付けられた記号*は、間接参照演算子と呼ばれる演算子です。間接参照演算子*を使って、ポインタが指し示す先の変数の値(この例では整数値1000)を示しています。ポインタが指し示す先の値のことを実体と呼びます。

    [メモリ配置図の例]
    exp3

  7. [実験4]ポインタを使って変数の値を変更する
  8.  次のソースコードを実行して、間接参照演算子を使ってポインタが指し示す変数の値を変更できることを確認しなさい。また、変数の値の書き換え前後のメモリ配置図を描いて、ポインタが示す変数の値が間接参照によって変更できる理由について確認しなさい。
    pointer2.c
    #include <stdio.h>
    int main(void)
    {
    	int a;
    	int *p;
    
    	a = 123;
    	p = &a;
    	printf("a = %d\n",a);
    	printf("ポインタが指し示す値 %d\n",*p);
    
    	*p = 789;
    	printf("ポインタが指し示す値 %d\n",*p);
    	printf("a = %d\n",a);
    
    	return 0;
    }
     変数aに代入された値と、ポインタpが指し示している値*pは同じ値になります。そのため、*pを変数aの「別名」もしくは「エイリアス」とも言います。

  9. [実験5]ポインタ変数の型が指し示す変数の型と同じでなければならい理由
  10.  [実験3]のプログラムpointer1.cにおいて、ポインタ変数pのデータ型を指し示す変数aの型とは異なるshort int型(サイズ2Byte)に変更した上で、コンパイル、実行して、その結果を確認しなさい。
     次に、変数aのデータ型をshort int型に変更して、その実行結果を確認しなさい。

     一つのアドレスを格納するためには必要な領域は32bit=4Byteです。ポインタ変数の型が、int *型でもshort int *型でも変わることはありません。ポインタ変数の型に指定されるintやshort intは指し示す先の変数の型で、アドレス計算を実現するためにコンパイラが必要とする情報です。アドレス計算については次回に詳しく見ていきます。

  11. [実験6]ポインタに変数の値を代入しなかった場合の結果について確認する
  12.  [実験3]のプログラムにおいて、以下のように1行をコメントした上で、コンパイル後、実行しなさい。また、その結果について考察しなさい。
    // p = &a;


NULLポインタ

 ポインタが何も指し示していない状態にするためには、ポインタに空を示す定数NULLを代入します。

ポインタ変数を引数とする関数

関数内で関数外の変数の値を変更する

 ローカル変数を別の関数に渡してその値を変更したい場合を考えます。関数の実引数にローカル変数が設定されると、関数にはその値のコピーが渡されます。これを値渡し(call by value)と言います。関数内でこの値を変更しても、コピーなので元のローカル変数の値が変わることはありません。一方、次のプログラム例のように、関数の仮引数をポインタ型とし、ローカル変数のメモリアドレスを実引数とすることで、関数の呼び出し側と呼び出される側でローカル変数を共有することができます。これを参照渡し(call by reference)と言います。関数内ではそのポインタが指し示すメモリ領域の値を変更することができ、結果として元のローカル変数の値が変更されることになります。
void twice(int *x)
{
	*x = x*2;		//2倍
}

int main(void)
{
	int a = 0;
	
	twice(&a);
	printf("a = %d", a);
	
	return 0;
}

関数内での処理結果を複数個返したい

 二つの整数を渡して値を交換する関数swap()について考えてみましょう。
次の関数swap()は交換したい変数の値を引数を使って引き渡しています。関数内では受け取った値の交換を実現していますが、関数の呼び出し側の実引数の値に影響はありません。なぜなら先の例と同様に、関数が引数で受け取った値は実引数のコピーだからです。
illegal swap function
void swap(int a, int b)
{
	int tmp;

	tmp = a;
	a = b;
	b = tmp;
}
 関数外の変数の値を変更したければ、引数をポインタ型として宣言して、関数内にその変数のポインタを引き渡すことです。
swap function
void swap(int *a, int *b)
{
	int tmp;

	tmp = *a;
	*a = *b;
	*b = tmp;
}
 引数が変数とポインタ変数の時のそれぞれのメモリ配置図を以下に示します。
pointer3

expand_lessBack to TOP

演習問題

  1. 次の変数cの値とそのアドレスを表示するプログラムを空欄を埋めて完成させなさい。
  2. ex2-1.c
    #include <stdio.h>
    
    int main(void)
    {
    	char c;
    	[[空欄ア]];
    
    	c = 'A';
    	p = [[空欄イ]];
    	printf("%c (%p)",[[空欄ウ]]);
    	return 0;
    }
    

  3. 次のプログラムの空欄を埋めて正しく実行できるようにしなさい。
  4. ex2-2.c
    #include <stdio.h>
    
    int main(void)
    {
        int a=1234567;
        int *p;
    
        p = &a;
        printf("変数aのアドレス: %p\n", [[空欄ア]]);
        printf("変数aの値: %d\n", [[空欄イ]]);
        printf("ポインタpの値: %p\n", [[空欄ウ]]);
        printf("ポインタpの指す値: %d\n", [[空欄エ]]);
    
        return 0;
    }
    

  5. 次は小数点を含む値XとYを入力して、ポインタを使って割り算X÷Yの結果を表示するプログラムです。複数の間違いがありコンパイルエラーとなります。修正して正しい結果が得られるようにしなさい。
  6. ex2-3.c
    #include <stdio.h>
    
    int main(void)
    {
        double x,y,z;
        int *p,*q;
    
        p = &x;
        q = &y;
    
        printf("X>> ");
        scanf("%lf",&p);
        printf("Y>> ");
        scanf("%lf",&q);
        
        z = *p / *q;
    	printf("%f/%f = %f\n",x,y,z);
    
        return 0;
    }
    

  7. 整数の被除数と除数を入力して商と剰余を求めるプログラムを作成しなさい。ただし、整数の被除数と除数を受け取って商と剰余を求める関数void devide(int a, int b, int *sho, int *joyo)を作成し、この関数を使って割り算の結果を表示するプログラムを作成氏なさい。

  8. 2つの整数を入力して昇順(小さい順)に並べ替えるプログラムを作成しなさい。ただし、2つの整数を受け取って昇順に並べ替える関数void sort2(int *a, int *b)を定義して利用すること。ただし、グローバル変数は使えないものとします。

OPEN ANSWER
  1. 回答:ア. char *p イ. &c ウ. c, p

  2. 回答:ア. &a イ. a ウ. p エ. *p

  3. 回答: (誤)int *p,*q; (正)double *p, *q 、(誤)scanf(%lf,&p); (正)scanf("%lf",&x); もしくは、scanf(%lf,p); 、(誤)scanf(%lf,&q); (正)scanf("%lf",&y); もしくは、scanf(%lf,q);

  4. 回答例 devide.c
  5. code/2/devide.c

  6. 回答例 sort2.c
  7. code/2/sort2.c


expand_lessBack to TOP

今週の確認テスト テスト(PDF)

本日の提出課題

 以下に示す3つの整数を入力して昇順(小さい順)に並べ替えて表示するプログラムkadai3-2.cを完成しなさい。ただし、3つのint型ポインタを受け取ってそのポインタが示す値を昇順に並べ替える関数sort3()と2つのint型ポインタを受け取ってそのポインタが示す値を交換する関数swap()を定義すること。加えて、main関数における関数sort3()の呼び出し時の引数の空欄を埋めなさい。グローバル変数は使えないものとします。

[kadai3-2.c]
#include <stdio.h>

void sort3(int *a, int *b, int *c);	//並べ替え
void swap(int *a, int *b);				//交換

int main(void)
{
	int a,b,c;

	printf("整数(a b c)> ");
	scanf("%d %d %d", &a, &b, &c);

	sort3( [ 空欄 ] );
	printf("%d, %d, %d\n",a,b,c);		//結果の表示

	return 0;
}

  • [提出方法]
    • 電子メールの添付ファイルとして提出してください。宛先は指定のアドレスです。
    • 表題は、課題2とします。
    • メール本文には、関数sort3()の引数にポインタ変数を利用している理由について記述すること。
    • 添付ファイルは下記の2点です。
    • 1) ソースコード・ファイル kadai3-2.c
      2) 実行結果画面(ウインドウのみ)のハードコピーの画像ファイル kadai3-2.jpg

  • [実行結果画面のハードコピーの取り方]
    1. [メニュー]→[アクセサリ]→Snipping Toolを起動する。ただし、Snipping Toolのウインドウが実行画面に重ならないように、必要に応じてウインドウを移動すること。
    2. タイトルバーを含めた実行結果画面をマウスドラッグして選択して、キャプチャする。
    3. メニューから「ファイル」→「名前をつけて保存」を選択する。ファイルの種類にはJPG形式を選択して、保存先フォルダを選び、ファイル名を付けて「保存」する。
    ※Windows10(creators update以降)では、Windows+Shift+S キーを押して、キャプチャしたい矩形領域をドラッグして、クリップボードにコピーします。アプリ「切り取り&スケッチ」が起動したら、画像をJPG形式で保存します。

  • [評価について]
  • プログラムの内容の評価とは別に以下の要件を満たすことを評価の前提とする。
    1. ソースコードがC言語で記述されていること。
    2. 提出されたソースコードファイルをコンパイルし、ワーニングやエラーが出力されないこと。scanfをscanf_sとすべきワーニングについては除外する。
    3. 提出されたソースコードファイルから生成される実行形式ファイルを実行した結果、所定の出力結果が得られること。
    4. 提出されたソースコードは、インデントや適当な改行が施された見やすい状態であること。
    5. 変数の利用目的、処理や判定の意味など必要と思われる解説を簡潔にコメント文として付けること。
    6. 提出された実行結果の画面コピーは、コマンドプロンプトのウインドウのみとすること。

  • [提出期限]
  • 2019年 10月2日(水)午後2時まで