【问题标题】:C++: delete object or delete members?C++:删除对象还是删除成员?
【发布时间】:2013-02-26 05:10:05
【问题描述】:

我想问一下功能上的区别;也许要求一个示例场景,我应该从以下主要方法中的一个选项中进行选择:

#include <iostream>

using namespace std;

class A{
    private:
        int x, y;
    public:
        A(int, int);
    };

class B{
    private:
        int *x, *y;
    public:
        B(int, int);
        ~B();
    };

A:: A(int x, int y){
    this->x = x; this->y = y;
    }

B:: B(int x, int y){
    this->x = new int(x);
    this->y = new int(y); 
    }

B:: ~B(){
    delete this->x;
    delete this->y;
    }

int main(){
    int x = 0, y = 0;
    A* objA = new A(x, y);  // line 1
    B objB1(x, y);          // line 2
    B* objB2 = new B(x, y); // line 3

    delete objA;
    delete objB2;
    return 0;
    }

我知道主方法B objB1(x, y) 中的第二个声明与其他两个明显不同,但是有人可以解释一下标记为第 1 行和第 3 行的构造函数之间的功能差异吗?两种声明中是否有任何不良做法?

谢谢

NAX

更新

首先,我感谢大家给出的所有答案,我真的得到了一些很好的见解。我已经编辑了上面的代码,因为一些答案指出我没有删除我使用的对象,这是公平的,但这不是我问题的目的。我只是想深入了解创建类的不同方法之间的功能差异。感谢所有针对这一点的人。我还在阅读答案。

【问题讨论】:

  • 坚持 A 类和第 2 行声明。
  • 阅读this,然后仔细查看您的代码并考虑哪些可能是“不好的做法”。
  • @naxchange 您实际上是在询问标记为 1 和 3 的行或 A 类和 B 类之间的区别吗?因为除了构造不同类型的对象之外,这两行是相同的。
  • @YaserZhian 好吧,我只是想知道在类定义和对象定义方面哪个才是真正更好的做法,因为上面的所有构造函数都是允许的。不过,当我阅读答案时,我想我应该早点看到它。
  • @WhozCraig 在上面的评论中发布的链接实际上非常棒,它在不同的层面上回答了我的问题。

标签: c++ class heap-memory dynamic-memory-allocation


【解决方案1】:

“功能上的区别……”

在第 1 行,您通过使用 new 关键字在 上分配 A 类型的对象。在堆上,为objA 指向的对象分配空间,这意味着在堆上连续创建了2 个ints,与您的ivar 定义一致。

在第 2 行,您在 stack 上创建了一个 B 类的新对象。当它超出范围时,它将自动调用其析构函数。但是,当 B 被分配时,它将为两个 int 指针(不是整数)分配空间,而这两个指针又将按照您在 B 中指定的那样在 heap 上分配构造函数。当objB1 超出范围时,指针将被析构函数成功指向deleted

在第 3 行,您在 heap 上创建了一个 B 类的新对象。因此,在堆上为两个 int 指针(不是 int)分配空间,然后通过使用 @ 987654326@ 关键字。当你deleteobjB2 时,析构函数被调用,因此两个“其他地方的整数”被释放,然后你在objB2 的原始对象也从堆中释放。

根据 WhozCraig 的评论,A 类绝对是您在示例中显示的两个类定义中最喜欢的。


编辑(评论回复):

WhozCraig 的链接基本上强烈反对使用原始指针。实际上,考虑到这一点,是的,我同意,纯粹基于内存管理,第 2 行更可取,因为 B 在技术上管理自己的内存(尽管它仍然使用原始指针)。

但是,我通常不喜欢(过度)在类中使用 new,因为 new 比等效的堆栈(或 non-new)分配慢很多。因此,我更喜欢 new 整个类而不是单个组件,因为它只需要一个 new 调用,并且所有 ivars 无论如何都分配在堆中。 (更好的是,placement new,但这远远超出了这个问题的范围)。

总结一下:

在内存管理的基础上,第 2 行(B 类)将是首选,但更好的是:

A objAOnStack(x, y); // Avoids heap altogether

如果您将第 1 行包装在一个智能指针中,例如 std::shared_ptrstd::unique_ptr 或类似的东西,那么第 1 行是最好的。

如果没有智能指针包装器,则不应真正考虑第 3 行(无论如何,回避嵌套的new 通常会更好地提高性能)。

【讨论】:

  • 从我从@WhozCraig 的评论中了解到,似乎首选的类定义是 B,首选的对象定义在第 2 行,不是吗?
  • @naxchange 我对@WhozCraig 提供的链接的解释是不同的:如果可能的话,它只是为了避免(“裸”)指针而支持“智能”指针和 RAII(以及其他标准模式) (这通常)。所以这意味着应该避免你的“B”类定义(正如你所拥有的那样),以支持 A 模式或 也许 一个使用 shared_ptr 而不是裸指针的类。但我确实同意第 2 行将是您想要创建对象的模式,在 main() 中列出的三个选项中。
【解决方案2】:

我通常更喜欢A 样式的对象,除非有令人信服的理由使用B 模式,仅仅是因为A 样式的对象更有效。

例如,当分配A 对象时,将保留 2 个整数(在您的机器上可能是 8 个字节)的内存,然后由传递给构造函数的参数初始化。当分配B 对象时,将保留2 个指针int 的内存(在您的机器上也可能是8 个字节),但是当B 对象在您的构造函数中初始化时,传递的每个值都将被复制到新创建的int(在堆上),因此总共使用了 8 个字节的内存。因此,在这个简单的示例中,您的 B 对象占用的内存是 A 对象的两倍。

此外,每次您想要访问 xyB 对象所引用的值时,都需要取消引用指针,这会增加一定程度的间接性和低效率(而且,在许多用例,还可能涉及安全性的 NULL 检查,它添加了一个分支)。当然,每当B 对象被销毁时,都必须进行额外的堆“清理”。 (如果非常频繁地创建和销毁大量堆碎片,这可能会逐渐导致堆碎片。)

【讨论】:

  • 那里不需要NULL 检查。 x!=NULL 是类不变量。您对传入的参数使用 NULL 检查。另外,请注意,存在的一个错误(隐式复制 ctor)不会被 NULL 检查捕获。
  • @MSalters 这就是我写“在许多用例中”的原因。我似乎很清楚,他所问的不仅仅是简单化、愚蠢的代码,而是更普遍地使用指向事物的指针作为相对于“直接” ivars 的 ivars。跨度>
  • 如果你的意思是“实例变量”(又名成员),那么 not-NULL 仍然是正常的期望。您可以使用boost::optional&lt;T&gt; 之类的东西来表示成员是可选的。顺便提一句。 “直接 ivars”的标准名称将是成员子对象。
【解决方案3】:

一般来说,A 类的方式比 B 类更可取。除非你有充分的理由,否则你应该坚持类似于 A 的设计。在简单的情况下,对于像这样的简单数据结构,B 类的方式是实施甚至可以被认为是不好的做法。

这有几个原因,这里没有特别的顺序:

  1. B 类比 A 类多进行两次动态内存分配。在运行时分配内存可能很慢,并且分配和释放大量不同大小的块可能导致所谓的“内存碎片*”(这是一件坏事。)
  2. B 类的实例比A 类的实例大。A 的实例是两个整数的大小,通常每个32 位,这使得整个实例为8 个字节。 B 的实例需要两个指针(每个指针可以是 32 位或 64 位,具体取决于您的代码是针对 32 位还是 64 位架构编译的)加上两个实际整数(每个 4 字节)加上堆分配器为每个分配存储的一些元数据,每个分配可能是 0 到 32 字节或更多字节。因此,B 的每个实例都比 A 的每个实例大 8、16 或(更多)字节,同时基本上执行相同的工作。
  3. 访问 B 实例内的字段(xy)比访问 A 实例内的字段慢。当访问 B 实例的成员时,您所拥有的只是它们的 指针的位置。因此 CPU 获取指针,然后它可以知道保存 xy 值的实际整数的地址,然后它可以读取或写入它们的值。
  4. 在 A 的实例中,您确定 xy 存储在连续的内存地址中。这是从 CPU 缓存中获得最大收益的最佳情况。在 B 的一个实例中,实际的xy 所在的地址可能彼此相距很远,您从 CPU 缓存中获得的好处会更少。
  5. 在 A 中,成员的生命周期与包含它们的对象的生命周期完全相同。对于B来说,没有这样的内在保证。在这个简单的例子中情况并非如此,但在更复杂的情况下,特别是在存在异常的情况下,这一点变成了一个明显而现实的危险。编程错误(例如,忘记delete 在析构函数的一些很少执行的路径中的一个成员)也是 B 的问题。

请注意,有时,将对象的生命周期与成员数据解耦是您真正想要的,但这通常不是好的设计。如果您想了解更多信息,请查阅 C++ 中的 RAII 模式。

顺便说一句,正如在其他 cmets 中所指出的,您必须为 B 类实现(或声明 private)复制构造函数和赋值运算符。

由于与上述相同的原因,您应该尽量避免newing 您的数据,这意味着在标记为 1、2 和 3 的行中,第 2 行实际上是创建实例的更好方法。

【讨论】:

    【解决方案4】:

    您应该为您的 B 类定义一个复制构造函数和一个赋值运算符。否则您将在使用这些指针时遇到严重问题。除此之外,第 1 行和第 3 行在功能上没有区别。唯一的区别在于实现。

    话虽如此,没有理由在 B 中使用指针。如果您需要固定数量的整数,请使用纯整数或纯数组。如果您需要可变数量的整数,请使用std::vector。如果您确实需要分配动态内存,请非常小心并考虑使用智能指针。

    如果你的 B 类只包含一个 [pointer to] 整数,它可能是这样的:

    class B
    {
        private:
    
            int * x;
    
        public:
    
            B (int i)       { x = new int(i); }
            B (const B & b) { x = new int(*b.x); }
            ~B()            { delete x; }
    
            B & operator= (const B & b)  // Corner cases:
            {                            //
                int * p = x;             // 1) b and *this might
                x = new int(*b.x);       //    be the same object
                delete p;                //
                return *this;            // 2) new might throw
            }                            //    an exception
    };
    

    即使在注释的极端情况下,此代码也会执行“正确的事情 (TM)”。

    另一种选择是:

    #include <utility>   // std::swap
    
    class B
    {
        private:
    
            int * x;
    
        public:
    
            B (int i)       { x = new int(i); }
            B (const B & b) { x = new int(*b.x); }
            ~B()            { delete x; }
    
            void swap (B & b)
            {
                using std::swap;
                swap (x, b.x);
            }
    
            B & operator= (const B & b)  // Corner cases:
            {                            //
                B tmp(b);                // 1) b and *this might
                swap (tmp);              //    be the same object
                return *this;            //
            }                            // 2) new might throw
    };                                   //    an exception
    

    但是,如果有两个指针 --- 就像在你的例子中---,你必须调用 new 两次。如果第二个new 未能抛出异常,您会希望自动delete 由第一个new 保留的内存...

    #include <utility>   // std::swap
    
    class B
    {
        private:
    
            int * x;
            int * y;
    
            void init (int i, int j)
            {
                x = new int(i);
    
                try
                {
                    y = new int(j);
                }
                catch (...)     // first new was OK but
                {               // second failed, so undo
                    delete x;   // first allocation and
                    throw;      // continue the exception
                }
            }
    
        public:
    
            B (int i, int j) { init (i, j); }
            B (const B & b)  { init (*b.x, *b.y); }
            ~B()             { delete x; delete y; }
    
            void swap (B & b)
            {
                using std::swap;
                swap (x, b.x);
                swap (y, b.y);
            }
    
            B & operator= (const B & b)  // Corner cases:
            {                            //
                B tmp(b);                // 1) b and *this might
                swap (tmp);              //    be the same object
                return *this;            //
            }                            // 2) new might throw
    };                                   //    an exception
    

    如果你有三四个 [指向] int 的 [指针]... 代码会变得更丑陋!这就是智能指针和 RAII(资源获取即初始化)真正有用的地方:

    #include <utility>   // std::swap
    #include <memory>    // std::unique_ptr (or std::auto_ptr)
    
    class B
    {
        private:
    
            std::auto_ptr<int> x;   // If your compiler supports
            std::auto_ptr<int> y;   // C++11, use unique_ptr instead
    
        public:
    
            B (int i, int j) : x(new int(i)),      // If 2nd new
                               y(new int(j)) {}    // fails, 1st is
                                                   // undone
            B (const B & b)  : x(new int(*b.x)),
                               y(new int(*b.y)) {}
    
            // No destructor is required
    
            void swap (B & b)
            {
                using std::swap;
                swap (x, b.x);
                swap (y, b.y);
            }
    
            B & operator= (const B & b)  // Corner cases:
            {                            //
                B tmp(b);                // 1) b and *this might
                swap (tmp);              //    be the same object
                return *this;            //
            }                            // 2) new might throw
    };                                   //    an exception
    

    【讨论】:

      【解决方案5】:

      第 1 行创建 objA 并留下内存泄漏,因为 objA 没有被删除。如果它被删除,成员 x 和 y 也将被删除。 objA 还支持复制构造函数和赋值运算符。这些调用不会有任何问题:

      func1(*objA)
      A objB = *objA.
      

      如果您对 objB2 执行相同的操作,您将遇到内存访问冲突,因为 x 和 y 指向的同一内存将被删除两次。您需要创建私有复制构造函数和赋值运算符来防止这种情况发生。

      关于场景:

      1. 第 1 行和第 3 行适用于将对象返回给调用函数。 调用函数需要负责删除 它。在 B 类中,x 和 y 可以是指向基类的指针。所以他们可以 是多态的。
      2. Line2 适合将此对象传递给下面的调用函数 调用栈。对象将在当前函数时被删除 退出。

      【讨论】:

        猜你喜欢
        • 2013-05-12
        • 1970-01-01
        • 2013-07-28
        • 2011-02-18
        • 2011-09-19
        • 1970-01-01
        • 2017-11-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多