C++のプログラミングを進める中で、多くの開発者が一度は直面し、頭を悩ませるのがundefined reference toというエラーメッセージです。
コンパイル自体は通っているように見えるのに、ビルドの最終段階で発生するこのエラーは、初心者からベテランまで多くの時間を奪う原因となります。
このエラーは、コンパイラがソースコード内の関数や変数の宣言を見つけたものの、リンカーがその実体(定義)を見つけられなかったときに発生するリンクエラーの一種です。
本記事では、2026年現在のモダンなC++開発環境も踏まえつつ、このエラーが発生する主な原因とその具体的な対処法について詳しく解説します。
リンクエラー「undefined reference to」の正体
C++のビルドプロセスは、大きく分けて「プリプロセス」「コンパイル」「アセンブル」「リンク」の4つのフェーズに分かれています。
コンパイルとリンクの違い
コンパイルフェーズでは、各ソースファイル(.cpp)が個別にオブジェクトファイル(.o または .obj)へと変換されます。
このとき、関数が他のファイルで定義されていても、ヘッダーファイル(.h または .hpp)に「宣言」さえあれば、コンパイラは「後で誰かがこの関数を見つけてくれるだろう」と判断して処理を続行します。
しかし、最終的な実行ファイルを作成するリンクフェーズでは、リンカーがすべてのオブジェクトファイルやライブラリをスキャンして、各関数の呼び出し元と実際の実体を結びつけ(解決)なければなりません。
この段階で、宣言に対応する実体が見つからない場合に、リンカーは「参照先が未定義である」としてundefined reference to '関数の名前'というエラーを出力します。
エラーメッセージの読み解き方
GNU系ツールチェーン(g++やclang++)を使用している場合、エラーメッセージには通常、どのオブジェクトファイルのどの関数内で、どのシンボルが見つからなかったかが示されます。
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x15): undefined reference to `calculate(int, int)'
collect2: error: ld returned 1 exit status
この例では、main.cppから呼び出されているcalculate(int, int)という関数の定義が見つからないことを示しています。
原因1:定義(実装)の欠如またはコンパイル対象漏れ
最も単純で頻繁に発生する原因は、関数の宣言はあるが、実装(定義)がどこにも書かれていない、あるいは実装したファイルがビルド対象に含まれていないケースです。
実装ファイルがコマンドラインに含まれていない
複数のファイルを分割して開発している場合、コンパイル・リンクのコマンドにすべてのソースファイルを含める必要があります。
誤った例
g++ main.cpp -o my_program
正しい例
g++ main.cpp functions.cpp -o my_program
functions.cppの中にcalculate関数の定義がある場合、後者のように明示的に含めなければなりません。
CMakeなどのビルドシステムを使用している場合は、add_executableにすべてのソースファイルがリストアップされているか確認してください。
プロトタイプ宣言と定義の不一致
宣言と定義で、引数の数、型、const修飾子の有無、あるいは戻り値の型が微妙に異なっていると、C++の名前マングリング(Name Mangling)機能により、別の関数として扱われてしまいます。
// header.h
int calculate(int a, int b);
// functions.cpp
// 引数の型が long になっているため、別物と判定される
int calculate(long a, long b) {
return a + b;
}
この場合、リンカーはint calculate(int, int)を探し続けますが、見つからないためエラーとなります。
原因2:ライブラリのリンク指定漏れ
外部ライブラリ(数学ライブラリ、OpenSSL、Boost、あるいは自作ライブラリ)を使用している場合、ヘッダーをインクルードしただけでは不十分です。
リンクオプションの不足
例えば、スレッド機能を使用する際に-pthreadを指定し忘れたり、数学関数を使用する際に-lmを忘れたりすると発生します。
よくあるリンクエラーの修正
| 使用する機能 | 指定すべきフラグ (例) |
|---|---|
| C++ 標準スレッド | -pthread |
| 数学関数 (C互換) | -lm |
外部共有ライブラリ libexample.so | -lexample |
| ライブラリのパス指定 | -L/path/to/library |
現代のC++開発では、CMakeを利用して以下のように記述するのが一般的です。
target_link_libraries(my_program PRIVATE example_lib)
原因3:CとC++の混在(extern “C” の欠如)
C言語で書かれたライブラリやソースファイルをC++から呼び出す際、あるいはその逆の場合に発生する特有の問題です。
名前マングリングの影響
C++は関数のオーバーロードをサポートするため、コンパイル時に関数名を「型情報を含んだ複雑な名前」に書き換えます。
これを名前マングリングと呼びます。
一方、C言語は名前マングリングを行わず、関数名がそのままシンボル名となります。
C++のコードからC言語の関数を呼び出す際、リンカーに「この関数はC言語の形式で探してほしい」と伝える必要があります。
誤った連携
// c_functions.h
void print_hello(); // C言語で実装されている
// main.cpp
#include "c_functions.h"
int main() {
print_hello(); // リンカーはマングリングされた名前を探してしまう
return 0;
}
正しい連携
// c_functions.h
#ifdef __cplusplus
extern "C" {
#endif
void print_hello();
#ifdef __cplusplus
}
#endif
このように、extern "C"で囲むことで、C++コンパイラに対して「C言語の規約で名前を扱え」と指示できます。
原因4:静的メンバ変数の実体定義漏れ
クラス内でstatic修飾子を付けたメンバ変数は、クラスの宣言内にあるのはあくまで「宣言」であり、ソースファイル(.cpp)側で実体を定義しなければなりません。
発生例
// MyClass.h
class MyClass {
public:
static int shared_value; // 宣言
};
// main.cpp
int main() {
MyClass::shared_value = 10; // ここで undefined reference が発生
return 0;
}
解決策
ソースファイル側で、メモリを確保するための定義を記述します。
// MyClass.cpp
#include "MyClass.h"
int MyClass::shared_value = 0; // 実体の定義
なお、C++17以降であれば、inline staticを使用することでヘッダーファイル内だけで定義を完結させることも可能です。
// C++17 以降の書き方
class MyClass {
public:
inline static int shared_value = 0;
};
原因5:テンプレートクラス・関数の実装場所
テンプレートは通常の関数とは異なり、コンパイル時にインスタンス化(具現化)されます。
そのため、コンパイラがテンプレートを使用する場所で、その実装内容(定義)を知っている必要があります。
失敗するパターン
テンプレートの宣言を.hに書き、実装を.cppに分けてしまうと、他のファイルからそのテンプレートを呼び出した際に、リンカーが実体を見つけられなくなります。
解決策
テンプレートの実装は、原則としてヘッダーファイル内に記述します。
// MyTemplate.h
template <typename T>
class MyTemplate {
public:
void doSomething(T value);
};
// ヘッダー内に実装を書く
template <typename T>
void MyTemplate<T>::doSomething(T value) {
// 処理
}
どうしても実装を分離したい場合は、.cppの末尾で「明示的インスタンス化」を行う手法もありますが、管理が複雑になるため、小規模なプロジェクトではヘッダー完結型が推奨されます。
原因6:仮想関数(vtable)に関する問題
クラスに仮想関数(virtual)が含まれている場合、リンカーはvtable(仮想関数テーブル)を作成します。
もし、純粋仮想関数ではない通常の仮想関数を宣言し、その実装を書き忘れていると、undefined reference to <code>vtable for ClassName'</code>というエラーが発生することがあります。
注意点
- 仮想デストラクタを宣言したが、実装を書いていない。
- 最初の非インライン仮想関数の定義がどこにも存在しない。
C++の言語仕様上、リンカーは特定の「鍵となる仮想関数」が定義されているファイルにvtableを出力しようとします。
そのため、一つでも実装漏れがあると、クラス全体のリンクが失敗する原因となります。
効率的なデバッグ手順
エラーが発生した際、闇雲にコードを書き換えるのではなく、以下の手順で原因を切り分けましょう。
1. シンボルの確認(nmコマンド)
オブジェクトファイルやライブラリの中に、目的のシンボルが含まれているかを確認します。
nm -C my_object.o | grep calculate
U(Undefined): 参照しているが未定義T(Text): 定義済み
もし自分の定義したはずの関数がUのままであれば、そのファイルが正しくコンパイルされていないか、定義の記述が間違っています。
2. ビルドログの精査
MakefileやCMakeの出力を見直し、意図した通りのパスからライブラリが読み込まれているかを確認してください。
特に複数のバージョンがあるライブラリ(OpenSSL 1.1と3.0など)が混在していると、予期せぬリンクエラーを引き起こすことがあります。
3. 名前空間の確認
関数が特定のnamespace内に定義されているのに、呼び出し側で名前空間を指定していない(あるいはその逆)場合も、別物として扱われます。
namespace utils {
void process() {}
}
// 呼び出し側
int main() {
process(); // エラー:utils::process() を探すべき
}
まとめ
C++におけるundefined reference toは、解決のヒントがメッセージの中に必ず隠されています。
エラーが発生した際は、まず「それは宣言の問題か、定義の問題か、それともリンクの設定か」を冷静に判断することが重要です。
本記事で紹介した主な原因を以下に整理します。
- 実装ファイルの不足:ビルドコマンドやCMakeLists.txtに必要なソースが含まれているか。
- プロトタイプの不一致:引数の型や
const、名前空間が宣言と定義で完全に一致しているか。 - ライブラリのリンク指定:
-lフラグやライブラリパスが正しいか。 - C互換性:C言語のライブラリを呼ぶ際に
extern "C"を使っているか。 - テンプレートの実装場所:テンプレートの実装がヘッダーファイルに書かれているか。
- 静的メンバの実体:クラス外での定義を忘れていないか。
これらのチェックリストを一つずつ確認していくことで、複雑に見えるリンクエラーも確実に解消できるはずです。
モダンなC++環境では、CMakeなどのツールがこれらを自動化してくれますが、根本的な仕組みを理解しておくことで、トラブルシューティングのスピードは飛躍的に向上します。
