はじめに:複雑な型の説明

ポインタを理解するには、多かれ少なかれ複雑な型が出てきます。そこで、まず複雑な型を完全に理解する方法を紹介します。複雑な型を理解するのは実は簡単です。型の中には多くの演算子が現れますが、これらも普通の式と同じように優先順位を持ち、その優先順位は演算の優先順位と同じです。そこで、その原則をまとめました。変数名から始めて、演算子の優先順位に従って結合しながら、一歩一歩分析していきます。

それでは、まず単純な型からゆっくり分析していきましょう。

  • int p;-- これは普通の整数型変数です
  • int *p;-- まず p のところから始めて、最初に*結合するので、p はポインタであることが分かります。次に int と結合し、ポインタが指す内容の型が int 型であることを示します。したがって、p は整数型データを指すポインタです。
  • int p[3]-- まず p のところから始めて、最初に[]結合し、p は配列であることを示します。次に int と結合し、配列の要素が整数型であることを示します。したがって、p は整数型データからなる配列です。
  • int *p[3];-- まず p のところから始めて、最初に [] と結合します。その優先順位が*高いため、p は配列です。次に * と結合して、配列の要素がポインタ型であることを示します。さらに int と結合して、ポインタが指す内容の型が整数型であることを示します。したがって、p は整数型データを指すポインタからなる配列です。
  • int (*p)[3];-- まず p のところから始めて、最初に * と結合し、p はポインタであることを示します。次に [] と結合します("()" のステップは無視してかまいません。単に優先順位を変えるためです)。ポインタが指す内容は配列であることを示し、さらに int と結合して、配列の要素が整数型であることを示します。したがって、p は整数型データからなる配列を指すポインタです。
  • int **p;-- まず p から始めて、先に * と結合し、p はポインタであることを示します。次に * と結合し、ポインタが指す要素がポインタであることを示します。さらに int と結合し、そのポインタが指す要素が整数型データであることを示します。二重ポインタおよびそれ以上のポインタは複雑な型ではめったに使われないため、後続のより複雑な型では多段ポインタは考慮せず、最大でも一段のポインタだけを考えます。
  • int p(int);-- p のところから始めて、最初に () と結合し、p は関数であることを示します。次に () の中を分析すると、その関数は整数型変数のパラメータを 1 つ持つことが分かります。さらに外側の int と結合し、関数の戻り値が整数型データであることを示します。
  • int (*p)(int);-- p のところから始めて、最初に *(ポインタ)と結合し、p はポインタであることを示します。次に () と結合し、ポインタが指すのは関数であることを示します。さらに () の中の int と結合し、関数が int 型のパラメータを 1 つ持つことを示し、最も外側の int と結合して、関数の戻り値の型が整数型であることを示します。したがって、p は、整数型パラメータを 1 つ持ち、戻り値の型が整数型である関数へのポインタです。
  • int *(*p(int))[3];-- この型はスキップしてかまいません。複雑すぎます。p から始めて、最初に () と結合し、p は関数であることを示します。次に () の中に入り、int と結合して、関数は整数型変数のパラメータを 1 つ持つことを示します。さらに外側の * と結合して、関数の戻り値がポインタであることを示します。そして最も外側の層に進み、最初に [] と結合して、返されたポインタが指すのは配列であることを示し、次に * と結合して、配列の要素がポインタであることを示し、さらに int と結合して、ポインタが指す内容が整数型データであることを示します。したがって、p は、パラメータが整数データ 1 つであり、整数型ポインタ変数からなる配列を指すポインタ変数を返す関数です。

ここまで説明すればだいたい十分です。我々の任務もこれくらいです。これらの型を理解すれば、他の型は朝飯前です。ただし、通常はあまり複雑な型を使わないほうがよいでしょう。複雑な型を使うとプログラムの可読性が大幅に低下するため、慎重に使用してください。上記のいくつかの型で十分です。


一、ポインタ詳説

ポインタは特別な変数であり、その中に格納された数値はメモリ内のアドレスとして解釈されます。ポインタを理解するには、ポインタに関する 4 つの側面を理解する必要があります。ポインタの型、ポインタが指す型、ポインタの値(あるいはポインタが指すメモリ領域)、ポインタ自体が占めるメモリ領域です。それぞれ説明しましょう。

例として、まずいくつかのポインタを宣言しておきましょう。

例:

int *ptr; char *ptr; int **ptr; int (*ptr)[3]; int *(*ptr)[4];

1. ポインタの型

文法的に見ると、ポインタ宣言文からポインタ名を取り除けば、残った部分がそのポインタの型です。これはポインタ自体が持つ型です。例 1 の各ポインタの型を見てみましょう。

  • 1、int *ptr;: ポインタの型はint*
  • 2、char *ptr;: ポインタの型はchar*
  • 3、int **ptr;: ポインタの型はint**
  • 4、int (*ptr)[3];: ポインタの型はint(*)[3]
  • 5、int *(*ptr)[4]; : ポインタの型はint*(*)[4]

どうでしょうか?ポインタの型を見つける方法はとても簡単ですよね?

2. ポインタが指す型

ポインタを介してポインタが指すメモリ領域にアクセスするとき、ポインタが指す型によって、コンパイラがそのメモリ領域の内容を何として扱うかが決まります。

文法的には、ポインタ宣言文からポインタ名と、名前の左側にあるポインタ宣言子 * を取り除くだけで、残りがポインタが指す型になります。例えば:

  • 1、int*ptr;: ポインタが指す型はint
  • 2、char*ptr;: ポインタが指す型はchar
  • 3、int**ptr;: ポインタが指す型はint*
  • 4、int(*ptr)[3];: ポインタが指す型はint()[3]
  • 5、int*(*ptr)[4]; : ポインタが指す型はint*()[4]

ポインタの算術演算において、ポインタが指す型は大きな役割を果たします。

ポインタの型(すなわちポインタ自体の型)と、ポインタが指す型は、2 つの異なる概念です。C に慣れてくると、ポインタに関わる「型」という概念を「ポインタの型」と「ポインタが指す型」の 2 つに分けることが、ポインタを極めるための重要点の 1 つであることに気づくでしょう。多くの本を読みましたが、質の悪い本の中にはポインタのこれら 2 つの概念を混同しているものがあり、そのため読んでいると前後矛盾して、ますます分からなくなるのです。

3. ポインタの値——あるいはポインタが指すメモリ領域またはアドレス

ポインタの値とは、ポインタ自体に格納されている数値のことです。この値は、コンパイラによって通常の数値ではなくアドレスとして扱われます。32 ビットプログラムでは、すべての型のポインタの値は 32 ビット整数です。32 ビットプログラムのメモリアドレスはすべて 32 ビット長だからです。ポインタが指すメモリ領域は、ポインタの値が表すメモリアドレスから始まり、長さが sizeof(ポインタが指す型) であるメモリ領域です。以降、ポインタの値が XX であると言うとき、それはそのポインタが XX を先頭アドレスとするメモリ領域を指していることを意味します。また、ポインタが某メモリ領域を指していると言うとき、それはそのポインタの値がそのメモリ領域の先頭アドレスであることを意味します。ポインタが指すメモリ領域とポインタが指す型は、まったく別の概念です。例 1 では、ポインタが指す型はすでに決まっていますが、ポインタが初期化されていないため、ポインタが指すメモリ領域は存在しないか、意味を持ちません。

これからは、ポインタに出会うたびに、次のことを問うべきです。このポインタの型は何か?ポインタが指す型は何か?そのポインタはどこを指しているか?(重点注意)

4 ポインタ自体が占めるメモリ領域

ポインタ自体はどれだけのメモリを占めるのでしょうか? sizeof(ポインタの型) を使うだけです。32 ビットプラットフォームでは、ポインタ自体は 4 バイトの長さを占めます。ポインタ自体が占めるメモリという概念は、ポインタ式(後で説明します)が左辺値であるかどうかを判断するときに役立ちます。


二、ポインタの算術演算

ポインタは整数を加算または減算できます。このポインタの演算の意味は、通常の数値の加減算の意味とは異なり、要素単位で行われます。例えば:

char a[20]; int *ptr=(int *)a; // 強制型変換は a の型を変更しません ptr++;

上記の例では、ポインタ ptr の型は int* で、その指す型は int です。ptr は整数型変数 a を指すように初期化されています。次の 3 番目の文で、ポインタ ptr に 1 が加算されます。コンパイラは次のように処理します。ポインタ ptr の値に sizeof(int) を加えます。32 ビットプログラムでは 4 が加算されます。32 ビットプログラムでは int は 4 バイトを占めるからです。アドレスはバイト単位で扱われるため、ptr が指すアドレスは元の変数 a のアドレスから高位アドレス方向に 4 バイト移動します。char 型の長さは 1 バイトなので、元々 ptr は配列 a の 0 番目の要素から始まる 4 バイトを指していましたが、このときは配列 a の 4 番目の要素から始まる 4 バイトを指すことになります。ポインタとループを使って配列を走査できます。例を見てみましょう。

int array[20]={0}; int *ptr=array; for(i=0;i<20;i++) { (*ptr)++; ptr++; }

この例では、整数型配列の各要素の値を 1 ずつ加算します。ループのたびにポインタ ptr を 1 要素分進めるため、ループのたびに配列の次の要素にアクセスできます。

再看例子:

char a[20]="You_are_a_girl"; int *ptr=(int *)a; ptr+=5;

この例では、ptr に 5 が加算されましたが、コンパイラは次のように処理します。ポインタ ptr の値に 5 × sizeof(int) を加算します。32 ビットプログラムでは、5 × 4 = 20 を加算することになります。アドレスの単位はバイトなので、加算後の ptr が指すアドレスは、加算前の ptr が指すアドレスよりも、高位アドレス方向へ 20 バイト移動したことになります。

この例では、5 を加算する前の ptr は配列 a の第 0 号ユニットから始まる 4 バイトを指していましたが、5 を加算した後、ptr は配列 a の有効範囲外を指すことになります。このような状況は応用上問題を引き起こす可能性がありますが、構文上は問題ありません。これもポインタの柔軟性を示しています。もし上記の例で、ptr が 5 を減算された場合、処理の流れはほぼ同じですが、ptr の値は 5 × sizeof(int) だけ減算され、新しい ptr が指すアドレスは、元の ptr が指すアドレスよりも低位アドレス方向へ 20 バイト移動します。

それでは、もう一つ例を挙げさせてください(誤解しやすい例です)。

#include<stdio.h> int main() { char a[20]=" You_are_a_girl"; char *p=a; char **ptr=&p; //printf("p=%d\n",p); //printf("ptr=%d\n",ptr); //printf("*ptr=%d\n",*ptr); printf("**ptr=%c\n",**ptr); ptr++; //printf("ptr=%d\n",ptr); //printf("*ptr=%d\n",*ptr); printf("**ptr=%c\n",**ptr); }

誤解その一、出力結果は Y と o

誤った解釈: ptr は char 型の二级ポインタであり、ptr++; を実行すると、ポインタに sizeof(char) が加算されるため、上記のような出力結果になります。これはおそらくごく一部の人が陥る結果です。

誤解その二、出力結果は Y と a 誤った解釈: ptr は char * 型を指しており、ptr++; を実行すると、ポインタに sizeof(char *) が加算されます(この値を 1 と考える人もいるかもしれませんが、その場合は誤解その一の答えになります。この値は 4 であるべきで、前述の内容を参照してください)。つまり &p+4 となります。それでは、一度間接参照演算を行えば、配列の 5 番目の要素を指すことになるのではないでしょうか? では、出力結果は配列の 5 番目の要素になるのではないでしょうか? 答えは否です。

正しい解釈:ptr の型は char ** であり、指している型は char * 型です。その指しているアドレスは p のアドレス(&p)です。ptr++; を実行すると、ポインタに sizeof(char*) が加算され、つまり &p+4 になります。では *(&p+4) はどこを指すのでしょうか。それは神様に聞いてください。あるいは神様がどこにあるのか教えてくれるかもしれません。したがって、最終的な出力はランダムな値になるか、あるいは不正な操作になる可能性があります。

ですが、array をポインタとみなす場合、それは配列の第 0 号ユニットを指し、型は int* であり、指している型は配列ユニットの型、すなわち int です。したがって、*array が 0 に等しくてもまったく不思議ではありません。同様に、array+3 は配列の第 3 号ユニットを指すポインタなので、

ポインタ ptrold に整数 n を加算(減算)した結果は、新しいポインタ ptrnew です。ptrnew の型は ptrold の型と同じであり、ptrnew が指す型も ptrold が指す型と同じです。ptrnew の値は、ptrold の値より n × sizeof(ptrold が指す型) バイトだけ増加(減少)します。つまり、ptrnew が指すメモリ領域は、ptrold が指すメモリ領域よりも高位(低位)アドレス方向へ n × sizeof(ptrold が指す型) バイト移動します。ポインタ同士の加減算について: 2 つのポインタの加算はできません。これは不正な操作です。加算した結果は、どこを指すのか分からない場所を示し、しかも無意味だからです。2 つのポインタの減算は可能ですが、型が同じである必要があります。一般に配列で使用されますが、詳細は省略します。


三、演算子 & と *

ここで&はアドレス取得演算子、*は間接参照演算子です。

&aの演算結果はポインタであり、ポインタの型は a の型に * を加えたもの、ポインタが指す型は a の型、ポインタが指すアドレスは、すなわち a のアドレスです。

*pの演算結果はさまざまです。要するに *p の結果は p が指しているものです。このものには次の特徴があります。その型は p が指す型であり、それが占めるアドレスは p が指すアドレスです。

実例:

int a=12; int b; int *p; int **ptr; p=&a; //&a の結果はポインタであり、型は int*、指している型は //int、指しているアドレスは a のアドレスです。 *p=24; //*p の結果は、ここでは型が int であり、それが占めるアドレスは //p が指すアドレスです。明らかに、*p は変数 a です。 ptr=&p; //&p の結果はポインタであり、そのポインタの型は p の型に * を加えたもので、 //ここでは int ** です。そのポインタが指す型は p の型であり、 //ここでは int* です。そのポインタが指すアドレスは、ポインタ p 自身のアドレスです。 *ptr=&b; //*ptr はポインタであり、&b の結果もポインタです。そしてこれら 2 つのポインタの //型と指している型は同じなので、&b を使って *ptr に代入 //することはまったく問題ありません。 **ptr=34; //*ptr の結果は ptr が指しているものであり、ここではポインタです。 //このポインタに対してもう一度 * 演算を行うと、結果は int 型の変数になります。

四、ポインタ式

式の結果がポインタである場合、その式をポインタ式と呼びます。

以下はポインタ式の例です:

実例:

int a,b; int array[10]; int *pa; pa=&a; //&a はポインタ式です。 Int **ptr=&pa; //&pa もポインタ式です。 *ptr=&b; //*ptr と &b はどちらもポインタ式です。 pa=array; pa++; //これもポインタ式です。
char *arr[20]; char **parr=arr; //arr をポインタとみなすなら、arr もポインタ式です。 char *str; str=*parr; //*parr はポインタ式です。 str=*(parr+1); //*(parr+1) はポインタ式です。 str=*(parr+2); //*(parr+2) はポインタ式です。

ポインタ式の結果はポインタであるため、ポインタ式もポインタが持つ 4 つの要素を持ちます。すなわち、ポインタの型、ポインタが指す型、ポインタが指すメモリ領域、ポインタ自身が占めるメモリです。

さて、ポインタ式の結果のポインタが、ポインタ自身が占めるメモリを明確に持っている場合、そのポインタ式は左辺値です。そうでなければ左辺値ではありません。例七では、&a は左辺値ではありません。なぜなら、それはまだ明確なメモリを占めていないからです。*ptr は左辺値です。なぜなら、*ptr というポインタはすでにメモリを占めているからです。実際、*ptr はポインタ pa です。pa がメモリ内に自分の位置を持っている以上、*ptr も当然自分の位置を持っています。


五、配列とポインタの関係

配列の配列名は、実はポインタとみなすことができます。次の例を見てください:

int array[10]={0,1,2,3,4,5,6,7,8,9},value; value=array[0]; //value=*array; と書くこともできます。 value=array[3]; //value=*(array+3); と書くこともできます。 value=array[4]; //value=*(array+4); と書くこともできます。

上記の例では、一般的に配列名 array は配列自体を表し、型は int*(array+3)は 3 に等しくなります。その他もこれに準じます。

実例:

char *str[3]={ "Hello,thisisasample!", "Hi,goodmorning.", "Helloworld" }; char s[80]; strcpy(s,str[0]); //strcpy(s,*str); と書くこともできます。 strcpy(s,str[1]); //strcpy(s,*(str+1)); と書くこともできます。 strcpy(s,str[2]); //strcpy(s,*(str+2)); と書くこともできます。

上記の例では、str は 3 つのユニットからなる配列であり、この配列の各ユニットはポインタで、これらのポインタはそれぞれ文字列を指しています。ポインタ配列名 str をポインタとみなす場合、それは配列の第 0 号ユニットを指し、その型は char ** であり、指している型は char * です。

*str もポインタであり、その型は char *、指している型は char、指しているアドレスは文字列 "Hello,thisisasample!" の最初の文字のアドレス、すなわち 'H' のアドレスです。注意: 文字列は配列に相当し、メモリ内では配列の形式で格納されます。ただし、文字列は配列定数であり、内容は変更できず、右辺値のみです。ポインタとみなす場合、それは定数ポインタであり、かつポインタ定数でもあります。

str+1 もポインタであり、配列の第 1 号ユニットを指し、その型は char**、指している型は char* です。

*(str+1) もポインタであり、その型は char*、指している型は char、それは "Hi,goodmorning." の最初の文字 'H' を指します。

以下、配列の配列名(配列の中に格納されているものも配列である場合)の問題をまとめます:

配列TYPE array[n]を宣言した場合、配列名 array には 2 つの意味があります:

  • 第一に、それは配列全体を表し、その型はTYPE[n];

  • 第二に、それは定数ポインタであり、そのポインタの型はTYPE*、そのポインタが指す型はTYPE、すなわち配列ユニットの型です。そのポインタが指すメモリ領域は配列の第 0 号ユニットであり、そのポインタ自身は独立したメモリ領域を占有します。注意すべきは、それは配列の第 0 号ユニットが占めるメモリ領域とは異なることです。そのポインタの値は変更できず、つまり array++ のような式は誤りです。異なる式の中で、配列名 array は異なる役割を果たすことができます。式 sizeof(array) では、配列名 array は配列自体を表すため、このとき sizeof 関数が測定するのは配列全体のサイズです。

式 *array では、array はポインタの役割を果たすため、この式の結果は配列の第 0 号ユニットの値です。sizeof(*array)測定されるのは配列ユニットのサイズです。

式 array+n(ここで n=0,1,2,...)において、array はポインタの役割を果たすので、array+n の結果はポインタであり、その型はTYPE *、その指す型は TYPE であり、配列の第 n 番目の要素を指します。したがって sizeof(array+n) が測定するのはポインタ型のサイズです。32 ビットプログラムでは結果は 4 です。

例:

int array[10]; int (*ptr)[10]; ptr=&array;:

上記の例で ptr はポインタであり、その型はint(*)[10]、その指す型は int

この節では関数 sizeof() について触れましたが、ここで質問です。sizeof(ポインタ名) が測定するのは、一体ポインタ自身の型のサイズでしょうか、それともポインタが指す型のサイズでしょうか?

答えは前者です。例えば:

int(*ptr)[10];

すると 32 ビットプログラムでは、次のようになります:

sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4

実際には、sizeof(オブジェクト) が測定するのは常にオブジェクト自身の型のサイズであり、他の何らかの型のサイズではありません。


六、ポインタと構造体型の関係

構造体型オブジェクトを指すポインタを宣言できます。例:

struct MyStruct { int a; int b; int c; }; struct MyStruct ss={20,30,40}; //構造オブジェクト ss を宣言し、ss のメンバを 20、30、40 に初期化した。 struct MyStruct *ptr=&ss; //構造オブジェクト ss を指すポインタを宣言した。その型は //MyStruct * であり、その指す型は MyStruct である。 int *pstr=(int*)&ss; //構造オブジェクト ss を指すポインタを宣言した。しかし pstr と //それが指す型 ptr は異なる。

では、ポインタ ptr を通じて ss の3つのメンバ変数にアクセスするにはどうすればよいでしょうか?

答え:

ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者
ptr->b;
ptr->c;

また、ポインタ pstr を通じて ss の3つのメンバ変数にアクセスするにはどうすればよいでしょうか?

答え:

*pstr; //访问了ss 的成员a。
*(pstr+1); //访问了ss 的成员b。
*(pstr+2) //访问了ss 的成员c。

私は自分の MSVC++6.0 で上記のコードをデバッグしたことがありますが、このように pstr を使って構造体メンバにアクセスするのは正規の方法ではないことを知っておく必要があります。なぜ正規でないかを説明するために、ポインタを通じて配列の各要素にアクセスする方法を見てみましょう。(構造体を配列に置き換えます)

int array[3]={35,56,37}; int *pa=array; //ポインタ pa を通じて配列 array の3つの要素にアクセスする方法は: *pa; //第0番目の要素にアクセスした *(pa+1); //第1番目の要素にアクセスした *(pa+2); //第2番目の要素にアクセスした

書式から見ると、ポインタを通じて構造体メンバにアクセスする正規でない方法の書式と同じですね。

すべての C/C++ コンパイラは、配列の要素を並べるとき、常に各配列要素を連続した記憶領域に格納し、要素と要素の間に隙間はありません。しかし、構造体オブジェクトの各メンバを格納するとき、あるコンパイル環境では、ワード整列やダブルワード整列、あるいは他の整列が必要になる場合があり、隣接する2つのメンバの間に複数の「パディングバイト」を追加する必要があります。これにより、各メンバの間に数バイトの隙間が生じることがあります。

したがって、例12では、*pstr が構造体オブジェクト ss の最初のメンバ変数 a にアクセスできたとしても、*(pstr+1) が必ず構造体メンバ b にアクセスできるとは保証されません。メンバ a とメンバ b の間にはパディングバイトがあるかもしれず、もしかすると *(pstr+1) はまさにそのパディングバイトにアクセスしてしまうかもしれません。これもポインタの柔軟性を示しています。もし各構造体メンバの間にパディングバイトがあるかどうかを確かめたいという目的なら、これはなかなか良い方法です。

ただし、ポインタで構造体メンバにアクセスする正しい方法は、例12のようにポインタ ptr を使う方法であるべきです。


七、ポインタと関数の関係

ポインタを関数へのポインタとして宣言できます。

int fun1(char *,int);
int (*pfun1)(char *,int);
pfun1=fun1;
int a=(*pfun1)("abcdefg",7); //通过函数指针调用函数。

ポインタを関数の仮引数にすることができます。関数呼び出し文では、ポインタ式を実引数として使えます。

int fun(char *); inta; char str[]="abcdefghijklmn"; a=fun(str); int fun(char *s) { int num=0; for(int i=0;;) { num+=*s;s++; } return num; }

この例の関数 fun は、文字列中の各文字の ASCII コード値の合計を計算します。前述のとおり、配列名もポインタです。関数呼び出しで str を実引数として仮引数 s に渡すと、実際には str の値が s に渡され、s が指すアドレスは str が指すアドレスと一致します。しかし、str と s はそれぞれ自分自身の記憶領域を占有します。関数本体内で s に対してインクリメント演算を行っても、それは同時に str に対してインクリメント演算を行ったことにはなりません。


八、ポインタ型変換

ポインタを初期化したりポインタに値を代入したりするとき、代入演算子の左辺はポインタ、右辺はポインタ式です。前述の例のほとんどでは、ポインタの型とポインタ式の型は同じであり、ポインタが指す型とポインタ式が指す型も同じです。

float f=12.3; float *fptr=&f; int *p;

上記の例で、ポインタ p に実数 f を指させたいとしたら、どうすればよいでしょうか?

次の文を使うのでしょうか?

p=&f;

違います。なぜなら、ポインタ p の型は int * であり、その指す型は int です。式 &f の結果はポインタであり、そのポインタの型は float *、その指す型は float です。

両者は一致しないため、直接代入する方法は使えません。少なくとも私の MSVC++6.0 では、ポインタへの代入文は代入演算子の両側の型が一致し、指す型も一致していることが要求されます。他のコンパイラでは試していませんが、皆さんは試してみてください。目的を達成するには「強制型変換」を行う必要があります:

p=(int*)&f;

ポインタ p があり、その型と指す型を次のように変更する必要がある場合、TYEP *TYPEその場合の構文形式は次のとおりです:(TYPE *)p

この強制型変換の結果は新しいポインタであり、その新しいポインタの型はTYPE *、その指す型はTYPE、その指すアドレスは元のポインタが指すアドレスです。そして元のポインタ p のすべての属性は変更されません。(必ず覚えておくこと)

関数がポインタを仮引数として使う場合、関数呼び出し文の実引数と仮引数の結合過程で、型が一致することを保証しなければなりません。一致しない場合は強制変換が必要です:

void fun(char*); int a=125,b; fun((char*)&a); void fun(char*s) { char c; c=*(s+3);*(s+3)=*(s+0);*(s+0)=c; c=*(s+2);*(s+2)=*(s+1);*(s+1)=c; }

注意、これは32ビットプログラムなので、int 型は4バイト、char 型は1バイトを占めます。関数 fun は、整数の4バイトの順序を逆転させる働きをします。気づきましたか?関数呼び出し文で、実引数 &a の結果はポインタであり、その型は int *、指す型は int です。仮引数のポインタの型は char *、指す型は char です。したがって、実引数と仮引数の結合過程で、int * 型から char * 型への変換を一度行わなければなりません。

この例に照らして、次のように考えることができます:

コンパイラが変換を行う過程を想像してみてください。コンパイラはまず一時ポインタ char *temp を構築し、次に temp=(char *)&a を実行し、最後に temp の値を s に渡します。したがって最終結果は次のようになります:s の型は char *、指す型は char、指すアドレスは a の先頭アドレスです。

すでに知っているように、ポインタの値はポインタが指すアドレスです。32ビットプログラムでは、ポインタの値は実際には32ビット整数です。

では、整数をポインタの値として直接ポインタに代入することはできるでしょうか?例えば次のような文です:

unsigned int a; TYPE *ptr; //TYPE は int、char、または構造体型などの型である。 a=20345686; ptr=20345686; //目的はポインタ ptr にアドレス 20345686 を指させることである ptr=a; //目的はポインタ ptr にアドレス 20345686 を指させることである //コンパイルしてみよう。結果、後ろの2つの文はすべて誤りであることがわかる。では、目的は達成できないのか?いいえ、まだ方法がある: unsigned int a; TYPE *ptr; //TYPE は int、char、または構造体型などの型である。 a=N //N は正当なアドレスを表さなければならない; ptr=(TYPE*)a; //ふふ、これでよいのだ。

厳密に言うと、ここでの (TYPE *) はポインタ型変換における (TYPE *) とは少し異なります。ここでの (TYPE*) は、符号なし整数 a の値をアドレスとして扱うことを意味します。上で強調したように、a の値は正当なアドレスを表さなければなりません。そうでない場合、ptr を使うときに不正な操作エラーが発生します。逆に、ポインタが指すアドレス、すなわちポインタの値を整数として取り出すことはできないでしょうか。完全に可能です。次の例は、ポインタの値を整数として取り出し、その整数をアドレスとしてポインタに代入することを示しています:

int a=123,b; int *ptr=&a; char *str; b=(int)ptr; //ポインタ ptr の値を整数として取り出す。 str=(char*)b; //この整数の値をアドレスとしてポインタ str に代入する。

これで、ポインタの値を整数として取り出すことも、整数値をアドレスとしてポインタに代入することもできることがわかりました。


九、ポインタの安全性の問題

次の例を見てください:

char s='a'; int *ptr; ptr=(int *)&s; *ptr=1298;

ポインタ ptr は int * 型のポインタであり、その指す型は int です。それが指すアドレスは s の先頭アドレスです。32ビットプログラムでは、s は1バイトを占め、int 型は4バイトを占めます。最後の文は、s が占める1バイトを変更するだけでなく、s に隣接する高アドレス方向の3バイトも変更します。この3バイトは何のためにあるのでしょうか?それはコンパイラだけが知っており、プログラムを書く人にはまず知ることはできません。もしかするとこの3バイトには非常に重要なデータが格納されているかもしれませんし、ちょうどプログラムの1つのコードかもしれません。そして、ポインタの不注意な使用によって、この3バイトの値が変更されてしまうのです!これは致命のなエラーを引き起こします。

もう一つの例を見てみましょう:

char a; int *ptr=&a; ptr++; *ptr=115;

この例は完全にコンパイルでき、実行もできます。しかし、わかりますか?3番目の文でポインタ ptr に対してインクリメント演算を行うと、ptr は整数型変数 a に隣接する高アドレス方向の記憶領域を指します。この記憶領域には何があるのでしょうか?私たちにはわかりません。それは非常に重要なデータかもしれず、あるいはコードかもしれません。

そして4番目の文はなんとこの記憶領域にデータを書き込んでしまうのです!これは深刻なエラーです。したがってポインタを使用する際、プログラマは心の中で明確に理解しておかなければなりません:私のポインタは一体どこを指しているのか。ポインタを使って配列にアクセスする際にも、配列の下限と上限の境界を超えないように注意しなければなりません。超えてしまうと同様のエラーを引き起こす可能性があります。

ポインタの強制型変換において:ptr1=(TYPE *)ptr2中で、もし sizeof(ptr2の型) が sizeof(ptr1の型) より大きい場合、ポインタ ptr1 を使って ptr2 が指す記憶領域にアクセスするのは安全です。もし sizeof(ptr2の型) が sizeof(ptr1の型) より小さい場合、ポインタ ptr1 を使って ptr2 が指す記憶領域にアクセスするのは安全ではありません。その理由については、読者が例十八と照らし合わせて考えてみれば、理解できるはずです。