C言語 関数の練習問題7問|引数・戻り値・スコープを固める
この記事は、C言語ドリルの5本目として関数を練習する記事です。処理に名前をつけて部品化する書き方を身につけると、長くなったプログラムが一気に読みやすくなります。
関数の定義・引数・戻り値からスコープ(変数の見える範囲)までを7問の練習問題(基礎5問+発展2問)で手を動かして固めるドリルです。解答と解説は各問題の「+」を押すと開きます。まず自分で書いてみて、それから答え合わせをしてください。実は、ここまで使ってきた printf も関数です(C言語に最初から用意されている標準ライブラリの関数)。今回は、それを自分で作る側に回ります。
<自己紹介>
筆者は現役の組込みエンジニアです。未経験向けのC言語練習問題を通じて最低限の言語知識を身につけ、最終的にはArduinoでマイコンプログラムを自作できるようになるのをゴールとして、このシリーズを作成しています。
目次
このテーマの要点
- 処理に名前をつけて部品化する(
void hello() { ... }) - 引数で値を渡し、戻り値で結果を返す
- 変数には見える範囲(スコープ)がある(ローカル / static / グローバル)
関数を覚えると、プログラムを「大きな1本の処理」から「小さな部品の組み合わせ」に変えられます。読みやすくなるだけでなく、同じ処理を使い回せる・テストしやすいという利点があり、実務のコードは例外なく関数の集まりでできています。
前提知識:関数の形・プロトタイプ宣言・スコープ
① 関数の形(定義・引数・戻り値)
関数を作るときに決めるのは「名前」「受け取る値(引数)」「返す値(戻り値)」の3つです。
int add(int a, int b) { // int=戻り値の型、a,b=引数
return a + b; // return=結果を返す
}
// 使うとき add(3, 4) → 7 が返ってくる
結果を返さない関数は、戻り値の型を void にします(void hello() { ... })。引数は「,」区切りでいくつでも並べられます。

② プロトタイプ宣言(先に「こんな関数がある」と教える)
C言語は上から順に読まれるので、mainより下に書いた関数を呼ぶと「そんな関数は知らない」と警告やエラーになることがあります。そこで、ファイルの先頭に「この形の関数が後で出てきます」と先に教えておくのがプロトタイプ宣言です。
#include <stdio.h>
int add(int a, int b); // プロトタイプ宣言(形だけ・末尾に ; )
int main(void) {
printf("%d\n", add(3, 4)); // 下にある関数を呼べる
return 0;
}
int add(int a, int b) { // 本体はmainの下でOK
return a + b;
}
形は簡単で、関数の1行目をコピーして末尾に ; を付けるだけです。関数をmainより上に書けば省略もできますが、関数が増えてくるとプロトタイプ宣言で整理するのが普通になります。
③ スコープ:変数の見える範囲と寿命
関数を覚えると、必ずセットで出てくるのがスコープ(変数の見える範囲)です。変数は「どこで宣言したか」で、見える範囲と値の寿命が決まります。
| 種類 | 宣言する場所 | 見える範囲 | 値の寿命 |
|---|---|---|---|
| ローカル変数 | 関数の中 | その関数の中だけ | 関数の実行中だけ(抜けると消える) |
| static変数 | 関数の中に static 付きで |
その関数の中だけ | プログラム終了まで(抜けても保持) |
| グローバル変数 | 関数の外 | どの関数からも | プログラム終了まで |
ポイントは「見える範囲」と「寿命」は別ものだということです。static変数は「見えるのは関数の中だけ、でも値は消えない」という中間の性質を持ちます。呼び出し回数を数えるカウンタなどに便利で、Q5で試します。
より正確な仕様は、cppreference(C言語リファレンス)で確認できます。
例題(まずは一緒に解いてみる)
練習問題に入る前に、簡単な例題を1つ、答えを見ながら流れを確認しましょう。
例題:2つの数を足して返す関数 add を作り、add(2, 3) の結果を表示してみましょう。
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
実行結果:5
解説:add は a と b を受け取り、a + b を return で返します。add(2, 3) と呼ぶと a=2, b=3 になり、5が返ってきます。値を渡して結果を受け取るのが関数です。
流れはつかめましたか? ここからは、自分の手で解いてみましょう。
練習問題(基礎5+発展2)
基礎5問は必ず解いてください。発展2問は余力があればで大丈夫です。まず自分で書いてから、各問題の「+ 解答・解説を見る」を開いて答え合わせをしましょう。
基礎 Q1
Hello と表示するだけの関数 hello(引数なし・戻り値なし)を作り、main から呼び出してください。
解答・解説を見る
#include <stdio.h>
void hello(void) {
printf("Hello\n");
}
int main(void) {
hello(); // 呼び出し
return 0;
}
実行結果:Hello
解説:戻り値がない関数は型を void にします。main の中で hello(); と書くと、その関数の中身が実行されます。関数は「定義(作る)」と「呼び出し(使う)」が別だ、という感覚をここでつかんでください。
基礎 Q2
2つの整数を受け取って足し算の結果を返す関数 add を作り、add(3, 4) の結果を表示してください。
解答・解説を見る
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
printf("%d\n", add(3, 4));
return 0;
}
実行結果:7
解説:int a, int b が引数(渡す値)、return a + b が戻り値(返す結果)です。add(3, 4) と呼ぶと、a に3、b に4が入り、7が返ってきます。この「値を渡して結果を受け取る」形が、関数の基本パターンです。
基礎 Q3
受け取った数を2乗して返す関数 square を、main より下に書いてください。プロトタイプ宣言を使って、square(5) の結果を表示します。
解答・解説を見る
#include <stdio.h>
int square(int x); // プロトタイプ宣言
int main(void) {
printf("%d\n", square(5));
return 0;
}
int square(int x) { // 本体はmainの下
return x * x;
}
実行結果:25
解説:プロトタイプ宣言 int square(int x); を先頭に書いたので、コンパイラは「後でこの形の関数が出てくる」と分かり、mainの下に本体があっても呼び出せます。試しにプロトタイプ宣言の行を消してコンパイルすると、警告やエラーが出ます。エラーを見てから戻すと、この1行の役割が体感できます。
基礎 Q4
グローバル変数 count を用意し、それを1増やす関数 increment を作ってください。main から3回呼び出して、最後の count を表示します(スコープの練習です)。
解答・解説を見る
#include <stdio.h>
int count = 0; // グローバル変数(関数の外)
void increment(void) {
count++; // どの関数からも見える
}
int main(void) {
increment();
increment();
increment();
printf("count=%d\n", count);
return 0;
}
実行結果:count=3
解説:count は関数の外で宣言したグローバル変数なので、increment からも main からも同じものを触れます。もし count を increment の中で宣言(ローカル変数に)すると、呼ぶたびに0から作り直されて、結果は1になります。関数をまたいで値を共有したいときはグローバル、という感覚をつかんでください。
基礎 Q5
呼ばれるたびに「何回目の呼び出しか」を表示する関数 counter を、グローバル変数を使わずに作ってください。ヒント:関数の中の static変数 は、関数を抜けても値が消えません。3回呼び出して確かめます。
解答・解説を見る
#include <stdio.h>
void counter(void) {
static int calls = 0; // static:初期化は最初の1回だけ
calls++;
printf("call %d\n", calls);
}
int main(void) {
counter();
counter();
counter();
return 0;
}
実行結果:call 1 / call 2 / call 3
解説:static を付けたローカル変数は、関数を抜けても値を保持します(初期化の = 0 は最初の1回しか実行されません)。Q4のグローバル変数と違い、calls は counter の中からしか見えないので、他の関数にうっかり書き換えられる心配がありません。「値は保ちたい、でも見せる範囲は狭くしたい」ときの定番です。
発展 Q6
配列 {60, 50, 40, 30, 20} の合計を返す関数 sumArray を作ってください。引数は「配列」と「要素数」の2つ。#4 配列との合わせ技です。
解答・解説を見る
#include <stdio.h>
int sumArray(int data[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += data[i];
}
return sum;
}
int main(void) {
int score[5] = {60, 50, 40, 30, 20};
printf("sum=%d\n", sumArray(score, 5));
return 0;
}
実行結果:sum=200
解説:配列を引数にするときは int data[] のように書き、要素数も一緒に渡すのが定石です(関数側では配列の大きさが分からないため)。こうして「合計を出す」処理を関数にしておけば、どんな配列にも使い回せます。ちなみに配列を渡すときの内部の仕組みはポインタと深い関係があり、次のテーマで正体が分かります。
発展 Q7
状態(モード)ごとに呼び出す関数を切り替える作りを試します。idle・run・stop の3つの関数を用意し、状態 0, 1, 2 の順にそれぞれを呼び出してください。(実務では、この「状態に応じて関数を呼び分ける」形をよく使います)
解答・解説を見る
#include <stdio.h>
void handleIdle(void) { printf("idle: waiting\n"); }
void handleRun(void) { printf("run: working\n"); }
void handleStop(void) { printf("stop: halted\n"); }
int main(void) {
int states[3] = {0, 1, 2}; // 0=idle, 1=run, 2=stop
for (int i = 0; i < 3; i++) {
switch (states[i]) {
case 0: handleIdle(); break;
case 1: handleRun(); break;
default: handleStop(); break;
}
}
return 0;
}
実行結果:idle: waiting / run: working / stop: halted
解説:処理を状態ごとの関数に分けておくと、main は「今どの状態か」を見て対応する関数を呼ぶだけになり、とてもすっきりします。これは実際の組込み開発でよく使う設計で、状態が増えても関数とcaseを足すだけで済みます。#2 の条件分岐と#4 の配列も、ここで一緒に効いています。
この発展問題は余力がある人向けです。基礎5問ができていれば、次のテーマ(ポインタ)に進んで大丈夫です。
例:実務で実際に書くコード
組込みのプログラムは、「初期化の関数を最初に1回」「制御の関数を繰り返し」という形に整理されます。ループの記事で見た骨組みを、関数で書き直すとこうなります。
int main(void) {
init_device(); // 初期化はこの1行に部品化
while (1) {
int value = read_sensor(); // 読む処理も
int result = judge(value); // 判断する処理も
write_output(result); // 出す処理も、全部関数
}
}
mainを読むだけで「初期化して、読んで、判断して、出す」という流れが分かります。細かい中身は各関数に隠して、mainは流れだけを見せる。これが関数で部品化する狙いです。
「1つの関数は1つの役割」。初学者のうちは、この意識で十分です。ただ、私が実務でやるときは、もう一段大きく「1つの機能=1つのモジュール(ファイル)」に分け、その中を関数に割っていくのが基本になります。そして
main は、状態遷移(今どのモードか)に応じて呼ぶ関数を切り替える作りにすることが多いです(発展Q7がその縮図です)。何度も使う処理はライブラリにまとめます。逆に、シフト演算のようなごく短い処理は、関数にせず関数マクロ(#define)で書くこともあります。これは#8 ビット演算で触れます。
最後につまずきポイントをおさらい
関数の中で宣言した変数(ローカル変数)は、その関数を抜けると消えます。別の関数からは見えませんし、呼ぶたびに作り直されます。「さっき増やしたはずの値が0に戻っている」ときは、それがローカル変数になっていないかを疑ってください。関数をまたいで保ちたい値は、グローバル変数か static変数にします。
もう一つは 引数の型と順番です。add(int a, int b) に対して add(3.5, 4) のように違う型を渡すと、意図しない結果になることがあります。関数を呼ぶときは、定義した型・順番のとおりに値を渡しましょう。
よくある質問(FAQ)
関数はどれくらい細かく分ければいい?
最初は「1つの関数で1つのこと」を目安にしてください。処理が長くなってきたり、同じコードを2回書きそうになったら、関数に切り出すサインです。慣れてきたら、機能ごとにファイルを分ける段階に進みます。
グローバル変数は使わない方がいいと聞いた
むやみに増やすと「どこで書き換わったか分からない」状態になるので、「使いすぎない」のは正しいです。ただし組込みでは、状態や設定値の共有に必要な場面では普通に使います。まず「関数の中だけで済む値はローカルに、保持だけならstaticに」を試して、それでも共有が必要な値だけグローバルにするバランスがおすすめです。
関数マクロと関数は何が違う?
関数マクロ(#define)は、コンパイル前に文字を置き換える仕組みで、ごく短い処理を関数呼び出しの手間なく書けます。組込みでは速度のために使うことがあります。詳しくは#8 ビット演算で扱います。
次に読む
ここまでで超基本の5テーマは完了です。次からは組込みで使用されることが多い内容になります。まずは多くの人がつまずくポインタへ。