CONTENTS / BACK-PAGE / NEXT-PAGE

5.2 ポインタ

5.2.1 ポインタと格納場所

 ポインタは、C言語の特徴を生かした有用な機能である。ポインタとは英語で「指し示す」という意味があり、プログラミング言語でも値が格納されている場所を指し示す時に用いられる。すでに学んだ変数への代入式 a=10; は、a という名前のついた場所に値10を格納することを意味していた。これは、計算機の中ではメモリの例えば1000番地に "a" という表札がかけてあり、そこに値10が格納されていることになる。C言語ではこの1000番地を指し示すための変数が用意されており、値が格納されている場所を直接操作することができる。これをポインタ変数と呼ぶ。図5−1にポインタの概念を示そう。

図5−1 ポインタの概念


 図5−1はポインタ変数 pointer_a が値10が格納されている場所、つまり1000番地を指している例である。ポインタ変数も当然のことながらメモリ内では、番地をもっていてここではxxxx番地である。ポインタとして宣言された変数は、格納番地しか扱うことができないので注意すること。簡単なプログラム5−4によりポインタの使い方を学ぼう。

プログラム5−4

/*  1 */  /*  Program No.5-4  */
/*  2 */  #include <stdio.h>
/*  3 */
/*  4 */  main()
/*  5 */  {
/*  6 */      int x = 100,  v[10];
/*  7 */      int *pt1, *pt2;
/*  8 */
/*  9 */      pt1 = &x;
/* 10 */      *pt1 += 1;
/* 11 */
/* 12 */      printf(" x = %d\n", x);
/* 13 */
/* 14 */      pt2 = &v[0];
/* 15 */      *pt2 = *pt1;
/* 16 */
/* 17 */      printf(" v[0] = %d\n", v[0]);
/* 18 */
/* 19 */  }

実行結果

x = 101 v[0] = 101

***解説***

6:int型変数xとint型配列変数v(0から9までの10個の要素を格納可能)の宣言である。xは100で初期化されている。
7:変数名の前に*を付けることにより、pt1とpt2がポインタ型の変数であることを宣言する。先頭のintは、pt1とpt2がint型のデータが格納されている場所を指すことを意味するものである。intの代わりにfloatやcharにすれば、float型のデータやchar型のデータが格納されている場所を指すことを意味する。
9:&変数名で変数に割り当てられたメモリ内の番地を表現する。この場 合、変数xに割当られた番地そのものが、ポインタ変数pt1に格納される 。
10:前節で学んだ代入演算子もポインタ変数に使えることを示した例である。この文を書き換えると、
*pt1 = *pt1 + 1
のようになる。*変数名により、ポインタ変数が指している場所に格納されている内容を意味することになり、この場合、pt1が指している場所(つまり変数x)の内容に1を加え、その結果をpt1が指している場所に格納する。10:の出力結果をみるとわかるように、9,10:の2文で x+=1 をxという表札を使わないで番地を直接利用して実行したことになる。よって10:の結果は100+1で101になる。
14:配列変数v[0]に割り当てられたメモリ内の番地をpt2に代入する。
15:pt1の指している場所(この場合x)の内容をpt2の指している場所に格納する。17:の出力結果からもわかるように、14,15:でv[0]=xを行なったことになり、結果としてxの値101が出力される。

ここで、

     (1) pt2=pt1;
     (2) *pt2=*pt1;
     (3) pt2=&pt1;

の違いを明確に理解しておこう。(1)の場合は、番地そのものをコピーしているのでpt1とpt2が同じ格納場所を指すことになる。(2)の場合は、pt1とpt2がそれぞれ指している場所に格納されているデータの値が等しい。

(3)の場合は、pt1に割り当てられているメモリ内の番地をpt2に代入することになり、プログラム5−4ではpt2はint型のデータが格納されている番地を格納するように7:で宣言されているため、ワーニング(アドレスはint型であるため)となってしまう。(3)のpt2はint型のデータを指すポインタのポインタであるため、 int **pt2; のように宣言してやればよいことになる。

***注意***

 ポインタ変数にも++演算子が使える。++(*pt1)とすれば、pt1の指す場所のデータの内容が+1される。++pt1とすると、pt1番地の次の番地を指すことになる。これについては、次節述べる。


つぎの例5−5によりさらにポインタの理解を深めることができるであろう。


例5−5 2つの変数の内容を入れ換える関数swapを定義せよ。

プログラム5−5

/*  1 */  /*  Program No.5-5  */
/*  2 */  #include<stdio.h>
/*  3 */
/*  4 */  void swapf(float *x, float *y);
/*  5 */
/*  6 */  main()
/*  7 */  {
/*  8 */      float a, b;
/*  9 */
/* 10 */      scanf("%f %f", &a, &b);
/* 11 */      printf(" a = %f   b = %f\n", a, b);
/* 12 */      swapf( &a, &b );
/* 13 */      printf(" a = %f   b = %f\n", a, b);
/* 14 */  }
/* 15 */
/* 16 */  void swapf(float *x, float *y)
/* 17 */  {
/* 18 */      float tmp;
/* 19 */
/* 20 */      tmp = *x;
/* 21 */      *x = *y;
/* 22 */      *y = tmp;
/* 23 */  }

実行結果

123
987
 a = 123.000000   b = 987.000000
 a = 987.000000   b = 123.000000

***解説***

 8:swap関数の呼び出し文である。&a、&bつまりaとbの番地を 引数として関数swapへ渡すことになる。これは、例4−10で解説した 番地呼び出しにあたる。
16:値を戻さない関数(void型)のヘッダ部である。*x,*yはfloat型のデータを呼び出すポインタ引数として宣言されている。x,yがポインタ型の引数であるため、番地を直接操作することにより、値を戻すことができるようになる。
20:変数tmpにxの指す内容のデータをコピーする。
21:変数yの指している内容をxの指している場所にコピーする。
22:tmpの値をyの指している場所へコピーする。

***注意***

 swap関数は、値を戻さない関数であるからポインタ(ここでは、a,bの番地)を関数に渡し、関数内で渡された番地の内容を変更していることになる。swap内では、番地そのものが変更されているのではなく、内容だけが変更されている。この場合、mainのa,bの番地とswapのx,yの番地がそれぞれ共有されていることになる。このため、swapは値を戻さない関数(void型)であるが、結果としてmainのa,bの値が入れ替わることになる。


 プログラム4−10−2をもう一度みてみよう。上の説明が理解できれば、プログラム4−10−2での解説が納得いくことであろう。


5.2.2 配列とポインタ

 配列は、計算機の内部では連続した番地のメモリを確保するようになっている。例4−11で配列をfunctionの引数にした時のことを思いだしてみよう。引数として配列の先頭番地つまり &配列名 としてfunctionを呼び出した。これは、連続した領域を配列として確保しているため、先頭の番地のみを渡してやれば、mainとfunctionでメモリを共有できたのである。実はこの段階でポインタの考え方を用いていたのである。C言語では、配列とポインタは同等に扱うほどに密接な関係がある。(配列はポインタであると言ってもよいくらいである。)配列は添字を操作することによって利用できたが、これをポインタを使っても全く同じ操作が可能になる。例えば、配列に格納されている数値を順に出力する簡単なプログラムを考えてみよう。プログラム5−6−1とプログラム5−6−2を比べることにする。

プログラム5−6−1

/*  1 */  /*  Program No. 5-6-1  */
/*  2 */  #include<stdio.h>
/*  3 */
/*  4 */  main()
/*  5 */  {
/*  6 */      int v[10] = {0,1,2,3,4,5,6,7,8,9}, i;
/*  7 */
/*  8 */      for(i = 0; i <= 9; i++)
/*  9 */          printf("%d ", v[i]);
/* 10 */      printf("\n");
/* 11 */  }

実行結果

0 1 2 3 4 5 6 7 8 9

プログラム5−6−2

/*  1 */  /*  Program No. 5-6-2  Pointer Version  */
/*  2 */  #include<stdio.h>
/*  3 */
/*  4 */  main()
/*  5 */  {
/*  6 */      int v[10] = {0,1,2,3,4,5,6,7,8,9}, i, *p;
/*  7 */
/*  8 */      p = &v[0];
/*  9 */      for(i = 0; i <= 9; p++, i++)
/* 10 */          printf("%d ", *p);
/* 11 */      printf("\n");
/* 12 */  }

実行結果

0 1 2 3 4 5 6 7 8 9

***解説***

8:配列vのメモリ内の領域の先頭番地をpに代入する。
9:iが0から9までつまり10回 10:を繰り返す。for文の第3パラメータp++は、ポインタ変数をインクリメント(1加えることを)している。この場合p++は、1番地先を指す。正しくは、1つ先の領域を指すことになる。
 例えば、初めのpはv[0]を指している。p++を1度実行されると、pはv[1]wp指し、さらにp++を実行するとpはv[2]を指すことになる。
10:pの指す内容つまりv[i]の内容を出力する。

***注意***

 8,10:を次のプログラム5−6−2’のように変更しても同じ結果が得られる。


プログラム5−6−2’

/*  1 */  /*  Program No. 5-6-2'  Pointer Version  */
/*  2 */  #include<stdio.h>
/*  3 */
/*  4 */  main()
/*  5 */  {
/*  6 */      int v[10] = {0,1,2,3,4,5,6,7,8,9}, i, *p;
/*  7 */
/*  8 */      p = &v[0];
/*  9 */      for(i = 0; i <= 9; i++)
/* 10 */          printf("%d ", *(p+i));
/* 11 */      printf("\n");
/* 12 */  }

実行結果

0 1 2 3 4 5 6 7 8 9

 この場合、9:の*(p+i)は次のような意味を持つ。

     iが0の時、ポインタはpでv[0]の番地を指す。
     iが1の時、ポインタはp++でv[1]の番地を指す。
     iが2の時、ポインタは(p++)++でv[2]の番地を指す。
     iが3の時、ポインタは((p++)++)++でv[3]の番地を指す。
       :             :        :
       :             :        :

 よって、プログラム5−6−1,5−6−2さらに5−6−2’のように異なった命令でも同じ実行結果が得られることがわっかたであろう。これらの関係を次の図で示す。

 ポインタ変数pにp++やp+2を行なうことは、整数iにi++やi+2を行なうことと意味が異なる。整数の場合、実際にiの内容が1または2増える。しかし、ポインタの場合、pが次の領域またはそのまた次の領域を指すのであって、pの内容そのものが1または2増えるのではない。実際には、一つの領域分のバイト数だけ加算される。

 プログラム5−4のpt1はint型のポインタ変数であるため、int型の領域分だけ先をさすことになる。以下のようなプログラムの一部について考えてみよう。

    int   v_i[10], *pi;
    float v_f[10], *Pf;

    pi = &v_i[0];
    pf = &v_f[0];
    ++pi;
    ++pf;
piとpfはそれぞれint型,float型のポインタ変数である。

 計算機の環境によって異なるが、int型の場合には16bit,float型の場合には32bitの領域を確保し、番地の振り方は8bitで1番地であると仮定しよう。このとき、上記プログラムでは、

   1000番地 v_i[0],  2000番地 v_f[0]
   1002番地 v_i[1],  2004番地 v_f[1]
     :               :
     :               :
   1018番地 v_i[9],  2036番地 v_f[9]
であるとする。float型の場合は、int型の倍の領域を確保しているため番地の割り付けも2倍になっていることに注意しよう。このような条件のもとで上記プログラムを実行すると、piはv_i[1]の番地つまり1002,pfはv_f[1]の番地つまり1004が格納される。同じ++演算を行なっても結果的には格納領域のバイト数(計算機に依存するが、例えばchar型は1バイト,int型は2バイト,float型は4バイト)分、加えられる。これは、++演算子はポインタ変数の型(int型のデータを指すのか?float型データを指すのか? など)を考慮し、次の番地を指すからである。

 以上の理由から、ポインタ変数はただ番地を格納するだけのものなのに、格納先データの型(char型を指すのか? int型を指すのか? float型を指すのか?)の指定が必要なのである。


5.2.3 文字列とポインタ

 5.2.2節でポインタは、連続した格納領域の先頭番地を与えることにより配列と同様の機能を果たすことを学んだ。このことを全く同様に文字の配列に適用すると、文字列の扱いが容易になる。プログラム4−8では、文字列を

   char string1[80];

のように宣言し、

   string1[0] = ’S’;
   string1[1] = ’a’;
   string1[2] = ’y’;
   string1[3] = ’u’;
   string1[4] = ’r’;
   string1[5] = ’i’;
   string1[6] = ’¥0’;
のように1つの配列要素に1文字を格納し、その上で操作していた。

 ポインタ変数を用いると同じ操作ができる。プログラム5−7で示そう。


プログラム5−7

/*  1 */  /*  Program No. 5-7  */
/*  2 */
/*  3 */  #include<stdio.h>
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      char  *string1 = "Sayuri & Masahiko";
/*  8 */      char  string2[] = "Zoh and Zoh'";
/*  9 */      int i;
/* 10 */
/* 11 */      while(*string1 != '\0')
/* 12 */      {
/* 13 */          printf("%c", *string1);
/* 14 */          string1++;
/* 15 */      }
/* 16 */      printf("\n");
/* 17 */
/* 18 */      for(i = 0; string2[i] != '\0'; i++ )
/* 19 */          printf("%c", string2[i]);
/* 20 */      printf("\n");
/* 21 */  }

実行結果

Sayuri & Masahiko
Zoh and Zoh'

***解説***

7:string1を文字のポインタとして宣言し、初期値として文字列 "Sayuri & Masahiko" が格納されている先頭番地が格納されている。いままで文字の代入にはシングルクオーテーション’’を用いていたが、文字列の代入にはダブルクオーテーション””を用いる。””を用いると文字列の最後を示す’¥0’記号が自動的に挿入される。
8:string2を文字の配列として宣言し、初期値として文字列 "Zoh and Zoh'" が格納されている。ここでも文字列の最後に’¥0’が挿入されている。
11〜16:string1が指している文字列の内容を出力する。
11:ポインタstring1の指す内容が’¥0’になるまで10,11を繰り返す命令である。
13:string1の指す内容の文字1つを出力している。
14:ポインタを1つ先の番地に進めている。
18〜20:文字配列string2の出力をする。

***注意***

  • 11〜16:は printf("%s\n", string1); のように書き換えられる。
  • 18〜20:は printf("%s\n", string2); のように書き換えられる。
  • string1とstring2の根本的な違いを下記の図で示す。

 string1はポインタであるから文字列が格納されている先頭番地のみを持っている。そのため文字列の長さには関係なく文字列を扱うことができる。しかし、文字列の内容を換えようとするとメモリ内の状況が不定なので保障されないため注意が必要である。このように文字列のポインタは、あらかじめ作られた文字列の先頭番地を指すような使い方をするべきである。また、string1はポインタ変数であるから、格納される番地はstring1++のように変化させてよい。

 string2は、初期化された文字列と¥0が格納された領域が確保された先頭番地が格納されている。この場合11文字(バイト)分が保証され、その中は自由に書き換えても問題はない。string2はこの11バイトの先頭番地が格納されていることと同等であるが、この番地は絶対変わることがない。

 文字列,配列,ポインタの関係がおわかりいただけましたか? それでは、演習をやってみましょう。


演習問題

 2つの文字列を受け取り、それらを1つの文字列として結合をする関数を作れ。ポインタで宣言した場合と、配列で宣言した場合の両方について考えてみよう。


 上ではポインタについての解説を行いました。計算機の記憶領域の取り方や計算機内部の動きにも関連があるため、ポインタの考え方そのものが難しいのです。大学で、プログラム言語の講義,演習を行なっても大半の学生がこのポインタでつまずきます。みなさんからの質問や反応から、ポインタの基本的な考え方についてもう少し時間をかけるべきだと考え、今回はポインタを使った簡単な例をいくつか示し、そこからポインタについて習得して頂きたいと思います。以下では今までのレクチャーのつづきを、次のページで補足説明としてポインタについて詳しく解説します。

 また、上で出した文字列の結合に関する演習問題は、ポインタの指す記憶領域の確保を行なわなければならず、前回の演習問題としては不適当でした。記憶領域の確保については、もうしばらく先に行なう予定です。申し訳ございませんでした。


5.2.4 ポインタを用いた簡単な例

 ここでは、今まで学んだことをポインタを用いてできるまたはポインタを用いた方が考え方がスマートであることをいくつかの例題から学ことにする。各例題ともポインタと配列の両方をについて示し、比較してみよう。まず、ごく簡単な例から示そう。


例5−8 n個のデータの平均を求める関数を作る。

プログラム5−8−1 平均を求める。(ポインタ版)

/*  1 */  /*   program 5-8-1  (pointer ver.) */
/*  2 */  #include <stdio.h>
/*  3 */  main()
/*  4 */  {
/*  5 */      float  heikin(int *d, int n);
/*  6 */
/*  7 */      int    data[100], n;
/*  8 */
/*  9 */      for (n = 0; n < 100; n++)
/* 10 */          data[n] = 0;
/* 11 */      n = -1;
/* 12 */      do{
/* 13 */         n++;
/* 14 */         scanf("%d", &data[n]);
/* 15 */      }while(data[n] > 0);
/* 16 */      printf("Heikin =%f\n", heikin(data, n));
/* 17 */  }
/* 18 */
/* 19 */  float heikin(int *pdata, int n)
/* 20 */  {
/* 21 */      float  sum = 0.0;
/* 22 */      int i;
/* 23 */
/* 24 */      for(i = 0; i < n; i++)
/* 25 */          sum += (float)*pdata++;
/* 26 */
/* 27 */        return(sum / (float)n);
/* 28 */  }

実行例

1 2 3 4 5 6 7 8 9 0 Heikin =5.000000

***解説***

9〜10:int型の配列要素100個をすべて0で初期化する。データの初期化を必ず行なうことを奬める。初期化は、プログラマが利用する領域の確保の再確認をするとともに、万一に備えてデータをクリアしておくとよい。C言語では、配列の添字のチェックつまり配列dataの領域を操作している時、配列の添字を越えた領域外を参照した場合、逆に領域外のデータを参照している時にdataの領域を犯すような操作をした場合、エラーメッセージを出してはくれない。このような領域のチェックはプログラマの責任において行なわれなければならない。そのため、何が起きてもプログラムが暴走しないようにするための1つの手段として、データの初期化は大切である。
11〜15:0以上の数値のみをデータとして扱うこととし、負の数値が入力されるまで、データの読み込みをする。つまり負の数は入力データの終了を示すことになる。
14:scanfのパラメータである&data[n]は、入力されたデータの格納番地を示している。この命令は、dataという配列の先頭番地を指すポインタからn個先の番地(ここでは&data[n]と書かれている)に整数型として読み込んだデータ(%d指令で整数型として読み込まれる)を格納することを意味している。このことは、 ポインタ=配列 と言われる1つの理由でもある。後の解説(No.14のつづき)で詳しく述べている。
16:データの格納されている配列の先頭番地とデータの個数nを渡して、データの平均値がfloat型で戻される。 printf("Heikin =%f\n", heikin( data, n ) ); は、printf("Heikin =%f\n", heikin( &data[0], n ) ); と記述してもよい。配列を関数引数として用いる場合、番地呼び出ししかできず、この場合もdataの格納されている先頭番地を渡している。データの個数nは、値を渡しているので値呼び出しになっている。
19: pdataをint型データが格納されているポインタ型として宣言し、配列dataの先頭番地を受け取る。nは値そのもの受け取っている。
21:総和を求めるための変数sumを宣言し、同時に0.0で初期化する。
24〜25:n個のデータの総和をsumに求める。この2行は、
                for( i=0; i<n; i++ )
                {
                        sum = sum + (float)*pdata;
                        pdata++;
                }
 と記述したのと同等である。*dataでdataが指すアドレスの内容を意味し、それを(floae)でキャストしている。つまり、int型のデータを一時的にfloat型に変換し代入を行なっている。これは、sumはfloat型であるため、同一の型どうしの演算を行なうためにキャストしている。

data++は、現在dataが指すアドレスを1つ先に進める。これにより配列のデータを次々に参照することを可能にしている。演算の実行の優先順位により、上記の命令が25:一文で書ける。

27:平均=総和/データの数 であるから、sumをnで割った値を関数heikinの値としてmainに戻している。nで割る前にnを一時的にfloat型にキャスト(float)して同一型どうしの演算を実現している。

プログラム5−8−2 平均を求める。(配列版)

/*  1 */  /*   program 5-8-2  (array ver.)  */
/*  2 */  #include <stdio.h>
/*  3 */  main()
/*  4 */  {
/*  5 */      float  heikin(int *d, int n);
/*  6 */
/*  7 */      int    data[100], n;
/*  8 */
/*  9 */      for (n = 0; n < 100; n++)
/* 10 */          data[n] = 0;
/* 11 */      n = -1;
/* 12 */      do{
/* 13 */          n++;
/* 14 */          scanf("%d", &data[n]);
/* 15 */      }while(data[n] > 0);
/* 16 */      printf("Heikin =%f\n", heikin(data, n));
/* 17 */  }
/* 18 */
/* 19 */  float heikin(int adata[], int n)
/* 20 */  {
/* 21 */      int    i;
/* 22 */      float  sum = 0.0;
/* 23 */
/* 24 */      for(i = 0; i < n; i++)
/* 25 */          sum += (float)adata[i];
/* 26 */      return( sum / (float)n );
/* 27 */  }

実行例

1
2
3
0
Heikin =2.000000

***解説***

プログラム5−8−1との違う点のみについて解説する。
19:mainの配列dataの先頭番地がadataに渡され、添字による操作を行なう宣言としてadata[ ]のように[ ]をつけて宣言する。
25:19での宣言により今回はデータの参照を、添字を使ってadata[i]とすることができる。ポインタで受け取った場合(プログラム5ー8ー1)は、*pdataで内容を参照し、pdata++で次のデータを指すようにしていた。配列として宣言した場合、添字のiを変化させることにより、adata[i]で内容を参照できる。プログラム5−8−1もプログラム5−8−2もともに配列dataの先頭番地が渡され、受ける方も番地を受け取っている。ただ、変数の宣言でポインタか配列の違いによってプログラムの記述方法が少し変わる以外は全く同じである。

プログラム5−8−3 平均を求める。(ポインタ版2)

/*  1 */  /*   program 5-8-3  (pointer special ver.) */
/*  2 */  #include <stdio.h>
/*  3 */  main()
/*  4 */  {
/*  5 */      float  heikin(int *d);
/*  6 */
/*  7 */      int    data[100], n;
/*  8 */
/*  9 */      for (n = 0; n < 100; n++)
/* 10 */          data[n] = 0;
/* 11 */      n = -1;
/* 12 */      do{
/* 13 */         n++;
/* 14 */         scanf("%d", &data[n]);
/* 15 */      }while(data[n] > 0);
/* 16 */      printf("Heikin =%f\n", heikin(data));
/* 17 */  }
/* 18 */
/* 19 */  float heikin(int *pdata)
/* 20 */  {
/* 21 */      float  sum = 0.0;
/* 22 */      int n = 0;
/* 23 */
/* 24 */      while(*pdata > 0)
/* 25 */      {
/* 26 */          sum += (float)*pdata++;
/* 27 */          n++;
/* 28 */      }
/* 29 */
/* 30 */        return(sum / (float)n);
/* 31 */  }

実行例

2
4
6
8
0
Heikin =5.000000

***解説***

19:平均を求める関数に、より汎用性を持たせるためにデータの個数を関数内で調べ、個数nを関数の引数に用いない方法がプログラム5−8−3である。ここでは、ポインタとしてpdataを宣言しているので、計算の仕方や記述方法はプログラム5−8−1と同じである。プログラムの仕様である、非負の整数値をデータとして扱い負の数値が入力データの終わりを示すデリミッタであることを関数の内部では利用する。
24〜27:sumに総和を求めると同時に、データの数をカウントしている。このとき、*pdata>0 が繰り返しの判定に用いられ、負の数値が出現するまで、繰り返される。

***注意***

 一般にポインタを用いるのはデータの数が不定である時、必要以上の領域をあらかじめ確保しなくてよい点で有効である。そのため、一般にはプログラム5ー8ー3のような関数にした方がよいと考えられる。しかし、今回の場合は、mainであらかじめdata[100]の領域が確保されているので、データ入力時にデータの個数を同時に求め、関数の引数としてnを渡す方がよいと考えられる。


 BASICを学んだ方達は、文字列操作を簡単に行なってきたであろう。そこで、次のような文字列操作の関数を作ると便利である。

例5−9

 与えられた文字列の中から、指定された部分の文字列(左からm文字目からn文字)を取り出す関数 midstr( instring, m, n, outstring ) を作る。ここで、instringは入力された文字列を、outstringは抽出された文字列が戻ってくるための領域である。

   instring  programming\0
     |
     | midstr( instring, 4, 5, outstring );
     ↓
   outsting  gramm\0

プログラム5−9−1 文字列の抽出 (ポインタ版)

/*  1 */  /*   program 5-9-1  (pointer ver.) */
/*  2 */  #include <stdio.h>
/*  3 */  #include <string.h>
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      void midstr(char *instring, int m, int n, char *outstring );
/*  8 */
/*  9 */      char  instring[80], outstring[80];
/* 10 */
/* 11 */      scanf("%s", instring);
/* 12 */      midstr(instring, 4, 5, outstring);
/* 13 */      printf(" %s :midstr(4,5) --> %s \n", instring, outstring);
/* 14 */  }
/* 15 */
/* 16 */  void midstr(char *pinstring, int m, int n, char *poutstring )
/* 17 */  {
/* 18 */      int i;
/* 19 */
/* 20 */      if( m<strlen(pinstring) )
/* 21 */      {
/* 22 */           for(i = 0; i < m - 1; i++)
/* 23 */               pinstring++;
/* 24 */           for(i = 0; i < n && *pinstring != '\0'; i++)
/* 25 */               *poutstring++ = *pinstring++;
/* 26 */      }
/* 27 */      *poutstring = '\0';
/* 28 */  }

実行例

PrOgRaMmInG
 PrOgRaMmInG :midstr(4,5) --> gRaMm

***解説***

11:文字列を入力する。%sで文字列として読み込まれ、instringの指すメモリの位置から入力文字列を格納する。9の宣言からinstringはchar型の80個の要素を持つ配列である。scanfのパラメータとして、その配列の先頭番地をinstringを与えている。この場合、&instring[0]としても同じで、やはり配列の先頭番地を指している。

***注意***

 scanfの第2パラメータは、アドレスを書かなければならない。そのため、プログラム5−9−1では、instringをそのまま書けばよいことになる。これは、instringは配列の先頭番地を意味するからである。しかしint型で宣言された変数の場合、アドレスを示すために&変数名とわざわざ&を付ける必要がある。

16:char型を指すポインタpinstring, poutstring に文字列の先頭番地が渡される。
20:入力文字列の長さが、mより大きいときには、文字列の最後を示す\0のみを戻すように21〜26を飛ばしている。
22〜23:入力文字列の初めからm文字先をポインタpinstringが指すように、m回pinstring++ を行なう。
24〜25:入力文字列のm番目から、n個poutstringへコピーする。この2文は、
        for(i = 0; i < n && *pinstring != '\0'; i++)
        {
            *poutstring = *pinstring;
             poutstring++;
             pinstring++;
        }
と書き換えられる。pinstring, poutstring ともにアドレスであるため、その内容を指すために *inrtsing, *outstring のように操作する。
27:文字列の最後は、\0が必要なため*poutstringに \0 を代入している。


プログラム5−9−2 文字列の抽出 (配列版)

/*  1 */  /*   program 5-9-2  (array ver.) */
/*  2 */  #include <stdio.h>
/*  3 */  #include <string.h>
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      void midstr(char instring[], int m, int n, char outstring[]);
/*  8 */
/*  9 */      char  instring[80], outstring[80];
/* 10 */
/* 11 */      scanf("%s", instring);
/* 12 */      midstr(instring, 4, 5, outstring);
/* 13 */      printf(" %s :midstr(4,5) --> %s \n", instring, outstring);
/* 14 */  }
/* 15 */
/* 16 */  void midstr(char ainstring[], int m, int n, char aoutstring[])
/* 17 */  {
/* 18 */      int i, l;
/* 19 */
/* 20 */      l = strlen(ainstring);
/* 21 */      for(i = 0; (i < n) && (i < l - m + 1); i++)
/* 22 */                aoutstring[i] = ainstring[i + m - 1];
/* 23 */      aoutstring[i] = '\0';
/* 24 */  }

実行例

programming
 programming :midstr(4,5) --> gramm

***解説***

16:配列の先頭番地として、ainstring[], aoutstring[] が受け渡される。
20〜23:プログラム5−9−1と同じ処理を行なっている。この場合、配列として宣言されているため添字を扱うことができ、一度でm番目の文字を参照できる。ポインタの場合プログラム5−9−1の22〜23で行なったようにポインタをm回先へ進ませないと、m番目の文字が参照できない点が異なっている。


例5−10

 与えられた文字列を逆転して戻す関数revstrを作る。
   instring  reverse\0
    |
    |
    ↓
   outstring  esrever\0

プログラム5−10−1 文字列の反転 (配列版)

/*  1 */  /*   program 5-10-2  (array ver.)  */
/*  2 */  #include <stdio.h>
/*  3 */  #include <string.h>
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      void revstr(char st1[], char st2[]);
/*  8 */
/*  9 */      char instring[80], outstring[80];
/* 10 */
/* 11 */      scanf("%s", instring);
/* 12 */      revstr(instring, outstring);
/* 13 */      printf("%s :revstr --> %s \n", instring, outstring);
/* 14 */  }
/* 15 */
/* 16 */  void revstr(char st1[], char st2[])
/* 17 */  {
/* 18 */      int i, n;
/* 19 */
/* 20 */      n = strlen(st1);
/* 21 */      n--;
/* 22 */      for(i = 0; i <= n; i++)
/* 23 */          st2[i] = st1[n - i];
/* 24 */      st2[i]='\0';
/* 25 */  }

実行例

reverse
reverse :revstr --> esrever

***解説***

20:文字列の長さを調べる関数strlenを用いる。
21:strlen関数は、文字列の最後を示す \0 もカウントしているため、実際の文字数は、そこから1つ減らさなければならない。

プログラム5−10−2 文字列の反転 (ポインタ版)

/*  1 */  /*   program 5-10-2  (pointer ver.) */
/*  2 */  #include <stdio.h>
/*  3 */  #include <string.h>
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      void revstr(char *st1, char *st2);
/*  8 */
/*  9 */      char instring[80], outstring[80];
/* 10 */
/* 11 */      scanf("%s", instring);
/* 12 */      revstr(instring, outstring);
/* 13 */      printf("%s :revstr --> %s \n", instring, outstring);
/* 14 */  }
/* 15 */
/* 16 */  void revstr(char *st1, char *st2)
/* 17 */  {
/* 18 */      int  i, n;
/* 19 */
/* 20 */      n = strlen( st1 );
/* 21 */      n--;
/* 22 */      for( i=0; i<n; i++ )
/* 23 */          st1++;
/* 24 */      for( i=0; i<=n; i++ )
/* 25 */          *st2++ = *st1--;
/* 26 */      *st2 = '\0';
/* 27 */  }

実行例

REVERSE
REVERSE :revstr --> ESREVER

***解説***

 プログラム5−10−1をそのままポインタで書き換えたものがプログラム5−10−2になる。
22〜23:st1の文字列の最後の文字(\0 の手前の文字)の所まで、ポインタを動かす。
24〜25:st1からst2へコピーされる。この際、st1のポインタは前に向かって、st2のポインタは後ろへ向かってコピー終了後にポインタが動く。

プログラム5−10−3 文字列の反転 (ポインタ版)

/*  1 */  /*   program 5-10-3  (pointer ver.) */
/*  2 */  #include <stdio.h>
/*  3 */  #include <string.h>
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      void revstr(char *st1, char *st2);
/*  8 */
/*  9 */      char instring[80], outstring[80];
/* 10 */
/* 11 */      scanf("%s", instring);
/* 12 */      revstr(instring, outstring);
/* 13 */      printf("%s :revstr --> %s \n", instring, outstring);
/* 14 */  }
/* 15 */
/* 16 */  void revstr(char *st1, char *st2)
/* 17 */  {
/* 18 */      char *d;
/* 19 */
/* 20 */      d = st1;
/* 21 */      while(*st1 != '\0')
/* 22 */          st1++;
/* 23 */      st1--;
/* 24 */      while(st1 >= d)
/* 25 */          *st2++ = *st1--;
/* 26 */      *st2 = '\0';
/* 27 */  }

実行例

REVERSE
REVERSE :revstr --> ESREVER

***解説***

 プログラム5−10−2を文字列の長さが分からないときには、このようにしなければならない。さらに配列は、小さい添字はアドレス値が小さく、大きい添字はアドレス値が大きくなるように連続して確保されることを利用すると、文字列の長さをカウントせずに24:のようにアドレス値の大小関係を用い、文字の長さ分の繰り返しの回数を作ることができる。


プログラム5−10−4 文字列の反転 (ポインタ特別版)

/*  1 */  /*   program 5-10-4  (ponter special ver.)  */
/*  2 */  #include <stdio.h>
/*  3 */  #include <string.h>
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      void revstrsp(char *st);
/*  8 */
/*  9 */      char inoutstring[80];
/* 10 */
/* 11 */      scanf("%s", inoutstring);
/* 12 */      printf("%s :revstrsp --> ", inoutstring);
/* 13 */      revstrsp(inoutstring);
/* 14 */      printf("%s \n", inoutstring);
/* 15 */  }
/* 16 */
/* 17 */
/* 18 */  void revstrsp(char *st)
/* 19 */  {
/* 20 */      void swapchar(char *a, char *b);
/* 21 */
/* 22 */      char *dum;
/* 23 */
/* 24 */      dum = st;
/* 25 */      while(*dum++ != '\0');
/* 26 */      dum--;
/* 27 */      dum--;
/* 28 */      while(st < dum)
/* 29 */      {
/* 30 */          swapchar(st, dum);
/* 31 */          st++;
/* 32 */          dum--;
/* 33 */      }
/* 34 */  }
/* 35 */
/* 36 */  void swapchar(char *a, char *b)
/* 37 */  {
/* 38 */      char c;
/* 39 */
/* 40 */      c = *a;
/* 41 */      *a = *b;
/* 42 */      *b = c;
/* 43 */  }

実行例

Special
Special :revstrsp --> laicepS

***解説***

 プログラム5−10−1,2,3とは引数の数が異なり、与えられた領域内で文字列の反転し、結果を同じアドレスに戻す方法である。mainからみると一つの配列変数内で処理がされている。

24−27:ポインタdumは、27が終了後には最後の文字(\0のすぐ前)を指している。 25が終了した時点では、dumは文字列終端を示す\0の次を指しているため、2回dum--を行い、最後の文字を指すようになっている。
28−34:2つのポインタstとdumによって入れ換えが行なわれる。stは文字列の終わりに向かって進み、dumは文字列に先頭に向かって進む。30は、二つのポインタの指す文字の入れ換えを行なう関数swapcharの呼び出しである。引数のstとdumに&がついていないことに注意すること。st,dumともにポインタ変数として定義されているため、プログラム5−5で定義したswapf関数の呼び出し時のように引数に&をつけなくても、st,dumはアドレスであることがわかっているからである。swapf関数の引数では、float型で宣言されていた変数のアドレスを渡しているため、&を付けていたのである。
36−43:二つのポインタの指す文字の入れ換えを行なう関数swapcharである。


例5−11

 10進数を文字列として受け取り、それを整数として戻す関数str_to_intを作る。


プログラム5−11 文字列を整数に変換する。

/*  1 */  /*  program  5-11   string to integer  */
/*  2 */  #include <stdio.h>
/*  3 */  main()
/*  4 */  {
/*  5 */      int str_to_int(char *moji);
/*  6 */        char   moji[20];
/*  7 */        
/*  8 */        scanf("%s", moji);
/*  9 */        printf("%d \n", str_to_int(moji));
/* 10 */  }
/* 11 */
/* 12 */  int str_to_int(char *moji)
/* 13 */  {
/* 14 */        int n, sign, value;
/* 15 */
/* 16 */      if( *moji == '-' )
/* 17 */      {
/* 18 */          sign = -1;
/* 19 */          moji++;
/* 20 */      }
/* 21 */      else sign = 1;
/* 22 */
/* 23 */      value = 0;
/* 24 */      while (*moji != '\0')
/* 25 */          if( '0' <= *moji && *moji <= '9' )
/* 26 */              value = value*10 + *moji++ - '0';
/* 27 */      return(sign * value);
/* 28 */  }

実行例

1235
1235

***解説***

 負の整数を意味する"−"の記号が文字列の先頭にある場合、負の数であることの判定として変数signに−1を代入する。文字列を数値に変換した後にこのsignの値をかけ算してやれば、負の数値も扱えるようになる。

 入力文字が、文字の"0"から"9"であれば数値に変換する。それ以外の文字は、無視するようになっている。( *moji - '0' )で文字に対応する数値を計算している。これは、文字コードを利用している。


例5−12

 10進数を整数として受け取り、文字列として戻す関数int_to_strを作る。


プログラム5−12−1 整数を文字列に変換する。

/*  1 */  /*  program  5-12-1   integer to string   */
/*  2 */ #include <stdi.h>
/*  3 */  main()
/*  4 */  {
/*  5 */          void int_to_str( int value, char *moji );
/*  6 */          int  value;
/*  7 */          char  moji[80];
/*  8 */
/*  9 */          scanf("%d", &value);
/* 10 */          int_to_str( value, moji );
/* 11 */          printf("%s \n", moji );
/* 12 */  }
/* 13 */
/* 14 */  void int_to_str( int value, char *moji )
/* 15 */  {
/* 16 */          int base, n;
/* 17 */
/* 18 */          if( value < 0 )
/* 19 */          {
/* 20 */                  *moji = '-';
/* 21 */                  value = -value;
/* 22 */                 moji++;
/* 23 */          }
/* 24 */
/* 25 */          for( base = 1; base<=value; base*=10 );
/* 26 */          if( value!=0)
/* 27 */                  base /= 10;
/* 28 */          while( 1<=base )
/* 29 */          {
/* 30 */                  n = (int)(value/base);
/* 31 */                  value %= base;
/* 32 */                  *(moji++) = n + '0';
/* 33 */                  base /= 10;
/* 34 */          }
/* 35 */          *moji = '\0';
/* 36 */  }

実行例

1234
1234

***解説***

18−23:負の整数に対する処理を行なっている。負の整数の時には、文字列の先頭に"−"を入れ、渡された数値に−1をかけ算して以後の処理を非負の場合と同じになるようにしている。
25−27:入力された数値の桁数をカウントし、変数baseに10の桁数乗を代入している。これは、後の処理で上位の桁から順に数値を1つづつ取るために行なっている。数値の桁数が少ない時、文字に変換すると数値の初めに"0"がついてしまう。この"0"をと取り除くためにも桁数のカウントは必要である。
28−35:入力数値の上位桁から順に1つづつ取り出し、対応する文字に変換している。この手法はよく用いられるため、覚えておくと便利である。

プログラム5−12−2 整数を文字列に変換する。

/*  1 */  /*  program  5-12-2   integer to string   */
/*  2 */  #include <stdio.h>
/*  3 */  #include "revstrsp.c"
/*  4 */
/*  5 */  main()
/*  6 */  {
/*  7 */      void int_to_str(int value, char *moji);
/*  8 */
/*  9 */        int  value;
/* 10 */        char  moji[80];
/* 11 */
/* 12 */        scanf("%d", &value);
/* 13 */        int_to_str(value, moji);
/* 14 */        printf("%s \n", moji);
/* 15 */  }
/* 16 */
/* 17 */  void int_to_str(int value, char *moji)
/* 18 */  {
/* 19 */        int sign;
/* 20 */        char *top;
/* 21 */
/* 22 */      top = moji;
/* 23 */        if( value < 0 )
/* 24 */        {
/* 25 */                value = -value;
/* 26 */                sign = -1;
/* 27 */        }
/* 28 */
/* 29 */      while(value / 10 > 0)
/* 30 */      {
/* 31 */          *moji++ = value % 10 + '0';
/* 32 */          value /= 10;
/* 33 */      }
/* 34 */      *moji++ = value + '0';
/* 35 */      if (sign == -1)
/* 36 */          *moji++ = '-';
/* 37 */      *moji = '\0';
/* 38 */      revstrsp(top);
/* 39 */  }

実行例

1234
1234

***解説***

 基本的な考え方は、プログラム5−12−1と同じであるが、桁数のカウントを行なわず、その代わりに数値の下位桁から順に文字に変換する方法である。そのために、処理の一番最後で文字列を反転させる関数revstrsp(プログラム5−10−4で用いたもの)を使っている。プログラム5−10−4で定義した関数revstrsp部分(18−43)をファイル"revstrsp.c"に保存しておき、3:の様に #include 文で宣言しておくと、コンパイル時に自動的に関数revstrspの本体を読み込んで、あたかもプログラム5−12−2のファイルにrevstrspの本体が宣言されているかのように、処理してくれる。このような処理は、プリプロセッサと呼ばれるコンパイルの前処理部が行なう。プリプロセッサについては、後に行なう予定である。


### ポインタについてのまとめ ###

◎変数の格納番地をポインタと呼び、その格納している変数をポインタ変数と呼ぶ。一般に、次のように宣言する。

◎文字列(文字配列)とポインタは、同等である。

◎配列へのポインタによる参照
 配列をポインタで参照することができる。 int a[3][4]; は、4個のint型の要素を3つ持った配列で、論理的には縦3,横4の2次元の升目で考えられる。

しかし、計算機内部ではこれらは物理的に続いた記憶域に取られる。

   int  *pa;
   pa = &a[0][0];  /*  または pa = a;  */
とすると、ポインタ変数 pa で配列 a[i][j] を参照することができる。例えば、*(pa+2) とすると a[0][2] が参照でき、*(pa+9) とすると a[2][2] が参照できる。また、
     pa = a[1];
とすると a[1] の最初の要素へのポインタになる。*(a[1]+2) とすると a[1] の2番目に要素つまり a[1][2] を参照することになり、さらに *(*(a+1) + 2) としても a[1][2] を参照することになる。

◎ポインタを要素に持つ配列
 上記のことを少し応用すると次のように、ポインタの配列を扱うこともできる。

 例
  char *s[4] = {"How ","are ","you","?" };

とすると、下図のような関係を作れる。


つづく

CONTENTS / BACK-PAGE / NEXT-PAGE