• 根据 C++ 标准 ODR 规则,inline函数内的静态局部变量经多翻译单元链接合并后,全局仅存在唯一实例。
  • Windows DLL 机制会破坏该标准语义:可执行文件(EXE)与动态链接库(DLL)分属独立模块,拥有隔离的符号寻址空间;链接器不会跨模块合并inline函数内的静态局部变量,每个模块会各自生成一份独立变量副本。
  • 该特性会严重破坏基于inline静态局部变量实现的单例模式,丢失 “全局唯一实例” 核心约束,极易引发实例未初始化、多模块读写不同实例、程序异常崩溃等未定义行为。

最小复现案例

singleton.h

#pragma once
#include <string>
#include <iostream>

class Singleton {
public:
  // ❌ 问题:inline 定义在头文件中
  static Singleton& instance() {
    static Singleton obj;  // EXE 和 DLL 各有一份
    return obj;
  }

  void set_name(const std::string& name) {
    name_ = name;
  }

  const std::string& name() const {
    return name_;
  }

  void print() {
    std::cout << "Singleton address: " << this << std::endl;
    std::cout << "name = \"" << name_ << "\"" << std::endl;
  }

private:
  Singleton() = default;
  std::string name_{ "<未初始化>" };
};

test.cpp

#include "singleton.h"
#include <iostream>

// 导出函数,供 EXE 调用
__declspec(dllexport) void dll_use_singleton() {
  std::cout << "--- DLL 侧访问单例 ---" << std::endl;
  Singleton::instance().print();
  // 此时 name_ 仍然是 "<未初始化>",因为 DLL 侧是另一个实例
}

main.cpp

#include "singleton.h"
#include <iostream>
#include <windows.h>

__declspec(dllimport) void dll_use_singleton();

int main() {
  SetConsoleOutputCP(CP_UTF8);
  std::cout << "--- EXE 侧设置单例 ---" << std::endl;
  Singleton::instance().set_name("Hello from EXE");
  Singleton::instance().print();

  std::cout << std::endl;

  // 调用 DLL 函数,DLL 内部也访问单例
  dll_use_singleton();

  return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(singleton_test LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

if(MSVC)
    add_compile_options(/utf-8)
endif()

# 构建 DLL
add_library(my_dll SHARED test.cpp)

# 构建 EXE
add_executable(main_app main.cpp)
target_link_libraries(main_app PRIVATE my_dll)

程序运行结果:

--- EXE 侧设置单例 ---
Singleton address: 00007FF637CD3518
name = "Hello from EXE"

--- DLL 侧访问单例 ---
Singleton address: 00007FFBD64D3508
name = "<未初始化>"

可以看到,单例地址不同,这说明主函数中访问的单例与dll访问的单例并不是同一个。

修复方案

将单例的声明与实现分开,singleton.h修改为:

#pragma once
#include <string>
#include <iostream>

#ifdef my_dll_EXPORTS
  #define SINGLETON_API __declspec(dllexport)
#else
  #define SINGLETON_API __declspec(dllimport)
#endif

class SINGLETON_API Singleton {
public:
  static Singleton& instance();

  void set_name(const std::string& name) {
    name_ = name;
  }

  const std::string& name() const {
    return name_;
  }

  void print() {
    std::cout << "Singleton address: " << this << std::endl;
    std::cout << "name = \"" << name_ << "\"" << std::endl;
  }

private:
  Singleton() = default;
  std::string name_{ "<未初始化>" };
};

添加singleton.cpp:

#include "singleton.h"

Singleton& Singleton::instance() {
  static Singleton obj;
  return obj;
}

同时修改CMakeLists.txt为:

cmake_minimum_required(VERSION 3.14)
project(singleton_test LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

if(MSVC)
    add_compile_options(/utf-8)
endif()

# 构建 DLL
add_library(my_dll SHARED test.cpp singleton.cpp)

# 构建 EXE
add_executable(main_app main.cpp)
target_link_libraries(main_app PRIVATE my_dll)

程序运行结果:

--- EXE 侧设置单例 ---
Singleton address: 00007FFBBBD84508
name = "Hello from EXE"

--- DLL 侧访问单例 ---
Singleton address: 00007FFBBBD84508
name = "Hello from EXE"

可以看到,此时单例全局仅存在一份。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐