第3週 ポインタと1次配列
INDEX

本日の目標

  • 一次元配列とポインタの関係を知る。
  • ポインタを使って配列を操作できる。
  • 配列を関数間でポインタを使って受け渡しができる。

予習・復習

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

本日の講義・演習予定

  1. 配列とアドレス
  2. 関数に配列を引き渡す
  3. リトルエンディアンとビッグエンディアン
  4. 演習問題
  5. 提出課題
内 容
  1. 配列とアドレス
  2. 関数に配列を引き渡す
  3. リトルエンディアンとビッグエンディアン


配列とアドレス

配列とアドレス

 配列はメモリにデータを格納する領域を連続的に確保します。例えば、int型の要素5個からなる配列
 int a[5];
では、下図のようにint型のサイズ(4バイト)×5個分=20バイト分の領域がメモリを占有します。要素a[0]の先頭アドレスが0x1000番地だとすると、以降の要素の先頭番地はa[1]が0x1004番地、a[2]が0x1008番地、a[3]が0x100C番地、a[4]が0x1010番地となります。

array1_1
Fig3-1. 配列とメモリ

 配列の各要素の先頭アドレスは各要素にアドレス演算子&を適用させることで取得できます。例えば、配列aの最初の要素の先頭アドレスは&a[0]で取得することができます。

[配列の各要素の先頭アドレス]
 &配列名[添字]
 また、配列名aは配列の開始アドレスを表していて、ポインタとして利用することができます。

[配列の開始アドレス]
 配列名
ただし、通常のポインタ変数とは違い、その値を変更することはできません。
 配列名はポインタ定数であり、その値を変更することはできない

アドレス演算

 配列のある要素を指し示すポインタをpとすると、p+1はその次の要素の先頭アドレスを指し示します。また、−1することでその前の要素の先頭アドレスを指し示すこともできます。配列の要素を指し示すポインタを+1したり、−1することで前後の要素の先頭アドレスを得ることができるわけです。このようなポインタに対する加減算をアドレス演算と言います。
 char型のデータを格納するために必要なメモリサイズは1バイトです。メモリ上で1バイトの領域を示すために必要なアドレスは1つだけです。int型(4バイト)やdouble型(8バイト)のように複数バイトの大きさからなるデータは、メモリ上に複数のアドレスをまたぐ領域を必要とします。例えば、int型の変数なら1000番地から1003番地までのアドレスになります。ですから、ポインタは複数バイトからなる型の変数を指し示すために先頭のアドレスだけでなく、そのサイズも知る必要があります。ポインタ変数を宣言する際に指し示す変数と同じ型を指定することで、コンパイラにそのサイズを知らせることができます。アドレス計算とはコンパイラがこのサイズを知ることで実現されています。
 次のプログラムはint型配列の各要素の先頭アドレスをポインタを使って表示しています。ポインタpにint型配列の開始アドレスが代入されたとすると、次の要素a[1]の先頭アドレスはp+1で求めることができます。
	int a[3] = {1,2,3};
	int *p;
	p = a;  // p=&a[0];と同じ
	printf("a[0]の先頭アドレス %p\n", p);
	printf("a[1]の先頭アドレス %p\n", p+1);
	printf("a[2]の先頭アドレス %p\n", p+2);

ポインタを+1すると次の要素を示す
 アドレス演算はポインタ型に応じて、例えばint *型であれば4の倍数バイト分、char *型であれば1の倍数バイト分数だけ増減されます。

プログラミング実験

  1. [実験1] 配列のメモリアドレスを取得する
  2.  はじめに次のプログラムを読んで配列aがメモリにどのように配置されるのか、メモリ配置図を描いて確認してみましょう。次に、実際にプログラムを実行した結果を確認して、メモリ配置図に各要素の先頭アドレスを記入してみましょう。もし、実行前のイメージと違っていた場合にはその理由を調べて配列のメモリ配置について納得しなさい。
    arrayaddress1.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[5] = {90,78,66,85,95};
    	int i;
    
    	for(i=0; i<5; i++)
    		printf("a[%d] = %d (%p)\n", i,a[i],&a[i]);
    
    	return 0;
    }
    

     次に、以下のプログラムのchar型の配列がメモリにどのように配置されるか調べてメモリ配置図を描いてみましょう。int型の配列の場合との違いを確認しましょう。
    arrayaddress2.c
    #include <stdio.h>
    
    int main(void)
    {
    	char c[6] = "Hello";	//もしくはc[6]={'H','e','l','l','o','\0'}
    	int i;
    
    	for(i=0; i<6; i++)
    		printf("c[%d] = %c (%p)\n", i,c[i],&c[i]);
    
    	return 0;
    }
    

  3. [実験2] 配列名とアドレス演算
  4.  次のプログラムを実行して配列名が配列の開始アドレスを示すポインタであることを確認しなさい。また、そのポインタに対してアドレス演算(+1)することで、次の要素の先頭アドレスが得られることを確認しなさい。
    arrayname1.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[5] = {90,78,66,85,95};
    	int i;
    
    	printf("配列名a -> (%p)\n",a);		//配列の開始アドレス
    	for(i=0; i<5; i++)
    		printf("*(a+%d) = %d (%p)\n", i,*(a+i),a+i);		// 間接参照 アドレス演算
    
    	return 0;
    }
    
    次のchar型の配列についても同様に配列名が配列の開始アドレスであることを確認しなさい。
    arrayname2.c
    #include <stdio.h>
    
    int main(void)
    {
    	char c[] = "Hello";	//{'H','e','l','l','o','\0'}でもOK
    	int i;
    
    	printf("配列名c -> (%p)\n",c);	//配列の開始アドレス
    	for(i=0; i<6; i++)
    		printf("c[%d] = %c (%p)\n", i,*(c+i), c+i);
    
    	return 0;
    }
    


  5. [実験3] ポインタ変数を使って配列を表示する
  6.  ポインタ変数を使って配列の各要素を表示するプログラムです。ポインタ演算によって配列の要素を連続的に参照できる仕組みについて確認し、ポインタが配列を指し示している仕組みを先のメモリ配置図に追加しなさい。
    array_pointer.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[5] = {90,78,66,85,95};
    	int *p;
    	int i;
    
    	p = a; // p = &a[0];でもOK
    
    	printf("a -> (%p)  p -> (%p)\n",a,p);	//配列の開始アドレス
    	for(i=0; i<5; i++)
    		printf("*(p+%d) = %d (%p)\n", i,*(p+i),p+i);
    
    	return 0;
    }
    
     変数aに代入された値と、ポインタpが指し示している値*pは同じ値になります。そのため、*pを変数aの「別名」もしくは「エイリアス」とも言います。
     次に、先のchar型の配列についてもポインタとアドレス演算によって各要素を指し示すことができることを確認するためにプログラムを作成し、コンパイル、実行しなさい。その結果についてもメモリ配置図に追加しなさい。

  7. [実験4] ポインタの演算
  8. 次のプログラムの実行結果を演算子の優先順位に注意して予測し、その後、実際に入力、コンパイル、実行してその結果と比較しなさい。予測と結果が違っていた場合は予測の何が間違っていたのか調べなさい。ただし、予測時のアドレスは実行時の値と違って良いが、その差分を考慮することで一致しているかを確認すること。
    point_to_array.c
    #include <stdio.h>
    
    int main(void)
    {
    	int a[5] = {90,78,66,85,95};
    	int *p;
    	int i;
    
    	p = a; // p = &a[0];でもOK
    
    	printf("p -> (%p)\n",p);	//配列の開始アドレス
    	printf("++*p = %d \n", ++*p);
    	printf("*p++ = %d \n", *p++);
    	printf("*++p = %d \n", *++p);
    
    	printf("p -> (%p)\n",p);
    
    	return 0;
    }
    
    ※演算子の優先順位はこちらで確認しましょう。


関数に配列を引き渡す

配列を渡す

 関数間で配列をそのまま渡すことはできません。例えば、main関数から関数funcへ配列を丸ごと渡すことはできません。引数を使って配列を渡したい場合には、配列の先頭アドレスを渡します。
 関数へ配列を渡す場合の引数の記述方法には以下の3つがあります。 いずれも配列の先頭アドレスを渡しています。
1)
void func(int a[])
{
	・・・
	a[i]	//i番目の要素の参照
	・・・
}
2)
void func(int a[10])
{
	・・・
	a[i]	//i番目の要素の参照
	・・・
}
3)
void func(int *a)
{
	・・・
	*(a+i)   //i番目の要素の参照a[i]でも可
	・・・
}

 なぜ、関数から関数へ配列を渡すことができないのでしょうか。関数が引数を使って受け取る値は、呼び出し側の値(実引数)のコピーでした。もし、配列を丸ごと受け取ることができたとして、その要素の数が1000個だった場合、実引数から仮引数へのコピー処理が1000回発生することになります。要素の数によっては大変負担の大き処理になることは明らかです。ですから、配列そのものではなく、配列の格納場所を渡すようにしてあるのです。
 以下は、配列を受けてすべての要素の合計値を戻り値する2つの関数の例です。配列を受け取るために、関数sumof_aは仮引数に配列a[]を、関数sumof_bは引数にポインタ*を指定しています。
sumof.c
#include <stdio.h>

int sumof_a(int a[], int size);
int sumof_b(int *a, int size);

int main(void)
{
	int score[5]={82,98,77,85,68};
	int i;

	printf("合計i: %d\n", sumof_a(score,5));
	printf("合計i: %d\n", sumof_a(score,5));

	return 0;
}

int sumof_a(int a[], int size)
{
	int i, sum=0;
	
	for(i=0; i<size; i++) sum += a[i];
	
	return sum;
}
int sumof_b(int *a, int size)
{
	int i, sum=0;
	
	for(i=0; i<size; i++) sum += *(a+i);
	
	return sum;
}
 どちらの関数を利用してもその結果が同じになることから、配列を受け取るための引数a[]と*aは等価であることがわかります。また、関数の中での配列の要素の参照においても、a[i]と*(a+i)が等価であることがわかります。

配列を共有する

 複数の関数に同じ配列の先頭アドレスを渡すことで、その配列を共有することができます。
 次の例では、main関数で宣言された配列aの先頭アドレスを関数setValue()とprintArray()に渡すことで、同じ配列を共有することになります。したがって、関数setValue()でその配列の要素の値が変更されれば、その後main関数でも関数setValue()でもその変更が反映されることになります。
shared.c
#include <stdio.h>

/* 配列のn番目の要素を値valueにする */
void setValue(int *a, int n, int value)
{
	*(a+n) = value;
}

/* 配列の表示 */
void printArray(int *a , int n)
{
	int i;

	for(i=0; i<n; i++)
		printf("%3d", a[i]);
	printf("\n");
}

int main(void)
{
	int a[5] = {0};

	setValue(a, 2, 7);	//2番目の要素を7に
	printArray(a,5);

	return 0;
}


[補]リトルエンディアンとビッグエンディアン

 int型やdouble型のデータサイズは4バイトや8バイトといった複数バイトからなるデータで、メモリに格納するためには複数のアドレスを跨ぐことになります。例えば、int型のデータ0x12345678をメモリに格納する方法にはアドレスの低位から1バイトごとに12,34,56,78と格納する方法と、逆に78,56,34,12と格納する方法があります。前者をビッグエンディアン、後者をリトルエンディアンと呼びます。このバイトオーダーの違いはプロセッサによって決まっています。
 次のプログラムは、バイトオーダがリトルエンディアンかビッグエンディアンかを確認するプログラムです。int型4バイトのデータを1バイト毎に表示するために、ポインタをchar型として、そのポインタに変数のアドレスを格納するためにchar *型でキャストしています
endianness.c
#include <stdio.h>

int main(void)
{
	int a = 0x12345678;		//4バイト
	char *p;		//1バイト

	p = (char *)&a;	//キャスト

	printf("%x (%p)\n", *p,p);
	printf("%x (%p)\n", *(p+1),p+1);
	printf("%x (%p)\n", *(p+2),p+2);
	printf("%x (%p)\n", *(p+3),p+3);

	return 0;
}

expand_lessBack to TOP

演習問題

  1. 10個の整数{12, 7, 33, 9, 15, 28, 11, 5, 38, 25}からなる配列を画面に表示するプログラムを作成しなさい。ただし、配列へのポインタと要素数を受け取って各要素を表示する関数void print_array(int *a, int size)を定義して利用すること。

  2. 10個の整数{12, 7, 33, 9, 15, 28, 11, 5, 38, 25}からなる配列を逆順に並べ替えた結果を表示するプログラムを作成しなさい。ただし、配列へのポインタとその要素数を受け取って要素を逆順に並べ替える関数void reverse(int *a, int size)を定義して利用すること。

  3. 10個の整数{12, 7, 33, 9, 15, 28, 11, 5, 38, 25}からなる配列のすべての要素を指定した値に設定した結果を表示するプログラムを作成しなさい。ただし、配列へのポインタを受け取ってその配列のすべての要素を指定した値dataでリセットする関数void reset_array(int *a, int size, int data)を定義して利用すること。

  4. 10個の整数{12, 7, 33, 9, 15, 28, 11, 5, 38, 25}からなる配列の最大値と最小値を求めた結果を表示するプログラムを作成しなさい。ただし、配列へのポインタとその要素数を受け取って最大値と最小値を求める関数void minmax(int *a, int size, int *max, int *min)を定義して利用すること。最大値と最小値の値は関数minmaxの呼び出し側と関数側でポインタを使って受け渡しするものとします。

  5. 10個の整数{12, 7, 33, 9, 15, 28, 11, 5, 38, 25}からなる配列の要素を交互に2つの配列に分配するプログラムを作成しなさい。ただし、3つの配列へのポインタを渡して1つの配列の要素を交互に2つの各配列に分配する関数void split(int *s, int size, int *a, int *b)を定義して利用すること。

OPEN ANSWER
  1. 回答例 ex3-1.c
  2. code/3/ex3-1.c

  3. 回答例 ex3-2.c
  4. code/3/ex3-2.c

  5. 回答例 ex3-3.c
  6. code/3/ex3-3.c

  7. 回答例 ex3-4.c
  8. code/3/ex3-4.c

  9. 回答例 ex3-5.c
  10. code/3/ex3-5.c


expand_lessBack to TOP

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

本日の提出課題

 以下の整数をN個入力してその値の合計と昇順に並べ替えた結果を表示するプログラムkadai3-3.cを完成させなさい。ただし、配列aを受け取ってその合計を戻り値とする関数int sum(int *a, int n)、配列aの全ての要素を表示する関数printArray(int *a, int n)を定義して利用すること。配列aを受け取って昇順に並べ替える関数void sort(int *a, int n)については、空欄部分を埋めて完成させること。仮引数のnはいずれも配列aの要素数を示す。また、これらの関数以外に関数は追加できないものとします。
#include <stdio.h>
#define N 7 

/* 配列aの要素の合計を求める */
int sum(int *a, int n)
{
	// ここを埋めて完成させる
}

/* 配列の要素を表示する */
void printArray(int *a, int n)
{
	// ここを埋めて完成させる
}

/* 昇順に並べ替える*/
void sort(int *a, int n)
{
	int i,j,tmp;
	
	for(i=1; i<n; i++){
		for(j=0; j<n-i; j++){
			if([空欄ア]){		// 隣の要素同士を比較し前の方が大きければ
				[空欄イ]	// 複数行空欄、要素を交換するコードをここに記述
			}
		}
	}
}

int main(void)
{
	int i;
	int score[N];

	for(i=0; i<N; i++)
		scanf("%d", &score[i]);

	printArray(score, N);
	printf("合計: %d\n",sum(score, N));

	sort(score,N);
	printArray(score,N);

	return 0;
}	

[関数sort()における並べ替え処理の考え方]
 要素数n個の配列を昇順に並べ替える場合の手順。
     1)先頭要素と隣の要素を比較する
     2)前の要素の値の方が大きければ、その値を交換する
     3)一つ右に移動して、隣合う要素の値を比較する。
     4)比較・交換を最後尾要素まで繰り返す。
     5)その結果、最も大きな要素が最後尾に移動する。
     6)最後尾の要素を除く配列に対して、手順1から6を繰り返す。


  • [提出方法]
    • 電子メールの添付ファイルとして提出してください。宛先は指定のアドレスです。
    • 表題は、課題3とします。
    • メール本文には、複数の関数で配列scoreを共有できる仕組みについて述べること。
    • 添付ファイルは下記の2点です。
    • 1) ソースコード・ファイル kadai3-3.c
      2) 実行結果画面(ウインドウのみ)のハードコピーの画像ファイル kadai3-3.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月 9日(水)午後3時まで