サラリーマンプログラマから

転職活動を機に始める浅はかなブログ

C++のポインタってやつ

そろそろポインタで混乱する人が出始めているらしいので、自分なりにポインタについて解説をしてみようかなと思います。ここではC++で説明します。

なぜポインタで混乱するか

私も最近の解説書を読み込んだりしたわけではありませんが、教え方に問題があると思っています。あなたは悪くありません。
今、あなたはおそらく「わかるけど、理解できない」状態になっているのではないでしょうか。
たとえば、何かを友人からオススメされた場合、何が良いのか熱く語られることでしょう。ポインタの教え方には何が良いのかがないように思います。つまり、ポインタの仕組みや機能はわかるけど、何が嬉しいのかがわからない状態になっているのではないでしょうか。ソリューションはあるが、プロブレムが無い状態です。

なので、ここでは出来る限り具体的に例を上げつつ、「確かにポインタって機能を使いたくなる!」と思えるように解説していきたいと思います。

とはいえまずは機能の確認から

ポインタの説明でよくあるのは、「ポインタは変数の箱のアドレスをいれる変数です」とかでしょうか。

#include <iostream>

int main() {
  int a;
  std::cout << "address  a: " << &a << std::endl;
  int * pa = &a;
  std::cout << "address pa: " << pa << std::endl;
}

機能としてはこんな感じですね。

次からどんなときにポインタを使うと嬉しいのか見ていこうと思います。

複数のデータを表示したいとき

3つのデータを表示するクラスを用意したときに、1つだけデータを更新したい場合。

#include <iostream>

class MultidataOutput {
 private:
  int data01_ = 0;
  int data02_ = 0;
  int data03_ = 0;
 public:
  void setData(int data01, int data02, int data03) {
    data01_ = data01;
    data02_ = data02;
    data03_ = data03;
  }

  void print() {
    std::cout << "data01: " << data01_ << std::endl;
    std::cout << "data02: " << data02_ << std::endl;
    std::cout << "data03: " << data03_ << std::endl;
  }
};

int main() {
  MultidataOutput output;
  output.setData(0, 1, 2);
  output.print();

  // data01だけ更新したい
  // data02, data03も覚えておかなくてはならない
  output.setData(10, 1, 2);
  output.print();
}

このように、表示したい全てのデータを覚えておく必要があります。 回避方法は、SetData01(int data)のようにするなどありますが、今は3つですが0を含むいくつかのデータを表示したい。となったときに対応しきれません。 こんなときに思いませんか?

変数の箱だけ知ってれば出力するときに中身を見にいくのに...

ポインタの出番です。

#include <iostream>

class MultidataOutput {
 private:
  int* data01_ = 0;
  int* data02_ = 0;
  int* data03_ = 0;
 public:
  void setData(int* data01, int* data02, int* data03) {
    data01_ = data01;
    data02_ = data02;
    data03_ = data03;
  }

  void print() {
    std::cout << "data01: " << *data01_ << std::endl;
    std::cout << "data02: " << *data02_ << std::endl;
    std::cout << "data03: " << *data03_ << std::endl;
  }
};

int main() {
  MultidataOutput output;
  int data01 = 0;
  int data02 = 1;
  int data03 = 2;

  output.setData(&data01, &data02, &data03);
  output.print();

  // data01だけ更新したい
  data01 = 10;
  output.print();
}

ポインタによって、箱の中身ではなく箱を設定しました。

ちなみに、いくつかのデータを表示したい場合はこのような形になります。

#include <iostream>
#include <iomanip>
#include <vector>

class MultidataOutput {
 private:
  std::vector<int*> data_;
 public:
  void addData(int* data) {
    data_.push_back(data);
  }

  void print() {
    for (int i = 0; i < data_.size(); i++) {
      std::cout << "data" << std::setw(2) << std::setfill('0') << i + 1
       << ": " << *data_.at(i) << std::endl;
    }
  }
};

int main() {
  MultidataOutput output;
  int data01 = 0;
  int data02 = 1;
  int data03 = 2;

  output.addData(&data01);
  output.addData(&data02);
  output.addData(&data03);
  output.print();

  // data01だけ更新したい
  data01 = 10;
  output.print();
}

継承を使いたいとき

C++の真骨頂でもある継承を使う場合です。先ほどの例から発展させて説明していきます。 先ほどは、Intのデータでしたが、今回はクラスごとにフォーマットされた文字列を表示する場合とします。

#include <iostream>

class MultidataOutput {
 private:
  int data01_ = 0;
  int data02_ = 0;
  int data03_ = 0;
 public:
  void setData(int data01, int data02, int data03) {
    data01_ = data01;
    data02_ = data02;
    data03_ = data03;
  }

  void print() {
    // data01, data03は data is [data] の形で、
    // data02は         data    {data} の形で出力したい
    std::cout << "data01 is [" << data01_ << "]" << std::endl;
    std::cout << "data02    {" << data02_ << "}" << std::endl;
    std::cout << "data03 is [" << data03_ << "]" << std::endl;
  }
};

int main() {
  MultidataOutput output;
  output.setData(0, 1, 2);
  output.print();
}

このような形です。これを継承を使ってより汎用的にしてみます。

#include <iostream>
#include <sstream>
#include <vector>

class FormatedData {
 public:
  virtual std::string GetFormatedData() const = 0;
};

class FormatedDataSquareBracket : public FormatedData {
 private:
  int data_ = 0;
 public:
  void SetData(int data) {
    data_ = data;
  }
  std::string GetFormatedData() const override {
    std::stringstream ss;
    ss << "data is [" << data_ << "]";
    return ss.str();
  }
};

class FormatedDataCurlyBracket : public FormatedData {
 private:
  int data_ = 0;
 public:
  void SetData(int data) {
    data_ = data;
  }
  std::string GetFormatedData() const override {
    std::stringstream ss;
    ss << "data    {" << data_ << "}";
    return ss.str();
  }
};

class MultidataOutput {
 private:
   std::vector<FormatedData*> data_;
 public:
  void addData(FormatedData* data) {
    data_.push_back(data);
  }

  void print() {
    for (const auto i: data_) {
      std::cout << i->GetFormatedData() << std::endl;
    }
  }
};

int main() {
  MultidataOutput output;
  FormatedDataSquareBracket data01;
  data01.SetData(0);
  FormatedDataCurlyBracket data02;
  data02.SetData(1);
  FormatedDataSquareBracket data03;
  data03.SetData(2);
  output.addData(&data01);
  output.addData(&data02);
  output.addData(&data03);
  output.print();
}

このようにデータを親クラスのポインタで設定することによって、関数がオーバーライドされ簡潔にかけるようになります。C++でポインタを使いたくなるのは主にこの形です。

大きいデータを使いたいとき

これはたまにあるのですが、大きいデータを扱いたいなと思ったときにポインタ(というか new)を使うことになります。

#include <iostream>

int main() {
  int data[10000000] = {0};
  std::cout << "created data" << std::endl;
}

実行環境によるのですが、エラーが起きてcreate dataが表示されなかったのではないでしょうか。表示された場合は配列の要素数を増やしてみてください。

次は、newを使ってみましょう。

#include <iostream>

int main() {
  int* data = new int[1000000000];
  std::cout << "created data" << std::endl;
}

これは大丈夫だったのではないでしょうか。面白いですね。ローカル変数とnewでは使える領域サイズに違いがあるみたいですね。

ここら辺は、スタックとヒープのメモリ空間が〜とかなるので、置いておきます。

最後に

以上がポインタを使いたいかなと思う場面です。C++の場合、継承を使ってより綺麗に、より拡張性を持ったコードを書きたくなったとき(つまり、オブジェクト指向プログラミング)に、ポインタを使いたくなるかなと思います。ただ、C++オブジェクト指向プログラミングはいろいろ論争があったりするんですけどね。

ポインタを完全に理解するにはコンピュータの知識が必要です。プログラムがどのようにして動いているかを理解し、メモリがどのように確保されているかなどをきちんと理解する必要があります。ここの説明がないのもポインタを難しくする要因ですね。

ことC++においては、個人的にですがポインタを積極的に使う必要はないと思っています。出来るかぎり参照を使うことで設計的にも綺麗になる傾向があると思います。(ポインタより参照の方がややこしい感じがしますが)なので、もちろんポインタを理解した方がいいはいいのですが、ポインタで挫折するぐらいなら、ポインタを使わなくても、多少コードが汚くても、まずは思い通りにプログラムを動かすことを目指して行ってほしいと思います。

最後の最後に

ここまで読んで、C言語も学習している人は「配列は?文字列は?」となるかもしれません。確かに配列も文字列もポインタとの関わりが深いです。

が、C++に置いては、配列も文字列もSTLを使うようにしましょう。迷ったらとりあえず配列ならvectorを、文字列ならstringを使用しましょう。正直、配列の扱いは無駄な混乱を生むだけです。まして文字列は人智を超えています。STLを使わないで文字列を扱いたいのであれば、まずは文字コードについて深く学習しましょう。そして、英語を羨む気持ちに包まれましょう。そもそも、C++は言語として文字列に弱いです。今後強くなっていく予定になっていますが配列で文字列を扱うのは難しいままでしょう。