C言語の研究4

第4章 ポインタ

さて、C言語がC言語足りうる、もちろんC++も含めて最大のメリットであると言える「ポインタ」について研究するときが来た。

1 ポインタとは

ポインタについては、Bruce Leeがその名作「燃えよドラゴン」(Enter The Dragon) の中で解説されているから、まず最初にそれを見ることにしよう。

ブルース・リー氏によるポインタの解説

ブルース・リー氏が解説するように、ポインタは月を差す指に似ている。

この指にとらわれていると、それが示す実体を見落とすことになる。

つまり、ポインタは実体の位置、つまり実体のアドレスを示すだけのものであると考えよう。

実体が整数型であれば、int *p_seisu;

実体が文字列形式であれば、 char *p_miji;

という具合に*を付けて、実体を示すポインタであることをコンパイラに宣言しておこう。

ちょっと例を見よう。整数aと整数bをポインタを利用して足し算してみよう。もちろんさっきの関数を利用してね。

#include <stdio.h>

int p_tasu(int *, int *); /*引数に2つの整数型ポインタをとり、
                                            整数を戻り値とする関数を宣言*/

void main(int argc, char **argv)
{
    int a = 10;
    int b = 20;
    int c;

    int *pa; /*aへのポインタを宣言*/
    int *pb; /*bへのポインタを宣言*/

    pa = &a; /*アドレス演算子によりaのアドレスをpaに代入*/
    pb = &b; /*アドレス演算子によりbのアドレスをpaに代入*/
   
    c = p_tasu(pa, pb);

    printf("a + b = %d\n",c);
}

int p_tasu(int *pa, int *pb)
{
    return(*pa + *pb); /*paとpbの実体を足す*/
}

どうだろうか、「a+b=30」と表示されただろうか。

ポインタには実体なんかない。だから、アドレスを設定してやる必要がある。

演算のときは、それぞれの実体が必要だから、ポインタに*を付けてやると実体を表示できる。

この2つを覚えておこう。

それでは、今の例題を利用して、整数aとbのアドレスとポインタpaとpbの内容を確認して、ポインタpaとpbのアドレスもついでに確認しておこう。

#include <stdio.h>

int p_tasu(int *, int *); /*引数に2つの整数型ポインタをとり、
                                            整数を戻り値とする関数を宣言*/

void main(int argc, char **argv)
{
    int a = 10;
    int b = 20;
    int c;

    int *pa; /*aへのポインタを宣言*/
    int *pb; /*bへのポインタを宣言*/

    pa = &a; /*アドレス演算子によりaのアドレスをpaに代入*/
    pb = &b; /*アドレス演算子によりbのアドレスをpaに代入*/
   
    c = p_tasu(pa, pb);

    printf("a + b = %d\n\n",c);

    printf("aのアドレスは %8.8xです\n",&a);
    printf("paの内容は %8.8xです\n\n",pa);

    printf("bのアドレスは %8.8xです\n",&b);
    printf("pbの内容は %8.8xです\n\n",pb);

    printf("paのアドレスは%8.8xです\n",&pa);
    printf("pbのアドレスは%8.8xです\n",&pb);
}

int p_tasu(int *pa, int *pb)
{
    return(*pa + *pb); /*paとpbの実体を足す*/
}

どうだろうか、aおよびbのアドレスとポインタのpaおよびpbの中身がそれぞれ同じであり、paとpbは生意気にも自分らのアドレスがちゃんとあることが分かってもらえただろうか。

<覚えておこう>

整数の変数aを int a; と宣言することは、CPUのメモリ空間に1つの整数型の変数の領域を確保させるということだった。

すると、int *a; と整数型の実体を指すポインタを宣言した場合はメモリ領域を確保してくれるのかな?

とんでもない。かつて北斗の拳が「ポインタには領域などな〜い」と言っていたかどうかは知らないが、ポインタを単に宣言しただけではメモリ領域は確保されない。実体があってのポインタである。

そこで、後に紹介する構造体などで、ポインタの領域をどうしても確保しなければならない時のために、これまた後ほどライブラリ関数で紹介するポインタなどの領域確保のためのmalloc関数がある。

2 ポインタ型を示すポインタ

今のは整数型の実体を示すポインタであったが、実体がポインタの場合は悲惨である。でもないか。

とにかく可能だ、そして訳がわからん。次のように表示する。

ポインタ *paを示すポインタは、**p_paのように*を2つ並べて宣言する。

これをさっきの例を使って確認しよう。

#include <stdio.h>

int p_tasu(int **, int **); /*引数に2つの整数型ポインタへのポインタをとり、
                                            整数を戻り値とする関数を宣言*/

void main(int argc, char **argv)
{
    int a = 10;
    int b = 20;
    int c;

    int *pa; /*aへのポインタを宣言*/
    int *pb; /*bへのポインタを宣言*/

    int **p_pa; /*ポインタpaへのポインタを宣言*/
    int **p_pb; /*ポインタpbへのポインタを宣言*/

    pa = &a; /*アドレス演算子によりaのアドレスをpaに代入*/
    pb = &b; /*アドレス演算子によりbのアドレスをpaに代入*/

    p_pa = &pa; /*アドレス演算子によりpaのアドレスをp_paに代入*/
    p_pb = &pb; /*アドレス演算子によりpbのアドレスをp_pbに代入*/
   
    c = p_tasu(p_pa, p_pb);

    printf("a + b = %d\n\n",c);

    printf("aのアドレスは %8.8xです\n",&a);
    printf("paの内容は %8.8xです\n\n",pa);

    printf("bのアドレスは %8.8xです\n",&b);
    printf("pbの内容は %8.8xです\n\n",pb);

    printf("paのアドレスは%8.8xです\n",&pa);
    printf("p_paの内容は %8.8xです\n\n",p_pa);

    printf("pbのアドレスは%8.8xです\n",&pb);
    printf("p_pbの内容は %8.8xです\n\n",p_pb);

    printf("p_paのアドレスは%8.8xです\n",&p_pa);
    printf("p_pbのアドレスは%8.8xです\n",&p_pb);
}

int p_tasu(int **p_pa, int **p_pb)
{
    return(**p_pa + **p_pb); /*p_paとp_pbの実体を足す*/
}

どうだろうか、「a+b=30」となって、paとpbのそれぞれのアドレスがp_pa、p_pbとそれぞれ一致することが分かってもらえただろうか。

3 配列とポインタ

ポインタの利用価値は配列を使うときにあると言ったら過言だろうか。

配列は同じ型の変数の集合である。一度、その型のポインタを宣言して、ポインタに配列の最初のアドレスを代入すれば、ポインタの値に1を加えて行くと、実体の配列の次の要素を指差すことができるんだ。

ちょっと例で見てみようか。

#include <stdio.h>

void main(int argc, char argv)
{
    int a[5] = {11, 12, 13, 14, 15};
    int *pa;
    int i;

    for(i=0, pa=a; i < 5; i++){
        printf("a[%d]の値は%dです\n",i ,a[i]);
        printf("*(pa + %d)の値は%dです\n\n",i ,*(pa+i));
    }
}

どうかな? a[i] と *(pa+i)の値が同様であることが確認できましたでせうか。

この原理から、よくポインタの値を変化させて配列の中身を見るプログラムを書く人がいるんだが、たとえばさっきの例を使ってみよう。

#include <stdio.h>

void main(int argc, char argv)
{
    int a[5] = {11, 12, 13, 14, 15};
    int *pa;
    int i;

    for( i=0, pa=a; i < 5; i++, pa++){
        printf("a[%d]の値は%dです\n",i ,*pa);
        printf("paの内容は%8.8xです\n\n",pa);
    }

    printf("a[%d]の中身は%d\n",i ,*pa); /*これはダメじゃ*/
}

paを増加させているのを忘れちまって、赤字のとこで「a[0]=11」とでも表示させたかったんだろうけど、paは配列外に出てしまってて、a[5]=16957578とか訳の分からん表示をするだろう。

つまり、ポインタの値自体を増加させることは、やめておいた方が賢いかもネ。

4 関数とポインタ (ポインタを渡された関数)

さっき、関数の引数として整数aとbのポインタであるポインタ*paおよび*pb、あるいは、整数型ポインタ*paと*pbのポインタである**p_paや**p_pbを渡したね。

この場合はaとbの足し算をしただけだからaとbの値は変化していなかった。

それじゃ、整数aを増加させる関数を考えてみよう。

#include <stdio.h>

int plus(int);
int p_plus(int *);
void main(int argc, char **argv)
{
    int x;
    int a = 10;
    int *pa;

    pa = &a;

    x = plus(a); /*値渡し*/
    printf("plusによるxの値は%dです aの値は%dです\n",x,a);

    x = p_plus(pa); /*アドレス渡し*/
    printf("p_plusによるxの値は%dです aの値は%dです\n",x,a);
}

int plus(int a)
{
    a += 10;
    return a;
}

int p_plus(int *pa)
{
    *pa += 10;
    return *pa;
}

分かってもらえただろうか、関数plusにaを渡した場合だが、aの値には変化がない。これは関数plusがaと同じ10の値を持つ変数を別に設定するからなのだ。これをどうしようが本体のaには全く影響がない。

ところが、aのポインタ*paを与えられた関数p_plusの方はとしては、実体aを指差すポインタpaを渡されているものだから、その実体であるaを直接さわることができるというわけだ。

さて、さっきの配列の例を使って、ポインタを渡された関数により、配列の内容を書き換えてやろうじゃないか。

#include <stdio.h>

int henka(int *);
void main(int argc, char argv)
{
    int a[5] = {11, 12, 13, 14, 15};
    int *pa;
    int i;

    pa = a;
    for( i=0; i < 5; i++){
        a[i] = henka(pa + i); /*配列の要素のポインタを渡す*/
        printf("a[%d]の値は%dです\n",i ,a[i]);
    }
}

int henka(int *p)
{
    return *p * 10; /*実体を10倍にして返す*/
}

こういう具合にポインタを渡された関数は、配列の各要素の中身を10倍にしたり、いろいろと実体を変化させることが出来て便利だなあ。

5 ポインタと文字列

文字列が文字定数の配列で最後に'\0'(ナルと読む。NULLとも書ける)が付くことは前に言ったが、ここで文字列とポインタの関係を明らかにしたい。

まず、簡単な例からスタートしよう。

#include <stdio.h>

void main(int argc, char **argv)
{
    char a[5] = "Pell";
    char *pa;

    pa = a;

    printf("%s\n",a);
    printf("%s\n",pa);
}

これを実行すると、どちらも「Pell」と表示する。でもなぜだろう? あるいはprintf関数の性質からなのか。

試しにa[0]アドレス、およびaとpaの内容を表示させてみよう。

#include <stdio.h>

void main(int argc, char **argv)
{
    char a[5] = "Pell";
    char *pa;

    pa = a;

    printf("%s\n",a);
    printf("%s\n\n",pa);

    printf("a[0]のアドレスは%8.8xです\n",&a[0]);
    printf("aの中身は%8.8xです\n",a);
    printf("paの中身は%8.8xです\n\n",pa);
}

全部、同じアドレス値だったね。

すると、&a[0]とaはおなじだから、printf("%s\n",&a[0]);というのはダメなのか、ちょっとやってみよう。

#include <stdio.h>

void main(int argc, char **argv)
{
    char a[5] = "Pell";
    char *pa;

    pa = a;

    printf("%s\n",&a[0]);
}

なんと、ちゃんと「Pell」と表示されちゃった。要するに、文字列をprintfで表示させるには、文字列の最初のアドレスを書いておけばいいんだね。分かりました。

ところで、単にポインタは実体を指すものにすぎないとブルース・リーは言っておられたが、次のように直接的に文字列を示すポインタに文字列を代入したらどうなるんだろうか?

#include <stdio.h>

void main(int argc, char **argv)
{
    char *pa = "Pell";

    printf("%s\n",pa);
}

これでも、「Pell」と表示できた。これは多分"Pell"という実体をコンパイラが先に作って、これを指差すポインタpaを後で用意したんだろうね。

このような実体のないポインタに値を代入することは、あまり私としては好きではないやり方ではあるんだ。

でも、文字列の配列を取り扱う場合は便利である。次の例を見よう。

ポインタを使わないで文字列の2次元配列を表示する場合、

#include <stdio.h>

void main(int argc, char **argv)
{
    int i;
    char a[3][10] ={ "Pell","Akira","Dragon"};

    for(i=0; i < 3; i++){
        printf("%s\n",a[i]);
    }
}

このように、a[3][10]ときっちりとした数値を宣言しておかないとコンパイラちゃんは許してくれない。

でもポインタ配列を使うならばですね。

#include <stdio.h>

void main(int argc, char **argv)
{
    int i;
    char *pa[] ={ "Pell","Akira","Dragon"};

    for(i=0; i < 3; i++){
        printf("%s\n",pa[i]);
    }
}

こんな具合に、*pa[]とアバウトな書き方でもOKだ。

さらには、ポインタ型を指差すポインタを使うとすると、

#include <stdio.h>

void main(int argc, char **argv)
{
    int i;
    char *pa[] = { "Pell","Akira","Dragon"};
    char **p_pa;

    p_pa = &pa[0]; /*pa[0]のアドレスをポインタ型を指すポインタp_paに代入*/

    for(i=0; i < 3; i++){
        printf("%s\n",*(p_pa+i));
    }
}

このように、p_paに1加えるとpa[0]から順番にpa[1],pa[2]と次々と表示させることが出来る。

6 関数を指すポインタ

ポインタは相手が何だろうと指差すことは平気である。月を示す指であることを忘れないようにしよう。

It is like a finger pointing away to the Moon.〜by Bruce Lee

先の例をちょっと変えて、ポインタ型を示すポインタは分かりにくいので、単なるポインタ同士の受け渡しに変えて、関数により名前を次々と表示するプログラムを作成しよう。

#include <stdio.h>

char *get_name(int);
void main(int argc, char **argv)
{
    int i;
    char *p;
    char *(*p_get_name)(int); /*関数get_nameを指すポインタを宣言*/

    p_get_name = get_name; /*関数get_nameのアドレスを設定*/

    for(i=0; i < 3; i++){
        p = (*p_get_name)(i); /*ポインタを介した関数の呼び出し*/
        printf("%s\n",p);
    }
}

char *get_name(int a)
{
    char *pa[] = { "Pell","Akira","Dragon"};
    char *p;

    p = pa[a];  /pa[a]をポインタpに代入*/
   
    return p;
}

どうだろうか、赤字の部分で関数を指すポインタを宣言した。次の赤字では実体の関数による結果をpに代入しているが、多少、ヒネクレたやり方だね。

関数を示すポインタが便利なのは、関数がいくつかある場合だろう。次に例を示そう。

#include <stdio.h>

int add(int, int);
int sub(int, int);
int mul(int, int);
int div(int, int);

void main(int argc, char **argv)
{
    int(*p_op[4])(int, int); /*関数へのポインタ配列を宣言*/
    int x=10, y=3, z;
    int i;

    p_op[0]=add;  /*各関数にポインタ配列の要素を割り当てる*/
    p_op[1]=sub; /*各関数にポインタ配列の要素を割り当てる*/
    p_op[2]=mul;  /*各関数にポインタ配列の要素を割り当てる*/
    p_op[3]=div;  /*各関数にポインタ配列の要素を割り当てる*/

    printf("x = %d y = %d\n",x, y);

    for(i=0; i<4; i++){
        z=(*p_op[i])(x,y);  /*iを1つ増加させるごとに、違う関数にアクセスできる*/
        printf(" z = %d\n",z);
    }
}

int add(int a, int b)
{
    return a+b;
}

int sub(int a, int b)
{
    return a-b;
}

int mul(int a, int b)
{
    return a*b;
}

int div(int a, int b)
{
    return a/b;
}

この場合は、関数へのポインタ配列の要素番号を増加させると、各関数にアクセスできて便利だ。こういうのが関数へのポインタを使うメリットだろうね。

7 コマンドラインとポインタ

さあ、いよいよmain関数で、main(int argc, char **argv)と変な引数をとっていた理由を説明できるときが来た。

mainも関数であることは以前も言ったが、このmain関数の2つの引数、int argcとchar **argvは実は、プログラムの実行時に割り当てるものなんだ。この意味で仮引数とも呼ばれている。

普通、PC-DOSでcopyコマンドを実行する場合なんかに、

copy akira pell というふうにタイプするだろう。これなんだ。

int型の argcには、今のタイプした文字列の個数、つまり3個の3が入る。yyy.exeのようにプログラム名だけをタイプした場合は1だね。

char **argvは上の例でいうと、"copy"と"akira"と"pell"の文字列配列型ポインタを指すポインタが入る。つまり、*argv[0]が"copy"、*argv[1]が"akira"そして*argv[2]が"pell"をそれぞれ指しているポインタ配列だったんだ。

だから、void main(int argc, char *argv[])と書いても良かったんだ。いや、変数名をargcやargvにしておく必要も本当はないんだけれど、慣例的にこの名前を使っているだけの話だ。

これを説明するまで、バカみたいに、main(int argc, char **argv)とややこしいことを書いてきた、許してくれ。

main関数が仮引数をとらない場合は、void main()としてもなんら支障なかったのに。

さっそく例をみよう。

#include <stdio.h>

void print_param(char *);
void main(int argc, char **argv)
{
    printf("バラメータの個数は%d個です\n",argc);
    for( ; *argv != NULL; argv++) print_param(*argv); /*argvの位置は最初にしておくとプログラム名から表示

                      するし、*argv[]の仮引数の配列要素がナルになるまで表示させる*/ 
}

void print_param(char *argv)
{
    printf("%s\n",argv);
}

まず、これを実行してもらってEXE(実行)ファイルを作成しよう。

へてから、その実行ファイルを適当なディレクトリにコピーして、MS-DOSプロンプトを立ち上げる。

後は、chdirコマンドで今の実行ファイルのあるディレクトリに移動する。(例) chdir c:\c リターン

そして、プロンプトに続いて、プログラム名 適当な言葉 適当な言葉・・・と入力してリターンキーを押そう。

するとどうだろう。パラメータの個数と、プログラム名 適当な言葉 適当な言葉・・・が表示されたかな。

 

よし、今度はコマンドラインからタイプした各適当な言葉の文字数も同時に表示させてみようか。

#include <stdio.h>

int get_su(char *); /*文字数を数える関数*/
void print_param(char *, int);

void main(int argc, char **argv)
{
    int n;

    printf("バラメータの個数は%d個です\n",argc);

    for( ; *argv != NULL; argv++){
        n = get_su(*argv);
        print_param(*argv, n);
    }
}

int get_su(char *argv)
{
    int i;
    for(i=0; *(argv+i) != NULL; i++);
    return i;
}


void print_param(char *argv, int n)
{
    printf("パラメータは%sで文字数は%dです\n",argv ,n);
}

どうだろうか、各パラメータと文字数が表示されただろうか。

この説明が終わったので、これからはmain関数に必要がある場合以外は、argcやargvの仮引数は書かないことにします。


back  next


HOME