見出し画像

C言語のポインタを理解しよう

こんにちは、アイネの佐々木です。
アイネはレガシーな基幹システムの開発を担当することが多く、そのため、C言語を使用することが多々あります。そこで今回は、難しいと言われているC言語のポインタについて説明します。少しでも、ポインタの理解の助けになれば幸いです。

なお、一般的なC言語のデータ型、配列型、構造体、あるいは制御構造(if,switch,do,while,for等)、関数の概念などについては知っている前提で説明させて頂きますので、どうかご了承ください。

また最初から最後まで通読されなくても大丈夫です。気になる部分だけでも読んでみて、一つでも参考になれば幸いです。


そもそもポインタって何?

一般にポインタ(pointer)という言葉は「指し示すもの」という意味を持ちます。
C言語のポインタは、他のメモリオブジェクト(変数、構造体など)を指し示すことのできる変数であり、メモリオブジェクトのある場所(アドレス)を指し示します。ポインタが変数を指し示す様子は以下のようにイメージするとよいでしょう。

ポインタはなぜ必要か

C言語においてポインタはとても便利なものです。またポインタを使わないと、処理を実現できない場合もあり、理解しておく必要があります。具体的には以下の通りです。

  • 関数に大きい情報(構造体オブジェクトの配列等)をそのまま渡すのではなく、小さい情報(アドレス)を渡すことで済ますことができます。

  • 関数の中で、引数の値を更新することができます。

  • 関数に配列を渡すことができます。

  • 関数への引数に関数ポインタを渡すことにより、外から与えられた関数を呼び出すことができます。

  • 文字列操作、ファイル操作、メモリ操作、時間操作などを行う場合、C言語の標準ライブラリ関数を使用しますが、ポインタの知識が必要となります。

実際に使ってみないとピンと来ないかもしれませんが、使っていくうちに自然とわかっていきます。

ポインタの概要と使用例

ポインタの定義(宣言)

ポインタは変数ですから、使用する前に変数の定義を行う必要があります。
このときに指し示す対象のオブジェクトの型を指定します。
例えば、以下のようにしてint型のオブジェクトを指し示すことのできる iptr という名前のポインタを定義します。
ポインタの定義のために使用する「*」をポインタ宣言子といいます。

int *iptr;

ポインタで他の変数を指し示す

int型の変数 iobj がある場合、「iptr = &iobj;」と書くと、iptr が iobj を指し示すようになります。「&」はアドレス演算子といいます。

int iobj;
int *iptr;

iobj = 10; 
iptr = &iobj;

この結果は以下のようにイメージするとよいでしょう。

ポインタで他の変数を参照する

間接参照演算子(*)を使って、他の変数(ポインタの指し示す変数)の値を参照することができます。
iptr が iobj を指し示しているとき、*iptr と iobj は同じです。
次の文を実行すると、標準出力に *iptr の値「10」が出力されます。

int iobj;
int *iptr;

iobj = 10; 
iptr = &iobj;

printf("%d\n", *iptr);

<実行結果>
10

また「*iptr = 50;」という文を実行することで、iobj の値は 50 に変化します。

int iobj;
int *iptr;

iptr = &iobj;
*iptr = 50;
printf("%d\n", iobj);

<実行結果>
50

ポインタ宣言子と間接参照演算子

乗算演算子も記号は「*」ですが、これとポインタ宣言子「*」を混同する人はいないと思います。一方で、ポインタ宣言子「*」と似たようなものに間接参照演算子「*」がありますが、これらは意味は違います。私は最初これらが別物であることを理解できていなかったため、ポインタ関連の文法が難しいと感じていたことを覚えています。

例えば、次のコードの「int *iptr = &iobj;」で使っている「*」はポインタ宣言子です。この文は「int型オブジェクトを指し示す変数iptrを 初期値=&iobj で使用する」という意味の宣言文です。ここでは「=」の左辺は「iptr」です(「*iptr」ではない)。「*」は iptr という名前の変数がintではなくポインタであることを示しています。
一方、「*iptr = 100;」で使っている「*」は間接参照演算子です。この文は「アドレス  iptr の指し示すオブジェクトに 100 を代入する」という意味の実行文です。ここでは「=」の左辺は「*iptr」です(「iptr」ではない)。「*アドレス」でアドレスにあるオブジェクト(したがって「*ポインタ」の場合はポインタの指し示すオブジェクト)を示しています。

int iobj;
int *iptr = &iobj;      ← 「*」はポインタ宣言子

*iptr = 100;            ← 「*」は間接参照演算子

ポインタ変数は他の変数のアドレス値を持つ

ポインタのイメージを掴んでいただくため、先に使用例を説明しましたが、ここでアドレスについて説明しておきます。
アドレスとは、メモリにおける位置情報のことをいいます。バイト単位で表します。32ビットCPUの環境の場合、ポインタや int型オブジェクトのサイズは4バイト(32ビット)ですので、ポインタ変数(iptr)も int型オブジェクト(iobj)も4バイト分のメモリ領域を確保します。
以下に「iptr = &iobj;」とした場合の図を示します。int型オブジェクト(iobj)がアドレス 1080 以降を確保しており値は 10 であることと、そのポインタ(iptr)がアドレス 1000 以降を確保しており値は 1080 である(アドレス 1080 を持つことにより iobj を指し示す)ことを示してます。したがって、iobj は、10 となります。

ポインタ変数のサイズは一定

ポインタが指し示すオブジェクトは、int型、char型、配列型、構造体など様々です。ポインタにはアドレスを設定するので、同じ環境であれば、指し示すオブジェクトの型と関係なく、サイズは一定になります。
例えば、char型オブジェクト(cobj)でも、double型オブジェクト(dobj)でも、ポインタ(cptr、dptr)のサイズは同じになります。

char cobj = 'A';
char *cptr;
double dobj = 10.123;
double *dptr;

cptr = &cobj;
dptr = &dobj;

配列でのポインタ使用方法

ポインタについて説明したいことはたくさんありますが、今回の記事では配列におけるポインタの使い方について解説します。

配列名は配列の先頭アドレスである

配列名は、配列の先頭アドレス(先頭要素のアドレス)を表します。
例えば、以下の int型の要素を持つ配列の場合、配列の先頭要素は iarr[0] で、そのアドレスは &iarr[0] です。配列名 iarr は、&iarr[0] と同じアドレスを指します。

int iarr[5];

int型のオブジェクトを指し示す iptr というポインタ変数を「int *iptr;」と定義した後、iptr に iarr の先頭アドレスを格納したければ「iptr = iarr;」とします。これは「iptr = &iarr[0];」でも同じです。
そうすると、*iptr は iarr[0] と同じになりますので、「*iptr = 10;」とするとiarr[0]に 10 が書き込まれます。

int iarr[5];
int *iptr;

iptr = iarr;       ← iptr = &iarr[0] としても同じ
*iptr = 10;        ← iarr[0]に 10 が書き込まれる

アドレス演算

ポインタ変数の値(アドレス)に対して加算または減算することができます。加算(+1)は次の要素を指すようになり、減算(-1)は前の要素を指すように計算されます。加減はオブジェクト単位であって、アドレスの1バイト単位での加減ではありません。間違いやすいので、注意が必要です。

例えば、下記の場合、「iptr + 2」は iarr[2] を指すアドレス値です。したがって、「*(iptr + 2) = 30;」を実行すると、iarr[2]に30が書き込まれます。

int iarr[5];
int *iptr;

iptr = iarr;
*iptr = 10;
*(iptr + 2) = 30;

ポインタ変数はインクリメントやデクリメントが可能です。例えば「iptr++;」の後に「*iptr = 20;」を実行すると、iptr はiarr[1]を指すようになり、iarr[1]に 20 が書き込まれます。こちらもオブジェクト単位であって、アドレスの1バイト単位での加減ではありませんので、注意してください。

int iarr[5];
int *iptr;

iptr = iarr;
*iptr = 10;
iptr++;
*iptr = 20;

ポインタ名と配列名の違い

iptr = iarr を実施した直後、iptr と iarr の値は同じ(配列 iarr の先頭アドレス)になりますので、同じように使用できます。
例えば、*(iptr + i)、*(iarr + i)、iptr[i]は、どれもiarr[i]と同じです。

int iarr[5];
int *iptr;
int i = 2;

iarr[i] = 30;
iptr = iarr;

printf("%d\n", *(iptr + i));
printf("%d\n", *(iarr + i));
printf("%d\n", iptr[i]);
printf("%d\n", iarr[i]);

<実行例>
30
30
30
30

しかし、iptr はポインタ変数なので変更可能ですが、以下のiarr1 は配列の先頭アドレスという意味の固定値(定数)ですので、変更できません。

int iarr1[5];
int iarr2[5];
int *iptr;

iptr = iarr2;         ← これは可能
iptr++;               ← これは可能
iptr--;               ← これは可能

iarr1 = irr2;         ← これは不可(コンパイルエラーになる)
iarr1++;              ← これは不可(コンパイルエラーになる)   
iarr1--;              ← これは不可(コンパイルエラーになる)

関数に配列を渡すには

関数に配列そのもの、すなわち 要素数=n の配列オブジェクトをコピーして渡すことはできません。その代わりに配列の先頭アドレスを渡すことができます。
ただし、配列の先頭アドレスだけでは、呼び出された関数側で配列の終わりを判断できませんので、配列の要素数とセットにして渡します。(文字列の場合は、終端を表す要素が'\0'と決まっており、配列の終わりを判定できるので、配列の先頭アドレスだけで問題ありません)
例えば、int配列を全てを -1 にセットする set_minus_one関数の場合、以下のように使用します。

 int iarr1[5];
 int iarr2[10];

 set_minus_one(iarr1, 5);
 set_minus_one(iarr2, 10);

set_minus_one関数は以下のようになります。

void set_minus_one(int *iarr, int n)
{
    while ( n > 0 ) {
        n--;
        *(iarr + n) = -1;
    }
}

const修飾子によって変数の変更を防ぐ

関数の引数にポインタを使用した場合、意図せず値を変更してしまう可能性があります。そのような場合、const修飾子を使用します。
const修飾子は、定義された変数の値を変更しない(変数を定数として使う)ことを宣言するものです。ポインタの定義の場合、先頭に const を付けると、ポインタ変数の指すオブジェクトを変更しないという意味になります。
関数にオブジェクトのアドレスを渡すとき、引数のポインタの指し示すオブジェクトの値を変更しない場合、関数の提供者は当該引数に const を付けるべきです。

先ほどの set_minus_one関数の場合は、iarr の指すオブジェクトを -1 に変更したいので、const を付けてはいけませんが、次のような int配列の和を返すsum関数の場合、配列の要素を変更する必要はありませんので、関数定義の仮引数に const を付けるべきです。

int iarr[] = { 10, 20, 30, 40, 50 };

printf("%d\n", sum(iarr, 5));
int sum(const int *iarr, int n)
{
    int ret = 0;

    while ( n > 0 ) {
        n--;
        ret += *(iarr + n);
    }
    return ret;
}

終わりに

本記事のポインタの説明は如何だったでしょうか。
ポインタは理解するのは難しい反面、理解すると相当に便利なものです。ポインタはとても奥が深く、一回の記事で説明しきれるものではありません。機会があれば、続編を作成したいと思います。
本記事の内容でコメント、質問等ございましたら、お気軽にお問い合わせください。


株式会社アイネでは一緒に働く仲間を募集しています
アイネに少しでもご興味を持たれた方は、ぜひお問い合わせください。
就職までは考えていないけれど、とりあえず話だけ聞いてみたいという方も大歓迎ですので、お気軽にお問い合わせください。

この記事が気に入ったらサポートをしてみませんか?