我是否应该使用"new"运算符来实例化一个类,为什么?

10

我知道这可能被解释为那些“你更喜欢什么”的问题之一,但我真的想知道为什么你会选择以下其中一种方法。

假设您有一个超复杂类,例如:


class CDoSomthing {

    public:
        CDoSomthing::CDoSomthing(char *sUserName, char *sPassword)
        {
            //Do somthing...
        }

        CDoSomthing::~CDoSomthing()
        {
            //Do somthing...
        }
};

我该如何在全局函数中声明一个局部实例?


int main(void)
{
    CDoSomthing *pDoSomthing = new CDoSomthing("UserName", "Password");

    //Do somthing...

    delete pDoSomthing;
}

-- 或 --


int main(void)
{
    CDoSomthing DoSomthing("UserName", "Password");

    //Do somthing...

    return 0;
}

11
哇,这个问题的标题直接来自于冗余部门。 - zweiterlinde
@zweiterlinde - 需要点赞那条评论 - random
“我是谁?如果是这样,那我有多少个?”这让人想起一些东西。话虽如此,这种奇怪的编程语言本身就非常冗余,你不觉得吗?只是在这里进行一些随意评论... - flow
7个回答

27

除非需要对象的生命周期延伸到当前块之外,否则最好使用本地变量(本地变量是第二选择)。这样做只是为了更轻松地处理内存管理问题。

附言:如果需要指针,因为需要将其传递给另一个函数,请使用取地址运算符:

SomeFunction(&DoSomthing);

第二个选项是做什么的? - OscarRyz
@Reyes,是的。您的第二个示例依赖于RAII。在这种情况下无关紧要(除非您调用main...),但对于许多其他情况,指针的异常处理是一个问题,并且依赖于RAII更安全。 - strager
我对于不用担心内存管理的想法也是一样的,但我是那种“老派”的开发者,每个malloc()都必须有一个匹配的free()。我猜我也会把这个扩展到我的类中,但这会增加很多额外的打字工作。 - NTDLS
如果我们谈论智能指针,那么new/delete(或malloc/free)不匹配将是相关的,但我们现在没有。这两个示例都是匹配的,第一个示例有一个new和一个delete,第二个示例没有。 - Mark Ransom
4
除非你在一个小堆栈的环境下工作(我在看着你,Windows CE...) - Tim Lesher
8
NTDLS:这不是传统做法,而是C++中极其糟糕的实践。这意味着会出现一类令人讨厌的错误,如果使用RAII是根本不可能发生的。在C语言中,你显然没有选择。这就是为什么不能把C++当成带类的C来处理。 - jalf

22
在声明变量时,将变量分配到栈内和堆内有两个主要的考虑因素 - 生命周期控制和资源管理。
当您对对象的生命周期有严格控制时,将其分配到栈内非常有效。这意味着您不会将该对象的指针或引用传递给超出本地函数作用域的代码。这意味着没有输出参数、没有COM调用、也没有新线程。尽管存在许多限制,但您可以在当前作用域正常或异常退出时自动清理对象(不过,您可能需要阅读一下带有虚析构函数的栈展开规则)。栈分配的最大缺点是 - 栈通常限制为4K或8K,因此您可能需要小心放置在其中的内容。
另一方面,将实例分配到堆上需要手动清理实例,这也意味着您有很大的自由来控制实例的生命周期。您需要在两种情况下这样做:a)您将要将该对象传递到作用域之外;或者b)对象太大,在栈上分配可能会导致栈溢出。
顺便说一下,这两种方法之间的一个不错的折衷方法是将对象分配到堆上,并在栈上分配智能指针以引用它。这样可以确保不浪费珍贵的栈内存,同时在作用域退出时仍可以自动清理对象。

我认为“栈的大小通常限制在4K或8K内”这一说法现如今不太适用了。我刚刚用Visual C++ 2005 Express进行了一个实验,发现我可以将char数组大小放到0xfbf8c(=1032076)字节,然后将其放置在hello world程序的栈上。再多就会发生堆栈溢出。除此之外还不错。 - Bill Forster
1
在Windows中,默认情况下(可以在线程创建时进行控制),用户模式下的堆栈限制为1 MB,x86内核模式下为12 kB,x64内核模式下为24 kB。在x86架构的Linux上,内核模式下的堆栈大小为8 kB或4 kB,具体取决于配置选项;也许这就是您所考虑的? - bk1e
哈哈,我在想DOS呢。我有一段时间没有看堆栈大小了。你知道,C#并不是很适合查看内存布局... :-) - Franci Penov
还有人在使用DOS吗...?我的意思是,认真的吗...? - Camilo Martin
@Camilo Martin - 是的。仍有许多销售终端使用DOS操作系统。 - Franci Penov

13

第二种形式是所谓的RAII(资源获取即初始化)模式。它比第一种具有许多优点。

当使用new时,必须自己使用delete并确保它总是被删除,即使抛出异常也要如此。你必须自己保证所有这些。

如果使用第二种形式,当变量超出作用域时,它将始终自动清除。如果抛出异常,堆栈展开时也会清除。

因此,您应该偏向RAII(第二个选项)。


@Oscar:是的,在问题的示例中,那个变量叫做DoSomthing。 - R. Martinho Fernandes
1
如果您能告诉我哪里出了问题,我会非常感激。我很高兴能够知道自己的错误并从中学习。 - R. Martinho Fernandes
1
我没有点踩,但是... 我对RAII的理解是一个对象拥有和管理资源。在这个例子中,对象本身就是资源,所以我不确定这个缩写是否适用。 - Mark Ransom

6
除了已经提到的内容外,在考虑性能时还需要考虑其他因素,特别是在内存分配密集型应用程序中:
1. 使用new将从堆中分配内存。在极度频繁的分配和释放情况下,您将付出高昂的代价,包括:
- 锁定:堆是进程中所有线程共享的资源。对堆的操作可能需要在堆管理器中进行锁定(由运行时库为您完成),这可能会显着减慢速度。 - 碎片化:堆会产生碎片。您可能会看到malloc/new和free/delete返回所需时间增加10倍。这与上述锁定问题相结合,因为处理碎片化堆需要更多时间,并且更多的线程排队等待heal锁。(在Windows上,您可以设置特殊标志以便堆管理器启发式地尝试减少碎片化。)
2. 使用RAII模式,内存只需从堆栈中取出即可。堆栈是每个线程的资源,它不会碎片化,没有锁定,而且在内存局部性方面(即CPU级别的内存缓存)可能对您有利。
因此,当您需要对象进行短暂(或作用域)时间时,一定要使用第二种方法(本地变量,在堆栈上)。如果需要在线程之间共享数据,请使用new/malloc(一方面必须使用,另一方面这些对象通常具有足够长的生命周期,因此与堆管理器相比,您几乎不需要付出任何代价)。

只是为了澄清,你打错了。它是RAII。而且不是我的想法叫它那样。这是一个众所周知的模式,显然是由Bjarn Stroustroup发明的。 - R. Martinho Fernandes

3
第二个版本会在抛出异常时解开堆栈。而第一个版本则不会。除此之外,我并没有看到太大的差别。

一个被分配到堆上(除非重载了operator new),另一个被分配到栈上。 - strager
@strager - 第一个在堆栈上有一个指针和一个对象,而后者全部在堆栈上。 - Paul Beckingham

2
两者最大的区别在于使用new关键字时会初始化一个指向对象的指针。如果不使用new,所初始化的对象将存储在堆栈中。如果使用new,则会返回一个指向已创建的新对象的指针,实际上是返回指向新对象的内存地址。这时,您需要管理变量的内存。当您使用完变量后,需要调用delete以避免内存泄漏。如果没有使用new运算符,则当变量超出范围时,内存将自动释放。
因此,如果您需要将变量传递到当前范围之外,则使用new更有效。但是,如果您需要创建临时变量或仅临时使用的变量,则将对象放在堆栈上将更好,因为您无需担心内存管理。

0

Mark Ransom是正确的,如果你要将变量作为参数传递给CreateThread-esque函数,你还需要使用new进行实例化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接