为什么不在C ++中使用指针?

时间:2009-06-30 15:26:03

标签: c++ pointers stack heap

假设我定义了一些类:

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}

然后使用它编写一些代码。我为什么要这样做?

Pixel p;
p.x = 2;
p.y = 5;

来自Java世界我总是写道:

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;

他们基本上做同样的事情,对吗? 一个在堆栈上而另一个在堆上,所以我将在以后删除它。两者之间有什么根本区别吗?为什么我更喜欢一个呢?

23 个答案:

答案 0 :(得分:186)

是的,一个在堆栈上,另一个在堆上。有两个重要的区别:

  • 首先,显而易见且不太重要的一个:堆分配很慢。堆栈分配很快。
  • 其次,更重要的是RAII。因为堆栈分配的版本会自动清理,所以有用。它的析构函数会自动调用,这样可以保证清除类分配的任何资源。这很重要,你如何避免C ++中的内存泄漏。你永远不要自己调用delete来避免它们,而是将它包装在内部调用delete的堆栈分配对象中,典型地在它们的析构函数中。如果您尝试手动跟踪所有分配,并在正确的时间呼叫delete,我保证每100行代码至少会有一次内存泄漏。

作为一个小例子,请考虑以下代码:

class Pixel {
public:
  Pixel(){ x=0; y=0;};
  int x;
  int y;
};

void foo() {
  Pixel* p = new Pixel();
  p->x = 2;
  p->y = 5;

  bar();

  delete p;
}

相当无辜的代码,对吧?我们创建一个像素,然后我们调用一些不相关的函数,然后我们删除像素。是否有内存泄漏?

答案是“可能”。如果bar抛出异常会怎样? delete永远不会被调用,像素永远不会被删除,我们会泄漏内存。现在考虑一下:

void foo() {
  Pixel p;
  p.x = 2;
  p.y = 5;

  bar();
}

这不会泄漏内存。当然,在这个简单的情况下,所有内容都在堆栈中,因此它会自动清理,但即使Pixel类在内部进行了动态分配,也不会泄漏。 Pixel类只会被赋予一个删除它的析构函数,无论我们如何离开foo函数,都会调用这个析构函数。即使我们离开它,因为bar引发了异常。下面这个有点人为的例子说明了这一点:

class Pixel {
public:
  Pixel(){ x=new int(0); y=new int(0);};
  int* x;
  int* y;

  ~Pixel() {
    delete x;
    delete y;
  }
};

void foo() {
  Pixel p;
  *p.x = 2;
  *p.y = 5;

  bar();
}

Pixel类现在在内部分配一些堆内存,但是它的析构函数负责清理它,所以当使用类时,我们不必担心它。 (我应该提一下,这里的最后一个例子很简单,为了显示一般原则。如果我们实际使用这个类,它也包含几个可能的错误。如果y的分配失败,x永远不会被释放如果Pixel被复制,我们最终会尝试删除相同数据的两个实例。所以在这里用最后的例子进行操作。真实世界的代码有点棘手,但它显示了一般的想法)< / p>

当然,相同的技术可以扩展到除内存分配之外的其他资源。例如,它可用于保证文件或数据库连接在使用后关闭,或者释放线程代码的同步锁。

答案 1 :(得分:30)

在添加删除之前,它们不一样 您的示例过于简单,但析构函数实际上可能包含执行某些实际工作的代码。这被称为RAII。

所以添加删除。即使异常传播,也要确保它发生。

Pixel* p = NULL; // Must do this. Otherwise new may throw and then
                 // you would be attempting to delete an invalid pointer.
try
{
    p = new Pixel(); 
    p->x = 2;
    p->y = 5;

    // Do Work
    delete p;
}
catch(...)
{
    delete p;
    throw;
}

如果你选择了一些更有趣的东西,比如一个文件(这是一个需要关闭的资源)。然后在Java中使用指针正确执行此操作。

File file;
try
{
    file = new File("Plop");
    // Do work with file.
}
finally
{
    try
    {
        file.close();     // Make sure the file handle is closed.
                          // Oherwise the resource will be leaked until
                          // eventual Garbage collection.
    }
    catch(Exception e) {};// Need the extra try catch to catch and discard
                          // Irrelevant exceptions. 

    // Note it is bad practice to allow exceptions to escape a finally block.
    // If they do and there is already an exception propagating you loose the
    // the original exception, which probably has more relevant information
    // about the problem.
}

C ++中的相同代码

std::fstream  file("Plop");
// Do work with file.

// Destructor automatically closes file and discards irrelevant exceptions.

虽然人们提到速度(因为在堆上查找/分配内存)。就个人而言,这对我来说不是一个决定因素(分配器非常快,并且针对不断创建/销毁的小对象的C ++使用进行了优化)。

我的主要原因是物体生命时间。本地定义的对象具有非常特定且定义良好的生命周期,并且保证在最后调用析构函数(因此可以具有特定的副作用)。另一方面,指针控制具有动态寿命的资源。

C ++和Java之间的主要区别是:

谁拥有指针的概念。所有者有责任在适当的时间删除对象。这就是为什么你很少在实际程序中看到 raw 指针的原因(因为没有与 raw 指针相关联的所有权信息)。相反,指针通常包含在智能指针中。智能指针定义谁拥有内存的语义,从而定义谁负责清理它。

例如:

 std::auto_ptr<Pixel>   p(new Pixel);
 // An auto_ptr has move semantics.
 // When you pass an auto_ptr to a method you are saying here take this. You own it.
 // Delete it when you are finished. If the receiver takes ownership it usually saves
 // it in another auto_ptr and the destructor does the actual dirty work of the delete.
 // If the receiver does not take ownership it is usually deleted.

 std::tr1::shared_ptr<Pixel> p(new Pixel); // aka boost::shared_ptr
 // A shared ptr has shared ownership.
 // This means it can have multiple owners each using the object simultaneously.
 // As each owner finished with it the shared_ptr decrements the ref count and 
 // when it reaches zero the objects is destroyed.

 boost::scoped_ptr<Pixel>  p(new Pixel);
 // Makes it act like a normal stack variable.
 // Ownership is not transferable.

还有其他人。

答案 2 :(得分:25)

逻辑上他们做同样的事 - 除了清理。只是您编写的示例代码在指针大小写中存在内存泄漏,因为该内存未被释放。

来自Java背景,你可能还没有完全准备好C ++围绕着分配的内容以及负责释放它的人有多少。

通过在适当的时候使用堆栈变量,您不必担心释放该变量,它会随堆栈帧一起消失。

显然,如果你非常小心,你总是可以在堆上分配并手动免费,但是好的软件工程的一部分是以不会破坏的方式构建东西,而不是信任你的超级 - 人类程序员永远不会犯错。

答案 3 :(得分:24)

每当我有机会时,我更喜欢使用第一种方法,因为:

  • 它更快
  • 我不必担心内存释放
  • p将是整个当前范围的有效对象

答案 4 :(得分:14)

“为什么不在C ++中使用指针”

一个简单的答案 - 因为管理内存成为一个巨大的问题 - 分配和删除/释放。

自动/堆栈对象删除了一些繁忙的工作。

这是我对这个问题的第一句话。

答案 5 :(得分:11)

代码:

Pixel p;
p.x = 2;
p.y = 5;

没有动态分配内存 - 没有搜索空闲内存,没有更新内存使用情况,没有。它完全免费。编译器在编译时为变量保留堆栈空间 - 它有足够的空间来保留并创建一个操作码来将堆栈指针移动到所需的数量。

使用new需要所有内存管理开销。

然后问题变成了 - 您是否希望为数据使用堆栈空间或堆空间。像'p'这样的堆栈(或局部)变量不需要解除引用,而使用new会增加一个间接层。

答案 6 :(得分:11)

一个好的一般经验法则是永远不要使用新的,除非你绝对必须。如果您不使用新程序,您的程序将更易于维护且不易出错,因为您不必担心清理它的位置。

答案 7 :(得分:10)

是的,起初有意义,来自Java或C#背景。要记住释放你分配的内存似乎没什么大不了的。但是当你第一次发现内存泄漏时,你会感到头疼,因为你很快就释放了所有东西。然后第二次发生,第三次你会更加沮丧。最后,由于内存问题导致六个月的头痛问题,你将开始厌倦它并且堆栈分配的内存将开始变得越来越有吸引力。多么美好和干净 - 只需将它放在堆栈上就可以忘掉它。很快你就可以随时使用它了。

但是 - 这种体验无可替代。我的建议?现在就试试吧。你会看到。

答案 8 :(得分:6)

第一种情况并不总是堆栈分配。如果它是对象的一部分,它将被分配到对象所在的任何位置。例如:

class Rectangle {
    Pixel top_left;
    Pixel bottom_right;
}

Rectangle r1; // Pixel is allocated on the stack
Rectangle *r2 = new Rectangle(); // Pixel is allocated on the heap

堆栈变量的主要优点是:

  • 您可以使用RAII pattern来管理对象。一旦对象超出范围,就会调用析构函数。有点像C#中的“使用”模式,但是是自动的。
  • 不可能有空引用。
  • 您无需担心手动管理对象的内存。
  • 它导致更少的内存分配。内存分配,特别是小内存,在C ++中可能比Java慢。

创建对象后,堆上分配的对象与堆栈上分配的对象(或任何地方)之间没有性能差异。

但是,除非您使用指针,否则不能使用任何类型的多态性 - 该对象具有完全静态类型,这在编译时确定。

答案 9 :(得分:6)

我的直觉反应只是告诉你,这可能会导致严重的内存泄漏。在某些情况下,您可能使用指针可能会导致混淆谁应该负责删除它们。在你的例子之类的简单情况下,很容易看到你应该在何时何地调用delete,但是当你开始在类之间传递指针时,事情会变得更加困难。

我建议您查看提升smart pointers library for your pointers.

答案 10 :(得分:6)

不是新事物的最好理由是,当事情进入堆栈时,你可以进行非常确定的清理。在Pixel的情况下,这不是那么明显,但在说文件的情况下,这变得有利:

  {   // block of code that uses file
      File aFile("file.txt");
      ...
  }    // File destructor fires when file goes out of scope, closing the file
  aFile // can't access outside of scope (compiler error)

在新建文件的情况下,您必须记住删除它以获得相同的行为。在上面的例子中似乎是一个简单的问题。但是,请考虑更复杂的代码,例如将指针存储到数据结构中。如果将该数据结构传递给另一段代码怎么办?谁负责清理工作。谁会关闭你的所有文件?

当你没有新的东西时,当变量超出范围时,析构函数就会清理资源。因此,您可以更有信心成功清理资源。

这个概念被称为RAII - 资源分配是初始化,它可以极大地提高您处理资源获取和处置的能力。

答案 11 :(得分:4)

对象生命周期。如果希望对象的生命周期超过当前作用域的生命周期,则必须使用堆。

另一方面,如果您不需要超出当前范围的变量,请在堆栈上声明它。当它超出范围时会自动销毁。小心地传递它的地址。

答案 12 :(得分:4)

我会说很多关于品味的问题。如果创建一个允许方法取指针而不是引用的接口,则允许调用者传入nil。由于您允许用户传入nil,因此用户传递为nil。

因为你必须问自己“如果这个参数是nil会发生什么?”,你必须更加防御性地编码,一直处理空检查。这说明了使用参考文献。

然而,有时你真的希望能够传递nil然后引用是不可能的:)指针给你更大的灵活性,让你更懒,这是非常好的。在知道你必须分配之前永远不要分配!

答案 13 :(得分:4)

问题不是指针本身(除了引入NULL指针),而是手动进行内存管理。

当然,有趣的是,我见过的每个Java教程都提到垃圾收集器是如此酷酷,因为你不必记得调用delete,实际上C ++只需要当您拨打delete时拨打newdelete[]时拨打new[]

答案 14 :(得分:2)

只有当你必须使用指针和动态分配的对象时。尽可能使用静态分配的(全局或堆栈)对象。

  • 静态对象更快(没有新的/删除,没有间接访问它们)
  • 无需担心对象生命周期
  • 更少的击键更具可读性
  • 更强大。每个“ - &gt;”是对NIL或无效内存的潜在访问

在此上下文中,通过'static'来澄清,我的意思是非动态分配。 IOW,任何不在堆上的东西。是的,他们也可能有对象生命周期问题 - 就单身破坏顺序而言 - 但将它们粘在堆上通常不能解决任何问题。

答案 15 :(得分:2)

为什么不把指针用于一切?

他们的速度较慢。

编译器优化对于指针访问语法不会那么有效,你可以在任意数量的网站上阅读它,但这里是一个不错的pdf from Intel.

检查页面,13,14,17,28,32,36;

  

检测不必要的内存   循环符号中的引用:

for (i = j + 1; i <= *n; ++i) { 
X(i) -= temp * AP(k); } 
  

循环边界的表示法   包含指针或内存   参考。编译器没有   任何预测价值的手段   指针n引用的是   一些人用循环迭代改变了   其他任务。这使用循环   重新加载n引用的值   对于每次迭代。代码生成器   引擎也可能拒绝安排   潜在的软件流水线循环   找到指针别名。自从   指针n引用的值不是   循环中的老化,它是   对循环索引不变,   加载* n s进行   在循环边界之外   更简单的调度和指针   消歧。

......这个主题有很多变化......

  

复杂的内存引用。或者在其他方面   单词,分析参考文献如   复杂的指针计算,应变   编译器生成的能力   高效的代码。代码中的位置   编译器或硬件所在的位置   执行复杂的计算   为了确定数据的位置   居住,应该是重点   注意。指针别名和代码   简化帮助编译器   识别内存访问模式,   允许编译器重叠   数据操作的内存访问。   减少不必要的内存引用   可能会暴露给编译器   管道软件的能力。许多   其他数据位置属性等   作为别名或对齐,可以   如果内存参考容易识别   计算保持简单。用于   强度降低或归纳   简化内存引用的方法   对于协助编译器至关重要。

答案 16 :(得分:1)

从不同角度看问题......

在C ++中,您可以使用指针(Foo *)和引用(Foo &)来引用对象。只要有可能,我使用引用而不是指针。例如,当通过引用函数/方法传递时,使用引用允许代码(希望)做出以下假设:

  • 引用的对象不归功能/方法所有,因此不应该delete该对象。这就像说,“在这里,使用这些数据,但在完成后将其还原”。
  • NULL指针引用的可能性较小。可以传递NULL引用,但至少它不是函数/方法的错误。无法将引用重新分配给新的指针地址,因此您的代码不会意外地将其重新分配给NULL或其他一些无效的指针地址,从而导致页面错误。

答案 17 :(得分:1)

问题是:为什么你会使用指针来做所有事情?堆栈分配的对象不仅更安全,更快速,而且输入更少,代码看起来更好。

答案 18 :(得分:0)

我未提及的一些内容是增加了内存使用量。假设有4个字节的整数和指针

Pixel p;

将使用8个字节,

Pixel* p = new Pixel();

将使用12个字节,增加50%。在分配足够的512x512图像之前,它听起来不是很多。然后你说2MB而不是3MB。这忽略了管理堆上的所有这些对象的开销。

答案 19 :(得分:0)

在堆栈上创建的对象创建得比分配的对象更快。

为什么?

因为分配内存(使用默认内存管理器)需要一些时间(找到一些空块或甚至分配该块)。

此外,您没有内存管理问题,因为当超出范围时,堆栈对象会自动销毁。

当您不使用指针时,代码更简单。如果您的设计允许您使用堆栈对象,我建议您这样做。

我自己不会使用智能指针使问题复杂化。

OTOH我在嵌入式领域做了一点工作,在堆栈上创建对象并不是很聪明(因为为每个任务/线程分配的堆栈不是很大 - 你必须小心)。

所以这是一个选择和限制的问题,没有任何回应适合所有人。

并且,尽可能不要忘记keep it simple

答案 20 :(得分:0)

基本上,当你使用原始指针时,你没有RAII。

答案 21 :(得分:0)

当我是一名新的C ++程序员时,这让我很困惑(这是我的第一语言)。有很多非常糟糕的C ++教程通常看起来分为两类:“C / C ++”教程,这实际上意味着它是一个C教程(可能有类),而C ++教程认为C ++是带有删除的Java

我认为在我的代码中的任何地方输入“new”花了大约1 - 1。5年(至少)。我经常使用像矢量这样的STL容器,它为我照顾。

我认为很多答案似乎要么忽略,要么就是避免直接说出如何避免这种情况。通常,您不需要在构造函数中使用new进行分配,并在析构函数中使用delete进行清理。相反,您可以直接将对象本身粘贴到类中(而不是指向它的指针),并在构造函数中初始化对象本身。然后,默认构造函数会在大多数情况下执行您需要的所有操作。

对于几乎任何不起作用的情况(例如,如果你冒着堆栈空间不足的情况),你应该使用标准容器之一:std :: string,std :: vector,and std :: map是我经常使用的三个,但是std :: deque和std :: list也很常见。其他(像std :: set和非标准rope之类的东西)使用不多,但表现相似。它们都是从免费商店分配的(C ++用于其他语言中的“堆”),请参阅:C++ STL question: allocators

答案 22 :(得分:-2)

除非将更多成员添加到Pixel类,否则第一种情况最好。 随着越来越多的成员被添加,存在堆栈溢出异常的可能性