为什么要使用指针而不是对象本身?


1598

我来自Java背景,已经开始使用C ++处理对象。但是我想到的一件事是,人们经常使用指向对象的指针,而不是对象本身,例如以下声明:

Object *myObject = new Object;

而不是:

Object myObject;

或者,而不是使用函数testFunc(),像这样说:

myObject.testFunc();

我们必须写:

myObject->testFunc();

但是我不知道为什么我们要这样做。我认为这与效率和速度有关,因为我们可以直接访问内存地址。我对吗?


403
感谢您质疑这种做法,而不仅仅是遵循它。大多数情况下,指针被过度使用。
Luchian Grigore 2014年

118
如果看不到使用指针的原因,请不要使用。喜欢对象。在原始指针之前,在unique_ptr之前,在shared_ptr之前,优先选择对象。
stefan 2014年

111
注意:在Java中,所有(基本类型除外)都是指针。所以您宁可问相反的问题:为什么我需要简单的对象?
Karoly Horvath'3

117
注意,在Java中,指针被语法隐藏。在C ++中,在代码中明确指出了指针与非指针之间的区别。Java到处都使用指针。
DanielMartín2014年

214
封闭得太广?认真吗 请大家注意,这种Java ++编程方式非常普遍,也是C ++社区中最重要的问题之一。应该认真对待。
Manu343726

Answers:


1570

不幸的是,您经常看到动态分配。这只表明有多少坏的C ++程序员。

从某种意义上说,您将两个问题捆绑在一起。首先是何时应该使用动态分配(使用new)?第二个是什么时候应该使用指针?

重要的提示是您应始终使用适当的工具进行作业。在几乎所有情况下,都比执行手动动态分配和/或使用原始指针更合适,更安全。

动态分配

在您的问题中,您演示了两种创建对象的方法。主要区别是对象的存储时间。在Object myObject;块内执行操作时,将创建具有自动存储持续时间的对象,这意味着当对象超出范围时将被自动销毁。执行此操作时new Object(),该对象具有动态存储持续时间,这意味着该对象将保持活动状态,直到您明确delete将其保留为止。您仅应在需要时使用动态存储期限。也就是说,您应该始终喜欢在可以的时候创建具有自动存储持续时间的对象

您可能需要动态分配的两种主要情况:

  1. 您需要使对象的寿命超出当前作用域 -该特定对象位于该特定内存位置,而不是其副本。如果您可以复制/移动对象(大多数情况下应该如此),则应该使用自动对象。
  2. 您需要分配大量内存,这很容易填满堆栈。如果我们不必为此担心(大多数时候您不必如此),那将是很好的,因为它确实超出了C ++的范围,但是不幸的是,我们必须处理系统的实际情况。我们正在开发。

当您确实需要动态分配时,应将其封装在智能指针或其他执行RAII的类型中(例如标准容器)。智能指针提供了动态分配对象的所有权语义。例如,查看std::unique_ptrstd::shared_ptr。如果正确使用它们,则几乎可以完全避免执行自己的内存管理(请参见零规则)。

指针

但是,除动态分配外,原始指针还有其他更通用的用法,但大多数都有您应首选的替代方法。和以前一样,除非您确实需要使用指针,否则请始终选择替代方法

  1. 您需要参考语义。有时您想使用指针传递对象(无论如何分配),因为您希望传递给它的函数可以访问该特定对象(而不是其副本)。但是,在大多数情况下,您应该更喜欢引用类型而不是指针,因为这是专门为它们设计的。请注意,这不一定是要延长对象的生存期到当前范围之外,如上述情况1所示。和以前一样,如果您可以传递对象的副本,则不需要引用语义。

  2. 您需要多态性。您只能通过指针或对对象的引用来多态调用函数(即,根据对象的动态类型)。如果这是您需要的行为,则需要使用指针或引用。同样,引用应该是首选。

  3. 您想nullptr通过在省略对象时传递一个来表示该对象是可选的。如果是参数,则应该首选使用默认参数或函数重载。否则,您最好使用一种封装此行为的类型,例如std::optional(在C ++ 17中引入-使用较早的C ++标准,请使用boost::optional)。

  4. 您希望解耦编译单元以缩短编译时间。指针的有用属性是您只需要指向类型的前向声明(要实际使用该对象,您需要一个定义)。这使您可以使编译过程的各个部分脱钩,从而可以大大缩短编译时间。请参阅“ Pimpl习惯用法”

  5. 您需要与C库或C样式库接口。在这一点上,您不得不使用原始指针。您可以做的最好的事情是确保仅在可能的最后时刻松开原始指针。您可以从智能指针获取原始指针,例如,通过使用其get成员函数。如果库为您执行了一些分配,希望您可以通过句柄对其进行分配,则通常可以使用自定义删除程序将句柄包装在智能指针中,该删除器将适当地分配对象。


82
“您需要该对象的寿命超出当前范围。” -关于这一点的附加说明:在某些情况下,您似乎需要该对象超出当前范围,但实际上并不需要。例如,如果将对象放入向量中,则该对象将被复制(或移动到)向量中,并且当其作用域结束时,可以安全地破坏原始对象。

25
请记住,现在在很多地方都使用s / copy / move /。返回对象绝对不意味着移动。您还应该注意,通过指针访问对象与创建对象的方式正交。
Puppy 2014年

15
我想念这个答案没有明确提到RAII。C ++(几乎全部)都与资源管理有关,RAII是在C ++上实现资源管理的方法(以及原始指针产生的主要问题:破坏RAII)
Manu343726 2014年

11
智能指针在C ++ 11之前存在,例如boost :: shared_ptr和boost :: scoped_ptr。其他项目有自己的等效项。您无法获得移动语义,并且std :: auto_ptr的分配存在缺陷,因此C ++ 11可以改善性能,但是建议仍然不错。(而且,不幸的是,仅访问C ++ 11编译器不够的,您可能希望所有代码都可以与C ++ 11一起使用的所有编译器都是必要的。是的,Oracle Solaris Studio,我是看着你。)
armb 2014年

7
@ MDMoore313您可以编写Object myObject(param1, etc...)
user000001 2014年

171

指针有很多用例。

多态行为。对于多态类型,使用指针(或引用)来避免切片:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

参考语义和避免复制。对于非多态类型,指针(或引用)将避免复制可能昂贵的对象

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

请注意,C ++ 11具有移动语义,可以避免将许多昂贵的对象复制到函数参数中并作为返回值。但是使用指针肯定会避免这些情况,并且将允许在同一对象上使用多个指针(而一个对象只能移动一次)。

资源获取。在现代C ++中,使用new运算符创建指向资源的指针是一种反模式。使用特殊的资源类(Standard容器之一)或智能指针std::unique_ptr<>std::shared_ptr<>)。考虑:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

原始指针应仅用作“视图”,而不能以任何方式涉及所有权,无论是通过直接创建还是通过返回值隐式使用。另请参阅C ++常见问题解答中的此问答

更细粒度的生命周期控制每次复制共享指针(例如,作为函数参数)时,它所指向的资源都会保持活动状态。new超出范围时,将销毁常规对象(不是由您直接创建的,不是由您直接创建的,还是由资源类内部创建的)。


17
“使用new运算符创建指向资源的指针是一种反模式”,我认为您甚至可以增强为让原始指针拥有某种东西是一种反模式。自unique_ptr/ move语义以来,不仅弃用了创建方法,而且不赞成将原始指针作为参数或返回值传递,这意味着所有权转移恕我直言
dyp 2014年

1
@dyp tnx,已更新并参考了有关此主题的C ++常见问题解答。
TemplateRex

4
在任何地方使用智能指针都是一种反模式。在某些特殊情况下,它是适用的,但是在大多数情况下,争夺动态分配(任意生存期)的相同原因也反对任何通常的智能指针。
James Kanze 2014年

2
@JamesKanze我并不是要暗示智能指针应该在所有地方使用,仅用于所有权,而且原始指针不应该用于所有权,而只能用于视图。
TemplateRex

2
@TemplateRex这似乎有点愚蠢,因为它hun(b)也需要了解签名,除非您可以在编译前不知道输入错误的类型就可以了。尽管通常不会在编译时发现引用问题,并且会花费更多的精力进行调试,但是如果您正在检查签名以确保参数正确无误,那么您还可以查看是否有任何参数是引用因此,参考位变成了没有问题的东西(尤其是在使用显示所选功能签名的IDE或文本编辑器时)。另外,const&
JAB

130

这个问题有很多出色的答案,包括前向声明,多态等重要用例,但是我感到您的问题“灵魂”的一部分没有得到答案-即Java和C ++中不同语法的含义。

让我们比较一下两种语言的情况:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

与此最接近的等效项是:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

让我们看看另一种C ++方式:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

想到它的最好方法是Java(隐式地)处理指向对象的指针,而C ++可能处理指向对象的指针或对象本身。对此有一些例外-例如,如果您声明Java“原始”类型,则它们是被复制的实际值,而不是指针。所以,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

也就是说,使用指针不一定是正确或错误的处理方式。但是,其他答案也令人满意。不过,一般的想法是,在C ++中,您对对象的生存期以及它们的生存位置拥有更多的控制权。

切记- Object * object = new Object()构造实际上是最接近典型Java(或C#)语义的构造。


7
Object2 is now "dead":我想你的意思myObject1或更确切的说是the object pointed to by myObject1
克莱门特

2
确实!改了一下。
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();是非常糟糕的代码。第二个new或第二个Object构造函数可能会抛出,并且现在object1已泄漏。如果使用raw new,则应尽快将newed对象包装在RAII包装器中。
PSkocik '02

8
确实,这可能是一个程序,而周围没有其他事情。值得庆幸的是,这只是一个说明片段,显示了C ++中的Pointer的行为-少数RAII对象无法代替原始指针的地方之一,正在研究和学习原始指针...
Gerasimos R

80

使用指针的另一个很好的理由是前向声明。在足够大的项目中,它们确实可以加快编译时间。


7
这确实增加了有用的信息,很高兴您能为您提供答案!
TemplateRex

3
std :: shared_ptr <T>也可以与T的前向声明一起使用。(std :: unique_ptr <T> 不能
比利时

13
@berkus:std::unique_ptr<T>确实与的正向声明一起使用T。您只需要确保在std::unique_ptr<T>调用的析构函数时T为完整类型即可。这通常意味着包含该std::unique_ptr<T>声明的类在头文件中声明其析构函数,并在cpp文件中实现它(即使实现为空)。
大卫·斯通

模块会解决这个问题吗?
Trevor Hickey

@TrevorHickey我知道老评论,但无论如何都要回答。模块不会删除依赖关系,但是应该使包含依赖关系的价格非常便宜,就性能成本而言几乎是免费的。另外,如果模块的总体加速足以使您的编译时间处于可接受的范围内,那么这也不再是问题。
艾迪阿卡皮(Aidiakapi)

78

前言

与炒作相反,Java与C ++完全不同。Java炒作机希望您相信,因为Java具有类似于C ++的语法,所以这些语言是相似的。没有什么比事实更遥远了。这种错误信息是Java程序员使用C ++并使用类Java语法而不了解其代码含义的部分原因。

往前走

但是我不知道为什么我们要这样做。我认为这与效率和速度有关,因为我们可以直接访问内存地址。我对吗?

相反,实际上。比堆栈得多,因为与堆相比,堆非常简单。一旦超出范围,自动存储变量(即堆栈变量)就会调用其析构函数。例如:

{
    std::string s;
}
// s is destroyed here

另一方面,如果使用动态分配的指针,则必须手动调用其析构函数。delete为您调用此析构函数。

{
    std::string* s = new std::string;
}
delete s; // destructor called

这与newC#和Java中普遍存在的语法无关。它们用于完全不同的目的。

动态分配的好处

1.您不必事先知道数组的大小

许多C ++程序员遇到的第一个问题是,当他们接受用户的任意输入时,您只能为堆栈变量分配固定大小。您也不能更改数组的大小。例如:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

当然,如果您使用的是std::string,则在std::string内部调整自身大小,这样就不会有问题。但本质上解决此问题的方法是动态分配。您可以根据用户的输入分配动态内存,例如:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

旁注:许多初学者犯的一个错误是可变长度数组的使用。这是一个GNU扩展,也是Clang中的一个扩展,因为它们反映了许多GCC扩展。因此,int arr[n]不应依赖以下内容 。

由于堆比堆栈大得多,因此可以随意分配/重新分配所需的内存,而堆栈有一个局限性。

2.数组不是指针

您要求的好处如何?一旦您了解了数组和指针背后的困惑/误解,答案就会变得很清楚。通常假定它们是相同的,但事实并非如此。这个神话来自这样一个事实,即指针可以像数组一样被下标,并且由于数组会在函数声明的顶层衰减到指针。但是,一旦数组衰减到指针,该指针就会丢失其sizeof信息。因此sizeof(pointer)将给出指针的大小(以字节为单位),在64位系统上通常为8个字节。

您不能分配给数组,只能对其进行初始化。例如:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

另一方面,您可以使用指针执行任何操作。不幸的是,由于指针和数组之间的区别是在Java和C#中动摇的,因此初学者不了解它们之间的区别。

3.多态性

Java和C#具有允许您将对象视为另一个对象的功能,例如使用as关键字。因此,如果有人想将一个Entity对象视为一个Player对象,则可以做到Player player = Entity as Player;这一点。如果您打算在仅适用于特定类型的同类容器上调用函数,这将非常有用。该功能可以通过以下类似方式实现:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

假设如果只有Triangles具有Rotate函数,则尝试在该类的所有对象上调用它都将是编译器错误。使用dynamic_cast,您可以模拟as关键字。需要明确的是,如果强制转换失败,它将返回一个无效的指针。所以!test本质上是一种检查testNULL或无效指针的简写形式,这意味着强制转换失败。

自动变量的好处

在看到了动态分配可以做的所有很棒的事情之后,您可能想知道为什么没有人不一直使用动态分配?我已经告诉过您一个原因,堆很慢。而且,如果您不需要所有的内存,也不要滥用它。因此,以下是一些不利因素,没有特别的顺序:

  • 这很容易出错。手动分配内存很危险,容易泄漏。如果您不熟练使用调试器或valgrind(内存泄漏工具),则可以将头发拔掉。幸运的是,RAII的习语和智能指针可以缓解这种情况,但是您必须熟悉“三法则”和“五法则”之类的实践。需要吸收很多信息,不知道或不在乎的初学者都会陷入这个陷阱。

  • 没有必要。与Java和C#不同new,在C ++中,随处可见使用关键字是惯用的,在C ++中,仅应在需要时使用它。俗语说,如果你有锤子,一切看起来都像钉子。以C ++开头的初学者很害怕指针,并习惯地学习使用堆栈变量,而Java和C#程序员则是在不了解指针的情况下开始使用指针!从字面上看,这是错误的一步。您必须放弃所有已知的知识,因为语法是一回事,学习语言是另一回事。

1.(N)RVO-Aka,(命名)返回值优化

许多编译器进行的一种优化是称为省略返回值优化。这些东西可以消除不必要的副本,这对于非常大的对象(例如包含许多元素的向量)很有用。通常,通常的做法是使用指针来转移所有权,而不是复制大对象来移动它们。这导致了移动语义智能指针的诞生

如果使用指针,则不会发生(N)RVO 。如果您担心优化,则利用(N)RVO而不是返回或传递指针会更有利且更不会出错。如果函数的调用者负责delete动态分配的对象等,则可能发生错误泄漏。如果像热土豆一样传递指针,则可能很难跟踪对象的所有权。只需使用堆栈变量,因为它更简单,更好。


“所以!test本质上是检查test是否为NULL或无效指针的简写形式,这意味着强制转换失败。” 我认为这句话必须改写清楚。
berkus 2014年

4
“ Java炒作机器希望您相信” –也许是在1997年,但这已经不合时宜,2014年不再有将Java与C ++进行比较的动机。
Matt R

15
古老的问题,但是在代码段中{ std::string* s = new std::string; } delete s; // destructor called……肯定delete不会起作用,因为编译器将不再知道什么s了?
badger5000 2014年

2
我没有给出-1,但是我不同意开篇声明。首先,我不同意任何“炒作”-可能是在2000年左右,但是现在这两种语言都已广为人知。其次,我认为它们非常相似-C ++是C与Simula结合在一起的孩子,Java添加了Virtual Machine,Garbage Collector,并且HEAVILY减少了功能,C#精简并重新引入了Java缺少的功能。是的,这使模式和有效使用方式大为不同,但是了解通用的基础结构/设计是有益的,以便使人们可以看到差异。
Gerasimos R

1
@James Matta:当然,您是正确的,内存就是内存,它们都是从同一物理内存分配的,但是要考虑的一件事是,使用堆栈分配的对象获得更好的性能特征是很常见的,因为堆栈-或至少达到其最高级别-当函数进入和退出时,很有可能在缓存中变得“热”,而堆没有这种好处,因此,如果您在堆中追逐指针,则可能会遇到多个缓存未命中的情况您可能不会加入堆栈。但是所有这些“随机性”通常有利于堆栈。
Gerasimos R

23

C ++提供了三种传递对象的方法:按指针,按引用和按值。Java限制了您使用后者(唯一的例外是基本类型,例如int,boolean等)。如果您不仅想像奇怪的玩具一样使用C ++,那么最好了解这三种方式之间的区别。

Java假装不存在“谁以及何时应该销毁它?”这样的问题。答案是:垃圾收集器,很棒而又糟糕。但是,它不能提供100%的内存泄漏保护(是的,Java 可以泄漏内存))。实际上,GC给您带来了错误的安全感。SUV越大,到疏散者的路就越长。

C ++让您与对象的生命周期管理面对面。好了,有办法解决这个问题(智能指针系列,Qt中的QObject等),但是它们都不能像GC那样以“即发即弃”的方式使用:您应始终牢记内存处理。您不仅应该关心销毁一个对象,还必须避免多次销毁同一对象。

还不怕吗?好的:循环引用-自己处理,人类。请记住:精确杀死每个对象一次,我们的C ++运行时不喜欢那些与尸体打交道的人,而让死者独处。

所以,回到您的问题。

当您通过值而不是指针或引用传递对象时,您将复制对象(整个对象,无论是几个字节还是巨大的数据库转储-您足够聪明,可以避免后者,是吗?您吗?)每次您执行'='。要访问对象的成员,请使用“。”。(点)。

当您通过指针传递对象时,您只复制了几个字节(在32位系统上为4个字节,在64位系统上为8个字节),即-该对象的地址。为了向所有人显示此信息,请在访问成员时使用该花式'->'运算符。或者,您可以使用“ *”和“。”的组合。

当使用引用时,您将获得一个伪装成值的指针。这是一个指针,但是您可以通过'。'访问成员。

而且,让您再次振作起来:当您声明几个用逗号分隔的变量时,(注意指针):

  • 类型给大家
  • 值/指针/引用修饰符是单独的

例:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptr已弃用,请勿使用。
尼尔

2
可以肯定的是,如果没有提供给构造函数一个包含引用变量的初始化列表,就不能将引用作为成员。(必须立即初始化引用。即使是构造函数体,也无法设置它,IIRC。)
cHao 2014年

20

在C ++中,在堆栈上分配的对象(使用Object object;块内的语句)仅存在于声明它们的范围内。当代码块完成执行时,声明的对象将被销毁。而如果您在堆上分配内存,请使用Object* obj = new Object(),它们将继续存在于堆中,直到您调用为止delete obj

当我不仅要在声明/分配它的代码块中使用该对象时,还会在堆上创建一个对象。


6
Object obj并不总是在堆栈上-例如全局变量或成员变量。
tenfour 2014年

2
@LightnessRacesinOrbit我只提到了在块中分配的对象,而不是全局变量和成员变量。目前尚不清楚,现在已更正-在答案中添加了“在块内”。现在希望它不是虚假的信息:)
Karthik Kalyanasundaram

20

但是我不知道为什么要这样使用它?

如果使用,我将比较它在函数体内的工作方式:

Object myObject;

在函数内部,您的 myObject一旦该函数返回将被销毁。因此,如果您不需要功能之外的对象,这将很有用。该对象将放在当前线程堆栈中。

如果在函数体内编写:

 Object *myObject = new Object;

然后由 myObject函数结束后,将不会被销毁,并且分配在堆上。

现在,如果您是Java程序员,那么第二个示例更接近于Java下对象分配的工作方式。这行:Object *myObject = new Object;等同于java:Object myObject = new Object();。区别在于,在java myObject下将被垃圾回收,而在c ++下将不会被释放,您必须在某处显式调用`delete myObject;'。否则会引入内存泄漏。

从c ++ 11开始,您可以使用安全的动态分配方式:new Object通过将值存储在shared_ptr / unique_ptr中。

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

同样,对象通常存储在容器中,例如map-s或vector-s,它们将自动管理对象的生命周期。


1
then myObject will not get destroyed once function ends绝对会的。
Lightness Races in Orbit

6
在指针的情况下,myObject仍将被销毁,就像其他任何局部变量一样。区别在于其值是指向对象的指针,而不是对象本身,并且哑指针的破坏不会影响其指针。因此,对象将幸免于难。
cHao 2014年

修复了本地变量(包括指针)当然会被释放的问题-它们在堆栈中。
marcinj 2014年

13

从技术上讲,这是一个内存分配问题,但是这里有两个更实际的方面。它与两件事有关:1)范围,当您定义没有指针的对象时,在定义了它的代码块之后,您将不再能够访问它,而如果您使用“ new”定义了指针,则您可以从您有指向该内存的指针的任何位置访问它,直到您在同一指针上调用“删除”为止。2)如果要将参数传递给函数,则需要传递指针或引用以提高效率。传递对象时,将复制该对象,如果这是一个使用大量内存的对象,则可能会占用CPU(例如,复制充满数据的向量)。当您传递一个指针时,您传递的只是一个整数(取决于实现,但大多数都是一个整数)。

除此之外,您还需要了解“ new”会在需要在某个时刻释放的堆上分配内存。当您不必使用“新”时,建议您在“堆栈上”使用常规的对象定义。


6

好吧,主要问题是为什么我应该使用指针而不是对象本身?我的回答是,您(几乎)永远不要使用指针代替对象,因为C ++具有引用,它比指针更安全,并保证与指针相同的性能。

您在问题中提到的另一件事:

Object *myObject = new Object;

它是如何工作的?它创建Object类型的指针,分配内存以适合一个对象并调用默认构造函数,听起来不错,对吗?但是实际上并不是很好,如果您动态分配内存(使用的关键字new),还必须手动释放内存,这意味着在代码中应该具有:

delete myObject;

这调用了析构函数并释放了内存,看起来很容易,但是在大型项目中可能很难检测到一个线程是否释放了内存,但是为此,您可以尝试使用共享指针,这会稍微降低性能,但是使用起来要容易得多他们。


现在,一些介绍已经结束,然后再回头讨论。

在函数之间传输数据时,可以使用指针而不是对象来获得更好的性能。

看一下,您已经拥有std::string(也是对象),并且其中包含很多数据,例如大XML,现在您需要对其进行解析,但是为此,您具有void foo(...)可以用不同方式声明的功能:

  1. void foo(std::string xml); 在这种情况下,您会将所有数据从变量复制到函数堆栈,这需要一些时间,因此性能会很差。
  2. void foo(std::string* xml); 在这种情况下,您将以与传递size_t变量相同的速度传递指向对象的指针,但是此声明容易出错,因为您可以传递NULL指针或无效的指针。通常使用的指针,C因为它没有引用。
  3. void foo(std::string& xml); 在这里,您传递引用,基本上与传递指针相同,但是编译器做了一些事情,您不能传递无效的引用(实际上,可以使用无效的引用来创建情况,但这是在欺骗编译器)。
  4. void foo(const std::string* xml); 这与秒相同,只是指针值不能更改。
  5. void foo(const std::string& xml); 这与第三个相同,但是对象值不能更改。

我想进一步说明的是,无论您选择了哪种分配方式(使用new常规),都可以使用这5种方式来传递数据。


还要提及的另一件事是,当您以常规方式创建对象时,您会在堆栈中分配内存,但是在您使用new堆来创建对象时会分配内存。分配堆栈的速度要快得多,但是对于真正的大数据数组来说,这有点小,因此,如果需要大对象,则应使用堆,因为可能会导致堆栈溢出,但是通常使用STL容器可以解决此问题,并记住std::string也是容器,有些人忘记了:)


5

假设您有class A一个包含,class B当您想调用class B外部的某个函数时,class A您将仅获得指向该类的指针,并且您可以做任何您想做的事,而且它还会更改class B您的上下文class A

但是要注意动态对象


5

使用指向对象的指针有很多好处-

  1. 效率(正如您已经指出的)。将对象传递给函数意味着创建对象的新副本。
  2. 使用第三方库中的对象。如果您的对象属于第三方代码,并且作者仅打算通过指针使用对象(无复制构造函数等),则可以通过该对象传递的唯一方法是使用指针。价值传递可能会引起问题。(深拷贝/浅拷贝问题)。
  3. 如果对象拥有资源,并且您希望不与其他对象共享所有权。

3

对此进行了详细讨论,但是在Java中,所有内容都是指针。它在堆栈分配和堆分配之间没有区别(所有对象都分配在堆上),因此您没有意识到要使用指针。在C ++中,可以根据内存需求将两者混合使用。在C ++(duh)中,性能和内存使用情况更具确定性。


3
Object *myObject = new Object;

这样做将创建对对象(在堆上)的引用,该引用必须显式删除以避免内存泄漏

Object myObject;

这样做会创建一个自动类型的对象(myObject)(在堆栈上),当该对象(myObject)超出范围时将被自动删除。


1

指针直接引用对象的存储位置。Java没有这样的东西。Java具有通过哈希表引用对象位置的引用。您无法使用这些引用执行Java中的指针算术之类的操作。

要回答您的问题,这只是您的偏好。我更喜欢使用类似Java的语法。


哈希表?也许在某些JVM中,但不要指望它。
Zan Lynx

Java附带的JVM呢?当然,您可以实现任何您能想到的东西,例如直接使用指针的JVM或执行指针数学的方法。这就像在说“人们不会死于普通感冒”,而得到的回答是“也许大多数人不会,但不要指望它!” 哈哈。
RioRicoRick

2
@RioRicoRick HotSpot将Java引用实现为本机指针,请参阅docs.oracle.com/javase/7/docs/technotes/guides/vm/…据我所知,JRockit的作用相同。它们都支持OOP压缩,但都不使用哈希表。性能后果可能是灾难性的。同样,“这只是您的喜好”似乎意味着两者只是等效行为的不同语法,当然不是。
马克斯·巴拉克拉夫


0

有了指针

  • 可以直接与内存对话。

  • 可以通过操纵指针来防止程序的大量内存泄漏。


4
在C ++中,使用指针,您可以为自己的程序创建一个自定义垃圾收集器 ”,这听起来像一个糟糕的主意。
2016年

0

使用指针的原因之一是与C函数接口。另一个原因是节省内存。例如:与其将包含大量数据且具有处理器密集型复制构造函数的对象传递给函数,不如将一个指针传递给该对象,从而节省了内存和速度,尤其是在循环中,但是在这种情况下,引用将更好,除非您使用的是C样式的数组。


0

在内存利用率极高的地区,使用指针很方便。例如,考虑一个minimax算法,该算法将使用递归例程生成成千上万个节点,然后使用它们评估游戏中的下一个最佳移动,取消分配或重置(如在智能指针中)的能力显着减少了内存消耗。非指针变量继续占据空间,直到它的递归调用返回一个值。


0

我将包括一个重要的指针用例。在基类中存储某些对象时,它可能是多态的。

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

因此,在这种情况下,您不能将bObj声明为直接对象,必须具有指针。


-5

“必要性是发明之母。” 我想指出的最重要的区别是我自己编码经验的结果。有时您需要将对象传递给函数。在这种情况下,如果您的对象属于非常大的类,则将其作为对象传递将复制其状态(您可能不希望..AND AND BIG OVERHEAD),从而导致复制对象的开销。 4字节大小(假定为32位)。上面已经提到了其他原因...


14
您应该更喜欢通过引用传递
bolov

2
我建议通过像可变恒定引用传递std::string test;,我们有void func(const std::string &) {},但除非该功能需要改变在这种情况下,我建议使用指针输入(使任何人读取代码执行通知&,并了解该功能可以改变其输入)
顶大师

-7

已经有许多出色的答案,但让我举一个例子:

我有一个简单的Item类:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

我做了一个向量来容纳一堆。

std::vector<Item> inventory;

我创建了一百万个Item对象,并将它们推回到向量上。我按名称对向量进行排序,然后对特定的项目名称进行简单的迭代二进制搜索。我测试了该程序,并且花费了8分钟以上才能完成执行。然后像这样更改我的库存向量:

std::vector<Item *> inventory;

...并通过新创建我的百万个Item对象。我对我的代码所做的唯一更改是使用指向Items的指针,除了最后添加用于内存清理的循环外。该程序将在40秒内运行,或者比10倍的速度提高要好。编辑:该代码位于http://pastebin.com/DK24SPeW。 通过编译器优化,它显示出我刚刚对其进行测试的机器仅增加了3.4倍,这仍然是可观的。


2
那么您是在比较指针还是还是在比较实际对象?我非常怀疑另一层间接访问是否可以提高性能。请提供代码!之后您是否正确清理?
Stefan

1
@stefan我比较对象的数据(特别是名称字段),以进行排序和搜索。我已经在帖子中提到过,我已经正确清理了。加速可能是由于两个因素:1)std :: vector push_back()复制对象,因此指针版本仅需要为每个对象复制单个指针。这不仅对复制的数据减少,而且对向量类的内存分配器的影响也较小,因此对性能有多种影响。
达伦

2
下面的代码与您的示例几乎没有区别:排序。仅针对排序而言,指针代码比非指针代码快6%,但总体而言,比非指针代码慢10%。ideone.com/G0c7zw
stefan

3
关键字:push_back。当然可以复制。emplace创建对象时应该就位(除非您需要将它们缓存在其他位置)。
underscore_d 2015年

1
指针向量几乎总是错误的。请不要在没有详细解释警告和利弊的情况下推荐他们。您似乎找到了一个专业人士,而这仅仅是由于编码错误的反例造成的,而且陈述不实
Lightness Races in Orbit》,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.