C ++:如何正确返回指向对象的指针

时间:2012-02-23 23:16:28

标签: c++

我一直在为这个问题磕磕绊绊。我试图返回指向对象的指针,我可以说

MyObject* obj = Manager::Create(int i, int j);

如何正确分配内存以避免泄漏?我以为我应该调用new来在堆上记忆,但最近我被告知了。

3 个答案:

答案 0 :(得分:4)

  

“我一直在为这个问题磕磕绊绊。我试图返回指向对象的指针,我可以说

   MyObject* obj = Manager::Create(int i, int j);
     

如何正确分配内存以避免泄漏?我以为我应该打电话给新记录,但我最近却被告知了。“

由于你在询问如何分配内存,并且因为这发生在Manager::Create内,我能看到的唯一合理解释是你是那个写Manager::Create函数的人。

所以,首先,你真的需要一个工厂“manager”实际管理什么(如果有的话)?

我的印象是,来自Java背景的人们在添加不必要的动态分配和工厂以及“管理者”,单身人士和信封模式等方面具有强大的倾向性,这些在C ++中通常是不合适的。

别。

例如,如果您的obj仅在本地范围内需要, 使用自动存储 (基于堆栈的分配和解除分配),这可以是订单比类似Java的动态分配更有效:

    MyObject obj( i, j );

这是适用的,那么问题“我如何正确分配内存以便没有泄漏”在C ++中有一个非常简单的答案: 只是声明变量 ,如上所述。

即使您必须从函数返回此类对象,这也适用。然后只需 按值返回 (显然是复制对象)。例如,与下面的foo::reduce函数一样,

#include <iostream>         // std::cout, std::endl
#include <string>           // std::string, std::to_string

namespace foo {
    using namespace std;

    class MyObject
    {
    private:
        string      description_;

    public:
        string description() const { return description_; }

        MyObject( int const x, int const y )
            : description_( "(" + to_string( x + 0LL ) + ", " + to_string( y + 0LL ) + ")" )
        {}
    };

    ostream& operator<<( ostream& stream, MyObject const& o )
    {
        return stream << o.description();
    }

    int gcd( int a, int b )
    {
        return (b == 0? a : gcd( b, a % b ));
    }

    MyObject reduce( int const a, int const b )
    {
        int const gcd_ab = gcd( a, b );
        return MyObject( a/gcd_ab, b/gcd_ab );
    }
}  // namespace foo

int main()
{
    using namespace foo;

    int const   a = 42;
    int const   b = 36;

    cout << MyObject( a, b ) << " -> " << reduce( a, b ) << endl;
}

现在让我们看看如何通过引入不必要的动态分配来使这种简洁,简单和高效的代码变得冗长,复杂和低效。我写“不必要”,因为标准C ++库的容器等和C ++语言的设施已经避免了动态分配的大多数传统原因。例如,以前您可能已经使用过指针以避免代价高昂的复制,并因此遇到“我需要清理,但是对象是在自动存储上分配还是动态?”的问题,使用C ++ 03可以使用智能诸如boost::shared_ptr之类的指针,无论对象的来源如何,都可以自动进行适当的破坏,而使用C ++ 11,您可以使用移动语义来大大避免复制效率低下,从而首先不会弹出问题。 / p>

因此,在下面的代码中使用动态分配,使用现代C ++,非常 artifical 解释;它没有任何实际优势。

但是通过这种动态分配,即使对于这个例子它是人为的并且被解释,必须保证适当的解除分配,并且通常的方法是 使用智能指针 < / strong>例如std::unique_ptr

#include <iostream>         // std::cout, std::endl
#include <memory>           // std::unique_ptr, std::default_delete
#include <string>           // std::string, std::to_string

namespace foo {
    using namespace std;

    class MyObject
    {
    friend struct default_delete<MyObject>;
    private:
        string      description_;

    protected:
        virtual ~MyObject() {}  // Restrics class to dynamic allocation only.

    public:
        typedef unique_ptr<MyObject> Ptr;

        string description() const { return description_; }

        MyObject( int const x, int const y )
            : description_( "(" + to_string( x + 0LL ) + ", " + to_string( y + 0LL ) + ")" )
        {}
    };

    ostream& operator<<( ostream& stream, MyObject const& obj )
    {
        return stream << obj.description();
    }

    int gcd( int a, int b )
    {
        return (b == 0? a : gcd( b, a % b ));
    }

    MyObject::Ptr reduce( int const a, int const b )
    {
        int const gcd_ab = gcd( a, b );
        return MyObject::Ptr( new MyObject( a/gcd_ab, b/gcd_ab ) );
    }
}  // namespace foo

int main()
{
    using namespace foo;

    int const   a = 42;
    int const   b = 36;

    MyObject::Ptr const pData( new MyObject( a, b ) );
    MyObject::Ptr const pResult( reduce( a, b ) );

    cout << *pData << " -> " << *pResult << endl;
}

注意protected析构函数和调用析构函数的代码的friend声明。 protected析构函数确保不能创建静态或自动实例,只能使用动态分配(例如,可以强制实施此限制,以便更容易实现类并确保没有自动对象链接到动态数据结构中)。 friend声明使protected析构函数可以被标准库的对象销毁函数访问 - 但遗憾的是,unique_ptr只使用了该函数。

我提到并举例说明这是因为你有一个工厂函数,它有时用于将类限制为动态分配(特别是来自Java的人)。出于此目的,工厂函数是不合适的,因为使用 n 构造函数,您需要 n 工厂函数。相反,使用protected析构函数,您只需要一个公共的删除函数,如上所示,这是由标准库提供的。

所以,结果,通常是“如何正确分配内存以便没有泄漏”这一问题的答案是 将解除分配责任委托给语言或标准库 ,或其他库组件。首先,这意味着使用自动存储(局部变量)和值返回。但是当有需要时,它还包括使用标准库集合类,例如std::vectorstd::map。只有在那些不提供所需功能的情况下,才考虑动态分配。然后将解除分配责任委托给std::unique_ptrstd::shared_ptr等智能指针。

答案 1 :(得分:2)

通常您会返回std::unique_ptr<MyObject>,并执行以下操作:

return std::unique_ptr<MyObject>(Manager::Create(i, j), &Manager::Free);

或者您是否正在尝试自己撰写Manager::Create?如果是这样的话:

std::unique_ptr<MyObject> Manager::Create(int i, int j)
{
    return std::unique_ptr<MyObject>(new MyObject(i, j)); // default deleter is appropriate
}

答案 2 :(得分:2)

  

我以为我应该打电话给新人来堆积内存,但最近我被告知了。

嗯......我觉得你有点困惑。是的,您可以调用new来动态分配内存。然而,有一些常见的模式(参见; RAII)用于不惜一切代价避免它,因为它是一种轻松的方式射击自己(阅读;写错误)。

在某些时候, somethign 必须调用new才能动态分配内存。首先,你需要问问自己;这是否需要动态分配?如果答案是否定的,那么就这样声明并继续前进。

void Foo() { 
    MyObject obj;  // automatic storage space, will be cleaned up when the scope is left
}

接下来,为什么不将指针存储在std::unique_ptr或类似的东西中?这将照顾你的管理,再次,你不管理记忆;

// calls delete in the destructor
std::unique_ptr pointer( new MyObject() );

BOOST库有一个等价的类(unique_ptr是C ++ 11)。

关键是内存是由类通过其构造函数和析构函数来管理的。您在创建对象时动态分配内存,并在其析构函数中释放它(即,调用delete)。您只需在尽可能紧的范围内堆叠分配这些内容,您就不必担心内存泄漏。

我每天都在写C ++,而且我几乎从不致电newdelete。作为一个小例子,让我们来看看这个类,这是一个范围指针的简单实现(注意;这不是一个“正确的”实现,而且过于简单!这个问题比这更难解决,但我只是单独使用它作为管理动态分配对象的生命周期的一个例子。)

template<class T>
class ScopedPointer {
public:
    ScopedPointer(T* obj) {
        m_pointer = obj;
    }

    ~ScopedPointer() {
        delete m_pointer;
    }

    inline T* operator->() 
    { 
        return( m_pointer );
    }

    inline bool IsValid() const {
        return m_pointer != NULL;
    }

private: 
    T* m_pointer;
};

您可以将此类用作指向动态分配内存的指针的包装器。当它离开它的范围时,它的析构函数将被调用并且内存将被清除。同样,这不是生产质量代码!这是不正确的,因为它缺少真实世界类需要的几种机制(复制/所有权语义,更先进的解除分配器等)。

void Foo() {
    ScopedPointer<MyObject> ptr( new MyObject() );
    ptr->whatever();
}  // destructor is called, dynamic memory is freed