前回の記事では、プログラムがRAMをどのように使用するのかを理解するために、スタックとヒープについて解説しました。
そこで登場したのが、次のようなコードです。
int *data;
data = (int *)malloc(sizeof(int) * 10);
この*が付いた変数は、ポインタ(Pointer)と呼ばれます。
ポインタはC/C++を学習するときにつまずきやすいテーマの1つですが、考え方はシンプルです。
ポインタとは、メモリ上の場所を示すアドレスを扱うための仕組みです。
ポインタを理解すると、配列、関数、構造体、動的メモリ確保だけでなく、組み込み開発で重要なレジスタ操作やメモリマップドI/Oについても理解しやすくなります。
ポインタとは?
ポインタとは、メモリアドレスを保持するための変数です。
通常の変数には「値」を保存します。
int a = 10;
この場合、変数aには整数の10が保存されています。
一方、ポインタには値そのものではなく、その値が保存されている場所のアドレスを保存できます。
上図のように、通常の変数が「値」を持つのに対して、ポインタは「その値が置かれている場所」を表すアドレスを保持します。
そもそもメモリアドレスとは?
プログラムで使用する変数は、メモリ上のどこかに配置されます。
それぞれの場所にはアドレスがあります。
たとえば説明用として、次のような状態を考えてみます。
アドレス 値 0x20001000 10 0x20001004 20 0x20001008 0
0x20001000という場所に10が保存されている、と考えることができます。
実際のアドレスは実行環境や配置によって変わります。上記の値は仕組みを説明するための例です。
変数にもアドレスがある
次のように変数を宣言したとします。
int a = 10;
プログラムではaという名前を使っていますが、実際にはメモリ上のどこかに値を保存する領域があります。
変数名:a アドレス 0x20001000 値 10
普段はアドレスを意識しなくても、変数名を使えばコンパイラが適切なアクセスを生成してくれます。
&を使うと変数のアドレスを取得できる
C/C++では、変数の前に&を付けることで、その変数のアドレスを取得できます。
int a = 10;
&a
aは変数の値を表し、&aは変数aのアドレスを表します。
| 記述 | 意味 |
|---|---|
a | 変数aの値 |
&a | 変数aのアドレス |
ポインタ変数を宣言してみよう
整数型intの変数を指すポインタは、次のように宣言できます。
int *p;
pは、int型のデータを指すポインタです。
ここへ変数aのアドレスを保存してみます。
int a = 10;
int *p;
p = &a;
この状態を図にすると、次のような関係になります。
変数 a
┌─────────┐
│ 10 │
└─────────┘
▲
│
│ aのアドレス
│
┌─────────┐
│ p │
└─────────┘
ポインタ
ポインタpには10が入っているのではなく、aが存在する場所のアドレスが入っています。
ポインタの*は何を意味する?
ポインタが指している場所の値へアクセスするときは、*を使います。
int a = 10;
int *p = &a;
int b = *p;
pにはaのアドレスが保存されています。
*pとすると、そのアドレスが指している場所へアクセスし、そこに保存されている値を参照できます。
p │ │ アドレス ▼ ┌─────────┐ │ 10 │ └─────────┘ ↑ *p
このようにポインタが指している先へアクセスすることを、間接参照(dereference)と呼びます。
&と*を整理しよう
ポインタで最初につまずきやすいのが&と*です。
| 記述 | 意味 |
|---|---|
&a | aのアドレスを取得する |
p | pに保存されているアドレス |
*p | pが指している場所の値 |
まずは「&で住所を調べる」「*でその住所にあるものを見る」と考えると理解しやすいでしょう。
ポインタ経由で値を書き換えられる
ポインタは値を読むだけでなく、指している場所の値を書き換えることもできます。
int a = 10;
int *p = &a;
*p = 20;
このコードを実行すると、pが指している変数aの値が変更されます。
実行前
a = 10
↓
*p = 20;
↓
実行後
a = 20
ポインタ自身にもアドレスがある
ポインタも変数の一種なので、ポインタ自身もメモリ上のどこかに存在します。
int a = 10;
int *p = &a;
概念的には次のようになります。
0x20001000 ┌─────────────┐ │ 10 │ ← a └─────────────┘ 0x20001008 ┌─────────────┐ │ 0x20001000 │ ← p └─────────────┘
つまりポインタp自身にもアドレスがあり、その中に別の場所を示すアドレスが保存されています。
ポインタにも型がある
ポインタには「何の型のデータを指しているか」という型情報があります。
int *p1;
char *p2;
float *p3;
uint32_t *p4;
| 型 | 意味 |
|---|---|
int * | int型のデータを指すポインタ |
char * | char型のデータを指すポインタ |
float * | float型のデータを指すポインタ |
uint32_t * | uint32_t型のデータを指すポインタ |
ポインタの型は、間接参照したときにどの型のデータとして扱うのか、ポインタ演算でどれだけアドレスを進めるのか、といった動作に関係します。
配列とポインタの関係
C/C++では、配列とポインタには深い関係があります。
int data[3] = {10, 20, 30};
int *p = data;
多くの式の中で配列名dataは、配列の先頭要素を指すポインタへ変換されます。
そのため、次のように要素へアクセスできます。
printf("%d\n", *p); // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30
p + 1は単純にアドレスを1バイト増やすという意味ではありません。
int *であれば、次のint要素を指すようにアドレスが進みます。
配列名とポインタは完全に同じではない
「配列=ポインタ」と説明されることがありますが、厳密には同じものではありません。
配列は複数の要素を持つオブジェクトであり、ポインタはアドレスを保持する変数です。
ただし、配列名は多くの式で先頭要素へのポインタへ変換されるため、ポインタと似た書き方で扱える場面が多くあります。
関数へポインタを渡す
ポインタを関数へ渡すことで、呼び出し元の変数を関数内から変更できます。
void setValue(int *p) {
*p = 100;
}
int main() {
int a = 0;
setValue(&a);
// a は 100
}
setValue()にはaの値そのものではなく、aのアドレスを渡しています。
関数内で*pを書き換えることで、呼び出し元のaを変更できます。
構造体とポインタ
組み込み開発では、構造体をポインタで扱うことも頻繁にあります。
typedef struct {
int x;
int y;
} Point;
Point p1 = {10, 20};
Point *p = &p1;
構造体ポインタからメンバへアクセスするときには、->演算子を使用できます。
printf("%d\n", p->x);
printf("%d\n", p->y);
p->xは、次の記述と同じ意味です。
(*p).x
ヒープとポインタの関係
前回学習したヒープでもポインタが重要になります。
int *data;
data = (int *)malloc(sizeof(int) * 10);
malloc()は確保したメモリ領域の先頭アドレスを返します。
そのアドレスをdataというポインタへ保存することで、確保したメモリへアクセスできます。
不要になったら解放します。
free(data);
data = NULL;
解放後にその領域へアクセスしてはいけません。NULLを代入することは、誤って同じポインタを使い続けるリスクを減らす方法の1つです。
NULLポインタとは?
NULLポインタは、有効なオブジェクトを指していないことを表すために使われます。
int *p = NULL;
この状態で*pのように間接参照してはいけません。
if (p != NULL) {
printf("%d\n", *p);
}
C++では、使用する言語バージョンや環境によってはnullptrを使うこともできます。
初期化していないポインタに注意
次のようなコードは危険です。
int *p;
*p = 10;
pが有効なアドレスを指すように初期化されていないため、どこへ書き込むのか分かりません。
誤ったアドレスへアクセスすると、プログラムのクラッシュやデータ破壊などにつながる可能性があります。
ポインタとメモリマップドI/O
ここで、以前学習したメモリマップドI/Oとポインタがつながります。
周辺回路のレジスタには、CPUからアクセスするためのアドレスが割り当てられています。
説明用として、次のようなコードを考えてみます。
volatile uint32_t *reg =
(volatile uint32_t *)0x60000020;
*reg = 0x00000001;
このコードでは、0x60000020というアドレスをuint32_t型のレジスタとして扱うポインタを作っています。
*regへ値を書き込むことで、そのアドレスに対応したレジスタへアクセスするイメージです。
ここで使用しているアドレスは説明用の架空の値です。実際のESP32-C3のレジスタアドレスではありません。
メモリマップドI/Oについては「メモリマップドI/O(MMIO)とは?アドレスでマイコンの周辺回路を制御する仕組みを解説」で詳しく説明しています。
なぜvolatileを付けるの?
ハードウェアレジスタを指すポインタでは、次のようにvolatileが付いていることがあります。
volatile uint32_t *reg;
周辺回路のレジスタはハードウェアによって値が変化したり、読み書きそのものに意味があったりします。
volatileを指定することで、そのようなアクセスを通常の変数と同じように不用意に省略・統合しないようコンパイラへ伝えます。
ポインタのサイズは何バイト?
ポインタにはアドレスを保存する必要があるため、ポインタ自身にもサイズがあります。
ただし、ポインタのサイズを「必ず4バイト」などと決めつけることはできません。
ポインタのサイズはCPUアーキテクチャやABIなどの実行環境によって異なります。
printf("%zu\n", sizeof(void *));
sizeof()を使えば、その環境でのポインタサイズを確認できます。
ポインタを使うときの注意点
ポインタは非常に強力ですが、誤ったアドレスへアクセスできてしまうため注意が必要です。
- 初期化していないポインタを使用しない
- NULLポインタを間接参照しない
- 配列の範囲外へアクセスしない
- 指しているデータに適した型を使う
- 解放済みのメモリへアクセスしない
- ポインタが指すオブジェクトの寿命に注意する
- 任意のアドレスへ不用意に読み書きしない
特にマイコンでは、不正なアドレスへのアクセスによって例外やリセットが発生したり、周辺回路へ意図しない操作を行ったりする可能性があります。
KUMITATE-C3でポインタを学ぶ意味
KUMITATE-C3をArduinoで使うだけなら、最初からポインタを深く理解していなくてもLEDやセンサなどを動かせます。
しかし、ライブラリのソースコードやESP-IDF、ドライバなどを読み始めると、ポインタは頻繁に登場します。
- データを関数へ渡す
- 配列やバッファを扱う
- 構造体を扱う
- 動的メモリを扱う
- ドライバやHALを読む
- 周辺回路のレジスタを扱う
ポインタを理解することで、Arduinoの関数を使うだけの段階から、ライブラリやマイコン内部の仕組みを読める段階へ進みやすくなります。
これまで学んだ内容がつながる
ここまでの記事を順番に読むと、組み込みプログラムがハードウェアを操作する流れがかなり見えてきます。
変数 ↓ メモリアドレス ↓ ポインタ ↓ ビット演算 ↓ レジスタ ↓ メモリマップドI/O ↓ GPIOなどの周辺回路 ↓ 実際のハードウェア
一方、プログラムがRAMをどのように使用するかという方向では、次のようにつながります。
変数・配列
↓
スタック / ヒープ
↓
メモリアドレス
↓
ポインタ
↓
データへのアクセス
まとめ
ポインタは、C/C++や組み込み開発を理解するうえで重要な仕組みです。
- ポインタはメモリアドレスを保持する変数
&を使うと変数のアドレスを取得できる*を使うとポインタが指す場所の値へアクセスできる- ポインタ経由で値を書き換えることもできる
- ポインタには指しているデータに応じた型がある
- 配列とポインタには深い関係がある
- 関数へアドレスを渡すことで呼び出し元のデータを変更できる
- malloc()などで確保したヒープ領域もポインタを通して扱う
- メモリマップドI/Oでもポインタを使ってレジスタへアクセスできる
- 不正なアドレスへのアクセスには十分注意する
ポインタは最初は記号が多く難しく見えますが、「アドレスを保存する」「そのアドレスの先を見る」という2つを理解すると整理しやすくなります。
組み込み開発ではポインタを理解することで、配列や通信バッファだけでなく、ドライバ、レジスタ、メモリ管理など、より低レイヤーのコードも読みやすくなります。

