Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does c++ operator new[]/delete[] call operator new/delete?

Tags:

c++

Does c++ operator new[]/delete[] (not mine) call operator new/delete?

After I replaced operator new and operator delete with my own implemenation, then the following code will call them:

int *array = new int[3];
delete[] array;

And When I also replaced operator new[] and operator delete[], then the above code will call only them.

My operators implementation:

void *operator new(std::size_t blockSize) {
    std::cout << "allocate bytes: " << blockSize << std::endl;
    return malloc(blockSize);
}

void *operator new[](std::size_t blockSize) {
    std::cout << "[] allocate: " << blockSize << std::endl;
    return malloc(blockSize);
}

void operator delete(void *block) throw() {
    int *blockSize = static_cast<int *>(block);
    blockSize = blockSize - sizeof(int);
    std::cout << "deallocate bytes: " << *blockSize << std::endl;
    free(block);
}

void operator delete[](void *block) throw() {
    int *blockSize = static_cast<int *>(block);
    blockSize = blockSize - sizeof(int);
    std::cout << "[] deallocate bytes: " << *blockSize << std::endl;
    free(block);
}

I have a second question which maybe not so related, why the code prints:

[] allocate: 12
[] deallocate bytes: 0

Instead of this:

[] allocate: 16
[] deallocate bytes: 16
like image 575
Stav Alfi Avatar asked Oct 17 '22 00:10

Stav Alfi


1 Answers

Since the allocation operators new and new[] pretty much do the same thing(a), it makes sense that one would be defined in terms of the other. They're both used for allocating a block of a given size, regardless of what you intend to use it for. Ditto for delete and delete[].

In fact, this is required by the standard. C++11 18.6.1.2 /4 (for example) states that the default behaviour of operator new[] is that it returns operator new(size). There's a similar restriction in /13 for operator delete[].

So a sample default implementation would be something like:

void *operator new(std::size_t sz) { return malloc(sz); }
void operator delete(void *mem) throw() { free(mem); }

void *operator new[](std::size_t sz) { return operator new(sz); }
void operator delete[](void *mem) throw() { return operator delete(mem); }

When you replace the new and delete functions, the new[] and delete[] ones will still use them under the covers. However, replacing new[] and delete[] with your own functions that don't call your new and delete results in them becoming disconnected.

That's why you're seeing the behaviour described in the first part of your question.


As per the second part of your question, you're seeing what I'd expect to see. The allocation of int[3] is asking for three integers, each four bytes in size (in you environment). That's clearly 12 bytes.

Why it seems to be freeing zero bytes is a little more complex. You seem to think that the four bytes immediately before the address you were given are the size of the block but that's not necessarily so.

Implementations are free to store whatever control information they like in the memory arena(b) including the following possibilities (this is by no means exhaustive):

  • the size of the current memory allocation;
  • a link to the next (and possibly previous) control block;
  • a sentinel value (such as 0xa55a or a checksum of the control block) to catch arena corruption.

Unless you know and control how the memory allocation functions use their control blocks, you shouldn't be making assumptions. For a start, to ensure correct alignment, control blocks may be padded with otherwise useless data. If you want to save/use the requested size, you'll need to do it yourself with something like:

#include <iostream>
#include <memory>

// Need to check this is enough to maintain alignment.

namespace { const int buffSz = 16; }

// New will allocate more than needed, store size, return adjusted address.

void *operator new(std::size_t blockSize) {
    std::cout << "Allocating size " << blockSize << '\n';
    auto mem = static_cast<std::size_t*>(std::malloc(blockSize + buffSz));
    *mem = blockSize;
    return reinterpret_cast<char*>(mem) + buffSz;
}

// Delete will unadjust address, use that stored size and free.

void operator delete(void *block) throw() {
    auto mem = reinterpret_cast<std::size_t*>(static_cast<char*>(block) - buffSz);
    std::cout << "Deallocating size " << *mem << '\n';
    std::free(mem);
}

// Leave new[] and delete[] alone, they'll use our functions above.

// Test harness.

int main() {
    int *x = new int;
    *x = 7;

    int *y = new int[3];
    y[0] = y[1] = y[2] = 42;

    std::cout << *x << ' ' << y[1] << '\n';

    delete[] y;
    delete x;
}

Running that code results in successful values being printed:

Allocating size 4
Allocating size 12
7 42
Deallocating size 12
Deallocating size 4

(a) The difference between new MyClass and new MyClass[7] comes later than the allocation phase, when the objects are being constructed. Basically, they both allocate the required memory once, then construct as many objects in that memory as necessary (once in the former, seven times in the latter).


(b) And an implementation is allowed to not store any control information inline. I remember working on embedded systems where we knew that no allocation would ever be more than 1K. So we basically created an arena that had no inline control blocks. Instead it had a bit chunk of memory, several hundred of those 1K blocks, and used a bitmap to decide which was in use and which was free.

On the off chance someone asked for more than 1K, the got NULL. Those asking for less than or equal to 1K got 1K regardless. Needless to say, it was much faster than the general purpose allocation functions provided with the implementation.

like image 97
paxdiablo Avatar answered Oct 23 '22 00:10

paxdiablo