C言語ポインタのメリットとわかりやすい使い方(オブジェクトを知って使いこなそう)

現在、様々な場面でシステム開発が行われますが、それぞれ最適なプログラミング言語が使用されます。

そんな中でも、C言語は非常に多くのエンジニアが開発を行っており、未だに稼働しているシステムは多いです。

C言語の中で、非常に重要な概念の一つが「ポインタ」です。

このC言語のポインタとは何でしょうか?

ざっくりと説明すると、ポインタとは
「別の変数のアドレスを格納している変数」
のことです。

C言語ポインタは、配列や構造体、関数の引数として利用されるほか、メモリ領域の動的確保などの分野に無くてはならないものになっています。

実はこのポインタ。概念が分かり難いという話を耳にしますが、それは何故でしょう。

今回は、このポインタをどのように理解すればよいのか、わかりやすく解説し、利用することのメリットについても紹介します。

C言語のポインタとは

ポインタとは、別の変数のアドレスを格納している変数であることは、冒頭で触れました。

アドレスとは
「仮想アドレス空間における記憶場所」
を示す通し番号のことで、
0x7fffc00や0x7ffeeef93ab9
のような16進で示されます。

変数のアドレスとは

ノートパソコンの機能や性能を表示するのに、
「8GB(ギガ・バイト)のメモリを搭載」
などの表示を目にしますが、これはメインメモリを指しています。

これは、今回お話するポインタで利用されるメモリとは別のものです。

この解説記事の中でメモリとは次に紹介する「仮想アドレス空間」を指すものとします。

仮想アドレス空間とは

パソコンで複数のプログラムを同時に動作させることが多いですが、このような場合OSが複数のプロセスを生成し、プロセス毎に仮想アドレス空間を割り当てています。

仮想アドレス空間のサイズは常に一定で、32ビットOSの場合には物理メモリのサイズとは無関係に常に4GBで、64ビットOSの場合は2TBです。

また、仮想アドレス空間におけるアドレスは、32ビットOSの場合は32ビット(4バイト)を、64ビットOSの場合は64ビット(8バイト)を使って表示します。

例えば、アドレスは通常16進表記され、後者(64ビットOS)の場合は
0x7ffeeef93ab9
のような値となります。

なお、16進表記の0xに続く0は省略可能で0x0006と0x6は同じ数値として処理されます。

変数は仮想アドレス空間に連続した区画(オブジェクト)と番地を持つ

コンピュータが動作する際、メモリは無数に分割され、機械的な処理に適した
「区画」
「番地」
「記憶場所」
「アドレス」
という概念で管理されています。

1つの区画は1バイト(8ビット)の値を格納できる大きさを持ち、夫々の区画を識別するのに番地が付与されています。

記憶場所には「変数の型」に応じた大きさを持つ連続した区画が割り当てられ、その場所を指し示す先頭の番地がアドレスとなります。

C言語では、仮想アドレス空間で個々の変数に割り当てられた連続した区画のことを「オブジェクト」と呼びます。

変数にはオブジェクトと型が割り当てられる

C言語の宣言文では、「変数名」と「文字や数字などの型」を指定します。

その後、作成したプログラムをコンパイルしてリンクする時に、格納する変数の型に合ったオブジェクトが確保されて、その変数に割り当てられます。

この際、「オブジェクトにも変数と同じ型が付与される」こともしっかり覚えておきましょう。

ポインタ初心者で「ポインタが分かり難い」と感じる人の中には、
「代入文などでの変数同士の型の不一致」
が起こることが理解できない人もいます。

このような方にとつて、「オブジェクトにも型が付与されている」ことを思い出せば、分かり難さはかなり軽減されるでしょう。

変数には、通常の変数とポインタ型変数の2つの種類が存在します。

通常の変数:値を格納するオブジェクトが割り当てられる

通常の変数の場合、宣言文に例えばchar ptと書くことで、変数pt用としてchar型の文字情報を格納できる大きさ1バイトのオブジェクトが用意されます。

文字や数字などの型には以下のものがあります。

char:文字型、1バイトで-126~127の数値で、1文字分の文字情報
unsigned char:文字型、1バイトで0~255の数値で、256種類の数を管理できる
short:符号付き整数型、2バイトで-32768~32767の数値
unsigned short:符号なし整数型、2バイトで0~65535の数値
long:符号付き整数型、4バイトで-2147483648~2147483647
unsigned long:符号なし整数型、4バイトで0~4294967295
int:符号付き整数型、サイズ・数値は共に環境依存
unsigned int:符号なし整数型、サイズ・数値は共に環境依存
float:浮動小数点型、4バイトで単精度浮動小数
double:浮動小数点型、8バイトで倍精度浮動小数

ポインタ型変数:アドレスを格納するオブジェクトが割り当てられる

ポインタ型変数の場合は、宣言文ではchar *ptのように書きます。

「*」はポインタ演算子と呼ばれるもので、ptがポインタ型変数であることを示します。

例えば、宣言文「char *pt」の場合、64ビットOSの場合には変数ptに大きさ8バイトのオブジェクトが用意され、そこに16進のアドレス値(例えば0x7ffeeef93ab9)が格納できます。

*ptは、格納されているアドレスで示されたオブジェクトの値を読み出すことを示し、それが可能なのは読み出すオブジェクトがchar型に限ることを宣言文chart *ptが示しています。

オブジェクトを意識すれば、ポインタの用途とメリットがわかりやすい

先に、「オブジェクトにも型が付与されている」ことを思い出せば、ポインタの分かり難さが軽減されると述べましたが、ここではそれを更に掘り下げて行きましょう。

「ポインタ型変数は、普通の変数には無いメモリの新しいアクセス方法」
であることを理解して使いましょう。

これが、どのような効果をもたらすのか、そのメリットについて見ていきましょう。

メリット1(関数において)ポインタ型仮引数の値の変更が実引数に反映される

ポインタ型変数の特徴を活かした用法の一つが「関数の引数」です。

以下でこれを見ていきましょう。

ポインタ型仮引数の値の変更が実引数の値に反映される

関数を定義する時に使用する引数を「仮引数」といい、関数を使用する時に引き渡す引数を「実引数」といいます。

C言語では、普通の変数を使った引数の場合、実引数から仮引数への引き渡しは「値」をコピーして行われ、関数の中で仮引数の値を変更しても実引数には影響しません。

ポインタ型変数の場合、例えば実引数として利用したい変数をaとします。

実引数には変数aのアドレス&aを設定します。

関数の中でアドレス&aのオブジェクトの値を書き換えても、実引数に書かれているアドレス&aは変わりません。

しかし、変数aの値は関数が書き換えたものになっているのです。

実際に開発をしている人でないと、中々理解が難しいかもしれませんが、関数内で

「1つの値を変えたい場合」は、単に関数の返り値として値を渡せば良いですが、それが複数になってきたりすると、ポインタ型変数のアドレスを渡して上げる方がシンプルになります。

メリット2(配列で)関数の仮引数に配列が使えないのでポインタを利用

ポインタ型変数の特徴を活かしたもう一つの顕著な用法は「配列」に関係しています。

以下でこれを見ていきましょう。

配列の代わりにポインタ変数が使える

charは1文字分のオブジェクトを獲得することですから、例えば文字列abcdeは格納するのには配列が使われます。

char array[5]=(‘a’,’b’,’c’,’d’,’e’)と宣言することでchar型のオブジェクトが連続して割り当てられ、array[0]には’a’、array[1]には’b’、・・・、array[4]には’e’が格納されます。

また、char *pntとすることでポインタ型のオブジェクトが1つ割り当てられます。

これまでの説明から、pntに配列の先頭オブジェクトのアドレスを代入する際に、pnt=&array[0]と書くことができます。

一方で、arrayは配列名ですが、配列の先頭オブジェクトのアドレスを保持していますから、pnt=arrayと書くことも可能です。
pnt=arrayとした場合array[0]と同じオブジェクトを指すので、array[0]の代わりに*pnt、array[1]の代わりに*(pnt+1)、array[2]の代わりに*(pnt+2)、・・・、array[4]の代わりに*(pnt+4)を使うことができます。

このように、ポインタ変数を使えば配列と同様な操作ができることが分かります。

関数の実引数を配列名にして、仮引数をポインタ変数とする

先の例で用いた配列char array[5]とポインタ変数char *pntを使って説明を続けましょう。

関数をfuncとします。

main側ではfunc(array)とし、funcの定義側ではfunc(char *pnt)とします。

上で説明した
「配列の代わりにポインタ変数が使える」
により、funcに配列の値を渡したり、func内で変更したものが配列に反映されます。

C言語では関数の仮引数に配列を使えませんが、このようにすれば引数で配列の受け渡しが可能になります。

メリット3(構造体で)関数の仮引数に構造体を利用できる

構造体もポインタ型変数が多く使われる分野です。

構造体とは複数の変数をまとめた構造のことです。

例えば、宣言文では構造体Personは
char name[50]
int age
char gender
をまとめたものと宣言します。

次にmainの中で、Person型の構造体を持つ変数memberを定義して、
name=”Tanaka”
age=”60”
genda=”m”
などと設定します。

mainでfunc(&member)とするにより、関数func(Person *mbr)の中で&memberのnameやage、genderなどのオブジェクトにアクセスして書き換えたりすることも出来ます。

また、より複雑な「構造体の配列」や「構造体の配列を関数の引数」とすることも可能です。

メリット4 メモリ領域の動的確保ができる

多くの数値をまとめて扱うために配列が多く使われます。

配列の宣言により配列に必要なメモリ領域がプログラム実行時に確保され、プログラムが終了するまでそのまま維持されます。

このようなやり方を「静的な」メモリ領域の確保と言います。

例えば、ファイルからデータを読み込む場合、時々で必要なメモリサイズが異なります。

このため、プログラム実行中に「必要な時に、必要な分だけメモリ領域を確保する」ことが求められ、このやり方を「動的な」メモリ確保と呼びます。

メモリを動的に確保する際にはmalloc関数を使いますが、「割り振られた領域の先頭アドレスを格納する」ためにポインタ型変数が使われます。

エンジニアをお探しならAMELAに

今回は、多くのプログラミング初心者が躓く「ポインタ」について見てきました。

最近のプログラムでは、聞く機会も少なくなりましたが、何年も前に作った大規模なシステムなどの場合、こういった原理を理解していなければ、保守や改修・追加開発が難しくなるケースもあるでしょう。

もしも現在、特定の分野のスキルに長けたエンジニアをお探しなら、是非AMELAにご相談下さい。

オフショア開発やIT人材派遣も行っている関係上、幅広いスキルセットを持ったエンジニアの紹介が可能です。