为什么这个程序中存在内存泄漏?在给定约束的情况下如何解决它(对包含 std::string 的对象使用 malloc 和 free)?

Anu*_*hra 58 c++ valgrind memory-leaks placement-new dynamic-memory-allocation

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

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

肯定丢失:1 个块中 31 个字节


约束条件

  1. 我一定会malloc在上面的示例中使用,因为在我的实际代码中,我在 C++ 项目中使用了 C 库,该项目malloc在内部使用了它。所以我无法摆脱malloc使用,因为我没有在代码中的任何地方明确地这样做。
  2. 我需要在我的代码中一次又一次地重新std::string name分配。Person

for*_*818 55

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

为一个 Person 对象分配内存:

auto p = (Person*)malloc(sizeof(Person));
Run Code Online (Sandbox Code Playgroud)

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

p = new(p)Person();
Run Code Online (Sandbox Code Playgroud)

释放通过 malloc 分配的内存:

free(p);
Run Code Online (Sandbox Code Playgroud)

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

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

auto p = (Person*)malloc(sizeof(Person));
p = new(p)Person();
p->~Person();
free(p);
Run Code Online (Sandbox Code Playgroud)

  • 为了真正清楚泄漏的内容:因为没有调用“Person”析构函数,所以“Person::name”没有被破坏,因此“std::string”分配的内存没有被释放。也就是说:“Person”正在被释放,但它指向的任何内存_都没有_。 (22认同)
  • 关于代码中注释的讨论总是有点棘手,但对我来说,我期望代码中唯一的注释是使用 malloc 的placement-new 的原因(分配器、练习...) (2认同)
  • @AnsonSavage,如果没有任何限制,他们可能不会对基本上是“std::string”的东西使用动态分配,它已经管理动态分配的内存 (2认同)

Hol*_*Cat 35

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

p->~Person();
Run Code Online (Sandbox Code Playgroud)

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


Mat*_* M. 31

查明问题

\n

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

\n
int main() {\n    auto p = (Person*)malloc(sizeof(Person));\n\n    //  +---+    +-------+\n    //  | p | -> | ~~~~~ |\n    //  +---+    +-------+\n\n    p = new(p)Person();\n\n    //  +---+    +-------+\n    //  | p | -> | name  |\n    //  +---+    +-------+\n\n    p->name=Test1::MSG1;\n\n    //  +---+    +-------+    +---...\n    //  | p | -> | name  | -> |Something...\n    //  +---+    +-------+    +---...\n\n    free(p);\n\n    //  +---+                 +---...\n    //  | p |                 |Something...\n    //  +---+                 +---...\n\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

是你的泄漏。

\n

解决问题

\n

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

\n
    \n
  • malloc这里由/处理的内存分配\xe2\x80\x94 free
  • \n
  • 通过调用构造函数和析构函数来初始化完成内存\xe2\x80\x94。
  • \n
\n

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

\n

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

\n
int main() {\n    auto p = (Person*)malloc(sizeof(Person));\n    p = new(p) Person();\n    p->name = Test1::MSG1;\n\n    std::cout << "name: "<< p->name << "\\n";\n\n    //  Problem "fixed".\n    p->~Person();\n\n    free(p);\n\n    std::cout << "done" << "\\n";\n\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

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

\n
namespace c {\nstruct Disposer<T> {\n    void operator()(T* p) {\n        p->~T();\n        free(p);\n    }\n};\n\ntemplate <typename T>\nusing UniquePointer<T> = std::unique_ptr<T, Disposer<T>>;\n\ntemplate <typename T, typename... Args>\nUniquePointer<T> make_unique(T* t, Args&&... args) {\n    try {\n        new (t) T(std::forward<Args>(args)...);\n    } catch(...) {\n        free(t);\n        throw;\n    }\n\n    return UniquePointer{t};\n}\n} // namespace c\n
Run Code Online (Sandbox Code Playgroud)\n

这样,我们就可以改进原来的例子:

\n
int main() {\n    auto raw = (Person*) malloc(sizeof(Person));\n\n    auto p = c::make_unique(raw);\n\n    p->name = Test1::MSG1;\n\n    std::cout << "name: "<< p->name << "\\n";\n\n    //  No need to call the destructor or free ourselves, welcome to RAII.\n\n    std::cout << "done" << "\\n";\n\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

注意:请勿使用std::endl、使用\'\\n\'"\\n"代替。在结束线路之上std::endl调用,这很少是您想要的——它会减慢速度。.flush()

\n


jxh*_*jxh 11

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

因此,正如您必须显式调用放置在newafter 的构造函数一样malloc,您也需要显式调用 before 的析构函数free

您还可以考虑重载newanddelete运算符。

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

这样,您可以正常使用newand delete,当在下面时它们将使用mallocand free

int main (void) {
    auto p = new Person;
    //... 
    delete p;
}
Run Code Online (Sandbox Code Playgroud)

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

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

当然,您可以unique_ptr通过显式调用 和 来使用自定义删除器mallocfree但这会更加麻烦,并且您的删除器仍然需要知道显式调用析构函数。

  • 请注意,operator new 获取一个大小作为输入 - 因此您通常应该使用它而不是计算 sizeof(Person); 例如,它被任何派生类使用(我不希望有任何派生类)。 (3认同)
  • 虽然我可能是错误的,但我从这个问题中了解到OP具有从C API获取的“malloc”存储,因此必须使用“new”在其中构造一个对象,即OP不负责分配。如果我是正确的,我认为在他的应用程序中覆盖“operator new / delete”没有意义。 (2认同)

Dav*_*lor 6

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

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

这可以让您安全地使用:

const auto p =
  std::unique_ptr<Person, PersonDeleterForSomeLib>(Person_factory());
Run Code Online (Sandbox Code Playgroud)

具有自动内存管理功能。您可以从函数返回智能指针,并且析构函数和析构函数都free()将在其生命周期结束时被调用。您也可以通过这种方式创建std::shared_ptr。如果由于某种原因您需要在智能指针仍然存在的情况下销毁该对象,您可以resetrelease它。