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




为什么要用指针 (18)

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

Object *myObject = new Object;

而不是:

Object myObject;

或者用一个函数,比如说testFunc() ,就像这样:

myObject.testFunc();

我们必须写:

myObject->testFunc();

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


从技术上讲,这是一个内存分配问题,但是这里有两个更实际的方面。 它与两件事有关:1)范围,当你定义一个没有指针的对象时,你将不能在它定义的代码块后访问它,而如果你定义了一个指向“新”的指针,那么你can access it from anywhere you have a pointer to this memory until you call "delete" on the same pointer. 2) If you want to pass arguments to a function you want to pass a pointer or a reference in order to be more efficient. When you pass an Object then the object is copied, if this is an object that uses a lot of memory this might be CPU consuming (eg you copy a vector full of data). When you pass a pointer all you pass is one int (depending of implementation but most of them are one 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".


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


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

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


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.


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:

  1. 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.
  2. void foo(std::string* xml); In this case you will pass pointer to object, same speed as passing size_t variable, however this declaration has error prone, because you can pass NULL pointer or invalid pointer. Pointers usually used in C because it doesn't have references.
  3. 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).
  4. void foo(const std::string* xml); Here is the same as second, just pointer value cannot be changed.
  5. 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 , but usually this issue is solved using STL containers and remember std::string is also container, some guys forgot it :)


I will include one important use case of pointer. When you are storing some object in the base class, but it could be polymorphic.

Class Base1 {
};

Class Derived1 : public Base1 {
};


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

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

So in this case you can't declare bObj as an direct object, you have to have pointer.


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 -

  1. Efficiency (as you already pointed out). Passing objects to functions mean creating new copies of object.
  2. 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).
  3. if the object owns a resource and you want that the ownership should not be sahred with other objects.

前言

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::string来替代, std::string内部调整自己的大小,这应该不是问题。 但本质上解决这个问题的办法是动态分配。 您可以根据用户的输入分配动态内存,例如:

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; 如果打算在仅应用于特定类型的同质容器上调用函数,这非常有用。 该功能可以通过以下类似的方式实现:

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失败。

自动变量的好处

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

  • 这是容易出错的。 手动内存分配是危险的,你很容易泄漏。 如果您不熟练使用调试器或valgrind (内存泄漏工具),您可能会将头发拉出头部。 幸运的是,RAII成语和智能指针缓解了这一点,但您必须熟悉“三定律”和“五法则”等做法。 这是很多的信息,并且不知道或不在乎的初学者会陷入这个陷阱。

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

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

很多编译器做的一个优化就是所谓的elision返回值优化 。 这些东西可以避免不必要的复制对于非常大的对象很有用,例如包含很多元素的矢量。 通常的做法是使用指针来转移所有权,而不是复制大对象来移动它们。 这导致了移动语义智能指针的出现

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


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


In areas where memory utilization is at its premium , pointers comes handy. For example consider a minimax algorithm, where thousands of nodes will be generated using recursive routine, and later use them to evaluate the next best move in game, ability to deallocate or reset (as in smart pointers) significantly reduces memory consumption. Whereas the non-pointer variable continues to occupy space till it's recursive call returns a value.


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).


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


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

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

Object myObject;

在函数内部,一旦这个函数返回,你的myObject就会被销毁。 所以这是有用的,如果你不需要你的功能之外的对象。 这个对象将被放在当前线程堆栈上。

如果你在函数体内写入:

 Object *myObject = new Object;

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

现在,如果你是java程序员,那么第二个例子更接近java下的对象分配。 这一行: Object *myObject = new Object; 相当于java: Object myObject = new Object(); 。 不同的是,在java下myObject会得到垃圾回收,而在c ++下它不会被释放,你必须在某处明确调用`delete myObject;' 否则你会引入内存泄漏。

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

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,它们将自动管理对象的生命周期。


指针有很多用例。

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

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中的Q&A

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


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...


C ++为您提供了三种传递对象的方法:通过指针,引用和值。 Java使用后者来限制你(唯一的例外是基本类型,如int,boolean等)。 如果你想使用C ++而不仅仅是一个奇怪的玩具,那么你最好了解这三种方法之间的区别。

Java假设没有像'谁和什么时候应该销毁这个'这样的问题'。 答案是:垃圾收集者,非常可怕。 尽管如此,它不能提供100%的内存泄漏保护(是的, Java 可以泄漏内存 )。 事实上,GC给你一种错误的安全感。 你的SUV越大,你的撤离路线越长。

C ++让你面对面的对象的生命周期管理。 那么,有办法处理这个问题( 智能指针系列,Qt中的QObject等),但是没有一个可以用于像GC那样的“放手而忘记”的方式:您应该始终记住内存处理。 你不仅要关心销毁一个物体,还必须避免多次销毁同一个物体。

还没有害怕? 好的:循环引用 - 自己处理它们,人类。 请记住:杀死每个对象一次,我们的C ++运行时不喜欢那些混淆尸体的人,让死亡的人独自一人。

所以,回到你的问题。

当你通过价值传递你的对象,而不是通过指针或引用,你复制对象(整个对象,无论是几个字节或一个巨大的数据库转储 - 你很聪明,以避免后者,aren'你呢?)每次你做'='。 要访问对象的成员,可以使用'。' (点)。

当您通过指针传递对象时,只需复制几个字节(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




c++11