第七回(ポインタ)


ポインタ

ポインタとは変数のアドレスを持つ変数です。ポインタはC言語の最初の難関とも言われています。

ポインタ

ポインタは変数のアドレスを持つ変数です。ポインタ変数、アドレス変数とも呼びます。 アドレスというのは変数の場所のことです。変数の場所がわかっているので、int x;という変数があったとして、 int *p;というポインタにint x;のアドレスを持たせた場合、*pの値を参照するとxの値を見ることができたり、*pの値を変えると、 xの値を変えたりできます。
以下に絵を用意しました。イメージとしては*pの*がどこかをさしているような感じでいいと思います。この場合だと、*pはxをさしています。

変数のアドレスの格納の仕方

では具体的にint *p;がint x;のアドレスを格納する方法の説明をします。p = &x;です。&xはxのアドレスを意味しています。また、*pで あるにも関わらず、pと書いています。ここも重要なところです。

ポインタの指す先の参照

ポインタ指す先を参照する場合は、上の例の場合、*pで参照できます。
次に具体的な例で見てみましょう。

サンプルソース
#include<stdio.h>

int main(void);

int main(void){
	int x = 1;
	int *p;
	
	p = &x;	//アドレスの格納
	
	printf("x = %d\n", x);
	printf("*p = %d\n", *p);
	
	*p = 10;	//ポインタの指す先の変数(この場合だとint型のx)に代入
	
	printf("x = %d\n", x);
	printf("*p = %d\n", *p);
	
	return(0);
}
実行結果
x = 1
*p = 1
x = 10
*p = 10

また、printf関数には%pというものも存在します。これを使って、引数に変数のアドレスを入れるとその変数のアドレスを表示することができます。
上で説明した通り、ポインタ変数pは変数xのアドレスを持っているわけですから、同じアドレスが表示されるはずです。
また、アドレスの表示の仕方は&(アドレスを見たい変数)で変数のアドレスを渡せるので、それをprintfの引数に渡して%pで表示します。
例えば、int x;という変数のアドレスを見たい場合はprintf("%p", &x);と書くことで表示できます。
以下に実際の例を記しました。

サンプルソース
#include<stdio.h>

int main(void);

int main(void){
	int x = 1;
	int *p;
	
	p = &x;
	
	printf("x = %p\n", &x);
	printf("*p = %p\n", p);
	
	return(0);
}
実行結果(僕の環境ではこうなりました。)
x = 0012FF88
*p = 0012FF88

ポインタの宣言の時、int *p;をint* p;と書く人もいます。これは同じ意味ですので、見かけたらそう考えてください。 個人的にはint *p;を推奨します。なぜなら、int* p, q;と宣言されているとややこしいためです。

ポインタと関数

ポインタの素晴らしいところのひとつです。関数と言えば、返せる値は1つだけでした。 なんと、ポインタを使うと複数の変数の値を返すこともできます。
また、関数で文字列を返すのは今までできませんでした。理由は配列なためです。複数も変数を返すのと同じ意味になるためです。 しかし、ポインタを使うと文字列を返すことも可能になります。

関数で複数の変数の値を返す

複数の変数の値を返す場合、関数の実引数に変数のアドレスを渡して、仮引数でポインタ変数に入れます。 後は先ほどのポインタの使い方で変数の値を変更すれば、関数で最終的に変更された値が実引数に渡した変数に入ります。 以下はポインタを用いて、変数の値を入れ替えた例です。

サンプルソース
#include<stdio.h>
#include<stdlib.h>

#define MAX 256

int main(void);
void print(int x, int y);
void swap(int *x, int *y);

int main(void){
	
	char X[MAX], Y[MAX];
	int x, y;
	
	printf("x: ");
	fgets(X, MAX, stdin);
	x = atoi(X);
	printf("y: ");
	fgets(Y, MAX, stdin);
	y = atoi(Y);
	
	print(x, y);
	swap(&x, &y);	//仮引数に変数のアドレスを渡す
	print(x, y);
	
	return(0);
}

void print(int x, int y){
	
	printf("x = %d\n", x);
	printf("y = %d\n", y);
	
	return;
}

void swap(int *x, int *y){	//ポインタ
	
	int dummy;
	
	dummy = *x;
	*x = *y;
	*y = dummy;
	
	return;
}
実行例
x: 2
y: 5
x = 2
y = 5
x = 5
y = 2

また、配列を関数の引数に渡すこともできます。この場合は配列の変数の先端の値を引数に渡します。
※アドレスを渡しているので、値を変えるとさしている先の配列の値も変わってしまいますので、注意が必要です。
int x[5];の場合は引数に&x[0]、また、xと書くだけでも&x[0]と書いているのと同じように解釈されます。
また、配列のポインタ変数int *xがint x[5];のアドレスを持っていた場合、x++;または、++x;で次の要素(0番目から1番目へ等)へいけます。
逆に--x;やx--;で戻ることも可能です。
以下は配列を使った例です。

サンプルソース
#include<stdio.h>

#define MAX 10

int main(void);
void print(int *x);

int main(void){
	
	int x[MAX] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
	
	print(&x[0]);
	
	return(0);
}

void print(int *x){
	
	int i;
	
	for(i = 0; i < MAX; i++){
		printf("x = %d\n", *x);
		x++;
	}
	return;
}
実行結果
x = 0
x = 1
x = 2
x = 3
x = 4
x = 5
x = 6
x = 7
x = 8
x = 9

次に配列の値を変えている例を示します。

サンプルソース
#include<stdio.h>
#include<stdlib.h>

#define MAX 10

int main(void);
void print(int *x);
void sum_r(int *x);

int main(void){
	
	int x[MAX] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
	
	print(x);	//&x[0]と渡しているのと同じ
	sum_r(x);
	print(x);
	
	return(0);
}

void print(int *x){
	
	int i;
	
	for(i = 0; i < MAX; i++){
		printf("x = %d\n", *x);
		x++;
	}
	return;
}

void sum_r(int *x){
	
	int i;
	
	for(i = 0; i < MAX; i++){
		*x += rand() % 100;	//rand関数(乱数生成用関数)
		x++;
	}。
	printf("\n");
}
実行結果
x = 0
x = 1
x = 2
x = 3
x = 4
x = 5
x = 6
x = 7
x = 8
x = 9

x = 30
x = 83
x = 92
x = 59
x = 21
x = 100
x = 21
x = 55
x = 34
x = 13
ポインタ変数(アドレス変数)のインクリメント・デクリメント

ポインタ変数では演算の優先度の関係でインクリメント・デクリメントの書き方が少し特徴的です。*xをインクリメントする場合は、 (*x)++;と書きます。*x++;と書いてもインクリメントされません。そう書くと、ポインタのさす位置を1つずらしてしまいます。
デクリメントは(*x)--;です。以下に例を示します。

サンプルソース
#include<stdio.h>

int main(void);
void increment(int *x);

int main(void){
	
	int x = 0;
	
	printf("x = %d\n", x);
	increment(&x);
	printf("x = %d\n", x);
	
	return(0);
}

void increment(int *x){
	
	(*x)++;
	
	return;
}
実行結果
x = 0
x = 1
関数で文字列を返す

文字列を関数の中で標準入力して、その値を返したいと思ったことはあるのではないでしょうか?そういうときに使うのがポインタです。 実引数を文字の配列の先端のアドレスにすれば、うまくいきます。
void 関数名(char *str)というようにすればいいです。 以下に例を示します。

サンプルソース
#include<stdio.h>

#define MAX 256

int main(void);
void name_input(char *str);

int main(void){
	
	char str[MAX];
	
	name_input(str);
	
	printf("Name: %s", str);
	
	return(0);
}

void name_input(char *str){
	
	printf("Name?\n> ");
	fgets(str, MAX, stdin);
	
	return;
}
sscanf関数

sscanf関数はatoiのような使い方や、fgets関数の最後の改行文字を取り除くときに使うことができます。
atoiのような使い方をするときは、sscanf(文字列の変数, "%d", 値を入れるint型の変数のアドレス);です。
改行を取り除くときはsscanf(文字列の変数, "%s\n", 文字列の先端のアドレス);です。
#include<stdlib.h>をインクルードする必要のあるatoi関数のことを考えると、こっちを使ってもいいかもしれないですね。

文字列の変数を関数に渡したときもアドレスを渡すので、中で変更されるとその変更がさしている先の変数でも起こります。 sscanf関数の例とともに示します。

サンプルソース
#include<stdio.h>
#include<string.h>

#define MAX 256

int main(void);
void Nyoro(char *str);

int main(void){
	
	char str[MAX];
	int a;
	
	printf("入力する文字: ");
	fgets(str, MAX, stdin);
	sscanf(str, "%s\n", str);
	
	Nyoro(str);	//&str[0]の省略
	
	printf("%s", str);
	return(0);
}

void Nyoro(char *str){
	
	strncat(str, "にょろ", 6);
	
}
実行例
入力する文字: めがっさにょろ
めがっさにょろにょろ
scanf関数

scanf関数は標準入力関数で、fgetsより軽いです。使い方はsscanf("%○(入力する文字の型によりdやs等になる)", 値を入れる変数のアドレス);です。 数字を入れるときは、int型の変数を用意します。int x;という変数に入れる場合は、scanf("%d", &x);です。
一見すると非常に便利ですが、以下の問題点があります。

等などといった問題点があるので、scanfは必ず入れる値が決まっている… プロコン(プログラミングコンテスト)等で使用する以外は極力使わないことをおススメします。

ポインタの配列

ポインタの配列は名前のとおりです。int *x[5];とすると、5つのポインタ変数ができます。

また、関数の実引数に配列の先端のアドレスを渡して使うときに*(p + 1)とすると、p++等でポインタのさしている位置を変えなくても 配列の1番目の要素を見ることができます。以下は例です。

サンプルソース
#include<stdio.h>

#define MAX 10

int main(void);
void print(int *x);

int main(void){
	
	int x[MAX] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
	
	print(&x[0]);
	
	return(0);
}

void print(int *x){
	
	int i;
	
	for(i = 0; i < MAX; i++){
		printf("x = %d\n", *(x + i));
	}
	return;
}
実行結果
x = 0
x = 1
x = 2
x = 3
x = 4
x = 5
x = 6
x = 7
x = 8
x = 9

また、配列のアドレスを渡した場合のみ、void print(int *x)のところをvoid print(int x[MAX])として、配列として扱うこともできます。 (void print(int *x)のままでも配列として扱うことができます。)
ただし、アドレスを渡しているので値を変えると、さしている先の値が変わるのは同じです。以下は例です。

サンプルソース
#include<stdio.h>

#define MAX 10

int main(void);
void print(int x[MAX]);

int main(void){
	
	int x[MAX] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
	
	print(&x[0]);
	
	return(0);
}

void print(int x[MAX]){
	
	int i;
	
	for(i = 0; i < MAX; i++){
		printf("x = %d\n", x[i]);
	}
	return;
}

ポインタと文字列

ポインタは今まで説明したようにどこかの変数をさすだけではありません。メモリの節約をすることもできます。
配列だと、確保する数を指定する必要がありましたが、ポインタだと必要ありません。
char *p = "RCC";と書くとメモリ上のどこかにとられた"RCC"文字列の先端のアドレスを直接ポインタで取得することができます。
メモリ上にとられた文字列は隙間なくとられるので、メモリの節約になります。
以下は例です。

サンプルソース
#include<stdio.h>

int main(void);

int main(void){
	
	char *s = "RCC";
	
	printf("*s = %s", s);	//文字列の先端のアドレスを見ている
	
	return(0);
}
実行結果
*s = RCC

また、ポインタの配列でも同様のことが可能です。

サンプルソース
#include<stdio.h>

#define MAX 7

int main(void);

int main(void){
	
	char *day[MAX] = {
		"Sunday",
		"Monday",
		"Tuesday",
		"Wednesday",
		"Thursday",
		"Friday",
		"Saturday"
	};
	int i;
	
	for(i = 0; i < MAX; i++){
		printf("%s\n", day[i]);
	}
	
	return(0);
}
実行結果
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

構造体のポインタ

構造体もポインタを使えますが、参照の書き方が特殊です。

struct data{
	int x;
	int y;
};

という構造体があったとき、宣言にstruct data data;とされていて、その構造体をさすポインタはstruct data *d;だったとします。 アドレスの渡し方はd = &dataで渡せます。そして、参照するときは、*data.xではなく、(*data).xです。これは優先度の問題によるものです。 ドット演算子だと非常に書くのが面倒になります。この面倒な書き方をなくせるものがあります。それがアロー演算子です。

アロー演算子

アロー演算子とは、構造体のポインタで参照するときに使います。演算子としては->と書きます。
(*data).xと書くところを、data->xと書きます。dataの前に*がないことも重要なところです。

では例をみてみましょう。

サンプルソース
#include<stdio.h>
#include<stdlib.h>

#define MAX 256

typedef struct Hitode{
	int num;
	int month;
	int day;
}Hitode;

int main(void);
void menu(void);

int main(void){
	menu();
	return(0);
}

void menu(void){
	
	Hitode data = {100, 2, 13};
	Hitode *d;
	char str[MAX];
	
	d = &data;
	
	printf("%d月 %d日に配れたひとでの数:%d\n", data.month, data.day, data.num);
	
	printf("月: ");
	fgets(str, MAX, stdin);
	d->month = atoi(str);
	printf("日: ");
	fgets(str, MAX, stdin);
	d->day = atoi(str);
	printf("ひとでの数: ");
	fgets(str, MAX, stdin);
	d->num = atoi(str);
	
	printf("%d月 %d日に配れたひとでの数:%d\n", data.month, data.day, data.num);
	
	return;
}
実行例
2月 13日に配れたひとでの数:100
月: 2
日: 22
ひとでの数: 222
2月 22日に配れたひとでの数:222
構造体の配列のポインタ

構造体の配列も先端のアドレスを渡せば、値を関数内で変更することができます。アロー演算子を使う以外は配列と一緒です。 以下はサンプルソースです。

サンプルソース
#include<stdio.h>
#include<stdlib.h>
#include<string.h>


#define MAX 256
#define NUM_MAX 5

typedef struct Hitode{
	int num;
	int month;
	int day;
}Hitode;

enum Menu{
	PRINT,
	INPUT,
	QUIT
};

typedef enum bool{
	false,
	true
}bool;

int main(void);
void menu(void);
void print(void);
void print_data(Hitode h[MAX], int i);
int input(Hitode *h, int i);

int main(void){
	menu();
	return(0);
}

void menu(void){
	
	Hitode hitode[NUM_MAX];
	int i = 0;
	char select[MAX];
	bool flag = false;
	
	while(true){
		
		print();
		fgets(select, MAX, stdin);
		
		printf("\n");
		
		switch(atoi(select)){
			
			case PRINT:
				print_data(hitode, i);	//データ表示
				break;
				
			case INPUT:
				if(i < NUM_MAX)i = input(hitode, i);	//データ入力
				else printf("INPUT MAX\n");
				break;
				
			case QUIT:
				flag = true;	//終了
				break;
		}
		if(flag == true)break;
	}
	printf("空飛ぶヒトデ、いいですっ☆\n");
}

void print(void){
	
	printf("0: PRINT\n");
	printf("1: INPUT\n");
	printf("2: QUIT\n");
	printf("select: ");
	
	return;
}

void print_data(Hitode h[MAX], int j){
	
	int i;
	
	for(i = 0; i < j; i++){
		printf("%d月 %d日に配れたヒトデでの数:%d\n", h[i].month, h[i].day, h[i].num);
	}
	return;
}

int input(Hitode *h, int i){
	
	char str[MAX];
	
	while(true){
		
		printf("月: ");
		fgets(str, MAX, stdin);
		(h + i)->month = atoi(str);
		printf("日: ");
		fgets(str, MAX, stdin);
		(h + i)->day = atoi(str);
		printf("ヒトデの数: ");
		fgets(str, MAX, stdin);
		(h + i)->num = atoi(str);
		
		i++;
		if(i < NUM_MAX){
			printf("入力を続けますか?\n0: No\n1: Yes\n");
			fgets(str, MAX, stdin);
			if(atoi(str) == 0)break;
		}
		else break;
	}
	
	return(i);
}
リダイレクション用
1
2
1
100
1
2
2
80
1
2
3
120
1
2
4
200
1
2
5
300
1
0
2
リダイレクションを用いた実行例
0: PRINT
1: INPUT
2: QUIT
select:
月: 日: ヒトデの数: 入力を続けますか?
0: No
1: Yes
月: 日: ヒトデの数: 入力を続けますか?
0: No
1: Yes
月: 日: ヒトデの数: 入力を続けますか?
0: No
1: Yes
月: 日: ヒトデの数: 入力を続けますか?
0: No
1: Yes
月: 日: ヒトデの数: 0: PRINT
1: INPUT
2: QUIT
select:
INPUT MAX
0: PRINT
1: INPUT
2: QUIT
select:
2月 1日に配れたヒトデでの数:100
2月 2日に配れたヒトデでの数:80
2月 3日に配れたヒトデでの数:120
2月 4日に配れたヒトデでの数:200
2月 5日に配れたヒトデでの数:300
0: PRINT
1: INPUT
2: QUIT
select:
空飛ぶヒトデ、いいですっ☆

ポインタのポインタ

ポインタのポインタは名前のとおりポインタのポインタです。つまり、変数のアドレスを持つ変数のアドレスを持つ変数です。
書き方は*が1つ増えるだけです。また、ポインタのポインタのポインタも可能です。使うことはまずありませんが…。以下は例です。

サンプルソース
#include<stdio.h>

int main(void);

int main(void){
	
	int a = 1, *b, **c;
	
	b = &a;
	c = &b;
	
	printf("a = %d\n", a);
	printf("b = %d\n", *b);
	printf("c = %d\n", **c);
	
	**c = 2;
	
	printf("a = %d\n", a);
	printf("b = %d\n", *b);
	printf("c = %d\n", **c);
	
	*b = 3;
	
	printf("a = %d\n", a);
	printf("b = %d\n", *b);
	printf("c = %d\n", **c);
	
	a = 0;
	
	printf("a = %p\n", &a);
	printf("b = %p\n", b);
	printf("c = %p\n", *c);
	
	return(0);
}
実行結果
a = 1
b = 1
c = 1
a = 2
b = 2
c = 2
a = 3
b = 3
c = 3
a = 0012FF88
b = 0012FF88
c = 0012FF88

動的メモリと静的メモリ

動的メモリとは自分でメモリを確保して自分で解放する必要があるメモリのことです。
また、int x;や、char str[MAX];等で宣言しているものは静的メモリといいます。これはサイズがわかっているので、 自動でメモリを確保しています。そして、プログラムが終了するまで確保されています。
また、関数内での静的メモリは関数が終了するまで確保されています。

動的メモリの使い方

動的メモリは専用の関数を使って確保して、専用の関数で解放します。ヘッダファイルでは、#include<stdlib.h>にあります。

malloc関数

この関数を用いて確保します。例としてはint型のポインタで確保する場合だと、int型のポインタのさす先を確保する感じです。
形は(確保する型 *)malloc(確保するサイズ);です。例としてはint *x;で確保する場合、x = (int *)malloc(10);と言った感じに確保します。 また、確保するサイズはsizeof演算子を使うといいです。

sizeof演算子

sizeof演算子はサイズを返す演算子です。sizeof(型名)やsizeof(変数)、sizeof(定数や式)等で使えます。
また、一般的な型のsizeを以下に記しておきます。(自分の環境で確認)

型名 サイズ
char 1
int 4
float 4
double 8
free関数

free関数は動的に確保したメモリを解放する関数です。これを行わないと、メモリが解放されず、メモリリークが発生してしまいます。
使い方はfree(動的に確保したメモリをさしているポインタ);です。

以下は動的にメモリを確保した例です。
サンプルソース
#include<stdio.h>
#include<stdlib.h>

int main(void);

int main(void){
	
	int *x;
	
	x = (int *)malloc(sizeof(int));	//動的にメモリ確保
	
	*x = 1;
	
	printf("*x = %d\n", *x);
	
	free(x);	//メモリの解放
	return(0);
}
実行結果
*x = 1

以下は文字列を関数で返す例です。動的に確保することで返すことができます。

実行結果
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#define MAX 256

int main(void);
char *input(void);

int main(void){
	
	char *str;
	
	str = input();
	
	printf("%s", str);
	
	free(str);	//動的に確保したメモリの解放
	return(0);
}

char *input(void){
	
	char *str;
	
	str = (char *)malloc(sizeof(MAX));	//動的にメモリ確保
	
	fgets(str, MAX, stdin);		//文字列を標準入力
	sscanf(str, "%s\n", str);
	
	return(str);	//ポインタを返す
}
実行結果
にょろ〜ん
にょろ〜ん

動的にメモリを確保することは大切です。これは、リスト構造や木構造といったデータ構造を作る時に必要不可欠になります

基本問題

以下の流れを満たすプログラムを作成してください。

  1. char型の配列a, bを宣言する。
  2. a, bに文字列を標準入力する関数Aを作る。
  3. a, bの中の文字列を標準出力する関数Bを作る。
  4. a, bの文字列を入れ替える関数Cを作る。
  5. 再び関数Bを呼び出して、a, bを標準出力する。
※配列名はa, bじゃなくてよく、関数名も自由とします。
実行例
> チョココロネ
> メロンパン
チョココロネ メロンパン
メロンパン チョココロネ

応用問題

ポインタを用いて石とりゲームを作ってください。石とりゲームとは特定の個数の石を用意して、順番に最低1個、最高3個をとっていき、最後の1個をとった人が負けというゲームです。
余裕があれば、コンピュータ(AI)を作ってください。rand関数とsrand関数を使えば、ランダムにとってくれるようにできて普通に戦えます。
また、コンピュータを強くする方法は、残りの石の数を4で割ったときのあまりを1になるように石をとらせればできます。
※☆や○等を石の代わりにして、自分の番のときに残りの個数を表示すると楽しいかもしれないです。

次回予告

次回はファイル操作について説明します。

戻る