协慌网

登录 贡献 社区

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

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

Object *myObject = new Object;

而不是:

Object myObject;

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

myObject.testFunc();

我们要写:

myObject->testFunc();

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

答案

你经常看到动态分配是非常不幸的。这只是表明有多少糟糕的 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成员函数。如果库为您执行某些分配,它希望您通过句柄释放,则通常可以使用自定义删除器将句柄包装在智能指针中,该删除器将适当地释放对象。

指针有很多用例。

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

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 ++ 中的反模式 。使用特殊资源类(标准容器之一)或智能指针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创建,直接由您或在资源类内部创建)。

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