【C++】LNK2019「未解決の外部シンボル」とは?|原因7パターンをわざと再現して、エラー文の読み方と直し方を整理

【C++】LNK2019「未解決の外部シンボル」とは?|原因7パターンをわざと再現して、エラー文の読み方と直し方を整理 C++

コンパイルは通ったのに、ビルドの最後でLNK2019が出て、実行ファイルができません。エラー文には「未解決の外部シンボル」という言葉と、?add@@YAHHH@Zのような、暗号めいた文字列が並んでいます。

LNK2019は、リンカーが「呼ばれている関数や変数の実体」を、どこにも見つけられなかったときのエラーです。原因は1つではなく、よくあるものだけでも何通りもあります。そこで、MSVCで原因を7パターンわざと再現して、実際に出たエラー文と、直したあとの実行結果を並べました。エラー文のどこを読めば原因が絞れるのかも、実物で確かめています。

検証は、Visual Studio 2022付属のコンパイラ(MSVC 19.44、x64)を、コマンドラインのclで動かして行いました。Visual Studioのビルドでも、同じリンカーが同じ形式のエラー文を出します。

LNK2019とは?

LNK2019とは、

・コンパイルは通ったのに、リンカーが「呼ばれている関数や変数の実体」をどこにも見つけられなかったときに出るエラー

です。C++のビルドは、大きく2段階に分かれています。

  1. コンパイル:ソースファイル(.cpp)を1つずつ、機械語の部品(.obj)に変換する。このとき、他のファイルにある関数は「宣言」があれば呼び出してよい。実体はあとで誰かが探してくれる前提で、呼び出し部分には「ここにaddを入れてね」という穴を残す
  2. リンク:すべての.objとライブラリ(.lib)を集めて、穴を1つずつ埋めて実行ファイル(.exe)にする。穴に入れる実体がどこにも無いと、この段階で失敗する

つまり、LNK2019は「宣言はあったので、コンパイルは通した。でも実体が見つからなかった」という意味です。構文エラー(C2143など)のように、書き方が間違っているわけではありません。「実体が、リンクの対象に入っていない」ことを疑うエラーです。

エラー文の読み方

エラー文は、見慣れないと暗号のように見えます。後述する原因④(宣言と定義の型が違う)で実際に出たエラー文に、読み方を書き込みました。

実際に出たLNK2019のエラー文に、読み方の注釈をつけた画像
実際に出たエラー文。見るのは「どのシンボルが」「どこから参照されて」「ヒントがあるか」の3点
  • シンボル:見つからなかった関数や変数の名前。"int __cdecl add(int,int)"のように、戻り値と引数の型まで含めた名前で出る
  • 括弧の中の長い文字列:C++が内部で使う「装飾名」。型の情報が名前に埋め込まれている
  • 「関数 main で参照されました」:見つからない名前を、どこが呼んでいるか。ここを手がかりに、呼び出し側のコードを探せる
  • 「ヒント」:リンカーが、似た名前の定義を見つけたときだけ出る(出ないことも多い)

装飾名は、MSVCに付属するundnameで人間が読める形に戻せます。実際に3つ戻してみました。

Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "?add@@YAHHH@Z"
is :- "int __cdecl add(int,int)"

Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "?add@@YAHHN@Z"
is :- "int __cdecl add(int,double)"

Microsoft (R) C++ Name Undecorator
Copyright (C) Microsoft Corporation. All rights reserved.

Undecoration of :- "??$maxOf@H@@YAHHH@Z"
is :- "int __cdecl maxOf<int>(int,int)"

?add@@YAHHH@Zがint add(int,int)、?add@@YAHHN@Zがint add(int,double)に戻りました。C++では、関数名に引数の型が埋め込まれます。だから、型が違う関数は、別の名前の別物として扱われます。この性質が、後述する原因④の正体です。

原因を7パターン再現する

ここからは、原因を1つずつ、わざと起こして確かめます。それぞれ「壊したコード」「出たエラー」「直し方と実行結果」の順です。

①宣言だけ書いて、定義を書いていない

もっとも基本の原因です。関数の宣言だけがあって、実体がどこにもありません。

▼main.cpp
#include <cstdio>

int add(int a, int b);          // 宣言だけ。定義はどこにも書いていない

int main() {
    std::printf("%d\n", add(1, 2));
    return 0;
}
main.cpp
main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl add(int,int)" (?add@@YAHHH@Z) が関数 main で参照されました
main.exe : fatal error LNK1120: 1 件の未解決の外部参照

エラー文のシンボルint add(int,int)が、そのまま「実体が無い関数」です。直すには、どこかに定義を書きます。

▼util.cpp(追加した)
int add(int a, int b) {
    return a + b;
}

cl main.cpp util.cppのように、定義を書いたファイルも一緒にビルドすると、通りました。

3

②定義を書いたcppを、ビルドに含めていない

定義はちゃんと書いてあるのに、そのファイルをビルドの対象にしていないパターンです。clで直接ビルドしているときは、コマンドにファイル名を書き忘れます。

▼main.cpp
#include <cstdio>
#include "util.h"

int main() {
    std::printf("%d\n", add(1, 2));
    return 0;
}
▼util.cpp(定義はここにある)
#include "util.h"

int add(int a, int b) {
    return a + b;
}
cl main.cpp                  ← util.cpp が入っていない
main.cpp
main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl add(int,int)" (?add@@YAHHH@Z) が関数 main で参照されました
main.exe : fatal error LNK1120: 1 件の未解決の外部参照

①とまったく同じ文面でした。エラー文だけでは、「定義を書き忘れた」のか「書いたのにビルドに入っていない」のか区別できません。Visual Studioでは、プロジェクトのソースファイル一覧に、そのcppが入っているかを確認します(プロジェクト外のフォルダに置いただけのファイルは、ビルドされません)。直すには、cl main.cpp util.cppのように、ファイルを加えます。

3

③ライブラリを指定していない

自分のコードではなく、Windows APIなどの外部ライブラリの関数を使うときに出るパターンです。例として、前に書いたSleepの精度の記事でも使うtimeBeginPeriodを呼びます。この関数の実体は、winmm.libというライブラリの中にあります。

▼main.cpp
#include <windows.h>
#include <mmsystem.h>
#include <cstdio>

int main() {
    timeBeginPeriod(1);          // 実体は winmm.lib の中にある
    std::printf("ok\n");
    timeEndPeriod(1);
    return 0;
}
main.cpp
main.obj : error LNK2019: 未解決の外部シンボル __imp_timeBeginPeriod が関数 main で参照されました
main.obj : error LNK2019: 未解決の外部シンボル __imp_timeEndPeriod が関数 main で参照されました
main.exe : fatal error LNK1120: 2 件の未解決の外部参照

シンボル名の頭に__imp_が付いています。これは「DLLから取り込む関数」という印です。外部ライブラリの関数で、この__imp_が付いたLNK2019が出たら、ライブラリ(.lib)の指定漏れを疑います。直し方は2通りあります。

cl main.cpp winmm.lib
▼方法2: コードの中で指定する(main_pragma.cpp)
#include <windows.h>
#include <mmsystem.h>
#include <cstdio>

#pragma comment(lib, "winmm.lib")   // ライブラリをコードの中から指定する

int main() {
    timeBeginPeriod(1);          // 実体は winmm.lib の中にある
    std::printf("ok\n");
    timeEndPeriod(1);
    return 0;
}
ok

Visual Studioなら、プロジェクトのプロパティの「リンカー → 入力 → 追加の依存ファイル」にwinmm.libを足すのが、一般的な方法です。どのライブラリに関数が入っているかは、MicrosoftのAPIドキュメントの「要件」欄に書かれています。

④宣言と定義で、引数の型が違う

一番見つけにくいのが、これです。ヘッダーではint add(int, int)と宣言しているのに、cppではint add(int, double)と書いてしまいました。

▼util.h
#pragma once
int add(int a, int b);          // int, int で宣言
▼util.cpp(引数の型が違う)
#include "util.h"

int add(int a, double b) {      // こちらは int, double で定義(別の関数になってしまう)
    return a + (int)b;
}
main.cpp
util.cpp
コードを生成中...
main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl add(int,int)" (?add@@YAHHH@Z) が関数 main で参照されました
  定義済みの一致する可能性があるシンボルに関するヒント:
    "int __cdecl add(int,double)" (?add@@YAHHN@Z)
main.exe : fatal error LNK1120: 1 件の未解決の外部参照

今回はユーティリティのファイルもビルドに入れています。それでもint add(int,int)が見つかりません。なぜなら、C++では引数の型が違う関数は、別の関数として扱われるからです。cppで定義したのはadd(int,double)であって、ヘッダーで宣言したadd(int,int)は、どこにも定義されていません。

今回は、リンカーが「ヒント」としてadd(int,double)を示してくれました。ヒントが出ないときは、dumpbinで.objの中身を直接見ます。

cl /c util.cpp
dumpbin /symbols util.obj | findstr add
009 00000000 SECT3  notype ()    External     | ?add@@YAHHN@Z (int __cdecl add(int,double))

このobjには、add(int,double)しか入っていません。呼び出し側が探しているadd(int,int)とは、型が違うことが分かります。直すには、型をヘッダーとそろえます。

▼util.cpp(直した)
#include "util.h"

int add(int a, int b) {      // ヘッダーと同じ引数の型にそろえる
    return a + b;
}
3

⑤テンプレートの定義を、cppに書いた

テンプレート関数の定義を、通常の関数と同じ感覚でcppに書いて、ヘッダーには宣言だけを置いたパターンです。

▼maxof.h
#pragma once
template <typename T>
T maxOf(T a, T b);              // 宣言だけをヘッダーに置いている
▼maxof.cpp
#include "maxof.h"

template <typename T>
T maxOf(T a, T b) {             // 定義はcppの中。ここでは誰もmaxOf<int>を使っていない
    return a > b ? a : b;
}
main.cpp
maxof.cpp
コードを生成中...
main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl maxOf<int>(int,int)" (??$maxOf@H@@YAHHH@Z) が関数 main で参照されました
main.exe : fatal error LNK1120: 1 件の未解決の外部参照

エラー文のシンボルはmaxOf<int>(int,int)です。テンプレートは、使われた型ごとに、使う側のファイルで中身を生成する仕組みです。maxof.cppの中では、誰もmaxOf<int>を使っていないので、int版が作られません。main.cppからは、定義が見えないので、作れません。結果、int版は、どこにも存在しません。直し方は、定義ごとヘッダーに置くことです。

▼maxof.h(直した)
#pragma once
template <typename T>
T maxOf(T a, T b) {             // テンプレートは定義ごとヘッダーに置く
    return a > b ? a : b;
}
7

⑥staticメンバ変数の実体を、書いていない

クラスのstaticメンバ変数は、クラスの中に書くのは「宣言」で、実体はクラスの外に1回だけ書く必要があります。

▼main.cpp
#include <cstdio>

class Counter {
public:
    static int count;           // 宣言だけ。クラスの外に定義が必要
    static void Up() { count++; }
};

int main() {
    Counter::Up();
    std::printf("%d\n", Counter::count);
    return 0;
}
main.cpp
main.obj : error LNK2019: 未解決の外部シンボル "public: static int Counter::count" (?count@Counter@@2HA) が関数 main で参照されました
main.exe : fatal error LNK1120: 1 件の未解決の外部参照

staticメンバ変数なので、LNK2001という別のエラー番号が出ると予想していました。ところが、実際に出たのはLNK2019でした。関数から参照している変数が見つからないときも、LNK2019になるようです。グローバル変数をexternで宣言して実体を書かなかったときも、同じでした。

main.cpp
main.obj : error LNK2019: 未解決の外部シンボル "int g_score" (?g_score@@3HA) が関数 main で参照されました
main.exe : fatal error LNK1120: 1 件の未解決の外部参照

直し方は、クラスの外に実体を1回だけ書くことです。

▼main2.cpp(追加した行)
int Counter::count = 0;           // クラスの外で実体を1回だけ定義する
1

⑦仮想関数の定義を、書き忘れた

最後は、ちょっと意地悪なパターンです。クラスのvirtual関数を宣言して、定義を書き忘れました。この関数は、どこからも呼んでいません。

▼main.cpp
#include <cstdio>

class Enemy {
public:
    virtual void Update();      // 宣言だけ。定義を書き忘れている
};

int main() {
    Enemy e;
    std::printf("ok\n");
    return 0;
}
main.cpp
main.obj : error LNK2001: 外部シンボル "public: virtual void __cdecl Enemy::Update(void)" (?Update@Enemy@@UEAAXXZ) は未解決です
main.exe : fatal error LNK1120: 1 件の未解決の外部参照

呼んでいないのに、リンクエラーになりました。仮想関数は、使わなくても、クラスの「仮想関数テーブル」に載るので、実体が必要になるからです。しかも、エラー番号が今までのLNK2019ではなくLNK2001で、文面も「関数 main で参照されました」ではなく「未解決です」だけでした。今回の環境では、「関数の呼び出し」から参照されたものがLNK2019、仮想関数テーブルのような、関数の外からの参照がLNK2001でした。直し方は、定義を書くことです(派生クラスで上書きするなら、基底クラスは= 0の純粋仮想にする手もあります)。

▼main2.cpp(追加した定義)
void Enemy::Update() {           // 定義を書く
    std::printf("update\n");
}
update
ok

【重要】再現して分かった罠:①と②は、エラー文が完全に同じ

7つ再現して、いちばん困ったのは、原因が違うのに、エラー文が一字一句同じことでした。①(定義を書いていない)と②(定義はあるが、ビルドに入っていない)は、どちらも次の文面です。

main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl add(int,int)" (?add@@YAHHH@Z) が関数 main で参照されました

エラー文を見て「定義を書いていないんだな」と思い込んで、ソースの中を探し続けても、見つかりません。②は、ソースをいくら見ても、定義があるからです。

そういうときは、次の順で疑うと早いです。

  1. そのシンボルの定義が、ソース全体のどこかにあるかを検索する(なければ①)
  2. あるなら、そのファイルが、ビルドの対象に入っているかを確認する(入っていなければ②)
  3. 入っているなら、引数の型やconstが、宣言とそろっているかを見る(④)。ヒントが出なければdumpbinで.objの中身を直接見る

エラー文から、原因を絞り込む早見表

エラー文の特徴まず疑うこと該当する原因
自分で書いた関数が見つからない定義を書いていない/cppがビルドに入っていない①②
シンボル名の頭に__imp_が付いているライブラリ(.lib)の指定漏れ③
「ヒント」に、引数の違う同名の関数が出る宣言と定義で、引数の型やconstが違う④
シンボル名に<int>のような型があるテンプレートの定義が、ヘッダーに無い⑤
public: staticと出るクラスのstaticメンバの実体を書いていない⑥
LNK2001で「呼んでいないのに」出る仮想関数の定義を書き忘れた⑦
今回再現した範囲での目安。原因は他にもあるので、あくまで最初に疑う順番として使う

LNK2019のよくある失敗例と対処法

①エラー文を見て、すぐ呼び出し側のコードを直そうとする

LNK2019は、呼び出し側が間違っているわけではなく、実体が足りないことを伝えています。呼び出し側のコードをいじる前に、「実体はどこにあるはずか」を探します。

②ビルドに入っているはずだと思い込む

特に、新しくcppを追加した直後に起きやすい原因です。ファイルを足したら、ビルド対象に入っているかを、先に確認します。

③型違いに気づかず、何度もビルドし直す

引数の型やconst、参照の有無が1つ違うだけで、別の関数になります。ヒントが出なくても、undnameとdumpbinで、「探されている名前」と「定義されている名前」を並べて比べると、一瞬で分かります。

注意点

  • LNK2019は「実体が、リンクの対象に無い」というエラー。構文ミスではない
  • ①と②は同じ文面になる。「定義があるか」「ビルドに入っているか」を順に確認する
  • 外部ライブラリの関数で__imp_が付いたら、.libの指定漏れ
  • C++は引数の型が違うと別の関数になる。型やconstの食い違いはdumpbinで確認できる
  • テンプレートは、定義ごとヘッダーに置く
  • 今回は、MSVC 19.44(Visual Studio 2022付属)で確認した。エラー番号や文面は、コンパイラや版で変わりうる

この記事のポイント

  • LNK2019は、リンクの段階で実体が見つからないときのエラー。コンパイルが通るのは、宣言があるから
  • エラー文の「シンボル」「参照元の関数」「ヒント」の3点を読む。装飾名はundnameで戻せる
  • 原因は7通り再現した。①定義がない ②ビルドに入っていない ③.libの指定漏れ ④型違い ⑤テンプレート ⑥staticメンバ ⑦仮想関数
  • ①と②は文面が同じ。ソースを探し続ける前に、ビルド対象を確認する

LNK2019は、エラー文の見た目が強烈なので身構えてしまいますが、読む場所は決まっています。実際に壊してみると、「このエラーが出たら、この順で見る」という流れが、はっきり見えました。

あわせて読みたい