📖
Go C++
  • Introduction
  • Chapter 1: What You Must Know First
    • Virtual Address Space of Process: Memory Partition and Layout
    • Function Call: Stack Frame
    • Program Compiling and Linking
  • Chapter 2: C++ Basics Improvement
    • Default Parameters
    • Inline Function
    • Function Overloading
    • new and delete
    • const and Pointers
    • References in Detail
  • Chapter 3: Object-Oriented Principles
  • Class and Object
  • Constructor and Destructor
  • Shallow Copy and Deep Copy
  • Initializer List
  • Various Member Functions
  • Pointer to Class Members
  • Chapter 4: Template Programming
  • Function Templates
  • Class Templates
  • Memory Allocators
  • Chapter 5: Operator Overloading
    • Operator Overloading
    • Introduction to Iterators
    • Issues of Iterator Invalidation
    • More about new and delete
    • Overloading of new and delete: Object Pool
  • Chapter 6: Inheritance and Polymorphism
    • Look inside Inheritance
    • More about Inheritance
    • Virtual Functions, Static Binding and Dynamic Binding
    • More about Virtual Functions
    • Understanding Polymorphism
    • Abstract Classes
    • Frequently Asked Interview Questions: Polymorphism
  • Chapter 7: Multiple Inheritance
    • Virtual Inheritance and Virtual Base Classes
    • Diamond Problem
    • Four Kinds of Type Conversions
  • Chapter 8: Standard Template Library
    • Sequence Containers
    • Container Adaptors
    • Associative Containers
    • More about Iterators
    • Function Objects
    • Generic Algorithms, Binders and Lambda Expressions
  • Chapter 9: Object Optimization
    • Behind the Object
    • Optimizing Objects in Functions
    • Member Functions with Rvalue References
    • Move Semantics and Perfect Forwarding
  • Chapter 10: Smart Pointers
    • Smart Pointers
    • Smart Pointers without Reference Counting
    • Smart Pointers with Reference Counting
    • Custom Deleters
  • Chapter 11: Function Objects and Binders
    • More about Binders
    • Introduction to std::function
    • Template Specialization and Argument Deduction
    • More about std::function
    • std::bind(): A Simple Thread Pool
    • More about Lambda Expressions
  • Chapter 12: Multithreading
    • Important Features in C++11
    • Multithreaded Programming with std::thread
    • Mutual Exclusion
    • Producer-Consumer Problem
    • Atomic Operations
    • Thread Visibility and volatile
  • Chapter 13: Design Patterns
    • Singleton Pattern
    • Factory Pattern
    • Proxy Pattern
    • Decorator Pattern
    • Adapter Pattern
    • Observer Pattern
Powered by GitBook
On this page

Was this helpful?

Memory Allocators

Now using what we've learnt in template programming, we write a class MyVector which is a vector container similar to std::vector. The code is shown below.

template <typename T>
class MyVector {
 public:
  MyVector(int size = 10) {
    _first = new T[size];
    _last = _first;
    _end = _first + size;
  }
​
  ~MyVector() {
    delete[] _first;
    _first = _last = _end = nullptr;
  }
​
  MyVector(const MyVector<T> &other) {
    int size = other._end - other._first;
    _first = new T[size];
    int len = other._last - other._first;
    for (int i = 0; i < len; i++) {
      _first[i] = other._first[i];
    }
    _last = _first + len;
    _end = _first + size;
  }
​
  MyVector<T> &operator=(const MyVector<T> &other) {
    if (*this == other) return *this;
    delete[] _first;
    int size = other._end - other._first;
    _first = new T[size];
    int len = other._last - other._first;
    for (int i = 0; i < len; i++) {
      _first[i] = other._first[i];
    }
    _last = _first + len;
    _end = _first + size;
    return *this;
  }
​
  void push_back(T &val) {
    if (full()) expand();
    *_last++ = val;
  }
​
  void pop_back() {
    if (!empty()) --_last;
  }
​
  T &back() const { return *(_last - 1); }
​
  bool full() const { return _last == _end; }
​
  bool empty() const { return _last == _first; }
​
  int size() const { return _last - _first; }
​
 private:
  T *_first;  // Start point
  T *_last;   // One step after the last valid element
  T *_end;    // One step after the last element in the space
​
  void expand() {
    int size = _last - _first;
    T *tmp = new T[2 * size];
    for (int i = 0; i < size; i++) {
      tmp[i] = _first[i];
    }
    delete[] _first;
    _first = tmp;
    _last = _first + size;
    _end = _first + size * 2;
  }
};

In the above class, we use three pointers pointing to the start point of the vector, one step after the last valid element, and one step after the last element in the space respectively. This container works fine when we put integers or float numbers inside, but there's something wrong with object elements. Let's look at the following case.

class Test {
public:
    Test(){
        cout << "Test()" << endl;
    }
    ~Test() {
        cout << "~Test()" << endl;
    }
    Test(const Test &) {
        cout << "Test(const Test &)" << endl;
    }
};
​
int main() {
    Test t1, t2, t3;
    cout << "------------------" << endl;
    MyVector<Test> v;
    v.push_back(t1);
    v.push_back(t2);
    v.push_back(t3);
    cout << "------------------" << endl;
    v.pop_back();
    cout << "------------------" << endl;
    return 0;
}

The code above has the output as follow:

Test()
Test()
Test()
------------------
Test()
Test()
Test()
Test()
Test()
Test()
Test()
Test()
Test()
Test()
------------------
------------------
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()
~Test()

Here we only put three objects inside MyVector, but why 10 objects are constructed and destroyed? That is because every time we construct MyVector, we use new to create an array of size 10. Notice that new not only allocates memory on the heap, but also call the constructor of objects as well. In this case, we only need to allocate memory without constructing objects. In general, three things should be done for our previous code.

  1. In vector constructor, memory allocation and object construction should be separated.

  2. In vector destructor, only existing elements in the container should be destroyed and deallocated.

  3. pop operation can't just move the pointer. Corresponding element should be destroyed as well.

To achieve this, we need the memory allocator. The memory allocator is one of the most important component of a container class. It does four things: memory allocation, memory deallocation, object construction and object destruction. Using a memory allocator, we can separate memory operations and object operations, which new and delete can not achieve.

template <typename T>
class Allocator {
  T *allocate(size_t size) { return (T *)malloc(sizeof(T) * size); }
​
  void deallocate(void *p) { free(p); }
​
  void construct(T *p, const T &val) { new (p) T(val); }
​
  void destroy(T *p) { p->~T(); }
};

Then we can improve MyVector with memory allocator. All the places involved memory management should be implemented with it.

template <typename T, typename Alloc = Allocator<T>>
class MyVector {
 public:
  MyVector(int size = 10) {
    //_first = new T[size];
    _first = _allocator.allocate(size);
    _last = _first;
    _end = _first + size;
  }
​
  ~MyVector() {
    // delete[] _first;
    for (T *p = _first; p != _last; p++) {
      _allocator.destroy(p);
    }
    _allocator.deallocate(_first);
    _first = _last = _end = nullptr;
  }
​
  MyVector(const MyVector<T> &other) {
    int size = other._end - other._first;
    //_first = new T[size];
    _first = _allocator.allocate(size);
    int len = other._last - other._first;
    for (int i = 0; i < len; i++) {
      //_first[i] = other._first[i];
      _allocator.construct(_first + i, other._first[i]);
    }
    _last = _first + len;
    _end = _first + size;
  }
​
  MyVector<T> &operator=(const MyVector<T> &other) {
    if (*this == other) return *this;
    // delete[] _first;
    for (T *p = _first; p != _last; p++) {
      _allocator.destroy(p);
    }
    //_first = new T[size];
    _first = _allocator.allocate(size);
    int len = other._last - other._first;
    for (int i = 0; i < len; i++) {
      //_first[i] = other._first[i];
      _allocator.construct(_first + i, other._first[i]);
    }
    _last = _first + len;
    _end = _first + size;
    return *this;
  }
​
  void push_back(T &val) {
    if (full()) expand();
    //*_last++ = val;
    _allocator.construct(_last, val);
    _last++;
  }
​
  void pop_back() {
    if (!empty()) {
      //--_last;
      _last--;
      _allocator.destroy(_last);
    }
  }
​
  T &back() const { return *(_last - 1); }
​
  bool full() const { return _last == _end; }
​
  bool empty() const { return _last == _first; }
​
  int size() const { return _last - _first; }
​
 private:
  T *_first;  // Start point
  T *_last;   // One step after the last valid element
  T *_end;    // One step after the last element in the space
  Alloc _allocator;
​
  void expand() {
    int size = _last - _first;
    // T *tmp = T[2 * size];
    T *tmp = _allocator.allocate(2 * size);
    for (int i = 0; i < size; i++) {
      // tmp[i] = _first[i];
      _allocator.construct(tmp + i, _first[i]);
    }
    // delete[] _first;
    for (T *p = _first; p != _last; p++) {
      _allocator.destroy(p);
    }
    _allocator.deallocate(_first);
    _first = tmp;
    _last = _first + size;
    _end = _first + size * 2;
  }
};

After then, we get the correct output. An object is only constructed after being pushed, and destroyed after being popped.

Test()
Test()
Test()
------------------
Test(const Test &)
Test(const Test &)
Test(const Test &)
------------------
~Test()
------------------
~Test()
~Test()
~Test()
~Test()
~Test()

Memory allocators are heavily used in STL containers, further details will be discussed in later chapters.

PreviousClass TemplatesNextOperator Overloading

Last updated 4 years ago

Was this helpful?