【问题标题】:Proper way to reassign pointers in c++在 C++ 中重新分配指针的正确方法
【发布时间】:2010-04-16 16:01:44
【问题描述】:

编辑:我知道在这种情况下,如果它是一个实际的类,我最好不要将字符串放在堆上。但是,这只是一个示例代码,以确保我理解理论。实际代码将是一棵红黑树,所有节点都存储在堆上。

在继续之前,我想确保我有正确的这些基本想法(我来自 Java/Python 背景)。我一直在网上搜索,但还没有找到这个问题的具体答案。

当你重新分配一个指向新对象的指针时,你是否必须先对旧对象调用 delete 以避免内存泄漏?我的直觉告诉我是的,但在继续之前我想要一个具体的答案。

例如,假设您有一个存储指向字符串的指针的类

class MyClass
{
private:
    std::string *str;

public:
MyClass (const std::string &_str)
{
    str=new std::string(_str);
}

void ChangeString(const std::string &_str)
{
    // I am wondering if this is correct?
    delete str;
    str = new std::string(_str)

    /*
     * or could you simply do it like:
     * str = _str;
     */ 
}
....

在 ChangeString 方法中,哪个是正确的?

如果您不使用 new 关键字作为第二种方式,我想我会挂断电话,它仍然会像您预期的那样编译和运行。这会覆盖这个指针指向的数据吗?还是做其他事情?

任何建议都会非常有用:D

【问题讨论】:

  • 您很可能一开始就不应该分配带有new 的字符串。你有什么具体原因吗?
  • 这里更自然的做法仍然是:*str = _str;(如果您只想分配内容,无需丢弃一个字符串并创建另一个字符串)
  • 这几乎就是我的问题的中心。如果我这样做,我会有内存泄漏吗?从下面的 cmets 中,听起来我会的。
  • 如果你想要ared-black树,std::map有什么问题?
  • @neil,红黑树的全部目的只是让我了解语言/语法/等。每次我学习一门新语言时,我都会先构建一棵红黑树,以确保我了解基础知识(这几乎是我的“hello world”)

标签: c++ memory-management pointers


【解决方案1】:

如果你必须解除分配旧实例并创建另一个,你应该首先确保创建新对象成功:

void reset(const std::string& str)
{
    std::string* tmp = new std::string(str);
    delete m_str;
    m_str = tmp;
}

如果你先调用delete,然后创建一个新的抛出异常,那么类实例将留下一个悬空指针。例如,您的析构函数最终可能会再次尝试删除指针(未定义的行为)。

您也可以通过将指针设置为 NULL 来避免这种情况,但上述方法仍然更好:如果重置失败,对象将保留其原始值。


关于代码注释中的问题。

*str = _str;

这是正确的做法。这是正常的字符串赋值。

str = &_str;

这将是分配指针并且完全错误。您将泄漏先前由str 指向的字符串实例。更糟糕的是,传递给函数的字符串很可能一开始就没有使用new 分配(您不应该混合指向动态分配对象和自动对象的指针)。此外,您可能正在存储一个字符串对象的地址,该对象的生命周期以函数调用结束(如果 const 引用绑定到一个临时对象)。

【讨论】:

    【解决方案2】:

    为什么你认为你需要在你的类中存储一个指向字符串的指针?指向 C++ 集合(如字符串)的指针实际上很少需要。您的课程几乎肯定应该是这样的:

    class MyClass
    {
    private:
        std::string str;
    
    public:
    MyClass (const std::string & astr) : str( astr )
    {
    }
    
    void ChangeString(const std::string & astr)
    {
        str = astr;
    }
    ....
    };
    

    【讨论】:

      【解决方案3】:

      这里只是指出来,但是

      str = _str;
      

      不会编译(您试图将 _str(通过引用传递的字符串的值)分配给 str(字符串的地址)。如果你想这样做,你会写:

      str = &_str;
      

      (您必须更改 _str 或 str 以使 constnest 匹配)。

      但是,正如你的直觉告诉你的那样,你会泄露 str 已经指向的任何字符串对象的内存。

      如前所述,当您在 C++ 中向类中添加变量时,您必须考虑该变量是属于该对象还是属于其他对象。

      如果它归对象所有,那么您最好将其存储为一个值并复制周围的东西(但您需要确保副本不会发生在您的背后)。

      它是不拥有的,那么你可以将它存储为一个指针,你不必一直复制东西。

      其他人会比我更好地解释这一点,因为我对此并不十分满意。 我最终做的很多事情就是编写这样的代码:

      class Foo {
      
      private :
         Bar & dep_bar_;
         Baz & dep_baz_;
      
         Bing * p_bing_;
      
      public:
         Foo(Bar & dep_bar, Baz & dep_baz) : dep_bar_(dep_bar), dep_baz_(dep_baz) {
             p_bing = new Bing(...);
         }
      
         ~Foo() {
           delete p_bing;
         }
      

      也就是说,如果一个对象依赖于 'Java' / 'Ioc' 意义上的某些东西(对象存在于其他地方,你没有创建它,你只想调用它的方法),我会存储依赖作为参考,使用 dep_xxxx。

      如果我创建对象,我会使用带有 p_ 前缀的指针。

      这只是为了让代码更“即时”。不确定是否有帮助。

      只是我的 2c。

      祝你内存管理好运,你说得对,是来自 Java 的棘手部分;在您感到舒适之前不要编写代码,否则您将花费​​数小时来追逐segaults。

      希望这会有所帮助!

      【讨论】:

      • 内存管理的核心是分配/解除分配给定内存的责任。在 Java/C# 中,释放由范围和语言语义管理。在 C/C++ 中,您处理它。
      【解决方案4】:

      C++ 中的一般规则是,对于使用“new”创建的每个对象,都必须有一个“delete”。确保这总是发生在困难的部分;)现代 C++ 程序员避免像瘟疫一样在堆上创建内存(即使用“new”),而是使用堆栈对象。真正考虑您是否需要在代码中使用“新”。很少需要。

      如果您来自垃圾收集语言的背景并且发现自己确实需要使用堆内存,我建议使用 boost 共享指针。你可以这样使用它们:

      #include <boost/shared_ptr.hpp>
      ...
      boost::shared_ptr<MyClass> myPointer = boost::shared_ptr<MyClass>(new MyClass());
      

      myPointer 具有与常规指针几乎相同的语言语义,但 shared_ptr 使用引用计数来确定何时删除它所引用的对象。它基本上是自己做垃圾收集。文档在这里:http://www.boost.org/doc/libs/1_42_0/libs/smart_ptr/smart_ptr.htm

      【讨论】:

      • 现代 C++ 程序员使用 new 来创建所有内容 - 但使用引用计数来避免编写 delete。大多数 C++ 程序员在您需要比堆栈多一点内存的系统上工作(除非您有一个具有 4gb 堆栈的系统!)
      • @josh - 如果我错了,请纠正我,但有很多时候你需要使用堆(在这种情况下是红黑树)。如果我们试图完全在堆栈上创建一棵大树,我们不会只是堆栈溢出吗?或者你的意思是我们可以在堆栈(最终用户)上创建红黑树对象,并且所有节点仍然可以放在堆上(最终用户不必担心)?
      • 请记住,引用计数本身不能检测循环,它不像垃圾回收那样万无一失。如果您指向标准类型,这不是问题,但如果您指向自定义对象,则需要更加小心并了解如何使用weak_ptr。
      • @Martin “现代 C++ 程序员用新的东西创造一切” - 说什么???或许我只是一个古老的 C++ 程序员。
      • 是的,我认为“现代”可能会以错误的方式惹恼某些人。但我坚持。我已经断断续续地用 c++ 编程了三年(差不多四年!)现在,你可以通过查看“新”和“删除”的使用频率来预测 c++ 代码的时代。
      【解决方案5】:

      我会为你写一个类。

      class A
      {
           Foo * foo;   // private by default
       public:
           A(Foo * foo_): foo(foo_) {}
           A(): foo(0) {}   // in case you need a no-arguments ("default") constructor
           A(const A &a):foo(new Foo(a.foo)) {}   // this is tricky; explanation below
           A& operator=(const &A a) { foo = new Foo(a.foo); return *this; }
           void setFoo(Foo * foo_) { delete foo; foo = foo_; }
           ~A() { delete foo; }
      }
      

      对于拥有此类资源的类,复制构造函数、赋值运算符和析构函数都是必需的。复制构造函数和赋值运算符的棘手部分是您需要精确地删除每个Foo 一次。如果复制构造函数初始值设定项是 :foo(a.foo),那么当正在初始化的对象被销毁时,该特定的 Foo 将被删除一次,而当正在从 (a) 初始化的对象被销毁时。

      这个类,我写它的方式,需要记录为拥有它正在传递的Foo指针的所有权,因为Foo * f = new Foo(); A a(f); delete f;也会导致双重删除。

      另一种方法是使用 Boost 的智能指针(这是下一个标准智能指针的核心),并在类定义中使用 boost::shared_ptr&lt;Foo&gt; foo; 而不是 Foo * f;。在这种情况下,复制构造函数应该是A(const A &amp;a):foo(a.foo) {},因为当指向它的共享指针的所有副本都被销毁时,智能指针将负责删除Foo。 (这里也有一些问题,特别是如果您将shared_ptr&lt;&gt;s 与任何其他形式的指针混合使用,但如果您始终坚持使用shared_ptr&lt;&gt;,您应该没问题。)

      注意:我是在没有通过编译器运行的情况下编写的。我的目标是准确和良好的风格(例如在构造函数中使用初始化程序)。如果有人发现问题,请发表评论。

      【讨论】:

      • @Roger:谢谢。我将其更改为分配给foo 的是Foo *。
      【解决方案6】:

      三个cmets:

      你也需要一个析构函数。

      ~MyClass() 
      {
          delete str;
      }
      

      在这种情况下,您真的不需要使用堆分配的内存。您可以执行以下操作:

      class MyClass {
          private: 
              std::string str;
      
          public:
              MyClass (const std::string &_str) {
                  str= _str;
              }
      
              void ChangeString(const std::string &_str) {
                  str = _str;
      };
      

      你不能做注释掉的版本。那将是内存泄漏。 Java 会处理这个问题,因为它具有垃圾收集功能。 C++ 没有这个特性。

      【讨论】:

      • 我在实际使用的类中有一个析构函数。这应该是一个示例代码来演示我的问题。不过还是谢谢。
      • 是的,我知道在这种情况下我不需要使用堆,但我想了解它是如何工作的,因为有时我需要使用堆(我将要做一个红黑树,但我想确保在继续之前了解基本概念)
      • 明白了。不确定你来自哪里。
      • 第三条评论有帮助吗?还是我应该详细说明?
      • 您的第一点是对的,但 OP 还需要一个复制构造函数和赋值运算符。任何管理资源的类(例如,在免费存储中分配的对象)必须具有这两个用户声明的函数和一个析构函数。
      【解决方案7】:

      当你重新分配一个指向新对象的指针时,你是否必须先对旧对象调用 delete 以避免内存泄漏?我的直觉告诉我是的,但在继续之前我想要一个具体的答案。

      是的。如果是原始指针,则必须先删除旧对象。

      当您分配新值时,有一些智能指针类会为您执行此操作。

      【讨论】:

        猜你喜欢
        • 2017-08-03
        • 2012-11-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-11-01
        • 1970-01-01
        • 1970-01-01
        • 2015-10-07
        相关资源
        最近更新 更多