2022年8月、ホームページを全面リニューアルしました! 情報を分かりやすくお伝えできるサイト作りを目指してまいります。

C言語のポインターを理解するためのプログラムを作る!

C言語で難関といわれているポインターの概念。その解りにくいポインターの概念を解りやすい説明プログラムを作って、理解しようとする新米プログラマーのプレゼン資料を公開する。メモリ上のアドレスとアドレス内に格納されたデータとの関係やメモリ上のアドレスに格納されたデータが示すアドレスとの関係など図を駆使して解りやすく説明する。ポインターの概念がどうしても解らないという方は、是非参照すると良いと思う。


1.ポインタープレゼン(その1)

1)一般的にポインターとはなんぞや?
ある変数がメモリのどこに納められているかを示すアドレスを格納し、アドレスとして扱うための変数。つまり、ポインタが持っている値はメモリの位置情報と言える。そのため、アドレス変数と呼ばれることもある。いわゆる、ポインタを使うことで変数を介した間接的な方法ではなく、直接メモリを書き換える、読み出すという操作が可能になる。

2)フローチャート
図1にフローチャートを示す。

図1

3)ソース

#include <stdio.h>
int main(void)
{
   int x, *y, **z, ***u;                       
// int型の変数とint型へのポインタの宣言
   int ma[ ] = {1, 2, 3, 4, 5, 6};                  
// int型の配列の宣言
   int *p;                               
// int型の配列へのポインタ
   char str[ ] = “ABCDEF”;                   
// char型の配列の宣言
   char *c = “123456“, *d = she”;                
// char型の配列へのポインタの宣言(dはcとの比較のため宣言)
   x = 5;                                
// xに5を代入
   y = &x;                                
// yにxのアドレスを代入
   z = &y;                                
// zにyのアドレスを代入
   u = &z;                                
// uにzのアドレスを代入
   p = ma;                                
// pに配列ma[ 0 ]のアドレスを代入
   printf(“x= %d, &x = %d\n”, x, &x);              
// 画面に表示
   printf(“y= %d, &y = %d, *y = %d\n”, y, &y, *y);
   printf(“z= %d, &z = %d, **z = %d\n”, z, &z, **z);
   printf(“u= %d, &u = %d, ***u = %d\n\n”, u, &u, ***u);
   printf(“ma= %d, &ma[ 0 ] = %d\n”, ma, &ma[ 0 ]);
   printf(“p= %d, &p = %d\n”, p, &p);
   for (inti = 0; i < 6; i++)
    printf(“ma[%d] = %d, *(p + %d) = %d\n”, i, ma[i], i, *(p + i));
   putchar(‘\n’);
   c = str;                               
// cに配列str[ 0 ]のアドレスを代入
   printf(“c= %s, c = %d, &c = %d, *c = %x\n”, c, c, &c, *c);
   printf(“d= %s, d = %d, &d = %d, *d = %x\n\n”, d, d, &d, *d);
   return (0);
}
   printf(“str= %s, str= %d, &str[ 0 ] = %d\n”, str, str, &str[ 0 ]);
   printf(“c= %s, c = %d, &c = %d, *c = %x\n”, c, c, &c, *

4)実行結果
図2に、実行結果を示す。

図2

■ 結果の補足
&x = y, &y = z, &z = u
x = *y = **z = ***u
ma = &ma[0] = p
str= &str[0] = c(代入後)
変数のアドレスは先に宣言したものから順にメモリの中(下の方が数は大きい)に埋められていく。(図3参照)
*c = 31 , *c = 41 , *d = 73 はそれぞれ16進数で、「JISコード」照らし合わせると、それぞれ ”1” , ”A” , ”s” を表している。これは、c及びdの指す文字列の先頭の文字と一致する。c(代入前)を%sで表示させると ’123456’(文字列) だが、%dで表示させると ”4325444”(数値) となる。だが、この数値はいったいなんぞや。

5)メモリ内に格納されたデータとアドレスの関係

図3
図4

■ このプログラムを実行して解ったこと
.文字列へのポインタ *c を %s で表示すると “123456” と初期化した文字がでるが、%dで表示すると、 ”4325444” と表示される。これは宣言した変数のアドレスとまったく違う数である。
.しかし、もうひとつの文字列へのポインタ*dをみると、%d表示で ”4325404” となり、cの値に近いものが出た。
.この確認のために、初期化前の*cの100個後までのメモリ内を表示させ読み取ったところ、ソースファイルに打ち込んだものがいくつか発見できた。その中に、配列への初期化子も確認できた。
.この数値は、ポインタへの配列の代入と同じように考えれば、初期化子のメモリ内に置かれた位置ではないかと考えられる。また、この値は、初期化する文字や文字数によっても変化するので、ランダムにメモリ内(ある程度はまとまった位置)に置かれるものであった。

5)ポインターってこう言うものだ(このプログラムから解ったこと)
■ ポインタは何重にも指定できる。
■ ポインタは、ただ“○○番地を指す”のではなく、“○○番地を先頭に格納された××型のオブジェクトを指す”のである。したがって、配列に対してポインタを指定した場合、*(p ±α) (pのα個後ろ(もしくは前)の要素)で、配列の要素をそれぞれ指すことができる。
■ 配列名を“添字演算子[ ]”を付けずに呼び出すと、その配列の先頭要素へのポインタとみなされる。
■ メモリ内のアドレスの変化は、型の大きさによって埋まり方が違う。例えば、int型は4Byteで、char型は1Byte(環境(OSのbit数)が異なれば型の語調は変化する)であったので、アドレスへの対応もその通りであった。


2.ポインタープレゼン(その2)

1)きっかけになったサンプルプログラム

#include<stdio.h>
intmain(void)
{
  char a[25] = {“ABCDEFGHIJK”};
  char b[25] = {“LMNOPQRSTU”};
  char *pa, *pb;
  pa = a;
  pb= b;
  //aの配列にbの配列をコピーする
  while (*pa++= *pb++)
;
  //コピーした配列aを出力させる
  while (*pa){
    printf(“%c”, *pa);
    pa++;
  }
  putchar(‘\n’);
  return (0);
}


#include<stdio.h>
intmain(void)
{
  char a[25] = {“ABCDEFGHIJK”};
  char b[25] = {“LMNOPQRSTU”};
  char *pa, *pb;
  pa = a;
  pb= b;
  inti = 0;
  //aの配列にbの配列をコピーする
  while (*(pa + i)*(pb+ i))
  i++;
  i = 0;
  //コピーした配列aを出力させる
  while (*(pa + i) ) {
    printf(“%c”, *(pa + i));
  i++;
  }
  putchar(‘\n’);
  return (0);
}

このプログラムをきっかけに、以下の目的でポインターを理解するプログラムを作成した。
■ ポインタの仕組みを理解する。
■ ポインタを利用したアクセスの違いを理解する。(相対的、絶対的)
■ ポインタを利用した配列を理解する。

2)どんなプログラムを作ったのか
ポインタで実現する文字列の操作
■ 例1では、ポインタのアドレスを相対的に操作
■ 例2では、ポインタのアドレスを絶対的に操作

3)フローチャート

4)プログラム(例1、例2)

#include <stdio.h>
int main(void)
{
/***** 例1の部分 *****/ /*ポインタのアドレスを相対的に操作*/

  char *mnthp[3] = {                  /* ポインタの配列の宣言*/
     “January”, “February”, “March”};
  char **p1, **p2;                   
/* 「ポインタのポインタ」の宣言*/
  inti;
  p1 = p2 = mnthp;                   
/* 「ポインタのポインタ」にポインタの配列*/
                                
/* の先頭番地を設定*/

  for (i = 0 ; i < 3 ; i++) {
    printf(“%s\n”, *(p1 + i));
                                    /* ポインタの値を操作し相対的に文字列を出力*/
  }
/***** 例2の部分 *****/ /*ポインタのアドレスを絶対的に操作*/
  for (i = 0 ; i < 3 ; i++) {
    printf(“%s\n”, *p2);
                                    /* ポインタの値を更新して絶対的に文字列を出力*/
  ++p2;
  }
  return(0);
}

5)今回のプログラムを作って解ったこと
■ 相対的、絶対的ということの意味

6)配列とメモリアドレスとポインタとメモリ内のデータとの関係

7)例1と例2が指すアドレスの比較

8)このプログラムから解ったこと
■ 相対的な変化ではポインタ自体が指すアドレスは変化しないのに対し、絶対的な変化ではポインタ自体が指すアドレスが変化する。
■ アドレスの変化を頭に入れた処理の仕方を考えなければ、思ってもいなかった値の相違や、エラーが起こる場合があるので注意しなければならない。
■ エラーを起こさないためにも、今回のような相対的な変化と絶対的な変化の両方の違いを理解し、どちらを使うか検討することが大切になると考える。

以上

コメント