C ++:删除对象还是删除成员?

时间:2013-03-11 05:10:05

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

我想询问功能差异;也许会问一个示例场景,我应该从下面主要方法中的一个选项中选择:

#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)中的第二个声明明显不同于其他2,但有人可以解释标记为1和3的行中构造函数之间的功能差异吗?两种声明都有不良做法吗?

由于

NAX

更新

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

5 个答案:

答案 0 :(得分:1)

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

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

此外,每次要访问xy B对象引用的值时,都需要取消引用指针,这会增加一个间接级别,效率低下(并且,在许多用例中,也可能涉及对安全性进行NULL检查,这会增加一个分支)。当然,每当B个对象被销毁时,都需要进行额外的堆“清理”。 (如果很多频繁地创建和销毁它们,这会逐渐导致堆碎片。)

答案 1 :(得分:1)

  

“功能差异......”

在第1行,您可以使用new关键字在上分配类型A的对象。在堆上,为objA指向的对象分配空间,这意味着在堆上创建2 ints,连续,符合您的ivar定义。

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

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

根据WhozCraig的评论,类objB2绝对是您在示例中显示的两个首选类定义。


编辑(评论回复):

WhozCraig的链接基本上强烈反对使用原始指针。鉴于这一点,是的,我同意,第2行纯粹是基于内存管理的首选,因为A在技术上管理自己的内存(尽管它仍然使用原始指针)。

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

总结一下:

第2行(类placement new)在内存管理的基础上是首选,但是比这更好:

B

第1行是最好的,前提是你将它包装在智能指针中,例如A objAOnStack(x, y); // Avoids heap altogether std::shared_ptr或类似的东西。

如果没有智能指针包装器,就不应该考虑第3行(并且通常更好地避免使用嵌套的std::unique_ptr)。

答案 2 :(得分:1)

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

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

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

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
};

即使在评论的角落里,这段代码也会做“The Right Thing(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

如果你有三个或四个[指向]整数...代码会变得更加丑陋!这就是智能指针和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

答案 3 :(得分:1)

一般来说,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的情况下也是一个问题。
  6. 请注意,有时,将对象的生命周期与成员数据分离是您真正想要的,但这通常不被认为是好的设计。如果您想了解更多信息,请在C ++中查找 RAII 模式。

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

    由于上述相同的原因,如果可以的话,你应该尽量避免new你的数据,这意味着在标记为1,2和3的行中,第2行实际上是更好的制作方法实例

答案 4 :(得分:0)

第1行创建objA并留下内存泄漏,因为objA未被删除。如果删除,则成员x和y也将被删除。 objA也支持复制构造函数和赋值运算符。这些电话不会出现问题:

func1(*objA)
A objB = *objA.

如果使用objB2执行相同的行,则会出现内存访问冲突,因为x和y指向的相同内存将被删除两次。您需要创建私有拷贝构造函数和赋值运算符以防止这种情况。

关于方案:

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