Lesson 5

関数の基本

Lesson 5 Chapter 1
関数とは

これまでに学習してきたC言語の構文を使うことで、基本的なプログラムを書くことができるようになりました。Lesson5では、関数について学習します。関数を作成できるようになることで、より高度なプログラムを作成することが可能になります。

関数とは

関数のイメージをつかんでもらうために、電車の自動券売機のプログラムを考えます。電車の自動券売機を作るためには、どのような「機能」が必要でしょうか。

機能の例
1. お金が入金され、入金額をディスプレイに表示する
2. 購入できる切符のボタンを光らせる、またはボタンを表示する
3. 切符の金額より高い金額が入金された場合、おつりを返却する
4. 購入された切符を出す
5. 取消ボタンが押された場合、処理を中止して、お金を返却する

これはほんの一例であり、実際の券売機にはたくさんの「機能」があります。
上記のように、いくつかの処理を1つの「機能」としてまとめたものが「関数」の概念になります。複雑な処理を行う関数から、小さい処理を行う関数まで、プログラムによって関数のコード量は変わります。プログラミングにおいて、自分で機能を分け、関数が書けることは大切なスキルです。結果、他の人が見ても可読性の高い、修正しやすいコードを書くことができます。

関数の宣言

関数は次のような構文で宣言します。

関数の宣言
戻り値の型 関数名(引数) {
  宣言または文;
  …
  return 値;
}

「値(戻り値)」と「引数」の2つについては、現時点では気にしないで構いません。後のChapterで解説します。
「関数」を作成するときに「関数名」をつけます。その際の命名規則は、変数名を付けるときと同様になります。
・半角英数字を用いる
・小文字アルファベットから始める
・「予約語」と名前が重複しないようにする

予約語とは

C言語の構文や命令に使用するワードのことを「予約語」と呼びます。あらかじめ、C言語の仕様で使用が禁止されており「予約語」を使ってプログラムを書いた場合、コンパイル時にエラーになります。「予約語」を一つ一つ覚える必要はありません。C言語の構文や命令で使用するワードは、変数名や関数名に使用できないと覚えましょう。

C言語の予約語一覧
char short int long
float double void auto
static const signed unsigned
extern volatile register return
goto if else switch
case default break for
while do continue typedef
struct enum union _Bool
_Complex _Imaginary inline restrict
_Alignas _Atomic _Generic _Noreturn
_Static_assert _Thread_local alignof

※C89~C11までの予約語を記載

main関数

C言語で書かれたプログラムは最初に「main関数」から実行します。Lesson1で最初に紹介しましたが、main関数は以下のような構成です。

sampleMain.c
#include <stdio.h>
int main(void) {

  // この部分にプログラムに行って欲しい処理を順番に書いていく

  return 0;
}

作成した関数を実行したいときは、main関数を宣言して、その中で関数を呼び出す必要があります。関数を呼び出すコードの例を確認してみましょう。

function1.c
#include <stdio.h>

// 作成した関数の宣言
void dispPuts(void) {
  puts("関数が実行されました");
  return;
}

// main関数の宣言
int main(void) {
  
  puts("プログラム開始");
  
  dispPuts();
  
  puts("プログラム終了");

  return 0;
}
実行結果
function1.exe
プログラム開始
関数が実行されました
プログラム終了

今までのプログラムと異なり、main関数の前に、作成した関数のコードが宣言されています。先ほどもいいましたが、C言語のプログラムの処理の開始はmain関数からになります。なので、main関数の中に「dispPuts();」と記述して、関数を呼び出します。関数を呼び出す場合は、以下のように記述します。引数がない場合は「関数名()」と記述して、引数の部分を省略します。

関数の呼び出し
関数名(引数);

処理の流れを整理すると、プログラムは以下の順で処理を実行します。
1. main関数を実行する
2. 関数「dispPuts()」を呼び出して実行する
3. dispPuts関数の処理を実行する
3. dispPuts関数の処理が終了してmain関数に戻る
4. プログラムを終了する
関数が呼び出されたときの処理の流れは重要ですので、しっかり覚えましょう。

値渡し

関数の構文の「引数」(ひきすう)について説明します。もう一度、関数の構文を確認しましょう。

関数の宣言
戻り値の型 関数名 (引数) {
  文;
  …
  return 値;
}

()内に「引数」と記述されています。関数に「引数」を記述して、呼び出し元の関数から別の関数に値をコピーして渡す方式を「値渡し」と呼びます。今まで登場した関数は「void」としか記述していませんでしたが「int a」のように変数を宣言することで、「引数による値渡し」が可能になります。プログラムを確認した方が理解しやすいので、実際にコードを書いてみましょう。

function2.c
#include <stdio.h>

// add関数
void add(int a) {
  
  printf("値渡しで渡された引数の値: %d\n", a);
  
  a = a + 1;
  
  printf("引数に1を加算した値: %d\n", a);
  
  return;
}

// main関数
int main(void) {
  
  puts("プログラム開始");
  
  add(3);
  
  puts("プログラム終了");

  return 0;
}
実行結果
function2.exe
プログラム開始
値渡しで渡された引数の値: 3
引数に1を加算した値: 4
プログラム終了

add関数には、引数として(int a)が宣言されています。main関数の中で「add(3)」と記述して引数に3を渡しています。呼び出す側が渡す引数のことを「実引数」と呼びます。これは「add関数を呼び出して、実引数として3を渡す」ということを示しています。逆に、add関数の中で、宣言している変数aを「仮引数」と呼びます。これは「main関数から受け取った値を仮引数から取得する」ということを示しています。結果を確認すると、実際に渡された値に1を加算した値4が表示されていることが分かります。

void関数

「function1.c」で登場したdispPuts関数をもう一度確認してみます。

void関数
void dispPuts(void) {
  puts("関数が実行されました");
  return;
}

引数という視点からみると
・引数を指定する括弧()の中に「void」と記述されている
・関数の処理の中では、引数のような値は存在しない
上記コードの引数の中に書いてある「void」は少し特殊なもので、「引数を使わない」という意味を示しています。この関数は処理の中で引数を使わないので、呼び出す際には、以下のように書きます。

void関数の呼び出し
dispPuts();

関数名だけ指定し、括弧()の中は空で呼び出します。

関数の汎用性

printf関数は、文字を画面出力する関数で、C言語の学習の中で、何度も登場する汎用性の高い標準ライブラリ関数です。では、自分で作成した関数にも汎用性を持たせるには、どうすればいいでしょうか。第一歩として、同じ処理を実行している部分を関数にすることを考えてみます。実際にソースの例を確認してみましょう。

function3.c
#include <stdio.h>
int main(void) {

  int total = 0;
  int stock1[] = {12, 25, 70, 56, 6};

  for (int i = 0; i < 5; i++) {
    total += stock1[i];
  }

  int stock2[] = {7, 89, 33, 24, 18};

  for (int i = 0; i < 5; i++) {
    total += stock2[i];
  }

  printf("total: %d", total);

  return 0;
}
実行結果
function3.exe
total: 340

for文を使って配列の値の合計値を表示するプログラムです。2つの配列があり、それぞれの合計値をfor文で計算します。for文の部分に注目してください。変数が異なるだけで実行している処理が同じだということが分かります。

function3.c
for (int i = 0; i < 5; i++) {
  total += stock1[i];
}

for (int i = 0; i < 5; i++) {
  total += stock2[i];
}

この部分を関数にできないか考えてみましょう。配列の計算結果を返却する関数を書いてみます。

function3.c
int sum(int stock[]) {
    int total = 0;
    for (int i = 0; i < 5; i++) total += stock[i];
    return total;
}

「戻り値の型」がintなので「return total;」とし、配列の計算結果を返却するsum関数が作成できました。引数は「int stock[]」と記述して、配列の値渡しを行います。実際にプログラムを書いて実行してみましょう。

function3.c
#include <stdio.h>

// sum関数
int sum(int stock[]) {
    int total = 0;
    for (int i = 0; i < 5; i++) total += stock[i];
    return total;
}

// main関数
int main(void) {

    int total = 0;
    int stock1[] = {12, 25, 70, 56, 6};
    int stock2[] = { 7, 89, 33, 24, 18 };

    total += sum(stock1);
    total += sum(stock2);

    printf("total: %d", total);
    
    return 0;
}
実行結果
function3.exe
total: 340

関数を使う前のコードと比較すると冗長だった部分がなくなり、コードが簡潔になりました。main関数でsum関数を呼び出すときは、sum(stock1)、sum(stock2)と書いて配列の変数を渡します。sum関数が引数から配列の値を受け取り、計算結果を「return total」で返却します。再びmain関数の処理に戻り、sum関数の計算結果をtotal変数に+=演算子を使って合計を計算します。
C言語に限らず、関数の概念はほかの言語でも共通する考え方です。コーディングに慣れるまでは、無理に関数を作成しなくても大丈夫です。たくさんのコードに触れることで「ここは関数を作成して処理を共通化しよう」、「コードが長すぎるから関数を作成して処理を分けよう」という考え方が身につきます。かといって、関数を増やせばいいというものではありません。関数を増やしすぎてしまい、可読性が落ちてしまうこともあります。
・何度も呼び出される関数である
・関数を作成することで、複数の処理を整理でき、コードの可読性が上がる
上記のようなケースに該当する場合は、関数の恩恵は大きいと言えます。

他の関数の呼び出し

複数の関数を呼び出す方法を学習しましょう。main関数で変数aをint型の5で初期化します。multiplie関数を作成して、引数で渡された変数aの2乗を計算して返却します。その計算結果を表示するprint関数を作成します。以下はコードの例になります。

function4.c
#include <stdio.h>

// multiplie関数
int multiplie(int a) {
  return a * a;
}

// multiplie関数
void print(int a) {
  printf("%d", a);
  return;
}

// main関数
int main(void) {

  int a = 3;
    
  print(multiplie(a));

  return 0;
}
実行結果
function4.exe
9

作成した関数の中で更に関数を呼び出しています。関数によりますが、関数で処理した結果を他の関数に直接渡しても、処理に影響が発生しない場合は「print(multiplie(a))」のように記述しても処理は正常動作します。関数毎に返却値を格納する変数を宣言する必要がなるくなるため、リソースを節約することが可能です。

Lesson 5 Chapter 2
参照と参照渡し

値渡しの限界

値渡しについては、前Chapterで理解が深まったと思います。では、関数を作成して2つの変数の値を入れ替えた結果を表示するプログラムを書いてみましょう。

replace1.c
#include <stdio.h>

// replace関数
void replace(int a, int b) {
  
  int temp = a;
  a = b;
  b = temp;
  
  puts("replace関数で値の入れ替えを実行");

  return;
}

// main関数
int main(void) {

  int a = 1;
  int b = 2;
  
  printf("入れ替え前の変数aの値: %d", a);
  printf("入れ替え前の変数bの値: %d", b);
  
  replace(a, b);

  printf("入れ替え後の変数aの値: %d", a);
  printf("入れ替え後の変数bの値: %d", b);

  return 0;
}
実行結果
replace1.exe
入れ替え前の変数aの値: 1
入れ替え前の変数bの値: 2
replace関数で値の入れ替えを実行
入れ替え後の変数aの値: 1
入れ替え後の変数bの値: 2

実引数でaの値1とbの値2をreplce関数を渡し、replace関数を使って、仮引数で受け取ったaとbの値を入れ替えましたが、同じ値になっていることが分かります。なぜ、このようなことが起こるのか不思議に思えます。本Chapterではその理由と、値の入れ替えができるコードの書き方について学習します。

参照

参照とは、変数の「アドレス」を指す識別子のことです。ポインタ(Lesson7)とよく似ていますが、ポインタよりも扱いやすくなっています。分かりやすく説明すると、参照は「変数の別名」と表現できます。参照は、必ず何らかの変数を指しています。変数の前に「&」(アンパサンド)を指定します。この演算子のことを「アドレス演算子」と呼びます。以下のように変数を書きます。詳細はLesson7で学習しますので、ここでは「変数のアドレスを渡す演算子」と理解しましょう。

参照
&変数名

参照渡し

「参照渡し」は、変数の前に「&」(アドレス演算子)を書いた変数を用いて「値渡し」を行うことを示します。「replace1.c」のプログラムを「値渡し」から「参照渡し」に書き替えてみましょう。

replace2.c
#include <stdio.h>

// replace関数
void replace(int *a, int *b) {

  int *temp = *a;
  *a = *b;
  *b = *temp;
  
  puts("replace関数で値の入れ替えを実行");
  
  return;
}

// main関数
int main(void) {

  // 参照変数の宣言
  int a = 1;
  int b = 2;

  printf("入れ替え前の変数aの値: %d\n", a);
  printf("入れ替え前の変数bの値: %d\n", b);

  replace(&a, &b);

  printf("入れ替え後の変数aの値: %d\n", a);
  printf("入れ替え後の変数bの値: %d\n", b);

  return 0;
}
実行結果
replace2.exe
入れ替え前の変数aの値: 1
入れ替え前の変数bの値: 2
replace関数で値の入れ替えを実行
入れ替え後の変数aの値: 2
入れ替え後の変数bの値: 1

replace関数を作成して「参照渡し」を行いました。入れ替え前の結果と、入れ替え後の結果を比較すると、値が入れ替わっていることが分かります。ここで「int *a, int *b」という見慣れない記号が登場してます。詳細については、Lesson7のポインタで解説します。ここでは「参照渡し」をしたい場合は、変数の前に「&」をつけて、変数の「アドレス」を渡すと理解しましょう。

アドレスとは

変数につけられたメモリ上の番号のことを指します。&aは変数aの「アドレス」を示すように「アドレス」の考え方はC言語を理解する上で重要なキーワードになります。このLessonでは割愛して、Lesson7で詳細を解説します。

三値のソート

配列に格納された3個の値をソート(昇順や降順に並び替える)について紹介します。最初は関数を作成せず、main関数だけを使って昇順でソートするプログラムを書きます。

sort1.c
#include <stdio.h>
int main(void) {

  int data[] = {112, 38, 54};
  int temp; 

  // 配列の0番目と1番目を比較
  if (data[0] > data[1]) {
    temp = data[0];
    data[0] = data[1];
    data[1] = temp;
  }

  // 配列の0番目と2番目を比較
  if (data[0] > data[2]) {
    temp = data[0];
    data[0] = data[2];
    data[2] = temp;
  }

  // 配列の1番目と2番目を比較
  if (data[1] > data[2]) {
    temp = data[1];
    data[1] = data[2];
    data[2] = temp;
  }
  
  printf("昇順結果: %d %d %d\n", data[0], data[1], data[2]);
  
  return 0;
}
実行結果
sort1.exe
昇順結果: 38 54 112

ソートするために、配列の値の比較処理を3回行います。

sort1.c
 if (data[0] > data[1]) {
  temp = data[0];
  data[0] = data[1];
  data[1] = temp;
}

最初に「data[0] > data[1]」で判定します。「112 > 38」の結果は「真」となるため、一時退避用の変数tempにdata[0]の値を代入して、data[0]にはdata[1]を代入します。そのあと、data[1]に変数tempの値(112)を格納します。
次に「data[0] > data[2]」の判定を行います。「38 > 54」を判定した結果は「偽」となるため、この処理は実行しません。
最後に「data[1] > data[2]」の判定を行います。「112 > 54」の比較を行い、結果は「真」となるため、配列に格納されている値の入れ替えを実行します。複雑な処理を実行しているように見えますが、一行ずつ確認すると、やっていることは今まで学習した内容と変わらないことが分かります。配列に格納されている値の順番を入れ替えているだけです。3つの値をソートしたい場合は、上記の処理を実行すれば、結果を取得できます。if文の演算子「>」を「<」に書き替えることで降順ソートした結果を取得することも可能です。
気づいている方もいるかもしれませんが、上記のプログラムは似たような処理を3回実行しており、その部分を関数化することができます。以下のコードに注目してください。

sort1.c


  // 1回目のif文の中の処理
  temp = data[0];
  data[0] = data[1];
  data[1] = temp;
  
  // 2回目のif文の中の処理
  temp = data[0];
  data[0] = data[2];
  data[2] = temp;

  // 3回目のif文の中の処理
  temp = data[1];
  data[1] = data[2];
  data[2] = temp;
  

「変数は異なりますが処理が同じ」ということが分かります。この処理を関数にしましょう。合わせて、if文の判定条件の演算子を「>」から「<」に書き替えて「降順」の結果が取得できるか確認します。

sort2.c
#include <stdio.h>

// swap関数
void sort(int *a, int *b) {
	int temp;
	temp = *a;
	*a = *b;
	*b = temp;
	return;
}

// main関数
int main(void) {

  int data[] = {112, 38, 54};
  int temp;

  // 配列の0番目と1番目を比較
  if (data[0] < data[1]) swap(&data[0], &data[1]);

  // 配列の0番目と2番目を比較
  if (data[0] < data[2]) swap(&data[0], &data[2]);

  // 配列の1番目と2番目を比較
  if (data[1] < data[2]) swap(&data[1], &data[2]);
  
  printf("降順結果: %d %d %d\n", data[0], data[1], data[2]);
  
  return 0;
}
実行結果
sort1.exe
降順結果: 112 54 38

swap関数を作成して「参照渡し」を行い、並び替えた結果を降順ソートで表示するプログラムに書き替えています。sort関数に実引数を渡す際は「sort(&data[0], &data[1])」と書いて各配列のアドレスを「参照渡し」しています。値を受け取ったsort関数では、仮引数*aと*bを使って、関数として切り出した、if文の中の処理と同じ処理になるように記述します。関数を作成した部分は、少し難しい内容を含んでいるため、完全に理解する必要はありません。「値渡し」と「参照渡し」の書き方の違いをしっかり押さえておきましょう。

Lesson 5 Chapter 3
有効範囲と記憶域期間

有効範囲

変数名や関数名などの識別子には、スコープと呼ばれる「有効範囲」があります。例えば、関数内で宣言した変数のスコープは、宣言の位置から関数の末尾までです。つまり、宣言から関数の終わりまで、その変数を使えます。一般的には、ブロック内で宣言した変数のスコープは、宣言の位置からブロックの末尾までです。関数で行う処理も{}で囲まれていますが、これもブロックです。
if文や多次元配列のChapterで「入れ子」(ネスト)構造について学習しました。外側のブロックに内側のブロックが記述されている構造です。

入れ子構造

外側のブロックA {
  内側のブロックB {
  
  
  }
}

ブロックについて簡単に復習したtころで、下記のような構造について、変数の「有効範囲」に考えてみましょう。

異なるブロックに同じ変数を宣言

ブロックA {
  ブロックB {
  
    int a = 1;
    printf("%d\n", a);
  
  }
  ブロックC {
  
    int a = 1;
    printf("%d\n", a);
    
  }
}

通常、同じ名前の変数を宣言するとコンパイルエラーとなり、プログラムが実行できませんが、上記の場合、ブロックが異なるためコンパイルエラーになりません。実際に上記のプログラムを書いてみましょう。

block1.c
#include <stdio.h>
int main(void) {
  
  {
    int a = 1;
    printf("%d\n", a);
  }

  {
    int a = 1;
    printf("%d\n", a);
  }
  return 0;
}
実行結果
block1.exe
1
1

このプログラムは問題なく実行できます。このようにブロックが異なれば、同じ変数名を再利用することが可能です。プログラミングをしていると、変数名を考えるのに手間がかかるのですが、同じ変数名を上手に再利用すると、名前を考える労力を減らせます。
入れ子構造を使って、変数の宣言をした場合の「有効範囲」について考えてみましょう。外側のブロックで変数aを宣言して整数1で初期化、内側のブロックでも、変数aを宣言して整数2で初期化した場合、、最終的な変数aの値はどうなるでしょうか。実際にプログラムを書いてみましょう。

block2.c
#include <stdio.h>
int main(void) {
  
  int a = 1;
  printf("%d\n", a);
  
  {
    int a = 1;
    printf("%d\n", a);  
  }
  
  printf("%d\n", a);  
  
  return 0;
}
実行結果
block2.exe
1
2
1

内側のブロックでは、内側のブロックで宣言した変数aが優先されるので、1が表示されています。最後の表示では、最初に外側のブロックで宣言した変数aの値が残っていて、2ではなく1が表示されていることが確認できます。変数の「有効範囲」とブロックの関係について学びました。「有効範囲」を理解することは、正しく動くプログラムを書いたり、変数名を再利用してプログラミングの手間を減らす上で、非常に役立ちます。

記憶域期間

変数には「記憶域期間」と呼ばれる生存期間が存在します。生存期間中の変数は、固定のアドレスに配置されるため、最後に書き込んだ値が保持されることが保証されます。この「記憶域期間」も、変数を宣言する方法によって異なります。今までは、関数の中で変数を宣言しましたが、関数の外でも変数を宣言することができます。宣言の方法が異なる、これらの変数について、「記憶域期間」や「有効範囲」がどうのように異なるか学習します。

関数内で宣言した変数

記憶域期間は「自動記憶域期間」になります。「自動記憶域期間」の変数は、ブロックに入ったときに生存期間が始まり、ブロックから出たときに生存期間が終了します。ここで「ブロックから出る」というのは、ブロックの末尾に到達した場合だけではなく、ブロックの途中でreturn文を使って呼び出し元に戻った場合なども含みます。いずれの場合も、関数内で宣言した変数は、呼び出し元に戻ったときには生存期間が終わっています。同じ関数をもう一度呼び出しても、前回の呼び出しで変数に格納した値は失われているので、改めて初期化や代入が必要になります。

関数内で宣言した変数の「記憶域期間」
関数外

  関数A {
    変数aの宣言 ← 「変数aはブロック内だけで読み書き可能」
    …
  }

  関数B {
    変数bの宣言 ← 「変数bはブロック内だけで読み書き可能」
    …
  }

関数内で宣言した変数の記憶域期間を確認してみましょう。main関数とfunction関数を作成して、同じ変数aを宣言して、値を表示するプログラムを書きます。

function5.c
#include <stdio.h>

// function関数
void function(void) {
  int a = 2;
  printf("%d\n", a);
  return;
}

// main関数
int main(void) {
  
  int a = 1;
  printf("%d\n", a);
  
  function();
  
  printf("%d\n", a);  
  
  return 0;
}
実行結果
function5.exe
1
2
1

上記のプログラムでは、main関数の変数aと、function関数で関数外の変数aを表示するプログラムを書きます。変数aが、それぞれ別の変数であることがポイントです。

関数外で宣言した変数

記憶域期間は「静的記憶域期間」です。「静的記憶域期間」の変数は、プログラムの起動時(main関数を実行する前)に生存期間が開始され、プログラムの終了時に生存期間が終了します。プログラムの実行中は、変数に格納した値が保持されます。「静的記憶域期間」の変数を初期化した場合は、プログラムの起動時に初期化されます。初期化しなかった場合は、自動的に0で初期化されます。

関数外で宣言した変数の「記憶域期間」
関数外

変数cの宣言    ← 「変数cはプログラム起動時に生存開始」

  関数A {      ← 「変数cは関数Aでも読み書き可能」
    変数aの宣言 
    …
  }

  関数B {      ← 「変数cは関数Bでも読み書き可能」
    変数bの宣言
    …
  }

どの関数からも読み書き可能なことを確認してみましょう。main関数とfunction関数で関数外の変数aを表示するプログラムを書きます。

function6.c
#include <stdio.h>

int a = 1;

// function関数
void function(void) {
  printf("%d\n", a);
  return;
}

// main関数
int main(void) {
  
  printf("%d\n", a);
  
  function();
  
  printf("%d\n", a);  
  
  return 0;
}
実行結果
function6.exe
1
1
1

結果から、どの関数からでも読み込み可能で、値もずっと1であることが分かります。
次に初期化しなかった場合は、0になることを確認してみましょう。

function7.c
#include <stdio.h>

int a;

// function関数
void function(void) {
  printf("%d\n", a);
  return;
}

// main関数
int main(void) {
  
  printf("%d\n", a);
  
  function();
  
  printf("%d\n", a);  
  
  return 0;
}
実行結果
function7.exe
0
0
0

結果から、初期化時に値を代入しなかった場合は、0となることが確認できます。
しかし、この方法だと変数cはどの関数からもアクセス可能(グローバル変数)なため、予期せぬところで値を変更されたり、その変更した場所の特定が難しくなります。結果、可読性の低下に繋がり、ソースの修正するのが大変になります。チームでプログラムを開発する場合、このようなグローバル変数を使用するかどうかルールを決めることがあります。使い方を間違えるとバグの原因となってしまうので、慎重に扱う必要があります。

Lesson 5 Chapter 4
インライン関数

インライン関数

関数の呼び出しや戻りの手間をなくすために、C言語にはインライン関数という機能があります。C99以降で使えるようになりました。インライン関数は通常の関数とは異なり、スタックを使った呼び出しや戻りの処理を行いません。代わりに、関数の内容(関数内に記述した処理)を、呼び出しに展開(コピー)します。関数の呼び出しが、関数の内容に置き換わるイメージです。これをインライン化と呼びます。インライン化によって、呼び出しや戻りの手間がなくなるので、プログラムが高速になる可能性があります。
インライン関数を定義する方法は、通常の関数を定義する際に、先頭にinline指定子をつけると、インライン関数になります。引数がない場合も同様です。

インライン関数の定義
inline 戻り値の型 関数名(引数の型 引数名, …) {
  宣言または文;
  …
  return 式;
}

注意が必要なのは、インライン関数は必ずしもインライン化されるわけではなく、インライン化するかどうかの判定はコンパイラに任されていることです。インライン化すると、関数呼び出しごとに関数の内容を展開するので、コンパイル後の機械語プログラムが長くなり、低速化につながることがあります。inlineをつけても、インライン化するメリットがないとコンパイラが判断した場合は、インライン化されないことがあります。inlineをつけるのは、コンパイラに「この関数をインライン化して欲しい」という希望を伝えると考えるとよいでしょう。コンパイラによっては、インライン化を有効にするために、コンパイル時に特別なオプションを指定する必要があります。GCCでは「-O2」というオプションが必要です。OはOptimize (オプティマイズ、最適化)の略で「-O2」はインライン化を含む色々な最適化(高速化を目的としたプログラムの加工)を適用するためのオプションです。最適化を適用したコンパイルは、次のように行います。

最適化を適用したコンパイル
gcc -o 実行ファイル名 ソースファイル名 -O2
最適化を適用したコンパイル(Windowsで日本語を含むソースファイルの場合)
gcc -o 実行ファイル名 ソースファイル名 -O2 -fexec-charset=cp932

実際にインライン関数を定義して、呼び出してみましょう。int型の引数aとbを受け取り、加算した結果をint型の戻り値として返すadd関数をインライン関数として定義し、呼び出して結果を表示するプログラムを書きます。

function8.c
#include <stdio.h>

// add関数(inline関数)
inline int add(int a, int b) {
  return a + b;
}

// main関数
int main(void) {
  
  printf("%d\n", add(23, 12));  
  printf("%d\n", add(40, 88));
  printf("%d\n", add(3, 49));
  printf("%d\n", add(59, 16)); 
  printf("%d\n", add(81, 32)); 
  
  return 0;
}
実行結果
function8.exe
35
128
52
75
113