AJ - 4.05.ポインタ Editorial /

Time Limit: 0 msec / Memory Limit: 0 KB

前のページ

キーポイント

  • プログラム上の変数はメモリ上に保存される
  • メモリは巨大な配列のようなもの
  • メモリ上の位置はアドレスという数値で識別される
    • アドレスは配列の添字に対応
  • ポインタはアドレスを扱う整数型
  • ポインタの使い方
操作 記法
ポインタの宣言 型 *ポインタ;
変数のアドレスを取得 &変数
ポインタの指す先へのアクセス(読み書き) *ポインタ
ポインタそのものへのアクセス ポインタ
ポインタ経由でのメンバアクセス ポインタ->メンバ
  • ヒープ領域の確保
型 *ポインタ1 = new 型;        // 1つ分の領域を確保
型 *ポインタ2 = new 型[n];     // 連続したn個分の領域を確保
  • ヒープ領域の解放
delete ポインタ1;   //「new 型」で確保したときに返ってきたポインタ
delete[] ポインタ2; //「new 型[n]」で確保したときに返ってきたポインタ

ポインタ

ポインタはメモリを直接扱うために用いられます。 ポインタを用いることで複雑な操作を行うことができます。 ポインタを全く使わずにプログラムを書くこともできますが、ポインタを使うとシンプルに書ける場合や、効率よく動作するプログラムを書ける場合があります。

このページの説明は3.01.整数型の内容を前提としています。忘れてしまった人や理解が曖昧な人はもう一度読み返してみてください。

メモリ

これまでは、変数を使用するとその分だけメモリを消費するというざっくりとした説明をしていましたが、 ここでもう少し詳しく説明しておきます。

メモリというのはコンピュータに取り付けられている記憶装置です。 ここでは巨大な配列のようなものだと思ってください。 コンピュータはプログラムに従って、この巨大な配列に値を書き込んだり、読み込んだりしながら動いていきます。

メモリは1byte(8bit)という単位を区切りとして扱われます。 この1バイトの単位はアドレス(番地)という数値によって識別されます。 アドレスは配列の添字に対応していると考えると分かりやすいでしょう。 C++で書くならvector<uint8_t>のような感じです。

8bit以上のサイズのデータを保存する場合は、8bit毎に分割して保存されます。 詳しくは細かい話で扱います。

メモリとC++

3.01.整数型で説明したように、 C++の数値型は扱える数値の範囲に応じて様々なものが用意されていました。 そして、それぞれの型にメモリ上でのサイズが決まっています。

例えば、int32_tは32bitの整数型なので、メモリ上の32bit(4byte)を使用することになります。 uint8_t型は8bitの整数型なのでメモリ上の8bit(1byte)を使用します。

sizeof演算子

「ある型がメモリ上で何バイトなのか」を知りたいときはsizeof(型)を使うことができます。

cout << sizeof(int32_t) << endl; // 4
cout << sizeof(int8_t) << endl;  // 1

ポインタ

メモリのアドレスは整数値で表すことができます。 この整数値を扱うための型がポインタ型であり、ポインタ型の変数をポインタといいます。

ポインタのイメージ

以下の図はメモリの101番地に12が入っていて、そこを指すポインタがあるイメージです。
ポインタのイメージ

ポインタの基本的な機能は、以下の2つです。

  • ポインタの指すアドレスに値を書き込む
  • ポインタの指すアドレスから値を読み込む

具体的な例を見てみます。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int x = 1;
  int *p;    // int8_t型に対するポインタを定義
  p = &x;    // xのアドレスで初期化
  *p = 2;    // ポインタが指すメモリへの書き込み
  cout << x << endl;  // 2

  int y;
  y = *p;  // ポインタ経由でxの値を読み取る
  cout << y << endl;  // 2
}
実行結果
2
2

以下のスライドで一行毎の動作を解説します。


ポインタの基本的な使い方は、この例で示したように以下の3つです。

  • &変数で変数のアドレスを得ることができる
  • 型 *ポインタ名でポインタを定義
  • *ポインタでポインタの指すメモリ領域へのアクセス(書き込み・読み込み)

同じ*という記号が別の意味で使われている点に注意が必要です。

この他の機能として、アドレス値の加算・減算を行うこともできます。

ポインタの使い方

ポインタの宣言

型 *ポインタ名;  // ポインタ変数の宣言

複数のポインタを同時に宣言する場合は、次のようにします。

型 *ポインタ名1, *ポインタ名2;

ポインタの指す先の変更

ポインタ = &変数; // 変数のアドレスでポインタを初期化
ポインタ = 別のポインタ; // 別のポインタと同じアドレスを指す

ポインタの指す先へのアクセス

*ポインタ

*を付けることで、ポインタの指す先に対して書き込み・読み取りを行うことができます。

型 *ポインタ*ポインタという形が出てきますが、 前者はポインタの宣言で、後者はポインタのアクセスであることに注意が必要です。

ポインタ関連の演算子はややこしいので、慣れないうちは一つずつ何をしているのかを確認しながら書きましょう。

ポインタの指す先のメンバアクセス

ポインタがオブジェクトを指している場合に、->演算子を用いてメンバへアクセスすることができます。 (*ポインタ).メンバと書くのと同じ意味ですが、よりシンプルに書くことができます。

オブジェクトを指すポインタ->メンバ
具体例
#include <bits/stdc++.h>
using namespace std;

struct A {
  int data;
  void print() {
    cout << data << endl;
  }
};

int main() {
  A a = A { 1 };

  // オブジェクトaを指すポインタ
  A *p = &a;
  p->print();  // a.print()の呼び出し
  p->data = 2; // a.dataの書き換え
  p->print();
}
実行結果
1
2

ポインタの値

ポインタ自体はメモリ上のアドレス(整数値)を保持しているということを説明しました。 アドレスはメモリを巨大な配列だとみなしたときに添字に対応する値です。

以下のプログラムは「ポインタの値そのもの」つまりポインタの指すアドレスを直接出力する例です。

#include <bits/stdc++.h>
using namespace std;

int main() {
  uint8_t x = 1;

  uint8_t *p;
  p = &x;  // ポインタの内容をxのアドレスで初期化
  cout << p << endl;  // ポインタの内容を出力
}
実行結果(例)
0x7ffcf0483a6c

*を付けずにポインタ名だけを書いたときにはポインタ自体の値に対するアクセスになる点に注意してください。

上の実行結果では0x7ffcf0483a6cという出力が得られています。 これはアドレスを16進数で表記したものです。10進数で書けば140724339751532です。 この値自体の意味は考える必要はありませんが、この例では変数xがメモリの0x7ffcf0483a6c番地に割り当てられていることが分かります。

16進法について 10進法が0〜9の10個の文字で数を表現するのに対し、0〜9の10文字とA~Fのアルファベットを合わせた16文字を使って数を表現するのが16進法です。

10進法 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
16進法 0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12 13 14 15 16 17 18 19 1A

また、16進数であることを明示するために先頭に0xを付けて0x1Aのように表すことがよくあります。 C++でもこのような表記で整数値を書くことができます。

アドレスを書くときには16進法がよく用いられます。 そしてcoutもポインタに対しては16進数でアドレスを出力するようになっています。

アドレス値の比較

ポインタ1 == ポインタ2
ポインタ1 != ポインタ2

==!=でポインタを比較することができ、これらはアドレス値を比較した結果になります。

具体例
#include <bits/stdc++.h>
using namespace std;

int main() {
  int x = 123;

  int *p = &x;  // xを指すポインタ
  int *q = &x;  // yを指すポインタ

  // アドレスの比較
  if (p == q) {
    cout << "p == q" << endl;
  } else {
    cout << "p != q" << endl;
  }
}
実行結果
p == q

スタック領域とヒープ領域

C++で変数を使うとそれに対応するメモリ領域が割り当てられるということを説明しました。

変数を使うにはその分だけメモリを割り当てる必要があります。 メモリは有限なので、必要な分だけメモリを割り当て、必要が無くなったらそのメモリを解放する必要があります。

このようなメモリの管理は面倒ですが、ある程度はコンパイラが自動でやってくれます。

ここでは、どのようにメモリが割り当てられるかを簡単に説明します。

メモリ領域の分類

プログラムから使用されるメモリは静的領域スタック領域ヒープ領域という大きく分けて3種類の領域に分けられます。

静的領域

グローバル変数はプログラムの開始から終了までずっと有効な変数なので、 ずっとメモリ上に割り当てられている必要があります。

プログラム開始時に割り当てられて終了時に解放されればよいので、 グローバル変数のメモリ割り当て・解放はコンパイラによって自動的に行われます。

このときにグローバル変数が配置されるメモリ領域を静的領域といいます。

スタック領域

ローカル変数はスコープの範囲内で有効なので、 スコープの始まりでメモリを確保し、スコープの終わりでメモリを解放すれば十分です。

ローカル変数の割り当てと解放もコンパイラによって自動的に行われます。 このときに配置されるメモリ領域をスタック領域といいます

ヒープ領域

プログラムを設計する上で、何らかの情報を「スコープの範囲を超えて」扱いたいことがあります。 このような場合に利用するのがヒープ領域です。

ヒープ領域は柔軟に使える分、プログラマが責任を持って割り当て・解放を行わなければなりません。

また、ヒープ領域はポインタを介して読み書きを行う必要があります。

ヒープ領域はメモリを有効活用したい場合のためのものです。 メモリ有効活用する必要のない場合は、予めグローバル変数として必要な分だけメモリを確保しておけば十分なことが多いです。

ヒープ領域の確保

次のようにしてヒープ領域のメモリを確保することができます。

型 *ポインタ1 = new 型;        // 1つ分の領域を確保
型 *ポインタ2 = new 型(引数);   // 1つ分の領域を確保(コンストラクタに引数を渡す)
型 *ポインタ3 = new 型[n];     // 連続したn個分の領域を確保

「連続してn個分の領域を確保」することで配列のように扱うことができます。

ヒープ領域から確保したメモリは、明示的に解放しない限り、プログラムが終了するまで残り続けます。

メモリを解放するには次のようにします。

delete ポインタ1;   //「new 型」や「new 型()」で確保したときに返ってきたポインタ
delete[] ポインタ3; //「new 型[n]」で確保したときに返ってきたポインタ

new 型[n]で確保したメモリをdelete ポインタのようにしたり、 明示的に確保した領域以外を指すポインタに対してdeleteしてはいけません。

次の例はヒープ領域を使う必要のないケースですが、使い方の例として見てください。

1つ分の領域を確保する例
#include <bits/stdc++.h>
using namespace std;

int main() {
  uint32_t *p;

  // uint32_t型の変数の分だけヒープ領域からメモリを確保する
  p = new uint32_t;

  // ポインタを介して使う
  *p = 123;
  cout << *p << endl;

  // メモリを解放する
  delete p;
}
実行結果
123
連続した領域を確保する例
#include <bits/stdc++.h>
using namespace std;

int main() {
  uint32_t *p;

  // uint32_t型の変数10個分だけ(つまり4*10=40バイト分)ヒープ領域からメモリを確保する
  p = new uint32_t[10];

  // ポインタを介して使う
  uint32_t *tmp = p;  // アドレス値のコピー
  for (int i = 0; i < 10; i++) {
    *tmp = i; // i番目にiを書き込む
    tmp++; // 次の要素を指すように変更
  }

  tmp = p;  // pの位置に戻す
  for (int i = 0; i < 10; i++) {
    cout << *tmp << endl;
    tmp++;
  }

  // メモリを解放する(10個分連続で確保したのでdelete[]を使う)
  delete[] p;
}
実行結果
0
1
2
3
4
5
6
7
8
9

注意点

メモリアクセスの制約

ポインタを通して好きな位置に値を書き込んだり読み込んだりできるかと言うと、そうではありません。 基本的に「変数のために確保されたメモリ領域」か「明示的に確保したメモリ領域」にしかアクセスしてはいけません。 めちゃくちゃなアドレスに対して無理やり書き込み・読み込みを行おうと試みるのは意図しない挙動に繋がります。

ポインタを扱うプログラムを書く場合は、許されていないメモリ領域にアクセスしていないかを常に気をつける必要があります。 この意味で、ポインタを扱うプログラムは難しいと言えます。


細かい話

ポインタの宣言における*の位置

ポインタの宣言における*の位置は以下のうちどれでもよいです。

型 *ポインタ名
型* ポインタ名
型 * ポインタ名

どれを使ってもよいですが、プログラムを通して統一するべきです。

複数のポインタを宣言する際に、それぞれに*が必要であるということが分かりやすいように、APG4bでは1番目の書き方を採用しています。

nullptr

ポインタが初期化されていないことを明示したり、有効なアドレスを指していないことを明示するための 特殊な値としてnullptrを用いることができます。nullptrは多くの場合0を意味します。

あるポインタがnullptrでないことを確認するために次のように書くことができます。

if (ポインタ) {
  (nullptrでないとき)
} else {
  (nullptrのとき)
}
#include <bits/stdc++.h>
using namespace std;

int main() {
  uint8_t x = 1;
  uint8_t *p = nullptr;  // 初期値として nullptr を使う
  p = &x;
  *p = 2;
  p = nullptr;  // 使い終わったポインタには nullptr を入れておく
  cout << (int)x << endl;  // 2

  if (p) {
    cout << "not nullptr" << endl;
  } else {
    cout << "nullptr" << endl;
  }
}
実行結果
2
nullptr

複数バイトの変数

1バイトより大きいデータは複数のバイトに分割されて保存されます。

例えば、12345という数値を保存することを考えてみます。 この数値は1バイト(0~255)の範囲に収まらないデータなので、 「57」と「48」という数に分割し、2バイトに分けて保存することにします。 すると、57 + 48 * 256 = 12345として元の値を復元することができます。

このように複数バイトにまたがって値を保存するときに、 どのように分割するかはコンピュータやコンパイラなどの環境によって決まっています。 詳しく知りたい人はエンディアンというキーワードで調べてみてください。

アドレス値の加減算

ポインタ(自体の値)に対して加減算を行うことで「その分だけずらした位置を指すポインタ」を得ることができます。 メモリを連続的に確保し配列のように用いる際に、ポインタの加減算は有用です。

次のプログラムは、int型10要素分のメモリを確保し、順番にアクセスする例です。

#include <bits/stdc++.h>
using namespace std;

int main() {
  // int型10要素分をヒープ領域から確保(先頭のアドレスがpに入っている)
  int *p = new int[10];
  int *q = nullptr;

  for (int i = 0; i < 10; i++) {
    q = p + i;  // i番目の要素のポインタを取得
    *q = i;  // q の指す位置に i を書き込む
  }

  q = p;
  for (int i = 0; i < 10; i++) {
    cout << *q << endl;
    q++; // q = q + 1と同じ意味(次の要素を指すポインタに変更)
  }

  delete[] p;  // メモリ解放
}
実行結果
0
1
2
3
4
5
6
7
8
9

より正確には、Tという型のポインタpに対してp + dと書いた場合、 pのアドレスをd要素分進めたアドレス(つまりsizeof(T)バイト先のアドレス)という意味になります。

参照・イテレータとの関係

C++の参照やイテレータは、ポインタをより扱いやすくするために用意された機能です。

以下にポインタを参照・イテレータのように使う例を挙げますが、 このような処理を実際にポインタを用いて行う必要はありません。

参照として使う例

#include <bits/stdc++.h>
using namespace std;

void f(int &ref) {
  ref = 2;
}

// ポインタを用いた参照渡し
void g(int *ptr) {
  *ptr = 2;
}

int main() {
  int x = 1;
  f(x);  // 参照渡し
  cout << x << endl;

  int y = 1;
  g(&y);  // yのアドレスを渡す
  cout << y << endl;
}
実行結果
2
2

イテレータとして使う例

#include <bits/stdc++.h>
using namespace std;

int main() {
  // イテレータを用いて順番にアクセス
  vector<int> a = { 1, 2, 3 };
  for (auto it = a.begin(); it != a.end(); it++) {
    cout << *it << endl;
  }

  // ポインタを用いて順番にアクセス
  vector<int> b = { 1, 2, 3 };
  // b.data() ... bのデータの先頭アドレスを返す(&b[0] と同じ)
  int *begin_addr = b.data();
  for (int *ptr = begin_addr; ptr < begin_addr + 3; ptr = ptr + 1) {
    cout << *ptr << endl;
  }
}
実行結果
1
2
3
1
2
3

voidポインタ

void *という特殊なポインタ型があります。 voidへのポインタは任意のポインタ値を扱うことができます。

voidへのポインタは、指す先へのアクセスを行うことはできません。 アクセスを行うには元のポインタ型にキャストする必要があります。

コンパイラは元々のポインタ型が何であったかをチェックしてくれないので、 voidポインタを扱うときはプログラマが責任を持って元の型を管理する必要があります。

以下はvoidポインタの配列に色々なオブジェクトへのポインタを入れる例です。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int x = 123;
  string y = "hello";
  double z = 4.5;

  // voidポインタの配列 順番に int*, string*, double* 型のポインタ
  vector<void *> ptrs = { &x, &y, &z };

  int *xp = (int *)ptrs[0];
  string *yp = (string *)ptrs[1];
  double *zp = (double *)ptrs[2];
  cout << *xp << endl;
  cout << *yp << endl;
  cout << *zp << endl;
}
実行結果
123
hello
4.5

STLのコンテナの実装

今まで使ってきたvectorは内部的にポインタを用いて実装されています。 基本的には、コンストラクタなどでヒープメモリを確保しておき、そこに要素を格納し、 デストラクタなどでメモリの解放を行う、といったことを行います。

これまで扱ってきた事項を組み合わせればシンプルなvectorを実装できるので、ぜひ挑戦してみてください。

ポインタの使いどころ

ポインタは参照やイテレータがある分、C++では使いどころが思いつかないかもしれません。

ヒープ領域の確保が絡んだときにポインタが真価を発揮すると言えるでしょう。 従って、メモリを効率的に使いたい場合にはポインタが必要になります。

また、競技プログラミングにおいても木構造やグラフなどのデータ構造を表現するときに ポインタを用いると便利なことがあります。 競技プログラミングを初めたばかりのうちはポインタが必要になるケースはほとんどないと思いますが、 将来使うことがあったら思い出してみてください。

スマートポインタ

C++ではメモリ確保を安全に楽に使うために、スマートポインタと呼ばれるライブラリが用意されています。

大きく分けて次の2種類のスマートポインタがあります

  • std::unique_ptr
  • std::shared_ptr

ここでは簡単な説明にとどめますが、興味がある人は調べてみてください。

std::unique_ptrは、メモリの「所有権」をやりとりし、 不必要になったタイミングで自動的にメモリが解放されます。 常に1つのstd::unique_ptrが所有権を持っていて、 所有権を持っていないポインタからは対象のメモリ領域を触ることができないようになっています。

#include <bits/stdc++.h>
using namespace std;

int main() {
  // int型1つ分の領域を確保(123で初期化)
  unique_ptr<int> p1 = make_unique<int>(123);
  *p1 += 1;
  cout << *p1 << endl;

  unique_ptr<int> p2;
  p2 = move(p1); // メモリの所有権をp2に移動
  //*p1 += 10;  // p1は所有権を失ったのでエラー
  *p2 += 1;
  cout << *p2 << endl;
}  // ここでp2の持っていた所有権が無効になり自動的にメモリが回収される
実行結果
124
125

std::shared_ptrは、複数のstd::shared_ptrオブジェクトがメモリの所有権を共有するモデルです。 こちらも、共有しているstd::shared_ptrオブジェクトが0個になったタイミングで対象のメモリが解放されます。

#include <bits/stdc++.h>
using namespace std;

int main() {
  shared_ptr<int> p3;
  {
    shared_ptr<int> p1 = make_shared<int>(123);
    {
      shared_ptr<int> p2 = p1;  // p2も所有権を共有
      *p2 += 1;
      p3 = p2;  // p3も所有権を共有
    } // p2が所有権を手放す(p1, p3が共有している状態)
    *p1 += 1;
  } // p1が所有権を手放す(p3が持っている状態)
  *p3 += 1;
  cout << *p3 << endl;
} // 所有者がいなくなり、メモリが解放される
実行結果
126

std::shared_ptrには循環参照が生じた場合にメモリが回収できなくなるという問題があります。 この場合std::weak_ptrを用いることで循環参照を回避できるようになっています。

前のページ