Class with dynamic memory

A string example

String class definition

class MyString {
    private:
        int buf_len;
        char * characters;
    public:
        MyString(int buf_len = 64, char *data = nullptr){
            this->buf_len = 0;
            this->characters = nullptr;
            create(buf_len, data); // this is a helper function, it will allocate memory and copy data
        }
        ~MyString(){
            delete[] characters;
        }
        // other member functions
};
  • Before a MyString object is used, the length of the string is not konwn.
    • As a result, the memory should be allocated dynamically.

The create function

    bool create(int buf_len,  const char * data)
    {
        this->buf_len = buf_len;

        if( this->buf_len != 0)
        {
            this->characters = new char[this->buf_len]{};
            if(data)
                strncpy(this->characters, data, this->buf_len);
        }
    
        return true;
    }

In the main function, we can use the MyString class as follow:

int main()
{
    MyString str1(10, "stevens");
    cout << "str1: " << str1 << endl;

    MyString str2 = str1; 
    cout << "str2: " << str2 << endl;

    MyString str3;
    cout << "str3: " << str3 << endl;
    str3 = str1;
    cout << "str3: " << str3 << endl;

    return 0;
}
  • Why memory leak and memory double free?

Break down

MyString

Hard copy

Helper functions

  • release(): release the memory
  • create(): first release the current memory, then allocate new memory and copy data
bool MyString::release(){
    this->buf_len = 0;
    if(this->characters!=nullptr){
        delete []this->characters;
        this->characters = nullptr;
    }
    return true;
}
bool MyString::create(int buf_len,  const char * data){
    release(); 
    this->buf_len = buf_len;
    if( this->buf_len != 0){
        this->characters = new char[this->buf_len]{};
    }
    if(data)
        strncpy(this->characters, data, this->buf_len);
    return true;
}

Copy constructor & copy assignment operator

One reason causes memory leak and double free is that the copy constructor provided by the compiler is not appropriate.

MyString::MyString(const MyString & other){
    this->buf_len = 0;
    this->characters = nullptr;
    create(other.buf_len, other.characters);
}

MyString& MyString::operator=(const MyString & other){
    create(other.buf_len, other.characters);
    return *this;
}
  • Now each objects has its own memory.
  • It’s a hard copy.

Self-assignment

We should improve the copy assignment operator by checking if the source and the target are the same object.

  • If the address of the source and the target are the same(self-assignment), do nothing.
    • If not, it will release the current object’s memory, then you can’t find the data anymore!
  • Otherwise, release the current memory, then allocate new memory and copy data.
MyString& MyString::operator=(const MyString & other){
    if(this == &other)
        return *this;
    create(other.buf_len, other.characters);
    return *this;
}

Soft copy

Problem of Hard copy

  • Frequently allocate and deallocate memory.
  • Not efficient when the memory is large.

Soft copy helps to solve the problem. However,

  • Multiple objects share the same memory.
  • How to decide when to release the memory?

Solution 1: Reference counting

In each MyString object, we maintain a reference count.

  • The reference count is initialized to 1.
    • The count will increase/decrease when the object is assigned or destroyed.
    • When the reference count is 0, the memory is released.
  • The reference count is stored in a shared part of the memory.
    • We can use a integer on the heap to store the reference count.

Solution 2: a struct to store both the reference count and the memory

Since both the reference count and the memory are used on the heap

  • we can use a struct to store them together.

=delete and =default

  • =delete is used to disable a function.
  • =default is used to enable a function that is defined by the compiler by default.
// in class definition
MyString(const MyString & other) = delete;
MyString& operator=(const MyString & other) = delete;

// main function
MyString str1 = "stevens";
MyString str2 = str1; // error
MyString str3;
str3 = str1; // error
  • Now the copy constructor and copy assignment operator are disabled.
  • You can’t use copy constructor and copy assignment operator.

Smart pointers

std::shared_ptr

Smart pointers are used to make sure that an object can be deleted when it is no longer used.

For std::shared_ptr, the reference count is maintained in the smart pointer.

  • Several std::shared_ptr can point to the same object.
  • The memory is automatically released when the reference count becomes 0.
#include <memory> // for std::shared_ptr
// ...
auto mt3 = std::make_shared<MyTime>(new MyTime(1, 70)); // c++11
std::shared_ptr<MyTime> mt1 = std::make_shared<MyTime>(1, 70); // c++17
std::shared_ptr<MyTime> mt2 = mt1;
cout << "mt1.use_count(): " << mt1.use_count() << endl;
cout << "mt2.use_count(): " << mt2.use_count() << endl;
// ...
  • The memory is automatically released when mt1 and mt2 go out of scope.
  • mt1.use_count() can be used to get the reference count.
    • It returns the number of std::shared_ptr objects pointing to the same memory.
    • when both mt1 and mt2 are alive, mt1.use_count() is the same as mt2.use_count().

std::unique_ptr

Different from std::shared_ptr, std::unique_ptr is a unique owner of the memory.

  • It does not allow copy assignment or copy constructor.
  • It can be moved to another std::unique_ptr, by std::move().
  • When the std::unique_ptr goes out of scope, the memory is released.
std::unique_ptr<MyTime> mt1 = std::make_unique<MyTime>(1, 70);
std::unique_ptr<MyTime> mt_test = mt1; // error
std::unique_ptr<MyTime> mt2 = std::move(mt1); // ok

cout << "mt1: " << *mt1 << endl; // segmentation fault
cout << "mt2: " << *mt2 << endl; // ok
  • std::move() is used to transfer the ownership of the memory.
  • After the transfer, mt1 loses the ownership of the memory, and it becomes a null pointer.

Summary

Rule of three

If a class requires a user-defined destructor, copy constructor, or copy assignment operator, it likely requires all three.

  • If all the data members are plain data types, no need to define your own.
  • If you handle the dynamic memory by yourself, you must define all three.

Alternatively,

  • Smart pointers can help you manage the memory automatically.
  • You can use =delete to disable the copy constructor and copy assignment operator.

Why no many kinds of constructors in Java and Python?

  • Programmers in Java and python deal with open files, sockets, database connections, etc.
  • These works are often connected to the memory management.
  • As a result, Java and Python simplify the memory management by using shared pointers.
    • Every object except for primitive types is an object.
    • Every copy is a soft copy and object deallocation is handled by the reference count.
  • Therefore, C++, which focuses on system-level programming, algorithmic data structures, and high-performance computing as its main business, has developed these concepts.
  • It incorporates the following ideas as fundamental elements of the language:
    • Copy/move operations
    • Pointers
    • Mutability
    • Multithreading
  • These concepts are extremely important in our operations, making them irreplaceable.

Recursion

Recursion is the technique of making a function call itself.

  • Break down a problem into smaller problems of the same type.
  • Each recursive call should make the problem smaller.
  • There should be a base case that terminates the recursion.
int factorial(int n){
    if(n == 0)
        return 1;
    return n * factorial(n-1);
}
  • The base case is n == 0, which returns 1.
  • Each recursive call makes the problem smaller by n-1.
  • When n becomes 0, the recursion terminates.

Another example:

void div2(int n){
    cout << "Entering div2 with n = " << n << endl;
    if(n == 0)
        return;
    div2(n/2);
    cout << "Leaving div2 with n = " << n << endl;
}

int main(){
    div2(1024);
    return 0;
}

Almost all recursive functions can be converted to iterative functions.

Pros of recursion:

  • Good at tree traversal
  • Less lines of source code

Cons of recursion:

  • Consume more stack memory
  • May be less efficient
  • Hard to implement and debug