关于C++:什么是未定义的引用/未解决的外部符号错误,以及如何修复它?

What is an undefined reference/unresolved external symbol error and how do I fix it?

什么是未定义的引用/未解析的外部符号错误?常见原因是什么?如何解决/预防?

随意编辑/添加您自己的。


编译C++程序在几个步骤中进行,如2.2所指定的(参考Keith Thompson的学分):

The precedence among the syntax rules of translation is specified by the following phases [see footnote].

  • Physical source file characters are mapped, in an implementation-defined manner, to the basic source character set
    (introducing new-line characters for end-of-line indicators) if
    necessary. [SNIP]
  • Each instance of a backslash character (\) immediately followed by a new-line character is deleted, splicing physical source lines to
    form logical source lines. [SNIP]
  • The source file is decomposed into preprocessing tokens (2.5) and sequences of white-space characters (including comments). [SNIP]
  • Preprocessing directives are executed, macro invocations are expanded, and _Pragma unary operator expressions are executed. [SNIP]
  • Each source character set member in a character literal or a string literal, as well as each escape sequence and universal-character-name
    in a character literal or a non-raw string literal, is converted to
    the corresponding member of the execution character set; [SNIP]
  • Adjacent string literal tokens are concatenated.
  • White-space characters separating tokens are no longer significant. Each preprocessing token is converted into a token. (2.7). The
    resulting tokens are syntactically and semantically analyzed and
    translated as a translation unit. [SNIP]
  • Translated translation units and instantiation units are combined as follows: [SNIP]
  • All external entity references are resolved. Library components are linked to satisfy external references to entities not defined in the
    current translation. All such translator output is collected into a
    program image which contains information needed for execution in its
    execution environment. (emphasis mine)
  • [footnote] Implementations must behave as if these separate phases occur, although in practice different phases might be folded together.

    指定的错误发生在编译的最后一个阶段,通常称为链接。它基本上意味着您将一组实现文件编译成对象文件或库,现在您希望让它们一起工作。

    假设您在a.cpp中定义了符号a。现在,b.cpp宣布并使用了这个符号。在链接之前,它只是假设符号是在某个地方定义的,但它还不关心在哪里。链接阶段负责找到符号并将其正确链接到b.cpp(实际上,是到使用它的对象或库)。

    如果您使用的是Microsoft Visual Studio,您将看到项目生成.lib文件。其中包含一个导出符号表和一个导入符号表。导入的符号将根据链接的库进行解析,导出的符号将提供给使用该.lib的库(如果有)。

    其他编译器/平台也有类似的机制。

    常见的错误消息有:用于Microsoft Visual Studio的error LNK2001error LNK1120error LNK2019、用于gcc的undefined reference to符号名。

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    struct X
    {
       virtual void foo();
    };
    struct Y : X
    {
       void foo() {}
    };
    struct A
    {
       virtual ~A() = 0;
    };
    struct B: A
    {
       virtual ~B(){}
    };
    extern int x;
    void foo();
    int main()
    {
       x = 0;
       foo();
       Y y;
       B b;
    }

    将使用GCC生成以下错误:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /home/AbiSfw/ccvvuHoX.o: In function `main':
    prog.cpp:(.text+0x10): undefined reference to `x'

    prog.cpp:(.text+0x19): undefined reference to `foo()'
    prog.cpp:(.text+0x2d): undefined reference to `A::~A()'

    /home/AbiSfw/ccvvuHoX.o: In function `B::~B()':
    prog.cpp:(.text._ZN1BD1Ev[B::~B()]+0xb): undefined reference to `A::~A()'

    /home/AbiSfw/ccvvuHoX.o: In function `B::~B()':
    prog.cpp:(.text._ZN1BD0Ev[B::~B()]+0x12): undefined reference to `A::~A()'

    /home/AbiSfw/ccvvuHoX.o:(.rodata._ZTI1Y[typeinfo for Y]+0x8): undefined reference to `typeinfo for X'
    /home/AbiSfw/ccvvuHoX.o:(.rodata._ZTI1B[typeinfo for B]+0x8): undefined reference to `typeinfo for A'

    collect2: ld returned 1 exit status

    以及与Microsoft Visual Studio类似的错误:

    1
    2
    3
    4
    5
    1>test2.obj : error LNK2001: unresolved external symbol"void __cdecl foo(void)" (?foo@@YAXXZ)
    1>test2.obj : error LNK2001: unresolved external symbol"int x" (?x@@3HA)
    1>test2.obj : error LNK2001: unresolved external symbol"public: virtual __thiscall A::~A(void)" (??1A@@UAE@XZ)
    1>test2.obj : error LNK2001: unresolved external symbol"public: virtual void __thiscall X::foo(void)" (?foo@X@@UAEXXZ)
    1>...\test2.exe : fatal error LNK1120: 4 unresolved externals

    常见原因包括:

    • 未能链接到适当的库/对象文件或编译实现文件
    • 已声明和未定义的变量或函数。
    • 类类型成员的常见问题
    • 模板实现不可见。
    • 符号被定义在C程序中,并用在C++代码中。
    • 跨模块/dll错误地导入/导出方法/类。(MSV专用)
    • 循环库依赖项
    • 未定义对"winmain@16"的引用
    • 相互依赖的库顺序
    • 多个同名源文件
    • 使用#pragma时输入错误或不包括.lib扩展名(Microsoft Visual Studio)
    • 模板好友的问题
    • UNICODE定义不一致


    班级成员:纯virtual析构函数需要实现。

    声明析构函数pure仍然需要定义它(与常规函数不同):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct X
    {
        virtual ~X() = 0;
    };
    struct Y : X
    {
        ~Y() {}
    };
    int main()
    {
        Y y;
    }
    //X::~X(){} //uncomment this line for successful definition

    这是因为在隐式销毁对象时调用基类析构函数,因此需要定义。

    virtual方法必须实现或定义为纯方法。

    这类似于无定义的非virtual方法,并添加了如下推理:pure声明生成一个虚拟vtable,您可能会在不使用函数的情况下得到链接器错误:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct X
    {
        virtual void foo();
    };
    struct Y : X
    {
       void foo() {}
    };
    int main()
    {
       Y y; //linker error although there was no call to X::foo
    }

    为此,宣布X::foo()为纯:

    1
    2
    3
    4
    struct X
    {
        virtual void foo() = 0;
    };

    virtual级成员

    有些成员需要定义,即使没有明确使用:

    1
    2
    3
    4
    struct A
    {
        ~A();
    };

    以下将产生错误:

    1
    A a;      //destructor undefined

    实现可以在类定义本身中内联:

    1
    2
    3
    4
    struct A
    {
        ~A() {}
    };

    或外部:

    1
    A::~A() {}

    如果实现在类定义之外,但在头中,则必须将方法标记为inline,以防止出现多个定义。

    如果使用,则需要定义所有使用的成员方法。

    一个常见的错误是忘记限定名称:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct A
    {
       void foo();
    };

    void foo() {}

    int main()
    {
       A a;
       a.foo();
    }

    定义应该是

    1
    void A::foo() {}

    static数据成员必须在单个翻译单元的类外部定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct X
    {
        static int x;
    };
    int main()
    {
        int x = X::x;
    }
    //int X::x; //uncomment this line to define X::x

    可以为类定义中的整型或枚举型的staticconst数据成员提供初始值设定项;但是,使用此成员的ODR仍然需要如上所述的命名空间范围定义。C++ 11允许对所有EDCOX1和9个数据成员进行类内初始化。


    未能链接到适当的库/对象文件或编译实现文件

    通常,每个翻译单元将生成一个对象文件,其中包含该翻译单元中定义的符号定义。要使用这些符号,必须链接到这些对象文件。

    在gcc下,您将在命令行中指定要链接在一起的所有对象文件,或者一起编译实现文件。

    1
    g++ -o test objectFile1.o objectFile2.o -lLibraryName

    这里的libraryName只是库的裸名称,没有特定于平台的添加。例如,在Linux上,库文件通常称为libfoo.so,但您只写-lfoo。在Windows上,同一个文件可能称为foo.lib,但您将使用相同的参数。您可能需要添加使用-L?directory?可以找到这些文件的目录。确保不要在-l-l后面写空格。

    对于Xcode:添加用户头搜索路径->添加库搜索路径->将实际的库引用拖放到项目文件夹中。

    在MSV下,添加到项目中的文件会自动将它们的对象文件链接在一起,并且会生成一个lib文件(在通常情况下)。要在单独的项目中使用这些符号,您需要需要在项目设置中包含lib文件。这是在Input -> Additional Dependencies中项目属性的链接器部分完成的。(到lib文件的路径应该是在Linker -> General -> Additional Library Directories中添加,当使用提供lib文件的第三方库时,如果不这样做通常会导致错误。

    也可能会忘记将文件添加到编译中,在这种情况下,不会生成对象文件。在gcc中,您将把文件添加到命令行中。在MSV中,将文件添加到项目将使其自动编译(尽管文件可以手动从构建中单独排除)。

    在Windows编程中,没有链接必要库的标志是未解析符号的名称以__imp_开头。在文档中查找函数的名称,它应该说明需要使用哪个库。例如,msdn将信息放入名为"library"的部分中每个函数底部的一个框中。


    已声明但未定义变量或函数。

    典型的变量声明是

    1
    extern int x;

    因为这只是一个声明,所以只需要一个定义。相应的定义是:

    1
    int x;

    例如,下面将生成一个错误:

    1
    2
    3
    4
    5
    6
    extern int x;
    int main()
    {
        x = 0;
    }
    //int x; // uncomment this line for successful definition

    类似的注释适用于功能。在不定义函数的情况下声明函数会导致错误:

    1
    2
    3
    4
    5
    6
    void foo(); // declaration only
    int main()
    {
       foo();
    }
    //void foo() {} //uncomment this line for successful definition

    请注意,您实现的函数与您声明的函数完全匹配。例如,您可能有不匹配的CV限定符:

    1
    2
    3
    4
    5
    6
    7
    8
    void foo(int& x);
    int main()
    {
       int x;
       foo(x);
    }
    void foo(const int& x) {} //different function, doesn't provide a definition
                              //for void foo(int& x)

    其他不匹配的例子包括

    • 函数/变量在一个命名空间中声明,在另一个命名空间中定义。
    • 声明为类成员的函数/变量,定义为全局(或相反)。
    • 函数返回类型、参数编号和类型以及调用约定并不完全一致。

    来自编译器的错误消息通常会给出已声明但从未定义的变量或函数的完整声明。将它与您提供的定义进行比较。确保每个细节都匹配。


    指定相互依赖的链接库的顺序错误。

    如果图书馆相互依赖,那么图书馆链接的顺序确实很重要。一般来说,如果库A依赖库B,那么libA必须出现在链接器标志中的libB之前。

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // B.h
    #ifndef B_H
    #define B_H

    struct B {
        B(int);
        int x;
    };

    #endif

    // B.cpp
    #include"B.h"
    B::B(int xx) : x(xx) {}

    // A.h
    #include"B.h"

    struct A {
        A(int x);
        B b;
    };

    // A.cpp
    #include"A.h"

    A::A(int x) : b(x) {}

    // main.cpp
    #include"A.h"

    int main() {
        A a(5);
        return 0;
    };

    创建库:

    1
    2
    3
    4
    5
    6
    7
    8
    $ g++ -c A.cpp
    $ g++ -c B.cpp
    $ ar rvs libA.a A.o
    ar: creating libA.a
    a - A.o
    $ ar rvs libB.a B.o
    ar: creating libB.a
    a - B.o

    编译:

    1
    2
    3
    4
    5
    6
    $ g++ main.cpp -L. -lB -lA
    ./libA.a(A.o): In function `A::A(int)':
    A.cpp:(.text+0x1c): undefined reference to `B::B(int)'

    collect2: error: ld returned 1 exit status
    $ g++ main.cpp -L. -lA -lB
    $ ./a.out

    所以再重复一遍,顺序很重要!


    什么是"未定义的引用/未解析的外部符号"

    我将尝试解释什么是"未定义的引用/未解析的外部符号"。

    note: i use g++ and Linux and all examples is for it

    例如,我们有一些代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // src1.cpp
    void print();

    static int local_var_name; // 'static' makes variable not visible for other modules
    int global_var_name = 123;

    int main()
    {
        print();
        return 0;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // src2.cpp
    extern"C" int printf (const char*, ...);

    extern int global_var_name;
    //extern int local_var_name;

    void print ()
    {
        // printf("%d%d
    ", global_var_name, local_var_name);
        printf("
    %d
    ", global_var_name);
    }

    生成对象文件

    1
    2
    $ g++ -c src1.cpp -o src1.o
    $ g++ -c src2.cpp -o src2.o

    在汇编程序阶段之后,我们有一个对象文件,其中包含要导出的任何符号。看看这些符号

    1
    2
    3
    4
    $ readelf --symbols src1.o
      Num:    Value          Size Type    Bind   Vis      Ndx Name
         5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 _ZL14local_var_name # [1]
         9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_var_name     # [2]

    我拒绝了一些输出行,因为它们无关紧要

    所以,我们看到跟随符号导出。

    1
    2
    [1] - this is our static (local) variable (important - Bind has a type"LOCAL")
    [2] - this is our global variable

    src2.cpp不输出任何内容,我们没有看到它的符号

    链接我们的对象文件

    1
    $ g++ src1.o src2.o -o prog

    然后运行它

    1
    2
    $ ./prog
    123

    链接器看到导出的符号并链接它。现在我们尝试取消对src2.cpp中的行的注释,如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // src2.cpp
    extern"C" int printf (const char*, ...);

    extern int global_var_name;
    extern int local_var_name;

    void print ()
    {
        printf("%d%d
    "
    , global_var_name, local_var_name);
    }

    并重新生成对象文件

    1
    $ g++ -c src2.cpp -o src2.o

    好的(没有错误),因为我们只构建对象文件,链接还没有完成。尝试链接

    1
    2
    3
    4
    $ g++ src1.o src2.o -o prog
    src2.o: In function `print()':
    src2.cpp:(.text+0x6): undefined reference to `local_var_name'

    collect2: error: ld returned 1 exit status

    发生这种情况是因为我们的本地变量名是静态的,即它对于其他模块不可见。现在更深入。获取转换相位输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    $ g++ -S src1.cpp -o src1.s

    // src1.s
    look src1.s

        .file  "src1.cpp"
        .local  _ZL14local_var_name
        .comm   _ZL14local_var_name,4,4
        .globl  global_var_name
        .data
        .align 4
        .type   global_var_name, @object
        .size   global_var_name, 4
    global_var_name:
        .long   123
        .text
        .globl  main
        .type   main, @function
    main:
    ; assembler code, not interesting for us
    .LFE0:
        .size   main, .-main
        .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
        .section    .note.GNU-stack,"",@progbits

    所以,我们已经看到没有本地变量名的标签,这就是为什么链接器没有找到它的原因。但我们是黑客:)我们可以修复它。在文本编辑器中打开src1.s并更改

    1
    2
    .local  _ZL14local_var_name
    .comm   _ZL14local_var_name,4,4

    1
    2
    3
    4
    5
    6
    7
        .globl  local_var_name
        .data
        .align 4
        .type   local_var_name, @object
        .size   local_var_name, 4
    local_var_name:
        .long   456789

    也就是说,你应该有如下的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
        .file  "src1.cpp"
        .globl  local_var_name
        .data
        .align 4
        .type   local_var_name, @object
        .size   local_var_name, 4
    local_var_name:
        .long   456789
        .globl  global_var_name
        .align 4
        .type   global_var_name, @object
        .size   global_var_name, 4
    global_var_name:
        .long   123
        .text
        .globl  main
        .type   main, @function
    main:
    ; ...

    我们已经更改了本地变量名的可见性,并将其值设置为456789。尝试从中生成对象文件

    1
    $ g++ -c src1.s -o src2.o

    好的,请参见readelf输出(符号)

    1
    2
    $ readelf --symbols src1.o
    8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 local_var_name

    现在本地变量名具有bind global(was local)

    链接

    1
    $ g++ src1.o src2.o -o prog

    然后运行它

    1
    2
    $ ./prog
    123456789

    好吧,我们破解它:)

    因此,当链接器在对象文件中找不到全局符号时,就会发生"未定义的引用/未解析的外部符号错误"。


    符号被定义在C程序中,并用在C++代码中。

    在C程序中定义了函数(或变量)EDCOX1(12),并尝试在C++程序中使用它。

    1
    2
    3
    4
    5
    void foo();
    int main()
    {
        foo();
    }

    C++链接器希望名称被篡改,因此必须声明函数为:

    1
    2
    3
    4
    5
    extern"C" void foo();
    int main()
    {
        foo();
    }

    等价地,不是在C程序中定义,函数(或变量)EDCOX1 OR 12是用C++定义的,但用C链接定义:

    1
    extern"C" void foo();

    并且尝试在C++程序中使用C++链接。

    如果一个完整的库包含在一个头文件中(并被编译为C代码),那么这个包含将需要如下所示:

    1
    2
    3
    extern"C" {
        #include"cheader.h"
    }


    如果所有其他操作都失败,请重新编译。

    我最近通过重新编译有问题的文件,消除了Visual Studio 2012中未解决的外部错误。当我重新构建时,错误就消失了。

    当两个(或更多)库具有循环依赖关系时,通常会发生这种情况。库A尝试使用b.lib中的符号,库B尝试使用a.lib中的符号。两者都不存在。当您试图编译时,链接步骤将失败,因为它找不到b.lib。将生成一个.lib,但没有dll。然后编译b,这将成功并生成b.lib。重新编译A现在可以工作了,因为现在找到了B.lib。


    跨模块/dll错误地导入/导出方法/类(特定于编译器)。

    MSV要求您使用__declspec(dllexport)__declspec(dllimport)指定要导出和导入的符号。

    这种双重功能通常通过使用宏获得:

    1
    2
    3
    4
    5
    #ifdef THIS_MODULE
    #define DLLIMPEXP __declspec(dllexport)
    #else
    #define DLLIMPEXP __declspec(dllimport)
    #endif

    THIS_MODULE只能在导出函数的模块中定义。这样,声明:

    1
    DLLIMPEXP void foo();

    扩展到

    1
    __declspec(dllexport) void foo();

    并告诉编译器导出函数,因为当前模块包含其定义。当将声明包含在不同的模块中时,它将扩展到

    1
    __declspec(dllimport) void foo();

    并告诉编译器该定义位于您所链接的某个库中(另请参见1)。

    您可以类似地导入/导出类:

    1
    2
    3
    class DLLIMPEXP X
    {
    };


    这是每个VC++程序员一次又一次看到的最令人困惑的错误消息之一。我们先把事情弄清楚。

    A.什么是符号?简而言之,符号就是名字。它可以是一个变量名、函数名、类名、TyWIFF名称,或者除了属于C++语言的那些名称和符号之外的任何东西。它是由依赖库(另一个用户定义的)用户定义或引入的。

    B.什么是外部的?在VC++中,每个源文件(.cpp,.c等)都被视为翻译单元,编译器一次编译一个单元,并为当前翻译单元生成一个对象文件(.obj)。(请注意,包含此源文件的每个头文件都将进行预处理,并被视为此翻译单元的一部分)翻译单元中的所有内容都被视为内部内容,其他所有内容都被视为外部内容。在C++中,可以使用EDCOX1、0、EDCOX1、1等关键字来引用外部符号。

    C.什么是"决心"?解析是一个链接时间项。在链接时间内,链接器尝试查找对象文件中无法在内部找到其定义的每个符号的外部定义。此搜索过程的范围包括:

    • 编译时生成的所有对象文件
    • 显式或隐式的所有库(.lib)指定为此建筑应用程序的附加依赖项。

    这个搜索过程称为解析。

    D.最后,为什么没有解决的外部符号?如果链接器找不到内部没有定义的符号的外部定义,则它将报告未解决的外部符号错误。

    e.LNK2019的可能原因:未解决的外部符号错误。我们已经知道这个错误是由于链接器找不到外部符号的定义,可能的原因可以分为:

  • 定义存在
  • 例如,如果我们在a.cpp中定义了一个名为foo的函数:

    1
    2
    3
    4
    int foo()
    {
        return 0;
    }

    在b.cpp中,我们希望调用函数foo,因此我们添加

    1
    void foo();

    要声明函数foo(),并在另一个函数体中调用它,例如bar()

    1
    2
    3
    4
    void bar()
    {
        foo();
    }

    现在,当您构建此代码时,您将收到一个lnk2019错误,抱怨foo是一个未解决的符号。在本例中,我们知道foo()的定义在a.cpp中,但与我们调用的定义不同(返回值不同)。这就是定义存在的情况。

  • 定义不存在
  • 如果我们想调用库中的某些函数,但是导入库没有添加到项目设置的附加依赖项列表(从:Project | Properties | Configuration Properties | Linker | Input | Additional Dependency设置)中。现在链接器将报告一个lnk2019,因为当前搜索范围中不存在该定义。


    模板实现不可见。

    非特定模板的定义必须对使用它们的所有翻译单位可见。这意味着您不能分离模板的定义到实现文件。如果必须分离实现,通常的解决方法是在头的末尾包含一个impl文件,声明模板。常见的情况是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    template<class T>
    struct X
    {
        void foo();
    };

    int main()
    {
        X<int> x;
        x.foo();
    }

    //differentImplementationFile.cpp
    template<class T>
    void X<T>::foo()
    {
    }

    要解决这个问题,您必须将X::foo的定义移动到头文件或使用它的翻译单元可见的某个位置。

    专用模板可以在实现文件中实现,并且实现不必可见,但必须事先声明专用化。

    有关进一步的解释和其他可能的解决方案(显式实例化),请参阅此问题和答案。


    未定义的对WinMain@16的引用或类似的"异常"main()入口点引用(特别是对于Visual Studio)。

    您可能错过了用实际的IDE选择正确的项目类型。IDE可能希望将Windows应用程序项目绑定到这样的入口点函数(如上面缺失的引用中所指定的),而不是常用的int main(int argc, char** argv);签名。

    如果您的IDE支持纯控制台项目,您可能希望选择此项目类型,而不是Windows应用程序项目。

    下面是从实际问题中更详细地处理的案例1和案例2。


    另外,如果您使用的是第三方库,请确保您有正确的32/64位二进制文件


    微软提供了一个#pragma来在链接时引用正确的库;

    1
    #pragma comment(lib,"libname.lib")

    除了包含库目录的库路径之外,这应该是库的全名。


    需要为新的工具集版本更新Visual Studio Nuget包

    我只是在尝试将libpng与Visual Studio 2013链接时遇到了这个问题。问题是包文件只有Visual Studio 2010和2012的库。

    正确的解决方案是希望开发人员发布一个更新的包,然后进行升级,但是它通过为VS2013提供一个额外的设置(指向VS2012库文件)来为我工作。

    我通过查找packagename\build
    ative\packagename.targets
    和在该文件中复制所有v110部分来编辑包(在解决方案目录中的packages文件夹中)。我在条件字段中将v110更改为v120,只是非常小心地将文件名路径全部保留为v110。这只允许Visual Studio 2013链接到2012年的库,在本例中,它起作用了。


    假设你有一个C++项目,它有一千个.cpp文件和一千个.h文件。我们还说这个项目还依赖于十个静态库。假设我们在Windows上,并且在Visual Studio 20xx中构建项目。当您按ctrl+f7 Visual Studio开始编译整个解决方案时(假设解决方案中只有一个项目)

    编译的意思是什么?

    • Visual Studio搜索到文件.vcxproj中,并开始编译扩展名为.cpp的每个文件。编译顺序未定义。因此不能假定首先编译文件main.cpp。
    • 如果.cpp文件依赖于其他.h文件以查找符号可能在.cpp文件中定义,也可能不定义。
    • 如果存在一个.cpp文件,其中编译器找不到一个符号,则编译器时间错误将引发消息symbol x could not found
    • 对于扩展名为.cpp的每个文件,都会生成一个对象文件.o,并且Visual Studio会将输出写入名为projectname.cpp.clean.txt的文件中,该文件包含链接器必须处理的所有对象文件。

    编译的第二步由链接器完成。链接器应合并所有对象文件,并最终生成输出(可能是可执行文件或库)。

    链接项目的步骤

    • 解析所有的对象文件,找到只在头文件中声明的定义(例如:前面答案中提到的类的一个方法的代码,或者事件是类中成员的静态变量的初始化)
    • 如果在对象文件中找不到一个符号,则会在其他库中搜索该符号。要将新库添加到项目配置属性->vc++目录->库目录,请在此处指定用于搜索库和配置属性的其他文件夹->链接器->输入以指定库的名称。赖氨酸-如果链接器找不到您在一个.cpp中写入的符号,则会引发一个链接器时间错误,听起来像error LNK2001: unresolved external symbol"void __cdecl foo(void)" (?foo@@YAXXZ)

    观察

  • 一旦链接器找到一个符号,他就不会在其他库中搜索它。
  • 链接库的顺序确实很重要。
  • 如果链接器在一个静态库中找到外部符号,则在项目的输出中包含该符号。但是,如果该库是共享的(动态的),则在输出中不包含代码(符号),但可能会发生运行时崩溃。
  • 如何解决这种错误

    编译器时间错误:

    • 确保你的C++项目语法正确。

    链接器时间错误

    • 定义在头文件中声明的所有符号
    • 如果编译器已经包含在当前编译的.cpp中,则使用#pragma once允许编译器不包含一个头。
    • 确保外部库不包含可能与在头文件中定义的其他符号冲突的符号。
    • 当您使用模板来确保在头文件中包含每个模板函数的定义时,允许编译器为任何实例化生成适当的代码。


    编译器/IDE中的错误

    我最近遇到了这个问题,结果发现它是Visual Studio Express 2013中的一个bug。我必须从项目中删除一个源文件,然后重新添加它来克服这个bug。

    如果您认为这可能是编译器/IDE中的错误,请尝试以下步骤:

    • 清理项目(一些IDE可以选择这样做,您也可以手动删除对象文件)
    • 尝试启动一个新项目,从原始代码复制所有源代码。


    链接的.lib文件与.dll关联

    我也有同样的问题。假设我有我的项目和测试项目。我有效地将myproject的lib文件链接到了testproject。但是,这个lib文件是作为myproject的dll生成的。另外,我不包含myproject中所有方法的源代码,只包含对dll入口点的访问。

    为了解决这个问题,我将myproject构建为lib,并将testproject链接到这个.lib文件(我将复制粘贴生成的.lib文件到testproject文件夹中)。然后我可以将我的项目重新构建为一个dll。它正在编译,因为testproject链接到的lib不包含myproject中类中所有方法的代码。


    使用链接器帮助诊断错误

    大多数现代的链接器都包含一个冗长的选项,可以不同程度地打印出来;

    • 链路调用(命令行),
    • 链接阶段包含哪些库的数据,
    • 图书馆的位置,
    • 使用的搜索路径。

    对于gcc和clang,您通常会将-v -Wl,--verbose-v -Wl,-v添加到命令行。这里可以找到更多的细节;

    • Linux LD手册页。
    • llvm链接器页。
    • "GCC简介"第9章。

    对于msvc,将/VERBOSE(特别是/VERBOSE:LIB)添加到链接命令行。

    • /VERBOSE链接器选项上的msdn页。


    既然人们似乎被引导到这个问题,当涉及到链接器错误时,我将在这里添加这个。

    GCC 5.2.0中链接器错误的一个可能原因是现在默认选择了一个新的libstdc++库abi。

    If you get linker errors about undefined references to symbols that involve types in the std::__cxx11 namespace or the tag [abi:cxx11] then it probably indicates that you are trying to link together object files that were compiled with different values for the _GLIBCXX_USE_CXX11_ABI macro. This commonly happens when linking to a third-party library that was compiled with an older version of GCC. If the third-party library cannot be rebuilt with the new ABI then you will need to recompile your code with the old ABI.

    因此,如果在5.1.0之后切换到GCC时突然出现链接器错误,这将是一件值得检查的事情。


    围绕GNU LD的不支持链接器脚本的包装

    一些.so文件实际上是GNU LD链接器脚本,例如libtbb.so文件是一个包含以下内容的ASCII文本文件:

    1
    INPUT (libtbb.so.2)

    一些更复杂的构建可能不支持这一点。例如,如果在编译器选项中包含-v,则可以看到mainwin gcc包装器mwdip将在要链接的库的详细输出列表中丢弃链接器脚本命令文件。一个简单的解决方法是将链接器脚本输入命令文件替换为该文件的副本(或symlink),例如。

    1
    cp libtbb.so.2 libtbb.so

    或者您可以将-l参数替换为.so的完整路径,例如,代替-ltbbdo /home/foo/tbb-4.3/linux/lib/intel64/gcc4.4/libtbb.so.2


    帮助模板…

    给定模板类型的代码段和友元运算符(或函数);

    1
    2
    3
    4
    template <typename T>
    class Foo {
        friend std::ostream& operator<< (std::ostream& os, const Foo<T>& a);
    };

    operator<<被声明为非模板函数。对于与Foo一起使用的每种类型的T,都需要一个非模板化的operator<<。例如,如果声明了Foo类型,那么必须有如下的运算符实现:

    1
    std::ostream& operator<< (std::ostream& os, const Foo<int>& a) {/*...*/}

    由于没有实现,链接器找不到它并导致错误。

    要纠正这个问题,您可以在Foo类型之前声明一个模板操作符,然后声明为一个朋友,适当的实例化。语法有点笨拙,但看起来如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // forward declare the Foo
    template <typename>
    class Foo;

    // forward declare the operator <<
    template <typename T>
    std::ostream& operator<<(std::ostream&, const Foo<T>&);

    template <typename T>
    class Foo {
        friend std::ostream& operator<< <>(std::ostream& os, const Foo<T>& a);
        // note the required <>        ^^^^
        // ...
    };

    template <typename T>
    std::ostream& operator<<(std::ostream&, const Foo<T>&)
    {
      // ... implement the operator
    }

    上述代码将操作员的友谊限定为Foo的相应实例化,即operator<< 的实例化仅限于访问Foo实例化的私有成员。

    备选方案包括:

    • 允许友谊扩展到模板的所有实例化,如下所示:

      1
      2
      3
      4
      5
      6
      template <typename T>
      class Foo {
          template <typename T1>
          friend std::ostream& operator<<(std::ostream& os, const Foo<T1>& a);
          // ...
      };
    • 或者,可以在类定义内内联执行operator<<

      1
      2
      3
      4
      5
      6
      template <typename T>
      class Foo {
          friend std::ostream& operator<<(std::ostream& os, const Foo& a)
          { /*...*/ }
          // ...
      };

    注意,当运算符(或函数)的声明只出现在类中时,该名称不可用于"普通"查找,只能用于参数相关的查找,来自cppreference;

    A name first declared in a friend declaration within class or class template X becomes a member of the innermost enclosing namespace of X, but is not accessible for lookup (except argument-dependent lookup that considers X) unless a matching declaration at the namespace scope is provided...

    在CopPype和C++FAQ上对模板朋友有进一步的阅读。

    显示上述技术的代码列表。

    作为失败代码示例的附带说明;g++警告如下

    warning: friend declaration 'std::ostream& operator<<(...)' declares a non-template function [-Wnon-template-friend]

    note: (if this is not what you intended, make sure the function template has already been declared and add <> after the function name here)


    您的链接在引用它们的对象文件之前使用库

    • 您试图编译程序并将其与gcc工具链链接起来。
    • 链接指定所有必需的库和库搜索路径
    • 如果libfoo依赖于libbar,则您的联动装置正确地将libfoo置于libbar之前。
    • 你的链接与undefined reference to发生故障。
    • 但是所有未定义的内容都在头文件中声明#included,实际上是在链接的库中定义的。

    例子在C中,它们同样可以是C++。好的。一个涉及您自己构建的静态库的最小示例

    MyoLIB C好的。

    1
    2
    3
    4
    5
    6
    7
    #include"my_lib.h"
    #include <stdio.h>

    void hw(void)
    {
        puts("Hello World");
    }

    MyoLiB.H好的。

    1
    2
    3
    4
    5
    6
    #ifndef MY_LIB_H
    #define MT_LIB_H

    extern void hw(void);

    #endif

    Eg1.C好的。

    1
    2
    3
    4
    5
    6
    7
    #include <my_lib.h>

    int main()
    {
        hw();
        return 0;
    }

    构建静态库:好的。

    1
    2
    $ gcc -c -o my_lib.o my_lib.c
    $ ar rcs libmy_lib.a my_lib.o

    编译程序:好的。

    1
    $ gcc -I. -c -o eg1.o eg1.c

    您试图将其与libmy_lib.a链接,但失败:好的。

    1
    2
    3
    4
    $ gcc -o eg1 -L. -lmy_lib eg1.o
    eg1.o: In function `main':
    eg1.c:(.text+0x5): undefined reference to `hw'

    collect2: error: ld returned 1 exit status

    如果您在一个步骤中编译和链接,则会得到相同的结果,例如:好的。

    1
    2
    3
    4
    $ gcc -o eg1 -I. -L. -lmy_lib eg1.c
    /tmp/ccQk1tvs.o: In function `main':
    eg1.c:(.text+0x5): undefined reference to `hw'

    collect2: error: ld returned 1 exit status

    一个涉及共享系统库的最小示例,压缩库libz

    Eg2C好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <zlib.h>
    #include <stdio.h>

    int main()
    {
        printf("%s
    "
    ,zlibVersion());
        return 0;
    }

    编译程序:好的。

    1
    $ gcc -c -o eg2.o eg2.c

    尝试将您的程序与libz链接,但失败:好的。

    1
    2
    3
    4
    $ gcc -o eg2 -lz eg2.o
    eg2.o: In function `main':
    eg2.c:(.text+0x5): undefined reference to `zlibVersion'

    collect2: error: ld returned 1 exit status

    如果您一次性编译和链接,则相同:好的。

    1
    2
    3
    4
    $ gcc -o eg2 -I. -lz eg2.c
    /tmp/ccxCiGn7.o: In function `main':
    eg2.c:(.text+0x5): undefined reference to `zlibVersion'

    collect2: error: ld returned 1 exit status

    以及实施例2中涉及pkg-config的变体:好的。

    1
    2
    3
    $ gcc -o eg2 $(pkg-config --libs zlib) eg2.o
    eg2.o: In function `main':
    eg2.c:(.text+0x5): undefined reference to `zlibVersion'

    你做错什么了?

    在对象文件和库的序列中,要链接以使程序,将库放在引用的对象文件之前他们。您需要将库放在引用的对象文件之后对他们来说。好的。

    正确链接示例1:好的。

    1
    $ gcc -o eg1 eg1.o -L. -lmy_lib

    成功:好的。

    1
    2
    $ ./eg1
    Hello World

    正确链接示例2:好的。

    1
    $ gcc -o eg2 eg2.o -lz

    成功:好的。

    1
    2
    $ ./eg2
    1.2.8

    正确链接示例2 pkg-config变量:好的。

    1
    2
    3
    $ gcc -o eg2 eg2.o $(pkg-config --libs zlib)
    $ ./eg2
    1.2.8

    解释

    从这里开始阅读是可选的。好的。

    默认情况下,由gcc在您的发行版上生成的链接命令,使用链接中从左到右的文件命令行序列。当它发现一个文件引用了并且不包含它的定义,将搜索定义在右边更远的文件中。如果它最终找到了一个定义,引用已解析。如果任何引用在结尾时仍未解决,链接失败:链接器不向后搜索。好的。

    首先,示例1,使用静态库my_lib.a。好的。

    静态库是对象文件的索引存档。当链接器在链接序列中找到-lmy_lib,并计算出这是指对于静态库./libmy_lib.a,它想知道您的程序需要libmy_lib.a中的任何对象文件。好的。

    libmy_lib.a中只有一个对象文件,即my_lib.o中只有一个定义在my_lib.o中,即函数hw。好的。

    链接器将决定您的程序是否需要my_lib.o,前提是它已经知道这一点。您的程序在一个或多个对象文件中引用了hw。已添加到程序中,但尚未添加任何对象文件包含hw的定义。好的。

    如果是这样,那么链接器将从库中提取my_lib.o的副本,并把它添加到你的程序中。然后,您的程序包含了hw的定义,所以它对hw的引用得到了解决。好的。

    当您尝试链接程序时,如:好的。

    1
    $ gcc -o eg1 -L. -lmy_lib eg1.o

    当链接器看到-lmy_lib。因为在那一点上,它还没有看到eg1.o。你的程序还没有提到hw:it完全不做任何引用,因为它所做的所有引用在eg1.o中。好的。

    因此,链接器没有向程序中添加my_lib.o,也没有进一步的添加。用于libmy_lib.a。好的。

    接下来,它找到eg1.o,并将其添加到程序中。中的对象文件链接序列总是添加到程序中。现在,程序使对hw的引用,不包含hw的定义;但是在悬挂机构序列中没有任何可以提供丢失的定义。对hw的引用最终未解决,链接失败。好的。

    第二个例子2,使用共享库libz。好的。

    共享库不是对象文件或类似文件的存档。它是更像是一个没有main功能的程序,以及而是公开它定义的多个其他符号,以便程序可以在运行时使用它们。好的。

    如今,许多Linux发行版都配置了自己的gcc工具链,使其语言驱动程序(gccg++gfortran等)指示系统链接器(ld根据需要链接共享库。你有一个发行版。好的。

    这意味着,当链接器在链接序列中找到-lz时,就会发现对于共享库(比如)/usr/lib/x86_64-linux-gnu/libz.so,它想知道它添加到您的程序中的任何尚未定义的引用是否具有由libz导出的定义。好的。

    如果是这样,那么链接器将不会从libz中复制任何块,并且将它们添加到您的程序中;相反,它只会博士化您程序的代码。因此:好的。

    • 在运行时,系统程序加载器会将libz的副本加载到无论何时加载程序的副本以运行它,都与您的程序相同。好的。

    • 在运行时,每当程序引用在libz,该引用使用libz副本在中导出的定义。同样的过程。好的。

    您的程序只想引用一个由libz导出的定义,即zlibVersion函数,在eg2.c中仅指一次。如果链接器将该引用添加到程序中,然后找到定义由libz导出,参考解析好的。

    但是当你试图链接程序时,比如:好的。

    1
    gcc -o eg2 -lz eg2.o

    事件顺序与示例1中的顺序相同。当链接器找到-lz时,没有任何引用在节目中:他们都在eg2.o里,还没有看到。所以链接器认为它对libz没有用处。当它到达eg2.o时,添加到程序中,对zlibVersion的引用未定义,则完成联动顺序;该引用未解析,链接失败。好的。

    最后,实施例2的pkg-config变化现在有一个明显的解释。壳体膨胀后:好的。

    1
    gcc -o eg2 $(pkg-config --libs zlib) eg2.o

    变成:好的。

    1
    gcc -o eg2 -lz eg2.o

    这又是示例2。好的。我可以在示例1中重现这个问题,但不能在示例2中重现。

    链接:好的。

    1
    gcc -o eg2 -lz eg2.o

    对你来说很好!好的。

    (或者:在Fedora 23上,这个链接对你很有用,但是在Ubuntu 16.04上失败了)好的。

    这是因为链接工作所在的发行版是不配置其GCC工具链以根据需要链接共享库。好的。

    回到过去,类Unix的系统将静态和共享链接起来是很正常的。图书馆有不同的规则。链接序列中的静态库已链接根据示例1中解释的需要,但是共享库是无条件链接的。好的。

    这种行为在链接时是经济的,因为链接器不必考虑程序是否需要共享库:如果是共享库,链接它。大多数链接中的大多数库都是共享库。但也有缺点:好的。

    • 它在运行时是不经济的,因为它会导致共享库与程序一起加载,即使不需要它们。好的。

    • 静态库和共享库的不同链接规则可能会令人困惑对于不专业的程序员来说,他们可能不知道-lfoo是否在他们的链接中将向/some/where/libfoo.a/some/where/libfoo.so作出决议,可能不理解共享库和静态库之间的区别不管怎样。好的。

    这种权衡导致了今天的分裂局面。一些发行版更改了共享库的gcc链接规则,以便原则适用于所有图书馆。一些发行版一直沿用旧版本方式。好的。为什么即使我同时编译和链接,我仍然会遇到这个问题?

    如果我只是这样做:好的。

    1
    $ gcc -o eg1 -I. -L. -lmy_lib eg1.c

    当然,GCC必须首先编译eg1.c,然后将结果链接起来带有libmy_lib.a的对象文件。所以它怎么可能不知道对象文件链接时需要吗?好的。

    因为使用单个命令编译和链接不会更改联动顺序的顺序。好的。

    当您运行上面的命令时,gcc会发现您需要编译+连锁。所以在幕后,它生成一个编译命令,并运行然后生成一个链接命令并运行它,就好像运行了两个命令:好的。

    1
    2
    $ gcc -I. -c -o eg1.o eg1.c
    $ gcc -o eg1 -L. -lmy_lib eg1.o

    所以链接失败,就像运行这两个命令一样。这个您在失败中注意到的唯一区别是GCC生成了一个编译+链接案例中的临时对象文件,因为您没有告诉它使用eg1.o。我们看到:好的。

    1
    /tmp/ccQk1tvs.o: In function `main'

    而不是:好的。

    1
    eg1.o: In function `main':

    也见

    指定相互依赖的链接库的顺序错误好的。

    将相互依赖的库按错误的顺序排列只是一种方法在其中,您可以获取需要对即将发生的事情进行定义的文件在以后的链接中,要比提供定义的文件晚。把图书馆放在引用它们的对象文件是犯同样错误的另一种方法。好的。好啊。


    UNICODE定义不一致

    Windows Unicode构建使用TCHAR等定义为wchar_t等。当不使用UNICODE构建时,定义为TCHAR定义为char等。这些UNICODE_UNICODE定义影响所有"T"字符串类型;LPTSTRLPCTSTR及其elk。

    在定义了UNICODE的情况下建立一个库,并试图在没有定义UNICODE的项目中链接它,这将导致链接器错误,因为TCHAR的定义将不匹配;charwchar_t的定义将不匹配。

    该错误通常包括一个函数一个派生类型为charwchar_t的值,这些值也可以包括std::basic_string<>等。当浏览代码中受影响的函数时,通常会有一个对TCHARstd::basic_string等的引用。这是一个警告信号,表明代码最初是为unicode和多字节字符(或"窄")构建而设计的。

    要纠正这一点,请使用一致的UNICODE_UNICODE定义来构建所有所需的库和项目。

  • 这可以通过以下两种方法来实现:

    1
    2
    #define UNICODE
    #define _UNICODE
  • 或在项目设置中;


    Project Properties > General > Project Defaults > Character Set

  • 或者在命令行上;

    1
    /DUNICODE /D_UNICODE
  • 另外,如果不打算使用Unicode,请确保未设置定义,和/或在项目中使用多字符设置并始终应用。

    不要忘记在"发布"和"调试"构建之间保持一致。


    当包含路径不同时

    当头文件及其关联的共享库(.lib文件)不同步时,可能会发生链接器错误。让我解释一下。

    链接器如何工作?链接器通过比较其签名,将函数声明(在头中声明)与其定义(在共享库中)匹配。如果链接器找不到完全匹配的函数定义,则可能会出现链接器错误。

    即使声明和定义似乎匹配,仍然可能出现链接器错误吗?对!它们在源代码中看起来可能是相同的,但这实际上取决于编译器看到的内容。从本质上来说,你可能会遇到这样的情况:

    1
    2
    3
    4
    5
    6
    7
    // header1.h
    typedef int Number;
    void foo(Number);

    // header2.h
    typedef float Number;
    void foo(Number); // this only looks the same lexically

    请注意,即使两个函数声明在源代码中看起来是相同的,但根据编译器的不同,它们实际上是不同的。

    你可能会问一个人是如何在这样的情况下结束的?当然包括路径!如果在编译共享库时,include路径导致header1.h,而您最终在自己的程序中使用header2.h,则会留下抓取头部的痕迹,想知道发生了什么(pun的意图)。

    下面将解释在现实世界中如何发生这种情况的一个例子。

    以实例进一步阐述

    我有两个项目:graphics.libmain.exe。这两个项目都依赖于common_math.h。假设库导出以下函数:

    1
    2
    3
    4
    // graphics.lib    
    #include"common_math.h"

    void draw(vec3 p) { ... } // vec3 comes from common_math.h

    然后你继续把图书馆包括在你自己的项目中。

    1
    2
    3
    4
    5
    6
    7
    // main.exe
    #include"other/common_math.h"
    #include"graphics.h"

    int main() {
        draw(...);
    }

    繁荣!你会得到一个链接器错误,你不知道它为什么会失败。原因是公共库使用了相同的不同版本,包括common_math.h(我在示例中通过包含不同的路径使它变得明显,但它可能并不总是那么明显。可能编译器设置中的include路径不同)。

    注意,在这个例子中,链接器会告诉您它找不到draw(),而实际上您知道它显然是由库导出的。你可以花几个小时挠头,想知道出了什么问题。问题是,链接器看到的签名不同,因为参数类型略有不同。在这个例子中,就编译器而言,vec3在两个项目中是不同的类型。这可能是因为它们来自两个稍微不同的包含文件(可能包含文件来自库的两个不同版本)。

    调试链接器

    如果您使用的是Visual Studio,dumpbin是您的朋友。我相信其他编译器也有类似的工具。

    过程如下:

  • 注意链接器错误中给出的奇怪的损坏名称。(例如,绘制@graphics@xyz)。
  • 将库中导出的符号转储到文本文件中。
  • 搜索感兴趣的导出符号,注意损坏的名称不同。
  • 注意为什么这些乱七八糟的名字会不一样。您将能够看到参数类型是不同的,即使它们在源代码中看起来是相同的。
  • 它们不同的原因。在上面给出的示例中,它们是不同的,因为包含文件不同。
  • [1]在Project中,我指的是一组链接在一起以生成库或可执行文件的源文件。

    编辑1:重写第一部分以便于理解。请在下面发表评论,让我知道是否还有其他问题需要解决。谢谢!


    清理和重建

    构建的"干净"可以消除以前构建、失败的构建、不完整的构建以及其他与构建系统相关的构建问题中可能留下的"死木头"。

    通常,IDE或构建将包含某种形式的"干净"功能,但这可能没有正确配置(例如,在手动生成文件中),或者可能会失败(例如,中间或结果二进制文件是只读的)。

    完成"清理"后,请确认"清理"已成功,并且所有生成的中间文件(例如自动生成文件)已成功删除。

    这个过程可以看作是最后的手段,但通常是一个好的第一步;特别是如果最近添加了与错误相关的代码(在本地或从源存储库添加)。


    EDCOX1 8变量声明/定义中缺失"外部"(C++)

    对于来自C的人来说,在C++全局EDCOX1中,8个变量具有内部(或静态)链接可能是一个惊喜。在C中,情况并非如此,因为所有全局变量都是隐式的extern(即当static关键字丢失时)。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // file1.cpp
    const int test = 5;    // in C++ same as"static const int test = 5"
    int test2 = 5;

    // file2.cpp
    extern const int test;
    extern int test2;

    void foo()
    {
     int x = test;   // linker error in C++ , no error in C
     int y = test2;  // no problem
    }

    正确的做法是使用头文件并将其包含在file2.cpp和file1.cpp中

    1
    2
    extern const int test;
    extern int test2;

    或者,可以在file1.cpp中声明const变量,并显式地声明extern


    尽管这是一个相当古老的问题,有多个可接受的答案,但我想分享如何解决一个模糊的"未定义引用"错误。

    不同版本的库

    我使用别名来引用EDCOX1的0个词:文件系统在C++ 17的标准库中,但是我的程序也需要在C++ 14中编译,所以我决定使用变量别名:

    1
    2
    3
    4
    5
    #if (defined _GLIBCXX_EXPERIMENTAL_FILESYSTEM) //is the included filesystem library experimental? (C++14 and newer: <experimental/filesystem>)
    using path_t = std::experimental::filesystem::path;
    #elif (defined _GLIBCXX_FILESYSTEM) //not experimental (C++17 and newer: <filesystem>)
    using path_t = std::filesystem::path;
    #endif

    假设我有三个文件:main.cpp、file.h和file.cpp:

    • file.h include的并包含上面的代码
    • file.cpp,file.h的实现,include的"file.h"
    • main.cpp include的和"file.h"

    注意MIN .CPP和file .h中使用的不同库,因为Me.CPP包含"文件.h"之后的文件系统>,这里使用的文件系统版本是C++ 17。我用以下命令编译程序:

    $g++ -g -std=c++17 -c main.cpp->将main.cpp编译为main.o$g++ -g -std=c++17 -c file.cpp->将file.cpp和file.h编译为file.o$g++ -g -std=c++17 -o executable main.o file.o -lstdc++fs->links main.o和file.o

    这样,文件.o中包含的、在main.o中使用的、需要path_t的任何函数都会给出"未定义的引用"错误,因为main.o引用std::filesystem::path,而file.o引用std::experimental::filesystem::path

    分辨率

    为了解决这个问题,我只需要将file.h中的更改为


    当链接到共享库时,请确保使用的符号不被隐藏。

    gcc的默认行为是所有符号都可见。但是,当使用选项-fvisibility=hidden构建翻译单元时,只有标记有__attribute__ ((visibility ("default")))的函数/符号在生成的共享对象中是外部的。

    您可以通过调用以下命令来检查正在查找的符号是否是外部的:

    1
    2
    # -D shows (global) dynamic symbols that can be used from the outside of XXX.so
    nm -D XXX.so | grep MY_SYMBOL

    隐藏/局部符号由带小写符号类型的nm显示,例如t而不是代码段的't:

    1
    2
    3
    nm XXX.so
    00000000000005a7 t HIDDEN_SYMBOL
    00000000000005f8 T VISIBLE_SYMBOL

    您还可以使用EDOCX1的"9"选项,用EDOCX1"12"选项来删除名称(如果使用C++)。

    与Windows DLL类似,可以用定义来标记公共函数,例如DLL_PUBLIC定义为:

    1
    2
    3
    4
    5
    #define DLL_PUBLIC __attribute__ ((visibility ("default")))

    DLL_PUBLIC int my_public_function(){
      ...
    }

    大致对应于Windows'/MSVC版本:

    1
    2
    3
    4
    5
    #ifdef BUILDING_DLL
        #define DLL_PUBLIC __declspec(dllexport)
    #else
        #define DLL_PUBLIC __declspec(dllimport)
    #endif

    关于可见性的更多信息可以在gcc wiki上找到。

    当翻译单元使用-fvisibility=hidden编译时,生成的符号仍然具有外部链接(由nm使用大写符号类型显示),如果目标文件成为静态库的一部分,则可以毫无问题地用于外部链接。只有当对象文件链接到共享库时,链接才会成为本地链接。

    要查找对象文件中隐藏的符号,请运行:

    1
    2
    3
    >>> objdump -t XXXX.o | grep hidden
    0000000000000000 g     F .text  000000000000000b .hidden HIDDEN_SYMBOL1
    000000000000000b g     F .text  000000000000000b .hidden HIDDEN_SYMBOL2


    • 使用G++44(Red Hat 4.4.7-8)可以按原样编译代码。

    Ubuntu有一个debian补丁的gcc-4.4/g++-4.4:不适合这个代码(以及其他一些NS2代码/补丁)。

    Ubuntu:mpolsr_umolsr-v1_ns235.patch→https://drive.google.com/file/d/0b7s…ew?USP=共享(创建于2017年,使用MPOLSR代码,无变化。)

    1
    tar xvf ns-allinone-2.35_gcc5.tar.gz

    https://drive.google.com/file/d/0b7s…电子战?USP =共享

    1
    2
    3
    4
    5
    6
    7
    8
    9
    cd ns-allinone-2.35/
    patch -p0 < mpolsr_umolsr-v1_ns235.patch  // umolsr version v1.0 is used
    ./install
                  // Stops with MPOLSR.cc
    cd ns-2.35/
                  // Edit the Makefile line 37 to CPP = g++34 , and run make
    make
                  // When 'make' stops with an mdart/* error, change to CPP = g++-4.4
    make

    GCC34 Ubuntu https://drive.google.com/file/d/0b7s255p3kfxnrtkzqnrsnxz6uvu/view?USP =共享

    G++34 Ubuntu https://drive.google.com/file/d/0b7s255p3kfxnv3j3bnvowgnwdg8/view?USP =共享


    不同的架构

    您可能会看到如下信息:

    1
    library machine type 'x64' conflicts with target machine type 'X86'

    在这种情况下,这意味着可用的符号适用于不同于您正在编译的体系结构。

    在Visual Studio上,这是由于错误的"平台",您需要选择正确的平台或安装库的正确版本。

    在Linux上,可能是由于错误的库文件夹(例如,使用lib而不是lib64)。

    在MacOS上,可以选择在同一个文件中传送两个体系结构。可能链接期望两个版本都存在,但只有一个存在。它也可能是错误的lib/lib64文件夹的问题。


    函数或类方法是用inline说明符在源文件中定义的。

    一个例子:

    主CPP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include"gum.h"
    #include"foo.h"

    int main()
    {
        gum();
        foo f;
        f.bar();
        return 0;
    }

    Fo.H(1)

    1
    2
    3
    4
    5
    #pragma once

    struct foo {
        void bar() const;
    };

    胶(H)(1)

    1
    2
    3
    #pragma once

    extern void gum();

    英尺.CPP(1)

    1
    2
    3
    4
    5
    6
    #include"foo.h"
    #include <iostream>

    inline /* <- wrong! */ void foo::bar() const {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    CPP(1)

    1
    2
    3
    4
    5
    6
    7
    #include"gum.h"
    #include <iostream>

    inline /* <- wrong! */ void gum()
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    如果您在其定义中指定gum(类似地,foo::barinline,那么编译器将内联gum(如果选择的话),方法是:

    • 不发出任何对gum的独特定义,因此
    • 不发出链接器可以引用gum定义的任何符号,而是
    • 将对gum的所有调用替换为gum编译体的内联副本。

    因此,如果在源文件gum.cpp中以内联方式定义gum,则编译成一个对象文件gum.o,其中所有对gum的调用都是内联的。并且没有定义链接器可以引用gum的符号。当你将gum.o与另一个对象文件(如main.o一起链接到程序中如果引用外部符号gum,则链接器无法解析那些推荐信。因此联动装置失效:

    编译:

    1
    g++ -c  main.cpp foo.cpp gum.cpp

    链接:

    1
    2
    3
    4
    5
    $ g++ -o prog main.o foo.o gum.o
    main.o: In function `main':
    main.cpp:(.text+0x18): undefined reference to `gum()'

    main.cpp:(.text+0x24): undefined reference to `foo::bar() const'
    collect2: error: ld returned 1 exit status

    如果编译器可以在调用gum的每个源文件中看到其定义,则只能将gum定义为inline。这意味着它的内联定义需要存在于包含在每个源文件中的头文件中。编译时可以调用gum。做两件事之一:

    或者不内联定义

    从源文件定义中删除inline说明符:

    英尺.CPP(2)

    1
    2
    3
    4
    5
    6
    #include"foo.h"
    #include <iostream>

    void foo::bar() const {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    CPP(2)

    1
    2
    3
    4
    5
    6
    7
    #include"gum.h"
    #include <iostream>

    void gum()
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    用它重建:

    1
    2
    3
    4
    5
    $ g++ -c  main.cpp foo.cpp gum.cpp
    imk@imk-Inspiron-7559:~/develop/so/scrap1$ g++ -o prog main.o foo.o gum.o
    imk@imk-Inspiron-7559:~/develop/so/scrap1$ ./prog
    void gum()
    void foo::bar() const

    成功。

    或正确地插入

    头文件中的内联定义:

    Fo.H(2)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #pragma once
    #include <iostream>

    struct foo {
        void bar() const  { // In-class definition is implicitly inline
            std::cout << __PRETTY_FUNCTION__ << std::endl;
        }
    };
    // Alternatively...
    #if 0
    struct foo {
        void bar() const;
    };
    inline void foo::bar() const  {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    #endif

    胶(H)(2)

    1
    2
    3
    4
    5
    6
    #pragma once
    #include <iostream>

    inline void gum() {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    现在我们不需要foo.cppgum.cpp

    1
    2
    3
    4
    5
    $ g++ -c main.cpp
    $ g++ -o prog main.o
    $ ./prog
    void gum()
    void foo::bar() const

    当链接器在声明文件(即头文件或声明文件)中找不到定义的任何定义时,就会发生错误。当找不到实现时会发生这种情况。

    编译后,链接器尝试从库或为虚拟函数或模板定义查找实现。如果找不到,这些错误会根据链接器体系结构发生。