Will this C++ code cause a memory leak (casting array new)

后端 未结 24 3053
暗喜
暗喜 2021-02-14 12:26

I have been working on some legacy C++ code that uses variable length structures (TAPI), where the structure size will depend on variable length strings. The structures are allo

相关标签:
24条回答
  • 2021-02-14 12:47

    The behaviour of the code is undefined. You may be lucky (or not) and it may work with your compiler, but really that's not correct code. There's two problems with it:

    1. The delete should be an array delete [].
    2. The delete should be called on a pointer to the same type as the type allocated.

    So to be entirely correct, you want to be doing something like this:

    delete [] (BYTE*)(pStruct);
    
    0 讨论(0)
  • 2021-02-14 12:47

    The C++ standard clearly states:

    delete-expression:
                 ::opt delete cast-expression
                 ::opt delete [ ] cast-expression
    

    The first alternative is for non-array objects, and the second is for arrays. The operand shall have a pointer type, or a class type having a single conversion function (12.3.2) to a pointer type. The result has type void.

    In the first alternative (delete object), the value of the operand of delete shall be a pointer to a non-array object [...] If not, the behavior is undefined.

    The value of the operand in delete pStruct is a pointer to an array of char, independent of its static type (STRUCT*). Therefore, any discussion of memory leaks is quite pointless, because the code is ill-formed, and a C++ compiler is not required to produce a sensible executable in this case.

    It could leak memory, it could not, or it could do anything up to crashing your system. Indeed, a C++ implementation with which I tested your code aborts the program execution at the point of the delete expression.

    0 讨论(0)
  • 2021-02-14 12:49

    The various possible uses of the keywords new and delete seem to create a fair amount of confusion. There are always two stages to constructing dynamic objects in C++: the allocation of the raw memory and the construction of the new object in the allocated memory area. On the other side of the object lifetime there is the destruction of the object and the deallocation of the memory location where the object resided.

    Frequently these two steps are performed by a single C++ statement.

    MyObject* ObjPtr = new MyObject;
    
    //...
    
    delete MyObject;
    

    Instead of the above you can use the C++ raw memory allocation functions operator new and operator delete and explicit construction (via placement new) and destruction to perform the equivalent steps.

    void* MemoryPtr = ::operator new( sizeof(MyObject) );
    MyObject* ObjPtr = new (MemoryPtr) MyObject;
    
    // ...
    
    ObjPtr->~MyObject();
    ::operator delete( MemoryPtr );
    

    Notice how there is no casting involved, and only one type of object is constructed in the allocated memory area. Using something like new char[N] as a way to allocate raw memory is technically incorrect as, logically, char objects are created in the newly allocated memory. I don't know of any situation where it doesn't 'just work' but it blurs the distinction between raw memory allocation and object creation so I advise against it.

    In this particular case, there is no gain to be had by separating out the two steps of delete but you do need to manually control the initial allocation. The above code works in the 'everything working' scenario but it will leak the raw memory in the case where the constructor of MyObject throws an exception. While this could be caught and solved with an exception handler at the point of allocation it is probably neater to provide a custom operator new so that the complete construction can be handled by a placement new expression.

    class MyObject
    {
        void* operator new( std::size_t rqsize, std::size_t padding )
        {
            return ::operator new( rqsize + padding );
        }
    
        // Usual (non-placement) delete
        // We need to define this as our placement operator delete
        // function happens to have one of the allowed signatures for
        // a non-placement operator delete
        void operator delete( void* p )
        {
            ::operator delete( p );
        }
    
        // Placement operator delete
        void operator delete( void* p, std::size_t )
        {
            ::operator delete( p );
        }
    };
    

    There are a couple of subtle points here. We define a class placement new so that we can allocate enough memory for the class instance plus some user specifiable padding. Because we do this we need to provide a matching placement delete so that if the memory allocation succeeds but the construction fails, the allocated memory is automatically deallocated. Unfortunately, the signature for our placement delete matches one of the two allowed signatures for non-placement delete so we need to provide the other form of non-placement delete so that our real placement delete is treated as a placement delete. (We could have got around this by adding an extra dummy parameter to both our placement new and placement delete, but this would have required extra work at all the calling sites.)

    // Called in one step like so:
    MyObject* ObjectPtr = new (padding) MyObject;
    

    Using a single new expression we are now guaranteed that memory won't leak if any part of the new expression throws.

    At the other end of the object lifetime, because we defined operator delete (even if we hadn't, the memory for the object originally came from global operator new in any case), the following is the correct way to destroy the dynamically created object.

    delete ObjectPtr;
    

    Summary!

    1. Look no casts! operator new and operator delete deal with raw memory, placement new can construct objects in raw memory. An explicit cast from a void* to an object pointer is usually a sign of something logically wrong, even if it does 'just work'.

    2. We've completely ignored new[] and delete[]. These variable size objects will not work in arrays in any case.

    3. Placement new allows a new expression not to leak, the new expression still evaluates to a pointer to an object that needs destroying and memory that needs deallocating. Use of some type of smart pointer may help prevent other types of leak. On the plus side we've let a plain delete be the correct way to do this so most standard smart pointers will work.

    0 讨论(0)
  • 2021-02-14 12:49

    I am currently unable to vote, but slicedlime's answer is preferable to Rob Walker's answer, since the problem has nothing to do with allocators or whether or not the STRUCT has a destructor.

    Also note that the example code does not necessarily result in a memory leak - it's undefined behavior. Pretty much anything could happen (from nothing bad to a crash far, far away).

    The example code results in undefined behavior, plain and simple. slicedlime's answer is direct and to the point (with the caveat that the word 'vector' should be changed to 'array' since vectors are an STL thing).

    This kind of stuff is covered pretty well in the C++ FAQ (Sections 16.12, 16.13, and 16.14):

    http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.12

    0 讨论(0)
  • 2021-02-14 12:49

    @ericmayo - cripes. Well, experimenting with VS2005, I can't get an honest leak out of scalar delete on memory that was made by vector new. I guess the compiler behavior is "undefined" here, is about the best defense I can muster.

    You've got to admit though, it's a really lousy practice to do what the original poster said.

    If that were the case then C++ would not be portable as is today and a crashing application would never get cleaned up by the OS.

    This logic doesn't really hold, though. My assertion is that a compiler's runtime can manage the memory within the memory blocks that the OS returns to it. This is how most virtual machines work, so your argument against portability in this case don't make much sense.

    0 讨论(0)
  • 2021-02-14 12:55

    @eric - Thanks for the comments. You keep saying something though, that drives me nuts:

    Those run-time libraries handle the memory management calls to the OS in a OS independent consistent syntax and those run-time libraries are responsible for making malloc and new work consistently between OSes such as Linux, Windows, Solaris, AIX, etc....

    This is not true. The compiler writer provides the implementation of the std libraries, for instance, and they are absolutely free to implement those in an OS dependent way. They're free, for instance, to make one giant call to malloc, and then manage memory within the block however they wish.

    Compatibility is provided because the API of std, etc. is the same - not because the run-time libraries all turn around and call the exact same OS calls.

    0 讨论(0)
提交回复
热议问题