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

76

假设我定义了一个类:

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个回答

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模式管理对象。一旦对象超出范围,其析构函数就会被调用。类似于C#中的“using”模式,但是自动进行。
  • 不存在空引用的可能性。
  • 无需担心手动管理对象的内存。
  • 它会导致较少的内存分配。在C++中,特别是小型内存分配,内存分配可能比Java更慢。

一旦对象被创建,堆分配的对象和栈(或其他地方)分配的对象之间没有性能差异。

然而,除非使用指针,否则不能使用任何形式的多态性——对象具有完全静态类型,由编译时确定。


4

我认为这更多是关于品味的问题。如果你创建了一个接口,允许方法接受指针而不是引用,那么就允许调用者传递nil。由于允许用户传递nil,用户将会传递nil。

既然你必须问自己“如果这个参数是nil会发生什么?”,你就必须更加谨慎地编写防御性代码,一直进行空指针检查。这就支持使用引用。

然而,有时你确实希望能够传递nil,这时引用就行不通了 :) 指针可以给你更大的灵活性,让你变得更加懒惰,这真的很好。在你知道必须要分配之前,永远不要进行分配!


他所指的不是函数参数,而是关于事物分配的位置(堆栈)。他指出Java将所有对象都放在堆上(我听说现代版本中有一些巧妙的技巧可以自动将某些对象放在堆栈上)。 - Evan Teran
我认为您正在回答一个关于指针与引用的不同问题,而不是关于 OP 关于基于堆栈或基于堆的对象的问题。 - saw-lau

4

对象生命周期。当您希望对象的生命周期超过当前范围的生命周期时,必须使用堆。

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


4

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

当然,有趣的部分是,我看过的每个Java教程都提到垃圾回收器非常酷,因为你不需要记得调用delete,但实际上,在C++中只有在调用new时才需要delete(并且在调用new[]时需要delete[])。


2

为什么不能用指针来做所有的事情?

它们更慢。

使用指针访问语义时,编译器优化的效果不会像其他情况那样有效,您可以在任何网站上了解有关此问题的信息,但是这里有一份不错的英特尔的pdf文档

请查看13、14、17、28、32、36页;

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

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

循环边界的表示法包含指针或内存引用。编译器无法预测指针n所引用的值是否会因迭代循环中的某些其他赋值而发生更改。此时,使用循环来重新加载n所引用的值以供每次迭代使用。当发现潜在的指针别名时,代码生成器也可能拒绝调度软件流水线循环。由于指针n所引用的值在循环内不会改变且对于循环索引是不变的,因此将*n的加载移到循环边界之外可以简化调度和指针消歧。
... 这个主题有许多变化 ...
复杂的内存引用,或者说分析如复杂指针计算等引用,会影响编译器生成高效代码的能力。在代码中,编译器或硬件执行复杂计算以确定数据位置的地方应该受到关注。指针别名和代码简化有助于编译器识别内存访问模式,允许编译器将内存访问与数据操作重叠。减少不必要的内存引用可能会使编译器能够管道化软件。如果内存引用计算保持简单,则许多其他数据位置属性(例如别名或对齐)可以轻松识别。使用强度降低或归纳方法简化内存引用对于帮助编译器至关重要。

链接走丢了。 :-( - derM

2
仅在必要时使用指针和动态分配的对象。尽可能使用静态分配(全局或堆栈)的对象。
  • 静态对象更快(无需new/delete,无需间接访问)
  • 没有需要担心的对象生命周期
  • 更少的按键次数,更易读
  • 更加健壮。每个“->”都是对NIL或无效内存的潜在访问

为了澄清,在这个上下文中,“静态”指非动态分配。也就是说,任何不在堆上的东西。是的,它们也可能有对象生命周期问题-就单例销毁顺序而言-但把它们放在堆上通常解决不了任何问题。


我不能说我喜欢“静态”的建议。首先,它并没有解决问题(因为静态对象无法在运行时分配),其次,它们自己也有很多问题(例如线程安全)。话虽如此,我没有给你打负分。 - jalf
你还应该注意到,静态变量存在启动和停止生命周期的问题(可以搜索“静态初始化顺序混乱”)。话虽如此,我也没有给你打负分。所以请不要对我做任何事情! :) - Johannes Schaub - litb
1
@Roddy - 你是不是指“自动”(栈分配)而不是“静态”?(我也没有给你-1。) - Fred Larson
@jalf- 或许“static”不是最合适的词。你是在考虑多线程下单例构造锁定的问题吗? - Roddy
我在考虑所有使用“static”关键字声明的变量。如果这不是你的意思,你应该避免使用那个词。 就像Fred所说,栈上的对象具有“自动”存储类别。如果这是你的意思,你的答案就更有意义了。 - jalf

1

当我是一个新的C++程序员(而且它是我的第一门语言)时,这让我非常困惑。有很多非常糟糕的C++教程,通常似乎可以分为两类:"C/C++"教程,实际上意味着这是一个C教程(可能带有类),以及认为C++是带有delete的Java的C++教程。

我想我花了大约1-1.5年(至少)在我的代码中任何地方输入"new"。我经常使用STL容器比如vector, 这为我处理了这个问题。

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

对于几乎任何这种情况都行不通的情况(例如,如果您冒着堆栈空间不足的风险),您应该使用其中一个标准容器:std::string、std::vector 和 std::map 是我经常使用的三个容器,但 std::deque 和 std::list 也很常见。其他容器(例如 std::set 和非标准 rope)使用较少,但行为类似。它们都从自由存储区(C++ 中某些其他语言中的“堆”)分配内存,请参见:C++ STL question: allocators


1

从不同的角度看这个问题...

C++ 中可以使用指针 (Foo *) 和引用 (Foo &) 来引用对象。在可能的情况下,我使用引用而不是指针。比如,在将引用传递给函数/方法时,使用引用允许代码(有望)做出以下假设:

  • 被引用的对象不是函数/方法所拥有的,因此不应该 delete 对象。就像说,“这里,用这个数据,但在完成后还回来。”
  • NULL 指针引用的可能性较小。虽然可能会被传递一个 NULL 引用,但至少不会是函数/方法的错误。引用不能被重新分配到新的指针地址,因此您的代码不可能意外将其重新分配为 NULL 或其他无效的指针地址,导致页面错误。

1
问题是:为什么你要把所有东西都用指针?栈分配的对象不仅更安全、更快速地创建,而且打字量更少,代码看起来更好。

0

我没有看到提到的是增加的内存使用量。假设4字节整数和指针。

Pixel p;

将使用8个字节,和

Pixel* p = new Pixel();

将使用12个字节,增加了50%。 这听起来不像很多,直到您为512x512图像分配足够的空间。 然后,您需要的是2MB而不是3MB。 这忽略了管理堆上所有这些对象的开销。


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