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)?

huangapple go评论70阅读模式
英文:

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)?

问题

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

#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运行它时,它会给我这个错误:

definitely lost: 31 bytes in 1 blocks


约束条件

  1. 在上面的示例中,我必须使用 malloc,因为在我的实际代码中,我在我的C++项目中使用一个C库,该库在内部使用 malloc。因此,我不能摆脱对 malloc 的使用,因为我在代码中没有显式使用它。
  2. 我需要在我的代码中一遍又一遍地重新分配 Personstd::string name
英文:

This is a minimal working example for the problem I am facing in my real code.

#include &lt;iostream&gt;

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

struct Person{
    std::string name;
};

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

    std::cout &lt;&lt; &quot;name: &quot;&lt;&lt; p-&gt;name &lt;&lt; std::endl;

    free(p);

    std::cout &lt;&lt; &quot;done&quot; &lt;&lt; std::endl;

    return 0;
}

When I compile it and run it via Valgrind, it gives me this error:

> definitely lost: 31 bytes in 1 blocks


Constraints

  1. I am bound to use malloc in the example above, as in my real code I use a C library in my C++ project, which uses this malloc internally. So I can't get away from malloc usage, as I don't do it explicitly anywhere in my code.
  2. I need to reassign std::string name of Person again and again in my code.

答案1

得分: 55

以下是代码的翻译部分:

为一个 Person 对象分配内存:

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

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

p = new(p)Person();

通过 malloc 分配的内存需要释放:

free(p);

通过放置 new 调用构造函数会创建一个 std::string。该字符串将在析构函数中被销毁,但析构函数从未被调用。free 不会调用析构函数(就像 malloc 不会调用构造函数一样)。

malloc 仅分配内存。放置 new 只在已分配的内存中构造对象。因此,在调用 free 之前,您需要调用析构函数。这是我知道的唯一一种需要显式调用析构函数的情况:

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

The important pieces of your code line by line...

Allocate memory for one Person object:

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

Construct a Person object in that already allocated memory via calling its constructor:

p = new(p)Person();

Free the memory allocated via malloc:

free(p);

Calling the constructor via placement new creates a std::string. That string would be destroyed in the destructor but the destructor is never called. free does not call destructors (just like malloc does not call a constructor).

malloc only allocates the memory. Placement new only constructs the object in already allocated memory. Hence you need to call the destructor before calling free. This is the only case I am aware of where it is correct and necessary to explicitly call a destructor:

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

答案2

得分: 35

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

p->~Person();

或者使用 std::destroy_at(p),这是相同的操作。

英文:

You must manually call the destructor before free(p);:

p-&gt;~Person();

Or std::destroy_at(p), which is the same thing.

答案3

得分: 31

问题定位

首先,让我们通过说明每个语句后内存的状态来明确问题所在。

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) 释放了最初由 malloc 分配的内存,但它没有释放由 p->name 分配的内存,当它被赋值时。

这就是内存泄漏的原因。

解决问题

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

  • 内存分配,由这里的 malloc/free 处理。
  • 初始化和清理该内存,由构造函数和析构函数的调用处理。

你缺少了对析构函数的调用,因此 Person 持有的资源被泄漏了。在这里是内存,但如果 Person 持有锁,你可能会有永久锁定的互斥锁等... 因此执行析构函数是必要的。

C 风格的方法是手动调用析构函数:

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

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

    // 问题“已修复”。
    p->~Person();

    free(p);

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

    return 0;
}

但这不是标准的 C++ 做法:它容易出错等等...

C++ 做法是使用 RAII 来确保 p 超出作用域时,所有它的资源都被正确处理:Person 的析构函数被执行,并且为 Person 本身分配的内存被释放。

首先,我们将创建一些辅助函数。我在这里使用了 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";

    // 不需要手动调用析构函数或释放,欢迎使用 RAII。

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

    return 0;
}

注意:不要使用 std::endl,请改用 '\n'"\\n"std::endl 除了添加换行符外还会调用 .flush(),这通常不是你想要的行为——它会减慢程序。

英文:

Pinpointing the problem

First of all, let us be clear about exactly what the problem is by illustrating the state of the memory after each statement.

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

    //  +---+    +-------+
    //  | p | -&gt; | ~~~~~ |
    //  +---+    +-------+

    p = new(p)Person();

    //  +---+    +-------+
    //  | p | -&gt; | name  |
    //  +---+    +-------+

    p-&gt;name=Test1::MSG1;

    //  +---+    +-------+    +---...
    //  | p | -&gt; | name  | -&gt; |Something...
    //  +---+    +-------+    +---...

    free(p);

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

    return 0;
}

As you can see, calling free(p) freed up the memory originally allocated by malloc, but it didn't free the memory allocated by p-&gt;name when it was assigned to.

This is your leak.

Solving the problem

There are two aspects to having a Person object on the heap:

  • A memory allocation&mdash;handled by malloc/free here.
  • Initializing and Finalizing that memory&mdash;handled by calls to constructors and destructors.

You're lacking the call to the destructor, hence resources held by Person are leaked. Here it's memory, but if Person held a lock you could have a forever locked mutex, etc... executing the destructor is therefore necessary.

The C-style approach is to call the destructor yourself:

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

    std::cout &lt;&lt; &quot;name: &quot;&lt;&lt; p-&gt;name &lt;&lt; &quot;\n&quot;;

    //  Problem &quot;fixed&quot;.
    p-&gt;~Person();

    free(p);

    std::cout &lt;&lt; &quot;done&quot; &lt;&lt; &quot;\n&quot;;

    return 0;
}

However this is not idiomatic C++: it's error prone, etc...

The C++ approach is to use RAII to ensure that when p goes out of scope, all its resources are properly disposed of: the destructor of Person is executed and the memory allocated for Person itself is freed.

First of all, we're going to create some helpers. I used the c namespace since I don't know the name of the C library you use, but I invite you to be more specific:

namespace c {
struct Disposer&lt;T&gt; {
    void operator()(T* p) {
        p-&gt;~T();
        free(p);
    }
};

template &lt;typename T&gt;
using UniquePointer&lt;T&gt; = std::unique_ptr&lt;T, Disposer&lt;T&gt;&gt;;

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

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

And with that, we can improve the original example:

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

    auto p = c::make_unique(raw);

    p-&gt;name = Test1::MSG1;

    std::cout &lt;&lt; &quot;name: &quot;&lt;&lt; p-&gt;name &lt;&lt; &quot;\n&quot;;

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

    std::cout &lt;&lt; &quot;done&quot; &lt;&lt; &quot;\n&quot;;

    return 0;
}

Note: Do not use std::endl, use &#39;\n&#39; or &quot;\n&quot; instead. std::endl calls .flush() on top of putting an end of line, which is rarely what you want -- it slows things down.

答案4

得分: 11

作为在其他回答中提到的,泄漏的源头在于Personname成员的析构函数没有被调用。在正常情况下,当Person的析构函数被调用时,name的析构函数会隐式地被调用。然而,Person从未被销毁。Person实例的内存只是使用free函数释放。

因此,就像你不得不在使用malloc后显式调用构造函数一样,你还需要在使用free之前显式调用析构函数。

你还可以考虑重载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); }
};

这样,你可以在使用newdelete时正常操作,实际底层将使用mallocfree

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

通过这种方式,你可以更自然地使用智能指针。

int main (void) {
    auto p = std::make_unique<Person>();
    //... unique指针会自动删除
}

当然,你也可以在使用mallocfree的情况下使用带有自定义删除器的unique_ptr,但这将变得更加繁琐,而且你的删除器仍然需要知道显式调用析构函数。

英文:

As mentioned in other answers, the source of the leak is that the destructor for the name member of Person does not get called. It would normally be called implicitly when the destructor for Person is called. However, Person is never destructed. The memory for the Person instance is simply released with free.

So, just as you had to explicitly invoke the constructor with the placement new after malloc, you also need to explicitly invoke the destructor before free.

You can also consider overloading the new and delete operators.

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

This way, you can use new and delete normally, when underneath they will use malloc and free.

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

And this way, you can more naturally use a smart pointer.

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

Of course, you could have used unique_ptr with a custom deleter with your explicit calls to malloc and free, but it would have been much more cumbersome, and your deleter would still need to know to explicitly invoke the destructor as well.

答案5

得分: 6

如其他人所提到的,由 Person 成员分配的动态内存只有在析构函数 ~Person 中被释放,free() 不会调用它。

如果你必须在需要一些初始化和清理工作的库中使用这个函数,而不仅仅是默认的情况,比如在这里,一个方法是定义一个新的删除器,供标准库智能指针使用:这将适用于你自己没有分配的内存块。

#include &lt;memory&gt;
#include &lt;new&gt; // std::bad_alloc
#include &lt;stdlib.h&gt;
#include &lt;string&gt;

struct Person{
    std::string name;
};

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


Person* Person_factory() // 外部代码的虚构函数。
{
  Person* const p = static_cast&lt;Person*&gt;(malloc(sizeof(Person)));
  if (!p) {
    throw std::bad_alloc();
  }
  new(p) Person();
  return p;
}

这样可以安全地使用:

const auto p =
  std::unique_ptr&lt;Person, PersonDeleterForSomeLib&gt;(Person_factory());

进行自动内存管理。你可以从函数中返回智能指针,当它的生命周期结束时,析构函数和 free() 都将被调用。你也可以用这种方法创建一个 std::shared_ptr。如果出于某种原因需要在智能指针仍然存在时销毁对象,你可以使用 resetrelease

英文:

As others have mentioned, dynamic memory allocated by the members of Person only gets freed by the destructor ~Person, which free() does not call.

If you have to use this function with a library that requires some initialization and clean-up other than the default, such as here, one approach is to define a new deleter, for the standard libray smart pointers to use: This will work even with a block of memory you did not allocate yourself.

#include &lt;memory&gt;
#include &lt;new&gt; // std::bad_alloc
#include &lt;stdlib.h&gt;
#include &lt;string&gt;

struct Person{
    std::string name;
};

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


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

This lets you safely use:

const auto p =
  std::unique_ptr&lt;Person, PersonDeleterForSomeLib&gt;(Person_factory());

with automatic memory management. You can return the smart pointer from the function, and both the destructor and free() will be called when its lifetime ends. You can also create a std::shared_ptr this way. If for some reason you need to destroy the object while the smart pointer is still alive, you can reset or release it.

huangapple
  • 本文由 发表于 2023年3月1日 15:10:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/75600524.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定