中村一義の「銀河鉄道より」の最初が完全にベートーヴェンの交響曲第7番なんですが…
#概要
簡単に C++ でのポインタについて書きます。
C++11 を標準としています。
ここにある情報は、内容が正しくても表現が間違っている可能性があります。気付き次第指摘願います。
#ポインタとは何か
ポインタは英語で pointer、「指指す者」という意味です。
オブジェクト A へのポインタとは、メモリ上に A が存在している領域を値とするオブジェクトです。
簡単に言うと、オブジェクト A へのポインタは、A を指し示すモノです。
#基本
##オブジェクトを指さないポインタの初期化
下のコードは、p
という名の、int
型オブジェクトを指すポインタを初期化します。p
はオブジェクトに指すように指定したくないので、nullptr
と初期化します。
int *p = nullptr;
下のコードは、3 行すべてが int
型オブジェクトを指すポインタを初期化します。3 つのポインタはどれもオブジェクトを指していません。
int *p0 = nullptr;
int *p1 = 0;
int *p2 = NULL;
nullptr
も NULL
も実際は 0
なのです。与えられたポインタが何らかのオブジェクトを指しているか否かを確かめたい時に、if
文を使えば簡単に書けます。
下のコードでは、p
を if
文の条件としています。ポインタの値は数字なので、ポインタである p
が何も指さず、値が 0
であれば、if
文の条件としてブーリアン値 false
に変換されます。逆に、p
が何かを指していた場合、値は 0
以外の数字です。0
でない整数をブーリアン値に変換すれば、true
となります。
// p というポインタが既に宣言されています。
if (p) {
// p は何らかのオブジェクトを指している様です。
std::cout << "p points to something!" << std::endl;
} else {
// p は何のオブジェクトも指していない様です。
std::cout << "p points to nothing." << std::endl;
}
##オブジェクトを指すポインタの初期化
下のコードは、int
型変数 x
を 12
と初期化し、int
型オブジェクトを指すポインタ p
を、x
を指すように初期化します。コード実行後、p
の値は x
がメモリに保存されている位置です。
int x = 12;
int *p = &x;
&x
のうちの &
はアドレス演算子といい、オブジェクトがメモリにある位置を返します。
ここでは、x
がメモリに保存されている位置を返します。
ここで 3 つ注意すべき点があります。
注意 アドレス演算子としての &
は、参照を宣言する時に使う &
とは違います。簡単に説明すると、**「あれはあれ、これはこれ」**です。ポインタを宣言する時に *
を使っていますが、これは後に紹介する間接演算子としての *
とも、乗算演算子の *
ともまた違います。減算演算子としての -
と、負数演算子としての -
が違うのと同じ訳です。見た目は同じだけど、意味が違うのです。
注意 ポインタは初期化してから使うことが常識です。
初期化せずにポインタが指すメモリ領域をアクセスしようとすると、undefined、すなわち「未定義」となり、コードが正しく動作しなくなります。
そのうえ、コンパイラは一連のエラーが、「あるポインタが初期化せずに使われた」という原因により引き起こされたことに気づけない場合があるので、デバッグが難しくなります。
しかし、時代は先進し、最近のコンパイラには初期化されていないポインタがあるか否かを確認し警告を出すフラグがあるようです。
注意 C++ において、初期化(initialization)と代入(assignment)は違います。
変数 A を値 V に初期化するということは、A を宣言する文と同じ文で V を A の値とすることです。
代入はいつでもどこでも変数の値を書き換えれば済むことです。
下のコードでは、initializedVariable
は宣言と同時に 24
に初期化されます。
notInitializedVariable
は初期化されず、宣言文の次の文で 24
と代入されます。
int initializedVariable = 24;
int notInitializedVariable;
notInitializedVariable = 24;
##ポインタの宣言文について
同じ型の変数とその型のオブジェクトを指すポインタの宣言文はまとめて一行で書くことができます。
下のコードは、int
型変数 i0
を 12
と初期化し、int
型オブジェクトを指すポインタ p0
を i0
を指すように初期化します。
int i0 = 12, *p0 = &i0;
下のコードは、int
型変数へのポインタ p
を初期化します。*
が識別子(変数の名前、つまり p
)から離れていますが、コードとしては動作します。
int* p = nullptr;
まるで int*
というポインタを型にしたようなこの書き方はあまりおすすめ出来ません。何故なら、こんな書き方も出来ますから。
int* p0 = nullptr, i0 = 12, *p1 = &i0;
p0
と p1
は int
型オブジェクトを指すポインタで、i0
は int
型変数です。一見、i0
がまるで int*
型変数のように見えます。
このような書き方は忌み嫌われるべきであると、僕は個人的に思います。
##ポインタへのポインタ
ポインタは立派なオブジェクトです。よって、ポインタを指すポインタもあり得ます。
「本当に使うの?」どうでしょうかね。
下のコードでは、int
型変数 x
、int
型オブジェクトを指すポインタ p
、int
型オブジェクトを指すポインタを指すポインタ pp
があります。
int x = 1;
int *p = &x;
int **pp = &p;
ちなみに、ポインタへの参照も定義出来ます。
下のコードでは、r
は int
型オブジェクトを指すポインタ p
への参照に初期化されます。
int *p = nullptr, *&r = p;
##ポインタが指すオブジェクトへのアクセス
オブジェクトを指すことがポインタの存在意義です。間接演算子 *
を使う事によって、ポインタが指すオブジェクトをアクセスできます。
下のコードの 1 行目では、int
型オブジェクトを指すポインタ p
は int
型変数 x
を指します。
int x = 12, *p = &x;
*p++;
int a = *p + 3;
2 行目では、p
が指すオブジェクトの値に 1 を足します。*p
の *
は間接演算子といい、ポインタが指すオブジェクトへの参照を返します。ここでは、p
が指すオブジェクト、つまり x
への参照を返します。
間接演算子については、参照を返すということが大事です。もし *p
が返すのは左辺値である参照ではなく、ただの右辺値であれば、*p++
のコードは x
の値に何も影響を与えません。
直接メンバ演算子 .
を使ってオブジェクトのメンバにアクセス出来ます。間接メンバ演算子 ->
を使えば、ポインタが指すオブジェクトのメンバにアクセス出来ます。
下のコードは Programmer
型の変数 greekfellows
を指すポインタ employee
を初期化します。そして、間接メンバ演算子 ->
を使って employee
が指す greekfellows
のメンバ関数をアクセスしています。
// Programmer 型のオブジェクトは quitJob() というメンバ関数があるとします。
Programmer greekfellows, *employee = greekfellows;
employee->quitJob(); // (*employee).quitJob(); と同じ
employee->
と (*employee).
は全く同じ意味です。*employee.
と書くとエラーなので注意(*
の優先順位が .
より低いので)。
#基本の斜め上を行く
##const
とポインタ
const
キーワードを使う事で、変数を定数として定義出来ます。
下のコードの 1 行目では、int
型変数 c
を 12
と初期化し、c
に再度値を代入することを許しません。よって、2 行目はエラーとなります。
const int c = 12;
c = 13; // error: const なオブジェクトに再度値を代入することは出来ません。
これが理解できる前提で話を進めていきます。const
キーワードによって再度値を代入されることを許されなくされたオブジェクトを、「const
**な」**オブジェクトと呼ぶことにします。
const
なオブジェクトは**必ず初期化されなければなりません。**ポインタもオブジェクトですから、const
なポインタも同じです。
###const
オブジェクトを指すポインタ
下のコードでは、const
な int
型変数 x
は初期化され、const
な int
型オブジェクトを指すポインタ p
は x
を指すように初期化されます。よって、3 行目はエラーとなります。
const int c = 10;
const int *p = &c;
*p = 11; // error: ポインタ越しでも、const なオブジェクトに再度値を代入することは出来ません。
###const
なポインタ
下のコードでは、int
型変数 x
と y
は初期化され、int
型オブジェクトを指す const
なポインタ p
は x
を指すように初期化されます。3 行目ではあくまで p
が指すオブジェクトの値を変えているので問題ありませんが、4 行目では p
が指すオブジェクトを変えている、すなわち p
の値を変えているので、エラーとなります。
int x = 10, y = 12;
int *const p = &x;
*p++;
p = &y; // error: const なオブジェクトに再度値を代入することは出来ません。ポインタもオブジェクトです。
###const
オブジェクトを指す const
なポインタ
下のコードでは、const
な int
型変数 c
と d
は初期化され、const
な int
型オブジェクトを指す const
なポインタ p
は c
を指すように初期化されます。よって、3 行目も 4 行目もエラーとなります。もっとも、エラーでコンパイラの作業を止める 3 行目が先にある限り、4 行目が実行されることもありませんが。
const int c = 18, d = 9;
const int *const p = &c;
*p++;
p = &d;
##typedef
とポインタ
typedef
を使うと、型指定子に別の名前を与えることが出来ます。もちろん、ポインタ型の指定子に名前を与えることも出来ます。
下のコードでは、int
型オブジェクトを指すポインタ型指定子を ints
と名付けています。よって、ints
型である p0
は、int
型オブジェクトを指すポインタとなります。
const ints
型の p1
は const
な ints
なので、int
型オブジェクトを指す const
なポインタとなります。
typedef int *ints;
int x = 9;
ints p0 = &x;
const ints p1 = &x;
##配列とポインタ
C++ での配列とポインタは限りなく近いモノです。よく知っておかないと自分の書くコードが滅茶苦茶になります(※体験談です)。
###ポインタの配列
下のコードでは、int
型オブジェクトを指すポインタを格納する配列 p
が宣言されます。格納されたポインタは全て(計 2 つ) nullptr
に初期化されています。
int *p[2] {nullptr, nullptr};
###配列を指すポインタ
下のコードでは、int
型オブジェクトを格納する配列を指すポインタ q
が宣言されます。q
は nullptr
に初期化されています。
int (*q)[2] = nullptr;
###配列を指すポインタへの参照、ポインタの配列への参照
int array[2]{0, 1}, *parray[2] {nullptr, nullptr};
int *(&ref)[2] = array;
int &(*ref)[2] = &array;
#続く!