C言語とC++の参考書を読み終えたのだけど、ポインタって奴の使い道がイマイチよく分からない。無理に使わなくてもいいんじゃない?
ポインタは実装するプログラムにもよりますが、例えば取り扱うファイルサイズが分からないようなプログラムを組む場合には必要な知識となります。
こんにちは。いちエンジニアのつちもぐらです。
C言語は30歳を過ぎてから仕事で使わざるを得なくなったので、頑張って勉強しました。
C言語とC++の参考書を一通り勉強した後「結局これ何に使うんだろ? 面倒そうだし、無理に使う事はないんじゃない?」と当時は思ったものです。
この記事が現在同じような思いをしている人達の役に少しでも立てば幸いです。
ポインタって何?
ポインタはメモリのアドレスを格納します。
パソコンにしてもスマホにしてもハードの構成は
CPU
↕︎
メモリ(ポインタ変数はこのアドレスを格納する)
↕︎
ストレージ(ハードディスク/SSD)
となっています。
プログラムの処理を開始すると、ストレージに保存されたプログラムはメモリへと展開され、CPUとやりとりをしながらプログラムは処理を進めます。
メモリにはアドレス(番地)が割り振られており、そのアドレスには
アドレス | データ/コマンド |
0xA0 | 0x3B82 |
0xA1 | 0x87BA |
0xA2 | 0xCBAD |
0xA3 | 0x4589 |
というように、プログラムを動かすためのデータやコマンドを機械語に変換した値が展開されています。
このイメージを持っているか否かで、ポインタの理解度はグッと変わって来ます。
ポインタのアドレスを出力するプログラム
初心者向けの参考書でよく見るコードを記載します。
#include<stdio.h> int main(void) { int a = 10; int *p; p = &a; printf("address=%x\n", p); printf("data=%d\n", *p); return 0; }
6行目でポインタ変数pを宣言して7行目で変数aのアドレスを代入、9行目でメモリのアドレスを、10行目でそのアドレスに格納されたメモリの中身を確認しています。
実行結果は以下となります。
address=e6be4b04 data=10
addressの出力値は実行する度に変わりますが、これはプログラムを実行する時にPC等のハード側から見て都合が良いメモリ位置にプログラムを展開しているからです。
このコードから、
- 変数が格納されたメモリのアドレスを取り出すには、変数名に&をつける。
- ポインタ変数に代入されたアドレスが指すメモリ位置に格納された値を確認するには、ポインタ変数名に*をつける。
という事が学べます。
ただこの時点で「よし、これからポインタを積極的に使っていこう!」と思う人は皆無かなと思います。記述が面倒になっているだけなので。。。
ポインタ変数を使った関数への参照渡し
C言語やC++は関数の戻り値を1つしか持てないので、2以上の出力を持つ関数を実装しようとした場合、参照という仕組みを使う必要があります。
C言語の参照渡し
#include<stdio.h> void func_ref (int a, int *b, int *c) { *b = a+10; *c = a*100; } int main(void) { int b,c; func_ref(10,&b,&c); printf("b=%d\n", b); printf("c=%d\n", c); return 0; }
関数func_refの出力としたい変数b,cをポインタ変数として、関数の引数とします。(3行目)
演算結果はポインタ変数に格納されたアドレスが指すメモリ位置に格納します。(5,6行目)
そしてfunc_ref関数の呼び出し側のmain関数では、変数b,cを定義して、変数のアドレスを&を使って関数に渡しています。(12行目)
実行結果は以下となります。
b=20 c=1000
これがC言語でポインタを使わざるを得ない状況の1つとなります。
ただこの手続きは誰もが面倒と思ったのか、C++で改良されています。
C++の参照渡し
include<cstdio> void func_ref (int a, int &b, int &c) { b = a+10; c = a*100; } int main(void) { int b,c; func_ref(10,b,c); printf("b=%d\n", b); printf("c=%d\n", c); return 0; }
こちらは関数func_refで出力としたい変数b,cの頭に&をつけているだけです。(3行目)
C++はこの記述だけで関数への参照渡しが実現出来ます。
なのでC++のユーザーは
関数の出力としたい変数の頭に&を付ければ良い。
と覚えれば、コーディング中にポインタを意識しなくて済みます。
結果としてコーディングにかかる時間やミスが減るので、C++を使用できるユーザーはこちらの記述を選択すべきでしょう。
動的にメモリを確保したい場合
C言語やその上位互換であるC++はコンパイル言語なので、プログラムが必要とする変数の型や個数、サイズを事前にコード中で定義しておく必要があります。
しかしそれだけでは扱うサイズがプログラム実行まで不明なデータを入力とするコードを実装出来ないので、C言語/C++ではポインタを使って動的にメモリを確保する仕組みが用意されています。
ポインタなんて使わずにとりあえず大き目のサイズの配列を定義しておいて、仕様として最大サイズを明記しておけばいいんじゃ・・・と考えた人がいるかもですが、
・入力データが少量の場合でも巨大なメモリを必要とする。
・入力データが最大サイズを超えてしまった場合にコードの修正が必要となる。
という理由から、その方法は少なくとも仕事では認められないでしょう。
サンプルコードと実行結果
プログラムを実行するまで入力サイズが分からない画像データを扱うために、動的メモリを確保するC++ソースコードの例を示します。
このコードでは便宜上、画像データと見立てた乱数を確保したメモリへ格納しています。
#include<cstdio> #include<cstdlib> int main(int argc, char *argv[]) { if ( argc <= 4 ) { printf("usage : \"main.exe\" width hight coordX coordY\n"); return 1; } // 外部からの引数を内部変数へ代入。 int width = atoi(argv[1]); int hight = atoi(argv[2]); int coordX = atoi(argv[3]); int coordY = atoi(argv[4]); // ポインタ変数を宣言しメモリを割当。 int *img; img = new int [width*hight]; if(!img){ printf("Memory allocate is failed!\n"); return 1; } // 割当たメモリへ画像データと見立てた乱数を代入。 // 代入前の座標のpixel値を標準出力。 for (int y=0; y<hight; y++){ for (int x=0; x<width; x++){ int pixel = rand()%256; img[y*width+x] = pixel; printf("(x,y)=(%d,%d); pixel=%03d\n" ,x,y,pixel); } } // 外部変数で指定した座標のpixel値を割当たメモリから呼び出し。 printf("img[%d][%d]=%03d\n", coordX,coordY,img[coordY*width+coordX]); delete [] img; eturn 0; }
画像サイズを水平5画素,垂直3画素、座標を(x,y=(1,1)としたプログラムの実行結果は以下となります。
tutimogura@MacBookAir 03_new % ./new_test.exe 5 3 1 1 (x,y)=(0,0); pixel=167 (x,y)=(1,0); pixel=241 (x,y)=(2,0); pixel=217 (x,y)=(3,0); pixel=042 (x,y)=(4,0); pixel=130 (x,y)=(0,1); pixel=200 (x,y)=(1,1); pixel=216 (x,y)=(2,1); pixel=254 (x,y)=(3,1); pixel=067 (x,y)=(4,1); pixel=077 (x,y)=(0,2); pixel=152 (x,y)=(1,2); pixel=085 (x,y)=(2,2); pixel=140 (x,y)=(3,2); pixel=226 (x,y)=(4,2); pixel=179 img[1][1]=216
ソースコードの解説
外部からの引数チェックと取り込み(6〜15行目)
このプログラムでは外部入力の変数が4つ(画像の水平サイズ、垂直サイズ、取り出したいpixel値のx座標、座標)必要なので、足りない場合はメッセージを出力してエラー終了としています。(6〜9行目)
次に外部入力した変数(文字列)を標準ライブラリatoi()を用いてint型の変数としてプログラムの内部に取り込みます。(12〜15行目)
ポインタの宣言と必要なメモリの割当(18〜23行目)
19行目でnew演算子を用いて、必要なサイズのメモリを割当ています。(C言語ではmalloc()関数でメモリを割り当てます。)
1pixelが1つのintサイズ(4バイト)データとすると、画像データの水平サイズ × 垂直サイズ x int(4バイト)のデータが必要となるので、
img = new int [width*hight];
とnew演算子を用いて、ポインタ変数imgに処理で必要となるメモリを割当てます。
20~23行目では正常にメモリが割り当てられたかチェックをしていますが、まずエラーとなる事はないでしょう。(他のプログラムがバックでゴリゴリと動いていて、メモリを相当圧迫している状況でもなければ)
pixel値(乱数)の生成とメモリへの代入(27〜33行目)
外部入力されたhigthとwidthの値にしたがって、確保したメモリ空間(img)へpixel値と見立てた乱数を代入しています。
また確認用に、代入直後のpixel値を標準出力しています。(31行目)
画像データから座標を指定して取り出し(36行目)
外部変数で指定したX座標(coordX)とY座標(coordY)を用いて、画像データを格納したメモリ(img)から、対応する座標のpixel値を呼び出しています。
実行結果から、正しく呼び出せている事が分かります。
new演算子で確保したメモリの開放(37行目)
delete演算子でポインタ変数imgに割り当てたメモリを開放しています。
不要になったメモリはどんどん開放していかないと、処理を続ける程にどんどんメモリを圧迫していく残念なプログラムとなってしまいます。
ただしプログラムが終了すればメモリは開放されるので、今回のような短いサンプルプログラムでは記述しなくても問題とはなりません。
最後に
C言語/C++初学者のほとんどがポインタでつまずくと言われています。
つまづく理由としてはポインタの文法よりも、その概念が理解出来ないのが原因みたいです。
そのため今回の記事では、メモリとCPUやストレージとの関係やメモリのアドレスマップを簡単ではありますが記載してみました。
今回の記事がポインタを学ぶエンジニアや学生の一助となれば幸いです。