【问题标题】:Mixing versions of the MSVCRTMSVCRT 的混合版本
【发布时间】:2025-12-11 00:05:01
【问题描述】:

所以,我有一个 C++ 库,其中包含 MSVCRT 的静态链接副本。我希望任何人都能够将我的库与任何版本的 MSVC 运行时一起使用。实现这一目标的最佳方法是什么?

我已经非常小心处理事情的方式了。

  1. 内存永远不会通过要释放的 DLL 屏障
  2. 运行时 C++ 对象不会跨越障碍(即矢量、地图等。除非它们是在障碍的那一侧创建的)
  3. 没有文件句柄或资源句柄在屏障之间传递

然而,我仍然有一些导致堆损坏的简单代码。

我的图书馆里有一个像这样的对象:

class Foos
{
public: //There is an Add method, but it's not used, so not relevant here
    DLL_API Foos();
    DLL_API ~Foos();

private:
    std::map<std::wstring, Foo*> map;
};

Foos::~Foos()
{
    // start at the begining and go to the end deleting the data object
    for(std::map<std::wstring, Foo*>::iterator it = map.begin(); it != map.end(); it++)
    {
        delete it->second;
    }
    map.clear();
}

然后我像这样在我的应用程序中使用它:

void bar() {
    Foos list;
}

从任何地方调用此函数后,我都会收到有关堆栈损坏的调试警告。如果我真的让它用完,它实际上会破坏堆栈和段错误。

我的调用应用程序是使用 Visual Studio 2012 平台工具编译的。该库是使用 Visual Studio 2010 平台工具编译的。

这只是我绝对不应该做的事情,还是我实际上违反了使用多个运行时的规则?

【问题讨论】:

  • 如果从析构函数中删除代码,错误是否仍然发生? (我问是因为这段代码应该是无操作的)
  • @anatolyg 是的,从析构函数中删除所有代码时,我仍然会遇到堆栈损坏
  • 它所做的一切都是创建map 字段(因为它不是指针)然后销毁它。这显然会导致堆栈损坏。如果我以 VS2010 为目标并以这种方式构建我的应用程序,它可以正常工作

标签: c++ visual-c++ msvcrt


【解决方案1】:

内存永远不会通过 DLL 屏障

但是,确实如此。事实上很多次。您的应用程序为类对象创建了存储,在本例中是在堆栈上。然后传递一个指向库中方法的指针。从构造函数调用开始。该指针是库代码中的 this

在这种情况下出现的问题是它没有创建正确的存储量。你得到了 VS2012 编译器来查看你的类声明。它使用 std::map 的 VS2012 实现。然而,您的库是用 VS2010 编译的,它使用了完全不同的 std::map 实现。具有完全不同的尺寸。得益于 C++11 的巨大变化。

这只是工作中的完全内存损坏,您的应用程序中写入堆栈变量的代码会损坏 std::map。反之亦然。

跨模块边界暴露 C++ 类充满了类似的陷阱。仅当您可以保证使用完全相同的编译器版本和完全相同的设置编译所有内容时才考虑它。没有捷径,你也不能混合调试和发布构建代码。制作库以便暴露实现细节当然是可能的,你必须遵守这些规则:

  • 仅用虚方法公开纯接口,参数类型必须是简单类型或接口指针。
  • 使用类工厂创建接口实例
  • 使用引用计数进行内存管理,因此始终由库释放。
  • 用硬性规则确定核心细节,例如打包和调用约定。
  • 绝不允许异常跨越模块边界,仅使用错误代码。

到那时你会很好地编写 COM 代码,以及你在 DirectX 中看到的样式。

【讨论】:

    【解决方案2】:

    map 成员变量仍然由应用程序创建,其中一些内部数据由应用程序而不是 DLL 分配(它们可能使用 map 的不同实现)。根据经验,不要使用 DLL 中的堆栈对象,在 DLL 中添加类似 Foos * CreateFoos() 的内容。

    【讨论】:

    • 嗯,它基本上包装了所有的分配。我试过new Foos(),然后delete list会出现堆损坏
    • @Earlz:它比这更复杂,一般来说,您不想公开任何可能具有不同二进制布局或可能通过 CRT 功能(和 STL 子对象)分配内存的数据结构的内部结构两者都有不同的二进制布局使用new分配内存)。实际上,这意味着您几乎必须在任何地方都使用 PIMPL 成语。
    • @MatteoItalia 实际上(你的评论)是最好的答案!
    【解决方案3】:

    运行时 C++ 对象不会跨越障碍(即向量、映射、 等等..除非它们是在屏障的那一侧创建的)

    你正是这样做的。您的 Foos 对象由堆栈上的主程序创建,然后在库中使用。该对象包含一个地图作为它的一部分...

    当您编译主程序时,它会查看头文件等以确定为 Foos 对象分配多少堆栈空间。并调用库中定义的构造函数......这可能期望对象的布局/大小完全不同

    【讨论】:

    • 请注意,这不仅取决于 CRT 版本,甚至取决于编译标志。在调试或发布模式下编译时,STL 对象确实具有不同的内存布局。
    • 所以,如果我要更改 map 位,使其成为一个指针,并在构造函数/析构函数中分配和释放,它可能没问题,因为这样 DLL 会控制分配...我认为即使在头文件中列出 private 字段也是可选的,但猜想它是分配所必需的。
    • 是的,这可能会奏效。不列出私有字段不是可选的:)
    【解决方案4】:

    它可能不符合您的需求,但不要忘记在头文件中实现整个内容可以简化问题(有点):-)

    【讨论】: