Pointers & Memory

Memory Addresses

C++ offers extensive control over the computer's memory. Many other programming languages emphasize this control less and try to manage memory automatically. We'll explore memory in detail in section 2 of this course, but we are covering the basics for now. At the end of the chapter, you'll have a much better understanding of computer memory, and you'll be able to use this in your code. 

You can imagine the computer's memory as a piece of lined paper with thousands of lines. When you declare a variable, you tell the computer to find an empty line to store the variable. This reserves the line for your variable. Nothing else will use this line now. The location in memory of a variable is called its address.

Imagine the computer finding an empty line and writing 7 on it.

int var_name = 7;

The computer knows you're referring to the line you requested to reserve when you use var_name.

Placing the & symbol in front of a variable gets its address.

std::cout << &var_name; // this outputs where it is stored. NOT its value

Note: The output differs every time you run it, depending on where there is space to place var_name.

Pointers

A pointer is a variable that holds an address. We can declare a pointer by adding an asterisk after the datatype.

int* var_ptr = &var_name;

Every pointer holds the same type of information (an address). The datatype shows the type of data it points to.

We have to dereference a pointer to use the value it points to. Adding an asterisk in front of the pointer gives us a reference to the variable it points to that we can use.

int* var_ptr = &var_name:
(*var_ptr) = 5; // This sets the value that var_ptr points to
std::cout << var_name; // This will now output 5

Pointers vs. References

Pointers are more flexible than references because we can reassign what they point to. Once a reference is assigned, we can't change the value it's tied to. This flexibility is especially useful in structs, where you have to initialize the reference variable in the constructor if you want it to work.

Nullptr

The nullptr value stands for null-pointer. You can assign a nullptr to any pointer.

int* pointer = nullptr;

A pointer that holds nullptr isn't pointing to anything. You can use this to check if a pointer is valid.

if (pointer == nullptr)
{
    std::cout << "ERROR: pointer is nullptr.\n"; 
}
else
{
    // Do stuff with `pointer` here
}

LValues & RValues

Pointers can only point to objects that have an identifiable location in memory. These values are known as LValues. You can place LValues on the left side of an assignment operator because they have an address that the computer can use to assign the value.

RValues are temporary values with no identifiable location in memory. Because of this, they aren't modifiable, and you can only use them on the right side of an assignment operator. The most common examples of RValues are literals (values like 1, 'A', etc.). Temporary results of expressions (x + y), function return values (that aren't references), and so on are also RValues. Pointers cannot point to them because they aren't stored in memory in the same way as LValues.

// ERROR: WON'T COMPILE
int* intpointer = &8; // Does not work because 8 is an RValue. It doesn't have a dedicated space in memory, so it has no address to store.

Local Allocation

The program destroys objects when no longer needed to free up resources. Using the address of a destroyed object is like searching for meaning in an alphabet soup. The data you're looking to use will be a random mess.

Local variables

The scope of a local variable determines its lifetime. When a function returns, the scope ends, destroying all local variables declared within it.

Example of dangerous function:

int* dangerousFunction() {
    int local = 29;
    return &local;  // Returning address of a local variable - leads to undefined behavior
}

This code will still compile. However, it returns the address of an object that's destroyed.

This behavior also carries over to objects declared within a block ({}). It doesn't matter if it's inside a function, an if statement, or a loop.

All member variables get destroyed with the object. If the program destroys an object, all pointers to its member variables become invalid.

Static variables are the exception to this. The program initializes static variables when it starts and destroys them when it ends, so returning an address to them will work.

Segmentation Faults

A segmentation fault (segfault) occurs when you try to use a pointer to a bad memory location. Using pointers that don't point to anything can have unforeseen consequences. In some cases, the memory is in use, and using the area of memory can lead to getting or changing random data. The program will crash in most cases. It's also common to encounter segfaults when trying to dereference a nullptr. You can avoid this by checking if a pointer is nullptr before using it.

You may also get a segfault when trying to access memory your program can't access. You can use the access operator ([])to get elements that don't exist in an array. This will get the data of wherever the element would be if it existed. Sometimes, this location won't be accessible by your program, and it will crash with a segfault.

New & Delete

The keywords new and delete give you complete control over the computer's memory. Using the new keyword lets you create an object in memory. You can store the address of the object when you create it.

// Dynamically allocates an integer and assigns the value 7 to it
int* ptr = new int;  
*ptr = 7;

Once done with a dynamically allocated object, you should free the allocated memory using delete. You should also reset the pointer if you aren't at the end of its scope.

delete ptr;  // Frees the memory allocated for the integer
ptr = nullptr;  // Good practice: nullify the pointer after deleting

Memory Leaks

Memory allocated with new isn't automatically deleted when it exits the scope. If we forget to delete a piece of memory before we stop using it, we reduce the program's available memory. This is called a memory leak. Avoiding memory leaks is a necessity when programming.

Pointers/Memory in Classes

Destructor

A destructor executes when an object is destroyed. Destructors have a syntax similar to the constructor. They use ´~´ followed by the name of the struct or class ~struct_name(). They are often used to clean up any memory the object might have reserved.

struct struct_name()
{
    // This code runs right before the object is destroyed
    ~struct_name() {
        std::cout << "struct_name destroyed! \n"; 
    }
};

Arrow Operator

As you get comfortable with pointers and start using them more, you might notice that you mostly use them for instances of structs/classes. The arrow operator solves the annoyance of having to dereference object pointers every time you want to work with them.

#include <iostream>

struct example_struct {
    int member_var;
    example_struct() : member_var(0) {}

    void print_var()
    {
        std::cout << member_var;
    }
};

int main()
{
    example_struct obj();
    example_struct* ptr = &obj;
  
    // These two do the same thing 
    (*ptr).print_var();
    ptr->print_var();
}

This

You have access to the this keyword whenever you're working inside a class or struct. this is a pointer to the current instance of the struct/class. You can use member functions and member variables more explicitly using this with the arrow operator.

struct struct_example {
    int member_var;

    void print_member_variable()
    {
        // These two lines of code are functionally identical. 
        std::cout << member_var;
        std::cout << this->member_var;
    }

You can use this to return the object you're working with as a value or reference.

class Car {
    std::string color;
    int wheels;

public:
    Car& setColor(const std::string& color) {
        this->color = color;
        return *this;
    }

    Car& setWheels(int wheels) {
        this->wheels = wheels;
        return *this;
    }
};

This structure lets you set multiple properties on the same line.

Car myCar;
myCar.setColor("Red").setWheels(4);  // Method chaining using `this`

Const Pointers

Pointers interact uniquely with the const keyword. Placing const in front of the datatype indicates that the pointer points to a const object. The object pointed to cannot be changed. However, you can still change what xptr points to.

const int x = 10;
const int* xptr = nullptr;
xptr = &x; //Reassigning doesn't give you an error because the pointer isn't const
*xptr = 5; // ERROR: This will fail because you're trying to reassign a const value

You can use this for functions that shouldn't change the passed data.


Placing the const after the pointer declaration shows that the pointer is const and can't be changed.

int x = 10;
int* const xptr = &x;
*x = 5; // This is fine. The object it points to isn't const
x = nullptr; // ERROR: Trying to reassign const

You can use this to modify the data pointed to while ensuring the address isn't changed. const pointers are rare because you lose many advantages of using a pointer instead of a reference by making them const.


You can also combine the two to have a const pointer that points to const data.

const int x = 10;
const int* const xptr = &x;

Technicalities of variables

The datatype of a variable determines how much space it will take and how the program treats the data in a location. Here's a more technical explanation of what happens when we declare a variable.

datatype variable_name = initial_value;
  1. The computer finds a spot in memory that fits the information the variable will store. This is known as a memory allocation. Every data type has a pre-determined size, determining how much memory is needed to hold it. There are different types of memory allocations. However, we will cover that after the advanced sections of the course.
  2. The computer notes down the location of the allocated memory, known as the address. This address is how the computer views the variable. When we refer to the variable_name, we are referring to its location in memory. Any operations on the variable translate to operations on the memory starting from its address and spanning the size of the datatype.
  3. If an initial_value is assigned, the memory at the variable's address is set to that value.
  4. If initial_value isn't assigned, the memory at the address will stay whatever it was when the memory was allocated. This can lead to unexpected code behavior if the variable is used before being initialized.

Questions

Questions

Questions

Questions

Questions

Questions

Questions

Questions

Questions