C++获取${workspaceFolder}及全局变量多定义问题与优化咨询
项目结构如下:
- root: - Module1 - Module2 - Module3 - main.cpp
main.cpp实例化Module1的类,该类包含Module2和Module3的成员。Module3的文件读取函数使用相对路径时会出错,因为相对路径基于可执行文件的启动目录,而非文件所在目录。
最初在main里实现了获取根路径的代码:
static std::string rootFolder{""}; int main(int argc, char *argv[]) { char buffer[MAX_PATH]; GetModuleFileNameA(NULL, buffer, MAX_PATH); std::string::size_type pos = std::string(buffer).find_last_of("\\/"); rootFolder = std::string(buffer).substr(0, pos) + "/../"; return EXIT_SUCCESS; }
注:仅实现Windows平台代码。
为了把rootFolder传递给Module3,我创建了globals.hpp全局变量文件(代码量少未写cpp实现):
#ifndef APP_GLOBALS #define APP_GLOBALS #include <string> #include <Windows.h> #include <string> static std::string rootFolder{""}; inline void initGlobals() { char buffer[MAX_PATH]; GetModuleFileNameA(NULL, buffer, MAX_PATH); std::string::size_type pos = std::string(buffer).find_last_of("\\/"); rootFolder = std::string(buffer).substr(0, pos) + "/../"; } #endif
在main.cpp和Module3的源文件中都包含了这个头文件,main里调用initGlobals(),但编译时出现多定义错误:
cmd.exe /C "cd . && C:\PROGRA~1\mingw64\bin\G__~1.EXE -g CMakeFiles/VulkanTriangle.dir/src/app.cpp.obj CMakeFiles/VulkanTriangle.dir/src/main.cpp.obj CMakeFiles/VulkanTriangle.dir/src/pipeline.cpp.obj CMakeFiles/VulkanTriangle.dir/src/core/window.cpp.obj -o ..\bin\VulkanTriangle.exe -Wl,--out-implib,libVulkanTriangle.dll.a -Wl,--major-image-version,0,--minor-image-version,0 -LD:/Documents/programming/vulkanTriangle/lib -lglfw3 -lvulkan-1 -lkernel32 -luser32 -lgdi32 -lwinspool -lshell32 -lole32 -loleaut32 -luuid -lcomdlg32 -ladvapi32 && cd ." CMakeFiles/VulkanTriangle.dir/src/pipeline.cpp.obj:D:/Documents/programming/vulkanTriangle/src/globals.hpp:10: multiple definition of `initGlobals()' CMakeFiles/VulkanTriangle.dir/src/main.cpp.obj:D:/Documents/programming/vulkanTriangle/src/globals.hpp:10: first defined here collect2.exe: error: ld returned 1 exit status ninja: build stopped: subcommand failed.
给initGlobals()添加inline关键字后问题解决,但有三个疑问:
- 添加
inline为何能解决多定义错误?我理解的头文件保护逻辑是这样的:
#ifndef MY_SUPER_UNIQUE_HEADER // 如果没定义,就执行下面的代码 #define THAT_HEADER #endif
- 出现多定义错误说明我对头文件加载逻辑理解有误,请解释该逻辑。
- 是否有更优方案初始化并传递rootFolder变量到Module3?
1. 为什么inline能解决多定义错误?
头文件保护(#ifndef/#define)只是防止单个编译单元(.cpp文件)里重复包含同一个头文件,完全解决不了多编译单元的多定义问题。
而C++标准规定:inline函数可以在多个编译单元中存在定义,只要所有定义的代码完全一致,链接器就会自动合并这些重复的定义,只保留一份。这样就不会触发“multiple definition”错误。
你之前没加inline时,每个包含globals.hpp的.cpp文件都会生成一个initGlobals()的定义,链接时多个定义冲突;加了inline后,链接器会处理这些重复定义,只留一份,错误就消失了。
另外注意:你头文件里的static std::string rootFolder其实是每个编译单元都有一份独立的副本,这可能不是你想要的——如果Module3修改了这个变量,main里的rootFolder不会同步。如果要真正的全局变量,应该用extern声明,在cpp里定义。
2. 头文件加载与多定义的逻辑
C++编译过程分两步:编译和链接:
- 编译阶段:每个.cpp文件是独立的编译单元,编译器会把
#include的头文件内容直接复制到当前.cpp里,然后编译成.obj目标文件。头文件保护只是防止同一个.cpp里多次复制同一个头文件的内容,但如果多个.cpp都包含同一个头文件,每个.cpp里都会有头文件里的函数/变量定义。 - 链接阶段:链接器会把所有.obj文件合并成最终的可执行文件。如果同一个函数/全局变量在多个.obj里都有定义,链接器就会抛出“multiple definition”错误——因为它不知道该用哪一份。
你之前的问题就是:initGlobals()在头文件里定义(没加inline),main.cpp和pipeline.cpp都包含了这个头文件,编译后两个.obj里都有initGlobals()的定义,链接时冲突。
3. 更优的方案推荐
方案一:避免全局变量,用依赖注入
这是最符合模块化设计的方案:
- 在
main里计算出rootFolder,然后把它作为参数传递给Module1的类构造函数,Module1再传递给Module3的类构造函数,或者直接在调用Module3的文件读取函数时传入路径。 - 示例:
优点:没有全局变量,代码耦合度低,测试更方便(可以随意传入不同路径测试)。// main.cpp int main() { std::string rootFolder = /* 计算根路径 */; Module1 module1(rootFolder); // 把路径传给Module1 // ... } // Module1的构造函数 Module1::Module1(const std::string& root) : module3(root) {} // 传递给Module3 // Module3的构造函数 Module3::Module3(const std::string& root) : m_root(root) {} // Module3的文件读取函数 void Module3::readFile() { std::string filePath = m_root + "data/file.txt"; // 读取文件 }
方案二:把全局变量和函数放到cpp文件中
如果一定要用全局变量,正确的做法是:
globals.hpp里只声明,不定义:#ifndef APP_GLOBALS #define APP_GLOBALS #include <string> extern std::string rootFolder; // 声明全局变量,不定义 void initGlobals(); // 声明函数 #endif- 创建
globals.cpp实现:
这样整个程序里#include "globals.hpp" #include <Windows.h> std::string rootFolder{""}; // 真正定义全局变量 void initGlobals() { char buffer[MAX_PATH]; GetModuleFileNameA(NULL, buffer, MAX_PATH); std::string::size_type pos = std::string(buffer).find_last_of("\\/"); rootFolder = std::string(buffer).substr(0, pos) + "/../"; }rootFolder和initGlobals()只有一份定义,链接时不会冲突,而且所有编译单元共享同一个rootFolder变量。
方案三:单例模式封装
把根路径封装成一个单例类,保证全局只有一个实例,第一次调用时初始化:
// path_manager.hpp #ifndef PATH_MANAGER #define PATH_MANAGER #include <string> class PathManager { public: static const std::string& getRootFolder() { static PathManager instance; return instance.m_rootFolder; } private: PathManager() { // 在这里初始化根路径 char buffer[MAX_PATH]; GetModuleFileNameA(NULL, buffer, MAX_PATH); std::string::size_type pos = std::string(buffer).find_last_of("\\/"); m_rootFolder = std::string(buffer).substr(0, pos) + "/../"; } std::string m_rootFolder; }; #endif
使用时直接调用PathManager::getRootFolder()即可,不需要手动初始化,而且C++11及以上版本中静态局部变量的初始化是线程安全的。
内容的提问来源于stack exchange,提问作者Wusiki Jeronii

