You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++动态库链接疑问:继承为何能消除未定义引用错误?

问题:动态加载共享库时,继承空基类为何能避免链接错误?

可正常运行的原始代码示例

头文件 classes.hpp

// classes.hpp
#pragma once

struct Base {
    virtual ~Base() = default;
    virtual void tell() = 0;
};

struct Derived : Base {
    Derived() = default;
    ~Derived() = default;
    void tell() override;
};

实现文件 classes.cpp

// classes.cpp
#include <iostream>
#include "classes.hpp"

extern "C" {
Derived* instantiate()
{
    return new Derived{};
}

void deinstantiate(Derived* ptr)
{
    delete ptr;
}
}

void Derived::tell()
{
    std::cout << "Hello!" << std::endl;
}

编译共享库命令

g++ -fPIC -shared -o classes.so classes.cpp

主程序 main.cpp

// main.cpp
#include <iostream>
#include <dlfcn.h>

#include "classes.hpp"


int main()
{
    using inst_t = Derived*(*)(void);
    using deinst_t = void(*)(Derived*);

    void* handle = dlopen("./classes.so", RTLD_NOW);
    inst_t inst = reinterpret_cast<inst_t>(dlsym(handle, "instantiate"));
    deinst_t deinst = reinterpret_cast<deinst_t>(dlsym(handle, "deinstantiate"));
    // some tests here...

    Derived* instance = inst();
    instance->tell();
    deinst(instance);
    dlclose(handle);

    return EXIT_SUCCESS;
}

主程序编译命令

g++ -ldl -o main main.cpp

异常场景描述

当移除Derived对Base的继承,修改头文件为以下内容时:

// new classes.hpp
#pragma once

struct Derived {
    Derived() = default;
    ~Derived() = default;
    void tell();
};

编译主程序会触发链接错误:

/usr/bin/ld: /tmp/ccLuFAZr.o: in function `main':
main.cpp:(.text+0x66): undefined reference to `Derived::tell()'
collect2: error: ld returned 1 exit status

核心疑问:已知无继承时链接错误是因为编译器需在编译期解析Derived::tell(),但为何继承带纯虚函数的Base就能规避该错误?由此提出两个问题:

  1. 这种情况的原理是什么?
  2. 有没有不使用(伪)继承的其他解决方法?

问题解答

1. 继承避免链接错误的原理

当Derived继承包含纯虚函数的Base后,Derived::tell()成为虚函数的重写,C++编译器对虚函数和普通成员函数的调用处理逻辑完全不同:

  • 普通成员函数是静态绑定:编译期就会确定调用地址,编译器会尝试在当前编译单元或已链接库中查找函数定义,找不到则直接报链接错误。
  • 虚函数是动态绑定:调用地址通过对象的虚表(vtable)在运行时确定,编译主程序时,编译器仅会确认Derived存在虚表,不会强制解析tell()的具体地址——实际调用地址会在运行时从共享库的虚表中获取。

简言之,继承让tell()变成虚函数,编译器知道该函数地址需延迟到运行时确定,因此不会在编译阶段强制要求找到其定义,自然避免了链接错误。

2. 不使用继承的替代方法

无需依赖伪继承,有两种稳妥的替代方案:

方案一:直接将tell()声明为虚函数

即使没有基类,给Derived::tell()加上virtual关键字,编译器就会按虚函数逻辑处理,编译主程序时不会强制查找函数定义:

// 修改后的classes.hpp
#pragma once

struct Derived {
    Derived() = default;
    ~Derived() = default;
    virtual void tell(); // 声明为虚函数
};

方案二:通过导出的函数指针间接调用

在共享库中导出tell()的包装函数,主程序通过dlsym获取函数指针后间接调用:

  • 修改classes.cpp,新增导出的包装函数:
// classes.cpp新增extern "C"代码块内容
extern "C" {
Derived* instantiate() { return new Derived{}; }
void deinstantiate(Derived* ptr) { delete ptr; }
void derived_tell(Derived* ptr) { ptr->tell(); } // 新增包装函数
}
  • 修改主程序,获取并调用函数指针:
// main.cpp中新增函数指针类型
using tell_t = void(*)(Derived*);
// ...
tell_t tell_func = reinterpret_cast<tell_t>(dlsym(handle, "derived_tell"));
// ...
tell_func(instance); // 替代instance->tell()

内容的提问来源于stack exchange,提问作者MaPo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 18:34:59