T.TAO
ブログへ戻る
/11 min read/Programming

C++ #3 ポインタと配列

#C++#Programming#ComputerSystems
C++ #3 ポインタと配列

このノートでは C++ におけるポインタと配列の要点を整理します。

ポインタ

基本概念

ポインタはプログラミング言語における重要な概念です。ポインタが格納しているのはメモリアドレスであり、それを通じてメモリ上のデータへ直接アクセスします。大事なことなので三度言います。

ポインタの値はアドレスです。ポインタの値はアドレスです。ポインタの値はアドレスです。

ポインタが指すアドレスにある値、それが本当のデータです。イメージとしては、ポインタは表札の番地であり、その番地で中に住んでいる人を指し示している、ということです。

宣言と使用

ある型の変数へのポインタを宣言する構文は次のとおりです。

C++type * ptr;

// あるいは type* ptr;
// あるいは type *ptr;

上の三つの書き方はいずれも合法で、まったく同じ意味を表します。形式の上では、type* は ptr の型がポインタであることを強調し、type *(*ptr) の型が type であることを強調します。ここで演算子 (*) はオーバーロードされており、その変数(ptr)がポインタであることを表しています。

たとえば、ある変数へのポインタを宣言するには次のようにします。

C++double *ptr;

注意してください。ポインタの値はアドレスであり、そのアドレスは整数として扱えることが多いものの、ポインタは整数型ではありません。整数は加減乗除ができますが、アドレスは加減はできても、二つのアドレスを掛けたり割ったりすることには意味がありません。したがって、整数をそのままポインタへ代入することには意味がありません。ただし明示的なキャストによって、意味のある宣言にすることはできます。

C++int *pt;
// pt = 0xB8000000; // 型が合わずエラー
pt = (int *)0xB8000000; // 型が合う

アドレス演算子 &

値を持つ変数に対しては、& でそのアドレスを取り出せます。そしてそのアドレスをポインタへ代入できます。

C++int *ptr;
int value = 5;
ptr = &value;

このあと、次の等式が成り立ちます。

C++*ptr == value;
ptr == &value;

確保したメモリの位置をポインタで記録する

アドレス演算子 & を使う方法の制約は、ポインタに対応する値の変数が必ず存在しなければならない点です。値の変数は宣言時にプログラムがメモリを確保しますが、実際には値の変数を伴わないポインタを使う場面が多くあります。実行時にヒープ上へ手動でメモリを確保し、ポインタでその位置を指す、というやり方です。

C ではライブラリ関数 malloc() をよく使います。C++ でも malloc() は使えますが、多くの場合は new キーワードを使います。

C++int *ptr = new int;

// C 流
// int *ptr = (int *)malloc(sizeof(int));
// あるいは sizeof(int) == 4 と分かっているなら
// int *ptr = (int *)malloc(4);

ここでは new のあとに型 int が続き、new は適切な長さのメモリブロックを見つけてそのアドレスを返し、そのアドレスが ptr の値になります。malloc の場合は int の長さを自分で指定し、明示的なキャストも行う必要があります。そうしないとポインタがその型として振る舞わない可能性があります(この点は後ほど詳しく扱います)。

C/C++ では、new(あるいは malloc)で新しいメモリブロックを要求したら、それが不要になった時点で必ず解放しなければなりません。さもないとメモリリークが起きます。解放の仕方は new には deletemalloc には free が対応します。これらは必ず対で使う必要があり、new の回数だけ delete が、malloc の回数だけ free が必要です。

C++int *ptr = new int;
//... int を使う処理のあと
delete ptr;

注意すべきは、解放される対象はメモリブロックであって、ポインタではないという点です。変数 ptr 自体は存在し続け、宣言し直すことはできません。別のメモリブロックを指すようにすることは可能です。

また、既に解放済みのメモリブロックを再び解放しようとしてはいけません。C++ における結果は不定です。宣言した変数が持つメモリ(& 演算子で得られるアドレス)を delete で解放することもできません。

配列

生成と解放

静的配列

C++ で静的配列を作るのは簡単です。

C++typeName arrayName [arraySize];

ここでの arraySize は変数であってはならず、定数か const が付いた値でなければなりません。だからこそ、この配列を静的と呼びます。

静的配列の宣言と初期化の例は次のとおりで、以下のどの書き方も使えます。

C++int cards[4] = {3, 4, 5, 6};
int hands[] = {3, 4, 5, 6}; // コンパイラが自動的に要素数を数える

float balance[100] {};      // 全要素が 0 に設定される
double earnings[4] {1, 2, 3, 4};

動的配列

C++ で上のような静的配列の書き方をすると、プログラムのコンパイル時にメモリ領域が割り当てられます。プログラムが最終的にその配列を使うかどうかにかかわらず、配列はメモリ上に存在します。このメモリ確保の方針を静的束縛(static binding)と呼びます。

これに対して、new キーワードで動的配列を作ることもできます。動的配列は動的束縛に関わり、コンパイル時にプログラムへ組み込まれつつ、その長さは実行時に確定します。不要になったら角括弧付きの delete で解放します。

C++int *parray = new int[10];
// ... parray を使う処理のあと
delete[] parray;

ここで delete[][] は、parray が指す要素(配列の先頭要素のアドレス)だけでなく、配列全体を解放すべきであることをプログラムに伝えています。ここではポインタ演算子 * を使って配列先頭要素のアドレスを表している点にも注意してください。

ポインタ演算

C/C++ では、ポインタと配列はほぼ等価です。それはポインタ演算が存在するためです。

定義

型 T のポインタ p について、p が指すアドレスを X とします。このとき次の二つの恒等式が成り立ちます。

Plain Text1. *(p+i) == X + sizeof(T) * i;
2. *(&Expr) == Expr;

例として、次の配列があるとします。

C++int A[5] = {1,2,3,4,5};

このとき、以下の出力結果をコメントに示します。

C++cout << A;
// 0x7ffd9a04f880

cout << A[0];
// 1

cout << *A;
// 1

cout << &A[0];
// 0x7ffd9a04f880

cout << A+1;
// 0x7ffd9a04f884

cout << A[1];
// 2

cout << *(A+1);
// 2

cout << &A[1];
// 0x7ffd9a04f884

cout << &A[0] + sizeof(int) * 1;
// 0x7ffd9a04f884

これで配列とポインタの関係がはっきりします。C++ は配列の名前をポインタとして解釈します。言い換えれば、配列名を配列の先頭要素のアドレスとして解釈し、A[i]*(A+i) として解釈するということです。

配列全体のアドレス

配列全体に対してアドレスを取るとき、配列名はその先頭アドレスとしてではなく、配列全体のアドレスとして解釈されます。

やや紛らわしいので、例で理解しましょう。

C++short B[10];

B は長さ 10 の short 配列です。short が 1 つ 2 バイトだとすれば、B が占める領域は 20 バイトになります。

このとき、

  • B+1:アドレス値を(B のアドレス値を基準に)2 だけ進める。
  • &B + 1:アドレス値を 20 だけ進める。

このシリーズの記事

C++ プログラミング
  1. 01C++ #1 データとメモリ
  2. 02C++ #2 Struct と Union
  3. 03C++ #3 ポインタと配列
  4. 04C++ #4 関数
  5. 05C++ #5 オブジェクトとクラス
  6. 06C++ #6 継承
  7. 07C++ #7 コピー制御と演算子オーバーロード
  8. 08C++ #8 ポリモーフィズム
  9. 09C++ #9 C++11 の新機能
  10. 10C++ #10 C++14 の新機能
  11. 11C++ #11 C++17 の新機能