M.Hiroi's Home Page

Linux Programming

お気楽C++プログラミング超入門

[ PrevPage | C++ | NextPage ]

ポインタ (前編)

プログラミング言語の学習には、どの言語にもいくつかの難関があります。C言語の場合、「ポインタ」が最大の難関と言われていますが、コンピュータの基本 (CPU やメモリの概念) を正しく理解していれば、けっして難しい話ではありません。

問題があるとすれば、C言語のポインタが不適切な操作や演算によってプログラムを簡単に暴走させてしまうことでしょう。もともと、C言語は UNIX という OS を記述するために開発されたプログラミング言語です。マシン語なみの操作ができないようでは役に立たないわけで、ポインタという危険なものでもユーザーに開放されているわけです。

C言語と同じく、C++もポインタを使うことができます。まず最初に、基本となるメモリの構成から説明しましょう。なお、ポインタの説明は拙作のC言語講座 ポインタ とほとんど同じです。C言語のポインタを理解されている方は前編を読み飛ばしてもらってもかまいません。

●メモリの構成

メモリにはプログラムやデータが記憶されています。メモリのことを「主記憶装置」といいます。CPU はメモリに格納されているプログラムを実行します。ハードディスクや CD-ROM, USB メモリなどに格納されているプログラムは、メモリに読み込まないと実行することはできません。

メモリ以外の記憶装置を「補助記憶装置」といいます。ハードディスクや CD-ROM, USB メモリなどいろいろな記憶装置がありますが、すべての方法に共通しているのは、情報をなんらかの方法で ON / OFF という 2 つの状態で表していることです。

この ON / OFF を数値の 1 と 0 に対応させます。つまり、コンピュータは情報を 0 と 1 で表すわけです。これを「ビット (bit)」といいます。ひとつのビットでは、0 か 1 かの 2 つの情報しか表せませんが、使用するビットの数を増やすと、それだけたくさんの情報を表すことができます。たとえば、4 ビット使用すると 16 通りの情報を表すことができます。このビットをたくさん集めたものがメモリなのです。

ビットでは情報が細かすぎるので、いくつかのビットをまとめた「バイト (byte)」を単位として、メモリは構成されています。現在は、1 バイトを 8 ビットとしてメモリを構成するコンピュータ [*1] がほとんどです。1 バイトは数値で表すと 0 から 255 までの 256 通りの情報を記憶できます。

メモリから値を読み出す、または書き込む場合、最小の大きさがバイトとなります。どのメモリから値を読み出すのか、またはどのメモリに値を書き込むのかを指定するために、メモリにはバイト単位で「アドレス (番地 : address)」がつけられています。これは私たちの住所や電話番号と同じです。メモリの場合は単純に数値で表します。下図を見てください。

C言語の場合、配列は 0 から数えましたね。コンピュータの世界では、ビットもアドレスも 0 から数えるのが普通です。32 bit CPU の場合、M の値は 232 - 1 = 4,294,967,295 (約 4 ギガ: giga) になります。64 bit CPU の場合は 264 - 1 = 18,446,744,073,709,551,615 (約 18 エクサ : exa [*2]) になります。

数値計算をするときなど、1 バイトでは情報量が少ない場合は、2 バイトまたは 4 バイトまとめてメモリを使用します。文字を表したい場合は、文字を数値に対応させます。アルファベットは 26 種類ありますから、大文字小文字、そしてほかの記号を合わせても 1 バイトあれば表現できます。

たとえば、アスキーコードという規則では、A という文字は 0x41 に対応します。漢字は 1 バイトで表現できないので、複数のバイトを使います。以前は漢字コードに JIS, シフトJIS, EUC などを使っていましたが、最近は UTF-8 を採用する Unix 系 OS やプログラミング言語が多くなりました。

グラフィックの場合は、点 (ドット : dot) をメモリに対応させて表現します。たとえば、1 ドットを 1 バイトで表現してみましょう。そのメモリの内容が 0 ならば、ドットが書かれていないことにします。それ以外のときはドットが書かれているという規則にします。このとき、1 から 255 に対応する色を決めておけば、256 色の絵が描けるわけです。もし、65536 色の絵を描くのであれば、1バイトでは表現できないので 1 ドットにつき 2 バイト使用することになります。フルカラー (1600 万色) 表示であれば、1 ドットにつき 3 バイト必要になるわけです。

このように、メモリに格納されるデータは、単なる数値に過ぎないのですが、使うソフトウェアによってその意味は異なるのです。ある場合は、数値計算のために使用され、ほかでは文字を格納するために、またあるときはグラフィックデータを保持します。

メモリをどのように使うかは、プログラマが決めます。そして、それを実現するための道具がプログラミング言語なのです。変数、配列、文字列といったデータも、すべてメモリに割り当てられますが、マシン語以外の高級言語ではアドレスを意識することはありません。プログラマは変数を定義するだけで、メモリの割り当てといった面倒なことはすべてプログラミング言語 (と OS) が行ってくれます。

-- note --------
[*1] 昔は 1 word = 16 ビットでメモリを構成するコンピュータもありました。
[*2] ギガやエクサは数を表す接頭辞で、大きな値ではメガ (mega) = 106, ギガ (giga) = 109, テラ (tera) = 1012, ペタ (peta) = 1015, エクサ (exa) = 1018 などがあります。

●ポインタの基本

一般に、プログラミング言語で扱う名前は、そのデータが割り当てられたメモリの先頭アドレスを表しています。変数名や配列名は、そのデータが割り当てられたメモリの先頭アドレスであり、関数名は、そのコードが配置されたメモリの先頭アドレス (関数の開始アドレス) を表しています。

高水準と呼ばれるプログラミング言語の場合、名前から実際のアドレスを求めることはできません。プログラマが勝手にメモリ割り当てを操作したり変更すると困る場合があるからです。もっとも、そのようなことをしなくてもプログラミングできないようでは、とても高水準とはいえません。

ところがC/C++の場合は違います。C/C++では名前からアドレスを求める演算子 & [*3] が用意されているのです。変数名にこの演算子を適用することで、変数が割り当てられたメモリの先頭アドレスを求めることができます。また、配列名や関数名はデータやコードの先頭アドレスに付けられた名前にすぎず、& 演算子を使わなくてもアドレスとして使用することができます。

そして、C/C++ではアドレスを格納する変数を定義することができます。これが「ポインタ」です。アドレスは整数値ですから、ポインタの中身は整数値です。ですが、その値はある変数が割り当てられているメモリの先頭アドレスです。つまり、ポインタは「ある変数を指し示している変数」ということになります。

また、関数名もアドレスを示しているのですから、ポインタに代入することができます。これを「関数へのポインタ」と呼びます。どちらにしても、ポインタはあるデータを指し示している変数なのです。これを図に示すと次のようになります。

変数 p はポインタです。図に示すように、ポインタもメモリ [*4] に割り当てられます。この例では 0x68000 番地になっています。CPU が 32 bit の場合、Cコンパイラはポインタに 4 バイトのメモリを割り当てる場合がほとんどで、0 から 0xFFFFFFFF までのアドレス指定が可能になります。これだと約 4 G バイトのメモリを扱うことができます。CPU が 64 bit の場合、ポインタには 8 バイトのメモリを割り当てることになりますが、本稿ではポインタの大きさは 4 バイトとして説明することにします。

変数 i は int 型としましょう。上図の場合、変数 i は 0x70000 番地から割り当てられていて、値は 0x100000 です。変数 p はポインタなので、変数 i のアドレスを代入することができます。すると、図のように変数 p の値は 0x70000 となり、変数 i を指し示すことになります。そして、ポインタ p を使って、変数 i の値を読み書きすることができるのです。これがポインタの基本的な考え方です。

ポインタは次のように宣言します。

データ型* 変数名;
データ型* 変数名 = &変数;

C言語の場合、ポインタは変数名の前にアスタリスク ( * ) を付けて表しますが、C++はデータ型の後ろにアスタリスクを付けて表すことが多いようです。もちろん、C言語と同じように記述しても問題なくコンパイルすることができます。

たとえば、int* p; とすると、p は int 型のポインタになります。変数 i を参照したい場合、アドレス演算子を使って p = &i; とします。これで変数 p は int 型の変数 i を指すポインタになります。また、ポインタを宣言するとき、初期値に変数のアドレスを指定することもできます。

p が参照している変数の値にアクセスする方法も簡単です。変数名の前に * を付けて *p とするだけです。これでポイントしている変数 (この場合は int 型の整数) にアクセスすることができます。つまり、*p は p が保持しているアドレスに格納されているデータを参照 [*5] するわけです。たとえば、int j = *p; とすれば、変数 j の値は 0x100000 になります。また、*p = 10 とすれば、p が指し示している変数の値を書き換えるので、i の値は 10 になります。

それでは実際にポインタを使ってみましょう。なお、図のアドレスは説明のためのもので、これから示すプログラムとは関係ありません。プログラムがロードされるアドレスは実行環境によって異なるので、変数 i と p のアドレスは実際にプログラムを実行してみないとわからないのです。ご注意くださいませ。

リスト : ポインタの使用例 (sample50.cpp)

#include <iostream>
using namespace std;

int main()
{
  int i = 10;
  int* p = &i;
  cout << &i << endl;
  cout << p << endl;
  cout << "i = " << i << endl;
  cout << "*p = " << *p << endl;
  *p = 100;
  cout << "i = " << i << endl;
  cout << "*p = " << *p << endl;
}
$ clang++ sample50.cpp
$ ./a.out
0x7ffdb6ed35ac
0x7ffdb6ed35ac
i = 10
*p = 10
i = 100
*p = 100

出力演算子 << は、引数がポインタであれば格納しているアドレスを 16 進数で表示します。&i のように変数のアドレスを渡してもかまいません。ポインタ p は &i で初期化されているので、どちらも同じアドレスが表示され、i と *p は同じ値 10 が表示されます。*p に 100 を代入すれば、i の値も 100 に書き換えられます。p は変数 i のアドレスを保持し、*p はそのアドレスに格納されているデータにアクセスできるのですから、*p に書き込みすれば変数 i の値も書き変わるのは当然ですね。

-- note --------
[*3] アドレス演算子といいます。
[*4] これは一般的な話で、コンパイラの最適化によって「レジスタ」に割り当てられることもあります。レジスタ (register) とは、CPU 内部にある一時記憶メモリのことです。一般に、レジスタはメモリよりも高速にアクセスすることができるので、変数はメモリよりもレジスタに割り当てた方がプログラムを高速に実行できます。
[*5] これを「間接参照」といいます。マシン語でいえば「間接アドレッシング」ですね。C/C++では * を間接演算子と呼びます。

●ポインタの利点と欠点

ところでこのポインタ、一体何の役に立つのでしょうか。C/C++の場合、次の利点があります。

(1) 「参照呼び (call by reference)」を実現する
(2) コンパクトで効率的なプログラムを書くことができる

(1) ですが、C/C++の関数は「値呼び (call by value)」です。関数の引数にポインタを渡すことで、呼び出し先の関数から呼び出し元の関数に定義されている変数にアクセスできるようになります。ただし、C++には「参照 (リファレンス)」という機能があり、これを使って参照呼びを実現することができます。参照呼びのためにポインタを使う必要はありません。これはあとで詳しく説明します。

(2) はアドレス計算によって実現されます。ポインタが格納しているアドレスは単なる正の整数値です。四則演算ができるわけではありませんが、整数値の代入や加減算 [*6]、ポインタ同士の比較を行うことができます。とくに、インクリメント (++)、デクリメント (--) 演算子と組み合わせることで、効率的なプログラムを記述することができます。

このほかに、プログラムの実行時にメモリを取得するときや、連結リストや二分木といった複雑なデータ構造を作るとき、ポインタはとても役に立ちます。

もっとも、いいことばかりではありません。よくある間違いがポインタの初期化忘れです。局所変数の場合、ポインタを宣言しただけでは、その値は定まっていません。どこをポイントしているのかわからないのですから大変危険です。また、ポインタには整数値を代入することができますが、次のように 0 を代入したらどうなるでしょうか。

リスト : ポインタの危険な操作 (sample51.cpp)

#include <iostream>
using namespace std;

int* q;

int main(void)
{
  int* p;
  p = 0;
  cout << p << endl;
  cout << q << endl;
  cout << *p << endl;
}
$ clang++ sample51.cpp
$ ./a.out
0
0
Segmentation fault

近代的な OS の場合、メモリはシステムエリアとユーザーエリアに区別されていて、ユーザーのプログラムがシステムエリアにアクセスすることを禁止しています。もし、このメモリにアクセスすると OS で Segmentation fault という例外 (エラー) が発生します。

C言語の場合、アドレス 0 を指すポインタを「ヌルポインタ (null pointer)」といい、NULL という記号 (マクロ) で表しますが、C++では 0 で表します。C++の場合、0 は整数型、浮動小数点数型、ポインタ型などの定数として使用することができ、プログラムの文脈により適切なデータ型に変換されます。

このように、ポインタの操作には危険がつきまとうのですが、そのかわりに、ハードウェアを制御するプログラム [*7] でも、C言語だけで作ることができます。このため、C言語は高級アセンブラとか汎用アセンブラと呼ばれています。

-- note --------
[*6] 実は単純な加減算ではなく、データのサイズが考慮されます。たとえば、int* p というポインタに p++ という操作を行うと、p に格納されているアドレスの値は +1 されるのではなく、int の大きさである 4 が加えられます。このことにより、配列のようにデータが連続して配置されている領域では、p++ だけで次のデータをポイントすることができるわけです。
[*7] メモリマップド I/O という考え方を採用している CPU では、ハードウェアとの入出力 (I/O) は、特定のメモリ (本当はメモリではないがメモリと同じようなアクセスが可能) を介して行われます。C/C++であれば、ポインタを使うことで特定のメモリにアクセスすることが可能です。

●キーワード nullptr

ところで、最近の規格 (C++11) では、ヌルポインタを表すキーワード nullptr が導入されました。nullptr はどんなポインタ型変数にも代入したり比較することができます。ただし、整数値と比較したり、整数型の変数に代入することはできません。

簡単な例を示しましょう。

リスト : nullptr の使用例 (sample52.cpp)

#include <iostream>
using namespace std;

void foo(int* x)
{
  cout << "foo(int *) call\n";
}

void foo(int x)
{
  cout << "foo(int) call\n";
}

int main()
{
  int x = 123;
  foo(&x);
  foo(nullptr);
  foo(0);
  foo(x);
}
$ clang++ sample52.cpp
$ ./a.out
foo(int *) call
foo(int *) call
foo(int) call
foo(int) call

foo に &x または nullptr を渡すと、最初に定義した foo(int*) が呼び出されます。nullptr がポインタとして扱われていることがわかります。0 または 123 を渡すと 2 番目に定義した foo(int) が呼び出されます。

これは当然の動作ですが、今までのようにヌルポインタを整数 0 で表すと、最初の関数ではなく 2 番目の関数が呼び出されることになります。これでは、最初の関数を呼び出したいときに困ってしまいます。ヌルポインタを整数 0 ではなくポインタ型として扱うことができると、このような問題を回避することができます。

●配列とポインタ

次は配列へのポインタを考えてみましょう。C/C++の場合、配列名が配列の先頭要素のアドレスを表します。たとえば、int a[100]; という配列を定義すると、配列名 a は配列の先頭要素のアドレス &a[0] と同じ意味になります。C/C++の配列は連続したメモリ領域に配置されるので、ポインタを使って配列の要素にアクセスすることができます。

簡単な例を示しましょう。次のリストを見てください。

リスト : 配列とポインタ (sample53.cpp)

#include <iostream>
using namespace std;

int main(void)
{
  int a[] = {1, 2, 3, 4, 5, 6, 7, 8};
  int* p = a;
  cout << "p = " << p << endl;;

  cout << *p << endl;
  cout << *(p + 3) << endl;
  cout << *(p + 7) << endl;

  int* q = a + 8;
  cout << "q = " << q << endl;
  while (p < q) {
    cout << "p = " << p << endl;
    cout << *p++ << endl;
  }
}
$ clang++ sample53.cpp
$ ./a.out
p = 0x7ffd9232a5c0
1
4
8
q = 0x7ffd9232a5e0
p = 0x7ffd9232a5c0
1
p = 0x7ffd9232a5c4
2
p = 0x7ffd9232a5c8
3
p = 0x7ffd9232a5cc
4
p = 0x7ffd9232a5d0
5
p = 0x7ffd9232a5d4
6
p = 0x7ffd9232a5d8
7
p = 0x7ffd9232a5dc
8

配列 a の要素は int なので、変数 p を int 型のポインタとして宣言して、p を a の先頭要素のアドレスに初期化すれば、p を使って配列の要素にアクセスすることができます。配列名は先頭要素のアドレスを表すので、int* p = a; とすれば OK です。

要素のアクセスは簡単で、*p が先頭要素、*(p + 1) が 1 番目の要素というように、*(p + n) で n 番目の要素にアクセスすることができます。p は int 型のポイントなので、p + 1 は p のアドレスに 1 を加えるのではなく、int の大きさである 4 が加算されることに注意してください。また、*(p + 1) = 100; とすれば、a[1] の値を書き換えることができます。

配列は連続した領域に配置されるので、配列 a の領域を半区間 [p, q) (p 以上 q 未満) で表すと、[0x7ffd9232a5c0, 0x7ffd9232a5c0 + 4 * 8) であることがわかります。これは int* q = a + 8; で求めることができます。ここで、*p++ とすると、*p の要素を参照したあと、ポインタ p を次の要素へ進める (アドレスを +4 する) ことができます。

C/C++の場合、ポインタ同士の比較は可能なので、条件 p < q を満たすあいだ処理を繰り返せば、配列 a の要素を順番に表示することができます。このとき、p のアドレスが +4 ずつ増えていることに注意してください。

●Cスタイル文字列

もう一つ簡単な例を示しましょう。Cスタイル文字列は文字 (char) の配列なので、ポインタを使って操作することができます。次のリストを見てください。

リスト : 文字列とポインタ (sample54.cpp)

#include <iostream>
using namespace std;

int main()
{
  char a[] = "hello, world";
  char b[] = "hello, Linux";
  char c[16];
  char* p;
  char* q;
  // 長さ
  int len = 0;
  p = a;
  while (*p++ != '\0') len++;
  cout << len << endl;
  // コピー
  p = a;
  q = c;
  while ((*q++ = *p++) != '\0');
  cout << c << endl;
  // 比較
  p = a;
  q = b;
  while (*p == *q) {
    if (*p == '\0') break;
    p++;
    q++;
  }
  if (*p == 0 && *q == 0) {
    cout << "equal" << endl;
  } else {
    cout << "not equal" << endl;
  }
}
$ clang++ sample54.cpp
$ ./a.out
12
hello, world
not equal

Cスタイルの文字列はヌル文字 '\0' で終端されているので、文字数を数えるのは簡単です。ポインタ p を a に初期化し、ヌル文字でなければ p と len をインクリメントしていくだけです。文字列のコピーも簡単です。p を a に、q を c に初期化します。*p の値を *q に代入して、その値がヌル文字でなければ p と q をインクリメントしていきます。

文字列の比較も簡単です。p と q を初期化して、*p と *q が等しいあいだ、p と q をインクリメントします。このとき、*p がヌル文字かチェックして、そうであれば文字列の終端に到達したので break で while ループを脱出します。*p と *q がともにヌル文字であれば、文字列の内容は等しいことがわかります。そうでなければ、途中で文字が一致しなかったので、文字列は等しくないことがわかります。

このように、文字列の操作はポインタを使って簡単に行うことができます。なお、C++にはCスタイル文字列を操作する標準ライブラリ <cstring> が用意されているので、実際にこのようなプログラムを作る必要はありません。

●値呼びと参照呼び

一般に、関数呼び出しには二つの方法があります。一つが「値呼び (call by value)」で、もう一つが「参照呼び (call by reference)」です。近代的なプログラミング言語では「値呼び」が主流です。

値呼びの概念はとても簡単です。

  1. 受け取るデータを格納する変数 (仮引数) を用意する。
  2. データを引数に代入する。
  3. 関数の実行終了後、引数を廃棄する。

値呼びのポイントは 2 です。データを引数に代入するとき、データのコピーが行われるのです。たとえば、変数 a の値が 10 の場合、関数 foo(a) を呼び出すと、実引数 a の値 10 が foo の仮引数にコピーされます。変数に格納されている値そのものを関数に渡すので、値渡しとか値呼びと呼ばれます。また、値呼びは任意の式の値を実引数として渡すことができます。たとえば foo(a + b) の場合、引数に渡された式 a + bを計算し、その結果が foo の仮引数に渡されます。

値呼びは単純でわかりやすいのですが、呼び出し先 (caller) から呼び出し元 (callee) の局所変数にアクセスできると便利な場合もあります。仮引数に対する更新が直ちに実引数にも及ぶような呼び出し方が「参照呼び」です。

●関数にポインタを渡す

C/C++の関数は「値呼び」ですが、引数にポインタを渡すと「参照呼び」と同様の動作を行うことができます。次の例を見てください。

リスト : 関数にポインタを渡す (sample55.cpp)

#include <iostream>
using namespace std;

void swap_bad(int a, int b)
{
  int c = a;
  a = b;
  b = c;
}

void swap(int* a, int* b)
{
  int c = *a;
  *a = *b;
  *b = c;
}

int main()
{
  int a = 10;
  int b = 20;
  swap_bad(a, b);
  cout << "a = " << a << ", b = " << b << endl;
  swap(&a, &b);
  cout << "a = " << a << ", b = " << b << endl;  
}
$ clang++ sample55.cpp
$ ./a.out
a = 10, b = 20
a = 20, b = 10

関数 swap はC/C++の関数が「値呼び」であることの説明によく使われる例題です。C/C++で局所変数の値を交換するには、一時的に値を格納する局所変数を使って行います。この処理を関数で行う場合、swap_bad のように引数の型を int a, int b とすると、変数の値が引数にコピーされるので、元の局所変数の値を交換することはできません。この場合、swap のように引数の型を int* a, int* b として変数のアドレスを渡すと、元の局所変数の値を交換することができます。

今回はここまでです。次回はC++の参照 (リファレンス)、多段階のポインタ、関数へのポインタなどについて説明します。


初版 2015 年 8 月 16 日
改訂 2023 年 4 月 9 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | C++ | NextPage ]