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


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

Object *myObject = new Object;

而不是:

Object myObject;

或者说,而不是使用函数 testFunc(), 喜欢这个:

myObject.testFunc();

我们要写:

myObject->testFunc();

但我无法弄清楚为什么我们应该这样做。我认为它与效率和速度有关,因为我们可以直接访问内存地址。我对吗?


1335
2018-03-03 11:54


起源


感谢您对这种做法提出质疑,而不仅仅是追随它。大多数情况下,指针被过度使用。 - Luchian Grigore
如果您没有看到使用指针的原因,请不要。喜欢对象。在原始指针之前的shared_ptr之前优先于unique_ptr之前的对象。 - stefan
注意:在java中,一切(基本类型除外)都是指针。所以你宁可问相反的问题:为什么我需要简单的物体? - Karoly Horvath
请注意,在Java中,指针会被语法隐藏。在C ++中,指针和非指针之间的区别在代码中是显式的。 Java在任何地方都使用指针。 - Daniel Martín
关闭为 太宽泛?真的吗?请大家,请注意这种Java ++编程方式 非常常见,也是C ++社区最重要的问题之一。应该认真对待。 - Manu343726


答案:


你经常看到动态分配是非常不幸的。这只是表明有多少糟糕的C ++程序员。

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

你需要的重要信息就是 总是使用适当的工具来完成工作。在几乎所有情况下,都比执行手动动态分配和/或使用原始指针更合适,更安全。

动态分配

在您的问题中,您已经演示了两种创建对象的方法。主要区别在于对象的存储持续时间。做的时候 Object myObject; 在块内,对象是使用自动存储持续时间创建的,这意味着当它超出范围时将自动销毁。当你这样做 new Object(),该对象具有动态存储持续时间,这意味着它将保持活动状态直到您明确 delete 它。您应该只在需要时使用动态存储持续时间。 那是, 你应该 总是 喜欢尽可能创建具有自动存储持续时间的对象

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

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

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

指针

但是,除了动态分配之外,还有其他更常用的原始指针,但大多数都有您应该选择的替代方案。像之前一样, 除非你真的需要指针,否则总是喜欢替代品

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

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

  3. 您想表示对象是可选的 允许一个 nullptr 在省略对象时传递。如果它是一个参数,您应该更喜欢使用默认参数或函数重载。否则,您应该更喜欢使用封装此行为的类型,例如 std::optional (在C ++ 17中引入 - 使用早期的C ++标准,使用 boost::optional)。

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

  5. 您需要与C库接口 或者是C风格的图书馆。此时,您被迫使用原始指针。你能做的最好的事情就是确保你只让你的原始指针在最后一刻松动。您可以从智能指针获取原始指针,例如,使用它 get 会员功能。如果库为您执行某些分配,它希望您通过句柄释放,则通常可以使用自定义删除器将句柄包装在智能指针中,该删除器将适当地释放对象。


1348
2018-03-03 12:06



“你需要的对象比目前的范围更长。” - 关于此的另一个注意事项:在某些情况下,您似乎需要使对象超过当前范围,但实际上并非如此。例如,如果将对象放在向量中,对象将被复制(或移动)到向量中,并且原始对象在其作用域结束时可以安全地销毁。 - hvd
请记住s / copy / move /现在很多地方。返回对象肯定不意味着移动。您还应注意,通过指针访问对象与其创建方式正交。 - Puppy
我想念这个答案明确提到RAII。 C ++是(几乎所有)关于资源管理的全部内容,而RAII是在C ++上实现它的方式(以及原始指针生成的主要问题:打破RAII) - Manu343726
智能指针在C ++ 11之前存在,例如boost :: shared_ptr和boost :: scoped_ptr。其他项目有自己的等价物。你不能得到移动语义,并且std :: auto_ptr的赋值是有缺陷的,所以C ++ 11改进了一些东西,但建议仍然很好。 (而且一个悲伤的挑剔,它是不够的访问 一个 C ++ 11编译器,您可能希望代码使用支持C ++ 11的所有编译器都是必需的。是的,Oracle Solaris Studio,我正在看着你。) - armb
@ MDMoore313你可以写 Object myObject(param1, etc...) - user000001


指针有很多用例。

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

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具有移动语义,可以避免将昂贵对象的许多副本转换为函数参数和返回值。但是使用指针肯定会避免使用指针,并允许在同一个对象上使用多个指针(而对象只能从一次移动)。

资源获取。使用。创建指向资源的指针 new 运营商是一个 反模式 在现代C ++中。使用特殊资源类(标准容器之一)或a 智能指针 (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 ++ FAQ的问答

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


156
2018-03-06 18:40



“使用new运算符创建指向资源的指针是一种反模式” 我想你甚至可以提高它 拥有一个原始指针拥有的东西是一种反模式。不仅创建,而且将原始指针作为参数传递或返回值意味着所有权转移恕我直销,因此不推荐使用 unique_ptr/移动语义 - dyp
@dyp tnx,更新并参考有关此主题的C ++ FAQ问答。 - TemplateRex
在任何地方使用智能指针都是一种反模式。有一些特殊情况适用,但大多数情况下,与动态分配(任意生命周期)争论的原因相反,也反对任何常见的智能指针。 - James Kanze
@JamesKanze我并不是说暗示智能指针应该在任何地方使用,只是为了所有权,而且原始指针不应该用于所有权,而只能用于视图。 - TemplateRex
@TemplateRex这看起来有点傻 hun(b) 还需要了解签名,除非你在编译之前不知道你提供了错误的类型。虽然引用问题通常不会在编译时被捕获并且需要花费更多精力进行调试,但如果您检查签名以确保参数正确,那么您还将能够查看是否有任何参数是引用因此参考位变为无问题(特别是在使用显示所选功能的签名的IDE或文本编辑器时)。也, const&。 - JAB


这个问题有许多优秀的答案,包括前向声明,多态等的重要用例,但我觉得你问题的“灵魂”的一部分没有得到回答 - 即不同的语法在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#)的语义。


111
2018-03-03 14:34



Object2 is now "dead":我想你的意思是 myObject1 或者更确切地说 the object pointed to by myObject1。 - Clément
确实!改写了一下。 - Gerasimos R
Object object1 = new Object(); Object object2 = new Object(); 是非常糟糕的代码。第二个新的或第二个Object构造函数可能会抛出,现在object1被泄露了。如果你使用原始 news,你应该换行 new尽快在RAII包装器中编辑对象。 - PSkocik
事实上,如果这是一个程序,那就不会发生任何其他事情了。值得庆幸的是,这只是一个解释片段,展示了C ++中的指针如何表现 - 以及RAII对象无法替代原始指针的少数几个地方之一,正在研究和学习原始指针...... - Gerasimos R


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


73
2018-03-07 07:30



这真的增加了有用信息的组合,很高兴你成功了! - TemplateRex
参考文献也适用于此。 - Zan Lynx
std :: shared_ptr <T>也适用于T的前向声明(std :: unique_ptr <T> 不) - berkus
@berkus: std::unique_ptr<T> 确实与前向声明有关 T。你只需要确保在析构函数时 std::unique_ptr<T> 叫做, T 是一个完整的类型。这通常意味着您的类包含 std::unique_ptr<T> 在头文件中声明它的析构函数并在cpp文件中实现它(即使实现为空)。 - David Stone
@DavidStone谢谢,它有效! - berkus


前言

与炒作相反,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

这与此无关 new C#和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 本质上是检查是否的简写 test 是NULL或无效指针,这意味着强制转换失败。

自动变量的好处

在看到动态分配可以做的所有伟大事情之后,你可能想知道为什么没有人不会一直使用动态分配?我已经告诉你一个原因,堆很慢。如果你不需要所有的记忆,你不应该滥用它。所以这里有一些缺点,没有特别的顺序:

  • 这很容易出错。手动内存分配很危险,而且容易发生泄漏。如果您不熟练使用调试器或 valgrind (内存泄漏工具),你可以从头上拔出你的头发。幸运的是,RAII成语和智能指针可以缓解这一点,但你必须熟悉诸如“三规则”和“五规则”等实践。这需要很多信息,而不知道或不关心的初学者会陷入这个陷阱。

  • 没有必要。与Java和C#不同,它使用的是惯用语 new 关键字无处不在,在C ++中,只有在需要时才应该使用它。常见的一句话是,如果你有锤子,一切看起来都像钉子。而以C ++开头的初学者害怕指针并学习习惯,Java和C#程序员使用堆栈变量 开始 通过使用指针而不理解它!这实际上是走错了路。你必须抛弃你所知道的一切,因为语法是一回事,学习语言是另一回事。

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

许多编译器所做的一个优化就是所谓的 省音 和 返回值优化。这些东西可以避免不必要的复制,这对于非常大的对象很有用,例如包含许多元素的向量。通常的做法是使用指针 所有权转让 而不是将大对象复制到 移动 他们身边。这导致了它的诞生 移动语义 和 智能指针

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


62
2018-03-07 10:00



“所以!测试本质上是检查测试是否为NULL或无效指针的简写,这意味着强制转换失败。”我认为这句话必须重写以保持清晰。 - berkus
“Java炒作机器希望你相信” - 也许是在1997年,但现在这已经不合时宜了,2014年将Java与C ++进行比较已不再有动力了。 - Matt R
旧问题,但在代码段 { std::string* s = new std::string; } delete s; // destructor called ......当然可以 delete 将无法正常工作,因为编译器不会知道什么 s 已经了? - badger5000
“...指针和数组之间的区别在Java中是手动挥动的......”完全没有。抛开Java没有指针(它有可空的引用),数组就像任何其他类型一样。您不能通过丢弃数组语法来“手动”引用,并假装它是对其第一个元素的引用。 int[] nums = { 0 }; int zero = nums; 不会编译。 - Justin
@Justin什么是指针,如果不是可空的引用? - Shoe


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

21
2018-03-03 12:00



std::auto_ptr 不推荐使用,请不要使用它。 - Neil
感谢std ::指出这一点,我编辑了答案 - Kirill Gamazkov
很可靠的是,如果没有为构造函数提供包含引用变量的初始化列表,则不能将引用作为成员。 (必须立即初始化引用。即使是构造函数体,也无法设置它,IIRC。) - cHao


在C ++中,在堆栈上分配的对象(使用 Object object; 块中的语句只会在它们声明的范围内。当代码块完成执行时,声明的对象将被销毁。 而如果你在堆上分配内存,使用 Object* obj = new Object(),他们继续住在堆里直到你打电话 delete obj

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


19
2018-03-03 12:19



Object obj 并不总是在堆栈上 - 例如全局变量或成员变量。 - tenfour
@tenfour是的。我知道 :) - Karthik Kalyanasundaram
然后你明知故意写了虚假信息! - Lightness Races in Orbit
@LightnessRacesinOrbit我只提到了块中分配的对象,而不是全局变量和成员变量。事情是不清楚,现在纠正它 - 在答案中“在一个街区内”添加。希望它现在不是虚假信息:) - Karthik Kalyanasundaram


但我无法弄清楚为什么要这样使用呢?

如果您使用以下内容,我将比较它在函数体内的工作原理:

Object myObject;

在功能里面,你的 myObject 一旦这个函数返回就会被销毁。因此,如果您不需要函数外的对象,这将非常有用。该对象将放在当前线程堆栈中。

如果你在函数体内写:

 Object *myObject = new Object;

然后指向的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,它们将自动管理对象的生命周期。


18
2018-03-03 12:05



then myObject will not get destroyed once function ends 绝对会。 - Lightness Races in Orbit
在指针的情况下, myObject仍将被销毁,就像任何其他局部变量一样。不同之处在于它的值是a 指针 一个对象,而不是对象本身,一个哑指针的破坏不会影响它的指针。所以 目的 将生存说毁灭。 - cHao
修正了当然变量(包括指针)当然会被释放 - 它们在堆栈上。 - marcinj