SU07 - Managing Memory
SU07 - Managing Memory
Contents
7.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1
7.1 Introduction
To date, all values of variables have been stored on the stack. In this study unit, we will
focus on memory and the management thereof. It will consider stack and heap memory,
and how values of types are “stored” on the stack and the heap. Depending on the
location of storage, how these values are accessed will be considered.
The study unit will revisit functions and specifically passing parameters by reference.
The study unit will conclude with a discussion of accessing string objects defined in the
string library with pointers.
2
We will consider the allocation of variables and their values both on the stack and on the
heap. When allocation is done on the stack, both the value and the variable is placed
on the stack. The memory address where the value of the variable is placed is called
the reference. In contrast, when heap memory is allocated, a pointer is placed on the
stack. This pointer points to a memory address in heap memory where the value that is
associated with the pointer resides.
7.2.1 References
A reference is another name for an existing variable. It links to the memory address where
the value represented by the variable resides. Consider the following example:
int x = 10;
int &ref = x;
In this code, x is a variable with value 10, stored on the stack. ref is a reference variable
that refers to the same memory location as x, and also stored on the stack. Changing the
value using either x or ref, changes the value stored at the memory address &x.
Remember, that when the variable goes out of scope, the memory on the stack is deallo-
cated. This means that if a reference is returned by a function and the function terminates,
the memory is no longer allocated to the particular variable.
7.2.2 Pointers
Pointers are useful due to them being allocated at run-time on the heap. It is therefore
not necessary to know how much memory is needed to be allocated during compile-time.
With pointers large memory-based structures, such as linked lists, trees and graphs can
be built and used at runtime, taking very little stack based memory to gain access to
3
the structures. Additional to having dynamic structures, there is typically more heap
memory available on the modern computer than there is stack memory.
A pointer is a variable that resides in stack memory that holds a memory address. This
memory address points to a location typically in heap memory that holds a value. For
example, int ∗p; defines a variable p in stack memory, that will hold a memory address on
the heap, once memory has been allocated. To allocate memory, the new operator is used.
In our example, p is defined as a pointer to a value of type int. Therefore, to allocate
memory to which p is pointing, the following statement is required: p = new int;. Once
heap memory has been allocated, values can be placed in the reserved memory. To do this
the statement ∗p = 10; can be used to allocate the integer 10 to the memory. Applying
the * operator to a pointer variable is referred to as dereferencing. A value, if known
when the memory is allocated, can be assigned when the new operator is called, that is
p = new int (10);. The following figure illustrates the memory after new has been called.
Consider the C++ code given below that defines a pointer p, assigns memory and the
value 10 to that memory location and then changes the value in the memory location to
30 before deleting the memory that was allocated and nulling the pointer.
int ∗p;
p = new int (10);
cout << p << ” ” << ∗p << endl;
∗p = 30;
cout << p << ” ” << ∗p << endl;
delete p;
p = NULL;
The series of C++ statements in the code given above are used to illustrate, in Figure 7.3,
how memory on the stack is statically allocated, we know we are storing a pointer on the
stack which will point to memory that needs to be dynamically allocated on the heap and
subsequently deallocated.
All memory that has been allocated with a new by the programmer must be deallocated
using the delete operator. This will release the memory back to the operating system
and free it up for either the same program to reuse or another program to use. Once the
memory has been freed, it is good programming practice to “null” the pointer variable.
This enables for comparison against a known value. To “null” the pointer variable, the
values of NULL, 0 or nullptr can be assigned to it. If no memory is being allocated, it is
best to assign NULL (or 0) to p.
4
Figure 7.3: Allocating memory on the heap
We will go into more depth about pointers when we look at dynamic arrays and structs.
These can become relatively large data types, and keeping a pointer on the stack to the
large structures on the heap conserves stack space and allows for dynamically growing
and shrinking structures to be managed on the heap.
5
Scenarios that often lead to segmentation faults includes:
int main() {
int∗ ptr = NULL; // Pointer initialized to NULL
std::cout << ∗ptr; // Dereferencing NULL pointer causes segfault
//Instead perform a check
if(ptr != NULL){
std::cout << ∗ptr;
}
return 0;
}
int main() {
int∗ p = new int(10);
delete p; // Memory freed
∗p = 5; // Accessing freed memory causes segfault
std::cout << ∗p;
return 0;
}
Buffer Overflow: Writing data beyond the allocated buffer can overwrite adjacent
memory, leading to unpredictable behaviour and potentially a segmentation fault.
Stack Overflow: Using too much stack memory, typically through deep or infinite
recursion, can lead to a stack overflow which is a type of segmentation fault. When this
happens, make sure that your base case for your recursion is correct.
#include <iostream>
using namespace std;
void recurse() {
int a = 0;
cout<<”Recurse”<<endl;
recurse(); // No ”end” to recursion
6
}
int main() {
recurse(); // Causes stack overflow
return 0;
}
• Validate Pointers: Before dereferencing pointers, check if they are NULL or have
been assigned valid memory addresses (use if statements).
7
• Functions with return types and no parameters.
Function overloading allows multiple functions to have the same name but different pa-
rameters, enabling variations in behaviour based on parameter input types or number.
Default parameters provide default values for function arguments, allowing functions to
be called with fewer arguments.
In this section, we will consider functions that accept references and pointers as parameters
and look at how this differs from using return values. We will consider when we need to
make parameters or variables immutable. First, a quick recap of pass by value.
int main() {
int age;
age = 18;
nextBirthday(age);
return 0;
}
Listing 7.1: nextBirthday pass by value and no return type
After calling the function nextBirthday, the value of age that is written to the console
has not changed from what it was before the function was called. One way to rectify this
is to write the function so that it returns the updated age. This function will also need to
pre-increment the parameter value to ensure the value returned has been incremented.
Refer to Listing 7.2.
8
int nextBirthday(int value) {
return ++value;
}
Listing 7.2: nextBirthday pass by value and int return type
This solution will require the calling code to be updated to age = nextBirthday(age);,
which is confusing to say the least and not the most readable and understandable code.
There are therefore instances where one would want to modify the value of a parameter (or
parameters) inside the function and have the change reflect after the successful execution
of the function. To facilitate this functionality, the parameters need to be defined as to
be passed by reference.
Question 7.1
How could nextBirthday be written so that post-incrementing (that is, value++) could
still be used.
Now, incrementing value in the function will result in the actual parameter being changed
and the change reflected in the calling code. Figure 7.4 illustrates how pass-by-value
(the top row of C++ instructions and their impact on the stack) and pass-by-reference
(bottom row of stack images) impacts the age value on the stack. As illustrated in the
figure, passing by reference will return the changes made to the actual parameter variable
in the function to be “sent back” to the calling code.
Pass by reference is useful, especially when a function manipulates more than one param-
eter value that needs to be accessible to the code that called the function. Remember
that a function can only return one value using the return statement.
If we can pass a value by reference as a parameter, is it possible for a function to return
a value by reference? Consider the implementation of the nextBirthday function given
below.
int& nextBirthday(int &value) {
value++;
return value;
}
Listing 7.4: nextBirthday pass by reference and a reference to an int return type
9
Figure 7.4: Call to the three versions of the nextBirthday function
You may assume that the function is called as follows with age defined as in Listing 7.1:
10
Figure 7.5: Calling the function int& nextBirthday(int &value)
Example
#include <iostream>
int main() {
int x, y;
x = 10;
y = 20;
11
swop(x,y);
return 0;
}
When the swop function is called with arguments x and y, the parameters a and b inside
the swop function become aliases for x and y respectively. This means that a and b do
not have their own separate memory locations; instead, they refer to the same memory
locations as x and y. Therefore, any modifications to a or b directly affect x and y.
This direct referencing is achieved using the reference operator (&) in the function dec-
laration, which ensures that the variables are not copied but rather referenced directly.
This is why after the swop function executes, the values of x and y in the main program
are exchanged. The memory that is being referred to by a and b inside the swop function
is the same memory that holds x and y.
Figure 7.6: Call to the Function swop - prior to execution of the body of swop
When calling functions that use pass by reference, the actual arguments must be vari-
ables that exist in memory, because the reference operator (&) in the function’s signature
requires a memory address to bind to. A reference needs an existing object (or variable)
to refer to.
Passing literals (such as 5, 10.5, or ’a’) or expressions (like x + y or z * 2) directly
into a function that expects references will result in a compilation error. This is because
these values are not at a specific memory address that the reference can bind to, unlike
variables which do have fixed memory locations.
The following would cause a compilation error:
#include <iostream>
12
}
int main() {
increment(5); // Error: cannot bind non−const lvalue reference to an rvalue
return 0;
}
Listing 7.5: Incorrect Call to Pass by Reference Function
int main() {
int value = 5;
increment(value); // Correct usage
std::cout << ”Incremented value: ” << value << std::endl;
return 0;
}
Listing 7.6: Correct Call to Pass by Reference Function
Since the age variable is allocated on the stack we’re not dealing with any heap allocations
for this particular case, so all memory will be on the stack.
13
Figure 7.7: Calling the function nextBirthday(int *value)
• (*value)++ in the function dereferences the pointer (accesses the memory location
the pointer refers to) and increments the integer stored there.
You will notice that there is not much difference between the second row of Figure 7.4 and
Figure 7.7. nextBirthday is this case is called with a reference to a value on the stack,
the variable value therefore holds this reference which points to where age is stored in
memory. To increment where value is pointing to, we need to dereference value (*value)
and increment the integer being pointed to (*value)++. Note the use of the brackets to
ensure that the integer is incremented and not the pointer. This is as a result of the
precedence of the operators, ++ has a higher precedence than what dereference (*) has.
When a function allocates memory on the heap and returns the pointer to the memory on
the heap, the onus is on the code that has called the function to clean the heap memory
and no longer on the function that called new.
7.3.5 Constness
Constness refers to the immutability of variables and parameters. This means they
cannot change. We have used the keyword const to define constant variables such as
const float PI = 3.1415; in Study Unit 3. Later in this study unit you will see how
const is used to ensure that memory addresses and values being pointed to in memory
cannot be changed. In this section we will focus on defining the constness of function
parameters.
Consider the following example.
int square(const int x) {
return x ∗ x;
}
Listing 7.7: const Function Parameters
In this example x is an int that is constant (read the parameter definition from right to
left). This means, within the function the value of x cannot be changed. When pass-
by-value without the const keyword, we could change the value of x inside the function
14
without changing the value of the actual parameter with which the function was called.
For example, int a = 10; square(a);. Making the value a constant means that the value
cannot be changed within the function. Therefore, x +=2; within the function body
would not compile.
If the parameter in Listing 7.7 was defined as const int& x, then the value of x cannot
be changed in the body of the function. Effectively making it equivalent to passing the
parameter by value.
Question 7.2
How does constness impact references and pointers when applied to functions? Exper-
iment with a few functions to see what the outcome is. Here is a sample of possible
functions to experiment with.
15
i.e. a number of characters next to each other in memory. With pointers, you have the
ability to iterate over these characters for various manipulations.
C++ differentiates between small strings and strings that are not considered small. A
small string is stored on the stack, while all other strings are stored on the heap. For
the purposes of this study unit, we will assume that all strings are stored on the heap.
A variable is therefore stored on the stack that points to the memory address that the
first character of string is store in. Enough memory is allocated to store the number
of characters that make up the specific string to be stored, plus the string termination
character ’\0’. The string "Hello World!", will therefore take 12 characters (so 12 *
sizeof(char) bytes) to store in memory.
To access the first character of a string that is defined by string myString = "Hello
World";, you can treat the string as an array (details of arrays will be discussed in Study
Unit 8) and use indexing. The statement myString[0] is the first character in the string,
whereas &myString is the reference to where the first character of the string is allocated
in memory.
The termination character is another really important character when it comes to ma-
nipulating strings. As strings are variable in length, it indicates where the string ends in
memory.
Question 7.3
What will &myString[0] result in when written out to the console?
Never increment or decrement the pointer used to create the string. Doing this will result
in you loosing your access to the beginning of the string. Always assign the beginning of
the string to a different pointer variable, specifically a variable of char*.
16
Figure 7.8: Allocating strings
Question 7.4
Write the corresponding while loop to iterate over the strPtr as previously defined using
an index.
Rather than iterating over the string with an index, we can use pointer arithmetic. This
will require a variable of char * to be assigned to the first character of the string. At
the end of each iteration of the loop, the pointer is incremented by the size of a single
character. Below is the code to iterate over the string represented by myString is given
on the left and strPtr is on the right.
The next sections will provides exampled of iterating over a string to print the string,
determine the length of the string and to revers the string respectively.
To write a function to print the string using pointers, the function will need to pass the
string by reference to make provision for both the string variables. The implementation
of the function is based on the two implementations previously given. The
void printString(string &str) {
char ∗current = &str[0];
while (∗current != ’\0’) {
17
cout << ∗current;
current++;
}
cout << endl;
}
As the function accepts a reference to the string as a parameter, for myString the function
is called directly using the variable as is. That is: printString(myString);. For strPtr,
the dereferenced version of the variable is used, that is printString(∗strPtr);.
The function to determine the length of a string is similar to printing a string. The func-
tion accepts a reference to a string as a parameter and returns an integer representing the
number of characters that string comprises of excluding the string termination character.
The listing below provides the implementation of a stringLength function that makes
use of pointer arithmetic.
int stringLength(string &str) {
int len = 0;
char ∗current;
current = &str[0];
while (∗current != ’\0’) {
len++;
current++;
}
return len;
}
To reverse a string using pointers, you use two pointers: one pointing at the start of the
string and the other at the end. You then swop the characters at these pointers, moving
them towards the center until they meet or cross each other.
The following function will use pointers to reverse an object of type string.
void reverseString(string &str) {
if (stringLength(str) != 0) {
char∗ end = &str[0]; // Pointer to the end of the string
// Move the ’end’ pointer to the last character (\0) of the string
while (∗end != ’\0’) {
end++;
}
end−−; // Set ’end’ to the last character (not the terminator)
18
// Swop the characters until ’start’ and ’end’ pointers meet or cross
while (start < end) {
swop(∗start,∗end);
start++;
end−−;
}
}
}
Listing 7.8: Reversing a string object
reverseString(∗strPtr);
cout << ”strPtr reversed = ” << ∗strPtr << endl;
Question 7.5
Write the swop function. The definition of the function is given by: void swop(char &a, char &b);
The function to reverse an object of type string, given in Listing 7.8 does not work for
strings that are defined as c-strings. That is an array of characters defined in the following
manners:
char str[] = ”Hello, world!”; // As an array of characters
// Swop the characters until ’start’ and ’end’ pointers meet or cross
while (start < end) {
19
swop(∗start,∗end);
start++;
end−−;
}
}
}
Listing 7.9: Reversing strings - string class and c-string definitions
Below are examples of how the function can be called based on how the string is defined.
string myString = (”Hello World”);
reverseString(&myString[0]); // Get the address of the first character of the string
cout << ”myString reversed = ” << myString << endl;
Caution! Using pointers for string manipulation requires careful management of memory
bounds to avoid common errors such as segmentation faults or memory leaks. Always
make sure that your pointer operations remain within the valid range of the string and
take special care not to dereference NULL pointers.
Date Change
09 May 2024 Initial preparation and presentation of the document. Preliminary version 0.1
22 May 2024 Added images and text.
20
Operator Associativity
0 postfix ++, postfix -- →
1 !, unary -, unary +, prefix ++, prefix -- ←
*a (dereference), &a (address-of)
2 /, *, % →
3 +, -, || →
4 <, <=, >, >= →
5 ==, ! = →
6 && →
7 || →
8 =, + = ←
21