Why should I use a pointer rather than the object itself?
我来自Java背景,已经开始使用C++中的对象。但我想到的一件事是,人们经常使用指向对象的指针,而不是对象本身的指针,例如此声明:
1 | Object *myObject = new Object; |
而不是:
1 | Object myObject; |
或者不使用函数,比如说
1 | myObject.testFunc(); |
我们必须写:
1 | myObject->testFunc(); |
但我不明白我们为什么要这样做。我假设这与效率和速度有关,因为我们可以直接访问内存地址。我说的对吗?
很不幸,您经常看到动态分配。这只是显示有多少坏C++程序员有。好的。
从某种意义上讲,您有两个问题捆绑在一起。第一个问题是什么时候应该使用动态分配(使用
重要的"带回家"信息是,您应该始终为工作使用适当的工具。在几乎所有情况下,都有比执行手动动态分配和/或使用原始指针更合适和更安全的方法。好的。动态分配
在您的问题中,您演示了两种创建对象的方法。主要区别在于对象的存储时间。当在一个块内执行
可能需要动态分配的主要两种情况:好的。
当您确实需要动态分配时,应该将其封装在智能指针或执行RAII的其他类型(如标准容器)中。智能指针提供动态分配对象的所有权语义。例如,看看
但是,除了动态分配之外,原始指针还有其他更一般的用途,但是大多数都有您应该选择的替代方法。和以前一样,除非你真的需要指针,否则总是选择其他选项。好的。
您需要引用语义。有时,您希望使用指针传递一个对象(不管它是如何分配的),因为您希望传递给它的函数能够访问该特定对象(而不是它的副本)。然而,在大多数情况下,您应该更喜欢引用类型而不是指针,因为这是专门为它们设计的。注意,这不一定是关于将对象的寿命延长到当前范围之外,如上面的情况1所示。和以前一样,如果您可以传递对象的副本,就不需要引用语义。好的。
你需要多态性。只能通过指向对象的指针或引用以多态方式调用函数(即根据对象的动态类型)。如果这是您需要的行为,那么您需要使用指针或引用。同样,应首选参考文献。好的。
您希望通过允许在省略对象时传递
您希望分离编译单元以提高编译时间。指针的有用属性是,您只需要指向类型的前向声明(要实际使用对象,需要定义)。这允许您分离编译过程的各个部分,这可以显著地缩短编译时间。参见pimpl习惯用法。好的。
您需要与C库或C样式库接口。此时,您必须使用原始指针。你能做的最好的事情就是确保你只在最后可能的时刻放松你的原始指针。例如,可以使用其
好啊。
指针有许多用例。
多态行为。对于多态类型,指针(或引用)用于避免切片:
1 2 3 4 5 6 7 8 9 10 11 | 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 |
引用语义和避免复制。对于非多态类型,指针(或引用)将避免复制可能昂贵的对象。
1 2 3 4 | 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具有移动语义,可以避免将昂贵对象的许多副本转换为函数参数和返回值。但是使用指针肯定会避免这些情况,并且允许同一对象上有多个指针(而一个对象只能从一次移动)。
资源获取。使用EDCOX1的9位操作符创建一个资源指针是现代C++中的一种反模式。使用特殊资源类(标准容器之一)或智能指针(
1 2 3 4 5 | { auto b = new Base; ... // oops, if an exception is thrown, destructor not called! delete b; } |
VS
1 2 3 4 | { auto b = std::make_unique<Base>(); ... // OK, now exception safe } |
原始指针只能用作"视图",而不应以任何方式涉及所有权,无论是通过直接创建还是通过返回值隐式创建。也请参阅来自C++ FAQ的这个问答。
每次复制共享指针(例如,作为函数参数)时,它所指向的资源都保持活动状态,从而实现更细粒度的生命周期控制。当超出范围时,常规对象(不是由
这个问题有很多很好的答案,包括前向声明、多态性等重要的用例,但是我觉得你的问题的"灵魂"的一部分没有被回答,也就是说,Java和C++之间不同的语法意味着什么。
让我们检查一下比较两种语言的情况:
爪哇:1 2 3 4 5 6 7 8 | 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++:1 2 3 4 5 6 7 8 | 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++方式:
1 2 3 4 5 6 | 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"原语"类型,它们是被复制的实际值,而不是指针。所以,
爪哇:1 2 3 | 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++中,你对对象的生命周期和它们生存的位置有更多的控制。
回溯点——EDCOX1×5构造实际上是最接近典型Java(或C语言)的语义。
使用指针的另一个很好的原因是向前声明。在一个足够大的项目中,它们可以真正加快编译时间。
序言
Java与C++无关,与炒作相反。Java炒作机器希望你相信,因为Java有C++类似的语法,所以语言是相似的。任何事情都离不开真相。这种错误信息是Java程序员进入C++并使用Java类语法而不理解其代码含义的部分原因。好的。我们向前走
But I can't figure out why should we do it this way. I would assume it
has to do with efficiency and speed since we get direct access to the
memory address. Am I right?Ok.
事实上,恰恰相反。堆比堆栈慢得多,因为与堆相比,堆栈非常简单。自动存储变量(也称为堆栈变量)一旦超出作用域,就会调用其析构函数。例如:好的。
1 2 3 4 | { std::string s; } // s is destroyed here |
另一方面,如果使用动态分配的指针,则必须手动调用其析构函数。
1 2 3 4 | { std::string* s = new std::string; } delete s; // destructor called |
这与C语言和Java中普遍使用的EDCOX1 1语法无关。它们的用途完全不同。好的。动态分配的好处
1. You don't have to know the size of the array in advance
Ok.
许多C++程序员遇到的第一个问题是,当它们接受来自用户的任意输入时,只能为堆栈变量分配固定大小。也不能更改数组的大小。例如:好的。
1 2 3 | char buffer[100]; std::cin >> buffer; // bad input = buffer overflow |
当然,如果使用
1 2 3 4 | int * pointer; std::cout <<"How many items do you need?"; std::cin >> n; pointer = new int[n]; |
Side note: One mistake many beginners make is the usage of
variable length arrays. This is a GNU extension and also one in Clang
because they mirror many of GCC's extensions. So the following
int arr[n] should not be relied on.Ok.
因为堆比堆栈大得多,所以可以根据需要任意分配/重新分配内存,而堆栈有一个限制。好的。
2. Arrays are not pointers
Ok.
你问这有什么好处?一旦你理解了数组和指针背后的混乱/神话,答案就会变得清晰。通常认为它们是相同的,但事实并非如此。这个神话来自这样一个事实:指针可以像数组一样被下标,并且由于数组在函数声明中衰减到顶级指针。但是,一旦数组衰减到指针,指针将丢失其
不能分配给数组,只能初始化它们。例如:好的。
1 2 3 4 | 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 |
另一方面,你可以用指针做你想做的任何事情。不幸的是,由于指针和数组之间的区别在爪哇和C语言中是挥手的,初学者不理解差异。好的。
3. Polymorphism
Ok.
Java和C语言具有允许您将对象视为另一个对象的工具,例如使用EDCOX1的6个关键字。因此,如果有人想将
1 2 3 4 5 6 7 8 9 10 11 | 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(); } |
所以假设只有三角形有一个旋转函数,如果您试图在类的所有对象上调用它,这将是一个编译器错误。使用
在看到动态分配所能做的所有伟大的事情之后,您可能想知道为什么没有人会一直不使用动态分配?我已经告诉过你一个原因,堆得太慢了。如果你不需要所有的记忆,你就不应该滥用它。所以这里有一些不按特定顺序排列的缺点:好的。
这很容易出错。手动内存分配是危险的,您很容易发生泄漏。如果您不擅长使用调试器或
valgrind (内存泄漏工具),您可能会把头发从脑袋里拔出来。幸运的是,raii习语和智能指针减轻了这一点,但您必须熟悉一些实践,如三法则和五法则。这是很多信息要接受,初学者谁不知道或不关心将落入这个陷阱。好的。这是不必要的。与Java和C语言不同的是,使用EDCOX1的1个关键字关键字是通用的,在C++中,如果需要的话,只应该使用它。俗话说,如果你有锤子,一切看起来都像钉子。而初学者从C++开始害怕指针并学会习惯使用堆栈变量,Java和C程序员开始使用指针而不理解它。这简直是走错了路。你必须放弃你所知道的一切,因为语法是一回事,学习语言是另一回事。好的。
1. (N)RVO - Aka, (Named) Return Value Optimization
Ok.
许多编译器所做的一个优化叫做省略和返回值优化。这些东西可以避免不必要的复制,这对于非常大的对象很有用,例如包含许多元素的向量。通常的做法是使用指针来转移所有权,而不是复制大型对象来移动它们。这导致了移动语义和智能指针的出现。好的。
如果使用指针,(n)不会发生RVO。如果您担心优化,那么利用(n)rvo而不是返回或传递指针更有益,也更不容易出错。如果函数的调用者负责
C++给出了传递对象的三种方法:指针、引用和值。Java限制了后者(唯一的例外是像INT、BooLoint等的原始类型)。如果你想使用C++而不仅仅是一个奇怪的玩具,那么你最好知道这三种方法的区别。
Java假装没有这样的问题:"谁和什么时候应该毁掉这个?"答案是:垃圾收集器,伟大而可怕。然而,它不能提供100%个防止内存泄漏的保护(是的,Java可以泄漏内存)。实际上,GC给了你一种错误的安全感。你的SUV越大,去疏散中心的路就越长。
C++让你面对对象的生命周期管理。嗯,有一些方法可以解决这个问题(智能指针系列、qt中的qObject等等),但是它们都不能像gc那样以"失火而忘记"的方式使用:您应该时刻记住内存处理。不仅要注意销毁一个对象,还必须避免多次销毁同一个对象。
还不害怕?好的:循环引用-你自己处理它们,人类。记住:每一个对象都精确地杀死一次,我们C++运行时不喜欢那些乱扔尸体的人,只留下死人。
所以,回到你的问题上来。
当您通过值传递对象时,而不是通过指针或引用传递对象时,您将复制该对象(整个对象,无论是几个字节还是一个巨大的数据库转储—您足够聪明,可以避免后者,是吗?)每次你做"="。要访问对象的成员,请使用"."(点)。
当您通过指针传递对象时,只复制几个字节(32位系统上4个,64位系统上8个),也就是这个对象的地址。为了向所有人展示这一点,当您访问成员时,可以使用这个花哨的"->"操作符。或者可以使用"*"和"."的组合。
当你使用引用时,你会得到一个假装是值的指针。它是一个指针,但您可以通过"."访问成员。
并且,再次打动你的头脑:当你声明用逗号分隔的几个变量时,那么(注意手):
- 每个人都有自己的类型
- 值/指针/引用修饰符是单独的
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 |
But I can't figure out why should we use it like this?
我将比较它在函数体内部的工作方式,如果您使用:
1 | Object myObject; |
在函数内部,一旦函数返回,您的
如果在函数体中写入:
1 | Object *myObject = new Object; |
那么,由
现在,如果你是Java程序员,那么第二个例子更接近于如何在Java下进行对象分配。这一行:EDCOX1〔2〕相当于Java:EDCX1〔3〕。不同之处在于,在Java MyObjor下会得到垃圾回收,而C++下它将不被释放,必须在某处显式调用"删除MyObjor";否则将引入内存泄漏。
由于C++ 11,您可以使用安全分配的动态分配方式:EDCOX1 OR 4,通过在SydDypTr/UnQuyQ-PTR中存储值。
1 2 3 4 | std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared"); // since c++14 std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); |
此外,对象通常存储在容器中,如map-s或vector-s,它们将自动管理对象的生命周期。
在C++中,在堆栈上分配的对象(使用块中的EDCOX1×0)语句将只存在于声明的范围内。当代码块完成执行时,声明的对象将被销毁。然而,如果您使用
当我不只是在声明/分配对象的代码块中使用对象时,我会在堆上创建一个对象。
从技术上讲,这是一个内存分配问题,但是这里有两个更实际的方面。这与两件事有关:1)作用域,当您定义一个没有指针的对象时,在定义它的代码块之后,您将无法再访问它,而如果您定义一个带有"new"的指针,那么您可以从任何有指向该内存的指针的地方访问它,直到您对同一指针调用"delete"。2)如果要将参数传递给函数,则需要传递指针或引用,以提高效率。当您传递一个对象时,对象就会被复制,如果这是一个使用大量内存的对象,这可能会占用CPU(例如,您复制一个充满数据的向量)。当您传递一个指针时,所有传递的都是一个int(取决于实现,但大多数是一个int)。
除此之外,您需要了解"new"在堆上分配了在某个时刻需要释放的内存。当您不必使用"new"时,我建议您在堆栈上使用常规的对象定义。
主要的问题是,为什么我应该使用指针而不是对象本身?我的答案是,你应该(几乎)从不使用指针而不是对象,因为C++有引用,它比指针更安全,并保证了与指针相同的性能。
你在问题中提到的另一件事:
1 | Object *myObject = new Object; |
它是如何工作的?它创建
1 | delete myObject; |
这调用析构函数并释放内存看起来很容易,但是在大型项目中,可能很难检测是否有一个线程释放了内存,但是为了这个目的,您可以尝试共享指针,这些指针会稍微降低性能,但是使用它们要容易得多。
现在一些介绍结束了,回到问题上来。
在函数之间传输数据时,可以使用指针而不是对象来获得更好的性能。
看一看,您有
我更想说的是,无论您选择哪种分配方式(使用
还有一件事要提,当您以常规方式创建对象时,您可以在堆栈中分配内存,但是当您使用
假设你有一个包含
但是要小心动态物体
使用指向对象的指针有很多好处-
这已经被讨论过了,但是在Java中,所有的东西都是指针。它不区分堆栈和堆分配(所有对象都分配在堆上),所以您不会意识到您正在使用指针。在C++中,根据内存需求,可以将两者混合。在C++(Duh)中,性能和内存使用更具确定性。
1 | Object *myObject = new Object; |
这样做将创建对对象(堆上)的引用,必须显式删除该对象以避免内存泄漏。
1 | Object myObject; |
这样做将创建自动类型的对象(myObject)(在堆栈上),当对象(myObject)超出范围时,该对象将自动删除。
指针直接引用对象的内存位置。Java没有这样的东西。Java有引用哈希表引用对象位置的引用。在这些Java中,你不能像Java中的指针算法那样做任何事情。
为了回答你的问题,这只是你的偏好。我更喜欢使用类似Java的语法。
带指针,
可以直接和记忆通话。
可以通过操纵指针来防止程序的大量内存泄漏。
我将包括一个重要的指针用例。当您在基类中存储一些对象时,它可能是多态的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Class Base1 { }; Class Derived1 : public Base1 { }; Class Base2 { Base *bObj; virtual void createMemerObects() = 0; }; Class Derived2 { virtual void createMemerObects() { bObj = new Derived1(); } }; |
所以在这种情况下,不能将bobj声明为直接对象,必须有指针。
在内存利用率很高的领域,指针很方便。例如,考虑一个minimax算法,在该算法中,将使用递归例程生成数千个节点,然后使用它们来评估游戏中的下一个最佳移动,取消分配或重置(如智能指针)的能力显著降低了内存消耗。而非指针变量继续占用空间,直到其递归调用返回值为止。
使用指针的一个原因是与C函数接口。另一个原因是保存内存;例如:不要将包含大量数据且具有处理器密集型复制构造函数的对象传递给函数,只需传递指向该对象的指针,这样可以节省内存和速度,尤其是在循环中时,但是在这种情况下,引用会更好,除非使用C样式数组。
"需要是发明之母。"我想指出的最重要的区别是我自己的编码经验的结果。有时需要将对象传递给函数。在这种情况下,如果您的对象是一个非常大的类,那么作为对象传递它将复制它的状态(您可能不想这样做..并且开销可能很大),从而导致复制对象的开销。而指针是固定的4字节大小(假定为32位)。上面已经提到了其他原因…
已经有很多很好的答案了,但让我举一个例子:
我有一个简单的项目类:
1 2 3 4 5 6 7 | class Item { public: std::string name; int weight; int price; }; |
我做了一个向量来容纳它们。
我创建了一百万个项目对象,并将它们推回到向量上。我按名称对向量进行排序,然后对特定的项名称进行简单的迭代二进制搜索。我测试程序,完成执行需要8分钟以上。然后我像这样改变我的库存向量:
…并通过new创建我的百万个项目对象。我对代码所做的唯一更改是使用指向项的指针,除了在末尾为内存清理添加的循环。这个程序在40秒内运行,或者比10倍的速度提高要好。编辑:代码位于http://pastebin.com/dk24spew通过编译器优化,我刚刚测试过的机器只增加了3.4倍,这仍然相当可观。