为什么不在C ++中使用指针?
假设我定义了一些类:
class Pixel { public: Pixel(){ x=0; y=0;}; int x; int y; }
然后使用它编写一些代码。 我为什么要做下面的事情?
Pixel p; px = 2; py = 5;
来自Java世界,我总是写:
Pixel* p = new Pixel(); p->x = 2; p->y = 5;
他们基本上做同样的事情,对吧? 一个在栈上,另一个在堆上,所以我将不得不删除它。 两者有什么根本的区别? 为什么我应该比另一个更喜欢?
是的,一个在栈上,另一个在堆上。 有两个重要的区别:
- 首先,显而易见的并不重要的一点是堆分配缓慢。 堆栈分配很快。
- 其次,更重要的是RAII 。 因为堆栈分配的版本会自动清理,所以它很有用 。 它的析构函数会自动调用,这样可以保证清除类中分配的任何资源。 这是你如何避免C ++中的内存泄漏的根本原因。 你可以通过不要自己调用
delete
来避免它们,而是把它封装在堆栈分配的对象中,这些对象在内部调用delete
,在析构函数中是典型的。 如果您尝试手动跟踪所有分配,并在正确的时间调用delete
,那么我保证每100行代码至less会有一次内存泄漏。
作为一个小例子,考虑这个代码:
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; }
漂亮的代码,对吧? 我们创build一个像素,然后我们调用一些不相关的函数,然后删除像素。 有没有内存泄漏?
答案是“可能”。 如果bar
抛出一个exception会发生什么? delete
永远不会被调用,像素永远不会被删除,我们会泄漏内存。 现在考虑一下:
void foo() { Pixel p; px = 2; py = 5; bar(); }
这不会泄漏内存。 当然,在这种简单的情况下,所有东西都在堆栈中,所以它会自动清理,但即使Pixel
类在内部进行了dynamic分配,也不会泄漏。 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; *px = 2; *py = 5; bar(); }
Pixel类现在在内部分配一些堆内存,但是它的析构函数负责清理它,所以在使用类时,我们不必担心。 (我应该提一下,这里最后一个例子被简化了很多,为了显示一般的原则,如果我们真的要使用这个类,它也包含了几个可能的错误,如果y的分配失败,x永远不会被释放,如果像素被复制,我们最终会试图删除相同的数据,所以最后一个例子就是用一些盐来代替,现实世界的代码有点复杂,但是它显示了一般的想法)
当然,相同的技术可以扩展到除内存分配之外的其他资源。 例如,它可以用来保证在使用后closures文件或数据库连接,或者释放线程代码的同步锁。
直到你添加删除它们是不一样的。
你的例子是微不足道的,但是析构函数实际上可能包含一些真正的工作的代码。 这被称为RAII。
所以添加删除。 确保即使exception正在传播时也会发生。
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; }
如果你select了一个更有趣的文件(这是一个需要closures的资源)。 然后在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.
虽然人们提到速度(因为在堆上查找/分配内存)。 就个人而言,这不是一个决定性的因素(分配器非常快速,并已针对不断创build/销毁的小对象的C ++使用进行了优化)。
我的主要原因是对象的生命时间。 一个本地定义的对象有一个非常具体和定义良好的生命周期,并保证析构函数在最后被调用(因此可以有特定的副作用)。 另一方面,指针控制具有dynamic寿命的资源。
C ++和Java的主要区别是:
谁拥有指针的概念。 所有者有责任在适当的时候删除该对象。 这就是为什么在真正的程序中很less看到这样的原始指针(因为没有与原始指针相关的所有权信息)。 而指针通常包含在智能指针中。 智能指针定义谁拥有内存的语义,从而谁负责清理它。
例子是:
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.
还有其他的。
从逻辑上讲,他们做同样的事情 – 除了清理。 只是你写的示例代码在指针的情况下有一个内存泄漏,因为这个内存没有被释放。
从Java的背景来看,你可能没有完全准备好C ++究竟有多less是围绕着分配什么东西以及谁来负责释放它。
通过适当的使用堆栈variables,您不必担心释放该variables,堆栈框架就会消失。
显然,如果你非常小心,你总是可以在堆上分配,而且可以手动释放,但是好的软件工程的一部分就是要build立一些不会破坏的东西,而不是相信你的超人类程序员 – 福永不犯错。
我比较喜欢用第一种方法,因为:
- 速度更快
- 我不必担心内存释放
- p将成为整个当前范围的有效对象
“为什么不在C ++中使用指针”
一个简单的答案 – 因为它成为一个pipe理内存的巨大问题 – 分配和删除/释放。
自动/堆栈对象删除了一些繁忙的工作。
这只是我要说的第一件事情。
一个好的一般的经验法则是永远不要使用新的,除非你绝对必须。 如果您不使用新的程序,您的程序将更容易维护,并且不易出错,因为您不必担心在何处进行清理。
代码:
Pixel p; px = 2; py = 5;
没有内存的dynamic分配 – 没有空闲内存的search,没有内存使用的更新,什么都没有。 这是完全免费的。 编译器在编译时为编译器预留了variables的空间 – 这样做有足够的空间来保存,并创build一个操作码来将堆栈指针移动到所需的数量。
使用new需要所有内存pipe理开销。
问题就变成了 – 你想为你的数据使用堆栈空间还是堆空间。 像“p”这样的堆栈(或本地)variables不需要解引用,而使用新的就会添加一个间接层。
是的,起初是有道理的,来自Java或C#背景。 要记住释放你分配的内存似乎不是什么大事。 但是当你第一次泄漏内存的时候,你会抓紧你的脑袋,因为你把你所有的东西都解放了出来。 然后第二次发生,第三次你会更加沮丧。 最后,由于内存问题六个月的头痛,你会开始厌倦它,堆栈分配的内存将开始看起来越来越有吸引力。 多么好,干净 – 只要把它放在堆栈上,忘掉它。 很快你就可以随时使用堆栈了。
但是 – 这个经验是无可替代的。 我的build议? 现在就试试吧。 你会看到的。
我的直觉反应只是告诉你,这可能会导致严重的内存泄漏。 在某些情况下,您可能正在使用指针可能会导致谁应该负责删除它们的混淆。 在简单情况下,比如你的例子,很容易看到什么时候你应该调用delete,但是当你开始在类之间传递指针时,事情会变得更加困难。
我build议寻find你的指针提升智能指针库。
不要新的东西的最好的理由是,当事情在堆栈上时,你可以非常确定的清理。 在像素的情况下这不是那么明显,但是在说文件的情况下,这变得有利:
{ // 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)
在新build一个文件的情况下,你将不得不记得删除它以获得相同的行为。 在上述情况下似乎是一个简单的问题。 然而考虑更复杂的代码,例如将指针存储到数据结构中。 如果你把这个数据结构传递给另一个代码呢? 谁负责清理。 谁会closures你所有的文件?
当你没有新的东西时,当variables超出范围时,资源只会被析构函数清除。 所以你可以有更大的信心,资源成功清理。
这个概念被称为RAII – 资源分配是初始化,它可以大大提高你处理资源获取和处置的能力。
第一种情况不总是分配堆栈。 如果它是一个对象的一部分,它将被分配到对象的任何地方。 例如:
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
堆栈variables的主要优点是:
- 您可以使用RAII模式来pipe理对象。 一旦对象超出范围,就会调用析构函数。 有点像C#中的“使用”模式,但自动。
- 没有空引用的可能性。
- 您不必担心手动pipe理对象的内存。
- 它会导致更less的内存分配。 内存分配,尤其是小内存分配,在C ++中可能比Java慢。
一旦对象被创build,在堆上分配的对象和在堆栈上分配的对象(或任何地方)之间没有性能差异。
但是,除非使用指针,否则不能使用任何forms的多态性 – 对象具有完全静态types,这是在编译时确定的。
对象生命周期 如果希望对象的生命周期超出当前作用域的生命周期,则必须使用堆。
另一方面,如果您不需要超出当前范围的variables,请在堆栈中声明它。 它会在超出范围时自动被销毁。 只要小心通过它的地址。
我会说这是一个关于品味的问题。 如果你创build一个接口允许方法取指针而不是引用,那么你允许调用者传入nil。 既然你允许用户通过零,用户将通过零。
既然你必须问自己“如果这个参数是零,会发生什么?”,你必须更加防守地编码,一直照顾空值检查。 这说明使用引用。
然而,有时你真的想能够通过零,然后参考是不可能的:)指针给你更大的灵活性,让你更懒,这是非常好的。 永远不要分配,直到知道你必须分配!
这个问题本身不是指针(除了引入NULL
指针外),而是手动进行内存pipe理。
有趣的是,我看过的每个Java教程都提到垃圾收集器是如此的酷热,因为你不必记得调用delete
,实际上C ++只需要在你调用new
时在你调用new[]
时delete[]
new[]
)。
只有在您必须使用指针和dynamic分配的对象。 尽可能使用静态分配(全局或堆栈)对象。
- 静态对象更快(没有新的/删除,没有间接访问它们)
- 没有对象的一生担心
- 更less的按键更易读
- 更强大。 每个“ – >”都有可能访问NIL或无效内存
为了澄清,在这个上下文中的“静态”,我的意思是非dynamic分配。 IOW,任何不在堆上的东西。 是的,他们也可以有对象生命期的问题 – 就单例破坏顺序而言 – 但是把它们粘在堆上通常不能解决任何问题。
为什么不使用指针的一切?
他们慢一点
编译器优化不会像指针访问语义一样有效,您可以在任意数量的网站上阅读它,但是这里有一个像样的英特尔pdf。
检查页面,13,14,17,28,32,36;
在循环表示法中检测不必要的内存引用:
for (i = j + 1; i <= *n; ++i) { X(i) -= temp * AP(k); }
循环边界的符号包含指针或内存引用。 编译器没有任何方法来预测指针n所引用的值是否被循环迭代改变了一些其他赋值。 这使用循环来为每个迭代重新加载由n引用的值。 当发现潜在的指针混叠时,代码生成器引擎也可以拒绝安排软件stream水线循环。 由于指针n所引用的值在循环内不是老化,并且对于循环索引是不变的,因此为了更简单的调度和指针消除歧义,将* ns加载到循环边界之外。
…这个主题的一些变化….
复杂的内存引用。 换句话说,分析诸如复杂的指针计算之类的引用会使编译器生成高效的代码的能力变得沉重。 编译器或硬件执行复杂计算以确定数据所在位置的代码位置应该是关注的焦点。 指针别名和代码简化有助于编译器识别内存访问模式,允许编译器通过数据操作来重叠内存访问。 减less不必要的内存引用可能会给编译器提供pipe道化软件的能力。 如果内存参考计算保持简单,许多其他数据位置属性(例如混叠或alignment)可以轻松识别。 使用强度降低或归纳方法来简化内存引用对于协助编译器至关重要。
从另一个angular度来看问题
在C ++中,您可以使用指针( Foo *
)和引用( Foo &
)来引用对象。 只要有可能,我使用引用而不是指针。 例如,当通过引用一个函数/方法传递时,使用引用允许代码(希望)做出以下假设:
- 引用的对象不属于函数/方法,因此不应
delete
该对象。 就好像在说:“在这里,使用这些数据,但是完成后就把它还给你”。 - NULL指针引用的可能性较小。 有可能传递一个NULL引用,但至less它不会是函数/方法的错误。 一个引用不能被重新分配到一个新的指针地址,所以你的代码不会被意外地重新分配给NULL或其他一些无效的指针地址,从而导致页面错误。
问题是:为什么你会使用指针的一切? 堆栈分配的对象不仅更安全,创build速度更快,而且键入更less,代码更好。
我没见过的东西是增加的内存使用量。 假设4个字节的整数和指针
Pixel p;
将使用8个字节,并且
Pixel* p = new Pixel();
将使用12个字节,增加50%。 直到您为512×512图片分配足够的空间,听起来不是很多。 那你说的是2MB而不是3MB。 这是忽略了pipe理堆上的所有这些对象的开销。
在堆栈上创build的对象的创build速度比分配的对象快。
为什么?
因为分配内存(使用默认的内存pipe理器)需要一些时间(find一些空的块,甚至分配该块)。
你也没有内存pipe理问题,因为堆栈对象在超出范围时会自动销毁。
当你不使用指针时,代码更简单。 如果你的devise允许你使用堆栈对象,我build议你这样做。
我自己不会使用智能指针使问题复杂化。
OTOH我在embedded式领域做了一些工作,在堆栈上创build对象不是很聪明(因为为每个任务/线程分配的堆栈不是很大 – 你必须小心)。
所以这是一个select和限制的问题,没有反应,以适应他们所有。
而且,一如既往不要忘记保持简单 ,尽可能多。
基本上,当你使用原始指针时,你没有RAII。
当我是一个新的C ++程序员(这是我的第一语言)时,这使我困惑不已。 有很多非常糟糕的C ++教程,通常看起来分为两类:“C / C ++”教程,这实际上意味着它是一个C教程(可能带有类),而C ++教程认为C ++是带有删除的Java 。
我想我花了大约1 – 1。5年(至less)在代码中的任何地方input“新”。 我经常使用像vector的STL容器,为我照顾这个。
我想很多答案似乎都忽略了,或者只是避免直接说出如何避免这个问题。 您通常不需要在构造函数中使用new进行分配,并在析构函数中使用delete进行清理。 相反,您可以直接将对象本身粘贴到类中(而不是指向它的指针),并在构造函数中初始化对象本身。 那么在大多数情况下,默认的构造函数会完成所有你需要
对于几乎任何情况下,这将无法正常工作(例如,如果你冒着堆栈空间的风险),你可能应该使用一个标准的容器无论如何:std :: string,std :: vector和std :: map是我经常使用的三种,但是std :: deque和std :: list也是很常见的。 其他(比如std :: set和非标准绳索 )的使用并不多,但是行为相似。 他们都从免费商店(C ++的一些其他语言的“堆”的说法) 分配 ,请参阅: C + +的STL问题:分配器
第一种情况是最好的,除非更多的成员被添加到像素类。 随着越来越多的成员被添加,可能会出现堆栈溢出exception