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

Object *myObject = new Object;

而不是:

Object myObject;

或者不用函数,让我们说testFunc(),如下所示:

myObject.testFunc();

我们必须写下:

myObject->testFunc();

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


当前回答

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

其他回答

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

如果我想在堆上创建一个对象,而不仅仅是在声明/分配该对象的代码块中使用该对象。

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

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

你问为什么你更喜欢指针而不是对象本身。一般来说,你不应该。

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

您需要引用语义。

如果需要引用语义,请使用引用,而不是指针;参见@ST3的答案。事实上,有人会认为,在Java中,传递的通常是引用。

你需要多态性。

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

您希望表示对象是可选的

绝对不要为此使用指针。您有std::可选的,与std::变体不同,它非常方便。请改用它。nullopt是空(或“null”)可选项。而且-这不是指针。

您希望分离编译单元以提高编译时间。

您也可以使用引用而不是指针来实现这一点。要在一段代码中使用Object&,只要说class Object;,即使用转发声明。

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

是的,好吧,如果你使用已经使用指针的代码,那么-你必须自己使用指针,不能绕过这一点:-(而且C没有引用。


此外,有些人可能会告诉您使用指针来避免复制对象。由于返回值和命名的返回值优化(RVO和NRVO),这对于返回值来说并不是一个真正的问题。在其他情况下,参考文献可以很好地避免复制。

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


PS-如果你确实需要一个指针,你仍然应该避免直接使用new和delete。智能指针可能会更好地为您服务,它会自动释放(不像Java那样,但仍然如此)。

非常不幸的是,您经常看到动态分配。这正好说明有多少糟糕的C++程序员。

从某种意义上说,你有两个问题合并在一起。第一个问题是什么时候应该使用动态分配(使用新的)?第二个问题是我们什么时候应该使用指针?

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

动态分配

在您的问题中,您演示了创建对象的两种方法。主要区别是对象的存储持续时间。执行Object myObject时;在一个块中,创建的对象具有自动存储持续时间,这意味着当它超出范围时将自动销毁。当您执行new Object()时,对象具有动态存储持续时间,这意味着它将保持活动状态,直到您显式删除它。您只应在需要时使用动态存储持续。也就是说,在可能的情况下,您应该始终倾向于创建具有自动存储持续时间的对象。

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

您需要该对象比当前作用域(即位于特定内存位置的特定对象,而不是其副本)更长寿。如果您可以复制/移动该对象(大多数情况下您应该这样做),则应该选择自动对象。您需要分配大量内存,这可能很容易填满堆栈。如果我们不必担心这一点(大多数时候你不必担心),那将是很好的,因为这确实超出了C++的权限,但不幸的是,我们必须处理我们正在开发的系统的现实。

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

指针

然而,除了动态分配之外,原始指针还有其他更一般的用途,但大多数都有您应该更喜欢的替代方法。如前所述,除非你真的需要指针,否则总是倾向于选择其他选项。

您需要引用语义。有时,您希望使用指针传递对象(无论它是如何分配的),因为您希望传递对象的函数能够访问该特定对象(而不是其副本)。然而,在大多数情况下,您应该更喜欢引用类型而不是指针,因为这正是它们的设计目的。注意,这不一定是关于将对象的寿命延长到当前范围之外,如上面的情况1所示。如前所述,如果可以传递对象的副本,则不需要引用语义。你需要多态性。您只能通过指向对象的指针或引用以多态方式(即,根据对象的动态类型)调用函数。如果这是您需要的行为,那么您需要使用指针或引用。同样,应首选参考文献。您希望通过在省略对象时允许传递nullptr来表示对象是可选的。如果它是一个参数,您应该更喜欢使用默认参数或函数重载。否则,您最好使用封装此行为的类型,例如std::optional(在C++17中引入-对于早期的C++标准,使用boost::option)。您希望分离编译单元以提高编译时间。指针的有用特性是只需要指向类型的前向声明(要实际使用对象,需要定义)。这允许您分离部分编译过程,这可能会显著缩短编译时间。参见Pimpl成语。您需要与C库或C样式库交互。此时,您必须使用原始指针。你能做的最好的事情就是确保你只在最后一刻放松你的原始指针。例如,通过使用智能指针的get成员函数,可以从智能指针获取原始指针。如果库为您执行了一些分配,它希望您通过句柄解除分配,则通常可以使用自定义删除器将句柄包装在智能指针中,以适当地解除分配对象。

对于指针,

可以直接与存储器对话。可以通过操纵指针防止程序的大量内存泄漏。