をるふちゃんのブログ

C++ | DirectX | VC++

ポインタと仲良くなる話 【初心者C++er Advent Calendar 2016 - 14日目】

ㄘんㄘんついてないから大丈夫。 ん??なんの話??

…じゃなくてすみません。忘れてました。

当記事に前後して こちらの記事 もご覧頂けると理解が深まるかもしれません。

メモリのアドレスとは

C++において、変数はデータを格納し必要に応じてそれを使えるようにするために データに名前をつけたもの です。
この「データ」はコンピュータのメモリ上に置かれるため、各変数はメモリ上の「場所(位置)」である アドレス を持つことになります。

このアドレスは一般的な環境においては8桁の16進数( = 4byte = 32bit )で表されます。

「アドレス」をデータとして格納することのできる型をC++ではポインタと呼びます。
ポインタはその性質上、任意の変数に対して別の方法でアクセスする方法を提供することができます。すなわち、ポインタを介して任意の変数を操作することができます。

ポインタを介して変数を操作する際に必要な情報は以下の2つです。

  • データの格納されているメモリ上の位置( = アドレス )
  • データを何バイト分読めばよいか

後者について補足しましょう。
先ほど紹介した記事に詳しく書かれていますが、とある事情 により、メモリは8bit( = 1byte )ずつ区切られて使われています。
しかし1byteに格納できる情報はたかが知れています(256通り)。このため幾つかのメモリブロックを連続して専有することで多くの情報を扱えるようにしています。

例えば多くの環境ではunsigned intは4byteとされています。つまり4つ分のメモリブロックを使うことで0 ~ 4,294,967,295の範囲でデータを格納することができるようになっています。
このため、メモリアドレスをポインタに格納するにあたって、正確には以下の情報を扱わねばなりません。

  • データの格納されているメモリブロックの 先頭 位置
  • 格納したいポインタの元となる
    ※型がわかれば何バイト読めばいいかわかりますから。

後者については、ポインタを変数の型に一対一対応させる形とすれば解決できます。このため ポインタにも型 があります。
そのため、ポインタの宣言時には型の直後に*を置いて区別しつつわかりやすいものとしています。

ポインタをとりあえず使う

以下のように、型名の直後に*を追加すると任意の型に対するポインタを定義できます。

int* pInt;
char* pChar;

ポインタに代入できるのはメモリのアドレスです。
メモリのアドレスは任意の変数名に&を付けることで取得できますね。

すなわち、

int i;
int* pInt = &i;

char c;
char* pChar = &c;

また、ポインタに格納されたアドレスにあるデータにアクセスするには、ポインタ名に*(間接参照演算子)をつける。

int i = 10;
int* pInt = &i;

std::cout << *i; // 10と出力される

ここでの*(間接参照演算子)と、先ほど定義の際に型名の直後に書いた*は全く別物であるということに注意してください。

ポインタを活用してみる①

任意の変数に対する別名(エイリアス)のようにポインタを使っているのでは意味がありません(むしろわかりにくい)。

ポインタの力が発揮されるのは例えば以下のようなときです。

  • 任意の関数に書込み可能な領域を提供したいとき

コードから見てみましょう。

#include <iostream>

int squa(int i);

int main(){
  std::cout << squa(5) << std::endl;
  return 0;
}

int squa(int i){
  return i * i;
}

このような関数を改良(改悪)し、値を返すのではなく直接書き換えるようにしたいとします。
しかし以下のようにすると上手くいきません。

#include <iostream>

void squa(int i);

int main(){
  int a = 6;
  squa(a);
  std::cout << a << std::endl;
  return 0;
}

void squa(int i){
  i *=  i;
}

動作としてはsqua()に渡したaが書き換えられて36になることを期待していますが、これは当然上手くいきません。
関数の呼び出し時にはコピーコンストラクタが呼び出されるからです(つまり関数呼出し時に複製が行われ、元の変数とは違うアドレスに仮引数として新しいデータが作られる)。

これを解決するためにポインタを利用することができます。

#include <iostream>

void squa(int* i);

int main(){
  int a = 6;
  squa(&a);
  std::cout << a << std::endl;
  return 0;
}

void squa(int* i){
  *i *=  *i; //冗長に書くと、 (*i) = (*i) * (*i);
}

挙動はざっくりと以下の通りです。

  • squa()の仮引数はポインタである
  • squa()が呼び出されるとコピーコンストラクタが呼ばれ渡したint型変数のアドレスが仮引数として定義される。
  • 仮引数のポインタを間接参照して、呼び出した時にアドレスを取得した変数(ここではa)に自身をかけ合わせて2乗の計算を行う
  • ポインタ経由で直接aを操作したので書き換えに成功した

ですから、C言語printf()には不要な、&scanf()で要求されるのはそういった事情があるのです(書き換えを行う必要があるのでメモリアドレスを取得する必要がある)。

だがしかし

このようなやり方はC++ではおすすめできません。
C言語にはない新しい機能「参照」を使ってよりスマートに記述できるからです。

参照型について

参照型は任意の変数に対する別名のようなものを提供します。
これはメモリアドレスを渡す必要はなく、 宣言時に 変数名をコンストラクタで渡すだけです。
ただしポインタのように参照する対象を変更することはできません。
また、参照型をつかって間接的に元の変数を操作する場合にも*(間接参照演算子)は不要です。
参照型につけた名前をそのまま書くことでアクセスができます。

ポインタの宣言時にはint* ptrなどと書きますが、その代わりに以下のように記述することで「参照型」変数を定義することができます。

int i = 10;
int& ref = i; //アドレス取得の& は不要

ref = 15; // i は 15になった。
std::cout << i; //  15が出力

i = 20 // このとき ref にアクセスすれば 20である。
std::cout << ref; // 20が出力

int j = 30;
ref = j; // ref の参照先はjにならず、ここでは i に値30が代入されるだけ。
std::cout << i; //  30が出力

j = 40;
std::cout << i; //  30が出力。jを変更しても参照しているわけではないので影響はない。
std::cout << ref; //  30が出力。上に同じ。

これを変数の仮引数に用いると、ポインタなしでより安全に書き換えを行うことができます。

#include <iostream>

void squa(int& i);

int main(){
  int a = 6;
  squa(a);
  std::cout << a << std::endl;
  return 0;
}

void squa(int& i){
  i *=  i;
}

仮引数として参照型が使われているので、関数がコールされる際に呼ばれるコピーコンストラクタでsqua()のブロック内にaエイリアスが作られるので、関数内での操作は呼び出し元に反映されます。

特に問題がない限りこの書き方を推奨します。このため、このような用途においてポインタはおすすめしません。
だめじゃん。

ポインタを活用してみる②

以下のような時、ポインタは絶大な威力を発揮します。

  • メモリを動的に確保する

ざっくりと説明すると、メモリをヒープで確保する( = メモリ資源を上手く利用する)ためには以下のような方法を取らないといけません。

  • ローカル変数での大きなデータの確保は極力避ける
  • ヒープで取れる方法を利用する

まぁSTLとか使っておけよという話なんですが。

new/delete

newについてもかんたんに触れておきます。

newnew演算子の直後に書いた型に対応するメモリ領域を確保し、その先頭アドレスを返す 演算子です(mallocも似ていますが、sizeofなどを使って確保するサイズをbyte単位で指定する必要があります)。
HogeType* ptr = new HogeType;

また、これはフリーストアと呼ばれるメモリ上の任意の位置を専有することになるので、プログラムを終了する前に 必ずメモリを解放 してください。
delete演算子に解放したい領域の先頭アドレスを渡すことで解放を行うことができます。
delete ptr;

メモリ解放は、 過不足なく 行うようにしてください。
殆どの場合、二重解放を行っても実際に問題が起きることは少ないですが、万が一、一回目の解放後に同じ領域に別のデータが割り当てられた場合、意図しない解放とセグメンテーション違反を起こすことになります。
この種のバグはたいへんデバグが難しい上に再現性がないのでとても追跡が厄介です。

もし心配であれば以下のようにすると良いでしょう。

delete ptr;
ptr = nullptr;

また、newでは配列を確保することもできます。
その場合、以下のようにします。

int* ptr = new ptr[10];
ptr[0] = 3;
ptr[1] = 1;
ptr[2] = 4;
ptr[3] = 1;
//~~中略~~
delete [] ptr;

deleteのあとに[]を置いていることに注意してください。配列を動的に確保した場合はこのように解放します。

動的確保しても配列にアクセスする際はアスタリスクをつけないので混乱するかもしれません。
この話はこの後の「余談」の項で扱います。

ともあれ、このようにするとメモリを動的に確保することができ、フリーストア領域を活用してスタックを消費することなく大容量のデータをメモリ上で上手く扱うことができます。
ゲームのコードを例に出してみます。

// main.cpp
#include "GameManager.h"

int main() {
    GameManager* game = new GameManager;

    //~~中略~~

    delete game;

    return 0;
}
// GameManager.h
#pragma once

class GameMap;

class GameManager {
public:
    GameManager();
    ~GameManager();
private:
    GameMap* m_gameMap;
};
// GameManager.cpp
#include "GameManager.h"
#include "GameMap.h"

GameManager::GameManager() {
    m_gameMap = new GameMap;
    m_gameMap->foo();
}

GameManager::~GameManager() {
    delete m_gameMap;
    m_gameMap = nullptr;
}
// GameMap.h
#pragma once
class GameMap {
public:
    void foo();
};
// GameMap.cpp
#include "GameMap.h"

#include <iostream>

void GameMap::foo() {
    std::cout << "worked!" << std::endl;
}

こんな感じです。
GameManager.hにて、class GameMap;と書いています。
これはクラスの前方宣言という記法で、 クラスの実態を必要としない場合は、そのクラスの存在をコンパイラに知らせるだけでよい というやり方です。
これによりヘッダのインクルードを減らし、あるいは隠蔽することができます(GameManager.cppで初めてインクルードしていますが、実装の詳細でヘッダを初めて読むので一回で済む)。
リンクライブラリ化する時に特に効果が出ますが、より良いC++API設計の観点からもヘッダから依存関係を取り除くのは良い事です。

また、クラスのインスタンス(実体)がどれほどのサイズになるか見当がつかないこともあり、スタック領域の消費を考えると、やはりフリーストアを使うためにも動的に確保すると良いでしょう。

余談

突然ですが以下のコードは有効でしょうか。

int a[10];

//~~中略~~

int main(){
  a[2] = 10;
  3[a] = 15; //!?
  //~~後略~~  
}

結論から言うと…有効です。問題ありません。アホですが。

配列はポインタと深いつながりがあります。 配列名は配列の先頭アドレスを指すポインタである ということはご存知でしょうか。
そのため、
a[0]*aは同値です。
同様に、
a[1]*(a+1)も同値です。
※ポインタに対する加減算はポインタの型の分だけ飛ばされる(この場合は多くの環境で4byte増えます)

ともあれ、この性質より[]には、 「ポインタに[]をつけることで、([]の内側に書いた値だけ飛ばした領域にある)メモリを間接参照する」 という性質があるということが読み取れます。
これを踏まえて、以下のように変形ができます。

a[4];
*(a + 4);
*(4 + a);
4[a];

4という値をポインタとして解釈するのです。そしてaという値だけメモリを読み飛ばせば、確かに配列a5番めの要素にアクセスすることと同じです。
まぁ、やる人はいませんが…。

以上です。

福島大学のオーケストラのエキストラ(賛助)出演をしており、例によってAdCのことを完全に忘れていました。 そろそろ謝っても許されない気がしてきた。

f:id:wolf_cpp:20161220063500p:plain

私、やりました。

次はyumetodoさんらしいです。