ポインタ

せりか式 - C 言語チュートリアル - ポインタ

いつもと違って,今回はポインタの基礎を少々

目次

ポインタとは

ポインタの概念図 メモリをいじり回すための手段です.
単なる変数は,変数が指している番地の値しか利用できませんが,ポインタ変数を利用することで,ポインタ変数が指している番地の値が指している番地の値を利用することができます. 少々ややこしいですが,1つ先まで利用できるようになるわけです. 右図の例の場合,変数iを利用するとiの値しか使うことができませんが,ポインタ変数pに変数iの番地を入れておくことで,間接的に変数iもいじることができるのです.

結局ややこしくなった上に,iしか使えないように見えますが,実際には違います.pは変数なのです. そのため,ポインタ変数pの値を修正することで,どの番地の値も読み出すことができるようになるのです. とは言っても好き勝手にpに値を代入しても,Segmentation fault(access violation)を出すだけですが.

変数宣言についている * がポインタを意味しているのですが,この * を複数書くことで,多段のポインタにすることができます. char **ptrならば,ptrは2つ先の値をいじることができます. 3つ以上はプログラムが読みにくくなるのでよほどのことが無い限り使われませんが,2つまでは良く使われます.

基本

種類

指す番地やデータによって,ポインタは大きく分けて3種類に分類できます.
1番目は変数を指す普通のポインタです.おそらく一番使用頻度が多い種類です.
2番目は関数を指す関数ポインタです.あまり使うことはありませんが,ある程度以上に高度なプログラムを書くためには必須です.
最後はNULLポインタです. また,種類とは違いますが,特殊なポインタの型として,void型のポインタというものがあります.

普通のポインタ

一番よく見かける種類です. 通常の変数宣言にポインタを示す*を付けたものになります. ポインタ自体は番地を指すものですが,指している先の型をあらかじめ決めておく必要があります.

普通のポインタ
1: int *pi;         // int型のポインタ
2: char **ppc;      // char型のポインタを指すポインタ
3: int *i[];        // int型のポインタの配列
関数ポインタ

関数も一種のポインタです.
関数を呼び出すと言うことはメモリ上に配置されたプログラムへジャンプすることを指します. メモリ上に配置されている以上,そこには番地があるはずです. この番地を指しているものが関数名になります.

関数ポインタ
x: int add(int a, int b)        // 加算するだけの関数を定義
x: {  return (a + b);  }
x: int sub(int a, int b)        // 同じく,減算するだけの関数を定義
x: {  return (a - b);  }

1: int r; 2: int (*func)(int a, int b); // 関数ポインタを定義 3: func = add; // 関数ポインタに関数の番地を代入 4: r = func(1, 2); // 1と2を引数にして関数を呼び出す 5: func = sub; // 関数ポインタに関数の番地を代入 6: r = func(1, 2); // 1と2を引数にして関数を呼び出す

2行目で関数ポインタを宣言しています.ポインタを示す*と名前を括弧()で括っていることに注意してください.
ここでは,引数として(int型, int型)を持ち,返値がint型である関数ポインタを宣言しています. このポインタに入れることができるのは,宣言と同じ関数だけです.
3行目で,関数ポインタfuncに関数addの番地を代入しています. 関数は言うまでもなく番地を示しているものなので,変数のときのように&を付ける必要はありません.
4行目は関数呼び出しです.3行目と同じ理由からfuncを利用するのに*を付ける必要はありません.
5,6行目は,3,4行目と同じです. 違うのは,4行目と,6行目の返値が異なるくらいです.

ちなみに,関数ポインタも一種の変数のため,配列として定義することもできます.

関数ポインタの配列
1: int r;
2: int (*func[2])(int a, int b);
3: func[0] = add;
4: func[1] = sub;
5: r = func[0](1, 2);
6: r = func[1](1, 2);
NULLポインタ

ポインタは番地を指す変数なので,数値の0で初期化するわけにはいきません. 代わりとなるものがこのNULLポインタになります. NULLポインタは番地0を指すポインタのことです. 番地0は特殊な番地で書き込むことはできません. ここへ書き込もうとするとエラーとなります.

普通のポインタ
1: int *p;          // int型のポインタ
2: p = NULL;        // ポインタをNULLで初期化する
void型のポインタ

void型のポインタは他のどの型のポインタにでもキャスティングすることができ,またどの型のポインタからもキャスティングすることができます. いわゆるオールマイティな型です. ですが,基本的にvoid型のポインタを宣言することはできません. というのも,ポインタはポインタが指している型が不明だと値を読み出せないためです.
この型は関数の引数や返値として使われることがあります. たとえばソート関数(qsort等)は,int型やfloat型などさまざまな型に対して使われますが,いちいちすべての型を引数とした関数を定義するわけにはいきません. 同じく,メモリを確保するための関数(malloc等)も確保する型のタイプごとに定義するわけにはいきません. そのため,void型のポインタが利用されるわけです.

使い方

ポインタを使うには,ポインタ変数と,実際に値を入れておく場所が必要です.

ポインタの使い方
1: int *p;          // ポインタ変数pを用意する
2: int i;           // 変数iを用意する
3: p = &i;          // 変数iの番地をポインタ変数pに代入
4: *p = 10;         // ポインタ変数pが指す場所,つまりi,に10を代入する
5: i++;
上の例の場合,ポインタ変数がpで,実際に値を入れておく場所がiになります.
ここでポインタを使うために必要な記号が2つあります. 1つはポインタを示す '*',もう1つは番地を示す '&'です.

プログラムを順に見ていきます.

1: int *p;
始めに,ポインタ変数pを使うことを宣言しています.ここで出ている*は1段分のポインタを使うことを意味しています.
この宣言により使えるpは2種類あります. 1つは単なるポインタ変数としてのp,もう1つは変数としての*pになります. ポインタ変数pに番地を入れ,変数*pで,ポインタpの指している番地の値を利用できます.
もし,2段分のポインタを使いたい場合は,int **p;のように,*の数が増えることになります.

2: int i;
ポインタ変数だけでは実際に値を入れておく場所がないため,ポインタ変数が実際に利用する場所を確保します.

3: p = &i;
ここでは&という記号が出てきます. この記号は,変数iの場所(番地)を表す記号です. 先ほどの図で言うと,&iは13になります. この番地をポインタ変数pに入れておくことで,変数*pで変数iをさわることが出来るようになります. *piは等価になるのです.
この&はポインタと違い,番地を示す物なので,複数の&が付くことはあり得ません. (番地の番地というものはありません)

4: *p = 10;
ポインタ変数pが指している変数*p,つまり,iに10を代入します. 3行目と違い,pには,*が付いていることに注意してください. *が付いているときは,*の付いている変数をポインタ変数として扱います.

5: i++;
もちろんiは,ポインタ経由でなくてもそのまま利用することもできます. このインクリメントの結果,iは11になります.

ポインタ演算

ポインタに対して有効な演算は加算と減算のみです. 乗算や除算などは意味を持たないため行うことはできません.
また,この加算・減算に関しても,ポインタならではの特殊な演算がされます.

ポインタに対する加算と減算
1: int *p;                        // ポインタ変数pを用意する
2: int i;                         // 変数iを用意する
3: p = &i;                        // 変数iの番地をポインタ変数pに代入
4: printf("address1: %p\n", p);   // ポインタの値を出力する
5: p = p + 1;                     // ポインタを1つ進める
6: printf("address2: %p\n", p);
7: p--;                           // ポインタを1つ戻す
8: printf("address3: %p\n", p);

このプログラムでは,3つの値を出力しています.もちろんポインタを1つ進めて戻しただけですので,address1とaddress3は同じ値が出力されます. 問題はaddress2です. 5行目において,ポインタに1加算しているだけのはずですが,実際に出力される値は1以上増えているはずです. これが,ポインタ演算の特徴です.

ポインタは,データを指してこそ意味があります. 例えば,float型は4バイトです. float型を指すポインタを1つ進めたいときに,わざわざプログラマがfloat型のサイズである4加算するのは面倒ですし,型のサイズを間違える等のバグにも繋がります. そのため,ポインタに対して演算を行った場合,そのポインタの持つ型に合わせて番地(アドレス)の計算が自動的に行われるようになっています.
実際に増減する単位は,sizeof演算子を使うことで知ることができます.

応用

配列への応用

文字の数を数える 配列は連続領域を使います.
そのため,ポインタ変数に配列の先頭アドレスを入れておき,その値を1つずつずらすことで,配列の中身へ順次アクセスすることができます.

文字の数を数える
1: char a[] = "string";
2: char *p;
3: p = &a[0];         // 配列の先頭番地をポインタ変数に代入
4: for(int c = 0; *p != '\0'; p++) // p++でポインタを一つ進める
5:   c++;
上のプログラムでは,3行目でpに配列の先頭アドレス13を代入します. その後,p++で,14,15と数字を大きくすることで,配列の次の要素にアクセスできるのです. ここから分かるとおり,ポインタ変数の値を1増やすと言うことは,配列の次の要素を参照するようになると言うことです.
つまり,次の2つのプログラムはほぼ等価です.
配列
1: char a[] = "string";
2: for(int i = 0; a[i] != '\0'; ++i)
3:   putchar(a[i]);
疑似配列
1: char *p = "string";
2: for(int i = 0; *(p + i) != '\0'; ++i)
3:   putchar(*(p + i));

配列の添え字には,本来,負の値を入れることができませんが,ポインタを利用することで,見た目上,負の値を添え字にすることができます.
負の添え字?
1: char a[] = "string";
2: char *p;
3: p = &a[4];            // 配列の4番地をポインタ変数に代入
4: *(p - 1) = 'o';       // pの指す番地の一つ前,つまりa[3] (i)

関数ポインタ
引数

関数ポインタは,他の関数の引数として使いフィードバックに使われます. 関数ポインタを使うことで,アルゴリズムとデータ処理を分離することができます. Cの標準関数で関数ポインタを利用しているものとしてqsortが上げられます.

qsort(クイックソート)のプロトタイプ
void qsort(void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *));

qsortは,Cの標準関数の一つでクイックソートのアルゴリズムを実装したものです. sort関数ですので値を比較する必要がありますが,データ処理(値の比較)とアルゴリズム(値の入れ替え)が切り離されているため,利用時にはデータ処理部分をqsortに渡す必要があります.
void *baseには,ソートの対象とするデータの先頭アドレスを渡します.どのようなデータにも対応できるようにvoid型のポインタとなっています.
size_t nmembには,ソートの対象の要素数を渡します.
size_t sizeには,各要素の大きさ(バイト数)を渡します.
最後にint(*compar)(const void *, const void *)に要素同士を比較する関数を渡します.
下にqsortを使ったソートの例を示します.

絶対値が小さい順にソートする例
1: int mycmp(const void *ptr1, const void *ptr2)
2: {
3:   int x = *(int *)ptr1, y = *(int *)ptr2;
4:   return fabs(x) > fabs(y);
5: }

6: int array[] = {30, -20, -35, 10}; 7: qsort(array, sizeof(array) / sizeof(array[0]), sizeof(array[0]), mycmp); 8: for(int i = 0; i %gt; sizeof(array) / sizeof(array[0]); ++i) 9: printf("%d ", array[i]);

というわけで,順に見ていきます.

1: int mycmp(const void *ptr1, const void *ptr2)

値を比較する関数mycmpを定義しています. qsortの4番目の引数と同じプロトタイプを持つ関数である必要があります.

3:   int x = *(int *)ptr1, y = *(int *)ptr2;

ptr1ptr2void型のポインタであるため,実際の型にキャスティングしてから値を取り出します.

4:   return fabs(x) > fabs(y);

取り出した各値を絶対値で比較します. qsortでは,1番目の引数のほうが大きければ正の値,2番目の引数のほうが大きければ負の値,同じであれば0を返す必要があります.

7: qsort(array, sizeof(array) / sizeof(array[0]), sizeof(array[0]), mycmp);

実際の関数呼び出しです.4番目の引数に自作した関数を指定しています.

このプログラムの出力結果は10 -20 30 -35となります.

返値

関数ポインタも変数なので,関数ポインタを返す関数を定義することもできます.

関数ポインタを返す関数
int (*func(void))(int, int)

これは,2つの部分から成ります. 1つは青い部分で,funcは引数を持たない関数ポインタを返す関数本体です.
もう1つは赤い部分で,返値であるint (*)(int, int)のプロトタイプです.
ただ,この書き方は非常にわかりにくいため,typedefを使って返値となる関数ポインタのプロトタイプ(int (*)(int, int))をあらかじめ定義しておく方法もあります.

上の関数定義と等価
typedef int (*functype) (int, int);
functype func(void);
ファイルからの読み込み

次の関数は,ファイルからデータを読み出し,文字列の配列にして返します.
ただし,読み出す行は100行までです.それ以降は改行も含め1行にまとめられます.また,空行はスキップされます.
ファイルからの読み込みに関する完全なソース

このプログラムはコンパイル後,引数に適当なテキストファイル名を指定して実行してください. 各行の先頭に通し番号とコロン(:)を付けて出力します.メモリ確保にmalloc使っているので,最後にfreeで解放するのを忘れないように注意.

ファイルからの読み出しの抜粋
char **loadfile(char *path)
{
    FILE *fp;
    char **array = NULL;
    char *buf;

    do{
        int i;

        /* ファイルサイズを調べる */
        long size;
        fp = fopen(path, "rb");
        fseek(fp, 0L, SEEK_END);
        size = ftell(fp);
        rewind(fp);

        /* 100行分のポインタ + ファイルの読み込みに必要なメモリを確保 */
        array = (char **)malloc(sizeof(char) * (size + 1) + sizeof(char *) * (MAX_LINE + 1));
        buf = (char *)&array[MAX_LINE+1]; /* 前半は配列で利用 */

        /* ファイルの内容を読み込み */
        if(fread(buf, sizeof(char), size, fp) < size) break;
        buf[size] = '\0';

        /* ファイルの中身を行単位で分割 */
        array[0] = strtok(buf, "\r\n");
        if(!array[0])                            break;
        i = 0;
        while(i < MAX_LINE)
            array[i] = strtok(NULL, "\r\n");

        /* 終端 */
        array[i] = NULL;
    }while(0);

    if(fp) fclose(fp);

    return array;
}
多次元配列の動的生成

次の関数は,擬似的に多次元配列を生成して返します.
mallocでメモリを確保しているので,最後にfreeでメモリを解放する必要があります.
多次元配列の動的生成に関する完全なソース

使い方の例
int  **iary  = (int **)aalloc(sizeof(int), 2, 5, 6); // int [5][6]の2次元配列
char ***cary = (char ***)aalloc(sizeof(char), 3, 10, 20, 30); // char [10][20][30]の3次元配列

iary[i][j] = 123;
strcpy(cary[i][j], "hogehoge");

free(cary);
free(iary);
多次元配列の動的生成の抜粋
void *aalloc(size_t size, int dim, ...)
{
	va_list argptr;
	va_start(argptr, dim);

	// 各次元における要素数を保存するために,次元数だけ配列を確保
	nums = (int*)malloc(sizeof(int) * dim);

	// 順次配列の要素サイズを読み込む
	// 同時に必要なポインタ数を数える
	for(i = 0; i < dim; ++i){
		nums[i]  = va_arg(argptr, int);
		pi      *= nums[i];
		sigma   += pi;
	}

	// 必要なだけメモリを確保
	ptr = malloc(sizeof(void *) * (sigma - pi) + size * pi);

	// 配列を生成
	pi    = 1;
	base = (char **)ptr;
	char **next;
	for(i = 0; i < dim-1; ++i){
		pi   *= nums[i];
		next  = base + pi;

		for(j = 0; j < pi; ++j){
			*base   = (char*)next;
			next   += nums[i+1];
			++base;
		}
	}

	// 要素を生成
	char *next;
	pi *= nums[i];
	next = (char *)base + pi;

	for(j = 0; j < nums[i]; ++j){
		*base    = next;
		next    += size;
		++base;
	}

	return ptr;
}

この記事へのコメントはせりかログポインタの例多次元配列の動的生成あたりにしてください.


トップへ