4.7. Memory and pointers#
Before going into the pointers, a few concepts should be introduced in advance, such as ‘memory allocation’ and ‘memory address of a variable’.
The working memory (also called Random Access Memory or RAM) in your computer can be imagined as a sequence of storage boxes. Each box has a sequence number (a memory address) and a value. When a variable is declared during the execution of the program, some of these storage boxes are assigned to this variable to store its value. How many boxes a variable needs depends on de variable’s data type, see Section 4.3. During compilation, the compiler knows which variables will occur in each function, and can thus compute how much memory (storage boxes) are need to reserved when a function is called.
When your program is executed, the operating systems allocates memory for your program, and may even extend the memory usage if your program requires more (this depends on the type of memory you use, more on that in a moment).
4.7.1. Stack and heap memory#
We can distinguish three types of memory that a program uses:
Program code is read-only and contains the compiled program itself, i.e. the machine code with instructions for your computer’s processor.
Stack memory is used to keep track of all functions that are currently being called, and contains for each of those functions all its variables. The memory that a function occupies is called a stack frame, and the active stack frame belongs to the currently active function. When a called function has finished, its memory contents from the stack is released so that it can be reused for new function calls, and the previous function that initiated the call reactivates: its stack frame becomes active again. Since only one function is executing at a time, only stack frame is active at a time, and the content of those variables needs to have been copied into the active stack frame when the new function call started. This basically happens when you pass a variable as a parameter to a new function: the value of those variables are copied to the stack frame for the new function call. Regular variables in functions, as we have seen in this manual till now, all used stack memory.
Heap memory can hold much more data than stack memory, and unlike stack memory the data on the heap persists even if the function that allocated the memory finishes. This means it can be used to pass around large data structures between functions, without requiring any copying from one function stack frame to the next. While stack memory is automatically allocated and released whenever a function call starts or terminates, heap memory must be explicitly managed by the programmer. The programmer must specify when and how much heap memory must be allocated, but must also keep track when the allocated memory is no longer needed by the program and can be released for reuse.
The exact memory address of a variable in a program cannot be known when developing the program. Every time a new stack frame or heap variable requires allocating memory, the next available memory address will depend how full the stack or heap already is at that moment. In other words, the exact memory address of a variable is only known at runtime, and can be retrieved and tracked using pointers.
4.7.2. Pointers#
Now we can explain what a pointer is: It is simply a variable that stores a memory address.
There are various use cases for pointers. Some examples include:
To point to a location in a simply array or string; by incrementing the pointer we can step to the next element in the array.
To build complex and dynamic data structures, such as trees and graphs where nodes “point” to other nodes.
To store the address of the data from another variable, for example to pass it to function.
Functions can also return pointers, even to data allocated on the heap. A function can obtain such heap memory by using the
newcommand, which requests the operating system to reserve memory for the requested data type or array and produces a pointer to this new memory if successful. Heap memory can store large amounts of data, and it remains available even when the function that callednewhas finished. Your program can keep passing the pointer to any function that needs to access the data, but must explicitly release the memory by usingdeleteon the pointer when the memory will no longer be used to let the operating system reclaim the memory for other uses.
4.7.3. Obtaining the memory address of a variable#
You can ask the address of a variable by using the “address of” operator & as follows:
&[VARIABLE_NAME]
The following program piece prints the address of myvar to console.
int myvar = 5;
cout << &myvar << endl;
Here, since myvar is used with &, what you will see in the console
is not the value of myvar (which is 5), but the address of the memory
that stores this value.
4.7.4. Declaring pointers#
Now that we can retrieve a variable’s memory address, which is just data, we can store the address in a variable of the suitable pointer data type. Pointers are declared as:
[TYPE] * [POINTER_NAME];
Then you can store the address of a variable in this pointer as shown in the code piece below.
int myvar = 5;
int * mypointer = &myvar;
cout << mypointer << endl;
Note that you can not assign the address of an integer variable to an integer variable;
it has to be assigned to a variable that holds an integer pointer.
Here, mypointer stores the address of myvar, not the value of it.
Thus, what we will see as output will be the address of myvar again.
Note
The spaces between the data type, * operator, and variable name are optional,
so all of the following are the same:
int * mypointer;
int *mypointer;
int* mypointer;
4.7.5. Dereferencing pointers#
The dereferencing operator * can be used to retrieve the actual integer value at an address.
This operator can be translated into English as “value pointed by”.
Thus, *mypointer means, the value pointed by mypointer.
Since mypointer stores the address of myvar, it shows the value of myvar now.
Therefore, to see the value of myvar via the pointer, the cout line
should be modified as:
cout << *mypointer << endl;
The value of a variable can also be changed by the pointer that stores its address using the dereferencing operator.
int myvar = 5;
int * mypointer = &myvar; // here * is part of the pointer type declaration, int *, not a dereferencing operator
*mypointer = 10; // here * is the dereferencing operator that turns the pointer into a regular variable
cout << myvar << endl;
You will see that the value of myvar is changed. Note that the value is
changed by *pointer = 10; not with pointer = 10;, because mypointer
stores the address of myvar, but we want to change the value at that
address.
Exercise 4.16
Write the code below, but before running it try to guess the outputs of the program.
#include <iostream>
using namespace std;
int main()
{
int myvar1 = 10;
int myvar2 = 20;
int * mypointer1 = &myvar1;
int * mypointer2;
*mypointer1 = 15;
mypointer2 = mypointer1;
mypointer1 = &myvar2;
*mypointer1 = 30;
cout << mypointer1 << endl;
cout << mypointer2 << endl;
cout << *mypointer1 << endl;
cout << *mypointer2 << endl;
cout << myvar1 << endl;
cout << myvar2 << endl;
cout << &myvar1 << endl;
cout << &myvar2 << endl;
return 0;
}
So, by using pointers, we can reach the values of a variable using the variable’s address. Another important usage of pointers is as function arguments.
4.7.6. Passing pointers as function arguments#
From Section 4.5.4.3, you know that the variables inside a function are
created temporarily as the execution of the function begins. These
variables are destructed as the execution of the function ends. The only
value that you can send outside of the function is via return command.
However, sometimes you may want to pass an argument and keep the changes
that are made inside the function. Let’s examine the following code:
#include <iostream>
using namespace std;
void func1(int * ptr)
{
*ptr = 5;
}
void func2(int var)
{
var = 10;
}
int main()
{
int a = 1;
func1(&a);
func2(a);
cout << "a = " << a << endl;
return 0;
}
Let’s Look at func1 very carefully. It takes a pointer as an argument.
Therefore, we should use an address of a variable as an argument. Then,
the function changes the value pointed by the pointer ptr to 5. Now,
look at the main function. The address of the variable a is entered as
an argument to func1 using the ‘&’ operator. As you may expect, since
func1 uses directly the address of the variable, the line *ptr = 5;
will change the value of the variable a, although this change is done
inside the function. On the other hand, func2 has a standard argument,
and the change in var variable does not effect the variable a.
Therefore, we expect to see 5 at the console.
Functions can return pointers too. All you need to do is to declare the function as below:
[TYPE] * [FUNCTION_NAME] ([ARGUMENTS])
{
}
4.7.7. Arrays are pointers!#
There are other interesting functionalities of pointers. We have already worked with arrays. Now we can say that, arrays are nothing else than pointers. For example, when you declare an array as
int arr[10];
arr stores the address of the first element of the array. Following
that address, 10 places are reserved for 10 integers. Actually since
arr stores an address, you can equate it to a pointer:
int arr[3];
int * ptr;
ptr = arr;
Now, the pointer ptr stores the same address as arr.
Again in Section 4.3, we have read from / written to a specific element of the array
using [ ] operators. Actually this is nothing else than adding an
offset to the address that is stored in the pointer. Therefore, writing
the code piece below
int arr[3];
arr[0] = 10;
arr[1] = 50;
arr[2] = 100;
cout << arr[0] << endl;
cout << arr[1] << endl;
cout << arr[2] << endl;
will give exactly the same result as
int arr[3];
int * ptr;
ptr = arr;
*ptr = 10;
*(ptr+1) = 50;
*(ptr+2) = 100;
cout << arr[0] << endl;
cout << arr[1] << endl;
cout << arr[2] << endl;
The [ ] operator is also valid for pointers! Therefore, the code
below will also give the same result. Do note that this can lead to
undefined behaviour if not used on a pointer to an actual array!
int arr[3];
int * ptr;
ptr = arr;
ptr[0] = 10;
ptr[1] = 50;
ptr[2] = 100;
cout << arr[0] << endl;
cout << arr[1] << endl;
cout << arr[2] << endl;
4.7.8. Dynamic memory allocation with new and delete#
The usage of the new and delete commands to manage the memory for a single new instance of a certain data type is as follows:
TYPE * POINTER_NAME; // declare pointer variable
POINTER_NAME = new TYPE; // request new memory, and assign its address to pointer
...
delete POINTER_NAME; // release memory
We can also “allocate”, that is reserve, certain amount of memory by using the new command, which returns a pointer.
However, if you use new you must also at some point release the memory back to the system by calling delete on the memory address.
Warning
When you allocate memory using new, it reserves space on the heap.
If you do not call delete to release that memory, it remains allocated even after the pointer goes out of scope.
This is called a memory leak.
A memory leak happens when a program loses the ability to access memory it has allocated, without freeing it. Over time, the amount of inaccessable memory can accumulate and exhaust available memory, especially if memory leaks exist in often called functions or in main loops. Until your program really terminates, the leaked memory cannot be reused, reducing available memory for your program and even other programs. As memory usage grows, the system may slow down, and in long-running applications, memory leaks can eventually lead to application or system crashes.
4.7.9. Dynamic memory allocation for arrays#
We have learned in Section 4.3.6 that we cannot define the size of an array during program execution,
and that instead the array size should be a constant number known at compile time.
This is because the examples there used arrays that were defined on the stack (they did not use new and delete),
and the compiler needs to know the stack frame of each function in advance.
But heap memory is dynamically allocated, which means we can make an array with number of elements defined at runtime.
Heap allocation using new will allocate the necessary memory for us on the fly.
Here is an example of an array initialized with the new command, and how its pointer can be used:
unsigned int n;
cin >> n; // let number of elements depend on the user
int * ptr = new int[n];
if (n > 2) {
// set some values, if n is large enough
ptr[0] = 10; // one way of reaching the value
*(ptr+1) = 50; // another way of reaching the value
ptr[2] = 100;
}
// loop over all elements
for (int j = 0; j < n; j++) {
cout << j << ": " << ptr[j] << endl;
}
delete[] ptr; // don't forget to release the memory when done
Important
Note that if we used new to request an array of values, then we must use the special delete[] command
(it is literally written like that, without anything in the square brackets)
on the pointer, instead of the regular delete!
The general syntax for using new and delete with arrays is:
TYPE * POINTER_NAME; // declare pointer variable
// request array memory, where NUMBER_OF_ELEMENTS does _not_ have to be fixed value
POINTER_NAME = new TYPE[NUMBER_OF_ELEMENTS];
...
delete[] POINTER_NAME; // delete array memory
As was explained, if we need dynamic arrays, it is even better to use the vector class from the C++ Standard Library,
which will be discussed in Section 4.6.2, which provides more safety guarantees and utility functions like resizing
an already filled array.
Nevertheless, under the hood the vector class itself uses pointers and new and delete to manage its dynamic memory!
Although we only give examples for integer type, all things above also apply to other variable types.
Exercise 4.17
Create a function quadEquation() that calculates the solutions to
quadratic equations. The formula for calculating quadratic equations is
shown below.
Arguments: The coefficients a, b, c and two pointers to both solutions.
Returns: false, if no real solution is available, otherwise true.
Test the function by outputting the given quadratic equations and their solutions.
Solutions a quadratic equation: \(ax^2 + bx + c = 0\)
If the discriminant satisfies: \(b^2 -4ac >= 0\), the equation has real solutions \(x_{1,2} = (-b \pm sqrt(b^2 - 4ac)) / 2a\).
If the value of \((b^2 - 4ac)\) is negative, no real solution exists.
Test values for quadratic equation solutions
\(2x^2- 2x - 1.5 = 0\) -> \(x_1 = 1.5, x_2 = -0.5\)
\(x^2 - 6x + 9 = 0\) -> \(x_1 = 3.0, x_2 = 3.0\)
\(2x^2 + 2 = 0\) -> none