C ポインタ

提供:yonewiki
2022年9月26日 (月) 10:46時点におけるYo-net (トーク | 投稿記録)による版
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

C++へ戻る


※このページではC言語にも存在していたという意味で記事タイトルがC ポインタになっていますが、
C++でも同様です。C++だけの機能がある場合は明記します。

ポインタ

 ポインタ技術は敷き詰めると1冊の本が描けるくらい、深い技術です。ここでは、基本的なことを説明しています。ポインタのより深い理解のためのポインタ応用の記事は別途作成の予定です。


 プログラムを組む上で、各種の型の変数を使いますが、変数自身はコンピュータ上のメモリ空間に保存されます。 ポインタはそのメモリ空間のアドレスを保持するための変数です。int型変数のためのint型のアドレス変数みたいなもの


 ここで少しメモリ空間についてのイメージを少しだけもってもらえるために、補足しておきます。
 メモリ空間にはアドレス番号と呼んでいるものが割り振られています。日本の空間の地図上のどこにあなたの家があるかを指し示す住所と同じようなものを、単なる番号だけで定義しているのです。半導体メモリDRAMについて少し調べると長方形上の敷地3cm*4cm位の中に4~8つほどの長方形の敷地があって、全部で4G個(個って書きましたが、情報処理用語としてはbit(ビット)という単位が使われます。Gはギガで10の9乗で10億)ほどの記憶領域があることがわかります。


 更にその3cm~4cmの敷地を同じ樹脂の中に複数枚重ねたり、その樹脂の塊を電子回路基盤に1個~16個くらいとりつけていたり、その電子回路基板を複数個とりつけたりして、現在のNotePCとかでは8GByte(8bitで1Byte(バイト))という容量のものを取り付けて販売している状況です。ある人はプログラム的には一本に並んだ数十億の箱だと言ったりしますが、物理的には、DRAMメモリのある敷地の中のある一点だったり、HDD上においやられた領域だったりするわけです。


 ※単位についてですが、K(キロ)は普通10の3乗で1000ですが、厳密にはコンピュータでは2の10乗の1024Byteが1KByteになります。2の10乗KByteの1024KByteで1MByte(メガバイト)ですし、1024MByteで1GByteとなります。但し、普通に1000Byteを1KByteとしたり、1000KByteを1MByte、1000MByteを1GByteとしている。ざっくりとした仕様でコンピュータの性能を表現しているものが一般的です。情報処理用語として1TByte(テラバイト テラは10の12乗)と表現した場合、1兆0995億1162万7776Byteですが、ざっくり仕様が使われている各種メーカの容量では、実際には10の12乗Byteで995億1162万7776Byte少ないです。仕様書の隅っこに1000kByteを1Byteとして計算していますとか明示してあります。


 CPU(Central Processing Unit)とよばれる中央処理装置にも記憶領域があります。キャッシュとよばれています。HDD(Hard Disk Drive)やSSD(Solid State Drive)にも処理記憶領域は作れます。いろいろなところに作れますがアドレスは番号だけで管理されています。CPUの中にある記憶領域を使った場合が最も早く処理され、次にDRAM、そしてSSD、HDD、その他という具合に保存されている場所によっても処理速度が異なるのも特徴です。記憶している領域により処理速度が低下する問題を解決する手法もよく使われる命令を処理速度の高いところにおいたりと、いろいろな試みがなされています。いろいろな試みについてはWindowsやUnixといったOS(Operating System)側が制御します。


 少し、脱線しましたが、記憶領域にはアドレス番号が割り振られているということでした。また更に脱線すると、32bitOSとか64bitOSという種類がありますが、
このアドレス番号の割り振りに使われる桁数に違いがあります。32bitなら2の32乗個のアドレス42億9496万7296個、64bitなら2の64乗個のアドレス1844京6744兆0737億0955万1616個が使えるということになります。 1Gは10億7374万1824Byteでしたから、40億ちょいっつうのは4GByteのことで、32bitOSでは4GByteまでしか使えないということになります。


 という簡単な基礎知識を頭の片隅に本題に入ります。
 例えば、Visual Studio2012ではint型は4Byteの変数で使える値の種類は2の4*8bit=32bitで2の32乗=42億9496万7296となります。実際には負の数も表現できますので、先頭1ビットは正負の記号を表し、0なら正、1なら負数で全部32bit全部が1のときが-1です。2進数の補数ってのを勉強しないとダメかも、負数の最大は100000…0000001で、-21億4748万3646~21億4748万3647が表現できる値です。その4Byteを収めてるアドレスは64Bitアドレスなら1844京…の中のどこかから4Byte分のアドレスを使ってるということです。全部のアドレス分の資源16EByte(Eはエクサで10の18乗で100京)がコンピュータに備わっていればの話ですが... とにかく、そういったアドレスを覚えさせるのが、ポインタ変数です。


ポインタ変数は
型名 *変数名;
で定義(Definition)できます。
例1:定義

#include <iostream>
int main() {
    int *pintNum; //ポインタ変数定義
    int intNum;  //通常のint型変数
    return 0;
}


ポインタのややこしいところは、ポインタ変数を使って代入するときも同じく*を使う事にあるのかもしれません。定義と混同しやすいです。
例2:ポインタ変数での代入

#include <iostream>
int main() {
    int *pintNum;
    int intNum = 10;
    pintNum = new int;
    *pintNum = 10; //*を使ってポインタ変数が保持しているアドレスの中身に10を代入。ポインタつまりはアドレスを10にしてる訳ではない。
    std::cout << "intNum=" << intNum << "\n"; 
    std::cout << "*pintNum=" << *pintNum << "\n";
    delete pintNum; 
    return 0;
}

実行結果※サンプルの動かし方はCpp クラスの一番下に記述があります。

intNum=10
*pintNum=10

pintNumはint型のアドレスを保持するための変数ですが、ポインタ変数の宣言以外の所で*pintNumと*をポインタ変数の前につけるとアドレスが指し示す中身を、定義されている型にしたがって代入したり、内容を取り出したりすることができます。但し、最初に宣言された段階ではint型のアドレスを保持できるだけで、アドレスは保持していません。つまり、その実体、内容つまり中身、も*pintNumと指定したとしても、中身への代入はできません。したがって宣言した直後に *pintNum=10; としてもエラーになります。


その問題を解決するのがnew演算子です(new演算子はC++だけでしか使えないので、Cの場合はmallocというメモリ確保関数とfreeというメモリ解放関数を駆使します。)。new演算子はポインタ変数の型と同じint型で、ポインタが指し示す先頭アドレスの生成と先頭アドレスからint型の分だけメモリ領域を確保し、ポインタ変数にアドレスを格納します。この結果アドレスの中身の方には名前がない状態ではありますが、ポインタ変数からでのみ操作できる変数が生成されたことになります。名もなき実体のアドレスをポインタ変数だけがしっているということです。delete演算子では、そうやって生成した名もなき実体のメモリ領域を解放することになります。もうここから先では使いません。どうぞご自由にという感じです。その宣言がされるまではずっとメモリ領域を確保し続けます。最近ではプログラムが終了したときに、このような手法でメモリ領域を確保した部分の解放処理をしてくれるようになりましたが、その昔、これを解放せずにプログラムが終わるという事態が多発していたのも事実です。使い終わったらいち早くかたずけることで資源を有効に利用できるのです。プログラム終了時に全部の片づけをやってくれてるのかは知りませんが…そういうことらしいです。


そして、アドレスそのものを使っての変数同士のやりとりは以下の例ように記述します。
例3:ポインタ変数で実際にアドレスを使う手法

#include <iostream>
int main() {
    int *pintNum;
    int intNum;
    intNum = 50;
    pintNum = &intNum; //これでpintNumはintNumのアドレス番号を記憶。&intNumで変数のアドレスを返す。 
    std::cout << "intNum=" << intNum << "\n"; 
    std::cout << "*pintNum=" << *pintNum << "\n"; 
    std::cout << "pintNum=" << pintNum << "\n"; 
    std::cout << "sizeof pintNum=" << sizeof(pintNum) << "\n"; 
    return 0;
}

実行結果

intNum=50
*pintNum=50
pintNum=00000000001BFC34  //ここの結果はプログラムの書き方を変えたり、プロジェクト設定環境が違うと異なる数字になります。64bit設定にするとアドレスの桁が数が増えるのは確か。
sizeof pintNum=8          //ここの結果はプログラムプロジェクト設定や環境が違うと異なる数字になります。

上記のように変数名の前に&をつけるとアドレスを返してくれる。このときの&をアドレス演算子と呼んでいます。したがってもともとアドレス変数であるpintNumに対しても&アドレス演算子をつけることができ、アドレス番号を格納しているアドレスが返ります。プログラム上ではポインタ変数はint型の変数と同じアドレスを見ることで、名前は違っても同じものを指し示すことができるというのが上記の例です。アドレス番号っていったのに数字じゃないのが返って来てるよ?って言う方は16進数や2進数を勉強する必要があります。16進数での表示がされています。


pintNumのアドレスはポインタ変数のアドレスで&pintNumですが、更に*をつけると*&pintNumとできこれはpintNumと同じになります。sizeof演算子は、引数に指定した型や値の大きさ、つまりアドレスをいくつ分、使っているのか、使うのかをbyte単位で返却してくれる演算子です。sizeof(pintNum)とすると32bitアプリケーションのプロジェクトならアドレスの大きさは4Byteなので4が返ってくるし、64bitアプリケーションのプロジェクトならアドレスの大きさは8Byteですから8が返ってきます。64bitPCでもプロジェクト設定がWin32なら4が返ってきます。64bitPCでVisualStudio2012をお使いの場合には、変更できると思っているのですが、※どういった組み合わせなら64bitアプリが作れるか掌握していないです。プロジェクトの設定をx64(64bit対応)に変更するには、Visual Studio2012のメニューのプロジェクト(P)->「プロジェクト名(ココも名前の付け方で違うと思います)」のプロパティ(P)で表示されるダイアログの右上隅にある 構成マネージャー(O)...を押下して表示されるダイアログでプロジェクトのコンテキスト(ビルドまたは配置するプロジェクト構成をチェック)(R)からアクティブソリューションプラットフォーム(P)のリストから<新規作成...>を選択して表示されるダイアログで、新しいプラットフォーム(P)からx64を選択して、あとは変更しないでOKボタンを押下すると新しいプラットフォーム設定にx64が追加され、アクティブになるので、これでx64環境向けのプロジェクトにできます。独り言になりますが、プラットフォームにARMも選択できるんすね。


アドレス覚えるだけだから、ポインタの宣言で型名なんているの?って思った人はC言語初心者の中でも少し洞察力が高いと思う。残念ながら型名はいります。その指し示しているアドレスからどこまでを変数として使っているかを把握する必要があるからです。代入もできるわけですから、さらには、変数が配列になっている場合には、インクリメント演算子で次の配列番号に移動できたりもします。まぁ少しづつ応用していきましょう。ここではポインタとは何かを理解できればよいと思います。文字列は配列ありきなので、ポインタの扱いがさらに複雑化してきますし、構造体やクラスのポインタも複雑な操作が必要になります。


ポインタが配列になる場合の例

int pnIntArr[3] = {100,110,120};
int* pnIntArrPos;

printf("★pnIntArrPosポインタ変数出力(配列操作)\n");
pnIntArrPos = pnIntArr;//pnIntArr配列の先頭アドレスをpnIntArrPosに。pnIntArrPos = &pnIntArr[0];と同じこと。
printf("pnIntArrPos + 0 :%d\n",*pnIntArrPos);

pnIntArrPos = pnIntArrPos + 1;//ポインタ変数に整数値を加えると、ポインタ変数の型のサイズだけアドレスが進む。つまり次の配列のアドレスになる。
printf("pnIntArrPos + 1 :%d\n",*pnIntArrPos);

pnIntArrPos = pnIntArrPos + 1;//繰り返しアドレスを進めてみる。
printf("pnIntArrPos + 2 :%d\n",*pnIntArrPos);
printf("\n");

//pnIntArrで宣言した配列もポインタ変数
printf("★pnIntArr配列変数出力(配列はポインタであることを示す)\n");

printf("pnIntArr    + 0 :%d\n",*pnIntArr);

//ポインタ変数のアドレスを変更しないで、出力する例
printf("pnIntArr    + 1 :%d\n",*(pnIntArr + 1));//*(pnIntArr + 1)は、pnIntArr[1]と同じです。

printf("pnIntArr    + 2 :%d\n",*(pnIntArr + 2));//繰り返し次の要素も出力。
printf("\n");

//*(pnIntArr + 2)のカッコを付け忘れると、*pnIntArr + 2 は、*(pnIntArr) + 2 のような解釈で、0番目の要素 + 2になっちゃいます。
printf("★pnIntArr配列変数出力の指定間違い\n");
printf("(pnIntArr)  + 2 :%d Miss!\n",*pnIntArr + 2);
</syntaxhighlight2>
出力結果
<syntaxhighlight2 lang="text">
pnIntArrPosポインタ変数出力(配列操作)
pnIntArrPos + 0 :100
pnIntArrPos + 1 :110
pnIntArrPos + 2 :120

pnIntArr配列変数出力(配列はポインタであることを示す)
pnIntArr    + 0 :100
pnIntArr    + 1 :110
pnIntArr    + 2 :120

pnIntArr配列変数出力の指定間違い
(pnIntArr)  + 2 :102 Miss!

最初に配列を勉強すると、このようなポインタの表現が理解しづらいものに感じられますが、配列の[]を使った表現はポインタ変数の特殊な表記方法と思った方がいいのかもしれません。[]で配列を定義するとアドレス変数としての意識が薄れ、配列変数がポインタの役割をもっていることを忘れてしまいそうになります。例えば int a[1] = {200 , 210};のように定義するとaというint型の変数を定義したかのように錯覚してしまって混乱するのだと思います。それで、printf("%d", a);で要素0番目の出力をしようとしたりする間違いをしてしまうだろうなぁと思う。[] を使った配列宣言は初期値が設定できるのと、要素番号でメモリの確保ができるのが利点です。ポインタが配列変数であるためには初期化された配列のアドレスを上記の例のように受け取るか、new演算子やmalloc関数で動的にメモリを確保し生成する必要があります。

char* pcStrNew;
pcStrNew = new char[256];

delete[] pcStrNew;

ポインタ変数は所詮アドレスを保持しているだけの32bitアプリなら4byteの固定長の変数です。プログラムの中で配列変数を指し示すようになるって感じですが、アドレスのインクリメントとかで、メモリを確保していないアドレスとかに行くとプログラムは不正な処理をしたことになりますので注意が必要です。ポインタはどこへでもいけるけど、知らない場所にいくとすぐ検挙されます。悪さをしちゃいけないです。


だから自分は配列の[]を使った時はポインタの変数としての役割と同じものを宣言したことを意識するべくpというプレフィックスを付けて、その後に型名のプレフィックスとしてintならn、charならc、wchar_tならwcという名前をつけ、配列にはStrもしくはArrという表記をつけるようにしています。そうすると、

int pnAArr[1] = {200,210};

char pcStrComment[] = ”コメント”;

char cSingle = ’A’;

のように定義でき、あとからpcStrCommentはポインタで、char* pcStrPos;char* pcPos;のような変数でpcStrPos = pcStrComment;という代入がプレフィックスが一致していることで正しいアドレス代入だと確信できます。 これを配列を使ったがばっかりにchar cStrComment[] = ”コメント”;としてしまうとpcPos = &cSinge;のようにできるとしてpcStrPos = cStrComment;って&がいるんだっけ?という迷いはなくなります。


ポインタ関連記事:
C 文字列
C 文字列配列※2次元配列


C++へ戻る