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

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

Object *myObject = new Object;

而不是:

Object myObject;

或者不使用函数,假设testFunc(),像这样:

myObject.testFunc();

我们必须写:

myObject->testFunc();

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

395653 次浏览

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

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

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

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

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

动态分配

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

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

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

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

指针

但是,除了动态分配之外,原始指针还有其他更一般的用途,但大多数都有您应该喜欢的替代方案。

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

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

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

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

  5. 你需要使用一个C库或者一个C风格的库。在这一点上,你被迫使用原始指针。你能做的最好的事情就是确保你只在最后一刻释放原始指针。你可以从智能指针中获取原始指针,例如,通过使用它的get成员函数。如果一个库为你执行了一些分配,它希望你通过句柄解除分配,你通常可以用一个自定义的删除器将句柄包装在智能指针中,该删除器将适当地解除对象的分配。

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

但要小心动态物体

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

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

指针有许多用例。

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

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" offgun(&d);   // OK, a Derived object IS-A Base objecthun(d);    // also OK, reference also doesn't slice

参考语义学和避免复制.对于非多态类型,指针(或引用)将避免复制潜在的昂贵对象

Base b;fun(b);  // copies b, potentially expensivegun(&b); // takes a pointer to b, no copyinghun(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;}

vs.

{auto b = std::make_unique<Base>();...       // OK, now exception safe}

原始指针应仅用作“视图”,而不应以任何方式参与所有权,无论是通过直接创建还是隐式通过返回值。另请参阅此问答来自C++FAQ

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

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

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

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

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

Object myObject;

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

如果你在函数体中写:

 Object *myObject = new Object;

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

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

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

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");
// since c++14std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared");

此外,对象通常存储在容器中,例如map-s或vector-s,它们将自动管理对象的生命周期。

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

指针直接引用对象的内存位置。Java没有这样的东西。Java有通过哈希表引用对象位置的引用。您不能用这些引用Java指针算术。

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

“需要是发明之母。我想指出的最重要的区别是我自己编码经验的结果。有时你需要将对象传递给函数。在这种情况下,如果你的对象是一个非常大的类,那么将其作为对象传递将复制其状态(你可能不想要…

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

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

Java:

Object object1 = new Object(); //A new object is allocated by JavaObject 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 heapObject * object2 = new Object(); //Another new object is allocated on the heapdelete 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 STACKObject object2; //Another new object is allocated on the STACKobject1 = 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#)语义学的。

前言

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.数组不是指针

你问的这个好处是什么?一旦你理解了数组和指针背后的混淆/神话,答案就会变得清晰。人们通常认为它们是相同的,但它们不是。这个神话来自于指针可以像数组一样下标的事实,也是因为数组在函数声明中衰减到顶层的指针。然而,一旦数组衰减到指针,指针就失去了它的sizeof信息。所以sizeof(pointer)将给出指针的大小,以字节为单位,在64位系统上通常是8字节。

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

int arr[5] = {1, 2, 3, 4, 5}; // initializationint arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array// be given by the amount of members in the initializerarr = { 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 trianglesif (!test) // not a trianglee.GenericFunction();elsee.TriangleOnlyMagic();}

所以说,如果只有三角形有一个Rotate函数,如果你试图对类的所有对象调用它,那将是一个编译器错误。使用dynamic_cast,你可以模拟as关键字。需要明确的是,如果强制转换失败,它会返回一个无效指针。所以!test本质上是检查test是NULL还是无效指针的简写,这意味着强制转换失败了。

自动变量的好处

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

  • 它很容易出错。手动分配内存是危险的,你很容易发生泄漏。如果你不熟练使用调试器或valgrind(一个内存泄漏工具),你可能会感到毛骨悚然。幸运的是,RAII习语和智能指针稍微缓解了这一点,但你必须熟悉诸如三规则和五规则等实践。要吸收的信息很多,要么不知道要么不在乎的初学者会掉进这个陷阱。

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

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

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

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

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 surpriseMyStruct *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 individuals1.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 values1.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

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

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

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

Object *myObject = new Object;

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

delete myObject;

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


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

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

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

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

更重要的是,无论您选择哪种分配方式(使用new定期),您都可以使用这5种方式来传递数据。


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

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

我有一个简单的Item类:

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

我做了一个向量来容纳一堆。

std::vector<Item> inventory;

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

std::vector<Item *> inventory;

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

用指针

  • 可以直接与记忆对话。

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

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

Object *myObject = new Object;

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

Object myObject;

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

在内存利用率最高的领域,指针很方便。例如,考虑一个极小极大算法,其中将使用递归例程生成数千个节点,然后使用它们来评估游戏中的下一个最佳移动,释放或重置的能力(如在智能指针中)显着减少内存消耗。而非指针变量继续占用空间,直到它的递归调用返回一个值。

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

Class Base1 {};
Class Derived1 : public Base1 {};

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

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

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

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

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

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

现在,这个规则确实有多个例外,其他答案已经详细说明了它们。问题是,这些天来,其中许多例外不再有效!让我们考虑一下接受的答案中列出的例外:

  1. 你需要参考语义学。

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

  1. 你需要多态性。

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

你想表示一个对象是可选的

绝对不要为此使用指针。你有#0,与std::variant不同,它非常方便。改为使用它。nullopt是一个空(或“null”)可选项。而且-它不是指针。

您希望解耦编译单元以缩短编译时间。

你也可以使用引用而不是指针来实现这一点。要在一段代码中使用Object&,说class Object;就足够了,即使用前置声明

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

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


另外,有些人可能会告诉你使用指针来避免复制对象。由于返回值和命名返回值优化(RVO和NRVO),这对于返回值来说并不是一个问题。在其他情况下-引用避免复制很好。

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


PS-如果你确实需要一个指针,你仍然应该避免直接使用#0和#1。你可能会更好地使用智能指针-它是自动释放的(不像Java,但仍然)。