2012/04/26

C言語 ポインタ pointer

難解と言われているポインタ。通常の変数と違って、任意の変数のアドレスを格納するのがポインタ。アドレスとは、メモリに割り当てられている住所のことで、変数はアドレスを持っている。プログラムが変数にアクセスするときには必ずアドレスが必要になる。ポインタはその変数のアドレスを直接扱うことができる。このポインタを使うことで、効率的なプログラムを実現することができ、C言語では欠かせない機能となっている。でも読みにくいコードになりやすく、巨大なプログラムになると、バグ出しが大変になってしまう。また深刻なダメージを与えかねないため、新しい言語の多くでポインタは廃止され、より安全な方向になっている。しかし内部的にはポインタと同じことをしている。単に表面的にポインタをなくし、ポインタを意識しなくてもプログラミングできるようにしているだけのこと。C言語のポインタを学ぶことで、メモリの扱いなどローレベルの動きがイメージしやすくなるので、高速なアルゴリズムなどを目指すならトライしておきたい。色々な使い方があるので、まずは簡単な例でポインタを確認するところからスタート。

ポインタの一番シンプルな例

#include <stdio.h>
int main(void){
  int x = 500;
  int *p;/* ポインタの宣言「*」アステリスクが付く */
  p = &x; /* &x は x のアドレス pポインタにxのaddressを代入 */
  /* int *p = &x; と書いても同じ */

  printf("x     = %d\n",x); /* xの中身= 500 */
  printf("x Add = %x\n",&x);/* xのアドレス= 23ff48 */

  printf("*p    = %d\n",*p);/* pが指すxの中身= 500 */
  printf("p     = %x\n",p); /* pの中身= 23ff48 */
  printf("p Add %%x = %x\n",&p);/* pのアドレス= 23ff4c */
  /* %p使用(4byte分16進数表示) pのアドレス= 0023ff4c */
  printf("p Add %%p = %p\n",&p);
  /* pのサイズ アドレスが入るので4byte */
  printf("p size= %d\n",sizeof(p));

  /* NULLを代入すると何も指さなくなる。0になる */
  p = NULL;

  printf("p NULL= %x\n",p);/* pの中身= 0 */
  return 0;
}
まずはxという通常の変数と*pというポインタを用意して、アステリスク「*」の付かないpにxのアドレスを代入してみる。「*」をつけると、ポインタpに入っているアドレスにある値を意味する。この場合は500になる。そして、x と p のそれぞれの中身、アドレスを確認してみる。この関係を把握することがポインタの第一歩だと思う。アドレスは当然、実行に度に変化するので上記はあくまでも参考アドレス。またポインタそのもののサイズは常に4byte。アドレスを入れるためのサイズとなっている。
x     = 500
x Add = 23ff48
*p    = 500
p     = 23ff48
p Add %x = 23ff4c
p Add %p = 0023ff4c
p size= 4
p NULL= 0


関数を分けてポインタを使う

#include <stdio.h>
/* 三角形の面積を計算 */
void calc(int *a,int *b,int *c){
  *c = (*a)*(*b)/2;
  return;/* 戻り値なし 本来省略 */
}

int main(void){
  int x = 10;/* 底辺 */
  int y = 12;/* 高さ */
  int z;/* 面積 */
  calc(&x,&y,&z);/* アドレス 参照渡し */
  printf("三角形の面積= %d\n",z);

  return 0;
}
三角形の面積を求める例。main関数から底辺&高さと結果を入れるzのアドレスをcalc関数へ渡す。calc関数ではそれをポインタで受け取り。計算するだけで、戻り値はなし。その後にmain関数でzを表示するとちゃんと結果が出ている。これはcalc関数でポインタが指す x、y、z を直接操作しているから可能になっている。このような方法を参照渡しという。値をほかの関数にコピーすることはせず、アドレスだけを渡すので、大きなデータを扱う場合にメリットが出てくるというもの。
三角形の面積= 60


文字列にポインタを使う

#include <stdio.h>
int main(void){
    int num,i;
    char *str ="Hello World";
    printf("%s\n",str);

    for(i=0;*(str+i)!='\0';i++){
      printf("%c\taddress:%p\n",*(str+i), &*(str+i));
    }

    str = "こんにちは世界";
    printf("%s\n",str);

    return 0;
}
文字列は配列以外にポインタを使って扱うことも可能。ポインタの型はcharなので1byte単位で操作できる。for文で1byteごとアドレスを移動することで、「Hello World」を1文字ずつ表示させてみた。その後で同じ変数strに日本語で「こんにちは世界」と代入して表示させてみた。
Hello World
H address:00403064
e address:00403065
l address:00403066
l address:00403067
o address:00403068
  address:00403069
W address:0040306A
o address:0040306B
r address:0040306C
l address:0040306D
d address:0040306E
こんにちは世界


配列にポインタを使う

#include <stdio.h>
int main(void){
   double taparray[]={
     0.005092958178940652,
     -3.2714752032660508e-18,
     -0.04221342765246919,
     1.3295207933702733e-17,
     0.2903456679433583,
     0.5,
     0.2903456679433583,
     1.3295207933702733e-17,
     -0.04221342765246919,
     -3.2714752032660508e-18,
     0.005092958178940652
     };
   double *ptap = taparray;
   int i;
   for(i=0; i<sizeof(taparray)/8; i++){
      printf("%f\taddress:%p\n",*(ptap+i),&*(ptap+i));
   }
   return 0;
}
配列をポインタで表示させてみる。型はdoubleなので8byte単位。
0.005093 address:0023FEE0
-0.000000 address:0023FEE8
-0.042213 address:0023FEF0
0.000000 address:0023FEF8
0.290346 address:0023FF00
0.500000 address:0023FF08
0.290346 address:0023FF10
0.000000 address:0023FF18
-0.042213 address:0023FF20
-0.000000 address:0023FF28
0.005093 address:0023FF30


複数文字列にポインタを使う

#include <stdio.h>
int main(void){
  char *day[] = {
 "Sunday",
 "Monday",
 "Tuesday",
 "Wednesday",
 "Thursday",
 "Friday",
 "Saturday"
  };
  int i;
  for(i=0; i<sizeof(day)/sizeof(day[0]); i++){
   printf("%s\n",day[i]);
  }
  return 0;
}
for文のところで、sizeof(day[0])を使って割っているのは、文字列の数を出すため。この場合は28/4で7となる。28は1バイト換算で、4は4バイトのアドレス分。32bitパソコンの場合は直接4で割ってもよいのだが、環境が変わればポインタのサイズも変わるので、固定的な数値を使うのはあまりよろしくない。
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday


ポインタのポインタ

**これが付いているのがポインタのポインタ。3つでも使えるけど読みにくくなるので、2つまでにしておいたほうがよい。ポインタはアドレスを指す変数なので、ポインタのポインタはアドレスを指す変数のアドレスを指す変数。段々ややこしくなってきた・・・
#include <stdio.h>
/* ポインタのポインタ */
void getDay(int *num, char **ppday){
  static char *day[] = {
    "Sunday","Monday","Tuesday","Wednesday",
    "Thursday", "Friday","Saturday" };
    *ppday = day[*num];
}
/* メイン関数 */
int main(void){
    int num = 2;
    char *pday = NULL;
    getDay(&num, &pday);
    printf("pday: %s\n", pday);
    return 0;
}
プログラムの内容は数値に対応した曜日の文字列を獲得して表示する。データのやり取りはアドレスが中心になっている。そのためポインタが多い。
こうなってくるとポインタがややこしいと思えてきた。まずメイン関数でpdayのポインタを宣言してNULLを代入して、何も指さない状態にする。そしてgetDay関数にpdayのアドレスを渡す。getDayではポインタのポインタとしてppdayにpdayのアドレスが代入される。そしてppdayの指すpdayにdayポインタが指す曜日のアドレスを代入している。理解できるかな? ぱっと見ただけではあれ?という感じだったけど、それぞれのポインタのアドレスとか、指している内容を表示させていくと仕組みが分かってくる。このようなことを複数の関数の間で複雑怪奇にやられると、見通しは悪くなるわな。近年の高級言語でポインタを意識させないようにしている理由が分かってきた。
pday: Tuesday


関数ポインタ

関数さえもポインタで扱う。関数にもアドレスが割り当てられているので、それを利用して、ポインタで扱ってしまうというもの。メリットとしては通常の変数のように扱えるところ。複数の関数を自在に扱えたり、関数を渡して処理させたり、いろいろな使い方が出来てしまう。サンプルは配列として扱ってみた。
#include <stdio.h>
/* プロトタイプ */
void function1(int num);
void function2(int num);
void function3(int num);

/* メイン関数 */
int main(void){
  /* 関数ポインタを宣言 */
  void (*func[])(int num) = {
       function1,
       function2,
       function3
  };
  /* 処理 */
  int i,j=0;
  for(i=0; i<3; i++){
    (*func[i])(j+=1);
  }
  for(i-=1; i>=0; i--){
    (*func[i])(j+=1);
  }
  return 0;
}
/* 関数1 */
void function1(int num){
    printf("function1 処理: %d\n",num);
}
/* 関数2 */
void function2(int num){
    printf("function2 処理: %d\n",num);
}
/* 関数3 */
void function3(int num){
    printf("function3 処理: %d\n",num);
}
関数ポインタを使うことで関数を配列として扱える。入門書にはあまり触れられていない部分なので、初心者レベルではあまり使わないかもしれない。
function1 処理: 1
function2 処理: 2
function3 処理: 3
function3 処理: 4
function2 処理: 5
function1 処理: 6


voidポインタ (汎用ポインタ)

#include <stdio.h>
int main(void){
  int numi =10;
  printf("numi add %p\t%d\n",&numi,numi);
  float numf = 3.14;
  printf("numf add %p\t%f\n",&numf,numf);
  char str = 'A';
  printf("str  add %p\t%c\n\n",&str,str);

  /* voidポインタ宣言 */
  void *pv;
  pv = &numi;
  printf("pv add %p\t\t%d\n",pv,*(int *)pv);
  pv = &numf;
  printf("pv add %p\t\t%f\n",pv,*(float *)pv);
  pv = &str;
  printf("pv add %p\t\t%c\n",pv,*(char *)pv);
  return 0;
}
型を宣言せずに使うことができる特殊なポインタ。そのため、どんな型のアドレスでも扱える。ただし参照するときキャストして型を指定する必要がある。
numi add 0023FF48 10
numf add 0023FF44 3.140000
str  add 0023FF43 A

pv add 0023FF48  10
pv add 0023FF44  3.140000
pv add 0023FF43  A


C言語 ANSI C89 Meadow & MinGW GCC 目次はこちら