Example Code: Memory Management#

Example code for C memory management (cmem.c).#
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>

#ifdef __APPLE__
// Macos hasn't implemented the C11 aligned_alloc as of the time 2019/8.
void * aligned_alloc(size_t alignment, size_t size)
{
    void * ptr;
    posix_memalign(&ptr, alignment, size);
    return ptr;
}
#endif

void outer();

int main(int argc, char ** argv)
{
    printf("frame address of main: %p\n", __builtin_frame_address(0));

    outer();

    return 0;
}

// Will be called by outer();
int64_t * inner()
{
    printf("frame address of inner: %p\n", __builtin_frame_address(0));

    // An array on the stack.  It is popped away when execution leaves this
    // function.  You cannot use the memory outside this function.
    int64_t data_stack[32];

    for (size_t it = 0; it < 32; ++it)
    {
        data_stack[it] = 100 + it;
    }
    printf("stack memory: %p\n", data_stack);

    // A dynamic array.
    int64_t * data_dynamic = (int64_t *) malloc(32 * sizeof(int64_t));

    for (size_t it = 0; it < 32; ++it)
    {
        data_dynamic[it] = 200 + it;
    }
    printf("dynamic memory: %p\n", data_dynamic);

    return data_dynamic;
}

void outer()
{
    printf("frame address of outer: %p\n", __builtin_frame_address(0));

    int64_t * data = inner(); // Initialize the data.
    printf("data returned from inner: %p\n", data);

    for (size_t it = 0; it < 32; ++it)
    {
        if (data[it] != 200 + it)
        {
            printf("error\n");
        }
    }
    printf("=== malloc tested\n");

    // You must free the memory after you finish using it.  Otherwise it will
    // remain in the process unclaimed, results in the "memory leak".
    free(data);
    //free(data); // Double free results into error.
    printf("=== free tested\n");

#if 0
    // You may not use the memory that is already freed.  The results is
    // undefined.
    for (size_t it = 0; it < 32; ++it)
    {
        if (data[it] != 200 + it)
        {
            printf("error\n");
        }
    }
#endif

    // The following two allocations result in the same zero-initialized array.
    //
    // The first one uses calloc.  If the OS returns the memory that is already
    // zero-initialized, calloc knows, and it doesn't need to redo the zero
    // initialization.
    data = (int64_t *) calloc(32, sizeof(int64_t));
    free(data);
    // The second one uses malloc and manual initialization.  The malloc call
    // does not provide any information about whether the memory is already
    // zero-initialized.
    data = (int64_t *) malloc(32 * sizeof(int64_t));
    // Even if the allocated memory was already zero-initialized by the OS, we
    // still need to do the initialization.
    for (size_t it = 0; it < 32; ++it) { data[it] = 0; }
    free(data);
    printf("=== calloc tested\n");

    // Reallocate the memory with smaller or larger size.
    data = (int64_t *) malloc((1UL << 20) * 2 * sizeof(int64_t));
    printf("address by malloc: %p\n", data);
    data = (int64_t *) realloc(data, (1UL << 20) * 1 * sizeof(int64_t));
    printf("address by realloc to smaller memory: %p\n", data);
    data = (int64_t *) realloc(data, (1UL << 20) * 4 * sizeof(int64_t));
    printf("address by realloc to larger memory: %p\n", data);
    free(data);
    printf("=== realloc tested\n");

    // Aligned allocation.
    int64_t * data1 = (int64_t *) malloc(sizeof(int64_t));
    printf("address by malloc: %p\n", data1);
    int64_t * data2 = (int64_t *) aligned_alloc(256, 256 * sizeof(int64_t));
    printf("address by aligned_alloc: %p\n", data2);
    free(data1);
    free(data2);
    printf("=== aligned_alloc tested\n");
}

Build cmem.c.#
$ gcc cmem.c -o cmem -std=c11 -O3 -g
Example code for C++ memory management (cppmem.cpp).#
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <cstdlib>
#include <iostream>

/*
 * A dummy class taking 8k bytes.
 */
struct Block
{
    Block()
    {
        std::cout << "Block (" << this << ") constructed" << std::endl;
    }
    ~Block()
    {
        std::cout << "Block (" << this << ") destructed" << std::endl;
    }
    int64_t buffer[1024];
};

void scalar_form()
{
    std::cout
        << "frame address of scalar_form: " << __builtin_frame_address(0)
        << std::endl;

    // Doing this place 8k bytes on stack.
    Block block_stack;
    for (size_t it = 0; it < 1024; ++it)
    {
        block_stack.buffer[it] = 1000 + it;
    }
    std::cout << "object on stack: " << &block_stack << std::endl;
    std::cout
        << "address difference: "
        << reinterpret_cast<std::size_t>(__builtin_frame_address(0))
         - reinterpret_cast<std::size_t>(&block_stack)
        << ", sizeof(Block): " << sizeof(Block)
        << std::endl;

    // Use the new expression.  Note that this "new" is an expression.  It
    // calls the operator ("::operator new"), but not the operator itself.
    Block * block_dynamic = new Block;
    std::cout << "object on dynamic memory: " << block_dynamic << std::endl;

    for (size_t it = 0; it < 1024; ++it)
    {
        block_dynamic->buffer[it] = 2000 + it;
    }
    std::cout << "=== new tested" << std::endl;

    // The delete expression that destruct and deallocate the memory of the
    // dynamic block object.  Similarly, the expression calls ::operator delete
    // for block_dynamic.
    delete block_dynamic;
    std::cout << "=== delete tested" << std::endl;
}

void array_form()
{
    // An array on the stack.  It is popped away when execution leaves this
    // function.  You cannot use the memory outside this function.
    int64_t data_stack[32];

    for (size_t it = 0; it < 32; ++it)
    {
        data_stack[it] = 100 + it;
    }
    std::cout << "stack array memory: " << data_stack << std::endl;

    // A dynamic array.
    int64_t * data_dynamic = new int64_t[32];

    for (size_t it = 0; it < 32; ++it)
    {
        data_dynamic[it] = 200 + it;
    }
    std::cout << "dynamic array memory: " << data_dynamic << std::endl;
    std::cout << "=== new[] tested" << std::endl;

    delete[] data_dynamic;
    std::cout << "=== delete[] tested" << std::endl;
}

void placement()
{
    char * buffer = new char[sizeof(Block)];

    Block * block = new (buffer) Block;
    for (size_t it = 0; it < 1024; ++it)
    {
        block->buffer[it] = it;
    }
    std::cout << "=== placement new tested" << std::endl;

#if 0
    // This induces undefined behavior.  Don't do this.
    delete block;
#endif

    // Instead of deleting the pointer block, call explicit the destructor and
    // delete the original buffer.
    block->~Block();
    delete[] buffer;
}

int main(int argc, char ** argv)
{
    scalar_form();
    array_form();
    placement();

    return 0;
}
$ g++ cppmem.cpp -o cppmem -std=c++17 -O3 -g
A simple C++ STL allocator that keeps track of bytes allocated (alloc.cpp).#
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#include <cstdlib>
#include <new>
#include <memory>
#include <limits>
#include <atomic>
#include <vector>
#include <iostream>

struct ByteCounterImpl
{

    std::atomic_size_t allocated = 0;
    std::atomic_size_t deallocated = 0;
    std::atomic_size_t refcount = 0;

}; /* end struct ByteCounterImpl */

/**
 * One instance of this counter is shared among a set of allocators.
 *
 * The counter keeps track of the bytes allocated and deallocated, and report
 * those two numbers in addition to bytes that remain allocated.
 */
class ByteCounter
{

public:

    ByteCounter()
      : m_impl(new ByteCounterImpl)
    { incref(); }

    ByteCounter(ByteCounter const & other)
      : m_impl(other.m_impl)
    { incref(); }

    ByteCounter & operator=(ByteCounter const & other)
    {
        if (&other != this)
        {
            decref();
            m_impl = other.m_impl;
            incref();
        }

        return *this;
    }

    ByteCounter(ByteCounter && other)
      : m_impl(other.m_impl)
    { incref(); }

    ByteCounter & operator=(ByteCounter && other)
    {
        if (&other != this)
        {
            decref();
            m_impl = other.m_impl;
            incref();
        }

        return *this;
    }

    ~ByteCounter() { decref(); }

    void swap(ByteCounter & other)
    {
        std::swap(m_impl, other.m_impl);
    }

    void increase(std::size_t amount)
    {
        m_impl->allocated += amount;
    }

    void decrease(std::size_t amount)
    {
        m_impl->deallocated += amount;
    }

    std::size_t bytes() const { return m_impl->allocated - m_impl->deallocated; }
    std::size_t allocated() const { return m_impl->allocated; }
    std::size_t deallocated() const { return m_impl->deallocated; }
    /* This is for debugging. */
    std::size_t refcount() const { return m_impl->refcount; }

private:

    void incref() { ++m_impl->refcount; }

    void decref()
    {
        if (nullptr == m_impl)
        {
            // Do nothing.
        }
        else if (1 == m_impl->refcount)
        {
            delete m_impl;
            m_impl = nullptr;
        }
        else
        {
            --m_impl->refcount;
        }
    }

    friend bool operator==(ByteCounter const & a, ByteCounter const & b);

    ByteCounterImpl * m_impl;

}; /* end class ByteCounter */

bool operator==(ByteCounter const & a, ByteCounter const & b)
{
    return a.m_impl == b.m_impl;
}

/**
 * Very simple allocator that counts the number of bytes allocated through it.
 *
 * It's made to demonstrate the STL allocator and only works in this example.
 * A lot of modification is needed to use it in a real application.
 */
template <class T>
struct MyAllocator
{

    using value_type = T;

    // Just use the default constructor of ByteCounter for the data member
    // "counter".
    MyAllocator() = default;

    template <class U> constexpr
    MyAllocator(const MyAllocator<U> & other) noexcept
    {
        counter = other.counter;
    }

    T * allocate(std::size_t n)
    {
        if (n > std::numeric_limits<std::size_t>::max() / sizeof(T))
        {
            throw std::bad_alloc();
        }
        const std::size_t bytes = n*sizeof(T);
        T * p = static_cast<T *>(std::malloc(bytes));
        if (p)
        {
            counter.increase(bytes);
            return p;
        }
        else
        {
            throw std::bad_alloc();
        }
    }

    void deallocate(T* p, std::size_t n) noexcept
    {
        std::free(p);

        const std::size_t bytes = n*sizeof(T);
        counter.decrease(bytes);
    }

    ByteCounter counter;

}; /* end struct MyAllocator */

template <class T, class U>
bool operator==(const MyAllocator<T> & a, const MyAllocator<U> & b)
{
    return a.counter == b.counter;
}

template <class T, class U>
bool operator!=(const MyAllocator<T> & a, const MyAllocator<U> & b)
{
    return !(a == b);
}

template <class T>
std::ostream & operator << (std::ostream & out, const MyAllocator<T> & alloc)
{
    out << "allocator: bytes = " << alloc.counter.bytes();
    out << " allocated = " << alloc.counter.allocated();
    out << " deallocated = " << alloc.counter.deallocated();
    return out;
}

int main(int argc, char ** argv)
{
    MyAllocator<size_t> alloc;

    std::vector<size_t, MyAllocator<size_t>> vec1(alloc);
    std::cout << alloc << std::endl;

    for (size_t it=0; it<1024; ++it)
    {
        vec1.push_back(it);
    }
    std::cout << alloc << std::endl;

    std::vector<size_t, MyAllocator<size_t>>(alloc).swap(vec1);
    std::cout << alloc << std::endl;

    std::vector<size_t, MyAllocator<size_t>> vec2(1024, alloc);
    std::cout << alloc << std::endl;

    std::vector<size_t, MyAllocator<size_t>> vec3(std::move(vec2));
    std::cout << alloc << std::endl;

    std::vector<size_t, MyAllocator<size_t>>(alloc).swap(vec3);
    std::cout << alloc << std::endl;

    return 0;
}
Template to help keep track of the number of living instances (icount.cpp).#
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include <cstdlib>
#include <new>
#include <memory>
#include <limits>
#include <atomic>
#include <vector>
#include <algorithm>
#include <iostream>

template <class T>
class InstanceCounter
{

public:

    InstanceCounter() { ++m_constructed; }
    InstanceCounter(InstanceCounter const & other) { ++m_copied; }
    ~InstanceCounter() { ++m_destructed; }

    static std::size_t active()
    {
        return m_constructed + m_copied - m_destructed;
    }
    static std::size_t constructed() { return m_constructed; }
    static std::size_t copied() { return m_copied; }
    static std::size_t destructed() { return m_destructed; }

private:

    static std::atomic_size_t m_constructed;
    static std::atomic_size_t m_copied;
    static std::atomic_size_t m_destructed;

}; /* end class InstanceCounter */

// Compiler will make sure these static variables are defined only once.
template <class T> std::atomic_size_t InstanceCounter<T>::m_constructed = 0;
template <class T> std::atomic_size_t InstanceCounter<T>::m_copied = 0;
template <class T> std::atomic_size_t InstanceCounter<T>::m_destructed = 0;

struct Data
  : public InstanceCounter<Data>
{

    std::size_t buffer[1024];

}; /* end struct Data */

struct Data2
  : public InstanceCounter<Data2>
{

    Data2() = default;
    Data2(Data2 const & other)
#if 0
    // Don't forget to call the base class copy constructor.  The implicit copy
    // constructor calls it for you.  But when you have custom copy
    // constructor, if you do not specify the base constructor, the default
    // constructor in the base class is used.
      : InstanceCounter<Data2>(other)
#endif
    {
        std::copy_n(other.buffer, 1024, buffer);
    }
    Data2 & operator=(Data2 const & other)
    {
        std::copy_n(other.buffer, 1024, buffer);
        return *this;
    }

    std::size_t buffer[1024];

}; /* end struct Data */

template <class T>
void report(std::string const & prefix)
{
    using ct = InstanceCounter<T>;

    std::cout
        << prefix
        << " instance: active = " << ct::active()
        << " constructed = " << ct::constructed()
        << " copied = " << ct::copied()
        << " destructed = " << ct::destructed()
        << std::endl;
}

int main(int argc, char ** argv)
{
    std::cout << "** Creation phase **" << std::endl;

    // Data.
    Data * data = new Data();
    report<Data> ("Data  (default construction)  ");

    Data * data_copied = new Data(*data);
    report<Data> ("Data  (copy construction)     ");

    std::vector<Data> dvec(64);
    report<Data> ("Data  (construction in vector)");

    // Data2.
    Data2 * data2 = new Data2();
    report<Data2>("Data2 (default construction)  ");

    Data2 * data2_copied = new Data2(*data2);
    report<Data2>("Data2 (copy construction)     ");

    std::vector<Data2> d2vec(64);
    report<Data2>("Data2 (construction in vector)");

    std::cout << "** Deletion phase **" << std::endl;

    // Data.
    std::vector<Data>().swap(dvec);
    report<Data>("Data ");
    delete data;
    report<Data>("Data ");
    delete data_copied;
    report<Data>("Data ");

    // Data2.
    std::vector<Data2>().swap(d2vec);
    report<Data2>("Data2");
    delete data2;
    report<Data2>("Data2");
    delete data2_copied;
    report<Data2>("Data2");

    return 0;
}