多个翻译单元中静态变量的多次构造和销毁

问题描述 投票:0回答:1

在 C++11 代码中,我有一个带有重要构造函数和析构函数(分配内存)的静态变量:

// in logger.h
class logger {
public:
    class log_data
    {
        // log_data allocates and frees memory during construction/destruction
    }

    static log_data l;
};
// in logger.cpp
logger::log_data logger::l;
然后,

logger.cpp 被编译为共享库和链接到共享库本身的可执行文件。我希望链接器能够识别 logger::l 被编译了两次并“合并”两个副本。

问题是,在运行时,这就是我认为正在发生的事情:

  1. 在调用 main() 之前,可执行文件通过调用构造函数在给定的内存地址上初始化 logger::l
  2. 库做了同样的事情,在相同的内存地址上,覆盖它
  3. main() 正常执行
  4. 库删除对象
  5. 可执行文件删除同一地址的对象

我很确定是这样,因为:

  • 在 x86 上,valgrind 检测到 log_data 构造函数中的内存块已被分配,并且在程序退出时肯定会丢失(这是有道理的,因为第一个初始化被第二个初始化覆盖)。由于某种原因,它不会抱怨程序退出时的双重破坏
  • 在armv7上,打印“double free or Corruption (fasttop)”后main()完成后程序出现段错误。我在该平台上没有可用的 valgrind 来仔细检查分配,但 gdb 在 main 退出后报告损坏的堆栈。

我的问题是:有没有办法告诉编译器,无论编译了多少个库或可执行文件,静态变量都只能构造和析构一次?还有其他解决办法吗?

我尝试从可执行编译中删除 logger.cpp,并将其仅与 1 个包含 logger.cpp 的共享库链接。它可以正常工作,但问题仍然存在,因为 logger.cpp 可以包含在多个共享库中,所有共享库都由同一个可执行文件使用

谢谢你

c++ static shared-libraries
1个回答
0
投票

可执行文件和共享库都将获得自己的

logger::log_data logger::l;
静态变量。您可以使用
nm
之类的工具来确认这一点。

我已经使用以下内容设置了基本的本地复制:

// foo.hpp
void foo();

// foo.cpp
#include "foo.hpp"

void foo() {}

// log.hpp
#include <iostream>

class logger {
public:
    logger() {
        std::cout<<"logger()"<<std::endl;
    }
    ~logger() {
        std::cout<<"~logger()"<<std::endl;
    }
    void log() {}
};

extern logger logger_global;

// log.cpp
#include "log.hpp"

logger logger_global;

// log_internal.cpp
#include "log.hpp"

void logger::log() {}

// main.cpp
#include <iostream>

#include "log.hpp"
#include "foo.hpp"

int main() {
    std::cout<<"main "<<std::endl;
    logger_global.log();
    foo();
}

// CMakeLists.txt
cmake_minimum_required(VERSION 3.14)

project(test LANGUAGES C CXX)

add_library(foo SHARED log.cpp foo.cpp)

add_executable(exec main.cpp log.cpp)
target_link_libraries(exec PUBLIC foo)

运行可执行文件:

$ ./exec
logger()
logger()
main 
~logger()
~logger()

看纳米:

$ nm exec | grep logger_global
0000000000001244 t _GLOBAL__sub_I_logger_global
0000000000004151 B logger_global
$ nm libfoo.so | grep logger_global
0000000000004031 B logger_global

可执行文件和库都有自己的

logger_global
,并且各自进行自己的静态构造和销毁。与静态库不同,链接器不能仅从共享库中获取所需的内容。整个共享库必须加载到内存中,并且作为其静态数据的一部分必须进行初始化。在调试器中运行,您可以验证是否使用相同的
logger::logger
指针调用了两次
this

您可能想知道

_GLOBAL__sub_I_logger_global
是什么以及为什么只有可执行文件才有它。该函数由编译器自动生成,并对全局记录器执行静态初始化。共享库实际上也有一个,但是,它有一个不同的名称:
_GLOBAL__sub_I_log.cpp

该怎么办?

您应该将通用记录器代码提取到其自己的库中,并使您的库和可执行文件链接到记录器库。

© www.soinside.com 2019 - 2024. All rights reserved.