C ++对象实例化


113

我是试图理解C ++的C程序员。许多教程使用片段演示了对象实例化,例如:

Dog* sparky = new Dog();

这意味着以后您将执行以下操作:

delete sparky;

这是有道理的。现在,在不需要动态内存分配的情况下,是否有任何理由使用以上内容代替

Dog sparky;

一旦火花超出范围就让析构函数被调用?

谢谢!

Answers:


166

相反,您应该始终偏向于堆栈分配,以作为经验法则,您的用户代码中永远不应包含新内容/删除内容。

就像您说的那样,当在堆栈上声明变量时,超出范围时将自动调用其析构函数,这是您跟踪资源寿命并避免泄漏的主要工具。

因此,通常,每次需要分配资源时,无论是内存(通过调用new),文件句柄,套接字还是其他任何资源,都将其包装在一个类中,在该类中构造函数获取资源,然后析构函数将其释放。然后,您可以在堆栈上创建该类型的对象,并且可以确保资源超出范围时可以释放资源。这样,您不必在任何地方跟踪新的/删除的对,以确保避免内存泄漏。

此成语最常用的名称是RAII

还要研究智能指针类,这些智能指针类用于在极少数情况下必须在专用RAII对象之外分配带有新对象的东西时包装结果指针。而是将指针传递给智能指针,该指针随后通过例如引用计数来跟踪其生存期,并在最后一个引用超出范围时调用析构函数。标准库具有std::unique_ptr用于基于范围的简单管理的功能,std::shared_ptr该库确实进行引用计数以实现共享所有权。

许多教程使用片段(如...)演示了对象实例化。

因此,您发现大多数教程都很烂。;)大多数教程都教给您糟糕的C ++实践,包括在不需要时调用new / delete创建变量,并且使您很难跟踪分配的生命周期。


当您想要类似auto_ptr的语义(转移所有权),但保留非抛出交换操作,并且不希望引用计数的开销时,原始指针很有用。边缘情况也许,但有用。
格雷格·罗杰斯

2
这是一个正确的答案,但是我永远不会养成在堆栈上创建对象的习惯的原因是,它从未完全清楚该对象的大小。您只是在请求堆栈溢出异常。
dviljoen

3
格雷格:当然。但是正如您所说,这是一个极端的情况。通常,最好避免使用指针。但是他们之所以使用语言,是有原因的,不能否认。:) dviljoen:如果对象很大,则将其包装在RAII对象中,该对象可以在堆栈上分配,并包含指向堆分配的数据的指针。
jalf

3
@dviljoen:不,我不是。C ++编译器不会不必要地膨胀对象。您将看到的最糟糕的情况通常是四舍五入到最接近的四个字节的倍数。通常,包含指针的类将占用与指针本身一样多的空间,因此在堆栈使用方面不花任何代价。
jalf

20

尽管在分配和自动释放方面将事物放在堆栈上可能是一个优点,但它也有一些缺点。

  1. 您可能不想在堆栈上分配大对象。

  2. 动态调度!考虑以下代码:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

这将打印“ B”。现在让我们看看使用堆栈时会发生什么:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

这将打印“ A”,这对于熟悉Java或其他面向对象的语言的人可能不直观。原因是您不再有指向该实例的指针B。而是B创建一个实例并将其复制到a类型为的变量A

有些事情可能会凭直觉发生,特别是当您不熟悉C ++时。在C中,您就有了指针,仅此而已。您知道如何使用它们,并且它们总是一样。在C ++中,情况并非如此。试想会发生什么,当你在这个例子中使用作为一个方法的参数-事情变得更复杂,但它使一个巨大的区别,如果a是类型A或者A*甚至A&(呼叫通过引用)。许多组合都是可能的,并且它们的行为都不同。


2
-1:无法理解值语义的人以及多态性需要引用/指针(以避免对象切片)的简单事实并不构成语言的任何问题。c ++的功能不应仅仅因为某些人无法学习其基本规则而被视为缺点。
underscore_d

感谢您的注意。我在方法中引入对象而不是指针或引用时遇到了类似的问题,这是多么混乱。该对象内部有指针,由于这种应对,它们被删除得太早了。
BoBoDev '17

@underscore_d我同意;此答案的最后一段应删除。不要以为我编辑了这个答案就意味着我同意这个事实。我只是不希望读取其中的错误。
TamaMcGlinn

@TamaMcGlinn同意。感谢您的良好编辑。我删除了意见部分。
UniversE

13

好吧,使用指针的原因与使用malloc分配的C中使用指针的原因完全相同:如果您希望对象的寿命比变量长!

如果可以避免,甚至强烈建议不要使用new运算符。特别是如果您使用异常。通常,让编译器释放对象会更安全。


13

我已经从不太了解&地址运算符的人那里看到了这种反模式。如果他们需要使用指针来调用函数,则它们将始终在堆上分配,以便获得指针。

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);

或者:无效FeedTheDog(Dog&hungryDog); 狗狗 FeedTheDog(dog);
Scott Langham

1
@ScottLangham是正确的,但这不是我要说明的重点。有时您正在调用一个带有可选NULL指针的函数,或者它是无法更改的现有API。
马克·兰瑟姆

7

将堆视为非常重要的房地产,并非常明智地使用它。基本的经验法则是,在可能的情况下尽可能使用堆栈,而在没有其他方法时则使用堆。通过在堆栈上分配对象,您可以获得许多好处,例如:

(1)。您无需担心异常情况下的堆栈展开

(2)。您不必担心由于分配的空间超出堆管理器所需而导致的内存碎片。


1
关于对象的大小应该有所考虑。堆栈受大小限制,因此应为堆分配非常大的对象。
亚当

1
@亚当,我认为纳文在这里仍然是正确的。如果您可以将一个大对象放在堆栈上,那么请这样做,因为这样更好。有很多原因您可能做不到。但是我认为他是正确的。
麦凯2013年

5

我担心的唯一原因是Dog现在分配在堆栈上,而不是堆上。因此,如果Dog的大小为兆字节,则可能有问题,

如果确实需要执行新/删除路线,请提防异常。因此,您应该使用auto_ptr或boost智能指针类型之一来管理对象寿命。


1

当您可以在堆栈上进行分配时,没有任何理由(在堆上)新建(除非出于某种原因,您的堆栈很小并且想要使用堆。

如果您确实想在堆上进行分配,则可能要考虑使用标准库中的shared_ptr(或其变体之一)。一旦所有对shared_ptr的引用都不存在,将为您执行删除操作。


有很多原因,例如,请参阅我的答案。
dangerousdave

我想您可能希望对象超出函数的作用域,但是OP似乎没有暗示他们正在尝试这样做。
Scott Langham

0

还有一个其他原因,您可能选择动态创建对象,这是其他人没有提到的。动态的,基于堆的对象使您可以利用多态


2
您也可以多态处理基于堆栈的对象,在这方面,我看不到堆栈/堆分配对象之间的任何区别。例如:void MyFunc(){Dog dog; 喂狗); } void Feed(Animal&animal){auto food = animal-> GetFavouriteFood(); PutFoodInBowl(food); } //在此示例中,GetFavouriteFood可以是每个动物用其自己的实现覆盖的虚函数。这将在不涉及堆的情况下进行多态工作。
Scott Langham

-1:多态只需要引用或指针;它完全与基础实例的存储持续时间无关。
underscore_d

@underscore_d“使用通用基类意味着成本:必须将对象堆分配为多态;” - Bjarne的Stroustrup的 stroustrup.com/bs_faq2.html#object
dangerousdave

大声笑,我不在乎Stroustrup自己是否这么说,但这对他来说是一个令人难以置信的尴尬报价,因为他错了。您知道自己进行测试并不难:在堆栈上实例化一些DerivedA,DerivedB和DerivedC。还要实例化一个指向Base的堆栈分配指针,并确认您可以将其放置在A / B / C上并进行多态使用。即使声明是由语言的原始作者提出的,也要对声明使用批判性思维和标准参考。此处:stackoverflow.com/q/5894775/2757035
underscore_d

这样说,我有一个对象,它包含2个独立的族,每个族近1000个,具有自动存储持续时间。我在堆栈上实例化了该对象,并通过引用或指针引用这些成员,从而实现了其上多态的全部功能。我的程序中没有任何内容是动态/堆分配的(堆栈分配的类拥有的资源除外),并且它不会通过iota更改对象的功能。
underscore_d

-4

我在Visual Studio中遇到了同样的问题。您必须使用:

yourClass-> classMethod();

而不是:

yourClass.classMethod();


3
这不能回答问题。您宁愿注意,必须使用不同的语法来访问在堆上分配的对象(通过指向对象的指针)和在栈上分配的对象。
阿列克谢·伊凡诺夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.