C言語でプログラムを書くとき、関数の引数と戻り値は欠かせない要素です。
コードを分割して整理したり、再利用性を高めたりするためにも、引数・戻り値の仕組みを正しく理解しておくことが重要です。
本記事では、宣言と定義の基本から、配列や構造体、そしてmain関数とコマンドライン引数まで、C言語の引数と戻り値について丁寧に解説します。
C言語の引数・戻り値とは
引数・戻り値の基本概念

C言語では、関数はある入力を受け取り、処理を行い、必要に応じて結果を返します。
このとき、関数に渡す入力を引数、関数が呼び出し元に返す値を戻り値と呼びます。
文章で整理すると、次のようになります。
関数は
- 呼び出すときに値を渡すことができ、その値を引数(argument)と呼びます。
- 処理が終わったあとに1つの値を返すことができ、それを戻り値(return value)と呼びます。
例えば、2つの整数を足し算する関数を考えます。
#include <stdio.h>
// 2つのintを受け取り、その合計をintで返す関数
int add(int x, int y) {
int result = x + y; // xとyを足し合わせる
return result; // 計算結果を呼び出し元へ返す
}
int main(void) {
int a = 3;
int b = 5;
// aとbを引数として渡し、戻り値をsumに受け取る
int sum = add(a, b);
printf("合計は%dです。\n", sum);
return 0;
}
合計は8です。
この例では、add関数が2つの引数xとyを受け取り、戻り値としてresultを返しています。
仮引数と実引数の違い

C言語では、引数には2種類の呼び方があります。
仮引数と実引数です。
名称が似ているため混乱しやすいですが、役割は明確に異なります。
- 仮引数(formal parameter)は、関数を定義するときに関数側が受け取るために宣言している変数です。
- 実引数(actual argument)は、関数を呼び出すときに、呼び出し元から渡す具体的な値(または変数)です。
先ほどのadd関数で確認します。
// 関数定義側 (仮引数)
int add(int x, int y) { // x, y が仮引数
return x + y;
}
int main(void) {
int a = 10;
int b = 20;
// 関数呼び出し側 (実引数)
int sum = add(a, b); // a, b が実引数
return 0;
}
このコードでは、関数定義のadd(int x, int y)中のxとyが仮引数です。
一方、add(a, b)のaとbが実引数です。
ポイントは、呼び出し時に「実引数の値」が「仮引数の変数」にコピーされるということです。
この「コピーされる」という性質は、次に説明する値渡しとも関係しています。
値渡しと参照渡し(ポインタ)の違い

C言語の関数に引数を渡す方法は、本質的には1種類です。
それは値渡し(call by value)です。
つまり、実引数の値をコピーして仮引数へ渡します。
しかし、ポインタを使うことで参照渡しのような振る舞いを実現できます。
値渡しとは
値渡しでは、次の特徴があります。
- 関数呼び出し時に値がコピーされる
- 関数内で仮引数を変更しても、呼び出し元の変数は変化しない
#include <stdio.h>
// 値渡しの例
void increment(int x) {
x = x + 1; // 仮引数xを1増やす (呼び出し元には影響しない)
printf("関数内のx = %d\n", x);
}
int main(void) {
int a = 5;
increment(a); // aの値5がコピーされてxに渡る
printf("関数呼び出し後のa = %d\n", a); // aは変化していない
return 0;
}
関数内のx = 6
関数呼び出し後のa = 5
このように、関数の中だけで値が変わり、呼び出し元のaには影響がありません。
ポインタを使う「参照渡し風」の方法
C言語には厳密な意味での「参照渡し」はありませんが、ポインタを引数に渡すことで、呼び出し元の変数を直接操作することができます。
これを日常会話として参照渡しと呼ぶことも多いです。
#include <stdio.h>
// ポインタを使った「参照渡し風」の例
void increment_ptr(int *p) {
// *pは「ポインタpが指す先の値」を意味する
*p = *p + 1; // 呼び出し元の変数を1増やす
printf("関数内の *p = %d\n", *p);
}
int main(void) {
int a = 5;
// &aは「aのアドレス(ポインタ)」
increment_ptr(&a); // aのアドレスを引数として渡す
printf("関数呼び出し後のa = %d\n", a); // aが実際に変化している
return 0;
}
関数内の *p = 6
関数呼び出し後のa = 6
ここで行われていることは次の通りです。
- 実引数として
&a(aのアドレス)を渡す - 仮引数
int *pとして値(アドレス)がコピーされる(値渡し) - 関数内で
*pを通じてアドレス先の変数aを書き換える
つまり、「ポインタという値」を値渡ししているが、結果的に元の変数を操作できるため、参照渡しのように見えるというわけです。
C言語の関数宣言と定義
引数・戻り値を含む関数の宣言方法

C言語の関数は、宣言(prototype宣言)と定義の2つの段階で記述するのが一般的です。
- 宣言は「このような関数が存在します」とコンパイラに知らせるもの
- 定義は「関数の中身(処理)」を実際に書くもの
引数と戻り値を含む関数の宣言は、次の形式になります。
戻り値の型 関数名(引数の型1 引数名1, 引数の型2 引数名2, ...);
具体例を見てみます。
#include <stdio.h>
// 関数宣言(プロトタイプ宣言)
// int型を2つ受け取り、int型を返す
int add(int x, int y);
int main(void) {
int a = 2, b = 7;
int result = add(a, b); // 宣言があるのでここで呼び出せる
printf("result = %d\n", result);
return 0;
}
// 関数定義(本体)
int add(int x, int y) {
return x + y;
}
result = 9
関数宣言では、戻り値の型・関数名・引数の型(と数)を必ず一致させる必要があります。
引数名は省略可能ですが、省略しない方が読みやすくなります。
void関数と戻り値あり関数の違い

関数の戻り値の型としてvoidを指定すると、その関数は値を返さない関数になります。
これをvoid関数と呼びます。
一方、戻り値の型がintやdoubleなどの場合は、必ずその型の値をreturnで返す必要があります。
#include <stdio.h>
// 戻り値あり関数 (int型を返す)
int add(int x, int y) {
return x + y; // int型の値を返す
}
// void関数 (戻り値なし)
void print_sum(int x, int y) {
int s = x + y;
printf("合計は%dです。\n", s);
// return; と書くこともできるが省略可能 (値は返せない)
}
int main(void) {
int a = 3, b = 4;
int result = add(a, b); // 戻り値を受け取る
printf("addの戻り値 = %d\n", result);
print_sum(a, b); // 戻り値がないので、変数に代入はできない
return 0;
}
addの戻り値 = 7
合計は7です。
値を計算して後で使いたいときは「戻り値あり関数」、画面表示などの「処理だけ行えばよい」ときはvoid関数というように、役割に応じて使い分けます。
複数の引数を持つ関数の定義例

C言語の関数は、カンマ区切りで複数の引数を受け取ることができます。
最大の数は標準で明確に制限されてはいませんが、実務的には10個以上になるようなら構造体でまとめるなどを検討すべきです。
次の例では、2つの整数と演算子を受け取り、計算結果を返す関数を定義しています。
#include <stdio.h>
// 2つの整数と演算子を受け取り、計算結果を返す
int calc(int a, int b, char op) {
int result;
if (op == '+') {
result = a + b;
} else if (op == '-') {
result = a - b;
} else if (op == '*') {
result = a * b;
} else if (op == '/') {
if (b == 0) {
printf("エラー: 0で割ることはできません。\n");
return 0; // エラー時の仮の値
}
result = a / b;
} else {
printf("エラー: 対応していない演算子です。\n");
return 0;
}
return result;
}
int main(void) {
int x = 10, y = 5;
printf("x + y = %d\n", calc(x, y, '+'));
printf("x - y = %d\n", calc(x, y, '-'));
printf("x * y = %d\n", calc(x, y, '*'));
printf("x / y = %d\n", calc(x, y, '/'));
return 0;
}
x + y = 15
x - y = 5
x * y = 50
x / y = 2
ここでは、int a、int b、char opの3つが引数で、それぞれ独立した値を受け取っています。
戻り値の型とreturn文の役割

関数の戻り値は、関数名の前の型によって定義されます。
そして、実際に値を返す処理はreturn文で行います。
重要なルールをまとめると次の通りです。
- 戻り値の型と
returnで返す式の型は互換性が必要 - 戻り値あり関数では、関数の全ての経路で
returnが実行されるように書く - returnが実行された時点で、その関数の処理は終了する
#include <stdio.h>
// 2つの整数のうち大きい方を返す関数
int max(int a, int b) {
if (a > b) {
return a; // aのほうが大きい
} else {
return b; // bのほうが大きいか等しい
}
// ここに到達することはない
}
int main(void) {
int x = 10, y = 20;
int m = max(x, y); // 戻り値をmに保存
printf("大きい方の値は%dです。\n", m);
return 0;
}
大きい方の値は20です。
戻り値の型とreturnに渡す式の型が一致しない場合、コンパイラから警告やエラーが出ることがあります。
例えば、戻り値の型がintなのにdoubleの値を返すと、暗黙の型変換が行われますが、意図しない丸めや精度の損失を招くことがあるので注意が必要です。
関数呼び出しと引数の渡し方
関数呼び出しの基本構文

関数の呼び出しは、次の形式で行います。
戻り値の受け取り変数 = 関数名(実引数1, 実引数2, ...);
戻り値を使わない場合は、左側を省略して関数名(...);とだけ書きます。
例として、単純な掛け算関数を呼び出すコードを示します。
#include <stdio.h>
// 2つの整数を掛け算して結果を返す
int multiply(int a, int b) {
return a * b;
}
int main(void) {
int x = 4, y = 5;
// 戻り値を受け取る呼び出し
int result = multiply(x, y);
printf("result = %d\n", result);
// 戻り値を直接printfに渡すこともできる
printf("multiply(2, 3) = %d\n", multiply(2, 3));
return 0;
}
result = 20
multiply(2, 3) = 6
このように、関数名のあとに丸括弧で実引数を並べるのが基本形です。
実引数の評価タイミングと順序

C言語では、実引数の評価順序は未定義であるという性質があります。
つまり、
func(f1(), f2());
という呼び出しで、f1()とf2()のどちらが先に実行されるかは、規格上決まっていません。
コンパイラによって順序が変わる可能性があります。
さらに重要なのは、副作用(変数の書き換えなど)の順序に依存するコードを書かないことです。
例えば次のようなコードは書いてはいけない例です。
#include <stdio.h>
int f1(int *p) {
(*p)++;
return *p;
}
int f2(int *p) {
(*p) *= 2;
return *p;
}
// これは好ましくない例
int bad_call(int *p) {
// f1とf2のどちらが先に評価されるか不定
return f1(p) + f2(p);
}
このようなコードでは、結果がコンパイラや最適化の状況により変わる可能性があります。
したがって、引数の評価順序が問題になるような書き方は避け、次のように明示的に分けるのが安全です。
int good_call(int *p) {
int a = f1(p); // ここで評価順序が明確になる
int b = f2(p);
return a + b;
}
このように書けば、常にf1が先に実行されることが保証されます。
配列・ポインタを引数に渡すときの注意点

C言語で配列を関数に渡すときは、配列全体がコピーされるわけではありません。
配列名は、関数呼び出し時に自動的に配列の先頭要素へのポインタに変換されるという仕様があるため、実際にはポインタが値渡しされていると考えるのが正確です。
次の例で確認します。
#include <stdio.h>
// 配列の要素をすべて2倍にする関数
void double_array(int *arr, int size) {
int i;
for (i = 0; i < size; i++) {
arr[i] = arr[i] * 2; // 呼び出し元の配列の要素が直接変更される
}
}
// 配列を表示する関数
void print_array(int *arr, int size) {
int i;
for (i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(void) {
int data[5] = {1, 2, 3, 4, 5};
printf("変更前: ");
print_array(data, 5);
// 配列名dataは、int *型(先頭要素へのポインタ)として渡される
double_array(data, 5);
printf("変更後: ");
print_array(data, 5);
return 0;
}
変更前: 1 2 3 4 5
変更後: 2 4 6 8 10
この例から分かるように、関数内で配列の要素を変更すると、呼び出し元の配列も変更されます。
これは、先ほど説明した「ポインタを使った参照渡し風」のケースと同じ構造です。
配列を引数にする際の注意点をまとめると次の通りです。
| 項目 | 内容 |
|---|---|
| 実際に渡されるもの | 配列の先頭要素へのポインタ |
| 関数側の仮引数の典型的な書き方 | int *arr または int arr[] |
| 配列のサイズ情報 | 自動では渡らないため、別の引数(例:size)として渡す必要がある |
| 関数内の変更の影響 | 配列要素の変更は呼び出し元にも反映される |
配列を保護したい場合は、C99以降であればconst int *arrのようにconstを付けることで、関数内から配列要素を書き換えられないようにすることもできます。
構造体を引数・戻り値に使う場合

C言語では、構造体(struct)も引数や戻り値として使うことができます。
ただし、構造体は複数のメンバからなる複合データなので、いくつかの注意点があります。
構造体を値渡しする場合
構造体をそのまま引数にすると、構造体全体がコピーされます。
そのため、関数内で構造体のメンバを変更しても、呼び出し元の構造体には影響しません。
#include <stdio.h>
struct Point {
int x;
int y;
};
// 構造体を値渡しする関数
void move_right(struct Point p) {
p.x = p.x + 10; // 引数pのコピーを書き換えているだけ
printf("関数内: x = %d, y = %d\n", p.x, p.y);
}
int main(void) {
struct Point pt = { 1, 2 };
move_right(pt); // ptのコピーが渡される
// ptは変化していない
printf("main関数: x = %d, y = %d\n", pt.x, pt.y);
return 0;
}
関数内: x = 11, y = 2
main関数: x = 1, y = 2
このように、構造体を値渡しすると、安全だがコピーコストがかかるという特徴があります。
大きな構造体を頻繁に渡す場合は、パフォーマンスに影響する可能性があります。
構造体のポインタを渡す場合
構造体のアドレス(ポインタ)を渡せば、呼び出し元の構造体を直接書き換えることができますし、コピーのコストも抑えられます。
#include <stdio.h>
struct Point {
int x;
int y;
};
// 構造体のポインタを渡す関数
void move_right_ptr(struct Point *p) {
// p->x は (*p).x の省略形
p->x = p->x + 10;
printf("関数内: x = %d, y = %d\n", p->x, p->y);
}
int main(void) {
struct Point pt = { 1, 2 };
move_right_ptr(&pt); // ptのアドレスを渡す
// pt自身が変更されている
printf("main関数: x = %d, y = %d\n", pt.x, pt.y);
return 0;
}
関数内: x = 11, y = 2
main関数: x = 11, y = 2
構造体を戻り値として返す
構造体は戻り値として返すこともできます。
C99以降では、最適化により効率的に扱われることが多くなりました。
#include <stdio.h>
struct Point {
int x;
int y;
};
// 2つのPointの和を返す
struct Point add_point(struct Point a, struct Point b) {
struct Point result;
result.x = a.x + b.x;
result.y = a.y + b.y;
return result; // 構造体を丸ごと返す
}
int main(void) {
struct Point p1 = {1, 2};
struct Point p2 = {3, 4};
struct Point sum = add_point(p1, p2);
printf("sum: x = %d, y = %d\n", sum.x, sum.y);
return 0;
}
sum: x = 4, y = 6
このように、複数の値を一度にまとめて渡したい・返したいときに構造体は非常に便利です。
main関数と戻り値の扱い
main関数の役割と書き方

C言語プログラムでは、main関数が実行の開始点です。
プログラムが起動すると、必ず最初にmain関数が呼び出されます。
標準的な書き方は次の2種類です。
int main(void) {
// 引数なし
return 0;
}
// または
int main(int argc, char *argv[]) {
// コマンドライン引数あり
return 0;
}
ほとんどの環境では戻り値の型はintでなければならないと考えてください。
古い書き方ではvoid main(void)のような形も見かけますが、これは標準Cでは未定義であり、推奨されません。
main関数の戻り値は、プログラムの終了ステータスとしてOSに返されます。
これについては後ほど詳しく説明します。
main関数の引数(argc・argv)とは

コマンドラインからプログラムを起動する場合、ユーザが指定した引数をプログラム側で受け取ることができます。
このとき使用するのがmain関数の引数argcとargvです。
- argc: コマンドライン引数の個数を表す
int型の値 - argv: 各引数の文字列へのポインタの配列(
char *argv[])
一般的には、次のように宣言します。
int main(int argc, char *argv[]) {
// ...
}
ここで、argv[0]には通常実行ファイルの名前が入り、argv[1]からargv[argc - 1]までがユーザが指定した引数です。
簡単な例で見てみましょう。
#include <stdio.h>
int main(int argc, char *argv[]) {
int i;
printf("引数の個数: %d\n", argc);
for (i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
このプログラムを次のように実行したとします。
./a.out hello 123 world
そのときの出力例は次のようになります。
引数の個数: 4
argv[0] = ./a.out
argv[1] = hello
argv[2] = 123
argv[3] = world
このように、コマンドラインから渡された情報を、文字列として受け取ることができます。
コマンドライン引数の使い方

コマンドライン引数は文字列として渡されるため、数値として使いたい場合は変換が必要です。
atoiやstrtolなどの関数を使います。
次のプログラムは、コマンドラインから2つの整数を受け取り、その和を計算して表示する例です。
#include <stdio.h>
#include <stdlib.h> // atoiを使うために必要
int main(int argc, char *argv[]) {
if (argc != 3) {
// 引数の個数が期待通りでない場合
printf("使い方: %s number1 number2\n", argv[0]);
return 1; // エラーとして1を返す
}
// argv[1], argv[2]は文字列なので、整数に変換する
int a = atoi(argv[1]);
int b = atoi(argv[2]);
int sum = a + b;
printf("%d + %d = %d\n", a, b, sum);
return 0; // 正常終了
}
$ ./a.out 10 20
10 + 20 = 30
$ ./a.out 10
使い方: ./a.out number1 number2
このように、argcで引数の個数をチェックし、argvで内容を受け取り、必要に応じて型変換を行うのが基本の流れです。
main関数の戻り値と終了ステータス

先ほど触れたように、main関数の戻り値はプログラムの終了ステータスとしてOSに渡されます。
これは、別のプログラムやシェルスクリプトからそのプログラムが正常に終了したかどうかを判定するために使われます。
一般的な慣習としては、
- 0を返す → 正常終了
- 0以外を返す → 何らかのエラーが発生
と解釈されます。
次の例では、引数が足りないときにエラーコード1を返し、成功時には0を返しています。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "エラー: 引数が1つ必要です。\n");
return 1; // エラーコード1
}
int value = atoi(argv[1]);
printf("入力値は%dです。\n", value);
return 0; // 正常終了
}
このプログラムをシェルから実行し、終了ステータスを確認してみます。
(Unix系環境の例)
$ ./a.out 10
入力値は10です。
$ echo $?
0
$ ./a.out
エラー: 引数が1つ必要です。
$ echo $?
1
main関数の戻り値を意識して設計しておくと、スクリプトや他のプログラムから利用するときに非常に便利です。
また、標準ライブラリにはEXIT_SUCCESSやEXIT_FAILUREといったマクロも用意されており、#include <stdlib.h>をすることで次のように書くこともできます。
#include <stdlib.h>
int main(void) {
// 何らかの処理
if (/* エラーがあった場合 */) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
この方が、コードの意図がより明確になるというメリットがあります。
まとめ
C言語における引数と戻り値は、関数を用いてプログラムを構造化するうえでの基礎となる概念です。
仮引数と実引数、値渡しとポインタを使った参照渡し風の違いを理解すると、配列や構造体といった複雑なデータも自在に扱えるようになります。
また、main関数の引数argc・argvを活用すれば、柔軟なコマンドラインツールを作ることができ、戻り値を終了ステータスとして正しく扱うことで、他プログラムとの連携性も高まります。
本記事で紹介したポイントを丁寧に押さえながら、実際のコードで手を動かして確認していくことが上達への近道です。
