いつもと違って,今回はポインタの基礎を少々
メモリをいじり回すための手段です.
単なる変数は,変数が指している番地の値しか利用できませんが,ポインタ変数を利用することで,ポインタ変数が指している番地の値が指している番地の値を利用することができます.
少々ややこしいですが,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);
ポインタは番地を指す変数なので,数値の0で初期化するわけにはいきません. 代わりとなるものがこのNULLポインタになります. NULLポインタは番地0を指すポインタのことです. 番地0は特殊な番地で書き込むことはできません. ここへ書き込もうとするとエラーとなります.
1: int *p; // int型のポインタ 2: p = NULL; // ポインタをNULLで初期化する
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++;
プログラムを順に見ていきます.
1: int *p;
2: int i;
3: p = &i;
4: *p = 10;
5: i++;
ポインタに対して有効な演算は加算と減算のみです.
乗算や除算などは意味を持たないため行うことはできません.
また,この加算・減算に関しても,ポインタならではの特殊な演算がされます.
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++;
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が上げられます.
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;
ptr1とptr2はvoid型のポインタであるため,実際の型にキャスティングしてから値を取り出します.
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; }
この記事へのコメントはせりかログの ポインタの例か 多次元配列の動的生成あたりにしてください.