はじめに
クラウド開発環境PaizaCloudクラウドIDE(ここからはPaizaCloudと書きます)でHello Worldをシリーズで書いています。今回はC言語ポインタ編です。
内容的には以下の続きです。
クラウド開発環境PaizaCloudクラウドIDEでHello World(C言語編)
今回は以下のような内容です。
- なぜポインタが大事で難しいか
- メモリ空間の説明
- printfデバッグの有効性(現場での重要さ)
- サンプルコード+キャストの注意
- 実行結果の要点
- ポインタの難しさ
- まとめ
なぜポインタは大事で難しいのか?
C言語の学習で多くの人が難しいと感じるのが「ポインタ」です。
その理由は次の2点にあります。
-
大事な理由
C言語ではポインタを使うことで効率的かつ柔軟にプログラムが書けます。
配列や関数、構造体と組み合わせることで高度なデータ構造や処理を実現できます。 -
難しい理由
ポインタが扱うのは「値そのもの」ではなく「メモリ上の場所(アドレス)」です。
直感的に理解しづらく、さらにメモリ管理(malloc / free)を自分で行う必要があるため、初心者にはハードルが高く感じられます。
メモリ空間とは?
C言語のプログラムが実行されるとき、コンピュータの「メモリ空間」にデータが展開されます。
メモリは基本的に 1バイト単位でアドレスが割り振られており、アドレス1つにつき1バイト分のデータを保持 できます。 つまり、整数型 int
(多くの環境で4バイト)なら、連続した4つのアドレスを占有して格納されることになります。
ポインタを使うと、これらの領域のアドレス(場所)を直接扱えるようになります。
以下は単純に10という値(Value)がとあるメモリ上のアドレス(Address)の場所に存在するという図です。

補足
ここで表示されるアドレスのサイズ(桁数)は実行環境に依存します。
今のPCは64ビット環境が主流なので、ポインタは8バイト(64ビット)で表現され、printf()での %p の出力も長い16進数になります。
実際の例
例えば int
型の配列を2つ並べて確保すると、アドレスが4ずつ飛んで配置されます(64bit環境の例):
arr[0] のアドレス = 0x7ffeea30
arr[1] のアドレス = 0x7ffeea34
このように、型ごとのサイズに応じて「次のアドレス」が決まるのです
領域の分類
- スタック領域 … 関数内のローカル変数が置かれる(関数呼び出しごとに積み重なる)
-
ヒープ領域 …
malloc
/free
によって動的に確保される領域 - 静的領域 … グローバル変数や静的変数が置かれる
printfでのデバッグのすすめ
C言語を学ぶとき、まず知っておくと便利なのが printf
を使ったデバッグ方法です。
ポインタやメモリを扱うと、プログラムがクラッシュ(異常終了)したり、思った通りの値が入っていなかったりすることがよくあります。 そんなときに 「途中経過を画面に出す」 だけで、原因の見当がつくことが多いのです。当然開発環境ではデバッガがありますが、printfでのデバッガの方法が基本になります。
なぜ有効か?
🔹 変数の値を確認できる
printf("a = %d\n", a);
とすれば、変数aが実際に何を持っているかすぐにわかります。
🔹 アドレスを確認できる
printf("a のアドレス = %p\n", (void*)&a);
とすれば、変数がメモリのどこに置かれているかを確認できます。
🔹 処理の流れを追える
関数の先頭やループの中で printf("ここまで来た\n"); と入れるだけで、処理がどこまで進んでいるかを知ることができます。
現場での有効性
- 組込みシステムや制御ソフトなど、デバッガが使えない/制限のある環境は珍しくない
- そうした環境では、シリアル出力やログとしてのprintfが今でも第一のデバッグ手段
- 「どの変数がどの値になっているか」「どこまで処理が進んだか」を即座に把握できるので、実務でも欠かせない
注意点
- printf("%p") を使うときは (void*) にキャストするのがC言語の規格で正しい書き方です。
- 出力のしすぎはかえって見にくくなるので、必要な箇所に絞るのがコツです。
このように printf を活用することで、「変数の中身」と「メモリ上での位置」を確認しながら理解を深めることができます。
次のサンプルプログラムでは、この方法を使って ポインタの実体を見える化していきます。
サンプルコード
#include <stdio.h>
#include <stdlib.h>
int global_var = 42; // 静的領域に展開される
int main(void) {
int a = 10;
int b = 20;
int arr[3] = {1, 2, 3};
// 普通の変数(スタック)
printf("a の値 = %d, アドレス = %p\n", a, (void*)&a);
printf("b の値 = %d, アドレス = %p\n", b, (void*)&b);
// ポインタ変数
int *p = &a;
printf("p が指す値 = %d, p の値(=aのアドレス) = %p, p自体のアドレス = %p\n",
*p, (void*)p, (void*)&p);
// 配列のアドレス(スタック上に連続して確保)
printf("arr = %p (配列の先頭アドレス)\n", (void*)arr);
printf("&arr[0] = %p (最初の要素のアドレス)\n", (void*)&arr[0]);
printf("&arr[1] = %p (次の要素のアドレス)\n", (void*)&arr[1]);
// ポインタで確認
int *ptr = arr; // arr[0] のアドレスを保持
printf("ptr = %p (ptrが指すアドレス)\n", (void*)ptr);
printf("ptr + 1 = %p (int型なので4バイト進む → &arr[1] と同じ)\n", (void*)(ptr + 1));
// 動的確保(ヒープ)
int *dyn = malloc(3 * sizeof(int));
dyn[0] = 100; dyn[1] = 200; dyn[2] = 300;
printf("mallocで確保した領域の先頭アドレス = %p\n", (void*)dyn);
printf("dyn[1] の値 = %d, アドレス = %p\n", dyn[1], (void*)&dyn[1]);
free(dyn);
// グローバル変数
printf("global_var の値 = %d, アドレス = %p\n", global_var, (void*)&global_var);
return 0;
}
注意:%p とキャスト
繰り返しますが、printf("%p") の引数は 必ず void* 型 であるとC言語の規格で定められています。int* のまま渡すと環境によっては動きますが、警告が出たり未定義動作になる可能性があります。そのため (void*) にキャストして使うのが正しい書き方です。
実行結果の例
以下はPaizaCloudで実行した例です。アドレスの表記は長い上に16進数なので、見にくいとは思いますが、3桁ぐらいで同じかどうか、差がどれぐらいあるかの比較をすればいいと思います。
実行して分かること
🔹 変数ごとに異なるアドレスを持つ
printf("a の値 = %d, アドレス = %p\n", a, (void*)&a);
printf("b の値 = %d, アドレス = %p\n", b, (void*)&b);
実行例ですと、変数aのアドレス下3桁が878で変数bのアドレスが87cです。これで変数aが4アドレス分の領域があって続いて変数bが配置されているということが分かります。
同様に見ていきます。
🔹 ポインタはアドレスを保持する変数であり、*p でその中身にアクセスできる
printf("p が指す値 = %d, p の値(=aのアドレス) = %p, p自体のアドレス = %p\n",
*p, (void*)p, (void*)&p);
実行例では、pの値とaのアドレス、pが指す値とaの値が同じことが分かります。
🔹 配列は連続領域になっていて、要素ごとに型サイズ分だけアドレスが増える
printf("arr = %p (配列の先頭アドレス)\n", (void*)arr);
printf("&arr[0] = %p (最初の要素のアドレス)\n", (void*)&arr[0]);
printf("&arr[1] = %p (次の要素のアドレス)\n", (void*)&arr[1]);
実行例では配列の先頭アドレスと最初の要素のアドレスは同じで、次の要素が4アドレス分離れています。この長さはint型の長さです。
🔹 配列はポインタでもアクセスできる
printf("ptr = %p (ptrが指すアドレス)\n", (void*)ptr);
printf("ptr + 1 = %p (int型なので4バイト進む → &arr[1] と同じ)\n", (void*)(ptr + 1));
実行例では、ポインタ変数で配列にアクセス出来ていることが分かります。注意ですが、ptr + 1 でint型の長さ を進めます。これでint型の配列にアクセス出来ます。
🔹 malloc/free で確保されるのはヒープ領域
printf("mallocで確保した領域の先頭アドレス = %p\n", (void*)dyn);
printf("dyn[1] の値 = %d, アドレス = %p\n", dyn[1], (void*)&dyn[1]);
ヒープ領域に確保されたアドレスが分かります。
🔹 グローバル変数は静的領域に展開される
printf("global_var の値 = %d, アドレス = %p\n", global_var, (void*)&global_var);
静的領域に確保されたアドレスが分かります。
ポインタの難しさの一つ
今までの内容を踏まえてですが、ポインタ変数を使うと、別の変数のように見えて実は同じメモリ領域を操作できる という特徴があります。
これは便利でもありますが、慣れていないと「どこで値が書き換わったのか分からない」という混乱の原因になります。
例
#include <stdio.h>
int main(void) {
int a = 10;
int *p = &a; // pはaのアドレスを指す
printf("a = %d\n", a);
*p = 20; // ポインタを通じてaを書き換える
printf("a = %d (ポインタから変更後)\n", a);
return 0;
}
ポイント
- p というポインタ変数を使って *p = 20; と書いたのに、結果として a が書き換わっている
- つまり「変数a」と「ポインタp経由の *p」は 同じメモリ領域を指している ので、どちらからでも値を変えられる
このように、ポインタは「どの領域を指しているか」を常に意識しないと、プログラムのどこで値が変更されたのか分かりにくいという難しさがあります。
この例では見れば分かると言えるかもしれないですが、関数間での引数でやり取りをしたりすると分かりにくくなる場合もあります。
初学者のうちは「ポインタを通じて書き換えると元の変数も変わる」という事実をまず体感するのがおすすめです。
まとめ
- printf("%p") を使えば、メモリ空間における変数の位置を直感的に理解できる
- ポインタは「アドレスを保持する変数」であり、値そのものではなく場所を扱う
- 配列はポインタでも順にアクセスできる
- ポインタ変数を使うと、別の変数のように見えて実は同じメモリ領域を操作できる
- 配列・動的メモリ・グローバル変数の位置を確認することで、C言語のメモリモデルが体感できる
次回は続きとしてデータベースのように「レコードを順次追加して管理する」といった場面で使用する構造体をレコード形式で管理し、realloc を使って可変長リストを扱う方法を紹介します。
クラウド開発環境PaizaCloudクラウドIDEでHello World(C言語ポインタ 発展編)
ありがとうございます。