Week 11_ Dynamic Memory
Week 11_ Dynamic Memory
Agenda
1. Pointers
○ Introduction to Pointers
■ What is a pointer?
■ Why are pointers important in C++?
2. Iterators
○ What is an Iterator?
○ Types of Iterators
■ Forward Iterator
■ Reverse Iterator
■ Random Access Iterator
3. Dynamic Memory
○ Introduction to dynamic memory allocation
○ Allocating memory using 'new' and 'delete' operators
○ Memory leaks and how to avoid them
1. Pointers
● Introduction to Pointers
What is a pointer?
Unlike a normal variable which may hold a value of a type like int or char, a
C++ pointer is a variable that stores the memory location of another
variable. It might be helpful to think of a pointer as a shortcut on a desktop.
A pointer stores the address of another variable and thus also gives us a way
to access and change the contents of the pointed-to variable.
Pointers in C++ are important for several reasons. A big consideration for
any programmer is the efficient use of memory. Pointers make it possible to
dynamically access and manipulate memory. With pointers, we can structure
memory access in custom ways. For example, we can use custom data
structures that do not otherwise fit into standard variable declarations.
Another example is efficient memory access for high-performance
applications.
For example, we can use pointer variables to modify and return by function,
a much better alternative to passing-by-value. Without pointers, we would
have to manually copy values, thus ending up with bulkier code. This works
fine when working with a single integer but can be more complex when
working with a more advanced data structure. Let’s take a look at an
example:
int main() {
int array[4];
int* foo = array;
createArray(array, 4);
for (int i = 0; i < 4; ++i) {
cout << *foo[i] << " ";
}
return 0;
} //Output: 1 2 3 4
As we saw above, pointers can also be used to point to, access and
manipulate the contents of a dynamic data structure like an array. While
pointers and arrays are not the same, an array name functions as a pointer.
Here, foo points to the first element of the array.
Pointers also uniquely allow for pointer operations or pointer arithmetic. You
can compare pointers not only for equality but also perform arithmetic
operations like < and <=. You can even add integers to pointers and subtract
one pointer from another.
In earlier versions of C++, the value specified for a null pointer was 0 or
NULL. NULL is defined in several standard library headers to represent the
value 0. Initializing a pointer to NULL is equivalent to initializing a pointer
to 0, but prior to C++11, 0 was used by convention. The value 0 is the only
integer value that can be assigned directly to a pointer variable without first
casting the integer to a pointer type (normally via a reinterpret_cast).
An example for pointer initialization:
The address operator (&) is a unary operator that obtains the memory
address of its operand.
So, we first get the memory address of the integer variable count and then
initialize the pointer variable countPtr with this address.
assigns the address of the variable y to pointer variable yPtr. Then variable
yPtr is said to “point to” y. Now, yPtr indirectly references variable y’s value
(5). The use of the & in the preceding statement is not the same as its use in
a reference variable declaration, where it’s always preceded by a data-type
name. When declaring a reference, the & is part of the type. In an expression
like &y, the & is the address operator. The following figure shows a
representation of memory after the preceding assignment. The “pointing
relationship” is indicated by drawing an arrow from the box that represents
the pointer yPtr in memory to the box that represents the variable y in
memory.
The following figure shows another pointer representation in memory with
integer variable y stored at memory location 600000 and pointer variable
yPtr stored at location 500000. The operand of the address operator must be
an lvalue—the address operator cannot be applied to literals or to
expressions that result in temporary values (like the results of calculations).
Using yPtr from the previous example, you can actually determine the value
of y, which in this case is 5. Accessing the value stored in a variable by using
a pointer is called indirection because you are indirectly accessing the
variable by means of the pointer. For example, you can use indirection with
the yPtr pointer to access the value in y. Indirection means accessing the
value at the address held by a pointer. The pointer provides an indirect way
to get the value held at that address.
Note:
With a normal variable, the type tells the compiler how much memory is
needed to hold the value. With a pointer, the type does not do this; all
pointers are the same size—it depends on the compiled machine code that
you produce either 32-bit or 64-bit (the default is 32-bit) you can choose this
setting during the compilation process and it depends on the specific
compiler you use, if you use 32-bit then all pointers will be 4-bytes in size
and if you use 64-bit then all pointers will be 8-bytes in size. The type tells
the compiler how much memory is needed for the object at the address, that
the pointer holds! In the declaration
Dereferencing pointers
The indirection operator (*) is also called the dereference operator. When a
pointer is dereferenced, the value at the address stored by the pointer is
retrieved.
Normal variables provide direct access to their own values. If you create a
new variable of type unsigned short int called yourAge, and you want to
assign the value in howOld to that new variable, you write:
A pointer provides indirect access to the value of the variable whose address
it stores. To assign the value in howOld to the new variable yourAge by way
of the pointer pAge, you write
The indirection operator (*) in front of the pointer variable pAge means “the
value stored at.” This assignment says, “Take the value stored at the address
in pAge and assign it to yourAge.” If you didn’t include the indirection
operator:
yourAge = pAge; // bad!!
The asterisk (*) is used in two distinct ways with pointers: as part of the
pointer declaration and also as the dereference operator.
When you declare a pointer, the * is part of the declaration and it follows the
type of the object pointed to. For example,
Also note that this same character (*) is used as the multiplication operator.
The compiler knows which operator to call based on how you are using it
(context).
Pointer arithmetic
This section describes the operators that can have pointers as operands and
how these operators are used with pointers. C++ enables pointer
arithmetic—a few arithmetic operations may be performed on pointers.
Pointer arithmetic is appropriate only for pointers that point to built-in array
elements.
Assume that int v[5] has been declared and that its first element is at
memory location 3000. Assume that pointer vPtr has been initialized to point
to v[0] (i.e., the value of vPtr is 3000). The following figure illustrates this
situation for a machine with four-byte integers. Variable vPtr can be
initialized to point to v with either of the following statements (because a
built-in array’s name implicitly converts to the address of its zeroth
element):
int* vPtr = v;
int* vPtr = &v[0];
In conventional arithmetic, the addition 3000 + 2 yields the value 3002. This
is normally not the case with pointer arithmetic. When an integer is added to,
or subtracted from, a pointer, the pointer is not simply incremented or
decremented by that integer, but by that integer times the size of the memory
object to which the pointer refers. The number of bytes depends on the
memory object’s data type. For example, the statement:
vPtr += 2;
would produce 3008 (from the calculation 3000 + 2 * 4), assuming that an
int is stored in four bytes of memory. In the built-in array v, vPtr would now
point to v[2]. If an integer is stored in eight bytes of memory, then the
preceding calculation would result in memory location 3016 (3000 + 2 * 8).
If vPtr had been incremented to 3016, which points to v[4], the statement:
vPtr -= 4;
would set vPtr back to 3000—the beginning of the built-in array. If a pointer
is being incremented or decremented by one, the increment (++) and
decrement (--) operators can be used. Each of the statements:
++vPtr;
vPtr++;
increments the pointer to point to the built-in array’s next element. Each of
the statements:
--vPtr;
vPtr--;
Pointer variables pointing to the same built-in array may be subtracted from
one another. For example, if vPtr contains the address 3000 and v2Ptr
contains the address 3008, the statement:
x = v2Ptr - vPtr;
In this section, we will learn how to swap or exchange two numbers using
pointers in C++. Pointers are used to keep the memory address of variables.
We will write one function that will take two pointers of two integer
variables and swap these integer variables by using the pointers.
1. Take the numbers as inputs from the user. Store these numbers in two
different integer variables.
4. With these three steps, the values of the variables will be swapped.
Below is the complete C++ program that swaps two user input numbers
using pointers:
#include <iostream>
using namespace std;
temp = *first;
*first = *second;
*second = temp;
}
int main() {
int a, b;
cout << "Before swap, a = " << a << ", b = " << b << endl;
swap(&a, &b);
cout << "After swap, a = " << a << ", b = " << b << endl;
}
Here,
2. We are taking the numbers as inputs from the user and these values
are assigned to the variables a and b respectively.
3. The method swap is used to swap two numbers using pointers. It takes
two integer pointers as the parameters and swaps the values stored in
the addresses pointed by these pointers.
b. It first assigns the value pointed by the variable first to the temp
variable.
4. This program is printing the values of the integers before and after the
numbers are swapped.
pointer_return_type function_name(parameters) {
statements;
}
//for example
int * MyFunction() {
statements;
}
Please note that, it is not a good idea to return the address of a local variable
outside the function as it goes out of scope after function returns. However,
this feature is useful if the local variable is defined as a static variable. A
static variable does not go out of scope even after the function returns and
preserves its values after each function call.
Example:
In the example below, a static variable is created inside the function and the
function returns the address of that variable.
#include <iostream>
using namespace std;
int * MyFunction() {
static int count = 0;
count++;
return &count;
}
int main () {
int * p;
for(int i = 1; i <= 3; i++) {
p = MyFunction();
cout << "Function returns: " << p << "\n";
cout << "Value of count: " << *p << "\n";
}
return 0;
}
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
int * MyFunction() {
static int rand_num[5];
return rand_num;
}
int main () {
int * p;
p = MyFunction();
for(int i = 0; i < 5; i++) {
cout << "(p + "<<i<<") = " << p+i << "\n";
cout << "*(p + "<<i<<") = " << *(p+i) << "\n";
}
return 0;
}
The output of the above code will be:
(p + 0) = 0x55beb3d2c170
*(p + 0) = 16
(p + 1) = 0x55beb3d2c174
*(p + 1) = 98
(p + 2) = 0x55beb3d2c178
*(p + 2) = 34
(p + 3) = 0x55beb3d2c17c
*(p + 3) = 87
(p + 4) = 0x55beb3d2c180
*(p + 4) = 56
2. Iterators
● What is an Iterator?
As we have observed, data structures have their unique ways of storing data,
so when applying algorithms to a particular data structure, it's essential to
consider how the data is stored inside the container and its ordering.
Unfortunately, attempting to implement an algorithm that works for all
containers can be frustrating. It requires adjusting the implementation for
each container, which can be a waste of time and energy, and it goes against
the concepts of reusability and extendibility.
● Types of Iterators
Iterators are divided into several types:
Forward Iterator
iterator = container.begin();
iterator++; // Go to next
element
iterator--; // Go to previous
element
Reverse Iterator
iterator = container.rbegin();
iterator++; // Go to previous
element
iterator--; // Go to next
element
The most powerful iterator that allows access to the elements at arbitrary
positions in constant time.
10 14 18 22 26
arr[0] => 10 + 0 * 4 = 10
arr[1] => 10 + 1 * 4 = 14
arr[2] => 10 + 2 * 4 = 18
int arr[5]; arr[3] => 10 + 3 * 4 = 22
arr[4] => 10 + 4 * 4 = 26
Consider that each cell represents a byte, and the gray cells have already
been allocated. We wish to allocate an array with 5 elements, which will
require 20 free bytes (5 elements x 4 bytes per element). It is important that
these 20 free bytes are consecutive, with no allocated bytes in between them.
This is beneficial because if we have the address of the first byte, we can
directly access any other element in the array.
3. Dynamic Memory
● Introduction to dynamic memory allocation
In the section, five areas of memory are mentioned:
○ Global namespace
○ The free store
○ Registers
○ Code space
○ The stack
Local variables are on the stack, along with function parameters. Code is in
code space, of course, and global variables are in the global namespace. The
registers are used for internal housekeeping functions, such as keeping track
of the top of the stack and the instruction pointer. Just about all of the
remaining memory is given to the free store, which is often referred to as the
heap.
Local variables don’t persist; when a function returns, its local variables are
destroyed. This is good because it means the programmer doesn’t have to do
anything to manage this memory space, but is bad because it makes it hard
for functions to create objects for use by other objects or functions without
generating the extra overhead of copying objects from stack to return value
to destination object in the caller. Global variables solve that problem at the
cost of providing unrestricted access to those variables throughout the
program, which leads to the creation of code that is difficult to understand
and maintain. Putting data in the free store can solve both of these problems
if that data is managed properly.
You can think of the free store as a massive section of memory in which
thousands of sequentially numbered cubbyholes lie waiting for your data.
You can’t label these cubbyholes, though, as you can with the stack. You
must ask for the address of the cubbyhole that you reserve and then stash
that address away in a pointer.
One way to think about this is with an analogy: A friend gives you the 800
number for Acme Mail Order. You go home and program your telephone
with that number, and then you throw away the piece of paper with the
number on it. If you push the button, a telephone rings somewhere, and
Acme Mail Order answers. You don’t remember the number, and you don’t
know where the other telephone is located, but the button gives you access
to Acme Mail Order. Acme Mail Order is your data on the free store. You
don’t know where it is, but you know how to get to it. You access it by using
its address—in this case, the telephone number. You don’t have to know that
number; you just have to put it into a pointer (the button). The pointer gives
you access to your data without bothering you with the details.
The stack is cleaned automatically when a function returns. All the local
variables go out of scope, and they are removed from the stack. The free
store is not cleaned until your program ends, and it is your responsibility to
free any memory that you’ve reserved when you are done with it. This is
where destructors are absolutely critical because they provide a place where
any heap memory allocated in a class can be reclaimed.
The advantage of the free store is that the memory you reserve remains
available until you explicitly state you are done with it by freeing it. If you
reserve memory on the free store while in a function, the memory is still
available when the function returns.
The return value from new is a memory address. Because you now know
that memory addresses are stored in pointers, it should be no surprise to you
that the return value from new should be assigned to a pointer. To create an
unsigned short on the free store, you might write
You can, of course, do this all on one line by initializing the pointer at the
same time you declare it:
In either case, pPointer now points to an unsigned short int on the free store.
You can use this like any other pointer to a variable and assign a value into
that area of memory by writing:
*pPointer = 72;
This means “Put 72 at the value in pPointer,” or “Assign the value 72 to the
area on the free store to which pPointer points.”
Note:
If new cannot create memory on the free store (memory is, after all, a
limited resource), it throws an exception, However, you can use the nothrow
object when you are requesting memory using the new keyword to return a
null pointer instead of throwing an exception if new cannot find enough
memory on the free store.
When you are finished with an area of memory, you must free it back to the
system. You do this by calling delete on the pointer. delete returns the
memory to the free store.
This situation is called a memory leak. It’s called a memory leak because
that memory can’t be recovered until the program ends. It is as though the
memory has leaked out of your computer.
To prevent memory leaks, you should restore any memory you allocate back
to the free store. You do this by using the keyword delete. For example:
delete pPointer;
When you delete the pointer, you are really freeing up the memory whose
address is stored in the pointer. You are saying, “Return to the free store the
memory that this pointer points to.” The pointer is still a pointer, and it can
be reassigned.
Most commonly, you will allocate items from the heap in a constructor, and
deallocate them in the destructor. In other cases, you will initialize pointers
in the constructor, allocate memory for those pointers as the object is used,
and, in the destructor, test the pointers for null and deallocate them if they
are not null.
Note:
When you call delete on a pointer, the memory it points to is freed. Calling
delete on that pointer again crashes your program! When you delete a
pointer, set it to nullptr. Calling delete on a null pointer is guaranteed to be
safe.
● Memory leaks and how to avoid them
Memory leaks are one of the most serious issues and complaints about
pointers. You have seen one way that memory leaks can occur. Another way
you might inadvertently create a memory leak is by reassigning your pointer
before deleting the memory to which it points.
Line 1 creates pPointer and assigns it the address of an area on the free store.
Line 2 stores the value 72 in that area of memory. Line 3 reassigns pPointer
to another area of memory. Line 4 places the value 84 in that area. The
original area—in which the value 72 is now held—is unavailable because
the pointer to that area of memory has been reassigned. No way exists to
access that original area of memory, nor is there any way to free it before the
program ends.
Note:
For every time in your program that you call new, there should be a call to
delete. It is important to keep track of which pointer owns an area of
memory and to ensure that the memory is returned to the free store when
you are done with it.