函数中堆分配与缺少堆分配的影响

Posted

技术标签:

【中文标题】函数中堆分配与缺少堆分配的影响【英文标题】:Effect of heap allocation vs lack of heap allocation in a function 【发布时间】:2020-07-13 19:19:26 【问题描述】:

我已经设法使用观察者设计模式编写了一个实现生命游戏的程序,但似乎我的一个函数 Grid::init() 仅在我在堆上分配单元并且不否则不行。下面是 Grid 的类定义。

// Grid class
// Grid is initially empty. Sets up a TextDisplay observer when init() is called
// and the grid is initialized with cell objects.
class Grid 
    TextDisplay *td = nullptr;         
    std::vector<std::vector<Cell>> cells; 
    size_t size; 
 
  public:
    ~Grid(); 
    void setObserver(Observer *ob); 
  
    void init( size_t n );              
    void turnOn( size_t r, size_t c );  
    void tick();                        
    
    friend std::ostream & operator<<( std::ostream & out, const Grid & g );
;

Cell 是网格的一个单元,用于存储位置和状态等信息。我试图通过向量间接分配堆上的单元格来初始化网格,但这不起作用;相反,当我改变 网格类的单元格元素是一个

std::vector<:vector>> 实现成功了。

【问题讨论】:

我们可以在minimal reproducible example 中看到非功能代码吗?赔率是您不想复制的东西被复制并破坏了Rule of Three。 在stack版本中,附加了observer后,你是否还在grid中添加了cell?向向量中添加元素会导致向量被重新定位,从而使指向该向量中元素的所有指针无效。 在地址清理器下运行你的程序。地址清理器可能是 C++ 历史上为 C++ 程序员制作的最酷、最神奇、最好的工具。那,并删除不应复制的类上的复制构造函数/复制赋值运算符(对于初学者来说可能是GridCell,也可能是Observer)。一旦你做了这两件事,我可以再看看你的代码。否则,我唯一的一般建议是,您可能有指向 Cell 的悬空指针,并且在复制向量时,指向 std::vector 中元素的指针会发生变化。 要使用地址清理程序,如果您使用的是 Clang 或 GCC,请编译并链接到 -fsanitize=address。如果您不使用 Clang 或 GCC(如果您使用的是 MSVC),那么安装 Clang 或 GCC 可能是值得的,这样您就可以使用地址清理程序。 en.wikipedia.org/wiki/AddressSanitizergithub.com/google/sanitizers/wiki/AddressSanitizer @DietrichEpp 我不能也不应该修改 Observer 类。 【参考方案1】:

我想我可能知道发生了什么。这段代码有几个问题,我想我已经弄清楚是什么问题组合导致了这种行为。

问题 #1: 类具有不安全的复制构造函数/复制赋值运算符。

一般来说,任何使用指针的类都应该覆盖复制构造函数、复制赋值操作符和析构函数。做到这一点的简单方法是干脆不使用指针,或者只使用智能指针,但这不是万能的。

特别是,Grid 复制是不安全的。 std::vector&lt;Cell *&gt;std::vector&lt;Cell&gt; 版本都不安全……但原因不同!

问题 #2: 这个奇怪的模式:

TextDisplay newDisplay(n);
td = new TextDisplaynewDisplay;

这会构造 两个 TextDisplay 对象。一个是另一个的副本。通常,这可能只是创建一个对象的一种迂回或缓慢的方式,但只有在类没有上述问题 #1 时才会出现这种情况。

是什么导致了问题

如果您创建包含std::vector&lt;Cell&gt;Grid 副本,所有单元格的地址都会更改!这不适用于Cell *

您应该通过删除 Grid 复制构造函数(和复制赋值运算符)来防止这种情况发生。

class Grid 
    Grid(const Grid &) = delete;
    Grid &operator=(const Grid &) = delete;
;

这可能会告诉你问题出在哪里。

摆脱裸指针

一般来说,现代 C++ 建议(自 2011 版以来)是使用智能指针而不是裸指针。

所以,不要使用Cell *,而是使用std::unique_ptr&lt;Cell&gt;std::shared_ptr&lt;Cell&gt;

这不一定适用于“非拥有”指针,如 Subject 内部的 std::vector&lt;Observer *&gt;,它不适用于内部指针(如指向 std::vector&lt;Cell&gt; 中单个单元格的指针),但是……

您可能应该使用std::vector&lt;std::vector&lt;std::unique_ptr&lt;Cell&gt;&gt;&gt; 而不是std::vector&lt;std::vector&lt;Cell *&gt;&gt;

您可能应该使用std::unique_ptr&lt;TextDisplay&gt; 而不是TextDisplay *

一般来说,如果你能找到避免调用new的方法,请避免调用new

关于堆的说明

注意std::vector&lt;Cell&gt;std::vector&lt;Cell *&gt; 都在堆上分配。只是std::vector&lt;Cell *&gt;多了一层间接性。

堆 +-------------------+ +------+ | std::vector | --> |细胞 | +-------------------+ +------+ | ... | 堆 +---------+ +--------+ +------+ | std::vector | --> |单元格 * | --> |细胞 | +---------+ +--------+ +------+ | ... |

【讨论】:

摆脱裸指针是最好的建议,不幸的是,指针似乎是一些学生所学的。 @Gord452: 如果删除拷贝+拷贝赋值导致分段错误,那么你的程序有问题。调用cells.clear()会破坏cells中的所有对象但不会释放向量使用的内存。如果向量包含Cell *,则Cell 对象将不会被释放。 @Gord452:如果您有分段错误并且您不知道它在哪里,请尝试使用地址清理程序(-fsanitize=address 与 GCC 或 Clang)或附加调试器。地址清理程序通常是找出问题所在的最快方法。 @Gord452: 如果你想把单元格放在栈上,你不能使用std::vector 根本 因为std::vector 使用堆(除非你有一些古怪的分配器正在发生)。将单元格放入堆栈的最简单方法是使用固定大小的数组,例如Cell cell[20][20];。但请记住,每个单元格都有一个std::vector,所以它仍然会以这种方式使用堆。 @DietrichEpp 感谢您的回答,尽管我似乎仍然遇到问题。我已经更新了我的问题。

以上是关于函数中堆分配与缺少堆分配的影响的主要内容,如果未能解决你的问题,请参考以下文章

java中堆与栈的区别

操作系统中堆(heap)与栈(stack)的区别

Java中堆内存和栈内存详解

Java中堆内存和栈内存详解

Java中堆内存和栈内存详解2

Java中堆内存和栈内存详解转