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

Eri*_*ric 75 c++ heap stack pointers

假设我定义了一些类:

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}
Run Code Online (Sandbox Code Playgroud)

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

Pixel p;
p.x = 2;
p.y = 5;
Run Code Online (Sandbox Code Playgroud)

来自Java世界我总是写:

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;
Run Code Online (Sandbox Code Playgroud)

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

jal*_*alf 187

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

  • 首先,显而易见且不太重要的一个:堆分配很慢.堆栈分配很快.
  • 其次,更重要的是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;
}
Run Code Online (Sandbox Code Playgroud)

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

答案是"可能的".bar抛出异常会发生什么?delete永远不会被调用,像素永远不会被删除,我们会泄漏内存.现在考虑一下:

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

  bar();
}
Run Code Online (Sandbox Code Playgroud)

这不会泄漏内存.当然,在这个简单的情况下,所有东西都在堆栈中,因此它会自动清理,但即使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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • @Milan:面对例外我会说100可能比1000更接近. (10认同)
  • +1.虽然,1leak/100loc太多了.可能每1000行代码1个. (5认同)
  • 是的,你可能能够写出前500行没有泄漏.然后再添加100行,其中包含6种不同的泄漏相同数据的方法,所有这些都在同一个函数中.当然,我没有测量过这个,但听起来不错.:) (4认同)
  • @Matt:哦,真的吗?如果不使用异常,则无需担心内存管理问题?这对我来说是新闻.我想大量的C程序员都希望他们也知道这一点.我相信很多用C编写的大型软件项目如果只知道这个小小的智慧就可以大大简化:只要没有例外,就没有必要管理你的记忆. (3认同)
  • @Matt:我不是.我故意解释他们.没有"错误".看看你在我的所有答案中留下的一系列评论,很清楚它们的价值是多少.无论如何,我在帖子中看不到任何"迷恋样板".我也没有看到任何旨在保护功能的东西.我看到一个非常简单的习惯用于编写非常简单的代码,使用起来非常简单.没有它,客户端代码将变得更加复杂和脆弱,并且类本身的实现可能会节省几行代码. (2认同)

Mar*_*ork 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;
}
Run Code Online (Sandbox Code Playgroud)

如果你选择了一些更有趣的东西,比如一个文件(这是一个需要关闭的资源).然后在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.
}
Run Code Online (Sandbox Code Playgroud)

C++中的相同代码

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

// Destructor automatically closes file and discards irrelevant exceptions.
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

例如:

 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.
Run Code Online (Sandbox Code Playgroud)

还有其他人.

  • 我喜欢将C++文件用法与Java进行比较(让我微笑). (9认同)
  • 同意.奖励积分因为它显示RAII用于管理其他类型的资源而不仅仅是内存分配. (2认同)

Cly*_*yde 25

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

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

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

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


rpg*_*rpg 24

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

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


Tim*_*Tim 14

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

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

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

这是我对这个问题首先要说的.


Ski*_*izz 11

代码:

Pixel p;
p.x = 2;
p.y = 5;
Run Code Online (Sandbox Code Playgroud)

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

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

那么问题就变成了 - 你想为你的数据使用堆栈空间或堆空间吗?像'p'这样的堆栈(或局部)变量不需要解除引用,而使用new会增加一个间接层.


Ste*_*eve 11

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


eee*_*aii 10

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

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

  • 你忘了提到它邪恶的双胞胎,双重释放.:)就在你认为你已经释放了所有内存的时候,你开始得到错误,因为你在释放它之后使用了内存,或者你试图释放已经被释放的内存. (6认同)

Eri*_*ric 6

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

我建议你查看boost 智能指针库.


Dou*_* T. 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)
Run Code Online (Sandbox Code Playgroud)

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

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

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


Bla*_*ura 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
Run Code Online (Sandbox Code Playgroud)

堆栈变量的主要优点是:

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

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

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