C言語のインライン関数とは、inline を付けて宣言した関数です。「この関数の呼び出しを、関数本体の展開で置き換えてよい」とコンパイラに伝えます。C99で標準化されましたが、規格上の位置づけは「できるだけ速く呼べるようにしてほしい」という提案で、展開するかどうかはコンパイラが決めます。実務で最初につまずくのは速度ではなくリンクです。inline だけを付けた関数は、最適化なし(-O0)でビルドするとリンクエラーになることがあります。この記事では、その理由と static inline を使った書き方、展開を確かめる方法、マクロとの違い、C++の inline との差を、clangでの実測を交えて解説します。
まとめ:C言語のインライン関数の要点
- 意味:
inlineは展開の許可とヒントです。付けても展開されないことがあり、付けなくても最適化時には展開されます。 - 最初の落とし穴:C99以降、
inline単独の定義は外部定義を作りません。-O0では呼び出しが残り、未定義シンボルでリンクに失敗します。 - 書き方の既定:ヘッダーに置く小さな関数は
static inlineにします。関数の実体を1つにしたいときは、ヘッダーにinline定義を置き、1つの .c ファイルでextern inline宣言をします。 - 確かめ方:展開されたかどうかは推測せず、clangの
-Rpass=inline、GCCの-Winline、またはアセンブリ出力で確認します。別々の翻訳単位にある関数も、ClangやGCCの-flto、MSVCの/LTCGなどでリンク時最適化を有効にすると、展開の対象になり得ます。 - マクロとの違い:インライン関数は引数を1回だけ評価し、型の検査も受けます。関数形式マクロで書けるものは、原則としてインライン関数に置き換えます(CERT C PRE00-C)。
- C++との違い:C++の
inlineは「複数の翻訳単位にある同じ定義を1つの実体として扱う」という意味が中心です。ヘッダーにinline関数を置けば、そのまま複数の .cpp から使えます。
インライン関数とインライン展開の違い
インライン展開は、コンパイラが関数呼び出しの箇所に関数本体をはめ込む最適化です。呼び出し命令・引数の受け渡し・戻りのジャンプがなくなり、展開後のコードは呼び出し元の文脈(定数の引数など)とまとめて最適化されます。インライン関数は、その展開の対象として inline 指定子を付けた関数を指す、言語上の用語です。
C11規格(ISO/IEC 9899:2011、委員会草案 N1570)の6.7.4は「関数をインライン関数にすることは、その関数の呼び出しをできるだけ速くすることを提案する」と定め、提案がどの程度効くかは処理系定義としています。つまり inline は命令ではありません。次の例で、sq と sq2 はどちらも static inline ですが、-O0 ではアセンブリに callq _sq2 が残り、sq2 は展開されませんでした(sq は後述の always_inline 付き)。-O2 ではどちらも呼び出しが消えます。
static inline __attribute__((always_inline)) int sq(int x) { return x * x; }
static inline int sq2(int x) { return x * x; }
int f(int x) { return sq(x) + sq2(x); }
「inline」の語自体は「行内の・その場に埋め込まれた」という意味で、呼び出しの行に本体を埋め込むことに由来します。
C言語でのinlineの書き方:3つの形と使い分け
C言語のインライン関数には、リンクの扱いが異なる3つの書き方があります。違いは「どの翻訳単位に関数の定義を置くか」です。翻訳単位は、.cファイルにヘッダーの内容を取り込み、前処理を行った結果を指します。
| 書き方 | 外部定義(実体) | -O0 での結果 | 主な用途 |
|---|---|---|---|
inline のみ |
作られない | リンクエラー | 単独では使わない |
static inline |
各 .c に内部の実体 | 成功 | ヘッダーの小関数 |
inline + extern 宣言 |
宣言した .c に1つ | 成功 | 実体を1つにしたい |
inline単独の定義がリンクエラーになる理由
C11の6.7.4第7段落は、ある翻訳単位の中で関数のファイルスコープ宣言がすべて extern なしの inline なら、その定義を「インライン定義」とし、インライン定義は外部定義を提供しないと定めています。呼び出しにインライン定義と外部定義のどちらが使われるかは未規定です。C23後の作業文書(N3220)では節番号が6.7.5に移りましたが、この規定は変わっていません。次のコードは多くの入門記事に載る形ですが、外部定義がどこにもありません。
#include <stdio.h>
inline int sq(int x) { return x * x; }
int main(void) { printf("%d\n", sq(7)); return 0; }
Apple clang 21.0.0(x86_64)でビルドした結果は次のとおりです(2026年10月12日に再実行して同じ結果)。
| オプション | 結果 |
|---|---|
-std=c17 -O0 |
Undefined symbols(_sq が未定義) |
-std=c23 -O0 |
Undefined symbols(同上) |
-std=c17 -O1 / -O2 |
49 と表示(展開されて呼び出しが消える) |
-std=gnu89 -O0 |
49 と表示 |
-std=c17 -fgnu89-inline -O0 |
49 と表示 |
最適化によってリンクが通る場合もありますが、このコードは規格の要件を満たしていません。C11の6.9第5段落では、このように式で使用する外部結合の関数には、プログラム全体で外部定義がちょうど1つ必要です。最適化レベルを上げても、外部定義の欠落を修正したことにはなりません。-std=gnu89 や -fgnu89-inline で通るのは、C99より前のGNU拡張では inline 単独の定義が外部定義も作る仕様だったからです。GCCのC方言オプションの説明は、-fgnu89-inline を「C99モードで従来のGNUの意味を使う」指定としています。古いコードをC99以降に移したときにこのエラーが出るのは、この意味の違いが原因です。
インライン定義にはもう1つ制約があります。外部結合の関数のインライン定義は、書き換え可能な static 変数を定義してはならず、内部結合の識別子を参照してもいけません(6.7.4第3段落)。clangは inline 関数内の static int n = 0; に対して -Wstatic-local-in-inline の警告を出しました。
static inline:ヘッダーに置く小さな関数の定番
static を付けると関数は内部結合になり、6.7.4の「内部結合の関数はどれもインライン関数にできる」が適用されます。展開されなかった場合は、その翻訳単位の中に関数の実体が作られるので、-O0 でもリンクは通ります。
#ifndef SQ_H
#define SQ_H
static inline int sq(int x) { return x * x; }
#endif
ヘッダーで static だけを付けた場合との実用上の違いは、未使用の警告です。GCCのマニュアルは -Wunused-function(-Wall に含まれる)の対象を「使われていない非inlineのstatic関数」としています。clangの -Wall で試すと、ヘッダーにある未使用の static 関数には unused function の警告が出て、同じヘッダーの未使用の static inline 関数には出ませんでした(.c ファイルの中に直接書いた未使用の static inline には出ます)。ヘッダーの関数を、インクルードする全ファイルが使うとは限りません。static inline が定番になっているのは、この警告を避けられるからでもあります。
注意点は、展開されなかった場合に実体が .c ごとに作られることです。2つの .c から同じ static inline 関数のアドレスを取って比べると、-O2 でも一致せず、nm には _one が2つ並びました。関数ポインタで同一性を比較するコードや、大きめの関数をヘッダーで static inline にする場合は、コードサイズの重複に注意してください。
書き順は static inline にします。inline static もコンパイルは通りますが、C11の6.11.5は、記憶域クラス指定子を宣言の先頭以外に置く書き方を「廃止予定の機能」としています。GCCは -Wextra に含まれる -Wold-style-declaration でこれを警告します。
inline+extern宣言:実体を1つだけ置く書き方
C99の意図どおりの書き方です。ヘッダーには inline 定義だけを置き、どれか1つの .c ファイルで extern inline 宣言をします。その .c ファイルには外部定義が1つだけ作られ、展開されなかった呼び出しはそこへリンクされます。
#ifndef SQ_H
#define SQ_H
inline int sq(int x) { return x * x; }
#endif
#include "sq.h"
extern inline int sq(int x);
3ファイル構成(main.c・f1.c・sq.c)で試すと、sq.c を含めれば -O0 でも 9 16 と表示され、sq.c を外すと -O0 で未定義シンボルのエラーになりました。ライブラリとして関数を公開し、ヘッダー経由の呼び出しでは展開も期待したい場合に向く形です。
コンパイラの展開判断とinline指定の効果
Clangの最適化リマークによる展開結果の確認
展開されたかどうかは、clangの -Rpass=inline(展開したもの)と -Rpass-missed=inline(しなかったもの)で、呼び出し箇所ごとに確認できます。Clang Users Manual は、インライナーの報告を得る例として clang -O2 -Rpass=inline を挙げています。次の r.c を Apple clang 21.0.0(x86_64)の -O2 でコンパイルし、出たリマークを表にまとめました。閾値の数値はこのコードと環境での結果です。
static inline int add(int a, int b) { return a + b; }
static int big(int n) {
unsigned int s = 0;
for (int i = 0; i < n; i++) s += (i * 31) ^ (s >> 3);
for (int i = 0; i < n; i++) s -= (i * 17) | (s << 2);
return s;
}
__attribute__((noinline)) static int ni(int x) { return x + 1; }
static int fact(int n) { return n <= 1 ? 1 : n * fact(n - 1); }
int use(int n, int m) { return add(n, m) + big(n) + big(m) + big(n+m) + ni(n) + fact(n); }
clang -O2 -c r.c -Rpass=inline -Rpass-missed=inline
| 関数 | 指定 | 結果 | 表示された閾値 |
|---|---|---|---|
| add(1行) | static inline |
展開 | 487 |
| big(ループ2つ) | static のみ |
展開 | 225 |
| ni | noinline |
展開しない | never |
| fact(再帰) | static のみ |
自分自身へは展開しない | never(recursive) |
読み取れることは2つあります。1つは、inline を付けていない big も展開されたことです。コンパイラは指定の有無にかかわらず、コストが閾値を下回る関数を展開します。もう1つは、inline 付きの add の閾値が487で、指定なしの225より高いことです。inline は「展開してよい」という許可に加えて、判断の基準を緩めるヒントとして働いています。-Os では指定なしの閾値が50まで下がりましたが、add は487のままでした。
GCCでは -Winline で、inline 宣言した関数が展開できなかったときに警告を出せます。ただしGCCのマニュアルは、関数の大きさやそれまでの展開量で判断が変わるため、小さな変更で警告が出たり消えたりすると注意しています。
GCCの最適化レベル別インライン化オプション
現行のGCC 16.2(2026年8月7日リリース)の最適化オプションの説明では、最適化なし(-O0)の既定は -fno-inline で、always_inline 属性の関数以外は展開しません。最適化レベルごとの主な既定は次のとおりです。
| GCCのオプション | 対象 | 有効になるレベル |
|---|---|---|
-finline-functions-called-once |
1回だけ呼ばれる static 関数 | -O1 / -O2 / -O3 / -Os(-Og は除く) |
-finline-small-functions |
呼び出しコードより小さい関数 | -O2 / -O3 / -Os |
-finline-functions |
inline 指定のない関数も含む全関数 | -O2 / -O3 / -Os |
この表のとおり、-O2 以上では inline の有無にかかわらず小さな関数は展開の候補になります。C言語で inline を付ける主な理由は、速度のヒントよりも、ヘッダーに定義を置いても多重定義にならないリンク上の扱いにあります。
再帰とアドレス取得の展開条件
- 再帰関数:前の表の
factでは自己呼び出しを展開しないというリマークが出ましたが、最終アセンブリでは再帰呼び出しも消えていました。リマークだけでは最終的な呼び出しの残存を判断できません。引数が定数なら、fact2(5)が-O2で即値120に置き換わったように、展開とは別の定数畳み込みで計算ごと消えることもあります。 - アドレスを取った関数:アドレスが関数の外へ渡る(戻り値や大域変数に入る)と、関数ポインタ経由の呼び出しに備えて実体が残ります。直接の呼び出しが展開されても同じです。一方、関数の中でアドレスを取ってそのまま呼ぶだけのコードは、clangの
-O2で呼び出しも実体も消えました。
LTO(-flto)による別ファイルの関数の展開
コンパイラは翻訳単位ごとに処理するため、別の .c ファイルにある関数は、定義が見えず展開できません。展開のために関数をヘッダーへ移す代わりに、リンク時最適化(LTO)を使う方法があります。次の2ファイルで、-flto の有無を比べました(Apple clang 21.0.0・-O2・2026年10月12日)。
/* sq.c */
int square(int x) { return x * x; }
/* m.c */
#include <stdio.h>
int square(int x);
int main(void) { printf("%d\n", square(5)); return 0; }
clang -O2 sq.c m.c -o nolto
clang -O2 -flto sq.c m.c -o lto
otool -tv nolto
otool -tv lto
| ビルド | main の中身 | square の実体 |
|---|---|---|
-O2 |
movl $0x5, %edi → callq _square |
残る(T _square) |
-O2 -flto |
movl $0x19, %esi(25を直接printfへ) |
消える |
LTOなしでは square(5) の呼び出しが残り、LTOありでは展開と定数畳み込みで結果の25(0x19)が直接書かれ、square の実体もなくなりました。MSVCでは /LTCG が同じ役割で、Microsoft Learn のインライン関数の解説は「ソース コードで要求されているかどうかにかかわらず、モジュール間のインライン展開を行う」と説明しています。呼び出し元と呼び出し先の両方をLTOで解析できるビルドなら、別ファイルのまま展開を試せます。関数をヘッダーへ移す前に、最適化結果を確認してください。ヘッダーに置くのは、LTOを使わない利用者にも展開を届けたいライブラリの小関数に絞れます。
always_inline・noinline・__forceinlineによる展開制御
展開を強制・禁止したい場合は、処理系の拡張を使います。標準の inline にはその力がありません。
| 処理系 | 強制 | 禁止 | 全体の制御 |
|---|---|---|---|
| GCC / Clang | __attribute__((always_inline)) |
__attribute__((noinline)) |
-fno-inline など |
| MSVC | __forceinline / [[msvc::forceinline]] |
__declspec(noinline) / [[msvc::noinline]] |
/Ob0〜/Ob3 |
always_inline は最適化なしでも効きます。冒頭の例の sq は、clangの -O0 でも「always inline attribute」で展開されたとリマークに出ました。GCCのマニュアルでも、-fno-inline は always_inline の関数だけを例外としています。使いどころは、デバッグビルドでも呼び出しを消したいごく小さな関数や、noinline と組み合わせて性能測定の条件を固定したい場面です。GCCのマニュアルは always_inline の関数を展開できなかった場合をエラーとして診断すると定めているので、測定なしで広く付けるものではありません。
MSVCの __forceinline も強制ではありません。Microsoft Learnは、__forceinline でも展開できない条件として、/Ob0(デバッグビルドの既定)でのコンパイル、可変長引数の関数、#pragma inline_recursion(on) を指定していない再帰関数、仮想関数の仮想呼び出し、関数ポインタ経由の呼び出しなどを挙げています。/Ob オプションの説明によれば、/Ob0 は展開を無効にし、/Ob1 は inline・__inline・__forceinline とクラス内で定義したメンバー関数だけを展開候補にします。/Ob2 は /O1・/O2 の既定で、明示的に展開を禁止していないすべての関数を対象にします。Visual Studio 2019からは、さらに積極的に展開する /Ob3 も使えます。
インライン関数と関数形式マクロの違い
C99でインライン関数が標準化される前から、関数形式マクロは呼び出しのコストを避ける手段として使われていました。GNU Cなどには、標準化前からインライン関数の拡張もありました。マクロは型も評価順も持たないテキスト置換なので、同じ見た目でも結果が変わります。
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
static inline int square(int x) { return x * x; }
static int calls = 0;
static int next(void) { calls++; return 3; }
int main(void) {
int a = SQUARE(next());
printf("macro : %d (calls=%d)\n", a, calls);
calls = 0;
int b = square(next());
printf("inline: %d (calls=%d)\n", b, calls);
return 0;
}
実行すると、マクロは macro : 9 (calls=2)、インライン関数は inline: 9 (calls=1) と表示されました。マクロは引数の式を2回書き写すので、next() が2回呼ばれます。引数が i++ なら、1つの式で同じ変数を2回書き換える未定義動作になり、clangは -Wunsequenced で警告しました。
| 観点 | インライン関数 | 関数形式マクロ |
|---|---|---|
| 引数の評価 | 1回 | 書かれた回数だけ |
| 型の検査 | プロトタイプで検査・変換 | 展開後の式で決まる |
| デバッガ | デバッグ情報があれば関数・行で停止可能 | 展開先の実行行で停止可能 |
| 型を問わない処理 | 型ごとに必要 | 1つで書ける |
| トークン連結・文字列化 | できない | できる |
JPCERTが翻訳を公開しているCERT Cコーディングスタンダードの PRE00-C は、「関数形式マクロよりもインライン関数やスタティック関数を使う」ことを推奨しています。例外として、トークンの連結や文字列化、型に依存しない処理、呼び出し元の自動変数を操作する処理など、関数では書けないものを挙げています。迷ったらインライン関数で書き、関数で表現できない理由がはっきりあるときだけマクロを使う、という判断で足ります。
インライン関数で速くなる場面と遅くなる場面
呼び出しのコストがどれほどかを、1行の関数を10億回呼ぶループで測りました(2026年10月12日・Apple clang 21.0.0・macOS 26.6.2・Intel Core i9-9880H)。展開なしの版は -DNOINL で noinline 属性を付けてビルドしています。
#include <stdio.h>
#include <time.h>
#define N 1000000000L
#ifdef NOINL
__attribute__((noinline))
#endif
static inline long add1(long x) { return x + 3; }
int main(void) {
struct timespec t0, t1;
long s = 0;
clock_gettime(CLOCK_MONOTONIC, &t0);
for (long i = 0; i < N; i++) s = add1(s) ^ i;
clock_gettime(CLOCK_MONOTONIC, &t1);
printf("%ld %.3f s\n", s, (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9);
}
clang -O2 bench.c -o bi
clang -O2 -DNOINL bench.c -o bn
| 条件 | 5回の実行時間 | 中央値 |
|---|---|---|
| 展開あり(static inline) | 0.956〜0.978 秒 | 0.971 秒 |
| 展開なし(noinline を付与) | 1.894〜1.962 秒 | 1.950 秒 |
本体が加算1回の関数では、呼び出しが残ると約2倍遅くなりました。出力値はどちらも 52355378176 で同じです。1回あたりに直すと約1ナノ秒の差ですが、この差には展開に伴うループ全体の最適化の違いも含まれ、呼び出し命令だけのコストではありません。関数本体の処理が長くなるほど、呼び出しコストの割合は小さくなります。展開の効果が出るのは、ループの内側で何度も呼ばれる、本体の小さい関数です。アクセサ、比較関数、ビット演算の小さなヘルパーが典型です。展開されると呼び出し元のループと一体で最適化されるため、SIMDの自動ベクトル化の対象になることもあります。
次の場合は、inline の採用を性能だけで判断せず、コードサイズや利用側の再ビルドも考慮します。
- 大きな関数を多数の箇所で展開する:展開でコードサイズが増えると、命令キャッシュへの負荷が増し、性能が低下することがあります。
-Osで閾値が下がるのはこのためです。 - ライブラリの公開API:ヘッダーに本体を出すと、本体を変えたときに利用側の再コンパイルが必要になります。
- 計測せずに付ける:
-O2以上ではコンパイラが自分で判断します。付けるかどうかは、プロファイラで呼び出しが上位に出た関数から検討してください。
C++のinline関数とC言語とのリンク規則の違い
C++の inline も展開のヒントですが、実務上の意味は「同じ定義が複数の翻訳単位にあってもよい」というリンクの規則です。構文はCと同じでも、ヘッダーに置いたときの振る舞いが変わります。
ヘッダーの定義を1つの実体にまとめる規則(ODR)
C++ドラフトの [dcl.inline] の注記は、外部結合のインライン関数・インライン変数は複数の翻訳単位で定義できるが「1つのアドレスを持つ1つの実体」であり、その関数の中で定義した static 変数も1つになると述べています。定義は同じトークン列で構成され、定義内の名前も原則として同じ実体を参照する必要があります(one definition rule)。同じヘッダーを使っていても、マクロや名前探索の違いでこの条件を破ることがあります。定義が1つの実体にまとまることを、clang++(-std=c++17)で2つの .cpp から確かめました。
// c.hpp
inline int next_id() { static int n = 0; return ++n; }
static inline int next_id_s() { static int n = 0; return ++n; }
| ヘッダーでの書き方 | 2つの .cpp で取ったアドレス | 関数内 static の連番 |
|---|---|---|
inline |
一致 | 1 → 2(共有) |
static inline |
不一致 | 1 → 1(別々) |
| 指定なし | duplicate symbol 'next_id()' でリンク失敗 |
|
C++でヘッダーに関数を書くときは、static inline ではなく inline にします。static inline は翻訳単位ごとに別の関数になるので、関数内の static 変数でカウンターやキャッシュを持つと、.cpp ごとに別々の値になります。また、前述のCのコード(inline 単独の定義)を clang++ -O0 でビルドすると、エラーにならず 49 と表示されました。Cとの差はここに出ます。
クラス内定義・constexpr・constevalの暗黙のinline
次の関数は inline を書かなくても inline です。GCCのインライン関数の説明も、ISO C++の要求として、クラス本体の中で定義したメンバー関数を inline 扱いにすると明記しています。
- クラス定義の中で定義したメンバー関数:アクセサをクラス定義に直接書けるのはこのためです。ただしC++20以降、名前付きモジュールに属するクラスでは、クラス内で定義したという理由だけでは暗黙の
inlineになりません([dcl.inline] の注記は対象を「グローバルモジュール」に限っています)。 constexpr関数(C++11):ヘッダーで定義して複数の .cpp から使っても、多重定義になりません。実測でも、inlineなしのconstexpr int cx(int)をヘッダーに置いた2ファイル構成でリンクが通りました。consteval関数(C++20):C++20の作業草案 N4861 の [dcl.constexpr] 第1段落は、最初の宣言でconstexprまたはconstevalを指定した関数を暗黙のインライン関数としています。
inline変数(C++17)による定数・カウンターの共有
C++17からは変数にも inline を付けられます。ヘッダーに inline int counter = 0; と書けば、すべての翻訳単位で1つの変数を共有し、実測でも2つの .cpp で取ったアドレスが一致しました。C++17より前は、ヘッダーで extern 宣言し、どれか1つの .cpp に定義を置く必要がありました。クラスの静的データメンバーも static inline int count = 0; とクラス内で定義できます。-std=c++14 でコンパイルすると、clangは「inline variables are a C++17 extension」と警告しました。
インライン関数に関するよくある質問
inlineを付ければ必ずインライン展開されますか?
されません。規格上 inline は提案で、効き方は処理系定義です。GCCやClangは -O0 では通常展開せず、最適化時もコストが閾値を超えれば展開しません。通常より強く展開を要求したいときは __attribute__((always_inline)) や __forceinline を使い、結果は -Rpass=inline やアセンブリ出力で確かめます。
static inline と inline static はどちらが正しいですか?
どちらもコンパイルできますが、static inline と書きます。C11の6.11.5は、static などの記憶域クラス指定子を宣言の先頭以外に置く書き方を廃止予定の機能としており、GCCは -Wextra で警告します。
インライン関数はデバッガでステップ実行できますか?
-O0 なら always_inline を付けない限り通常の関数として残るので、ブレークポイントを置けます。最適化ビルドで展開された関数は呼び出しとして存在しないため、デバッガによっては本体の行を追えても関数としては止まれません。最適化を変えると不具合が再現しなくなる場合があるため、問題が起きたビルドの情報も残してください。
再帰関数をインライン関数にできますか?
inline を付けること自体はできます。自己呼び出しの展開可否は処理系と条件によります。今回のclangの例では「recursive」を理由に自己呼び出しの展開を見送りましたが、GCCには深さを制限して再帰展開する機能があります。GCCは always_inline の関数を展開できないとエラーにするため、再帰関数には付けません。MSVCは #pragma inline_recursion(on) を指定すれば、最大16段まで再帰を展開できます。
C++でインライン関数を使うメリットは何ですか?
最大のメリットは、関数の定義をヘッダーに書いても多重定義のリンクエラーにならないことです。通常の非テンプレート関数や、C++17以降の inline 変数をヘッダーで定義できます。関数テンプレートの複数定義は別の規則で認められ、クラス内で定義したアクセサは、書かなくても暗黙の inline になります。展開による高速化は副次的で、最適化を有効にしたビルドでは inline の有無にかかわらずコンパイラが判断します。