応用理工学情報処理 8回目

今回は、テキスト第10章のポインタとテキスト11章の文字列とポインタについて学習します。

本日の課題の要約

(1)ポインタに関係する演算子: &:アドレス演算子(変数のアドレスを返す)、 *:間接演算子(そのアドレスに格納された値を返す)
(2)ポインタ変数の宣言: int *p; // ポインターをpとして、*pが整数型ということなので、*pをこれまで扱ってきた宣言と同様に宣言すればよい。
(3)ポインタに出来る演算は整数の加算と減算のみ:その数だけ離れた数値を指すようになる。
(4)文字列定数とポインタ: char *p = "123"; // 文字列 "123" は値としてはプログラム中に格納された文字列データ(末尾がナル文字\0の文字配列)を指すポインタです。

===実習課題1:これまでの復習(制限時間:5分)===

以下のプログラム中の /// ここにコードを書き加える ///部分にコードを追加する(他の部分には加えない)ことによってプログラムを完成させよ。つまり、読み込んだ配列中に含まれる偶数の和と奇数の和を計算する部分を指定位置に書き加えよ。

#include <stdio.h>
#define N 5
int main(void)
{
    int data[N];
    int i, se=0, so=0; // se:偶数の合計、so:奇数の合計
    printf("%d個の整数を入力\n", N);
    for (i=0; i<N; i++) {
        printf("%d個目:", i+1); scanf("%d", &data[i]);
    }
    /// ここにコードを書き加える ///
    printf("偶数の合計:%d、奇数の合計:%d\n", se, so);
    return (0);
}

===実習課題2:ポインタによる呼び出し元の値の変更 (制限時間:5分)===

以下のプログラムのmain関数の前に関数my_absを加えて、プログラムが正常に動作するようにせよ。

#include <stdio.h>
int main(void)
{
	int n;
    printf("整数を入力:"); scanf("%d", &n);
	my_abs(&n);
	printf("nの絶対値は%dです。\n", n);
	return (0);
}

===実習課題3:ポインタ演算 (制限時間:5分) ===

以下のプログラムを、乱数を用いて作成した英小文字100個の文字列から指定した文字の個数を表示するようにしたい。main関数の前に配列の添え字演算子[ ]を使用しない関数chnumを加えて、プログラムが正常に動作するようにせよ。

#include <stdio.h>
int main(void)
{
	char abc[101], ch;
	for (int i=0; i<100;i++) abc[i] = (char)(rand( ) % 26 + 0x61);
	abc[100] = '\0';
	printf("数える英小文字を入力:"); scanf("%c", &ch);
	printf("文字列「%s」の中に文字%cが含まれる個数:%d", abc, ch, chnum(abc, ch));
	return (0);
}

===実習課題4:ポインタを返す関数 (制限時間:5分) ===

以下のプログラムを、乱数を用いて作成した英小文字100個の文字列から指定した文字の手前までをカットした文字列を表示するようにしたい。main関数の前に配列の添え字演算子[ ]を使用しない関数trim_strを加えて、プログラムが正常に動作するようにせよ。加えるべき関数trim_strは指定した文字を指すポインタである。また、指定した文字が存在しない場合には文字列終端のナル文字へのポインタを返す。

#include <stdio.h>
int main(void)
{
	char abc[101], ch;
	for (int i=0; i<100;i++) abc[i] = (char)(rand( ) % 26 + 0x61);
	abc[100] = '\0';
	printf("数える英小文字を入力:"); scanf("%c", &ch);
	printf("「%s」から最初の文字%c手前までカットした文字列「%s」\n", abc, ch, trim_str(abc, ch));
	return (0);
}

(a) ポインタ 

「1」ポインタ ポインタとは、変数などのオブジェクトが記憶領域上のどこにあるかを表すアドレスを保持している変数です。大事な点は、アドレスだけでなくそこにあるデータの型情報も保持している点です。

これまで学んできたプログラミングの中でも、ポインタを使ってきたところがあります。

(1)標準ライブラリ関数scanf で整数変数nに値を読み込むとき、scanf ("%d", &n)のように2番目の実引数で変数名n の前に &を付けて用いた。
===>&n はn のアドレスを表す。&n の値を保持する変数がポインタ

(2)配列を引数に持つ関数では、例えば int data[10]; を実引数として関数に渡す場合には配列の名前だけを指定して、sum( data)のようにしていた。
===>配列の名前は、配列の最初の要素のアドレスを保持するポインタ(定数)となる。

ポインタ:他の変数のアドレスを内容とする変数

int *p; int型変数のポインタpの宣言
int a = 5:

アドレス演算子(&演算子)
&a   変数aのアドレス
ポインタはこのアドレスを格納する変数
p = &a;
間接演算子(*演算子)
*p   pが指すアドレスの内容
(今の場合a に等しく、また* (&a)と同じ)

ポインタの動作を確認しよう。-以下のプログラムを動作させてポインタがどのように働くのかを理解してください。

#include <stdio.h>
int main(void)
{
	int a=5, b=10;
	int *x; /* int型ポインタの宣言、(*x)がint型 */

/* アドレス演算子、間接演算子の動作確認  */
	printf("変数aのメモリ上のアドレスは %pです。\n", &a); //アドレスを表示する変換指定は%p
	printf("そのアドレスの内容は、%dです。\n", *(&a));

/* ポインタへの代入。 代入する値によってそれが指す内容が異なる。*/
	x = &a;
	printf("x = &aで*xはaと等しくなる:*x=%d\n", *x);
	x = &b;
	printf("x = &bで*xはbと等しくなる:*x=%d\n", *x);
	return 0;
}

「2」関数呼び出しとポインタ

C言語では、関数の引数は関数が呼び出されたときにその関数内部でのみ用いる変数にコピーされるため、その関数内で呼び出し元の変数の値を変化させることはできない。これを、値による呼び出し(call by value)と呼ぶ。

しかし、関数の引数に変数のアドレス(ポインタ)を用いることで、間接的に呼び出し元の変数の値を変化させることができる。

以下のプログラムで、関数の引数にポインタを用いた場合の効果を理解しましょう。

#include <stdio.h>

void call_by_value( int x)
{
     x = 5;  
/*=>呼び出し元は変化しない。*/
}
void call_by_reference( int *x)
{
     *x = 5;  
/*呼び出し元の変数の値が5に変化する。*/
}

int main(void)
{
	int x = 10;
	printf("Original: x = %d\n", x);
	
	call_by_value(x);
	printf("After call_by_value(x): x = %d\n", x);
	
	call_by_reference(&x); //ポインタなのでxのアドレスを渡す
	printf("After call_by_reference(x): x = %d\n", x);
	
	return 0;
}

「3」ポインタを用いた関数の例 

2値を昇順に並べ替える(テキストp.307 List10-8) ポインタを用いることによって、呼び出し側で値を入れ替えなくても良くなっています。

/*
	二つの整数値を昇順に並べる
*/

#include  <stdio.h>

/*--- pxとpyが指すオブジェクトの値を交換 ---*/
void swap(int *px, int *py)
{
	int	 temp = *px;
	*px = *py;
	*py = temp;
}

/*--- *n1≦*n2となるように並べる ---*/
void sort2(int *n1, int *n2)
{
	if (*n1 > *n2)
		swap(n1, n2);
}

int main(void)
{
	int	 na, nb;

	puts("二つの整数を入力してください。");
	printf("整数A:");	  scanf("%d", &na);
	printf("整数B:");	  scanf("%d", &nb);

	sort2(&na, &nb);

	puts("昇順にソートしました。");
	printf("整数Aは%dです。\n", na);
	printf("整数Bは%dです。\n", nb);

	return (0);
}

 

(b) 文字列とポインタ

「4」配列とポインタ 

第6章で配列を引数とする関数を学習した際、配列の場合には関数内で呼び出しもとの配列の値を書き換えることが出来ました。これは、ポインタを引数とする関数の場合と同じです。配列を宣言すると、自動的にその配列名が最初の要素を指すポインタとなります。たとえば配列をint array[100]; と宣言したならば、*arrayはarray[0]とまったく同じになります。アスタリスク"*"つまり間接演算子は、そこのアドレスに格納されている情報を表します。また、*(array + 1) は array[1]と同じになります。ポインタは型情報を持っているので、4バイトのint型だとarray + 1 の演算を行うと自動的に4バイト進んだ位置のアドレスになります。

int array[100]; /* 配列宣言 */

配列の[ ]を除いたarrayは、配列の最初の要素へのポインタとなる。
つまり *array とarray[0]は等しい

*(array)   <=> array[0]
*(array+1) <=> array[1]
*(array+2) <=> array[2]

printf("%p, %p, %p\n", array, array+1, array+2);によって配列の要素のアドレスを出力すると、整数型はここのコンピューターでは4バイトなので、a, a+1, a+2のアドレスが4ずつ増加していることがわかる。

以下のプログラムはテキストp280 List10-11です。プログラムをコメントのように書き換えてみましょう。関数への配列の受け渡しをポインタの観点から見てください。

/*
	配列の受渡し
*/

#include  <stdio.h>

void ary_set(int vc[], int n, int val)   //int *vcと解釈される。
{
	int  i;

	for (i = 0; i < n; i++)
		vc[i] = val;         // *(vc + i) =val; と書くこともできる
}

int main(void)
{
	int	 i;
	int	 ary[] = {1, 2, 3, 4, 5};

	ary_set(ary, 5, 99);

	for (i = 0; i < 5; i++)
		printf("ary[%d] = %d\n", i, ary[i]);

	return (0);
}

「5」文字列とポインタ 

char str[ ] = "ABC";
char *ptr = "ABC";

これら2つの文は、ほとんど同じ意味を持っています。 str[0], ptr[0]はどちらも'A'であり、str[3]とptr[3]はどちらも'\0'です。また、strはstr[0]を指すポインタであり、ptrはptr[0]を指すポインタです。

違う点は、ポインタptrの方はptr = "DEF";というように別の文字列を指すように変更可能であるのに対して、strの方は別の配列を指すようには変更出来ない点です。配列宣言はメモリ領域の確保であり、char str[ ] = "ABC";では、確保したメモリ領域を"ABC"で初期化しています。これに対してchar *ptr = "ABC";の方は文字列リテラル(文字列定数)"ABC"を指すポインタでptrを初期化しています。このため、配列宣言ではchar str[6] = "ABC";というように文字列の長さを超えて領域を確保することができますが、ポインタ宣言の場合にはすでに存在しているメモリ領域のあるアドレスを指し示すだけです。

文字列におけるポインタの演算の例として、文字列をコピーする(テキストp.294, List 11-6)プログラムを作成してみましょう。

以下の点に注意して、次のプログラムを動かしてみてください。
(1)関数str_copyはポインタを返す関数として定義されている。このため、printf関数の引数として直接この関数の返り値を渡すことも出来る。
(2)関数内のwhileループの条件判断では、文字列の終端が'\0'であることを利用している。また、*d++ = *s++では、まず*d = *sの代入が行われた後にd = d+1及びs = s+1の演算が行われる。

/*
	文字列をコピーする
*/

#include  <stdio.h>

/*--- 文字列sをdにコピーする ---*/
char *str_copy(char *d, const char *s)
{
	char  *t = d;

	while (*d++ = *s++)
		;
	return (t);
}

int main(void)
{
	char  str[128] = "ABC";
	char  tmp[128];

	printf("str = \"%s\"\n", str);

	printf("コピーするのは:"); scanf("%s", tmp);
	str_copy(str, tmp);

	puts("コピーしました。");
	printf("str = \"%s\"\n", str);
	
	return (0);
}

「6」文字列を扱うライブラリ関数(テキストp.335) 

C言語には、多くの文字列を扱う関数がstring.hで定義されている。例えば以下のようなものがある。

(1) 文字列sの長さを返す(Null文字は含まない):size_t strlen(const char *s); // string.h

ここで、size_tは、typedef unsigned int size_t;として、unsigned int の別名(エイリアス)として定義されている型である。

(2) 文字列s2をs1にコピーする。返却値はs1:char *strcpy(char *s1, const char *s2); // string.h

(3) 2つの文字列の比較。返却値はs1=s2のとき0:int strcmp(char *s1, const char *s2); // string.h

(4) printfの出力先をchar型配列としたもの。返却値は書き込んだ文字数:int sprintf(char *s, const char *format, ...); //stdio.h

これらを用いた簡単なプログラムを以下に示す。

#include <stdio.h>
#include <string.h>

int main(void)
{
	char s1[50];
	char *s2 = "Hello World !";
	
	printf("文字列s2の長さは%dです。\n", strlen(s2));
	strcpy(s1, s2);
	printf("コピー後: s1 = %s\n", s1);
	
	printf("ファイル名の自動生成\n");
	for (int i=1; i < 4; i++){
	    sprintf(s1, "image%2d.tif", i);
		puts(s1);
	}
	
	return 0;
}

====== 演習問題 8A (p8a.c) ==============

3つのint型整数を降順に並べ替える関数 void sort3a( int *a, int *b, int *c) { /* */} を内部で他の関数を用いずに作成し、下記表示例のように3つの整数をキーボードから読み込んでそれを降順にモニター画面に表示するプログラムを作成せよ。 つまり、キーボードから3つのint型の値a, b, cを読みこんでsort3a(&a, &b, &c);を実行するとa>= b >= cという不等式が成立するように値が入れ替わり、それらの値を モニター画面上に以下の通りに表示するプログラムを作成せよ。ここで、イタリック体太字はキーボードから入力した値を示す。
===== 表示例 =====
3つの整数を入力してください。
整数A:1
整数B:3
整数C:2
降順にソートしました。
整数Aは3です。
整数Bは2です。
整数Cは1です。

======== 

====== 演習問題 8B (p8b.c) ==============

演習問題5Bで作成した0から10までの乱数の平均値と標準偏差を求めるプログラムを、乱数を格納する配列を宣言するdouble data[N];以外では配列の[ ]演算子を全く用いないで同じ計算を実行するように書き換えて下さい。

====== 演習問題 8C (p8c.c) ==============

C言語の標準ライブラリ関数には、文字列で表された数値をdouble型に変換する関数としてstdlib.hで定義されているatofがある(テキストp.302参照)。下記のプログラムはこの関数を用いたものである(数字は符号・小数点も含めて15文字以内に制限している)。これと同じ動作を行う関数my_atof( )を作成し、下記プログラムを自分が作成したmy_atof関数を用いるように書き換えよ。ただし、キーボードから文字列として入力する数値には以下の制限を設ける。
(1) 最初の文字は負の符号か0~9の数字に限られる。
(2) 必ず小数点を含む。
(3) 指数表現(1.0E-2のような)は用いない。 入力する文字列に含まれる文字は0から9までの数字か負の符号を表す-の記号と小数点のみであるとして良い。
(ヒント:符号を表す整数型変数を用意して、最初の文字が負の符号であれば-1として最後にこれを掛ける。小数点より前の部分と後の部分の2つに分けて考える。文字列を配列として扱って1文字ずつ処理する。文字の'0'と数字の0は異なるが、例えば文字の'7'は'7' - '0'を計算すれば数字の7となる。配列の要素を添え字番号順に数字に変換してその前に変換された数字に10を掛けてから加える操作を行っていくと整数に変換できる。小数点より後の部分は例えば3桁の整数123ならば1000で割れば小数部分の0.123となる。割る前に(double)でdouble型へ変換することを忘れないようにすること。)

#include  <stdio.h>
#include <stdlib.h>

int main(void)
{
	char str[16];
	
	printf("実数値を文字列として入力:");
	scanf("%s", str);
	
	printf("文字列として表示:%s\n", str);
	printf("浮動小数点型数値として表示:%f\n", atof(str));
	return (0);
}

====== 以上 ==============