C言語 ポインタの練習問題7問|組込みで避けて通れない理由も解説
この記事は、C言語ドリルの6本目としてポインタを練習する記事です。入門書でいちばん挫折されやすいテーマですが、組込みでは避けて通れません。だからこの記事では、「なぜ組込みではポインタが必須なのか」を先に説明してから、7問の練習問題(基礎5問+発展2問)で & と * を固めます。解答と解説は各問題の「+」を押すと開きます。
<自己紹介>
筆者は現役の組込みエンジニアです。未経験向けのC言語練習問題を通じて最低限の言語知識を身につけ、最終的にはArduinoでマイコンプログラムを自作できるようになるのをゴールとして、このシリーズを作成しています。私自身、C言語で最初に大きくつまずいたのがこのポインタでした。
目次
このテーマの要点
- ポインタは「値」ではなく変数の住所(アドレス)を入れる変数
&で住所を取り出し、*でその住所の中身にアクセスする- 組込みではレジスタ操作と関数へのデータ渡しで使う
覚える記号は & と * の2つだけです。難しく見えるのは記号の意味が最初は頭に入りにくいからで、やること自体は「住所を調べる」「住所を頼りに中身を読み書きする」の2つしかありません。
なぜ組込みではポインタが必須なのか
先に「何に使うのか」から話します。ここが分かると、ポインタを学ぶ理由がはっきりするからです。
理由は大きく2つあります。1つ目はレジスタ操作。マイコンの中では、LEDをつなぐピンや通信の設定が「レジスタ」と呼ばれる番地の決まったメモリに割り当てられています。つまりマイコンを直接動かすことは、「決まった住所のメモリを読み書きすること」とほぼ同じ。この「住所を指定して中身を触る」操作こそが、ポインタの考え方そのものです。
2つ目は関数へのデータ渡しです。#5 関数でやったとおり、引数は値のコピーが渡されるので、関数の中から呼び出し元の変数は変えられませんでした。ポインタで「住所」を渡せば、関数の中から呼び出し元のデータを直接読み書きできます。配列やセンサー値の格納先を関数に渡す場面で、実務では毎日のように使います。
なので、この記事の7問で完璧に理解できなくても大丈夫です。& と * の動きを手で確かめておいて、実際にマイコンを触るときに戻ってくる。その使い方で十分かと思います。
前提知識:アドレス・&・*
① メモリとアドレス(すべての変数には住所がある)
プログラムの変数は、すべてメモリのどこかに置かれています。そしてメモリには1バイトごとに通し番号(アドレス=住所)が振られています。int x = 10; と書くと、「どこかの住所に、10の入った箱が置かれる」わけです。
ポインタは、この住所を入れるための変数です。値そのものではなく「どこにあるか」を持ちます。

② 宣言・&・*(3点セット)
int x = 10;
int *p; // int型の住所を入れるポインタ変数(型 + * + 変数名)
p = &x; // &x = 「xの住所」。それをpに入れる
printf("%d\n", *p); // *p = 「pが指す住所の中身」→ 10
*p = 20; // pが指す住所の中身を書き換え → xが20になる
読み方はこうです。& は「〜の住所」、* は「その住所の中身」。この2つの変換が行き来できれば、ポインタの基礎は終わりです。
アドレスは
printf("%p", (void *)&x); で表示できます。000000c65d7ff834 のような16進数が出て、実行するたびに値が変わります(OSが置き場所を毎回決めるため)。この表示は学習とデバッグ用で、製品のコードに残すことはまずありません。
③ ポインタと配列(配列名は先頭の住所)
int data[3] = {1, 2, 3};
int *p = data; // 配列名は「先頭要素の住所」。&data[0] と同じ
printf("%d\n", *p); // 先頭の中身 → 1
p[1] = 20; // data[1] を書き換えられる
配列の各要素は、#4 配列で見たとおりメモリに一直線に並んでいます。だから「先頭の住所」さえ分かれば、そこから順にたどれます。関数に配列を渡すときは、この「先頭の住所」を渡すのが定番です(発展Q6でやります)。
より正確な仕様は、cppreference(C言語リファレンス)で確認できます。
例題(まずは一緒に解いてみる)
練習問題に入る前に、簡単な例題を1つ、答えを見ながら流れを確認しましょう。
例題:変数 x = 10 の住所をポインタ p に入れ、①xの住所 ②pの中身 ③*p の3つを表示してみましょう。
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x;
printf("%p\n", (void *)&x); // ① xの住所
printf("%p\n", (void *)p); // ② pの中身
printf("%d\n", *p); // ③ pが指す先の中身
return 0;
}
実行結果(例):①と②に同じアドレス(000000c65d7ff834 のような16進数)、③に 10
解説:p = &x とした瞬間から、pの中身は「xの住所」です。だから①と②は必ず同じ値になります。そして *p は「その住所の中身」なので、xの値である10が出ます。①②が一致するのを自分の目で確かめるのが、この例題のいちばんの目的です。
流れはつかめましたか? ここからは、自分の手で解いてみましょう。
練習問題(基礎5+発展2)
基礎5問は必ず解いてください。発展2問は余力があればで大丈夫です。まず自分で書いてから、各問題の「+ 解答・解説を見る」を開いて答え合わせをしましょう。
基礎 Q1
整数の変数 age = 20 を宣言し、①ageの値と、②ageの住所(アドレス)を表示してください。住所の表示には %p を使います。
解答・解説を見る
#include <stdio.h>
int main(void) {
int age = 20;
printf("%d\n", age); // 値
printf("%p\n", (void *)&age); // 住所
return 0;
}
実行結果(例):20 と 0000000d261ffa9c(アドレスは実行のたびに変わります)
解説:変数名に & を付けるだけで、その変数の住所が取り出せます。「どんな変数にも住所がある」ことを確かめる問題です。%p で表示するときは (void *) を付けるのがCの正式な作法です(付けなくても動くことが多いですが、警告の原因になります)。
基礎 Q2
変数 x = 55 の住所をポインタ p に入れ、pを経由してxの値を表示してください(x を直接printfに渡すのは禁止です)。
解答・解説を見る
#include <stdio.h>
int main(void) {
int x = 55;
int *p = &x;
printf("%d\n", *p); // pが指す先の中身 = 55
return 0;
}
実行結果:55
解説:*p と書くと「pに入っている住所へ行って、中身を読む」動きになります。p(住所そのもの)と *p(住所の中身)の区別が、ポインタの一番の基本です。迷ったら上の図に戻ってください。
基礎 Q3
変数 x = 0 をポインタ p 経由で 10に書き換えて、xの値を表示してください(x = 10; と直接書くのは禁止です)。
解答・解説を見る
#include <stdio.h>
int main(void) {
int x = 0;
int *p = &x;
*p = 10; // pが指す先(=x)に10を書き込む
printf("x=%d\n", x);
return 0;
}
実行結果:x=10
解説:*p は読むだけでなく、代入の左側に置けば書き込みになります。「xに一度も触っていないのに、xの値が変わる」。これがポインタの力で、この動きをそのまま使うのが後の「関数の中から呼び出し元の変数を変える」(Q5)です。
基礎 Q4
配列 data = {1, 2, 3} の先頭の住所をポインタ p に入れ、①先頭の値を *p で表示、②p 経由で2番目の要素を20に書き換えて表示してください。
解答・解説を見る
#include <stdio.h>
int main(void) {
int data[3] = {1, 2, 3};
int *p = data; // 配列名 = 先頭要素の住所
printf("%d\n", *p); // ① 先頭の中身 → 1
p[1] = 20; // ② data[1] を書き換え
printf("%d\n", data[1]); // → 20
return 0;
}
実行結果:1 と 20
解説:配列名 data をそのまま代入すると、&data[0](先頭の住所)と同じ意味になります。& が要らないのはこのためです。住所を受け取ったポインタは、p[1] のように配列と同じ書き方で各要素を読み書きできます。
基礎 Q5
エラー回数 errCount = 5 を、関数 setZero の中から0にリセットして表示してください。関数の形は void setZero(int *n) とします。
解答・解説を見る
#include <stdio.h>
void setZero(int *n) {
*n = 0; // 受け取った住所の中身を0にする
}
int main(void) {
int errCount = 5;
setZero(&errCount); // 「errCountの住所」を渡す
printf("errCount=%d\n", errCount);
return 0;
}
実行結果:errCount=0
解説:#5 関数では「引数はコピーなので、関数の中から呼び出し元は変えられない」でした。住所を渡せばこの壁を越えられます。関数側はポインタで受け取り、*n = 0 で「その住所の中身」を書き換える。呼び出し側は &errCount と住所を渡す。この形は実務のコードで何度も出てきます。
発展 Q6
配列 {10, 20, 30, 40, 50} の合計を返す関数 sumArray を作ってください。形は int sumArray(const int *arr, int n)(先頭の住所と要素数を受け取る)とします。
解答・解説を見る
#include <stdio.h>
int sumArray(const int *arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i]; // ポインタでも配列と同じ書き方でOK
}
return sum;
}
int main(void) {
int data[5] = {10, 20, 30, 40, 50};
printf("sum=%d\n", sumArray(data, 5));
return 0;
}
実行結果:sum=150
解説:「配列を関数に渡す」の正体は、先頭の住所と要素数を渡すことです。配列そのものは渡らないので、要素数 n を別の引数で教えるのがセットになります。const は「この関数は中身を書き換えません」という宣言で、読み取り専用の引数に付けておくと安全です。
発展 Q7(読解問題)
次の1行は、マイコンの入門コードによく出てくる形です。何をしているのかを、①キャストしている型 ②* の役割 ③この行の意味、の3点で説明してください(動かす問題ではありません)。
*(volatile unsigned char *)0x25 = 0x01;
解答・解説を見る
解答:
① (volatile unsigned char *) は「数値 0x25 を、unsigned char型が置いてある住所として扱え」というキャストです。
② 先頭の * は「その住所の中身にアクセスする」=Q3でやった書き込みの * と同じです。
③ 全体で「アドレス0x25番地のメモリに 0x01 を書き込む」という意味になります。
解説:マイコンでは、0x25のような決まった番地にレジスタ(ピンの設定など)が割り当てられています。だから「番地を直接指定して書き込む」この形が、レジスタ操作の基本形。volatile は「この場所の値はプログラムの外(ハードウェア)で変わりうるので、コンパイラの最適化で読み書きを省略しないで」と伝えるキーワードです。番地はマイコンの機種ごとに違うので、この行はそのまま動かさず、形を読めるようになれば十分です。実際のマイコンでは、この番地に名前を付けたマクロ(PORTB など)を使って書きます。
この発展問題は余力がある人向けです。基礎5問ができていれば、次のテーマ(構造体)に進んで大丈夫です。
例:実務で実際に書くコード
私の実務でポインタの出番がいちばん多いのは、関数にデータの格納先を渡す形です。センサー値の取得関数は、だいたいこの形をしています。
int result;
if (read_sensor(&result) == OK) { // 格納先の住所を渡す
// resultに取得値が入っている
}
戻り値は「成功/失敗」に使い、取得した値そのものはポインタ経由で受け取る。Q5でやった「関数の中から呼び出し元の変数に書き込む」がそのまま実務の形です。
最後につまずきポイントをおさらい
宣言のときの
int *p の * は「これはポインタ型です」という目印。それ以外の場所の *p は「pが指す住所の中身」。同じ記号で意味が違うのが混乱のもとなので、「宣言のときだけ型の一部」と覚えてください。
もう一つ気を付けたいのが住所を入れる前のポインタを使うことです。int *p; と宣言しただけのpはどこを指しているか分からず、その状態で *p = 10; と書き込むと、どこか知らないメモリを壊します(PCなら大抵クラッシュ、マイコンでは気づきにくい厄介なバグになります)。ポインタは「&で住所を入れてから使う」を徹底してください。
よくある質問(FAQ)
ポインタが理解できず挫折しそうです
全部を理解してから進む必要はありません。& と * の行き来(Q1〜Q3)ができれば、このシリーズを進める分には足ります。私も本だけでは分からず、マイコンでレジスタを触ったときに初めて腑に落ちました。使う場面に出会うと理解が追いつくタイプの知識なので、いったん先へ進むのも手です。
int* p と int *p、どちらで書く?
どちらも同じ意味なので、プロジェクトのコーディングルールに合わせれば大丈夫です。int* p, q; と書くとqがポインタにならない(pだけ)という罠があるため、私は int *p の書き方を使っています。
ポインタと配列は同じもの?
別物です。配列名が「先頭の住所」として振る舞う場面が多いので同じに見えますが、配列は「箱の並びそのもの」、ポインタは「住所を入れる1個の箱」です。まずは「関数に渡すときは先頭の住所が渡る」(Q6)だけ押さえれば十分です。
次に読む
ポインタの山を越えたら、残りは2テーマです。次は複数のデータを1つにまとめる「構造体」へ進みましょう。構造体をポインタで扱う書き方(->)でも、今回の知識をそのまま使います。