使用new/delete过度分配内存

8
使用 mallocfree,可以轻松地分配带有额外数据的结构。但是如何使用 new/ delete 实现相同的功能呢?
我知道我可以使用放置 new 语法和 malloc 进行分配,但如果我将一个对象放置在由 malloc 分配的内存中,delete 是否能正常并可移植地工作呢?
我想要实现的与下面的示例相同,但使用 new/ delete 而不是 malloc/ free,以便正确调用构造函数 / 析构函数:
#include <cstdlib>
#include <cstring>
#include <iostream>

class Hamburger {
  int tastyness;
 public:
  char *GetMeat();
};

char *Hamburger::GetMeat() {
    return reinterpret_cast<char *>(this) + sizeof(Hamburger);
}

int main(int argc, char* argv[])
{
   Hamburger* hb;
   // Allocate a Hamburger with 4 extra bytes to store a string.
   hb = reinterpret_cast<Hamburger*>(malloc(sizeof(Hamburger) + 4));
   strcpy(hb->GetMeat(), "yum");
   std::cout << "hamburger is " << hb->GetMeat() << std::endl;
   free(hb);
}

输出:汉堡很好吃


这太棒了。int tastyness 通常被称为 size_t size,以便明显地展示其有多么有用。 - porgarmingduod
为什么你想要这样做?为什么不在类中包含一个char *成员? - dlev
你的方法已经假设汉堡对象的末尾后面还有字节。你无法将这些字节直接分配给对象本身吗?也许你有一个更复杂的例子,使得为所有汉堡分配一些字符在末尾不是有意义的? - Chad La Guardia
2
@Chad,假设不像这里的示例一样总是分配4个额外字节,而是由用户输入确定值。这有效地允许可变大小的对象。您无法使用普通类来实现这一点。您必须使用多个分配,然后它们不再是一个连续的块。 - Rob Kennedy
1
Rob说的。这种东西的用例是拥有一个作为动态数据后续的某种标头的结构体。 - porgarmingduod
5个回答

3
您可以不用malloc/free或未定义的行为来实现这个,(我不确定reinterpret_cast,但至少构造/析构可以很好地完成)。
要分配内存,您可以直接调用全局operator new。之后,您可以使用旧的放置new在那里构造对象。不过,您必须保护ctor-call,因为如果ctor失败,则调用的“放置删除”函数将不会释放任何内存,而只是什么也不做(就像放置new一样)。
为了在之后销毁对象,您可以(并且可能)直接调用析构函数,为了释放内存,您可以调用全局operator delete。
我认为,也可以像处理任何普通对象一样将其删除,因为调用析构函数和全局operator delete之后就是正常delete所做的事情,但我不能百分之百确定。
您的示例修改如下:
#include <cstdlib>
#include <cstring>
#include <iostream>

class Hamburger {
    int tastyness;
public:
    char *GetMeat();
};

char *Hamburger::GetMeat() {
    return reinterpret_cast<char *>(this) + sizeof(Hamburger);
}

int main(int argc, char* argv[])
{
    Hamburger* hb;
    // Allocate space for a Hamburger with 4 extra bytes to store a string.
    void* space = operator new(sizeof(Hamburger) + 4);
    // Construct the burger in that space
    hb = new (space) Hamburger; // TODO: guard ctor call (release memory if ctor fails)
    strcpy(hb->GetMeat(), "yum"); // OK to call member function on burger now

    std::cout << "hamburger is " << hb->GetMeat() << std::endl;

    // To delete we have to do 2 things
    // 1) call the destructor
    hb->~Hamburger();
    // 2) deallocate the space
    operator delete(hb);
}

2
如果我是你,我会使用放置 new 和显式析构函数调用,而不是使用 delete
template< typename D, typename T >
D *get_aux_storage( T *x ) {
    return reinterpret_cast< D * >( x + 1 );
}

int main() {
    char const *hamburger_identity = "yum";
    void *hamburger_room = malloc( sizeof( Hamburger )
                                   + strlen( hamburger_identity ) + 1 );
    Hamburger *hamburger = new( hamburger_room ) Hamburger;
    strcpy( get_aux_storage< char >( hamburger ), hamburger_identity );
    cout << get_aux_storage< char const >( hamburger ) << '\n';

    hamburger->~Hamburger(); // explicit destructor call
    free( hamburger_room );
}

当然,在证明有必要之后才应该进行这种优化。(你真的能通过这种方式节省内存吗?这会使调试变得更加困难吗?)
从技术上讲可能没有太大的区别,但对我来说,“new”和“delete”表示正在创建和销毁一个对象,即使该对象只是一个字符。当您将字符数组分配为通用“块”时,它使用数组分配器(专门适用于数组)并在其中构建字符。然后,您必须使用放置new在这些字符的顶部构造一个新对象,这实际上是对象别名或双重构造,接着是双重销毁,当您想要删除所有内容时。
最好使用malloc/free绕过C++对象模型,而不是扭曲它以避免将数据处理为对象。
哦,还有一种选择是使用自定义的operator new,但这可能会引起问题,所以我不建议这样做。
struct Hamburger {
  int tastyness;
public:
  char *GetMeat();
  static void *operator new( size_t size_of_bread, size_t size_of_meat )
      { return malloc( size_of_bread + size_of_meat ); }
  static void operator delete( void *ptr )
      { free( ptr ); }
};

int main() {
    char const *hamburger_identity = "yum";
    size_t meat_size = strlen( hamburger_identity ) + 1;
    Hamburger *hamburger = new( meat_size ) Hamburger;
    strcpy( hamburger->GetMeat(), hamburger_identity );
    cout << hamburger->GetMeat() << '\n';
}

重载 new/delete 就是我刚想到的解决方案!事实上,我正要回来把它作为答案发布并继续我的快乐之旅。我会寻找关于为什么它可能会成为一个棘手问题的信息,但如果你能详细说明那就太好了。 - porgarmingduod
@porgarmingduod:查找背后的详细规则通常会带来一些普遍的复杂性。例如,这禁止在“汉堡包”上使用放置新的方法。由于您已经限制自己只能以一种方式创建“汉堡包”,并禁止派生类,因此它可能是可以的,并且保持简单。 - Potatoswatter
啊,我明白你的意思了。这不是你必须使用的东西。顺便问一下,你说的“已经禁止派生类”是什么意思?我在一个简单的类层次结构中实现了它,就我所知,它运行得很好。额外的大小被赋予了0默认值,因此那些不需要额外数据的派生类可以使用普通的“new”调用进行分配。 - porgarmingduod
@porgarmingduod: 我明白了...那么请注意可选数据。如果你有一个层次结构,那么很可能你并不是在试图从一个被分配了数百万次的类中节省字节。你确定你不只是想要在Hamburger中添加一个std::string成员吗? - Potatoswatter

2

嗯,让我看看。你绝对不能使用new/malloc分配内存,然后使用free/delete释放内存,必须使用匹配的一对。

如果你真的想这么做,我想你可以使用"hp = new char[sizeof(Hamburger) + 4]"和"delete[]((char *) hp)",以及显式的构造函数/析构函数调用。

我能想到的唯一原因是你没有Hamburger源代码--也就是说它是一个库类。否则你只需要添加一个成员!你能解释一下为什么要使用这个想法吗?


1
正如我所提到的,使用new/delete的整个目的是为了获取构造函数/析构函数调用。至于用例,它是用于存储一个标题,后跟外部数据块的类。我想将其存储为类的原因是有一堆其他类遵循相同的模式,但具有特定的数据(即声明为成员的数据)。使用相同的基类/标题使哈希函数可以统一处理它们。等等。相信我,这里有用例 :) - porgarmingduod
啊,没错,@Rob Kennedy 上面提到的可变大小的想法。是的,我能看出来这为什么会很方便。既然你可以设计类,只需确保它们实际上不需要一个非平凡的析构函数,并使用 char::delete[] 进行释放。 - Ernest Friedman-Hill

0

如果你有一组相对有限的填充量,那么还有另一种方法可以解决这个问题。你可以创建一个带有填充量作为模板参数的模板类,并使用可能的填充量实例化它。所以,例如,如果你知道你只需要16、32或64字节的填充,你可以像这样做:

template <int Pad>
class Hamburger {
    int tastiness;
    char padding[Pad];
};

template class Hamburger<16>;
template class Hamburger<32>;
template class Hamburger<64>;

很可爱。但在这种情况下,填充量完全是动态的。 - porgarmingduod

0

有没有任何原因,使得直接、简单和安全的方式不适用?

class Hamburger {
public:
    void Extend( const std::string& pExtension) {
        mContent += pExtension;
    }
    const std::string& GetMeat() ...

private:
    std::string mContent;
};

int main() {
    Hamburger hb;
    hb.Extend("yum");
    std::cout << "hamburger is " << hb.GetMeat() << std::endl;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接