为什么这个程序中存在内存泄漏,我该如何解决它,给定约束(对包含 std::string 的对象使用 malloc 和 free)?[复制]

Why is there a memory leak in this program and how can I solve it, given the constraints (using malloc and free for objects containing std::string)? [duplicate]

提问人:Anurag Vohra 提问时间:3/1/2023 最后编辑:Peter MortensenAnurag Vohra 更新时间:3/6/2023 访问量:5375

问:

这是我在实际代码中面临的问题的最小工作示例。

#include <iostream>

namespace Test1 {
    static const std::string MSG1="Something really big message";
}

struct Person{
    std::string name;
};

int main() {
    auto p = (Person*)malloc(sizeof(Person));
    p = new(p)Person();
    p->name=Test1::MSG1;

    std::cout << "name: "<< p->name << std::endl;

    free(p);

    std::cout << "done" << std::endl;

    return 0;
}

当我编译它并通过 Valgrind 运行它时,它给了我这个错误:

绝对丢失:1 个区块 31 个字节


约束

  1. 我一定会在上面的示例中使用,因为在我的实际代码中,我在 C++ 项目中使用了一个 C 库,它在内部使用它。因此,我无法摆脱使用,因为我没有在代码中的任何地方明确地使用它。mallocmallocmalloc
  2. 我需要在我的代码中一次又一次地重新分配。std::string namePerson
C++ 内存泄漏 valgrind dynamic-memory-allocation placement-new

评论

23赞 HolyBlackCat 3/1/2023
您必须在调用析构函数之前调用。free
18赞 Some programmer dude 3/1/2023
执行 placement-new 时,必须显式调用对象析构函数。就像不构造对象一样,不破坏对象。mallocfree
7赞 PaulMcKenzie 3/1/2023
这是最小的工作示例 -- 你忘记了#include <string>#include <cstdlib>
6赞 Peter 3/1/2023
@PaulMcKenzie 不过,这是一个可以理解的疏忽 - 一些(尽管不是全部)现实世界的编译器/库已经包含在内(标准既不要求也不阻止它)。<string><cstdlib><iostream>
3赞 Toby Speight 3/2/2023
@PaulSanders,我认为没有必要,因为它返回的内存适当对齐,以便可以将其分配给指向具有基本对齐要求的任何类型的对象的指针(当然,或者是空指针)。alignasstd::malloc()

答:

35赞 HolyBlackCat 3/1/2023 #1

您必须在以下之前手动调用析构函数:free(p);

p->~Person();

或者,这是一回事。std::destroy_at(p)

55赞 463035818_is_not_an_ai 3/1/2023 #2

代码中的重要部分逐行...

为一个 Person 对象分配内存:

auto p = (Person*)malloc(sizeof(Person));

通过调用其构造函数,在已分配的内存中构造一个 Person 对象:

p = new(p)Person();

释放通过 malloc 分配的内存:

free(p);

通过放置调用构造函数会创建一个 .该字符串将在析构函数中被销毁,但析构函数永远不会被调用。 不调用析构函数(就像不调用构造函数一样)。newstd::stringfreemalloc

malloc仅分配内存。Placement new 仅在已分配的内存中构造对象。因此,您需要在调用 .这是我所知道的唯一一种情况,其中显式调用析构函数是正确且必要的:free

auto p = (Person*)malloc(sizeof(Person));
p = new(p)Person();
p->~Person();
free(p);

评论

2赞 stefaanv 3/1/2023
关于代码中注释的讨论总是有点棘手,但对我来说,我期望代码中唯一注释是将 placement-new 与 malloc 一起使用的原因(分配器、练习......
22赞 Roger Lipscombe 3/2/2023
为了真正清楚地说明泄漏的内容:因为析构函数没有被调用,所以没有被解构,所以分配的内存没有被解除分配。也就是说:正在被释放,但它指向的任何记忆都没有PersonPerson::namestd::stringPerson
2赞 463035818_is_not_an_ai 3/2/2023
@AnsonSavage没有任何约束,他们可能不会对基本上已经管理动态分配内存的东西使用动态分配std::string
1赞 Daniel Schepler 3/3/2023
@AnsonSavage我认为 valgrind 和地址消毒剂都会给出一个错误,即如果您尝试这样做,则会出现错误。mallocoperator delete
1赞 ShadowRanger 3/3/2023
@AnsonSavage:可以在分配的项目之前添加特定的元数据,不同的(通常是扩展或替换)元数据是走私的。 依靠该元数据来正确执行清理。如果 并且不同意确切的元数据结构,那么 和 也会,当你混合它们时会发生可怕的事情。newmallocdeletenewmallocdeletefree
11赞 jxh 3/2/2023 #3

正如其他答案中提到的,泄漏的来源是 成员的析构函数没有被调用。当调用析构函数 for 时,通常会隐式调用它。但是,永远不会被破坏。实例的内存只需使用 释放。namePersonPersonPersonPersonfree

因此,正如您必须在 之后使用位置显式调用构造函数一样,您还需要在 之前显式调用析构函数。newmallocfree

还可以考虑重载 and 运算符。newdelete

struct Person {
    std::string name;
    void * operator new (std::size_t sz) { return std::malloc(sz); }
    void operator delete (void *p) { std::free(p); }
};

这样,您可以使用 和 通常,当他们在下面时,他们会使用 和 .newdeletemallocfree

int main (void) {
    auto p = new Person;
    //... 
    delete p;
}

这样,您可以更自然地使用智能指针。

int main (void) {
    auto p = std:make_unique<Person>();
    //... unique pointer will delete automatically
}

当然,您可以将自定义删除器与显式调用 和 一起使用,但这会更麻烦,并且您的删除器仍然需要知道显式调用析构函数。unique_ptrmallocfree

评论

2赞 Erel 3/2/2023
虽然我可能弄错了,但我从这个问题中了解到,OP 已经从 C API 获取了存储,因此必须使用放置来构造其中的对象,即 OP 不负责分配。如果我是对的,我认为覆盖在他的应用程序中没有意义。mallocnewoperator new / delete
3赞 Hans Olsson 3/2/2023
请注意,运算符 new 获取一个大小作为输入 - 因此您通常应该使用它而不是计算 sizeof(Person);例如,它被任何派生类使用(我不期望任何)。
1赞 jxh 3/2/2023
@Erel 最小的示例并没有说明确切的用例,但该示例让我们相信 OP 正在调用 API 来获取对象的内存,然后释放该对象。这些活动恰恰是超载的,可以为您做。newdelete
0赞 jxh 3/3/2023
@HansOlsson已修复。当重载 和 时,我通常假设编码人员可能会对每个对象执行重载,包括 derived。我的代码通常会提供的大小与正在分配的大小相匹配。newdeleteassertclass
6赞 Davislor 3/2/2023 #4

正如其他人所提到的,由 成员分配的动态内存仅由 析构函数释放,析构函数不调用。Person~Personfree()

如果您必须将此函数与需要一些初始化和清理的库一起使用,而不是默认的库,例如此处,一种方法是定义一个新的删除器,供标准 libray 智能指针使用: 即使使用您自己未分配的内存块,这也将起作用。

#include <memory>
#include <new> // std::bad_alloc
#include <stdlib.h>
#include <string>

struct Person{
    std::string name;
};

struct PersonDeleterForSomeLib {
  constexpr void operator()(Person* ptr) const noexcept {
    ptr->~Person();
    free(ptr);
  }
};


Person* Person_factory() // Dummy for the foreign code.
{
  Person* const p = static_cast<Person*>(malloc(sizeof(Person)));
  if (!p) {
    throw std::bad_alloc();
  }
  new(p) Person();
  return p;
}

这使您可以安全地使用:

const auto p =
  std::unique_ptr<Person, PersonDeleterForSomeLib>(Person_factory());

具有自动内存管理功能。您可以从函数中返回智能指针,并且析构函数 和 都将在其生存期结束时被调用。您也可以以这种方式创建。如果由于某种原因需要在智能指针仍处于活动状态时销毁对象,则可以或它。free()std::shared_ptrresetrelease

评论

0赞 Peter Mortensen 3/3/2023
“STL”是某种同义词?不是字面上的STL
0赞 Davislor 3/3/2023
@PeterMortensen我稍微清理了一下上面的措辞(以及哪些内存正在泄漏)。
31赞 Matthieu M. 3/2/2023 #5

查明问题所在

首先,让我们通过说明每个语句后的内存状态来明确问题到底是什么。

int main() {
    auto p = (Person*)malloc(sizeof(Person));

    //  +---+    +-------+
    //  | p | -> | ~~~~~ |
    //  +---+    +-------+

    p = new(p)Person();

    //  +---+    +-------+
    //  | p | -> | name  |
    //  +---+    +-------+

    p->name=Test1::MSG1;

    //  +---+    +-------+    +---...
    //  | p | -> | name  | -> |Something...
    //  +---+    +-------+    +---...

    free(p);

    //  +---+                 +---...
    //  | p |                 |Something...
    //  +---+                 +---...

    return 0;
}

如您所见,调用释放了最初由 分配的内存,但并没有释放分配到时分配的内存。free(p)mallocp->name

这是你的泄密。

解决问题

在堆上拥有对象有两个方面:Person

  • 内存分配 - 由 / 此处处理。mallocfree
  • 初始化完成该内存 - 通过调用构造函数和析构函数进行处理。

您缺少对析构函数的调用,因此 持有的资源被泄露。这里是记忆,但如果有一把锁,你可以有一个永远锁定的互斥锁,等等......因此,执行析构函数是必要的。PersonPerson

C 风格的方法是自己调用析构函数:

int main() {
    auto p = (Person*)malloc(sizeof(Person));
    p = new(p) Person();
    p->name = Test1::MSG1;

    std::cout << "name: "<< p->name << "\n";

    //  Problem "fixed".
    p->~Person();

    free(p);

    std::cout << "done" << "\n";

    return 0;
}

然而,这不是惯用的 C++:它容易出错,等等......

C++ 方法是使用 RAII 来确保当超出范围时,其所有资源都得到正确处置:执行析构函数释放为其自身分配的内存。pPersonPerson

首先,我们将创建一些帮助程序。我使用了命名空间,因为我不知道您使用的 C 库的名称,但我邀请您更具体:c

namespace c {
struct Disposer<T> {
    void operator()(T* p) {
        p->~T();
        free(p);
    }
};

template <typename T>
using UniquePointer<T> = std::unique_ptr<T, Disposer<T>>;

template <typename T, typename... Args>
UniquePointer<T> make_unique(T* t, Args&&... args) {
    try {
        new (t) T(std::forward<Args>(args)...);
    } catch(...) {
        free(t);
        throw;
    }

    return UniquePointer{t};
}
} // namespace c

有了这个,我们可以改进原始示例:

int main() {
    auto raw = (Person*) malloc(sizeof(Person));

    auto p = c::make_unique(raw);

    p->name = Test1::MSG1;

    std::cout << "name: "<< p->name << "\n";

    //  No need to call the destructor or free ourselves, welcome to RAII.

    std::cout << "done" << "\n";

    return 0;
}

注意:不要使用 std::endl,而是使用 '\n' 或 “\n”。 std::endl 在放置行尾的顶部调用 .flush(),这很少是你想要的——它会减慢速度。