Loading...

Go Back

Next page
Go Back Course Outline

C++ Full Course


Memory Management in C++

C++ Memory Management

Memory management is a fundamental aspect of programming, and C++ gives you a lot of control – which also means a lot of responsibility. Let's dive into each of these concepts with explanations and illustrative code examples.

Pointers

At their core, pointers are variables that hold memory addresses. They allow you to indirectly access and manipulate data stored in memory.

Raw Pointers

These are the most basic type of pointers in C++. They store the memory address of a variable.

#include <iostream>

int main() {
  int number = 10;
  int* ptr = &number; // 'ptr' now holds the memory address of 'number'

  std::cout << "Value of number: " << number << std::endl;
  std::cout << "Address of number: " << &number << std::endl;
  std::cout << "Value of ptr (address of number): " << ptr << std::endl;
  std::cout << "Value pointed to by ptr: " << *ptr << std::endl; // Dereferencing the pointer

  *ptr = 20; // Modifying the value at the memory address pointed to by 'ptr'
  std::cout << "New value of number: " << number << std::endl;

  return 0;
}
Value of number: 10
Address of number: 0x7fffffffdfe4 // The actual address will vary
Value of ptr (address of number): 0x7fffffffdfe4 // The actual address will vary
Value pointed to by ptr: 10
New value of number: 20

Pointer Arithmetic

You can perform certain arithmetic operations on pointers, but it's crucial to understand that these operations are scaled by the size of the data type the pointer points to.

#include <iostream>

int main() {
  int arr[] = {1, 2, 3, 4, 5};
  int* ptr = arr; // 'ptr' points to the first element of the array

  std::cout << "Value at ptr: " << *ptr << std::endl; // Output: 1

  ptr++; // Increment 'ptr' to point to the next integer in the array
  std::cout << "Value at ptr: " << *ptr << std::endl; // Output: 2

  ptr += 2; // Increment 'ptr' by 2 integers
  std::cout << "Value at ptr: " << *ptr << std::endl; // Output: 4

  return 0;
}
Value at ptr: 1
Value at ptr: 2
Value at ptr: 4

nullptr

nullptr (introduced in C++11) is a special null pointer literal that represents a pointer that doesn't point to any valid memory location. It's safer to use than the old NULL macro.

#include <iostream>

int main() {
  int* ptr = nullptr;

  if (ptr == nullptr) {
    std::cout << "ptr is a null pointer." << std::endl;
  } else {
    std::cout << "ptr is not a null pointer." << std::endl;
  }

  // Attempting to dereference a nullptr leads to undefined behavior (often a crash)
  // std::cout << *ptr << std::endl; // Avoid this!

  return 0;
}
ptr is a null pointer.


Dynamic Memory

Dynamic memory management allows you to allocate and deallocate memory during the runtime of your program. This is essential when you don't know the size of the memory you need at compile time.

new and delete (for single objects)

The new operator allocates memory on the heap and returns a pointer to the allocated memory. The delete operator is used to deallocate memory that was previously allocated with new.

#include <iostream>

int main() {
  int* dynamicInt = new int; // Allocate memory for a single integer
  *dynamicInt = 42;
  std::cout << "Value of dynamicInt: " << *dynamicInt << std::endl;

  delete dynamicInt; // Deallocate the memory
  dynamicInt = nullptr; // Good practice to set the pointer to nullptr after deleting

  return 0;
}
Value of dynamicInt: 42

new[] and delete[] (for arrays)

To allocate memory for an array dynamically, you use new[]. Correspondingly, you must use delete[] to deallocate the memory.

#include <iostream>

int main() {
  int size = 5;
  int* dynamicArray = new int[size]; // Allocate memory for an array of 5 integers

  for (int i = 0; i < size; ++i) {
    dynamicArray[i] = i * 2;
  }

  for (int i = 0; i < size; ++i) {
    std::cout << dynamicArray[i] << " ";
  }
  std::cout << std::endl;

  delete[] dynamicArray; // Deallocate the array memory
  dynamicArray = nullptr;

  return 0;
}
0 2 4 6 8

Memory Leaks

A memory leak occurs when you allocate memory using new (or new[]) but fail to deallocate it using delete (or delete[]). Over time, this can consume all available memory, leading to program crashes or system instability.

#include <iostream>

void causeMemoryLeak() {
  int* leakedMemory = new int[100];
  // We allocate memory but never call delete[] leakedMemory;
}

int main() {
  for (int i = 0; i < 10; ++i) {
    causeMemoryLeak(); // Repeatedly allocating memory without deallocating
    std::cout << "Iteration " << i + 1 << ": Memory allocated (and leaked)." << std::endl;
    // In a real scenario, this would eventually lead to issues.
  }
  return 0;
}
Iteration 1: Memory allocated (and leaked).
Iteration 2: Memory allocated (and leaked).
Iteration 3: Memory allocated (and leaked).
Iteration 4: Memory allocated (and leaked).
Iteration 5: Memory allocated (and leaked).
Iteration 6: Memory allocated (and leaked).
Iteration 7: Memory allocated (and leaked).
Iteration 8: Memory allocated (and leaked).
Iteration 9: Memory allocated (and leaked).
Iteration 10: Memory allocated (and leaked).

Smart Pointers (C++11+)

Smart pointers are class templates that encapsulate raw pointers and automatically manage the lifetime of the dynamically allocated objects they point to. This helps prevent memory leaks.

unique_ptr

A unique_ptr provides exclusive ownership of the dynamically allocated object. Only one unique_ptr can point to an object at any given time. When the unique_ptr goes out of scope, the object it manages is automatically deleted. unique_ptr cannot be copied, but it can be moved using std::move().

#include <iostream>
#include <memory>

int main() {
  std::unique_ptr<int> up1(new int(100)); // 'up1' owns the allocated integer

  std::cout << "Value pointed to by up1: " << *up1 << std::endl;

  // std::unique_ptr<int> up2 = up1; // Error: Cannot copy unique_ptr

  std::unique_ptr<int> up2 = std::move(up1); // Ownership is transferred to 'up2'
  if (up1) {
    std::cout << "up1 still points to: " << *up1 << std::endl; // This won't be executed
  } else {
    std::cout << "up1 is now null." << std::endl;
  }
  std::cout << "Value pointed to by up2: " << *up2 << std::endl;

  // When up2 goes out of scope, the allocated integer will be automatically deleted.

  return 0;
}
Value pointed to by up1: 100
up1 is now null.
Value pointed to by up2: 100

shared_ptr

A shared_ptr allows multiple smart pointers to own the same dynamically allocated object. It uses a reference count to keep track of how many shared_ptr instances are pointing to the object. When the last shared_ptr owning the object goes out of scope, the object is automatically deleted.

#include <iostream>
#include <memory>

int main() {
  std::shared_ptr<int> sp1(new int(50));
  std::cout << "Reference count of sp1: " << sp1.use_count() << std::endl; // Output: 1

  std::shared_ptr<int> sp2 = sp1; // 'sp2' now also owns the same object
  std::cout << "Reference count of sp1: " << sp1.use_count() << std::endl; // Output: 2
  std::cout << "Reference count of sp2: " << sp2.use_count() << std::endl; // Output: 2

  std::shared_ptr<int> sp3 = sp2;
  std::cout << "Reference count of sp1: " << sp1.use_count() << std::endl; // Output: 3
  std::cout << "Reference count of sp2: " << sp2.use_count() << std::endl; // Output: 3
  std::cout << "Reference count of sp3: " << sp3.use_count() << std::endl; // Output: 3

  std::cout << "Value pointed to by sp1: " << *sp1 << std::endl;
  *sp2 = 75;
  std::cout << "Value pointed to by sp3: " << *sp3 << std::endl; // All shared_ptr instances see the change

  sp1.reset(); // 'sp1' no longer owns the object
  std::cout << "Reference count of sp2 after sp1.reset(): " << sp2.use_count() << std::endl; // Output: 2

  // When sp2 and sp3 go out of scope, the integer will be deleted.

  return 0;
}
Reference count of sp1: 1
Reference count of sp1: 2
Reference count of sp2: 2
Reference count of sp1: 3
Reference count of sp2: 3
Reference count of sp3: 3
Value pointed to by sp1: 50
Value pointed to by sp3: 75
Reference count of sp2 after sp1.reset(): 2

weak_ptr

A weak_ptr is a non-owning pointer that observes a shared_ptr. It does not increment the reference count and therefore does not prevent the object from being deleted. weak_ptr is useful for breaking circular dependencies between shared_ptr instances. To access the object pointed to by a weak_ptr, you need to convert it to a shared_ptr using the lock() method. If the object has already been deleted, lock() will return a null shared_ptr.

#include <iostream>
    #include <memory>
    
    int main() {
      std::shared_ptr<int> sp(new int(123));
      std::weak_ptr<int> wp = sp; // 'wp' observes 'sp'
    
      std::cout << "Reference count of sp: " << sp.use_count() << std::endl; // Output: 1
    
      if (std::shared_ptr<int> lockedSp = wp.lock()) {
        std::cout << "Value accessed through weak_ptr: " << *lockedSp << std::endl;
        std::cout << "Reference count of sp (inside if): " << lockedSp.use_count() << std::endl; // Output: 2 (temporary shared_ptr)
      } else {
        std::cout << "Object no longer exists." << std::endl;
      }
    
      sp.reset(); // The object is now deleted because the last shared_ptr is gone
    
      if (std::shared_ptr<int> lockedSp = wp.lock()) {
        std::cout << "Value accessed through weak_ptr: " << *lockedSp << std::endl; // This won't be executed
      } else {
        std::cout << "Object no longer exists." << std::endl; // Output: Object no longer exists.
      }
    
      return 0;
    }
    
Reference count of sp: 1
Value accessed through weak_ptr: 123
Reference count of sp (inside if): 2
Object no longer exists.


RAII (Resource Acquisition Is Initialization)

RAII is a programming idiom in C++ where the acquisition of a resource (like memory, file handles, network sockets, etc.) is tied to the lifetime of an object. The resource is acquired during the object's initialization, and it is automatically released when the object goes out of scope (i.e., its destructor is called).

Smart pointers are a prime example of RAII. The unique_ptr and shared_ptr objects acquire ownership of the dynamically allocated memory during their construction, and they automatically release that memory in their destructors.

Example with a custom class managing a file:

#include <iostream>
    #include <fstream>
    #include <stdexcept>
    
    class FileWriter {
    private:
      std::ofstream file;
      std::string filename;
    public:
      FileWriter(const std::string& fname) : filename(fname), file(fname) {
        if (!file.is_open()) {
          throw std::runtime_error("Could not open file: " + filename);
        }
        std::cout << "File '" << filename << "' opened." << std::endl;
      }
    
      ~FileWriter() {
        if (file.is_open()) {
          file.close();
          std::cout << "File '" << filename << "' closed." << std::endl;
        }
      }
    
      void writeLine(const std::string& line) {
        if (file.is_open()) {
          file << line << std::endl;
        } else {
          throw std::runtime_error("File not open for writing.");
        }
      }
    };
    
    void processFile() {
      FileWriter writer("output.txt");
      writer.writeLine("This is the first line.");
      writer.writeLine("This is the second line.");
      // When 'writer' goes out of scope here, its destructor will automatically close the file.
    }
    
    int main() {
      try {
        processFile();
      } catch (const std::runtime_error& error) {
        std::cerr << "Error: " << error.what() << std::endl;
        return 1;
      }
      return 0;
    }
    
File 'output.txt' opened.
File 'output.txt' closed.

In this example, the FileWriter class manages the opening and closing of a file. The file is opened in the constructor, and it's automatically closed in the destructor. Even if an exception is thrown within the processFile function, the writer object will still go out of scope, and its destructor will be called, ensuring the file is properly closed. This demonstrates the core principle of RAII: tying resource management to object lifetime.

By using smart pointers and adhering to the RAII principle, you can significantly reduce the risk of memory leaks and other resource management issues in your C++ programs, leading to more robust and maintainable code.

Go Back

Next page