我来自Java背景,已经开始使用C++中的对象。但我想到的一件事是,人们经常使用指向对象的指针,而不是对象本身,例如这样的声明:
Object *myObject = new Object;
而不是:
Object myObject;
或者不用函数,让我们说testFunc(),如下所示:
myObject.testFunc();
我们必须写下:
myObject->testFunc();
但我不明白我们为什么要这样做。我认为这与效率和速度有关,因为我们可以直接访问内存地址。我说得对吗?
非常不幸的是,您经常看到动态分配。这正好说明有多少糟糕的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成员函数,可以从智能指针获取原始指针。如果库为您执行了一些分配,它希望您通过句柄解除分配,则通常可以使用自定义删除器将句柄包装在智能指针中,以适当地解除分配对象。
主要问题是为什么我应该使用指针而不是对象本身?我的回答是,你应该(几乎)永远不要使用指针而不是对象,因为C++有引用,它比指针更安全,并且保证了与指针相同的性能。
你在问题中提到的另一件事:
Object *myObject = new Object;
它是如何工作的?它创建Object类型的指针,分配内存以适应一个对象,并调用默认构造函数,听起来不错,对吧?但实际上,如果你动态分配内存(使用关键字new),你还必须手动释放内存,这意味着在代码中你应该:
delete myObject;
这调用析构函数并释放内存,看起来很简单,但在大型项目中,可能很难检测一个线程是否释放了内存,但为此,您可以尝试共享指针,这会稍微降低性能,但使用它们要容易得多。
现在一些介绍已经结束,回到问题上来。
在函数之间传输数据时,可以使用指针而不是对象来获得更好的性能。
看看,你有std::string(它也是对象),它包含了很多数据,例如大XML,现在你需要解析它,但为此你有一个函数void foo(…),它可以用不同的方式声明:
void foo(std::string xml);在这种情况下,您将把变量中的所有数据复制到函数堆栈中,这需要一些时间,因此性能会很低。void foo(std::string*xml);在这种情况下,您将以与传递size_t变量相同的速度将指针传递给对象,但这种声明容易出错,因为您可以传递NULL指针或无效指针。指针通常在C中使用,因为它没有引用。void foo(std::string&xml);这里传递引用,基本上和传递指针一样,但编译器做了一些事情,不能传递无效引用(实际上,可能会使用无效引用创建情况,但这会使编译器感到棘手)。void foo(const std::string*xml);这里与第二个相同,只是指针值不能更改。void foo(const std::string&xml);这里与第三个相同,但对象值不能更改。
我还想提的是,无论您选择了哪种分配方式(新的还是常规的),您都可以使用这5种方式传递数据。
另一件事要提的是,当您以常规方式创建对象时,您会在堆栈中分配内存,但当您使用新对象创建对象时会分配堆。分配堆栈要快得多,但对于真正大的数据数组来说,它有点小,所以如果你需要大对象,你应该使用堆,因为你可能会遇到堆栈溢出,但通常这个问题是使用STL容器解决的,记住std::string也是容器,有些人忘记了:)