为什么我应该使用指针而不是对象本身?
我来自Java背景,已经开始使用C ++中的对象。 但有一件事发生在我身上,人们经常使用指向对象的指针而不是对象本身,例如这个声明:
Object *myObject = new Object;
而不是:
Object myObject;
或者使用一个函数,比如testFunc()
,就像这样:
myObject.testFunc();
我们必须写:
myObject->testFunc();
但是我不明白为什么要这样做。 我会假设它与效率和速度有关,因为我们可以直接访问内存地址。 我对吗?
非常不幸的是,你经常看到dynamic分配。 这只是表明有多less不好的C ++程序员。
从某种意义上说,你有两个问题捆绑在一起。 首先是什么时候应该使用dynamic分配(使用new
)? 第二个是我们应该什么时候用指针?
重要的信息是,你应该总是使用适当的工具来完成这项工作 。 在几乎所有的情况下,比手动dynamic分配和/或使用原始指针更合适和安全。
dynamic分配
在你的问题中,你已经演示了两种创build对象的方法。 主要区别是对象的存储时间。 当做Object myObject;
在一个块内,对象被创build为自动存储持续时间,这意味着它将在超出范围时被自动销毁。 当您执行new Object()
,该对象具有dynamic存储持续时间,这意味着它保持活动状态直到您明确地delete
它。 您只需要使用dynamic存储持续时间。 也就是说, 你应该总是喜欢创build具有自动存储持续时间的对象 。
您可能需要dynamic分配的主要两种情况:
- 你需要这个对象超出当前的范围 – 特定的对象在那个特定的内存位置,而不是它的副本。 如果您可以复制/移动对象(大部分时间您应该),您应该更喜欢自动对象。
- 你需要分配大量的内存 ,这很容易填满堆栈。 如果我们不需要关心这些(大部分时候你不需要),那将是非常好的,因为它实际上超出了C ++的范围,但不幸的是我们必须处理系统的实际情况。为…开发。
当你绝对需要dynamic分配的时候,你应该把它封装在一个智能指针或其他执行RAII的types(比如标准容器)中。 智能指针提供dynamic分配对象的所有权语义。 例如,看看std::unique_ptr
和std::shared_ptr
。 如果适当地使用它们,几乎可以完全避免执行自己的内存pipe理(请参阅“零规则” )。
指针
但是,除了dynamic分配之外,还有其他更普遍的用于原始指针的用途,但是大多数用途都是您应该更喜欢的。 和以前一样, 除非你真的需要指针,否则总是比较喜欢这个select
-
你需要引用语义 。 有时候你想用一个指针传递一个对象(不pipe它是如何分配的),因为你想要传递给它的函数能够访问那个特定的对象(不是它的一个副本)。 但是,在大多数情况下,您应该更喜欢引用types的指针,因为这是他们专门devise的。 注意,这不一定是关于延长对象的生命周期超出当前的范围,如上面的情况1。 和以前一样,如果传递一个对象的副本没问题,则不需要引用语义。
-
你需要多态 。 您只能通过指针或对象的引用多态调用函数(即根据对象的dynamictypes)。 如果这是你需要的行为,那么你需要使用指针或引用。 同样,参考应该是首选。
-
您希望通过在省略对象时允许传递
nullptr
来表示对象是可选的。 如果是参数,则应该使用默认参数或函数重载。 否则,您应该更喜欢使用封装此行为的types,如std::optional
(在C ++ 17中引入 – 使用较早的C ++标准,使用boost::optional
)。 -
你想分离编译单元来提高编译时间 。 指针的有用属性是只需要指向types的前向声明(实际使用该对象,您将需要一个定义)。 这使您可以将编译过程的某些部分分开,这可能会显着缩短编译时间。 见Pimpl成语 。
-
您需要与C库或C风格的库进行交互 。 在这一点上,你不得不使用原始指针。 你可以做的最好的事情是确保你只在最后一刻让你的指针松动。 您可以从智能指针获取原始指针,例如,使用其
get
成员函数。 如果一个库为您执行了一些您希望通过句柄来释放的分配,那么通常可以用一个自定义的删除器将该句柄包装在一个智能指针中,该指针将适当地释放该对象。
指针有很多用例。
多态行为 。 对于多态types,使用指针(或引用)来避免切片:
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
引用语义并避免复制 。 对于非多态types,指针(或引用)将避免复制潜在的昂贵对象
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
运算符创build指向资源的指针是现代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 }
原始指针只能用作“视图”,并不以任何方式参与所有权,无论是通过直接创build还是隐式通过返回值。 另请参阅C ++ FAQ中的Q&A 。
更细粒度的生命期控制每当共享指针被复制时(例如作为函数参数),指向的资源就会保持活跃状态。 规则对象(不是由new
创build的,或者直接由你或者在资源类中创build的)在超出范围时被销毁。
这个问题有很多很好的答案,包括前向声明,多态性等重要的用例,但是我觉得你的问题的“灵魂”的一部分没有得到回答,即不同的语法在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", ie 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...
最好的办法就是 – 或多或less – Java(隐式地)处理指向对象的指针,而C ++可以处理指向对象的指针或对象本身。 也有例外 – 例如,如果声明Java“原始”types,它们是被复制的实际值,而不是指针。 所以,
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的语法的原因之一。
我们开始
但是我不明白为什么要这样做。 我会假设它与效率和速度有关,因为我们可以直接访问内存地址。 我对吗?
实际上相反。 堆比堆栈慢得多,因为与堆相比,堆栈非常简单。 自动存储variables(也称为堆栈variables)一旦超出范围就会调用它们的析构函数。 例如:
{ std::string s; } // s is destroyed here
另一方面,如果使用dynamic分配的指针,则必须手动调用其析构函数。 delete
为你调用这个析构函数。
{ std::string* s = new std::string; } delete s; // destructor called
这与C#和Java中stream行的new
语法无关。 它们被用于完全不同的目的。
dynamic分配的好处
1.你不必事先知道数组的大小
许多C ++程序员遇到的第一个问题是,当他们接受用户的任意input时,只能为堆栈variables分配一个固定的大小。 你不能改变数组的大小。 例如:
char buffer[100]; std::cin >> buffer; // bad input = buffer overflow
当然,如果你使用std::string
来替代, std::string
内部调整自己的大小,这应该不成问题。 但基本上这个问题的解决scheme是dynamic分配。 您可以根据用户的input分配dynamic内存,例如:
int * pointer; std::cout << "How many items do you need?"; std::cin >> n; pointer = new int[n];
附注 :许多初学者犯的一个错误是使用可变长度数组。 这是一个GNU扩展,也是Clang中的一个,因为它们反映了许多GCC的扩展。 所以下面的
int arr[n]
不应该被依赖。
由于堆比堆栈大得多,因此可以随意分配/重新分配尽可能多的内存,而堆栈则有其局限性。
数组不是指针
这是你问的好处? 一旦你理解了数组和指针背后的混淆/神话,答案就会变得清晰。 人们通常认为它们是相同的,但它们并不相同。 这个神话来自这样一个事实,即指针可以像数组一样下标,因为数组会衰减到函数声明中顶层的指针。 但是,一旦数组衰减到指针,指针就会丢失其信息的sizeof
。 因此, sizeof(pointer)
将以字节为单位给出sizeof(pointer)
的大小,通常在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
关键字。 所以如果有人想把一个Entity
对象当作一个Player
对象,可以将Player player = Entity as Player;
如果打算在仅适用于特定types的同质容器上调用函数,这非常有用。 该function可以通过以下类似的方式实现:
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有一个旋转函数,如果你试图在类的所有对象上调用它,将会是一个编译器错误。 使用dynamic_cast
,你可以模拟as
关键字。 要清楚的是,如果一个强制转换失败,它将返回一个无效的指针。 所以!test
本质上是检查test
是NULL还是一个无效指针的简写,这意味着cast失败。
自动variables的好处
在看到dynamic分配能做的所有伟大的事情之后,你可能想知道为什么没有人不总是使用dynamic分配? 我已经告诉过你一个理由,堆很慢。 如果你不需要所有的记忆,你不应该滥用它。 所以这里有一些缺点,没有特别的顺序:
-
这是错误的倾向。 手动内存分配是危险的,你很容易泄漏。 如果您不熟练使用debugging器或
valgrind
(内存泄漏工具),您可能会将头发拉出头部。 幸运的是,RAII成语和智能指针可以缓解这一点,但是您必须熟悉“三定律”和“五定律”等实践。 这是一个很多的信息,而不知道或不在乎的初学者会陷入这个陷阱。 -
没有必要。 与Java和C#不同的是,在任何地方使用
new
关键字,在C ++中,只有在需要的时候才应该使用它。 常见的一句话是,如果你有一把锤子,一切看起来都像钉子。 而以C ++开始的初学者害怕使用指针并习惯性地使用堆栈variables,而Java和C#程序员开始使用指针而不理解它! 这实际上是走错了路。 你必须放弃你所知道的一切,因为语法是一回事,学习语言是另一回事。
1.(N)RVO – 又名,(名称)返回值优化
许多编译器做的一个优化就是所谓的elision和返回值优化 。 这些东西可以避免不必要的复制对于非常大的对象很有用,比如包含很多元素的向量。 通常的做法是使用指针来传递所有权,而不是复制大对象来移动它们。 这导致了移动语义和智能指针的出现 。
如果您使用指针,则(N)RVO不会发生。 如果您担心优化,那么利用(N)RVO而不是返回或传递指针会更有利,更不容易出错。 如果函数的调用者负责delete
dynamic分配的对象等,就可能发生错误泄漏。 如果指针像热土豆一样被传递,那么跟踪对象的所有权就很困难。 只是使用栈variables,因为它更简单,更好。
C ++为您提供了三种传递对象的方法:通过指针,引用和值。 Java限制你后者(唯一的例外是像int,布尔等原始types)。 如果你想使用C ++而不是像一个奇怪的玩具,那么你最好弄清楚这三种方式之间的区别。
Java假设没有“谁应该摧毁这个?”这样的问题。 答案是:垃圾收集器,伟大而可怕的。 不过,它不能提供100%的内存泄漏保护(是的, Java 可以泄漏内存 )。 事实上,GC给你一个错误的安全感。 你的SUV越大,你的撤离路线越长。
C ++让你面对面的对象的生命周期pipe理。 那么有办法处理这个问题( 智能指针系列,Qt中的QObject等等),但是没有一个可以像GC那样用'fire and forget'方式:你应该时刻记住内存的处理。 你不仅要关心摧毁一个对象,还必须避免多次摧毁同一个对象。
还没害怕? 好的:循环引用 – 自己处理它们,人类。 记住:杀死每个对象一次,我们的C ++运行时不喜欢那些混淆尸体,离开死亡的人。
所以,回到你的问题。
当你通过价值传递你的对象,而不是通过指针或引用,你复制对象(整个对象,无论是几个字节或一个巨大的数据库转储 – 你很聪明,以避免后者,aren'你呢?)每次你做'='。 要访问对象的成员,可以使用'。' (点)。
当您通过指针传递对象时,只需复制几个字节(32位系统上的4个,64位上的8个),即该对象的地址。 为了向大家展示这一点,您在访问会员时使用这个奇特的“ – >”操作符。 或者,您可以使用“*”和“。”的组合。
当你使用引用时,你会得到假装为一个值的指针。 这是一个指针,但是你通过'。'来访问成员。
而且,再次打击你的思想:当你声明多个由逗号分隔的variables时,(注意手):
- types是给予每个人的
- 值/指针/参考修饰符是单独的
例:
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
在C ++中,在堆栈上分配的Object object;
(使用Object object;
块内的语句)只能在它们声明的范围内。当代码块完成执行时,声明的对象被销毁。 而如果你在堆上分配内存,使用Object* obj = new Object()
,它们将继续存在于堆中,直到调用delete obj
为止。
当我喜欢不仅在声明/分配它的代码块中使用对象时,我会在堆上创build一个对象。
但我不明白为什么我们应该这样使用它?
我将比较它在函数体内的工作原理,如果你使用:
Object myObject;
里面的函数,一旦这个函数返回,你的myObject
就会被销毁。 所以这是有用的,如果你不需要你的function之外的对象。 这个对象将被放在当前线程堆栈上。
如果你在函数体内部写入:
Object *myObject = new Object;
那么一旦函数结束, myObject
指向的Object类实例将不会被销毁,并且分配在堆上。
现在如果你是java程序员,那么第二个例子更接近java下的对象分配。 这一行: Object *myObject = new Object;
相当于java: Object myObject = new Object();
。 不同的是,在java下,myObject会得到垃圾回收,而在c ++下它不会被释放,你必须在某处明确地调用`delete myObject;' 否则你会引入内存泄漏。
由于c ++ 11,您可以使用安全的dynamic分配方式: new Object
,通过将值存储在shared_ptr / unique_ptr中。
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,它们会自动pipe理对象的生命周期。
从技术上讲,这是一个内存分配问题,但是这里有两个更实际的方面。 它与两件事有关:1)作用域,当你定义一个没有指针的对象时,你将不能在它定义的代码块之后访问它,而如果你定义一个指向“新”的指针,那么你可以在任何有指针的地方访问它,直到你在同一个指针上调用“delete”为止。 2)如果你想传递一个函数的参数,你想传递一个指针或引用,以提高效率。 当你传递一个对象时,对象被复制,如果这是一个使用大量内存的对象,这可能是CPU消耗(例如,你复制一个向量充满数据)。 当你传递一个指针时,你传递的是一个int(取决于实现,但大多数是一个int)。
Other than that you need to understand that "new" allocates memory on the heap that needs to be freed at some point. When you don't have to use "new" I suggest you use a regular object definition "on the stack".
Let's say that you have class A
that contain class B
When you want to call some function of class B
outside class A
you will simply obtain a pointer to this class and you can do whatever you want and it will also change context of class B
in your class A
But be careful with dynamic object
There are many benefits of using pointers to object –
- Efficiency (as you already pointed out). Passing objects to functions mean creating new copies of object.
- Working with objects from third party libraries. If your object belongs to a third party code and the authors intend the usage of their objects through pointers only (no copy constructors etc) the only way you can pass around this object is using pointers. Passing by value may cause issues. (Deep copy / shallow copy issues).
- if the object owns a resource and you want that the ownership should not be sahred with other objects.
Well the main question is Why should I use a pointer rather than the object itself? And my answer, you should (almost) never use pointer instead of object, because C++ has references , it is safer then pointers and guarantees the same performance as pointers.
Another thing you mentioned in your question:
Object *myObject = new Object;
它是如何工作的? It creates pointer of Object
type, allocates memory to fit one object and calls default constructor, sounds good, right? But actually it isn't so good, if you dynamically allocated memory (used keyword new
), you also have to free memory manually, that means in code you should have:
delete myObject;
This calls destructor and frees memory, looks easy, however in big projects may be difficult to detect if one thread freed memory or not, but for that purpose you can try shared pointers , these slightly decreases performance, but it is much easier to work with them.
And now some introduction is over and go back to question.
You can use pointers instead of objects to get better performance while transferring data between function.
Take a look, you have std::string
(it is also object) and it contains really much data, for example big XML, now you need to parse it, but for that you have function void foo(...)
which can be declarated in different ways:
-
void foo(std::string xml);
In this case you will copy all data from your variable to function stack, it takes some time, so your performance will be low. -
void foo(std::string* xml);
In this case you will pass pointer to object, same speed as passingsize_t
variable, however this declaration has error prone, because you can passNULL
pointer or invalid pointer. Pointers usually used inC
because it doesn't have references. -
void foo(std::string& xml);
Here you pass reference, basically it is the same as passing pointer, but compiler does some stuff and you cannot pass invalid reference (actually it is possible to create situation with invalid reference, but it is tricking compiler). -
void foo(const std::string* xml);
Here is the same as second, just pointer value cannot be changed. -
void foo(const std::string& xml);
Here is the same as third, but object value cannot be changed.
What more I want to mention, you can use these 5 ways to pass data no matter which allocation way you have chosen (with new
or regular ).
Another thing to mention, when you create object in regular way, you allocate memory in stack, but while you create it with new
you allocate heap. It is much faster to allocate stack, but it is kind a small for really big arrays of data, so if you need big object you should use heap, because you may get stack overflow, but usually this issue is solved using STL containers and remember std::string
is also container, some guys forgot it 🙂
This is has been discussed at length, but in Java everything is a pointer. It makes no distinction between stack and heap allocations (all objects are allocated on the heap), so you don't realize you're using pointers. In C++, you can mix the two, depending on your memory requirements. Performance and memory usage is more deterministic in C++ (duh).
Suppose you want to visit your friends home, Tell him to bring his home to your place. This is Passing object itself.
Then ask him his home address, he will give you the address. Now you can visit his home this is Pass by Pointer.
getting address from your friend is easier than copying his home everywhere. this is why you should use pointers to objects.
A pointer directly references the memory location of an object. Java has nothing like this. Java has references that reference the location of object through hash tables. You cannot do anything like pointer arithmetic in Java with these references.
To answer your question, it's just your preference. I prefer using the Java-like syntax.
You shouldn't . People (many people, sadly) write it out of ignorance.
Sometimes dynamic allocation has its place but, in the examples you give, it is wrong .
If you want to think about efficiency, then this is worse , because it introduces indirection for no good reason. This sort of programming is slower and more error-prone .
Object *myObject = new Object;
Doing this will create a reference to an Object (on the heap) which has to be deleted explicitly to avoid memory leak .
Object myObject;
Doing this will create an object(myObject) of the automatic type (on the stack) that will be automatically deleted when the object(myObject) goes out of scope.
"Necessity is the mother of invention." The most of important difference that I would like to point out is the outcome of my own experience of coding. Sometimes you need to pass objects to functions . In that case if your object is of a very big class then passing it as an object will copy its state (which you might not want ..AND CAN BE BIG OVERHEAD) thus resulting in overhead of copying object .while pointer is fixed 4 byte size (assuming 32 bit).Other reasons are already mentioned above…
With pointers ,
-
can directly talk to the memory.
-
can prevent lot of memory leaks of a program by manipulating pointers.
One reason for using pointers is to interface with C functions. Another reason is to save memory; for example: instead of passing an object which contains a lot of data and has a processor-intensive copy-constructor to a function, just pass a pointer to the object, saving memory and speed especially if you're in a loop, however a reference would be better in that case, unless you're using an C-style array.
There are many excellent answers already, but let me give you one example:
I have an simple Item class:
class Item { public: std::string name; int weight; int price; };
I make a vector to hold a bunch of them.
std::vector<Item> inventory;
I create one million Item objects, and push them back onto the vector. I sort the vector by name, and then do a simple iterative binary search for a particular item name. I test the program, and it takes over 8 minutes to finish executing. Then I change my inventory vector like so:
std::vector<Item *> inventory;
…and create my million Item objects via new. The ONLY changes I make to my code are to use the pointers to Items, excepting a loop I add for memory cleanup at the end. That program runs in under 40 seconds, or better than a 10x speed increase. EDIT: The code is at http://pastebin.com/DK24SPeW With compiler optimizations it shows only a 3.4x increase on the machine I just tested it on, which is still considerable.