プログラミング

C++のundefined referenceとLNK2019を直す方法

関数の宣言はあるのにリンクできない原因を、定義とビルド対象から解説。複数cppファイル、VS Codeの設定、練習問題と解答を紹介します。

コードの赤い波線は消えたのに、ビルドするとundefined reference。複数のファイルへ分け始めると出会いやすいエラーです。

まず確認するのは、呼び出した関数の中身が存在し、実行ファイルへ組み込まれているかです。今回はC++17以降の小さな計算プログラムで、宣言・定義・リンクのつながりを見ていきます。関数自体は関数の入門から確認できます。

undefined referenceはどの段階のエラー?

GNU系の開発環境では、例えば次のように表示されます。文言や記号は環境で異なります。

undefined reference to `add(int, int)'

Visual StudioのMSVCでは、似た問題がLNK2019: unresolved external symbolとして表示されます。どちらも、呼び出した関数などの実体を、部品をつなぐ段階で見つけられない場合に出るエラーです。Microsoft公式のLNK2019の説明

ビルドは、ソースコードから実行できるプログラムを作る一連の作業です。大まかには、各ソースを機械向けの部品にするコンパイルと、その部品をつなぐリンクに分けられます。部品になったファイルをオブジェクトファイル、つなぐ道具をリンカーと呼びます。GCCの処理段階

文法が正しく読めても、必要な部品がそろっているとは限りません。「どこで止まったか」を分けると、再インストールより先に見る場所が分かります。

1. 宣言と定義を3ファイルで見比べる

練習用フォルダへ、次の3ファイルを作ります。

calc.hは関数の形を知らせる

#ifndef CALC_H
#define CALC_H

int add(int a, int b);

#endif

int add(int a, int b);は宣言です。「整数を2つ受け取り、整数を返すaddがある」と知らせます。まだ足し算の処理は書いていません。前後の#ifndefなどは、同じヘッダーを重ねて読み込むのを防ぐ指定です。

calc.cppに実際の処理を書く

#include "calc.h"

int add(int a, int b) {
    return a + b;
}

こちらが定義です。{}の中に処理があります。calc.cpp自身も同じヘッダーを読み込み、宣言との食い違いに気付きやすくします。

main.cppから呼び出す

#include <iostream>
#include "calc.h"

int main() {
    std::cout << add(3, 5) << '\n';
    return 0;
}

main.cppは宣言を読んでいるので、呼び出しに必要な型が分かります。しかし、ヘッダーに書かれていないcalc.cppの中身まで、#include "calc.h"だけで自動的に組み込まれるわけではありません。

2. 定義を書いたcppもビルド対象へ入れる

GCCのC++用コマンドg++が使える環境で、次のようにmain.cppだけを指定すると、addの本体を見つけられません。

g++ -std=c++17 main.cpp -o app

calc.cppも加えます。

g++ -std=c++17 main.cpp calc.cpp -o app

ビルドに成功したら実行します。WindowsのPowerShellで実行ファイルがapp.exeなら、次です。

.\app.exe

macOSやLinuxでappなら、./appです。出力は8になります。作られるファイル名は環境の設定でも変わるため、ビルド先にできた名前を確認してください。

-oは出力名の指定、-std=c++17は使うC++の版の指定です。ヘッダーの.hを追加することより、定義を含む.cppを渡すことが今回の修正です。

分けてコンパイルすると何が見える?

次の形でも作れます。

g++ -std=c++17 -c main.cpp
g++ -std=c++17 -c calc.cpp
g++ main.o calc.o -o app

-cはリンクせず、オブジェクトファイルを作る指定です。最後の行でmain.oとcalc.oをつなぎます。calc.oを渡し忘れると、やはり定義が見つかりません。GCCのリンクオプション

#include "calc.cpp"に変えて回避するより、どのソースをビルドへ含めるかを整えましょう。ほかでも同じ.cppをビルドすると、今度は定義を二重に含める問題につながります。

3. 名前と引数の食い違いを確認する

ファイルを加えても直らないなら、エラーに出た関数名と引数を読みます。

確認点 食い違いの例
大文字・小文字 宣言はadd、定義はAdd
引数の型 宣言はadd(int, int)、定義はadd(double, double)
名前空間 宣言はcalc::add、定義は別の場所のadd
定義そのもの 宣言だけ書いて、処理本体がない

名前が同じでも、引数が違えば別の関数になり得ます。返り値だけを眺めるのではなく、エラー中のadd(int, int)と対応する定義を探してください。

外部ライブラリの関数で起こるなら、そのライブラリをリンクする設定も確認します。#includeは宣言を読む操作なので、それだけでライブラリ本体の指定まで済むとは限りません。

VS Codeの実行ボタンだけで失敗する場合

ターミナルでは2つの.cppを渡して成功するのに、実行ボタンだけ失敗するなら、ビルド用のタスクを確認します。

VS CodeのC++向け設定例には、引数に${file}を使うものがあります。これは今開いているファイルの指定なので、main.cppだけが対象になる場合があります。VS Code公式のMinGW設定

ビルド時に表示されたコマンドへ、必要な両方のファイルが含まれているか見比べてください。別の実行拡張機能を使っているなら、その拡張機能の設定も確認します。VS Code自体が、すべての.cppを自動的にまとめてくれるとは限りません。

ファイルが増えてきたら、CMakeなどのビルドシステムで構成を管理する方法もあります。まずは2ファイルの指定で動きを理解すると、設定の役割を追いやすくなります。

練習:showMessageの本体はどこ?

message.hでvoid showMessage();を宣言し、message.cppで次を定義しました。

#include <iostream>
#include "message.h"

void showMessage() {
    std::cout << "Hello\n";
}

main.cppは次の内容です。

#include "message.h"

int main() {
    showMessage();
}

g++ main.cpp -o appでshowMessage()のundefined referenceが出た場合、どう直しますか?

模範解答

g++ -std=c++17 main.cpp message.cpp -o app

解説

ヘッダーの宣言で呼び出し方は分かっても、処理本体はmessage.cppにあります。両方をビルドへ含め、実行するとHelloを表示できます。

エラーを読んだら、「宣言はあるか」「定義はあるか」「その定義がリンクへ参加しているか」の順に確認してみましょう。初めの2つの違いを復習したい場合は、関数を作る・宣言する基本へ戻れます。

← 記事一覧へ戻る