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

Why should I use a pointer rather than the object itself?

提问人:gEdringer 提问时间:3/3/2014 最后编辑:artmgEdringer 更新时间:4/8/2023 访问量:418023

问:

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

Object *myObject = new Object;

而不是:

Object myObject;

或者,与其使用函数,不如说 ,如下所示:testFunc()

myObject.testFunc();

我们必须写:

myObject->testFunc();

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

C ++11 指针 C++-FAQ

评论

169赞 stefan 3/3/2014
如果您没有看到使用指针的理由,请不要这样做。首选对象。在unique_ptr之前优先选择对象,在原始指针之前shared_ptr优先使用对象。
139赞 Karoly Horvath 3/3/2014
注意:在 Java 中,所有内容(基本类型除外)都是指针。所以你宁愿问相反的问题:为什么我需要简单的对象?
136赞 Daniel Martín 3/3/2014
请注意,在 Java 中,指针被语法隐藏。在 C++ 中,指针和非指针之间的区别在代码中明确。Java 到处都使用指针。
261赞 Manu343726 3/4/2014
关闭太宽泛?认真地?请注意,这种 Java++ 编程方式非常普遍,也是 C++ 社区中最重要的问题之一。它应该被认真对待。
54赞 user253751 2/11/2016
当我只能使用房屋本身时,我为什么要使用房屋地址?我没有告诉亚马逊把我的包裹寄到123 Nowhere St,我只是把我的房子带到仓库,他们把我买的东西放进去,然后我把它带回来。

答:

27赞 Karthik Kalyanasundaram 3/3/2014 #1

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

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

评论

7赞 tenfour 3/3/2014
Object obj并不总是在堆栈上 - 例如全局变量或成员变量。
3赞 Karthik Kalyanasundaram 3/4/2014
@LightnessRacesinOrbit 我只提到了在块中分配的对象,而没有提到全局变量和成员变量。问题是不清楚,现在纠正了它 - 在答案中添加了“在一个块内”。希望它不是虚假信息现在:)
1826赞 17 revs, 6 users 90%Joseph Mansfield #2

非常不幸的是,您经常看到动态分配。这只是显示了有多少糟糕的C++程序员。

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

重要的带回家的信息是,您应该始终使用适合工作的工具。在几乎所有情况下,都有比执行手动动态分配和/或使用原始指针更合适、更安全的方法。

动态分配

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

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

  1. 您需要该对象比当前范围更长 - 该特定内存位置的特定对象,而不是它的副本。如果您可以复制/移动对象(大多数时候您应该这样做),您应该更喜欢自动对象。
  2. 您需要分配大量内存,这很容易填满堆栈。如果我们不必关心这个问题(大多数时候你不应该关心这个问题),那就太好了,因为它确实超出了 C++ 的范围,但不幸的是,我们必须处理我们正在开发的系统的现实。

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

指针

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

  1. 您需要引用语义。有时,您希望使用指针传递对象(无论它是如何分配的),因为您希望将该对象传递到的函数能够访问该特定对象(而不是它的副本)。但是,在大多数情况下,应首选引用类型而不是指针,因为这是它们专门用于的用途。请注意,这不一定是要将对象的生存期延长到当前范围之外,如上面的情况 1 所示。和以前一样,如果你可以传递对象的副本,你就不需要引用语义。

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

  3. 您希望通过允许在省略对象时传递 来表示对象是可选的。如果是参数,则应首选使用默认参数或函数重载。否则,最好使用封装此行为的类型,例如(在 C++17 中引入 - 使用早期的 C++ 标准,使用 )。nullptrstd::optionalboost::optional

  4. 您希望分离编译单元以缩短编译时间。指针的有用属性是,您只需要指向类型的正向声明(若要实际使用该对象,您需要一个定义)。这允许您解耦编译过程的某些部分,这可能会显著缩短编译时间。参见疙瘩成语

  5. 您需要与 C 库或 C 样式库交互。此时,你被迫使用原始指针。你能做的最好的事情就是确保你只在最后一刻让你的原始指针松动。例如,可以从智能指针获取原始指针,方法是使用其成员函数。如果库希望您通过句柄解除分配,则通常可以使用自定义删除器将句柄包装在智能指针中,该删除器将适当地解除分配对象。get

评论

97赞 3/3/2014
“你需要对象比当前范围更长”——关于这一点的补充说明:在某些情况下,你似乎需要对象比当前范围更长,但你实际上没有。例如,如果将对象放在矢量中,则该对象将被复制(或移动)到矢量中,并且原始对象在其作用域结束时可以安全地销毁。
28赞 Puppy 3/3/2014
请记住,现在在很多地方都要记住 s/copy/move/。返回对象绝对不意味着移动。您还应该注意,通过指针访问对象与对象的创建方式正交。
18赞 Manu343726 3/4/2014
我错过了在这个答案中对 RAII 的明确提及。C++ 全部(几乎全部)与资源管理有关,而 RAII 是在 C++ 上做到这一点的方法(以及原始指针生成的主要问题:破坏 RAII)
11赞 armb 3/5/2014
智能指针在 C++11 之前就已经存在,例如 boost::shared_ptr 和 boost::scoped_ptr。其他项目有自己的等效项。你无法获得移动语义,而且 std::auto_ptr 的赋值是有缺陷的,所以 C++11 改进了事情,但建议仍然很好。(还有一个可悲的吹毛求疵,仅仅访问 C++11 编译器是不够的,您可能希望代码使用的所有编译器都必须支持 C++11。是的,Oracle Solaris Studio,我看着你。
7赞 user000001 3/9/2014
@MDMoore313 你可以写Object myObject(param1, etc...)
5赞 Quest 3/3/2014 #3

假设你有 包含 当你想调用 outside 的某个函数时,你只会获得一个指向这个类的指针,你可以做任何你想做的事,它也会改变你的上下文class Aclass Bclass Bclass Aclass Bclass A

但要小心动态对象

13赞 in need of help 3/3/2014 #4

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

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

197赞 TemplateRex 3/3/2014 #5

指针有许多用例。

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

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++ 中的反模式。使用特殊资源类(标准容器之一)或智能指针(或)。考虑:newstd::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++ FAQ 中的此问答

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

评论

20赞 dyp 3/3/2014
“使用 new 运算符创建指向资源的指针是一种反模式”我认为你甚至可以将其增强为让原始指针拥有某些东西是一种反模式。不仅是创建,而且将原始指针作为参数或返回值传递,暗示所有权转移恕我直言已被弃用,因为 /move 语义unique_ptr
6赞 James Kanze 3/3/2014
在任何地方使用智能指针是一种反模式。在一些特殊情况下,它是适用的,但大多数时候,主张动态分配(任意生存期)的相同原因也反对任何通常的智能指针。
2赞 TemplateRex 3/3/2014
@JamesKanze我并不是说智能指针应该在任何地方使用,只是为了所有权,而且原始指针不应该用于所有权,而只用于视图。
2赞 JAB 3/4/2014
@TemplateRex 这似乎有点傻,因为这也需要了解签名,除非您在编译之前不知道您提供了错误的类型。虽然引用问题通常不会在编译时被发现,并且需要更多的精力来调试,但如果你正在检查签名以确保参数是正确的,你还可以查看是否有任何参数是引用,因此引用位就不是问题了(尤其是在使用显示所选函数签名的 IDE 或文本编辑器时)。也。hun(b)const&
1赞 Max Barraclough 4/11/2018
@James “在任何地方使用智能指针是一种反模式。”你的意思是不必要的堆分配,还是智能指针本身?
5赞 Rohit 3/3/2014 #6

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

  1. 效率(正如你已经指出的那样)。将对象传递给 函数意味着创建对象的新副本。
  2. 使用第三方库中的对象。如果您的对象 属于第三方代码,作者打算仅通过指针(没有复制构造函数等)来使用他们的对象,这是您可以绕过它的唯一方法 对象正在使用指针。按值传递可能会导致问题。(深 复制/浅拷贝问题)。
  3. 如果对象拥有资源,并且您希望所有权不应与其他对象共享。
22赞 marcinj 3/3/2014 #7

但是我不明白我们为什么要这样使用它?

如果您使用以下方法,我将比较它在函数体中的工作方式:

Object myObject;

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

如果你在函数体内部编写:

 Object *myObject = new Object;

那么一旦函数结束,指向的对象类实例就不会被销毁,并且分配在堆上。myObject

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

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

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赞 Lightness Races in Orbit 3/3/2014
then myObject will not get destroyed once function ends它绝对会。
6赞 cHao 3/3/2014
在指针的情况下,仍将被销毁,就像任何其他局部变量一样。不同之处在于,它的值是指向对象的指针,而不是对象本身,并且哑指针的销毁不会影响其指针。因此,该物体将在上述破坏中幸存下来。myObject
0赞 marcinj 3/4/2014
修复了局部变量(包括指针)当然会被释放 - 它们在堆栈上。
80赞 Burnt Toast 3/3/2014 #8

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

评论

3赞 berkus 3/14/2014
std::shared_ptr<T> 也适用于 T 的前向声明。 (std::unique_ptr<T> )
14赞 David Stone 11/28/2014
@berkus:适用于 的正向声明。你只需要确保当调用析构函数时,是一个完整的类型。这通常意味着包含 的类在头文件中声明其析构函数,并在 cpp 文件中实现它(即使实现为空)。std::unique_ptr<T>Tstd::unique_ptr<T>Tstd::unique_ptr<T>
0赞 Trevor Hickey 11/9/2015
模块会解决这个问题吗?
0赞 Aidiakapi 10/14/2017
@TrevorHickey 我知道的旧评论,但无论如何都要回答它。模块不会删除依赖关系,但应该使包含依赖关系变得非常便宜,在性能成本方面几乎是免费的。此外,如果模块的一般加速足以使编译时间处于可接受的范围内,那么这也不再是问题。
1赞 Jack G 12/30/2017
为什么要以牺牲最终用户为代价来加快自己的编译时间?
2赞 Cozmo 3/5/2014 #9

指针直接引用对象的内存位置。Java 没有这样的东西。Java 具有通过哈希表引用对象位置的引用。你不能用这些引用做任何像 Java 中的指针算术这样的事情。

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

评论

0赞 Zan Lynx 3/6/2014
哈希表?也许在某些 JVM 中,但不要指望它。
0赞 Cozmo 3/26/2015
Java 附带的 JVM 呢?当然,你可以实现任何你能想到的东西,比如直接使用指针的JVM,或者做指针数学的方法。这就像说“人们不会死于普通感冒”,然后得到的回应是“也许大多数人不会,但不要指望它!哈哈。
2赞 Max Barraclough 4/11/2018
@RioRicoRick HotSpot 将 Java 引用实现为本机指针,请参阅 docs.oracle.com/javase/7/docs/technotes/guides/vm/...据我所知,JRockit 也是这样做的。它们都支持 OOP 压缩,但都不使用哈希表。性能后果可能是灾难性的。此外,“这只是你的偏好”似乎暗示这两者只是等效行为的不同语法,当然它们不是。
-5赞 sandeep bisht 3/5/2014 #10

“需求是发明之母。” 我想指出的最重要的区别是我自己编码经验的结果。 有时需要将对象传递给函数。在这种情况下,如果你的对象是一个非常大的类,那么将它作为对象传递将复制它的状态(你可能不希望这样做。并且可能很大开销),从而导致复制对象的开销,而指针是固定的 4 字节大小(假设为 32 位)。其他原因在上面已经提到......

评论

15赞 bolov 3/6/2014
您应该更喜欢通过引用传递
2赞 Top-Master 12/16/2018
我建议像我们拥有的变量一样传递常量引用,但除非函数需要更改输入,在这种情况下,我建议使用指针(以便任何阅读代码的人都会注意到,并理解该函数可能会更改其输入)std::string test;void func(const std::string &) {}&
152赞 Gerasimos R 3/7/2014 #11

这个问题有很多很好的答案,包括前向声明、多态性等重要用例,但我觉得你的问题的一部分“灵魂”没有得到回答——即不同语法在 Java 和 C++ 中的含义。

让我们来看看比较这两种语言的情况:

爪哇岛:

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“原始”类型,则它们是复制的实际值,而不是指针。 所以

爪哇岛:

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++ 中,您可以更好地控制对象的生存期以及它们所在的位置。

要点是,该结构实际上是最接近典型 Java(或 C#)语义的结构。Object * object = new Object()

评论

2赞 Petr Skocik 2/11/2016
Object object1 = new Object(); Object object2 = new Object();是非常糟糕的代码。第二个 new 或第二个 Object 构造函数可能会抛出,现在 object1 被泄露了。如果您使用的是原始 s,则应尽快将 ed 对象包装在 RAII 包装器中。newnew
15赞 Gerasimos R 2/19/2016
事实上,如果这是一个程序,并且没有其他事情发生。值得庆幸的是,这只是一个解释片段,显示了 C++ 中的指针的行为方式 - 并且 RAII 对象不能替代原始指针的少数几个地方之一,是研究和学习原始指针......
92赞 user3391320 3/7/2014 #12

前言

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
}

这与 C# 和 Java 中流行的语法无关。它们用于完全不同的目的。new

动态分配的好处

1.您不必提前知道数组的大小

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

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

当然,如果您改用 ,请在内部调整自身大小,这样就不是问题了。但从本质上讲,这个问题的解决方案是动态分配。您可以根据用户的输入分配动态内存,例如:std::stringstd::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. 数组不是指针

你问这有什么好处?一旦你理解了数组和指针背后的混乱/神话,答案就会变得清晰。人们通常认为它们是相同的,但事实并非如此。这个误区来自这样一个事实,即指针可以像数组一样被下标,并且由于数组在函数声明中衰减为顶层的指针。但是,一旦数组衰减为指针,指针就会丢失其信息。因此,将给出指针的大小(以字节为单位),在 64 位系统上通常为 8 个字节。sizeofsizeof(pointer)

不能分配给数组,只能初始化它们。例如:

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# 具有允许您将对象视为另一个对象的工具,例如使用 关键字。因此,如果有人想将对象视为对象,则可以这样做:如果您打算在仅应用于特定类型的同类容器上调用函数,这将非常有用。该功能可以通过以下类似的方式实现:asEntityPlayerPlayer 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 函数,那么如果您尝试在类的所有对象上调用它,那将是编译器错误。使用 ,您可以模拟关键字。需要明确的是,如果强制转换失败,它将返回无效的指针。So 本质上是用于检查是否为 NULL 或无效指针的简写,这意味着强制转换失败。dynamic_castas!testtest

自动变量的优点

在了解了动态分配可以做的所有伟大事情之后,您可能想知道为什么没有人不一直使用动态分配?我已经告诉过你一个原因,堆很慢。如果你不需要所有这些内存,你就不应该滥用它。因此,这里有一些缺点,排名不分先后:

  • 它容易出错。手动分配内存很危险,并且容易发生泄漏。如果您不熟练使用调试器或(内存泄漏工具),您可能会从头上拔掉头发。幸运的是,RAII 习语和智能指针可以缓解这种情况,但您必须熟悉 The Rule Of Three 和 The Rule Of Five 等实践。要吸收的信息很多,要么不知道,要么不在乎的初学者都会落入这个陷阱。valgrind

  • 没有必要。与 Java 和 C# 不同,在 Java 和 C# 中,到处使用关键字是惯用的,在 C++ 中,您应该只在需要时使用它。俗话说,如果你有一把锤子,一切看起来都像钉子。从 C++ 开始的初学者害怕指针并习惯性地学习使用堆栈变量,而 Java 和 C# 程序员则在不理解指针的情况下开始使用指针!这简直是走错了路。你必须放弃你所知道的一切,因为语法是一回事,学习语言是另一回事。new

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

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

如果使用指针,则不会发生 (N)RVO。如果您担心优化,利用 (N)RVO 比返回或传递指针更有益且更不容易出错。如果函数的调用方负责处理动态分配的对象等,则可能会发生错误泄漏。如果指针像烫手山芋一样四处传递,则很难跟踪对象的所有权。只需使用堆栈变量,因为它更简单、更好。delete

评论

5赞 Matt R 3/14/2014
“Java炒作机器希望你相信” - 也许在1997年,但这现在已经不合时宜了,不再有动力在2014年将Java与C++进行比较。
21赞 badger5000 6/30/2014
老问题,但在代码段....这肯定是行不通的,因为编译器不再知道是什么了?{ std::string* s = new std::string; } delete s; // destructor calleddeletes
3赞 Gerasimos R 1/19/2017
我不是给 -1,但我不同意所写的开场白。首先,我不同意有任何“炒作”——可能围绕 Y2K,但现在这两种语言都很好理解。其次,我认为它们非常相似 - C++是C与Simula结合的孩子,Java添加了虚拟机,垃圾收集器并大量削减了功能,而C#简化并重新引入了Java中缺失的功能。是的,这使得模式和有效用法大不相同,但了解通用基础结构/设计是有益的,这样人们就可以看到差异。
2赞 Gerasimos R 10/30/2019
@James Matta:你当然是对的,内存就是内存,它们都是从同一个物理内存分配的,但需要考虑的一件事是,使用堆栈分配的对象获得更好的性能特征是很常见的,因为堆栈 - 或者至少是其最高级别 - 在函数进入和退出时,缓存中“热”的可能性非常高。 虽然堆没有这样的好处,所以如果你在堆中跟踪指针,你可能会得到多个缓存未命中,而你可能不会在堆栈上。但所有这些“随机性”通常有利于堆栈。
1赞 Timothy Baldwin 12/29/2022
@badger5000我已经进行了编辑来解决这个问题。
24赞 Kirill Gamazkov 3/7/2014 #13

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赞 Neil 3/10/2014
std::auto_ptr已弃用,请不要使用。
4赞 cHao 7/26/2014
可以肯定的是,如果不为构造函数提供包含引用变量的初始化列表,则不能将引用作为成员。(必须立即初始化引用。甚至构造函数主体也来不及设置它,IIRC。
4赞 cmollis 3/8/2014 #14

这已经详细讨论过了,但在 Java 中,一切都是一个指针。它不区分堆栈和堆分配(所有对象都分配在堆上),因此您不会意识到您正在使用指针。在 C++ 中,您可以混合使用两者,具体取决于您的内存要求。性能和内存使用在 C++ (duh) 中更具确定性。

7赞 ST3 3/8/2014 #15

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

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

Object *myObject = new Object;

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

delete myObject;

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


现在一些介绍结束了,回到问题。

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

看一看,你有(它也是对象),它包含很多数据,例如大XML,现在你需要解析它,但为此你有可以用不同方式声明的函数:std::stringvoid foo(...)

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

我还想提一下,无论您选择哪种分配方式(使用或常规),您都可以使用这 5 种方式来传递数据。new


另一件值得一提的事情是,当你以常规方式创建对象时,你会在堆栈中分配内存,但在你创建它时,你会分配堆。分配堆栈要快得多,但对于非常大的数据数组来说,它有点小,所以如果你需要大对象,你应该使用堆,因为你可能会得到堆栈溢出,但通常这个问题是使用 STL 容器解决的,记住也是容器,有些人忘记了它:)newstd::string

-8赞 Darren 2/6/2015 #16

已经有很多很好的答案,但让我举一个例子:

我有一个简单的 Item 类:

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

我制作了一个向量来容纳一堆它们。

std::vector<Item> inventory;

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

std::vector<Item *> inventory;

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

评论

2赞 stefan 2/6/2015
那么,您是在比较指针还是仍在比较实际对象?我非常怀疑另一个级别的间接性可以提高性能。请提供验证码!事后你清理得当吗?
1赞 Darren 2/6/2015
@stefan,我比较了对象的数据(特别是名称字段)以进行排序和搜索。正如我在帖子中已经提到的,我清理得很好。 加速可能是由于两个因素:1) std::vector push_back() 复制对象,因此指针版本只需要为每个对象复制一个指针。这会对性能产生多重影响,因为不仅复制的数据较少,而且向量类内存分配器受到的冲击也较少。
2赞 stefan 2/6/2015
下面的代码显示您的示例几乎没有区别:排序。指针代码比非指针代码快 6%,但总体上比非指针代码慢 10%。ideone.com/G0c7zw
4赞 underscore_d 7/15/2015
关键词: .当然,这是副本。在创建对象时,您应该就地操作(除非您需要将它们缓存在其他地方)。push_backemplace
2赞 Lightness Races in Orbit 1/21/2019
指针的向量几乎总是错误的。请不要在没有详细解释注意事项和优缺点的情况下推荐它们。你似乎找到了一个专业人士,这只是一个编码不佳的反例的结果,并歪曲了它
-1赞 lasan 6/1/2016 #17

带指针

  • 可以直接与内存对话。

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

1赞 user6244076 1/12/2017 #18

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

4赞 Palak Jain 4/16/2017 #19
Object *myObject = new Object;

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

Object myObject;

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

1赞 seccpur 2/19/2018 #20

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

1赞 user18853 3/15/2018 #21

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

Class Base1 {
};

Class Derived1 : public Base1 {
};


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

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

所以在这种情况下,你不能将 bObj 声明为直接对象,你必须有指针。

2赞 RollerSimmer 8/20/2020 #22

C++ 中对象指针的关键优势是允许多态数组和同一超类的指针映射。例如,它允许将长尾小鹦鹉、鸡、知更鸟、鸵鸟等放在鸟的阵列中。

此外,动态分配的对象更灵活,可以使用堆内存,而本地分配的对象将使用 STACK 内存,除非它是静态的。堆栈上有大型对象,尤其是在使用递归时,无疑会导致堆栈溢出。

2赞 einpoklum 7/4/2022 #23

tl;dr:不要“使用指针而不是对象本身”(通常)

你问为什么你应该更喜欢指针而不是对象本身。好吧,作为一般规则,你不应该。

现在,这条规则确实有多个例外,其他答案已经阐明了它们。问题是,如今,这些例外中的许多都不再有效!让我们考虑一下已接受的答案中列出的例外情况:

  1. 您需要引用语义。

如果需要引用语义,请使用引用,而不是指针;请参阅@ST3的答案事实上,有人可能会争辩说,在 Java 中,你传递的通常是引用。

  1. 你需要多态性。

如果您知道将要使用的类集,通常只需使用(请参阅此处的描述)并使用访问者模式对它们进行操作。现在,诚然,C++的变体实现并不是最漂亮的景象;但我通常更喜欢它,而不是用指针弄脏。std::variant<ClassA, ClassB, ClassC>

您希望表示对象是可选的

绝对不要为此使用指针。你有 std::optional,与 不同,它非常方便。请改用它。 是空的(或“null”)可选的。而且 - 这不是一个指针。std::variantnullopt

您希望分离编译单元以缩短编译时间。

您也可以使用引用而不是指针来实现此目的。要在一段代码中使用,只需说 ,即使用前向声明就足够了。Object&class Object;

您需要与 C 库或 C 样式库交互。

是的,好吧,如果你使用已经使用指针的代码,那么 - 你必须自己使用指针,无法绕过它:-(并且 C 没有引用。


此外,有些人可能会告诉您使用指针来避免复制对象。好吧,对于返回值来说,这并不是真正的问题,因为返回值和命名返回值优化(RVO 和 NRVO)。在其他情况下 - 引用避免复制就好了。

不过,底线规则仍然与公认的答案相同:只有在有充分理由需要指针时才使用指针。


PS - 如果您确实需要指针,您仍然应该避免直接使用 newdelete智能指针可能会更好地为您服务 - 它是自动释放的(不像在 Java 中,但仍然如此)。