ケリーの姉の技術ブログ

ケリーの姉が仕事で学んだ技術に関するアウトプットを行うブログです。たまにゲームの作成秘話なんかも

C言語のポインタを理解した話

こんにちは。ケリーの姉です。 家の片づけをしていたら、入社したてでフレッシュだった時代に仕事内容を一生懸命メモしていたノートを発見しました。

そのノートには、当時「Javaの知識を生かせる現場だよ」とだまされて「C言語開発」の現場に飛ばされた私が、C言語を理解するために一生懸命メモした内容が書かれていました。

今回は、そのメモから「ポインタ」のお話を書いていこうと思います。

「ポインタ」ってなあに?

C言語を勉強し始めた人の挫折ポイントとして有名なのが、この「ポインタ」という存在です。

「変数が格納されている【アドレス】を格納する変数のこと」と書かれている参考書が多いと思います。

うん。なんのこっちゃ?って話ですよね。私もそうでした。

そのポインタを理解した新人の頃の私のメモには、こう記されていました。

 「ポインタ」は「ショートカット」である。

「ポインタ」を「ショートカット」として考えてみよう

例えば、こんな構造体があるとします。

test {
    int X;
    long Y;
    double Z;
}

これは、フォルダ「test」にX、Y、Zという3つのファイルがある状態をイメージしてください。

で、ポインタに「test」のアドレスを格納すると、こんな記述になります。

*pointer = &test

先頭に「*」をつけるとポインタの格納型宣言。 「&」をつけるとアドレス

これは、ショートカットのパスを設定しているイメージとして、私は理解しています。  「printer」というショートカットに「test」フォルダのパスを格納しているって感じですね。

ポインタ(ショートカット)ができれば、そこに格納されたパスをたどって実体のデータにアクセスできます。  

ポインタを使ってアクセスしてみよう。

 構造体にアクセスするときはアロー演算子を使うと便利です。

->

↑こんなやつ。矢みたい。

これとポインタをつかって、test構造体のメンバXにアクセスするのはこう書きます

pointer -> X

こうすると、ショートカットで遷移した先にいる「X」の中身を見せて!って意味になります。

余談

(*pointer).X という書き方も同じ意味になります。 ポインタ使わないならtest.Xも同じ

ポインタの使い分いどころ

私は、別の関数で値の書き換えをする時によくつかっていました。

関数というのは独立国家みたいなもので、お互いの関数内で定義された変数はみれません。 でも、プログラムを組んでいたら「関数Aにある変数を関数Bで書き換えたい!」っていう時はあるはずです。

はい、そんなときに役にたつのがポインタ。 これを使えば、他の関数の変数を書き換えができちゃいます。

書き換えができないパターン

まず、初心者がやりがちなパターンを書いてみます。

#include <studio.h>

/* 引数に値を格納する関数 */
void setNum (int val) {
  val = 999;
  return;
}

int main () {
  int def = 123;

  /* 変更前の数値 */
  printf("呼び出し前:%d\n", def);

  setNum(def);

   /* 変更後の数値 */
  printf("呼び出し後:%d\n", def);
  return 0;
}

このソースの実行結果、両方ともdefの値は「123」で出力されます。 この処理の流れなら、setNumで999がセットされて帰ってくるように見えますが、ここに罠があります。 実は、setNumで受けとった値「val」は、defの値をvalにコピーしたものであり、def本体が渡されているわけではないのです。 なので、setNumでは、コピーされた別物に値を設定していることになります。

こういうときに、ポインタの出番です。

書き換えができるパターン

#include <studio.h>

/* 引数に値を格納する関数。アドレスが来るので、受け取り型がポインタになっている点に注意 */
void setNum (int *ptr) {
  /* ポインタ(ショートカット)の先にある値を999に書き換える */
  *ptr = 999;
  return;
}

int main () {
  int def = 123;

  /* 変更前の数値 */
  printf("呼び出し前:%d\n", def);

  /* defのアドレスを関数に渡す */
  setNum(&def);

   /* 変更後の数値 */
  printf("呼び出し後:%d\n", def);
  return 0;
}

こうすると、defの値は関数呼び出し前は「123」、呼出し後は「999」になります。

ざっくり流れを解説すると、defのアドレスをsetNum関数に渡したとき、例のごとくコピーが渡されます。 そのコピーをsetNum側で受け取り、受け取ったものをptrで受け取っています。 ※int型のアドレスを格納するから int ptrという宣言になります。もちろん、ptrは自分の好きな文字列で

コピーされたとはいえ、ショートカットの遷移先は同じです。 なのでdefが存在する場所まで遷移して999の値に置き換える→mainに戻ったとき、defの中身が999になる。 という感じです。

たとえ話

当時の私も、ポインタによる別関数での書き換え処理を理解するのに苦しんでいましたが、理解できたときのメモにはこうかかれていました。

社員の共有フォルダに勤務表がおいてある。 ここから、勤務表を自分のPCにコピーするのが「通常の引数」 コピーした勤務表を更新しても、共有フォルダにある勤務表は更新されない。

ポインタは、自分のPCにショートカットを作ることである。 作成したショートカットに共有フォルダの勤務表が格納されているパスをコピーするのが「アドレスの引数」 ショートカットから遷移した先にある勤務表を更新することは、共有フォルダの勤務表を更新することである。

通常の変数とポインタのデータ型の意味の違い

C言語にはいろいろな型がありますが、ポインタの宣言時も型の指定が必要です しかし、通常の変数の型とポインタの型では、データ型の意味が異なります。

  /* 変数の宣言 */
  char cVal;
  short sVal;
  int iVal;

  /* ポインタの宣言 */
  char *cPtr;
  short *sPtr;
  int *iPtr;

これらの型のサイズですが、通常の変数の場合は、 charは8bit、shortは16bit、intは32bit と、型によって異なります。

しかし、ポインタはすべて32bitで固定です。 なぜなら、どんな型であってもアドレスは32bitで来るからです。

ポインタの型宣言の意味は?

ずばり、「ショートカット遷移先のデータのサイズ」を意味しています。 cPtrは32bitですが、*cPtrは8bitになります。

最後に、姉がよくやった間違い

  /* ポインタの宣言は型 *ポインタ名 */
  int *iPtr;
  int data = 123
 
  /* ポインタにアドレスを代入するときは*をつけない */
 iPtr = &data

 /* 遷移先を見るときは*をつける */
 printf("データは%d\n", *iPtr);

姉は、ポインタにアドレスを代入するときつけてしまったり、 通常の変数にアドレスを代入してしまったり、通常の変数につけたりしていました。 まぁ、慣れれば何とかなります。

以上、ざっくりですが過去を思い出して記録してみました。 次回もがんばって書きます。