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

jv2fixgn  于 2023-03-05  发布在  其他
关注(0)|答案(5)|浏览(125)
    • 此问题在此处已有答案**:

How to properly free the memory allocated by placement new?(5个答案)
昨天关门了。
这是我在实际代码中遇到的问题的最小工作示例。

#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.在上面的例子中,我必须使用malloc,因为在我的真实代码中,我在C++项目中使用了一个C库,它在内部使用了malloc,所以我无法摆脱malloc的使用,因为我在代码中没有明确地使用它。
1.我需要在代码中一次又一次地重新分配Personstd::string name

mlnl4t2r

mlnl4t2r1#

你代码中的重要部分一行一行的...
为一个Person对象分配内存:

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

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

p = new(p)Person();

释放通过malloc分配的内存:

free(p);

通过放置new调用构造函数会创建一个std::string。该字符串会在析构函数中被销毁,但析构函数永远不会被调用。free不会调用析构函数(就像malloc不会调用构造函数一样)。
malloc只分配内存。Placement new只在已经分配的内存中构造对象。因此,在调用free之前,您需要调用析构函数。这是我所知道的唯一一种情况,在这种情况下,显式调用析构函数是正确的和必要的:

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

gxwragnw2#

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

p->~Person();

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

wmomyfyw

wmomyfyw3#

找出问题所在

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

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";

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

    free(p);

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

    return 0;
}

然而这不是惯用的C++:容易出错等等。
C++的方法是使用RAII来确保当p超出作用域时,其所有资源都被正确处理:执行X1 M9 N1 X的析构函数 * 并且 * 释放为X1 M10 N1 X本身分配的存储器。
首先,我们要创建一些helper,我使用了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(),这通常不是您想要的--它会减慢速度。*
sqxo8psd

sqxo8psd4#

正如在其他答案中提到的,泄漏的来源是Personname成员的析构函数没有被调用。通常在调用Person的析构函数时会隐式调用它。但是,Person从未被析构。Person示例的内存只是随free释放。
因此,正如必须显式调用new后的构造函数一样,也需要显式调用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 pointer will delete automatically
}

当然,您可以使用带有定制删除器的unique_ptr来显式调用mallocfree,但是这样做会麻烦得多,并且您的删除器还需要知道显式调用析构函数。

nwlqm0z1

nwlqm0z15#

正如其他人所提到的,由Person的成员分配的动态内存只能由析构函数~Person释放,而free()不会调用析构函数。
如果你必须在一个需要初始化和清理的库中使用这个函数,比如这里,一种方法是定义一个新的deleter,供标准库智能指针用途:即使您没有分配自己的内存块,这也可以正常工作。

#include <memory>
#include <stdexcept>
#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::runtime_error("Out of memory.");
  }
  new(p) Person();
  return p;
}

这使您可以安全地用途:

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

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

相关问题