为什么不对 C + + 中的所有东西都使用指针呢?

假设我定义了一些类:

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;

他们基本上做同样的事情,对吗? 一个在堆栈上,另一个在堆上,所以稍后我将不得不删除它。这两者之间有什么根本的区别吗?我为什么要选择其中一个?

14369 次浏览

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

我建议研究一下增压 指针的智能指针库。

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

  • 更快
  • 我不用担心内存分配问题
  • P 将是整个当前范围的有效对象

不创建所有内容的最佳理由是,当内容在堆栈上时,您可以进行非常确定性的清理。对于 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 ——资源分配是初始化,它可以极大地提高您处理资源获取和处置的能力。

“为什么不对 C + + 中的所有东西都使用指针”

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

自动/堆栈对象可以删除其中一些繁忙的工作。

这只是我对这个问题的第一反应。

我得说这很大程度上取决于品味。如果您创建了一个接口,允许方法接受指针而不是引用,那么您就允许调用方传入 nil。因为允许用户传入 nil,所以用户 威尔传入 nil。

因为您必须问自己“如果这个参数为零会发生什么?”,您必须更加谨慎地编写代码,始终关注空检查。这说明要使用引用。

但是,有时候您真的希望能够传入 nil,然后引用就不可能了:)指针给予您更大的灵活性,并允许您变得更懒惰,这非常好。永远不要分配,直到知道你必须分配!

从逻辑上讲,他们做同样的事情——除了清理。只有您编写的示例代码在指针情况下有内存泄漏,因为该内存没有释放。

从 Java 背景来看,您可能还没有完全准备好 C + + 在多大程度上围绕着跟踪已分配的内容以及谁负责释放它。

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

显然,如果你非常小心的话,你总是可以在堆上手动分配,但是好的软件工程的一部分就是以这样的方式构建东西,它们不会被破坏,而不是相信你的超级人类程序员从来不会犯错误。

对象生存期。如果希望对象的生存期超过当前范围的生存期,则必须使用堆。

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

密码:

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

不会动态分配内存-不会搜索空闲内存,不会更新内存使用情况,什么都不会。完全免费。编译器在编译时为变量在堆栈上保留空间——它有很多空间可以保留,并创建一个操作码来移动堆栈指针所需的数量。

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

那么问题就变成了——您是想要为数据使用堆栈空间还是堆空间。堆栈(或局部)变量,如“ p”不需要解引用,而使用 new 会添加一个间接层。

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

  • 首先,一个显而易见但不那么重要的问题是: 堆分配很慢,堆分配很快。
  • 其次,更重要的是 拉尔。因为堆栈分配的版本是自动清理的,所以它是 很有用。它的析构函数是自动调用的,这允许您保证类分配的任何资源都被清理干净。这就是在 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 就永远不会被释放,如果像素被复制,我们最终会导致两个实例都试图删除同一个数据。最后一个例子还有待商榷。真实世界的代码稍微复杂一些,但是它显示了一般的思想)

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

一个很好的一般经验法则是,除非万不得已,否则永远不要使用 new。如果您不使用 new,那么您的程序将更容易维护,并且更少出错,因为您不必担心在哪里清理它。

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

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

为了澄清,在这个上下文中的“静态”,我指的是非动态分配。哦,任何不在堆里的东西。是的,它们也可能存在对象生存期问题——就单例破坏顺序而言——但是把它们放在堆上通常不能解决任何问题。

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

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 慢。

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

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

是的,起初这是有道理的,来自 Java 或 C # 背景。看起来释放你分配的内存并不是什么大不了的事情。但是当你第一次出现内存泄漏时,你会抓狂,因为你发誓你释放了所有内存。然后第二次发生,第三次你会变得更加沮丧。最后,在因为内存问题而头疼了六个月之后,你会开始对它感到厌倦,堆栈分配的内存将开始变得越来越有吸引力。多么干净利落啊,把它放在堆栈上,然后忘掉它。很快你就可以随时使用堆栈了。

但是,这种经历是无可替代的。我的建议?试试你的方法,暂时的。你会明白的。

在添加删除之前,它们是不一样的。
您的示例过于琐碎,但析构函数实际上可能包含执行一些实际工作的代码。这被称为 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 的主要区别在于:

谁拥有指针的概念。所有者有责任在适当的时候删除对象。这就是为什么在实际的程序中很少看到类似的 生的指针(因为没有与 生的指针相关的所有权信息)。相反,指针通常包装在智能指针中。智能指针定义了谁拥有内存以及谁负责清理内存的语义。

例如:

 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.

还有其他人。

从另一个角度来看这个问题..。

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

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

问题是: 为什么所有事情都要使用指针?堆栈分配的对象不仅创建起来更安全、更快捷,而且键入更少,代码看起来也更好。

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

Pixel p;

将使用8字节,并且

Pixel* p = new Pixel();

将使用12字节,增加50% 。这听起来并不多,直到你分配足够的512x512图像。那么你说的是2MB 而不是3MB。这忽略了管理堆上包含所有这些对象的堆的开销。

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

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

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

为什么?

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

另外,由于堆栈对象在超出作用域时会自动销毁自己,因此不会出现内存管理问题。

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

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

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

因此,这是一个选择和限制的问题,没有适合它们所有的反应。

而且,一如既往不要忘记 简单点,尽可能多。

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

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

为什么不对所有事情都使用指针呢?

他们更慢。

编译器优化不会像指针访问语义那样有效,你可以在任何数量的网站上阅读它,但这里有一个不错的 来自英特尔的 pdf。

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

检测不必要的记忆 references in the loop notation:

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

循环边界的符号 包含指针或内存 编译器没有 任何方法来预测 指针 n 所引用的 通过循环迭代改变了一些 使用循环 重新加载 n 引用的值 代码生成器 引擎也可能拒绝调度 电位时软件流水线回路 找到指针别名 指针 n 引用的值不是 在循环中变化 对循环索引不变,则 装载要运载的 * n 在循环边界之外 更简单的调度和指针 消除歧义。

... 这个主题的许多变化... 。

复杂的内存引用 分析参考文献,例如 复杂的指针计算、应变 编译器生成 有效的代码。在代码中的位置 编译器或硬件在哪里 进行复杂的计算 命令,以确定数据的位置 居住,应该是重点 注意。指针别名和代码 简化有助于编译器 识别内存访问模式, 允许编译器重叠 具有数据操作的内存访问。 减少不必要的内存引用 可以向编译器公开 软件管道的能力。许多 其他数据位置属性,例如 作为别名或对齐,可以是 容易识别的内存引用 计算过程保持简单 强度降低或感应 简化内存引用的方法 是协助编译器的关键。

当我还是一个新的 C + + 程序员(这是我的第一语言)时,这让我很困惑。有很多非常糟糕的 C + + 教程,通常似乎属于两个类别之一: “ C/C + +”教程,这实际上意味着它是一个 C 教程(可能有类) ,和 C + + 教程认为 C + + 是 Java 与删除。

我想我花了大约1-1.5年(至少)在我的代码中输入“ new”。我经常使用像 Vector 这样的 STL 容器,它为我解决了这个问题。

我认为很多答案似乎要么忽略,要么只是避免直接说如何避免这一点。通常不需要在构造函数中使用 new 进行分配,在析构函数中使用 delete 进行清理。相反,您可以直接将对象本身固定在类中(而不是指向它的指针) ,并在构造函数中初始化对象本身。那么在大多数情况下,缺省构造函数会做你需要的一切。

对于几乎所有这种方法不起作用的情况(例如,如果您冒着堆栈空间用完的风险) ,您可能无论如何都应该使用标准容器之一: std: : string、 std: : Vector 和 std: : map 是我最常使用的三种,但 std: : deque 和 std: : list 也非常常见。其他的(比如 std: : set 和非标准的 绳子)使用的不多,但是行为相似。它们都是从免费存储中分配的(在其他一些语言中,C + + 表示“堆”) ,请参见: C + + STL 问题: 分配器