对虚表的未定义引用

当构建我的c++程序时,我得到了错误消息

未定义的引用'vtable…

这个问题的原因是什么?我该怎么解决呢?


碰巧,我得到以下代码的错误(类的问题是CGameModule.),我不能为我的生活理解问题是什么。起初,我认为这与忘记给虚拟函数赋予一个主体有关,但据我所知,一切都在这里。继承链有点长,但这里有相关的源代码。我不确定我还需要提供什么信息。

注意:这个错误似乎发生在构造函数中。

我的代码:

class CGameModule : public CDasherModule {
public:
CGameModule(Dasher::CEventHandler *pEventHandler, CSettingsStore *pSettingsStore, CDasherInterfaceBase *pInterface, ModuleID_t iID, const char *szName)
: CDasherModule(pEventHandler, pSettingsStore, iID, 0, szName)
{
g_pLogger->Log("Inside game module constructor");
m_pInterface = pInterface;
}


virtual ~CGameModule() {};


std::string GetTypedTarget();


std::string GetUntypedTarget();


bool DecorateView(CDasherView *pView) {
//g_pLogger->Log("Decorating the view");
return false;
}


void SetDasherModel(CDasherModel *pModel) { m_pModel = pModel; }




virtual void HandleEvent(Dasher::CEvent *pEvent);


private:






CDasherNode *pLastTypedNode;




CDasherNode *pNextTargetNode;




std::string m_sTargetString;




size_t m_stCurrentStringPos;




CDasherModel *m_pModel;




CDasherInterfaceBase *m_pInterface;
};

继承自…

class CDasherModule;
typedef std::vector<CDasherModule*>::size_type ModuleID_t;


/// \ingroup Core
/// @{
class CDasherModule : public Dasher::CDasherComponent {
public:
CDasherModule(Dasher::CEventHandler * pEventHandler, CSettingsStore * pSettingsStore, ModuleID_t iID, int iType, const char *szName);


virtual ModuleID_t GetID();
virtual void SetID(ModuleID_t);
virtual int GetType();
virtual const char *GetName();


virtual bool GetSettings(SModuleSettings **pSettings, int *iCount) {
return false;
};


private:
ModuleID_t m_iID;
int m_iType;
const char *m_szName;
};

哪个继承自....

namespace Dasher {
class CEvent;
class CEventHandler;
class CDasherComponent;
};


/// \ingroup Core
/// @{
class Dasher::CDasherComponent {
public:
CDasherComponent(Dasher::CEventHandler* pEventHandler, CSettingsStore* pSettingsStore);
virtual ~CDasherComponent();


void InsertEvent(Dasher::CEvent * pEvent);
virtual void HandleEvent(Dasher::CEvent * pEvent) {};


bool GetBoolParameter(int iParameter) const;
void SetBoolParameter(int iParameter, bool bValue) const;


long GetLongParameter(int iParameter) const;
void SetLongParameter(int iParameter, long lValue) const;


std::string GetStringParameter(int iParameter) const;
void        SetStringParameter(int iParameter, const std::string & sValue) const;


ParameterType   GetParameterType(int iParameter) const;
std::string     GetParameterName(int iParameter) const;


protected:
Dasher::CEventHandler *m_pEventHandler;
CSettingsStore *m_pSettingsStore;
};
/// @}




#endif
770954 次浏览
  • 你确定CDasherComponent有析构函数体吗?它肯定不在这里——问题是它是否在.cc文件中。
  • 从样式的角度来看,CDasherModule应该显式地定义它的析构函数virtual
  • 看起来CGameModule在结尾(在}; // for the class之后)有一个额外的}
  • CGameModule是否被链接到定义CDasherModuleCDasherComponent的库?

GCC常见问题解答有一个条目:

解决方案是确保定义了所有非纯的虚方法。注意,即使析构函数被声明为pure-virtual [class.dtor]/7,也必须定义析构函数。

因此,你需要为虚析构函数提供一个定义:

virtual ~CDasherModule()
{ }

所以,我已经找到了问题所在,这是一个糟糕的逻辑和不完全熟悉automake/autotools世界的组合。我正在向Makefile中添加正确的文件。am模板,但我不确定在我们的构建过程中哪一步实际上创建了makefile本身。所以,我用一个旧的makefile编译,它对我的新文件一无所知。

感谢您的回复以及GCC FAQ的链接。我一定会阅读它,以避免这个问题出现真正的原因。

不管怎样,在虚析构函数上忘记主体会生成以下结果:

未定义的引用'虚表为CYourClass'。

我添加一个说明,因为错误消息是欺骗性的。(这是gcc 4.6.3版。)

对vtable的未定义引用也可能由于以下情况而发生。试试这个:

A类包含:

virtual void functionA(parameters)=0;
virtual void functionB(parameters);

B类包含:

  1. 上述函数a的定义。
  2. 上述函数b的定义。

类C包含:现在你正在编写一个类C,你将从类a派生它。

现在如果你尝试编译,你会得到未定义的引用vtable类C作为错误。

原因:

functionA被定义为纯虚值,其定义在类B中提供。 functionB被定义为虚拟(不是纯虚拟),所以它试图在类A中找到它的定义,但你在类b中提供了它的定义

解决方案:

    使函数B为纯虚函数(如果你有这样的要求) virtual void functionB(parameters) =0; (这是经过测试的) 在类A中为函数b提供定义,保持其为虚值。 (希望它能工作,因为我没有尝试这样做)

我只是得到了这个错误,因为我的.cpp文件不在makefile中。

通常,如果您忘记编译或链接到包含定义的特定对象文件,就会遇到这个错误。

如果所有这些都失败了,那就寻找副本。我被构造函数和析构函数的显式初始引用误导了,直到我在另一篇文章中读到引用。它是任何未解析方法。在我的例子中,我以为我已经用不必要的麻烦的const char *xml替换了使用char *xml作为参数的声明,但相反,我创建了一个新的声明,并保留了另一个。

不是过柱而是。如果你正在处理继承的第二个谷歌命中是什么我错过了,即。应该定义所有虚方法。

如:

virtual void fooBar() = 0;

详见answare c++对虚表和继承的未定义引用。刚刚意识到上面已经提到了,但它可能会帮助到一些人。

如果您正在使用Qt,请尝试重新运行qmake。如果这个错误出现在小部件的类中,那么qmake可能没有注意到应该重新生成ui类虚表。这为我解决了问题。

GNU c++编译器必须决定将vtable放在哪里,以防对象的虚函数定义分布在多个编译单元中(例如,一些对象的虚函数定义在一个.cpp文件中,另一些在另一个.cpp文件中,等等)。

编译器选择将vtable放在与第一个声明的虚函数定义相同的位置。

现在,如果由于某种原因忘记为对象中声明的第一个虚函数提供定义(或者错误地忘记在链接阶段添加已编译的对象),您将得到这个错误。

作为副作用,请注意,只有对于这个特定的虚函数,你才不会得到像你错过了函数foo这样的传统链接器错误。

我刚刚遇到了另一个导致这个错误的原因,你可以检查一下。

基类将纯虚函数定义为:

virtual int foo(int x = 0);

子类有

int foo(int x) override;

问题是"=0"应该在括号外的拼写错误:

virtual int foo(int x) = 0;

所以,如果你向下滚动这么远,你可能没有找到答案,这是另一个需要检查的东西。

这里有很多猜测,各种各样的答案。下面我将给出一个相当小的代码来重现这个错误,并解释为什么会出现这个错误。

重现此错误的代码相当少

IBase.hpp

#pragma once


class IBase {
public:
virtual void action() = 0;
};

Derived.hpp

#pragma once


#include "IBase.hpp"


class Derived : public IBase {
public:
Derived(int a);
void action() override;
};

Derived.cpp

#include "Derived.hpp"
Derived::Derived(int a) { }
void Derived::action() {}

myclass.cpp

#include <memory>
#include "Derived.hpp"


class MyClass {


public:
MyClass(std::shared_ptr<Derived> newInstance) : instance(newInstance) {


}


void doSomething() {
instance->action();
}


private:
std::shared_ptr<Derived> instance;
};


int main(int argc, char** argv) {
Derived myInstance(5);
MyClass c(std::make_shared<Derived>(myInstance));
c.doSomething();
return 0;
}

你可以像这样使用GCC编译:

g++ -std=c++11 -o a.out myclass.cpp Derived.cpp

你现在可以通过在hbase .hpp中删除= 0来重现错误。我得到这个错误:

~/.../catkin_ws$ g++ -std=c++11 -o /tmp/m.out /tmp/myclass.cpp /tmp/Derived.cpp
/tmp/cclLscB9.o: In function `IBase::IBase(IBase const&)':
myclass.cpp:(.text._ZN5IBaseC2ERKS_[_ZN5IBaseC5ERKS_]+0x13): undefined reference to `vtable for IBase'
/tmp/cc8Smvhm.o: In function `IBase::IBase()':
Derived.cpp:(.text._ZN5IBaseC2Ev[_ZN5IBaseC5Ev]+0xf): undefined reference to `vtable for IBase'
/tmp/cc8Smvhm.o:(.rodata._ZTI7Derived[_ZTI7Derived]+0x10): undefined reference to `typeinfo for IBase'
collect2: error: ld returned 1 exit status

解释

请注意,上面的代码不需要任何虚拟析构函数、构造函数或任何其他额外的文件才能成功编译(尽管您应该拥有它们)。

理解这个错误的方法如下: 链接器正在寻找IBase的构造函数。派生的构造函数将需要它。然而,由于Derived重写了IBase的方法,因此它附加了引用IBase的虚表。当链接器说“对IBase虚表的未定义引用”时,这基本上意味着Derived对IBase有虚表引用,但它找不到任何已编译的IBase目标代码来查找。因此,基本问题是IBase类只有声明而没有实现。这意味着IBase中的一个方法被声明为虚方法,但我们忘记将其标记为纯虚方法或提供其定义

分小费

如果所有这些都失败了,那么调试这个错误的一种方法是构建最小的程序,它可以编译,然后不断更改它,使它达到您想要的状态。在此期间,继续编译以查看它何时开始失败。

注意ROS和柳絮构建系统

如果你在ROS中使用catkin构建系统编译上述一组类,那么你将需要在CMakeLists.txt中执行以下命令:

add_executable(myclass src/myclass.cpp src/Derived.cpp)
add_dependencies(myclass theseus_myclass_cpp)
target_link_libraries(myclass ${catkin_LIBRARIES})

第一行基本上是说,我们想要创建一个名为myclass的可执行文件,构建它的代码可以在下面的文件中找到。其中一个文件应该有main()。请注意,您不必在CMakeLists.txt中的任何地方指定.hpp文件。此外,您不必将Derived.cpp指定为库。

我在Windows XP和MinGW编译器下使用Qt,这个东西快把我逼疯了。

基本上,即使添加了我,moc_xxx.cpp也会生成空的

Q_OBJECT

删除所有使函数虚的,显式的和任何你猜的都不起作用的东西。最后,我开始逐行删除,结果发现我有

#ifdef something

围绕文件。即使#ifdef为true也不会生成moc文件。

所以删除所有#ifdefs就解决了这个问题。

这种事情在Windows和VS 2013中没有发生。

在我的情况下,我使用Qt,并在foo.cpp(不是.h)文件中定义了QObject子类。修复方法是在foo.cpp的末尾添加#include "foo.moc"

我认为值得一提的是,当你试图链接到具有至少一个虚方法和链接器找不到文件的任何类对象时,你也会得到消息。 例如:< / p >

Foo.hpp:

class Foo
{
public:
virtual void StartFooing();
};

Foo.cpp:

#include "Foo.hpp"


void Foo::StartFooing(){ //fooing }

编译:

g++ Foo.cpp -c

和main.cpp:

#include "Foo.hpp"


int main()
{
Foo foo;
}

编译和链接:

g++ main.cpp -o main

给出我们最喜欢的错误:

< p > / tmp / cclKnW0g。o:在函数main': main.cpp:(.text+0x1a):未定义 引用vtable for Foo' collect2: error: ld返回1 exit < / p >状态

我的理解是这样的:

  1. Vtable在编译时为每个类创建

  2. 链接器不能访问Foo.o中的虚表

解释为什么虚表可能丢失,以及如何修复它。答案很长,因为它解释了为什么编译器可能会忘记创建虚表。(编辑)

vtable是什么?

在尝试修复错误消息之前,了解错误消息所谈论的内容可能是有用的。我将从高水平开始,然后再深入到更详细的细节。这样,一旦人们熟悉了虚变量,就可以跳过前面的内容。现在有一群人在前面跳。对于那些留下来的人:

虚表基本上是多态性 在c++中最常见的实现。当使用虚表时,每个多态类在程序的某个地方都有一个虚表;你可以认为它是类的(隐藏的)static数据成员。多态类的每个对象都与其最派生类的虚表相关联。通过检查这种关联,程序可以发挥其多态魔力。重要提醒:虚表是一个实现细节。c++标准并没有强制要求它,即使大多数(全部?)c++编译器使用虚表来实现多态行为。我所介绍的细节要么是典型的,要么是合理的。编译器可以偏离这一点!

每个多态对象都有一个(隐藏的)指向对象最派生类虚表的指针(在更复杂的情况下,可能有多个指针)。通过查看指针,程序可以判断“real"对象的类型是(构造期间除外,但让我们跳过这种特殊情况)。例如,如果类型为A的对象不指向A的虚表,那么该对象实际上是从A派生出来的某个对象的子对象。

名字&;vtable"来自于“__abc0virtual function __abc1”。它是一个存储指向(虚拟)函数的指针的表。编译器根据表的布局方式选择它的约定;一种简单的方法是按照虚函数在类定义中声明的顺序遍历它们。当调用虚函数时,程序遵循对象指向虚表的指针,转到与所需函数关联的条目,然后使用存储的函数指针调用正确的函数。有各种各样的技巧来完成这个工作,但我不会在这里详细介绍。

在哪里/何时生成vtable ?

虚表是由编译器自动生成的(有时称为"emit ")编译器可以在看到多态类定义的每个翻译单元中发出虚表,但这通常是不必要的。另一种选择(GCC使用,可能由其他人)是选择一个翻译单元来放置虚表,类似于选择一个源文件来放置类的静态数据成员。如果这个选择过程没有选择任何翻译单元,那么虚表就变成了一个未定义的引用。因此出现了这个错误,其信息不可否认不是特别清楚。

类似地,如果选择过程确实选择了一个翻译单元,但是该目标文件没有提供给链接器,那么虚表将成为一个未定义的引用。不幸的是,在这种情况下,错误消息甚至比选择过程失败的情况更不清楚。(感谢那些提到这种可能性的受访者。否则我可能已经忘记了。)

如果我们从将一个(单个)源文件分配给每个类的传统开始,那么gcc使用的选择过程是有意义的。最好在编译源文件时发出虚表。我们称之为目标。然而,即使没有遵循这一传统,选拔过程也需要发挥作用。因此,我们不寻找整个类的实现,而是寻找类中特定成员的实现。如果遵循传统-并且如果该成员实际上已经实现 -那么这就实现了目标。

gcc(可能还有其他编译器)选择的成员是第一个非纯虚函数的非内联虚函数。如果您是在其他成员函数之前声明构造函数和析构函数的一员,那么析构函数很有可能被选中。(你记得将析构函数设为虚函数,对吧?)也有例外;我认为最常见的例外是当为析构函数提供了内联定义以及当请求默认析构函数时(使用"= default")。

精明的人可能会注意到,允许多态类为其所有虚函数提供内联定义。这难道不会导致选择过程失败吗?在旧的编译器中是这样的。我听说最新的编译器已经解决了这个问题,但是我不知道相关的版本号。我可以尝试查找这个,但它更容易要么编码绕过它或等待编译器抱怨。

总而言之,“未定义的vtable引用”有三个关键原因;错误:

  1. 成员函数缺少定义。
  2. 目标文件未被链接。
  3. 所有虚函数都有内联定义。

这些原因本身不足以导致错误。相反,这些是解决错误的方法。不要期望故意创建这些情况之一一定会产生此错误;还有其他要求。请期望解决这些情况将解决此错误。

(好吧,当这个问题被问到时,第3条可能已经足够了。)

如何修复错误?

欢迎回来,跳过前面的人!:)

    查看你的类定义。找到第一个非纯虚函数(不是"= 0")并且你提供了它的定义(不是"= default")
    • 如果没有这样的函数,请尝试修改您的类,使之有一个。(错误可能已解决。)
    • 请参见菲利普·托马斯的回答以获得警告。
  1. 找到这个函数的定义。如果缺少,请添加!(错误可能已解决。)
    • 如果函数定义在类定义之外,则确保函数定义使用限定名,如ClassName::function_name
  2. 检查你的link命令。如果它没有提到该函数定义的目标文件,请修复它!(错误可能已解决。)
  3. 对每个虚函数重复步骤2和3,然后对每个非虚函数重复步骤2和3,直到错误得到解决。如果仍然被卡住,那么对每个静态数据成员重复操作。
< p > 例子
要做什么的细节可能会有所不同,有时会分支成单独的问题(如什么是未定义的引用/未解决的外部符号错误,我如何修复它?)。不过,我将提供一个在特定情况下该如何做的示例,这可能会使新程序员感到困惑

步骤1提到修改类,使其具有特定类型的函数。如果这个功能的描述超出了你的理解范围,那么你可能正处于我想要解决的情况。记住,这是实现目标的一种方式;这不是唯一的方法,在你的具体情况下,很容易有更好的方法。让我们将你的类命名为A。你的析构函数(在你的类定义中)是否声明为其中之一

virtual ~A() = default;

virtual ~A() {}

? 如果是这样,两个步骤将把析构函数更改为我们想要的函数类型。首先,将该行更改为

virtual ~A();

其次,在项目的源文件中放入以下一行(最好是类实现的文件,如果你有的话):

A::~A() {}

这使得你的(虚拟)析构函数是非内联的,并且不是由编译器生成的。(请随意修改内容以更好地匹配您的代码格式风格,例如在函数定义中添加头注释。)

我尝试了JaMIT的所有详细步骤,仍然被这个错误难住了。经过一番反复,我终于明白了。我太粗心了。您应该能够使用以下示例代码重现这个令人痛苦的错误。

[jaswantp@jaswant-arch build]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /build/gcc/src/gcc/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++,d --with-isl --with-linker-hash-style=gnu --with-system-zlib --enable-__cxa_atexit --enable-cet=auto --enable-checking=release --enable-clocale=gnu --enable-default-pie --enable-default-ssp --enable-gnu-indirect-function --enable-gnu-unique-object --enable-install-libiberty --enable-linker-build-id --enable-lto --enable-multilib --enable-plugin --enable-shared --enable-threads=posix --disable-libssp --disable-libstdcxx-pch --disable-libunwind-exceptions --disable-werror gdc_include_dir=/usr/include/dlang/gdc
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 10.2.0 (GCC)


// CelesetialBody.h
class CelestialBody{
public:
virtual void Print();
protected:
CelestialBody();
virtual ~CelestialBody();
};
// CelestialBody.cpp
#include "CelestialBody.h"


CelestialBody::CelestialBody() {}


CelestialBody::~CelestialBody() = default;


void CelestialBody::Print() {}
// Planet.h
#include "CelestialBody.h"


class Planet : public CelestialBody
{
public:
void Print() override;
protected:
Planet();
~Planet() override;
};
// Planet.cpp
#include "Planet.h"


Planet::Planet() {}
Planet::~Planet() {}


void Print() {} // Deliberately forgot to prefix `Planet::`
# CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project (space_engine)
add_library (CelestialBody SHARED CelestialBody.cpp)
add_library (Planet SHARED Planet.cpp)
target_include_directories (CelestialBody PRIVATE ${CMAKE_CURRENT_LIST_DIR})
target_include_directories (Planet PRIVATE ${CMAKE_CURRENT_LIST_DIR})
target_link_libraries (Planet PUBLIC CelestialBody)


# hardened linker flags to catch undefined symbols
target_link_options(Planet
PRIVATE
-Wl,--as-needed
-Wl,--no-undefined
)

我们得到了我们最喜欢的误差。

$ mkdir build
$ cd build
$ cmake ..
$ make
[ 50%] Built target CelestialBody
Scanning dependencies of target Planet
[ 75%] Building CXX object CMakeFiles/Planet.dir/Planet.cpp.o
[100%] Linking CXX shared library libPlanet.so
/usr/bin/ld: CMakeFiles/Planet.dir/Planet.cpp.o: in function `Planet::Planet()':
Planet.cpp:(.text+0x1b): undefined reference to `vtable for Planet'
/usr/bin/ld: CMakeFiles/Planet.dir/Planet.cpp.o: in function `Planet::~Planet()':
Planet.cpp:(.text+0x3d): undefined reference to `vtable for Planet'
collect2: error: ld returned 1 exit status
make[2]: *** [CMakeFiles/Planet.dir/build.make:104: libPlanet.so] Error 1
make[1]: *** [CMakeFiles/Makefile2:97: CMakeFiles/Planet.dir/all] Error 2
make: *** [Makefile:103: all] Error 2


我在Planet.cpp中所做的事情当然应该通过这个技巧来解决

  1. 查看类定义。找到第一个非纯虚函数(不是"= 0")并且您提供了其定义(不是"= default")的非内联虚函数。

from JaMIT的 answer。 如果有其他人尝试了以上所有方法,但都不起作用,可能你也像我一样,不小心忘记在一个或多个成员函数前加上<ClassName>::前缀

要么我需要检查一下眼睛,要么我需要睡一会儿。

对于Qt用户使用CMakeList.txt

在CMakeLists.txt: set(CMAKE_AUTOMOC ON)中添加这一行

克里斯·莫勒报道如果你忘记moc一个头,你会得到这个错误

当我试图实现抽象工厂模式时,我也有这个问题,但忘记了链接一些库。因此,如果没有帮助喷射,检查是否所有必需的库都被链接

FWIW我能够避免以下错误:

 ld: /usr/local/lib/libvmaf.a(svm.cpp.o):(.data.rel.ro._ZTI7QMatrix[_ZTI7QMatrix]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'

当连读"C"项目的静态库从c++ "项目,通过将-lstdc++添加到链接参数。所以它是gcc -lstdc++,现在它工作了。

最常见的方法是将-lstdc++添加到库的pkgconfig .pc文件库列表中。或者改用g++链接。

这是GCC中的一个错误特性。也就是说,g++编译器本身不能抱怨未定义的虚方法,因为它们可以在其他地方定义。但是-它不存储关于缺少哪些虚拟成员的信息;它只存储了一个und定义的虚表符号,链接器会抱怨。

相反,如果要列出缺少的成员,则链接器可以告诉您它们是什么。

针对GCC: 错误42540有一个公开的错误。不幸的是,它已经13岁了。