【问题标题】:Is the use of vectors of pointers here unnecessary or worse, cause memory leaks?在这里使用指针向量是否不必要或更糟,会导致内存泄漏?
【发布时间】:2009-10-02 09:02:41
【问题描述】:

我对 C++ 编程比较陌生,但我是一名 10 年的 C 程序员,所以我对对象的指针比对对象的引用更舒服。

我正在编写纸牌游戏 - 这种设计不安全吗?有没有更好的办法?

反正我有课SolitaireGame

class SolitaireGame:
{
    public:
        SolitaireGame( int numsuits = 1 );
    private:
        Deck * _deck;
        vector<Card> _shoe;
};

Deck 是这样定义的:

class Deck:
{
public:
 Deck::Deck( vector<Card>& shoe );
 ~Deck();
 int DealsLeft() const { return deals_left; }
 Card * PullCard();
private:
 int deals_left;
 int num_each_deal;
 deque<Card *> _cards;
};

Deck 构造函数引用Card 对象的向量(牌盒,通常为 104 张牌),并将指向每张牌的指针推送到它自己的指针双端队列中。

Deck::Deck( vector<Card>& shoe )
{
    vector<Card>::iterator iter = shoe.begin();

    while( iter != shoe.end() )
    {
        _cards.push_front( &(*iter) );
        iter++;
    }
}

}

鞋子是在SolitaireGame 构造函数中创建的。一旦创建了这个由动态创建的Card 对象组成的向量 - 然后我将对该向量的引用传递给构造函数。

SolitaireGame::SolitaireGame( int numsuits ):_numsuits(numsuits ) 
{
    Card * c;
    vector<Card> _shoe;

    for( int i = 0; i < NUM_CARDS_IN_SHOE; i++ )
    {
        c = new Card();
        _shoe.push_back( *c );
    }

    _deck = new Deck( _shoe );
}

我的想法是,鞋子将成为 Card 对象的实际内存的容器,DeckColumns 只处理指向这些 Card 对象的指针。

【问题讨论】:

  • 感谢所有指出我在 SolitaireGame 构造函数中的 vector&lt;Card&gt; _shoe 变量遮蔽了 _shoe 成员变量的人。在发布问题时,这实际上是一个错字(代码不包含此变量)。我想我不会编辑它,所以每个人的答案都有意义。

标签: c++ pointers vector


【解决方案1】:

只要使用这个 sn-p 代码,您就会泄漏动态创建的卡片。

Card * c;
vector<Card> _shoe;

for( int i = 0; i < NUM_CARDS_IN_SHOE; i++ )
{
    c = new Card();
    _shoe.push_back( *c );
}

_shoe.push_back( *c )c 指向的Card 对象的副本 添加到Cards 的向量中。然后,您无法删除之前行中创建的原始Card

分配NUM_CARDS_IN_SHOE Cards 的向量可以更简单地实现如下:

std::vector<Card> _shoe( NUM_CARDS_IN_SHOE );

查看您的卡片结构,您似乎拥有(或几乎拥有)对象之间的严格所有权,因此我认为您不需要动态创建 Cards。

请注意,您的局部变量 _shoe 正在隐藏类变量 _shoe。这可能不是您想要的,因为您传递给 Deck 构造函数的本地 _shoe 将在构造函数结束时超出范围。

如果您在SolitaireGame 中对变量重新排序,您可能可以执行以下操作:

class SolitaireGame:
{
public:
    SolitaireGame( int numsuits = 1 );
private:
    vector<Card> _shoe;
    Deck _deck;
};

SolitaireGame::SolitaireGame( int numsuits )
    : _shoe(NUM_CARDS_IN_SHOE)
    , _deck(_shoe)
{
}

我已将 _deck 更改为指针。我使用的事实是,成员变量是按照类定义中声明的顺序构造的,因此_shoe 将在它作为_deck 的构造函数的引用传递之前完全构造。这样做的好处是我不再需要动态分配_deck。由于没有使用new,我知道我不会有任何未接来电到delete,因为不需要显式释放任何内容。

您是对的,您可以将指向_shoe 中的Cards 的指针存储在_deck 中而不会出现任何内存管理问题,但请注意,您不能在Cards 中添加或删除任何_shoe 在游戏的生命周期内,否则您将在 _deck 中的所有指针无效。

【讨论】:

    【解决方案2】:

    我认为有两个错误:

    1. 当您执行_shoe.push_back( *c ); 时,您正在创建 Card 对象的副本,因此保留给 c 的内存将永远不会被释放。顺便说一句,您应该始终检查每个新的是否存在一个补充删除。你的删除在哪里?
    2. 在您的 Deck 构造函数中,您正在保存指向 堆栈中 (vector&lt;Card&gt; _shoe;) 的对象的指针,因此 SolitaireGame 构造函数一结束,它们就会被删除,并且您的指针将被无效的。编辑:我看到你的类中有另一个 _shoe,所以没有必要声明另一个 _shoe 局部变量,实际上只需不声明它就可以解决这个问题。

    希望对你有所帮助。

    【讨论】:

      【解决方案3】:

      初步想法:

      1. 在 SolitaireGame 类中,您将 _shoe 声明为:

        vector&lt;Card&gt; _shoe;

        但是在构造函数中,你可以像这样将堆对象推到它上面:

        c = new Card();

        _shoe.push_back( *c );

        所以,你需要像这样声明它:

        vector&lt;Card*&gt; _shoe;

      2. 您不会在构造函数中初始化变量,例如 Deck 类中的 deal_left 和 num_each_deal。我假设您将其省略以不弄乱代码,但这是个好主意。

      3. 类 SolitaireGame 创建并拥有 Deck 对象。它还有一个带有指向 SolitaireGame 的 Card 对象的指针的 Deck。这里的所有权不清楚 - 谁删除​​了它们?虽然在多个容器中拥有指向对象的指针会起作用,但它会使调试更加困难,因为存在多次删除、删除后使用、泄漏等的范围。也许可以简化设计。可能最初 Deck 拥有 Card 对象,当它们被移除时,它们会被放入 SolitaireGame 中的向量中,并且不会同时存在于两者中。

      4. 在 SolitaireGame 的构造函数中,您声明了另一个卡片向量,它隐藏了类声明中的声明。当您将 Card 对象推送到它上面时,它们不会被推送到正确的向量,这将在构造函数结束时超出范围,并且您的类成员将为空。只需从构造函数中删除它即可。

      不管怎样,我需要一杯茶。之后我会再看一下,看看我是否可以提出其他建议。

      【讨论】:

      • 谢谢你的回复,pxb。是的,你是对的,我在最初的帖子中省略了 deals_left 的初始化,以免弄乱代码。回覆。第 1 点,声明 vector&lt;Card*&gt; _shoe 声明了一个指向对象的指针向量,但我正在做的是在通过 _shoe.push_back( *c ) 执行 push_back 之前取消引用指针。我认为c 是一个我正在取消引用的指针,所以我实际上是在推动对象本身。还是我理解错了?
      • 是的,执行 *c 将取消对指针的引用,因此您将推送回向量的内容将是对象的副本(即,将调用一个新对象和复制构造函数复制内部数据)。这是你打算做的吗?它可能非常昂贵,并且您将获得每个 Card 对象的副本,而不是指向同一个对象的指针。如果向量在内部增长,则将重新分配内存,并且将再次复制每个对象。这可能是您想要的,但对我来说似乎不寻常,并且可能导致细微的错误/泄漏。
      【解决方案4】:

      我认为 new 关键字不应该出现在这些类的代码中的任何地方,而且我不明白您为什么要通过指针来共享卡片。存储向量中保存的项目的地址是灾难的根源 - 您需要保证在获取地址后不会对向量进行修改,因为它往往会在不告诉您的情况下在内存中移动事物。

      假设 Card 对象除了一两个整数之外不存储任何东西,使用副本和值会简单得多。

      _deck = new Deck( _shoe );
      

      再次,我看不出有任何理由通过动态分配包含两个 int 和一个 deque 的对象来增加程序的复杂性。

      如果您担心复制您拥有的一些较大的类的成本(我估计这对感知性能的影响为零),那么就不要复制它们,而是通过 const 引用(如果您不需要改变实例),否则非常量引用/指针。

      【讨论】:

        【解决方案5】:

        这个程序会内存泄漏,想知道原因吗?或如何?

        push_back

        请记住此调用不要插入您提供的元素,而是创建它的副本供自己使用。阅读this了解详情

        所以

            Card *c = new Card(); // This is on heap , Need explicit release by user
        
        If you change it to 
        
            Card c; // This is on stack, will be release with stack unwinding 
        

        复制下面的程序并执行它,{我只是添加了日志记录},试试上面的两个选项

        #include<iostream>
        #include <vector>
        #include <deque>
        
        using namespace std;
        
        const int NUM_CARDS_IN_SHOE=120;
        
        class Card
        {
        public:
            Card()
            {
                ++ctr;
                cout<<"C'tor callend: "<<ctr<<" , time"<<endl;
            }
            ~Card()
            {
                ++dtr;
                cout<<"D'tor called"<<dtr<<" , time, num still to release: "<<((ctr+cpy)-dtr)<<endl;
            }
            Card& operator=(const Card & rObj)
            {
                return *this;
            }
        
            Card (const Card& rObj)
            {
                ++cpy;
                cout<<"Cpy'tor called"<<cpy<<endl;
            }
        private:
        
        
            static int ctr,dtr,rest,cpy;
        };
        int Card::ctr;
        int Card::dtr;
        int Card::rest;
        int Card::cpy;
        class Deck
        {
        public:
         Deck::Deck( vector<Card>& shoe );
         ~Deck();
         int DealsLeft() const { return deals_left; }
         Card * PullCard();
        private:
         int deals_left;
         int num_each_deal;
         std::deque<Card *> _cards;
        };
        
        Deck::Deck( vector<Card>& shoe )
        {
            vector<Card>::iterator iter = shoe.begin();
        
            while( iter != shoe.end() )
            {
                _cards.push_front( &(*iter) );
               iter++;
            }
        }
        class SolitaireGame
        {
            public:
                SolitaireGame( int numsuits = 1 );
            private:
                Deck * _deck;
                std::vector<Card> _shoe;
        };
        
        
        
        
        
        SolitaireGame::SolitaireGame( int numsuits )
        {
            Card * c;
            vector<Card> _shoe;
        
            for( int i = 0; i < numsuits; i++ )
            {
                c = new Card();
                _shoe.push_back( *c );
            }
        
            _deck = new Deck( _shoe );
        }
        
        
        
        int main() 
        
        {
            {
            SolitaireGame obj(10);
            }
            int a;
            cin>>a;
            return 0;
        }
        

        【讨论】:

        • 感谢您的回复 SSS。这是一个很好的演示,关于push_back 如何创建副本(在发布这个问题之前我不知道!) - 但是,有点切线,我现在完全困惑为什么调用复制构造函数 35次而不是 10 次!我目前正在调查...
        • @BeeBand ,我没有正确测试所有东西,如果我犯了任何错误,我很抱歉,我的主要目的是让你清楚 push_back 的概念
        【解决方案6】:

        由于这样的游戏对象总是有自己的牌组,因此您应该考虑让牌组对象成为 SolitairGame 中的一个真实成员——而不仅仅是一个指针。这将使甲板对象的生命周期管理更加简单。例如,您将不再需要自定义析构函数。请记住,STL 容器包含副本。如果你写类似

        myvector.push_back(*(new foo));
        

        你有内存泄漏。

        此外,存储指向向量元素的指针是危险的,因为指针(或一般的迭代器)可能会变得无效。对于一个向量,当它需要增长时就是这种情况。另一种方法是 std::list ,它使迭代器在插入、删除等之后保持有效。

        另外,请记住,在 C++ 中,结构和类通常会获得隐式复制构造函数和赋值运算符。尊敬rule of three。要么禁止复制和分配,要么确保资源(包括动态分配的内存)得到妥善管理。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-09-08
          • 1970-01-01
          • 2021-06-14
          • 1970-01-01
          • 2016-08-17
          • 2022-06-13
          • 1970-01-01
          相关资源
          最近更新 更多