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. 继承避免链接错误的原理
当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
相关产品推荐
相关产品推荐

