为什么C++虚函数在链接时未定义?

8

我在使用C++中的虚函数时遇到了一些麻烦,可能是在构造函数中错误地使用了它们。问题是,在将一个由我编写的组件库链接到我的最终可执行文件时,虚函数被标记为未定义,尽管我已经为其编写了实现并链接。

我有以下类:

template<class BufferType, class ConnectionType, class HandlerType>
class UdpConnection
{
public:
UdpConnection(size_t dispatchCount) : service(),
        listener(service),
        pool(dispatchCount), sysMsgHandlers(),
        bufferPool(), buffers()
    {
        assert(dispatchCount > 0);
        initBuffers(dispatchCount);
        initSysHandlers();
    }
protected:
    virtual void initSysHandlers() = 0;
}

在我的子类中:
class UdpClient : public UdpConnection<SyncBufferHandler, UdpClient, ClientNetworkHandler>
{
    protected:
        void initSysHandlers();
}

子类源文件:

void UdpClient::initSysHandlers()
{

}

正如您所看到的,我在构造函数中调用了一个虚函数。据我所知,这应该是可以的,因为我知道我的子类构造函数还没有被调用,所以我不能使用任何实例变量,但我只是向std::map添加了一些子类特定的项。

Linking CXX static library libnetwork.a
[ 75%] Built target network                                                                                           
Scanning dependencies of target testclient
[ 87%] Building CXX object CMakeFiles/testclient.dir/src/test/testclient.cpp.o                                        
Linking CXX executable testclient                                                                                     
src/network/libnetwork.a(udpclient.cpp.o): In function `voip::network::UdpConnection<voip::network::client::SyncBufferHandler, voip::network::client::UdpClient, voip::network::client::ClientNetworkHandler>::UdpConnection(unsigned long)':
udpclient.cpp:(.text._ZN4voip7network13UdpConnectionINS0_6client17SyncBufferHandlerENS2_9UdpClientENS2_20ClientNetworkHandlerEEC2Em[voip::network::UdpConnection<voip::network::client::SyncBufferHandler, voip::network::client::UdpClient, voip::network::client::ClientNetworkHandler>::UdpConnection(unsigned long)]+0x10d): undefined reference to `voip::network::UdpConnection<voip::network::client::SyncBufferHandler, voip::network::client::UdpClient, voip::network::client::ClientNetworkHandler>::initSysHandlers()'
collect2: ld returned 1 exit status

这里我做错了什么?如果需要更多信息,请问!想尽可能保持简短!
3个回答

21
您正在从基类构造函数中调用虚函数。在构造和析构期间,有关虚函数分派的特殊规则:
实际上,在执行基类UdpConnection的构造函数时,对象的动态类型是UdpConnection,而不是UdpClient,因此所选的虚函数的最终覆盖者是UdpConnection,而不是最派生类UdpClient。
这意味着当您在UdpConnection构造函数中调用initSysHandlers()时,每次都会调用UdpConnection::initSysHandlers(),而不是最派生类中的重写。由于您没有提供UdpConnection::initSysHandlers()的定义,因此会出现链接器错误。
专家建议是"永远不要在构造或析构过程中调用虚函数"

1
好的,谢谢!我也觉得是这样,但错误信息实在是太不具体了(因为我一开始以为不能使用子类的任何成员)。有时候我希望编译器能提供更多描述性的提示! - Max
@Max:是的,有些编译器会在你从构造函数中调用虚函数时给出有用的警告。其他编译器则不太友好。 - James McNellis
我不小心点击了向下箭头,现在它已经修复了。我终于获得了我的评论家徽章 xD。 - ak.

3
不要在构造函数调用虚函数。构造函数从基类到派生类进行。因此,构造函数UdpConnection::UdpConnection还不知道该类已被继承。此时尚未形成派生类的vtable,也不知道要调用哪个重载函数的地址。

这实际上比“vtable尚未形成”要棘手得多。有一个有趣的例子涉及从初始化列表调用虚函数。 (https://dev59.com/kVPTa4cB1Zd3GeqPl72P) - James McNellis
我仍然相信这个问题与vtable有关。该问题发生在链接过程中,当对象文件在统一的地址空间中组合在一起时。希望有一天能了解更多关于C++内部的知识... - ak.

2

在构造函数或析构函数中调用虚拟函数是不合适的,因为它们没有预期的行为。直到构造函数返回之前,对象才被完全创建。

基类构造函数在派生类构造函数之前被调用;因此,基类数据成员和函数首先被创建和定义。所以在你的情况下,UdpConnection ctor 将尝试调用 UdpConnection::initSysHandlers,而不是 UdpClient::initSysHandlers,因为后者还没有被创建。由于 UdpConnection::initSysHandlers 是纯虚函数,在那个时候它是未定义的。


1
调用是否具有预期的行为取决于您对行为的预期。 - James McNellis
即使行为看起来符合预期,它仍然是错误的。 - cchampion

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接