Why not use pointers for everything in C++?

2020-01-24 19:11发布

Suppose that I define some class:

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}

Then write some code using it. Why would I do the following?

Pixel p;
p.x = 2;
p.y = 5;

Coming from a Java world I always write:

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;

They basically do the same thing, right? One is on the stack while the other is on the heap, so I'll have to delete it later on. Is there any fundamental difference between the two? Why should I prefer one over the other?

23条回答
【Aperson】
2楼-- · 2020-01-24 19:32

Yes, one is on the stack, the other on the heap. There are two important differences:

  • First, the obvious, and less important one: Heap allocations are slow. Stack allocations are fast.
  • Second, and much more important is RAII. Because the stack-allocated version is automatically cleaned up, it is useful. Its destructor is automatically called, which allows you to guarantee that any resources allocated by the class get cleaned up. This is essentialy how you avoid memory leaks in C++. You avoid them by never calling delete yourself, instead wrapping it in stack-allocated objects which call delete internally, typicaly in their destructor. If you attempt to manually keep track of all allocations, and call delete at the right times, I guarantee you that you'll have at least a memory leak per 100 lines of code.

As a small example, consider this code:

class Pixel {
public:
  Pixel(){ x=0; y=0;};
  int x;
  int y;
};

void foo() {
  Pixel* p = new Pixel();
  p->x = 2;
  p->y = 5;

  bar();

  delete p;
}

Pretty innocent code, right? We create a pixel, then we call some unrelated function, and then we delete the pixel. Is there a memory leak?

And the answer is "possibly". What happens if bar throws an exception? delete never gets called, the pixel is never deleted, and we leak memory. Now consider this:

void foo() {
  Pixel p;
  p.x = 2;
  p.y = 5;

  bar();
}

This won't leak memory. Of course in this simple case, everything is on the stack, so it gets cleaned up automatically, but even if the Pixel class had made a dynamic allocation internally, that wouldn't leak either. The Pixel class would simply be given a destructor that deletes it, and this destructor would be called no matter how we leave the foo function. Even if we leave it because bar threw an exception. The following, slightly contrived example shows this:

class Pixel {
public:
  Pixel(){ x=new int(0); y=new int(0);};
  int* x;
  int* y;

  ~Pixel() {
    delete x;
    delete y;
  }
};

void foo() {
  Pixel p;
  *p.x = 2;
  *p.y = 5;

  bar();
}

The Pixel class now internally allocates some heap memory, but its destructor takes care of cleaning it up, so when using the class, we don't have to worry about it. (I should probably mention that the last example here is simplified a lot, in order to show the general principle. If we were to actually use this class, it contains several possible errors too. If the allocation of y fails, x never gets freed, and if the Pixel gets copied, we end up with both instances trying to delete the same data. So take the final example here with a grain of salt. Real-world code is a bit trickier, but it shows the general idea)

Of course the same technique can be extended to other resources than memory allocations. For example it can be used to guarantee that files or database connections are closed after use, or that synchronization locks for your threading code are released.

查看更多
一夜七次
3楼-- · 2020-01-24 19:34

The first case is not always stack allocated. If it's part of an object, it'll be allocated wherever the object is. For example:

class Rectangle {
    Pixel top_left;
    Pixel bottom_right;
}

Rectangle r1; // Pixel is allocated on the stack
Rectangle *r2 = new Rectangle(); // Pixel is allocated on the heap

The main advantages of stack variables are:

  • You can use the RAII pattern to manage objects. As soon as the object goes out of scope, it's destructor is called. Kind of like the "using" pattern in C#, but automatic.
  • There's no possibility of a null reference.
  • You don't need to worry about manually managing the object's memory.
  • It causes fewer memory allocations. Memory allocations, particularly small ones, are likely to be slower in C++ than Java.

Once the object's been created, there's no performance difference between an object allocated on the heap, and one allocated on the stack (or wherever).

However, you can't use any kind of polymorphism unless you're using a pointer - the object has a completely static type, which is determined at compile time.

查看更多
唯我独甜
4楼-- · 2020-01-24 19:34

Why not use pointers for everything?

They're slower.

Compiler optimizations will not be as effective with pointer access symantics, you can read up about it in any number of web sites, but here's a decent pdf from Intel.

Check pages, 13,14,17,28,32,36;

Detecting unnecessary memory references in the loop notation:

for (i = j + 1; i <= *n; ++i) { 
X(i) -= temp * AP(k); } 

The notation for the loop boundaries contains the pointer or memory reference. The compiler does not have any means to predict whether the value referenced by pointer n is being changed with loop iterations by some other assignment. This uses the loop to reload the value referenced by n for each iteration. The code generator engine also may deny scheduling a software pipelined loop when potential pointer aliasing is found. Since the value referenced by pointer n is not anging within the loop and it is invariant to the loop index, the loading of *n s to be carried outside of the loop boundaries for simpler scheduling and pointer disambiguation.

... a number of variations on this theme....

Complex memory references. Or in other words, analyzing references such as complex pointer computations, strain the ability of compilers to generate efficient code. Places in the code where the compiler or the hardware is performing a complex computation in order to determine where the data resides, should be the focus of attention. Pointer aliasing and code simplification assist the compiler in recognizing memory access patterns, allowing the compiler to overlap memory access with data manipulation. Reducing unnecessary memory references may expose to the compiler the ability to pipeline the software. Many other data location properties, such as aliasing or alignment, can be easily recognized if memory reference computations are kept simple. Use of strength reduction or inductive methods to simplify memory references is crucial to assisting the compiler.

查看更多
爱情/是我丢掉的垃圾
5楼-- · 2020-01-24 19:35

My gut reaction is just to tell you that this could lead to serious memory leaks. Some situations in which you might be using pointers could lead to confusion about who should be responsible for deleting them. In simple cases such as your example, it's easy enough to see when and where you should call delete, but when you start passing pointers between classes, things can get a little more difficult.

I'd recommend looking into the boost smart pointers library for your pointers.

查看更多
我只想做你的唯一
6楼-- · 2020-01-24 19:38

"Why not use pointers for everything in C++"

One simple answer - because it becomes a huge problem managing the memory - allocating and deleting/freeing.

Automatic/stack objects remove some of the busy work of that.

that is just the first thing that I would say about the question.

查看更多
登录 后发表回答