C言語:ポインタ入門

基本
メモリの動的確保
関数へ参照渡しもどきをする
関数へのポインタ
ポインタへのポインタ
voidへのポインタ
コールバック関数1
複雑な宣言
初期化

基本

♪一番の問題

ぼくたちダメPGは、 普通の解説でポインタを理解できるほど頭が良くありません。 メモリを意識しろとか、バグがいっぱい出るとか、 めっちゃ難しいから覚悟しろとか、 脅かされてしまい、ぼくたちにはどうせ無理だとあきらめてしまいます。

ぼくたちにとっていちばんの問題はポインタは何の役に立つの? と思うわけです。 ポインタが必要になるときって、 メモリを動的確保したり、 関数へ参照渡しもどきをするときとか、 コールバック関数(関数ポインタ)を使うとか、 そんなもんですよね。

♪いくつかのルールを覚える

ポインタの宣言は*を名前の前につけます。 char *p;でcharを指すポインタ、 double *dp;でdoubleを指すポインタ、 FILE *fp;でFILEを指すポインタが宣言されます。

アドレス演算子(&)はアドレスを得る。 間接演算子(*)はポインタの指す値を得る。 ポインタがNULLを指せば何も指していないことが保証されるのですが、 ここまでは分かりますよね。 で、よくある最初のポインタを使うプログラムは、

main()
{
    int i;/* 普通の変数iを宣言 */
    int *ip;/* intを指すポインタ */

    ip = &i;/* ipはiを指す */
    *ip = 19920715;/* ipの指すもの(i)に19920715を代入 */
    printf("i=%d\n", i);
}

この実行結果がi=19920715になるわけです。

♪初期化

初期化されていない変数には変な値が入っています。 int x;とやってすぐにprintf("%d", x);とxの値を表示すればよく分かるはずです。 Cは吝嗇なので、N88BASICとかのように0で初期化してくれません。 同じようにポインタも初期化されませんから、変な値が入っています。 つまり、どこを指しているか分からない状態です。

ポインタを使うにあたっては必ず自分の管理下にあるところを指すようにしなければいけません。 管理外の領域に対してデータの入出力を行うことは、好きな女子の笛を舐める行為と同等です。 自分はいいのですが、相手にとっては苦痛の可能性があります。

メモリの動的確保

配列を使うんですが大きさを自由に変えたい場合、 とっても大きい場合は動的確保します。

C++では言語にnew/deleteがあるんですが Cは関数ですよね。mallocとかfreeとか。

mallocで行っていることは、 メモリのどこかあいている所を支配下に置き、その位置を返しているだけです。 freeは引数の指す領域の支配をやめるだけです。 freeした後の領域は無秩序状態になります。

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

main()
{
    char *p;
    char a[200] = "土下座は快楽だろうか。";
    
    p = malloc(200);/* これでpは配列のように扱える。 */
    strcpy(p, "みずほ銀行で100円の宝くじを換金したい。");

    /* pのサイズはポインタのサイズ(機種依存)に、aのサイズは必ず200になる。 */
    printf("pのサイズ=%d aのサイズ=%d\n", sizeof p, sizeof a);

    p = realloc(p, 300);/* pの指すメモリブロックの大きさを300にする。 */
    puts(p);/* reallocはもとの内容を保持している。 */
    free(p);/* pの指すメモリ領域を解放する。NULLなら何もしない。 */

    p = calloc(200, 1);/* callocは確保したメモリブロックを0クリアしてくれる。 */
    puts(p);/* 当然何も表示されないはず。 */
    free(p);/* プログラムの最後はfreeしなくても良いらしい。 */
}

ポインタを配列のように扱う場合とか、配列を引数として関数に渡すときは、 サイズがポインタの大きさになりますんで、注意が必要です。 ポインタ配列の場合、char *p[10]とかやったら、10バイトではなくポインタのサイズ*10バイトの大きさになりますよ。 僕たちは気をつける必要がありますね。

♪注意点

mallocには落とし穴があります。 mallocで得た領域はfreeするかプログラムの終了を迎えるまで、律儀に支配され続けています。

#include <stdlib.h>
main()
{
    char *p;
    p = malloc(19830120);
    p = malloc(19830120);
}

このようにすると、1回目のmallocで支配した領域は、 2回目のmallocによってpが新しい領主になるため、 どこからも相手にされなくなってしまい放置プレイ状態となります。 mallocした場所はかならずヒモをつけておく必要がありますね。

関数へ参照渡しもどきをする

有名なswap関数があります。 Cは値渡しなので、

void swap(int horikita, int maki)
{
    int hara;
    hara = horikita;
    horikita = maki;
    maki = hara;
}

main()
{
    int a,b;

    a=1560;
    b = 1467;
    swap(a,b);
}

とやっても、swap関数内でパラメータにどんな値を代入しても 戻ってきたとき、a,bの値は変わりませんね。

だからポインタを仲介役にして、参照渡しもどきをします。

void swap(int *horikita, int *maki)
{
    int hara;
    hara = *horikita;
    *horikita = maki;
    *maki = hara;
}

main()
{
    int a,b;

    a=1560;
    b = 1467;
    swap(&a,&b);
}

これでポインタをつかった参照渡しごっこが出来るわけです。 またでっかい構造体とか配列は先頭のアドレスだけを渡すほうが速いようですね。

きわめて重要なことですが、constを使うことで安全にポインタを渡すことができます。 だって、constは初期化は出来るけど値の変更は出来ないのですから。 C言語は値渡しなので、constがなくても指す場所は変わりません。 でも、指すものは変えられます。それをconstで防ごうということです。

void natsuho(const char*str)
{
    /* この中でstrの指すもの内容を変えることはできない。 
    str[0] = 4;とかするとエラー。
    */
    puts(str);
}

main()
{
    char moji[999] = "長澤まさみちゃんの出世作はなごり雪である。 ○か×か";
    
    natsuho(moji);
}

でもconst char *strは const char へのポインタです。 指すものを変えることは出来ませんが指す場所なら変更可能です。

void natsuho(const char*str)
{
    char *p;
    
    p = str;
    strcpy(p , "x");/* strcpy(str, "ABC");こんなのはエラーだけど、pからなら変更できてしまう。 */
    puts(str);
}


main()
{
    char moji[999] = "長澤まさみちゃんの出身地はどこでしょうか(配点54点)";
    
    natsuho(moji);
}

関数へのポインタ

書き方が変なので気持ち悪いです。 でもCを作った人が決めたルールなので甘んじて受けましょう。 基本は次の通りです。

/* 戻り型 (*名前)(引数); */
int (*func)(char *);/* funcはchar *を引数にもちintを返す関数へのポインタ。 */
func = atoi; /* funcはatoi関数を指す */
x = (*func)("123"); /* atoiと同じ働きになる */
x = func("123"); /* 普通の関数と同じ書き方もok */
x = (int(*)(char *))printf;/* printf関数をint (*)(char *)へキャスト。 */
((int(*)(const char *, ...))x)("5");/* xをprintfの型にキャストしてさらに実行 */

関数へのポインタ(関数ポインタ)は、名前の前に*をつけて、カッコで囲みます。 カッコがなければ、int *func(char *)となり、intへのポインタを返す関数になります。 だから、int (*func)(char *)と必ず*と名前をカッコで囲んであげます。

基本を抑えた所で、 さっそく関数ポインタを使ってみましょう。 有名なsignal関数があります。こいつからやっつけましょう。 プロトタイプが気持ち悪いです。

void (*signal(int sig, void (*handler)(int)))(int); 

私たちはこういった入り組んだものを見てもすぐに理解できるわけがありません。 でも私たちでも理解できるぞ! ということを見せ付けてやろうではありませんか!

signalは名前ですよね。で前に*がついてるから 関数ポインタみたいですよね。 だったらvoid (*signal)(int sig,void (*handler)(int));でいいじゃん? 最後の(int)が余るんですけど?  それともvoid *signal(int sig,void (*handler)(int))を意味してるのか? やっぱり(int)が邪魔だよ。プロトタイプがおかしいのか?  ワケワカラン! やめた。 やっぱり俺/僕/私はダメなんだと自己嫌悪に陥ります。

まず、分解してみましょう。 signalの後ろの()の中身はどうみても引数です。 とりあえず、外してみましょう。

void (*signal(  ))(int); 
              ↑
       int sig, void (*handler)(int)

すこしわかりやすくなったでしょう。 次にvoid (*signal())(int)の分解です。 signalは*と()にはさまれていますが、 どちらと結ばれるでしょうか。 *と()では()の方が優先されるため、 signal()とvoid(*)(int)にわかれます。

だから、以下のようになります。

void (*  )(int);
       ↑
     signal(  )
            ↑
      int sig, void (*handler)(int) 

これをどう読み取るかです。 signal関数は(int sig, void (*handler)(int))を引数に持つ。ここまでは良いと思います。 で、戻り値は...?  左側にある*つまりポインタが戻ります。 どんなポインタ? 残ったvoid (* )(int)です。 intを引数にもち、戻り値なしの関数へのポインタが戻り値の型になります。

signal関数は、intと関数へのポインタを引数にもち、関数へのポインタを返す関数です。 下のように表現されることもあります。

typedef void (*psigfunc)(int);
psigfunc signal(int, psigfunc handler);

これでsignalを変数だとか、関数ポインタとか言ってる連中に対抗できますね。

ポインタへのポインタ

char **cpp;とかいうやつです。cppはcharへのポインタを指します。 int **ipp;ならintへのポインタを指します。

ポインタへのポインタを使うと、 ポインタを参照渡しごっこすることが可能です。 関数内で指す場所を変えることが出来るのです。

#include <stdlib.h>

void po(char **pp, int size)
{
    *pp = malloc(size);
}

main()
{
    char *p;
    
    po(&p, 100);
    strcpy(p, "ゴミデータ_");
    puts(p);
}

でもmain関数を次のようにすると変になります。 ぼくの環境では落ちました。

#include <stdlib.h>

void po(char **pp, int size)
{
    *pp = malloc(size);
}

main()
{
    char **pp;
    
    po(pp, 100);
    strcpy(*pp, "ゴミデータ_");
    puts(*pp);
}

なぜ落ちたのでしょうか。 &pとして渡しても、ppをそのまま渡した場合も 同じだと思いませんか?

でもこんな違いがあるのです。

p → [?]           pはcharを指します。指す場所はどこか分かりません。
&p → p → [?]     &pはpを指します。pはどこを指してるか分かりません。
pp → [?] → [?]   ppはどこか(1)を指しています。どこか(1)はどこか(2)を指しています。

po関数で行ったことは、メモリの確保です。 char **ppと、ポインタへのポインタを受け取って *pp = malloc()とppの指すものにメモリブロックのポインタを代入しております。

つまり、このようになります。

&p → p → [malloc]
pp → [?] → [malloc]

ppはどこを指しているか分からないのです。 もしかしたらNULLかもしれませんし、 安全な場所かもしれませんし、 とんでもなく危険な場所かもしれません。 ppの指す場所は自分の支配下にない可能性が高いのです。 だから落ちることがあるのです。

こんなことをすると落ちるでしょう。

main()
{
    int *ip;
    *ip = 100;
}

ipはどこかを指していますがその場所は分かりません。 指す場所は自分の支配下でなければいけません。

voidへのポインタ

通称、汎用ポインタです。 これを使うところは限られています。

メモ1:voidへのポインタは、そのままでは間接演算子を使えません。

main()
{
    
    void *vp;//voidへのポインタ
    char *cp;//charへのポインタ
    char moji[] = "長澤まさみちゃんのNTTのCMが可愛い";
    
	vp = moji;//vpはmojiを指す
    
    *(vp + 4) = 0;//できない。(エラーになる。)
    *((char *)vp + 4) = 0;//キャストすれば間接演算子使える。
    cp = vp;//他のポインタに渡すことはできる。
    
    return 0;
}

malloc/freeのように関数の戻り型や引数の型がどのようなポインタ型か分からないときにも使われますね。 とうぜん、引数は型を変換しないと使えませんけどね。

コールバック関数1

コールバック関数とは、関数に関数を渡して、処理の途中で指定した関数を呼ばせるようにすることです。 Win32APIのウインドウプロシージャでおなじみですね。

下のプログは、ファイルを4096バイト読み込んでコールバック関数に渡すだけです。 コールバック関数では、受け取ったデータをstdoutに受け流しているだけです。

/* コールバック */
#include <stdio.h>
#include <stdlib.h>

/* コールバック関数 */
int kakidasi(int size, char *data)
{
    return fwrite(data, 1, size, stdout);
}

/* ファイルを読み込んでコールバック関数に渡す */
int readfile(const char *file, int (*callback)(int, char *))
{
    FILE *fp;
    int read;
    char buf[4096];
    
    fp = fopen(file, "rb");
    if (fp == NULL) {
        perror("エラーだよ");
        return 0;
    }
    do {
        read = fread(buf, 1, sizeof(buf), fp);
        callback(read, buf);
    } while (read);
    fclose(fp);
    return 0;
}

main(int argc, char *argv[])
{
    readfile(argv[1], kakidasi);
}

複雑な宣言

C言語で難しいのが複雑な宣言。 僕たちにおいて最も難しいところです。 丸暗記が一番です。

int niji[8][8]; /* 二次元配列 */
int *pniji[8]; /* ポインタの配列 */
int (*aniji)[8]; /* 配列へのポインタ */
int **pp; /* ポインタへのポインタ */

応用です。K&R p149 複雑な宣言からコピペ。

char (*(*x())[])()
char (*(*x[3])())[5]

はい、わけがわかりません。 実際こういった宣言を見る機会なんて無いはずです。 signal関数の時と同じように、分解してみよう!

char (*(*x())[])()

char (*  )()
       ↑
       (*  )[]
         ↑
         x()

外側から読みます。 xはcharを返す関数へのポインタ、への配列へのポインタ、を返す関数です。 ややこしい。

char (*(*x[3])())[5]

char (*  )[5]
       ↑
      (  )()
       ↑
       *x[3]

xは、char5つの配列へのポインタ、を返す関数、のポインタ配列3つです。 とにかく、ややこしいものは分解するのが一番です。

初期化

ポインタとはすこしはなれるけど、 C言語の初期化のやり方のメモ。

//初期化
#include <stdio.h>
int x = 4;

int aaa[] = {5, 10, 60, 4};//aaa[4]の大きさになる
char str[] = "hasagawa kyoko";//文字列の大きさ
char *p = "asdf";//"asdf"をどこかに確保して指すだけ
char *pp[] = {"1qaz", "2wsx", "3edc"};
struct {
    int key;
    char *mes;
} oopo[] = {
    {1, "aragaki yui"}, 
    {2, "tokuzawa naoko"},
    {4, "ogura yuko"}
    
};

int map[][3] = {
    {1,2,3},
    {4,5,6},
    {7,8,9}
};

int mp[][4] = {
    {1},    //1,0,0,0
    {2},    //2,0,0,0
    {3},    //3,0,0,0
};



int main(void)
{
    puts(pp[2]);
    puts(oopo[1].mes);
    
    printf("%d %d %d\n", map[1][1], map[0][2], map[2][0]);
    printf("%d %d %d\n", mp[1][1], mp[0][2], mp[2][0]);
    return 0;
}