1

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

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

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

谢谢

纳克斯

更新

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

4

5 回答 5

1

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

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

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

于 2013-03-11T05:27:50.650 回答
1

“功能上的区别……”

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

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

在第 3 行,您在堆上创建了一个 B 类的新对象。因此,在堆上为两个int 指针(不是 int)分配空间,然后这些 int 又通过使用关键字在堆上的其他位置分配。new当 youdelete objB2时,析构函数被调用,因此两个“其他地方的整数”被释放,然后你的原始对象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_ptrorstd::unique_ptr或类似的东西,则第 1 行是最好的。

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

于 2013-03-11T05:29:24.557 回答
1

您应该为您的 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

如果你有三四个 [pointers to] 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
于 2013-03-11T05:30:21.033 回答
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 获取指针,然后它可以知道保存xand值的实际整数的地址,然后它就y可以读取或写入它们的值。
  4. 在 A 的实例中,您确定x并且y存储在连续的内存地址中。这是从 CPU 缓存中获得最大收益的最佳情况。在 B 的实例中,实际x和所在的地址y可能彼此相距很远,并且您从 CPU 缓存中获得的好处会更少。
  5. 在 A 中,成员的生命周期与包含它们的对象的生命周期完全相同。对于B来说,没有这样的内在保证。在这个简单的例子中情况并非如此,但在更复杂的情况下,特别是在存在异常的情况下,这一点就变成了一个明显而现实的危险。在 B 的情况下,编程错误(例如,在一些很少执行的析构函数路径中忘记了delete一个成员)也是一个问题。

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

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

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

于 2013-03-11T05:37:40.550 回答
0

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

func1(*objA)
A objB = *objA.

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

关于场景:

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