C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。
基本文法の復習を終えてから読む場合は、C言語の基礎練習問題集の配列の最大値までを解いておくと理解しやすくなります。
C言語のポインタは、初めて学ぶと特にイメージしづらい部分です。
* や & という記号が出てくるため、記号の意味だけを覚えようとして混乱しやすくなります。
最初に押さえることは、次の関係です。
ptrのようなポインタ変数は、ほかの変数を指すためのアドレスを保存できます。
&numberでアドレスを取り出し、*ptrでその先の変数を扱います。
この記事では、変数・アドレス・ポインタ変数・&・* の関係を、図と短いコードで順番に確認します。
ここで扱うのは、主に変数を指すポインタです。
number のアドレスと、number へのポインタは、初めてのうちはほぼ同じイメージで扱って構いません。&number は「number のアドレス」であり、「number を指すポインタとして使える値」でもあります。
一方で、ptr はその値を保存するポインタ変数です。この区別だけは押さえておくと、説明やコードを読み違えにくくなります。
最初から細かい仕組みをすべて理解する必要はありません。
まずは、コードを見たときに次の3つを区別できるようになれば十分です。
値を扱っているのか
アドレスを扱っているのか
アドレスを保存するポインタ変数を扱っているのか
この記事で分かること
- 変数には値とアドレスがある
-
&は変数のアドレスを取得する -
ptrのようなポインタ変数にはアドレスを保存できる -
*ptrは、ポインタ変数が指す先の変数を扱う - 文字列は
char型の配列として扱われる - 配列名は、多くの式の中で先頭要素を指すポインタとして使われる
- ポインタを使って元の変数の値を変更できる
- 関数へアドレスを渡すと、呼び出し元の変数を変更できる
- 初期化していないポインタ変数や
NULLをそのまま使ってはいけない
この記事では、ポインタの基本的な考え方に集中します。
次の内容は、別の記事で扱います。
- 構造体と構造体ポインタ
- 配列とポインタの細かい関係やポインタ演算
- 動的メモリ確保
-
malloc、calloc、realloc、free memset
最初に覚える言葉
ポインタを理解する前に、言葉の関係を確認します。
| 言葉 | 意味 |
|---|---|
| 値 | 変数の中に入っているもの。例:10
|
| アドレス | 変数がメモリ上に置かれている場所 |
| ポインタ | ある変数やデータを指し示す値。この記事では、変数のアドレスを表すものとして扱う |
| ポインタ変数 | ポインタを保存する変数。例:int *ptr の ptr
|
たとえば、&number は number のアドレスです。同時に、number を指すポインタとして使える値です。
number :number の値
&number :number のアドレス(number を指すポインタ)
ptr :そのアドレスを保存するポインタ変数
アドレスは、次のような値で表されます。
0x1004
これは説明用の例です。実際のアドレスは、実行する環境や実行するタイミングによって変わります。
アドレスの数字そのものを覚える必要はありません。
「変数には値だけでなく、置かれている場所もある」と考えることが大切です。
1. 変数には「値」と「場所」がある
まずは普通の変数を見ます。
int number = 10;
このコードでは、number という変数を作り、その中に 10 を入れています。
ここで、number には次の2つがあります。
値:10
場所:メモリ上のどこか
図では、number の値が 10、置かれている場所が 0x1004 になっています。
次のように分けて考えます。
number :number の値
&number :number が置かれている場所
この時点では、次の理解で十分です。
変数は、値を入れる箱
アドレスは、その箱が置かれている場所
2. & は変数のアドレスを取得する
変数の前に & を付けると、その変数のアドレスを取得できます。
#include <stdio.h>
int main(void)
{
int number = 10;
// %p はアドレスを表示するための指定です。
// %p には void * 型の値を渡すため、(void *) を付けます。
printf("%p\n", (void *)&number);
return 0;
}
&number は、number の値ではありません。
number が置かれている場所、つまり number のアドレスです。
| 書き方 | 意味 |
|---|---|
number |
number の値 |
&number |
number が置かれている場所。number を指すポインタとして使える値 |
たとえば、次のように考えます。
number :10
&number :0x1004
この記事では、&number を「number のアドレス」と呼びます。number へのポインタという言い方をしても、ここでは同じ内容を指しています。
3. ポインタ変数にアドレスを保存する
ポインタ変数は、ほかの変数のアドレスを保存できます。
int number = 10;
int *ptr = &number;
このコードは、次の順に読むと分かりやすくなります。
number に 10 を入れる
number のアドレスを取得する
そのアドレスを ptr に入れる
ここで大切なのは、ptr に 10 は入っていないことです。
ptr に入っているのは、number のアドレスです。
number :10
ptr :number が置かれている場所
ptr は、number のアドレスを保存するポインタ変数です。
ポインタ変数は、次のようにイメージすると分かりやすくなります。
number は値が入った箱
ptr は箱の場所を書いたメモ
int *ptr の * は何か
次のコードでは、ptr を int 型の変数を指せるポインタ変数として宣言しています。
int *ptr;
ptr の型は int * 型です。
これは、「ptr には int 型の変数を指すアドレスを保存できる」という意味です。
そのため、int 型の変数である number のアドレスを入れられます。
int number = 10;
int *ptr = &number;
この宣言の * は、「ptr がポインタ変数である」ことを表します。
次の章で扱う *ptr は、ptr が指している先の変数を扱うために使います。
同じ * でも、宣言の中に書く場合と式の中に書く場合では役割が異なります。
複数のポインタ変数を宣言するときは、* をそれぞれの変数名の前に書きます。
int *first_pointer;
int *second_pointer;
次の書き方では、second はポインタ変数ではありません。
int *first_pointer, second;
4. * はポインタ変数が指している先を扱う
式の中で *ptr と書くと、ptr が持っているアドレスの先にある変数を扱えます。
#include <stdio.h>
int main(void)
{
int number = 10;
int *ptr = &number;
printf("%d\n", *ptr);
return 0;
}
実行結果です。
10
ptr に入っているのはアドレスです。
一方、*ptr は、そのアドレスにある number を表します。printf の中では、その値である 10 が使われます。
次の違いを整理します。
| 書き方 | 意味 |
|---|---|
ptr |
number のアドレスを保存しているポインタ変数 |
*ptr |
ptr が指している先の変数。この例では number
|
たとえば、次のように考えます。
ptr :住所が書かれたメモを見る
*ptr :メモに書かれた住所へ行き、そこにある箱を使う
*ptr は、値を読むだけでなく、値を書き換えるためにも使えます。
*ptr = 20;
このコードでは、ptr が指している先、つまり number に 20 を入れます。
5. 文字列は char 型の配列
C言語では、名前やメッセージのような文字の並びを、文字列として扱います。
文字列は、char 型の値を順番に並べた配列です。
たとえば、次のコードを見てください。
char name[] = "Egami";
見た目では、Egami という5文字を入れているように見えます。
ただし、C言語の文字列には、最後に文字列の終わりを表す \0 が自動的に入ります。
\0 は画面には表示されませんが、文字列の終わりを表すために必要な特別な値です。
printf("%s", name); のように文字列を表示するときは、\0 を見つけた位置までを文字列として扱います。
そのため、"Egami" は5文字ですが、char 型の配列としては、\0 を含めて6個の要素を使います。
次のコードで、文字列全体と先頭の文字を確認できます。
#include <stdio.h>
int main(void)
{
char name[] = "Egami";
printf("名前: %s\n", name);
printf("先頭の文字: %c\n", name[0]);
printf("2文字目: %c\n", name[1]);
return 0;
}
実行結果です。
名前: Egami
先頭の文字: E
2文字目: g
name[0] は先頭の E、name[1] は次の g を表します。
name[0] : E
name[1] : g
name[2] : a
name[3] : m
name[4] : i
name[5] : '\0'
文字列を入れる char 型の配列には、表示したい文字数に加えて、最後の \0 を入れる場所が必要です。
たとえば、Egami を入れる場合は5文字だけではなく、6個分の char 型の領域が必要です。
配列名は、先頭要素を指すポインタとして使える
文字列を入れた配列は、ポインタ変数とつなげて使えます。
char name[] = "Egami";
char *name_pointer = name;
name は配列です。ポインタ変数ではありません。
ただし、多くの式の中では、配列名の name は先頭要素である name[0] を指すポインタとして使われます。
char name[] = "Egami";
char *name_pointer = name;
// char *name_pointer = &name[0]; と同じように使えます。
このとき、name_pointer には name[0] のアドレスが入ります。
そのため、次の2つは同じ文字を表します。
name[0]
*name_pointer
実際に確認してみます。
#include <stdio.h>
int main(void)
{
char name[] = "Egami";
char *name_pointer = name;
printf("配列から取り出す: %c\n", name[0]);
printf("ポインタから取り出す: %c\n", *name_pointer);
return 0;
}
実行結果です。
配列から取り出す: E
ポインタから取り出す: E
ただし、配列とポインタ変数は同じものではありません。
name は文字を保存するための配列です。
一方、name_pointer は、配列の先頭要素を指すアドレスを保存するポインタ変数です。
そのため、ポインタ変数は別の場所を指すように変更できます。
name_pointer = &name[1];
この場合、name_pointer は g の場所を指します。
一方、配列名そのものへ別の場所を代入することはできません。
// name = &name[1]; // 配列名には代入できません。
文字列リテラルをポインタで扱う場合
次の書き方では、message は文字列そのものではなく、文字列リテラルの先頭を指すポインタ変数です。
const char *message = "Hello";
文字列リテラルの内容を書き換えてはいけません。
そのため、文字列リテラルを指す場合は、const char * と書く形を基本にします。
const char *message = "Hello";
// message[0] = 'h'; // 変更してはいけません。
文字列の内容を変更したい場合は、char の配列として用意します。
char message[] = "Hello";
message[0] = 'h';
ここまでの内容は、次のように整理できます。
| 書き方 | 意味 |
|---|---|
char name[] = "Egami"; |
文字列を入れた char の配列 |
name[0] |
先頭の文字 |
name |
多くの式の中では、先頭要素を指すポインタとして使われる |
&name[0] |
先頭の文字が置かれている場所 |
char *name_pointer = name; |
先頭の文字を指すアドレスを保存するポインタ変数を作る |
*name_pointer |
ポインタ変数が指している文字 |
const char *message = "Hello"; |
変更しない文字列リテラルを指すポインタ変数 |
6. ポインタを使って値を変更する
*ptr は代入の左側にも書けるため、指している先の変数の値を変更できます。
#include <stdio.h>
int main(void)
{
int number = 10;
int *ptr = &number;
*ptr = 20;
printf("number = %d\n", number);
return 0;
}
実行結果です。
number = 20
コードでは number = 20; と直接書いていません。
それでも number の値が変わっています。
理由は、ptr が number のアドレスを持っており、*ptr = 20; でそのアドレスにある number を書き換えたためです。
このコードは、次のように整理できます。
number :値そのもの
&number :number のアドレス
ptr :number のアドレスを保存したポインタ変数
*ptr :ptr が指している先の変数。この例では number
ここまでの内容を、短いコードで確認します。
int number = 10;
int *ptr = &number;
*ptr = 20;
このコードは、次の意味です。
number を作る
number のアドレスを ptr に入れる
ptr が指している先の値を 20 にする
結果として number が 20 になる
7. ポインタ変数は指す先を切り替えられる
ポインタ変数には、別の変数のアドレスを入れ直せます。
#include <stdio.h>
int main(void)
{
int first = 10;
int second = 20;
int *ptr = &first;
printf("*ptr = %d\n", *ptr);
ptr = &second;
printf("*ptr = %d\n", *ptr);
return 0;
}
実行結果です。
*ptr = 10
*ptr = 20
最初の ptr は first のアドレスを持っています。
int *ptr = &first;
そのあと、ptr に second のアドレスを入れています。
ptr = &second;
次の2つは意味が違います。
ptr = &second;
*ptr = second;
| 書き方 | 意味 |
|---|---|
ptr = &second; |
ptr に second のアドレスを入れ、指す先を second に切り替える |
*ptr = second; |
ptr が現在指している変数に、second の値を入れる |
ここは混同しやすいため、コードを書くときに「アドレスを入れたいのか」「値を入れたいのか」を確認してください。
8. 関数へアドレスを渡すと、元の変数を変更できる
関数へ普通の値を渡した場合、関数の中で受け取るのは値のコピーです。
そのため、関数の中で値を変更しても、呼び出し元の変数は変わりません。
まずは、値を渡す場合を確認します。
#include <stdio.h>
// 受け取った値を 3 増やします。
// ただし、受け取るのは呼び出し元の値のコピーです。
void add_three(int temperature)
{
temperature = temperature + 3;
}
int main(void)
{
int temperature = 22;
printf("変更前: %d\n", temperature);
add_three(temperature);
printf("変更後: %d\n", temperature);
return 0;
}
実行結果です。
変更前: 22
変更後: 22
add_three の中では temperature を 25 にしています。
ただし、関数の中にある temperature は、main 関数にある temperature のコピーです。
そのため、main 関数の temperature は変わりません。
main 関数の temperature
22
↓
値のコピーを関数へ渡す
↓
add_three 関数の temperature
22
↓
関数の中だけで 25 に変わる
↓
main 関数の temperature は 22 のまま
次に、アドレスを関数へ渡す場合を確認します。
#include <stdio.h>
// 渡されたアドレスにある値を 3 増やす関数です。
void raise_temperature(int *temperature_pointer)
{
*temperature_pointer = *temperature_pointer + 3;
}
int main(void)
{
int temperature = 22;
printf("変更前: %d\n", temperature);
// temperature が置かれている場所を関数へ渡します。
raise_temperature(&temperature);
printf("変更後: %d\n", temperature);
return 0;
}
実行結果です。
変更前: 22
変更後: 25
処理の流れです。
temperature のアドレスを関数へ渡す
↓
raise_temperature 関数が、そのアドレスを受け取る
↓
*temperature_pointer で、そのアドレスにある temperature を扱う
↓
値を 3 増やす
↓
main 関数の temperature も 25 になる
このように、関数の呼び出し元にある変数へ変更を反映したいとき、アドレスを引数として渡します。
9. 初めてのうちは避ける書き方
ポインタ変数は、正しいアドレスが入っている状態で使う必要があります。
アドレスが正しくない状態で *ptr を使うと、プログラムが異常終了したり、関係ないデータを壊したりすることがあります。
初期化していないポインタ変数を使う
次のコードは危険です。
int *ptr;
*ptr = 10;
ptr には、どの変数を指すアドレスが入っているか保証されていません。
初期化していないポインタ変数は、値を確認しようとしても使わないでください。
変数を指す場合は、使う前に正しいアドレスを入れます。
int number = 10;
int *ptr = &number;
NULL のまま使う
NULL は、ポインタ変数が有効な変数やデータを指していない状態を表すために使います。
#include <stddef.h>
int *ptr = NULL;
この状態で *ptr を使ってはいけません。
*ptr = 10;
NULL を使う場合は、使用前に確認します。
#include <stddef.h>
int *ptr = NULL;
if (ptr != NULL)
{
*ptr = 10;
}
この例では、ptr が NULL であるため、*ptr = 10; は実行されません。
型が違うアドレスを入れる
ポインタ変数の型と、指す変数の型は合わせます。
int number = 10;
int *ptr = &number;
number は int 型なので、ptr も int * 型です。
次のように、型を強制変換して無理に別の型の変数を指す書き方は避けてください。
double price = 100.5;
int *ptr = (int *)&price;
このようなポインタ変数を使って price を int 型として読み書きすると、期待しない値になったり、プログラムが正しく動かなくなったりします。
型が違うデータを扱いたい場合は、ポインタの型を無理に変えるのではなく、適切な型の変数を使ってください。
10. イメージで覚える
ポインタ変数は、住所を書いたメモにたとえると分かりやすくなります。
| C言語の要素 | たとえ |
|---|---|
| 変数 | 値が入った箱 |
| アドレス | 箱が置かれている住所 |
&number |
number の住所を調べる |
ポインタ変数 ptr
|
住所を書いておくメモ |
*ptr |
メモに書かれた住所へ行き、その場所にある箱を使う |
このたとえは、最初の理解を助けるためのものです。
&number は「number のアドレス」であり、「number を指すポインタ」と言っても、この段階では同じように扱えます。
コードでは、次のように読めれば十分です。
int number = 10;
int *ptr = &number;
*ptr = 20;
number に 10 を入れる
ptr に number のアドレスを覚えさせる
ptr が指す先の値を 20 に変える
number の値が 20 になる
11. 確認課題
課題1:関数へアドレスを渡して値を変える
次のコードは、温度をポインタ変数を通して変更するプログラムです。
実行する前に、temperature、ptr、*ptr がそれぞれ何を表しているか考えてみてください。
#include <stdio.h>
void raise_temperature(int *temperature_pointer)
{
*temperature_pointer = *temperature_pointer + 3;
}
int main(void)
{
int temperature = 22;
int *ptr = &temperature;
printf("変更前: %d\n", temperature);
raise_temperature(ptr);
printf("変更後: %d\n", temperature);
return 0;
}
次の順番で考えます。
- 最初の
temperatureの値は何か -
ptrには何が入っているか -
raise_temperature(ptr);で関数に何が渡るか -
*temperature_pointerは、どの変数を表すか - 最後に
temperatureはいくつになるか
答えを見る
最初の状態です。
temperature :22
ptr :temperature のアドレスを保存するポインタ変数
*ptr :temperature。この時点では値は 22
raise_temperature(ptr); では、ptr に入っている temperature のアドレスが関数へ渡されます。
関数の中では、temperature_pointer が temperature のアドレスを持ちます。
*temperature_pointer = *temperature_pointer + 3;
これは、temperature に 3 を足す処理です。
実行結果は次のとおりです。
変更前: 22
変更後: 25
追加課題
次のコードを実行すると、first と second はそれぞれいくつになるでしょうか。
#include <stdio.h>
int main(void)
{
int first = 10;
int second = 20;
int *ptr = &first;
ptr = &second;
*ptr = 99;
printf("first = %d\n", first);
printf("second = %d\n", second);
return 0;
}
答えを見る
first = 10
second = 99
ptr = &second; によって、ptr には second のアドレスが入ります。
そのあとで *ptr = 99; を実行しているため、変更されるのは second です。
発展課題:ポインタを使って2つの値を入れ替える
次のプログラムを完成させてください。
swap 関数は、first と second のアドレスを受け取り、2つの値を入れ替える関数です。
実行結果が次のようになるように、swap 関数の中身を書いてください。
入れ替え前: first = 10, second = 20
入れ替え後: first = 20, second = 10
#include <stdio.h>
void swap(int *first_pointer, int *second_pointer)
{
/* ここに処理を書いてください */
}
int main(void)
{
int first = 10;
int second = 20;
printf("入れ替え前: first = %d, second = %d\n", first, second);
swap(&first, &second);
printf("入れ替え後: first = %d, second = %d\n", first, second);
return 0;
}
考える順番です。
-
first_pointerとsecond_pointerには、それぞれ何が入っているか -
*first_pointerと*second_pointerは、それぞれどの変数を表すか - 値を上書きする前に、どちらか一方の値を一時的に保存する必要がないか
答えを見る
#include <stdio.h>
void swap(int *first_pointer, int *second_pointer)
{
int temporary;
temporary = *first_pointer;
*first_pointer = *second_pointer;
*second_pointer = temporary;
}
int main(void)
{
int first = 10;
int second = 20;
printf("入れ替え前: first = %d, second = %d\n", first, second);
swap(&first, &second);
printf("入れ替え後: first = %d, second = %d\n", first, second);
return 0;
}
実行結果です。
入れ替え前: first = 10, second = 20
入れ替え後: first = 20, second = 10
swap(&first, &second); では、first と second の値ではなく、それぞれが置かれている場所を関数へ渡しています。
そのため、関数の中で *first_pointer と *second_pointer を書き換えると、main 関数にある first と second の値も変わります。
値を入れ替えるときは、片方の値をすぐに上書きすると元の値が失われます。
そのため、temporary へ一時的に保存してから入れ替えます。
temporary に first の値を保存する
↓
first に second の値を入れる
↓
second に temporary の値を入れる
まとめ
ポインタまわりは、次のように整理できます。
| 書き方 | 意味 |
|---|---|
number |
変数の値 |
&number |
number が置かれている場所。number を指すポインタとして使える値 |
ptr |
アドレスを保存するポインタ変数 |
*ptr |
ポインタ変数が指している先の変数。この例では number
|
ポインタを見たときは、次の3つを確認してください。
今扱っているのは値か。
今扱っているのはアドレスか。
今扱っているのはアドレスを保存するポインタ変数か。
この区別ができれば、ポインタの基本は押さえられています。
次の記事
次は、商品・社員・センサー情報のように、複数の項目を1件のデータとしてまとめて扱うために、構造体、typedef、enum、構造体の配列を学びます。
構造体を使えるようになると、現場でよくある「一覧データ」を扱いやすくなります。
main 関数の引数、argc、argv、void mainとの違いも確認したい場合は、補足記事を参照してください。









