C言語 ポインタ変数定義の正しい解釈とは【*の意味を解説】

C言語
この記事は約13分で読めます。

こんにちは、ナナです。

ポインタ変数を定義するときは、次のように書きますね。

short * pnum;
  • 「どうして、こんな書き方をするのかな?」
  • 「アスタリスク*の記号が、なぜ必要なのかな?」

と思っている方いませんか?

ポインタ変数定義の書き方にはすべて意味があるのです。その秘密を明らかにしていきます。

本記事では次の疑問点を解消する内容となっています。

本記事で学習できること
  • ポインタ変数定義の正しい解釈の仕方とは?
  • ポインタ変数定義の各部品が必要となる理由とは?
  • 「変数」と「ポインタ変数」との関係性とは?
  • ポインタで使われる「*」の意味とは?
  • なぜ、ポインタの参照先にアクセスするときに「*」が必要なのか?

では、ポインタの変数定義について学んでいきましょう。

ポインタの全貌を学びたい方は『C言語 ポインタを使いこなせ【身に付けるための9の極意】』の記事から順に読むことをお勧めします。

スポンサーリンク

ポインタ変数の定義の解釈

師匠!ポインタ変数の定義って変わってますよね。「*」が付いてるんです。あれは呪印なんですか?意味を、その意味を私は知りたいんですっ!

ナナ
ナナ

ポインタ定義で出てくる「*」の意味だね。ちゃんと意味があるんですよ。ポインタ定義には意味があって、あの形になっているんです。

ポインタ変数の定義は次のように行いますね。

short * pnum;

このように、普段の変数の定義に「*」を付けることでポインタ変数を定義できます。

それでは、このポインタ変数の定義を掘り下げていきます。これを理解することで、ポインタ変数定義の真の意味を知ることになります。

ポインタ変数定義の正しい解釈

変数定義とポインタ変数の定義を具体的に比べてみます。データ型と変数ラベルの間に「*」が付いていますね。

変数とポインタ変数の定義

この変数定義の解釈ですが、各部品を分離して差を比べてみましょう。実は下図左のように捉えるのは間違いであり、右側の見方が正しいです。

ポインタの正しい解釈

この比較結果を見ると戸惑う人もいるかもしれませんが、この解釈こそが正しいポインタの解釈です。各部品は次の意味となります。

部品解釈
変数につけるラベル名を示す。皆さんが自由に名前を与えることができる。
部品①に対してのデータ型を示す。
データ型を「ポインタ型」にしたい場合は「*」を指定する。
ポインタが参照する先のデータの「データ型」を示す。

部品②の「*」は、

変数が「ポインタ型」であることを示すための記号

なんです。

ナナ
ナナ

この解釈を間違えるとポインタ定義が、チンプンカンプンになってしまいます。だからこそ、部品の意味を知る必要があるのです。

ポインタの定義に部品③が存在する理由

ポインタ変数の定義には、部品③としてデータ型の指定が必要になります。その必然性を理解しましょう。

ポインタ変数とは、その性質上2つのメモリを管理しています。

それは、「ポインタ変数自身のメモリ」と「ポインタ参照先のメモリ」の2つです。

ポインタ変数は2つのメモリを管理

ポインタ変数はこの2つのメモリに対してアクセスできる必要があります。メモリにアクセスといえば・・・皆さん覚えていますか?もう一度思い出しましょう。

メモリにアクセスするために必要となる条件が2つありました。

メモリへのアクセス条件
  1. メモリの場所が特定できること
  2. メモリにアクセスするサイズ・型が明確になっていること

ポインタ変数は管理する2つのメモリに対して、それぞれこの条件を満たす必要があります。まずはポインタ変数自身のメモリについて、この条件を確認してみましょう。

ポインタ変数自身へのメモリアクセス条件を確認してみよう!

ポインタ変数に対するアクセス条件

アクセス条件その1:メモリの場所は特定されているか?

ポインタ変数には番地という数値情報を設定する必要があるため、ポインタ変数自身のメモリに対してアクセスできる必要があります。

本例のポインタ変数には、部品①で指定されたpnumという変数ラベルが付いていますから、この条件はクリアしています。

アクセス条件その2:メモリにアクセスするサイズ・型が明確になっているか?

部品②において「*」を指定しており、ポインタ型であることが確定しています。よってこの条件もクリアしています。

つまり、ポインタ変数自身のメモリにアクセスするだけであれば、部品③のデータ型はいらないということです。

ナナ
ナナ

それでは続いて、ポインタの「参照先のメモリ」に対して同じように条件を確認してみましょう。

ポインタ参照先へのメモリアクセス条件を確認してみよう!

ポインタ参照先に対するアクセス条件

アクセス条件その1:メモリの場所は特定されているか?

pnumポインタ変数から参照先のnum変数への場所の特定方法は、ポインタ変数自身が保有している「100番地」によって特定できています。

よって、条件クリアです。

アクセス条件その2:メモリにアクセスするサイズ・型が明確になっているか?

本例では、num変数はshort型になっています。

つまり、

ポインタ変数からnum変数にアクセスするためには、num変数がshort型であることをポインタ変数が知っている必要があります。

ここで出てくるのが、部品③のデータ型です。

部品③こそがnum変数のデータ型を示しており、ようやくpnumポインタ変数からのメモリアクセス条件がクリアできるのです。

もしも部品③がなかったら、参照先メモリの型が特定できないため読み書きができません。これがポインタ変数に部品③が必要となる理由です。

ナナ
ナナ

ポインタの定義には部品が3つ必要です。それは、ポインタ変数自身とポインタの参照先のメモリにアクセスするために必要な条件を満たすためなんです。

ちゃんと理由があるのがわかりますね。

スポンサーリンク

ポインタ変数定義と参照される変数の関係まとめ

師匠!忍術では、水遁と土遁の術を組み合わせると木遁の術になるんです。

変数とポインタ変数にもいろいろな組み合わせがありますよね。でも、組み合わせ方がわかりせん。どうやって組み合わせるんですか?

ナナ
ナナ

変数とポインタ変数の組み合わせはすごく大事なことです。これを間違えると正しくポインタからメモリアクセスができません。

ルールがありますので、ルールに合わせて組み合わせてください。

ポインタの部品③の必要性がわかったところで、変数とポインタ変数の組み合わせの関係性をまとめましょう。

組み合わせのルールがわかるサンプルプログラム

char型、short型、long型の各変数に対するポインタ変数は、次のように組み合わせて利用します。

// 的と弓矢の作成(変数定義)
char    num1;    // 的①
short   num2;    // 的②
long    num3;    // 的③
char  * pnum1;   // 弓矢①
short * pnum2;   // 弓矢②
long  * pnum3;   // 弓矢③

// 照準の設定(番地情報の設定)
pnum1 = &num1;   // 弓矢① --> 的①
pnum2 = &num2;   // 弓矢② --> 的②
pnum3 = &num3;   // 弓矢③ --> 的③

// 矢を射る(値の設定)
*pnum1 = 0x01;       // 的①への書き込み
*pnum2 = 0x2345;     // 的②への書き込み
*pnum3 = 0x6789abcd; // 的③への書き込み

このプログラムをじっくりと考察してみてください。

ポインタ変数の部品③は、必ず参照先変数のデータ型と一致させる必要があります。

ポインタ参照まとめ

ポインタ参照が必ずこの関係性になるように、皆さんが意識してプログラミングする必要があります。

ナナ
ナナ

ポインタの部品③を、参照先変数のデータ型と一致させる。これは絶対守るポインタのルールです。

スポンサーリンク

ポインタの「*」記号の意味を理解しよう

師匠!ポインタの術を使う時に「*」の呪印がたくさん出てくるんです。呪印が付くときと付かないときが、だんだんわからなくなってきました…。

「*」っていったい何なんでしょうか?

ナナ
ナナ

そうだよね。それはポインタを学び始めた多くの人が陥る状態だね。

この問題はC言語の仕様側にも原因があるのですが、今さら仕様を変えるわけにもいきませんので、C言語側のルールに従うしかありません。

「*」の扱い方を学びましょう!

ポインタを使いだすと、いろいろなところでアスタリスク「*」が登場します。この記号の解釈を間違えるとポインタが理解できません。

解釈方法を解説していきます。

ポインタで多用される「*」記号

ポインタの扱いに慣れていない方は「*」をどんな時に付けるのか、付けないのかがわからなくなることがあります。

それは「*」の意味を、しっかりと理解できていないことが原因なんです。

C言語の中で「*」は3つのシーンで使用され、シーン毎に記号の意味が異なることに注意が必要です。

*が登場するプログラム

それぞれの「*」の意味は、次のものとなります。

パターン「*」記号の意味
ポインタ変数の定義時に使用される。変数がポインタ型であることを示す。
定数や変数に対して乗算を行うことを示す。
定数や変数に対して乗算を行うことを示す。

ポインタを使用するときに強く意識する必要があるのが①と③です。

この2つを混同してしまうと「*」の意味が分からなくなります。①と③の見分け方は「*」の左にデータ型が存在するかどうかにより区別がつきます。

このようにプログラムの中では、同じ記号でも文脈によって異なる意味として扱われる記号がたくさんあり、これらを正しく判別できる必要があります。

ナナ
ナナ

文脈から正しい「*」の意味を判別できるようにしましょう。

なぜポインタの参照先にアクセスするときに「*」が必要なのか

ポインタには次のように「間接参照演算子」と呼ばれる「*」を付けることでポインタの参照先メモリにアクセスできるのでした。

// pnumの参照先メモリに100を書き込む
*pnum = 100;

皆さん、このプログラムを見て「*」をなぜ付けないといけないのか疑問に思いませんか?
シンプルに、次の書き方ではダメなのかと思いませんか?

// この書き方でpnumの参照先メモリに書いたことにはならないのか?
pnum = 100;

これではダメなのです。間接参照演算子である「*」の記号には、ちゃんと存在しなければならない理由があるのです。

もう一度おさらいです。

ポインタ変数はその性質上2つのメモリを管理します。そのため、皆さんはどちらのメモリ内容を読み書きしたいかを選択する必要があります。

どっちを書き換えるの?

この2つのどちらを選択するかは皆さんしか決めることはできません。

つまり、

皆さんには、この2つの選択をしなければならない責務があるのです。

ここで登場するのが「*」です。「*」を付与するかどうかで、この2択のどちらであるかを意思表明するのです。

*を付ける付けないの意味

このようにポインタ変数に「*」を付けるか/付けないかによって、どちらのメモリ内容に対する操作なのかを皆さんが選択するのです。

「*」を付けることで「ポインタから間接的にメモリにアクセスしますよ」という選択の記号こそが、間接参照演算子「*」なのです。

これが「*」が存在する必要性です。

ナナ
ナナ

「*」は2択のどちらを選ぶかを皆さんが決めるための記号なんです。これがなかったら、どっちを選んでいるのかわかりませんね。だから必要なんです。

スポンサーリンク

Q&A:ポインタ変数の定義に関するよくある質問

ナナ
ナナ

ポインタ変数の定義に関する質問なんでもいいよ。

Q:ポインタ変数に「*」を付けた時にアクセスするのが、参照先のメモリなのか、ポインタ変数自身なのかを忘れてしまう。覚える方法は?

ポインタ変数に「*」の呪印を付ける場合と付けない場合で、どっちのメモリにアクセスするのかわからなくなっちゃうんですっ!

ナナ
ナナ

そうだねー、慣れの部分もあるけどね。「*」を付けないラベルのみへの代入は、ラベルの貼られたメモリへの読み書きになると覚えるといいね。

ポインタを使い始めたばかりの方は「*」を付けるか/付けないかのどちらが正しいか混乱してしまうことは珍しくありません。

ポインタを使ったプログラムをたくさん書くと自然と身に付きますが、考え方だけ解説しましょう。

次のプログラムは、変数とポインタ変数の各ラベルに対して値を代入するプログラムです。

char   num1;
char * pnum1;

num1  = 20;
pnum1 = &num1;
ラベルへの代入

ラベルへの代入とは、ラベルが貼られているメモリへの代入と解釈されます。これはポインタ変数といえど同じです。

次は「*」を付けたポインタ変数への代入です。

char   num1;
char * pnum1;

num1   = 20;
pnum1  = &num1;

*pnum1 = 50;
*付きの代入

ポインタ変数を利用した「遠隔参照」というのは、通常の変数にはできない特殊な参照です。

特殊だからこそ「*」という明確な印を付けないと、参照できないようにしてあるのです。

スポンサーリンク

課題:ポインタの変数定義が学べたかを確認しよう

ナナ
ナナ

もしも、プログラムが上手く動かなくて困ったときは、答えを見るのではなく「デバッガ」の使い方を学びましょう。

この記事を見ると問題の解決技術が身に付きます。困ったときのオススメ記事です!

課題1

課題内容

次の関数を作成せよ。

課題1_1

次のプログラムに上記関数を追加し、関数呼び出しを実施せよ。出力期待結果が表示されるように適宜プログラムコードを追加せよ。

main.c

#include <stdio.h>

int main(void)
{
    // 日付格納用の変数の定義


    // getEdisonBirthday関数の呼び出し


    // 出力期待値に合わせて表示を行う
    printf("誕生日:%d年%d月%d日");

    return 0;
}

出力期待結果

誕生日:1847年2月11日

main.c

#include <stdio.h>

void getEdisonBirthday(long * pYear, unsigned char * pMonth, unsigned short * pDay)
{
    *pYear  = 1847;
    *pMonth = 2;
    *pDay   = 11;

    return;
}

int main(void)
{
    //  日付格納用の変数の定義
    long            year;
    unsigned char   month;
    unsigned short  day;

    //  getEdisonBirthday関数の呼び出し
    getEdisonBirthday(&year, &month, &day);

    //  出力期待値に合わせて表示を行う
    printf("誕生日:%d年%d月%d日", year, month, day);

    return 0;
}
ナナ
ナナ

getEdisonBirthday関数の引数で定義されるポインタ参照先のデータ型と的となる変数の型は一致させる!これが大事なことです。