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

Object *myObject = new Object;

而不是:

Object myObject;

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

myObject.testFunc();

我们必须写下:

myObject->testFunc();

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


当前回答

前言

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中流行的新语法无关。它们用于完全不同的目的。

动态分配的好处

1.您不必事先知道阵列的大小

许多C++程序员首先遇到的问题之一是,当他们接受用户的任意输入时,只能为堆栈变量分配固定大小。也不能更改数组的大小。例如:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

当然,如果改用std::string,std::字符串会在内部调整自身大小,这样就不会有问题。但本质上解决这个问题的方法是动态分配。您可以根据用户的输入分配动态内存,例如:

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(指针)将以字节为单位给出指针的大小,在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关键字。因此,如果有人想将实体对象视为Player对象,可以执行Playerplayer=EntityasPlayer;如果您打算在仅应应用于特定类型的同构容器上调用函数,这非常有用。功能可通过以下类似方式实现:

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本质上是检查测试是否为NULL或无效指针的简写,这意味着强制转换失败。

自动变量的优点

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

它容易出错。手动分配内存是危险的,并且容易发生泄漏。如果你不熟练使用调试器或valgrind(一种内存泄漏工具),你可能会抓狂。幸运的是,RAII习语和智能指针稍微缓解了这一点,但您必须熟悉诸如“三法则”和“五法则”之类的实践。这是一个需要学习的大量信息,初学者如果不知道或者不在乎,就会陷入这个陷阱。这是没有必要的。与Java和C#不同,在C++中,到处使用新关键字是一种习惯,只有在需要的时候才应该使用它。当初学者开始使用C++时,他们害怕指针,并习惯性地学习使用堆栈变量,而Java和C#程序员开始使用指针时却不懂它!这实际上是踩错了脚。你必须放弃你所知道的一切,因为语法是一回事,学习语言是另一回事。

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

许多编译器进行的一种优化是省略和返回值优化。这些东西可以避免不必要的复制,这对于非常大的对象(例如包含许多元素的向量)非常有用。通常,通常的做法是使用指针来转移所有权,而不是复制大型对象来移动它们。这导致了移动语义和智能指针的出现。

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

其他回答

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

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

这已经详细讨论过了,但在Java中,一切都是指针。它不区分堆栈和堆分配(所有对象都在堆上分配),因此您不会意识到您正在使用指针。在C++中,可以根据内存需求将两者混合使用。C++中的性能和内存使用更具确定性(duh)。

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++提供了三种传递对象的方法:通过指针、引用和值。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

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

Class Base1 {
};

Class Derived1 : public Base1 {
};


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

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

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