当构建我的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

当前回答

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

什么是虚表?

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

A vtable is basically the most common implementation of polymorphism in C++. When vtables are used, every polymorphic class has a vtable somewhere in the program; you can think of it as a (hidden) static data member of the class. Every object of a polymorphic class is associated with the vtable for its most-derived class. By checking this association, the program can work its polymorphic magic. Important caveat: a vtable is an implementation detail. It is not mandated by the C++ standard, even though most (all?) C++ compilers use vtables to implement polymorphic behavior. The details I am presenting are either typical or reasonable approaches. Compilers are allowed to deviate from this!

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

The name "vtable" comes from "virtual function table". It is a table that stores pointers to (virtual) functions. A compiler chooses its convention for how the table is laid out; a simple approach is to go through the virtual functions in the order they are declared within class definitions. When a virtual function is called, the program follows the object's pointer to a vtable, goes to the entry associated with the desired function, then uses the stored function pointer to invoke the correct function. There are various tricks for making this work, but I won't go into those here.

在哪里/何时生成虚表?

A vtable is automatically generated (sometimes called "emitted") by the compiler. A compiler could emit a vtable in every translation unit that sees a polymorphic class definition, but that would usually be unnecessary overkill. An alternative (used by gcc, and probably by others) is to pick a single translation unit in which to place the vtable, similar to how you would pick a single source file in which to put a class' static data members. If this selection process fails to pick any translation units, then the vtable becomes an undefined reference. Hence the error, whose message is admittedly not particularly clear.

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

The selection process used by gcc makes sense if we start with the tradition of devoting a (single) source file to each class that needs one for its implementation. It would be nice to emit the vtable when compiling that source file. Let's call that our goal. However, the selection process needs to work even if this tradition is not followed. So instead of looking for the implementation of the entire class, let's look for the implementation of a specific member of the class. If tradition is followed – and if that member is in fact implemented – then this achieves the goal.

The member selected by gcc (and potentially by other compilers) is the first non-inline virtual function that is not pure virtual. If you are part of the crowd that declares constructors and destructors before other member functions, then that destructor has a good chance of being selected. (You did remember to make the destructor virtual, right?) There are exceptions; I'd expect that the most common exceptions are when an inline definition is provided for the destructor and when the default destructor is requested (using "= default").

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

总之,有三个关键原因导致“未定义的引用虚表”错误:

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

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

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

如何修复错误?

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

Look at your class definition. Find the first non-inline virtual function that is not pure virtual (not "= 0") and whose definition you provide (not "= default"). If there is no such function, try modifying your class so there is one. (Error possibly resolved.) See also the answer by Philip Thomas for a caveat. Find the definition for that function. If it is missing, add it! (Error possibly resolved.) If the function definition is outside the class definition, then make sure the function definition uses a qualified name, as in ClassName::function_name. Check your link command. If it does not mention the object file with that function's definition, fix that! (Error possibly resolved.) Repeat steps 2 and 3 for each virtual function, then for each non-virtual function, until the error is resolved. If you're still stuck, repeat for each static data member.

例子 具体操作的细节可能会有所不同,有时还会分成不同的问题(比如什么是未定义的引用/未解决的外部符号错误以及如何修复它?)不过,我将提供一个在特定情况下该如何做的示例,这可能会使新程序员感到困惑。

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

virtual ~A() = default;

or

virtual ~A() {}

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

virtual ~A();

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

A::~A() {}

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

其他回答

你确定CDasherComponent有一个析构函数体吗?它肯定不在这里——问题是它是否在.cc文件中。 从样式的角度来看,CDasherModule应该显式地定义它的析构函数虚函数。 看起来CGameModule在末尾有一个额外的}(在}之后;//类)。 CGameModule是否被链接到定义CDasherModule和CDasherComponent的库中?

我尝试了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中所做的事情当然应该通过这个技巧来解决

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

JaMIT的回答。 如果有其他人尝试了以上所有方法,但没有任何效果,可能您也像我一样,不小心忘记给一个或多个成员函数加上<ClassName>::前缀。

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

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

如:

virtual void fooBar() = 0;

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

我认为值得一提的是,当你试图链接到任何至少有一个虚方法的类的对象,而链接器无法找到该文件时,你也会得到这样的消息。 例如:

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

给出我们最喜欢的错误:

/ tmp / cclKnW0g。o:在函数main': main.cpp:(.text+0x1a):未定义 参考vtable Foo' collect2: error: ld返回1退出 状态

我的理解是这样的:

Vtable在编译时为每个类创建 链接器不能访问Foo.o中的虚表

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