C++: The Basics - eps1.16_Pointer
Hello and welcome. As a child I often heard the saying:
“You don’t point a naked finger at clothed people!”.
But in programming, pointing at others is perfectly allowed. Especially in programming languages that are close to hardware. Languages like C++ even offer an extra data type for this: the Pointer or Zeiger in German.
This enables you to write high-level applications, i.e. applications with high abstraction, and still stay close to the machine. Once you have understood Pointer and also References, then you can write programs that use the resources of your system effectively. And that’s part of the challenge of developing embedded systems.
What is a pointer?
In eps1.3, we learned about variables as placeholders for different values in memory. They reserve space in memory for a value with specific data types, which can then be retrieved using the variable name.
Pointers are also variables. But unlike conventional variables, they do not hold a value, but addresses in memory. Like all variables, pointers naturally occupy memory space. The special thing about them is that the value they contain is interpreted as a memory address. A pointer is therefore a special variable that points to a location in memory.
Whew. Sounds pretty confusing at first.
What does the memory look like anyway?
Think of memory as an array with byte-sized elements. The array has (2^n) - 1
elements, where n
is the number of bits on the address bus of the processor used. So it can vary depending on the used CPU. The address of the first byte is 0
and the address of the last byte is (2^n) - 1
.
Typically, memory addresses are represented as hexadecimal numbers. You can recognize these numbers by the prefix ‘0x’. The hexadecimal number system is base 16
.
And what does this mean?
No matter if in the supermarket or on Amazon, everywhere the decimal number system is used for the representation of prices, weight, measurements or serial numbers. It is very familiar to us from our school days. Decimal means to the base 10
. So we can represent the 10 values 0-9
in one digit.
Consequently, hex numbers can represent 16 values in one digit. 0-9
are equal to the decimal values, but after that we still have the numbers A-F
. A
is the hexadecimal representation for 10
and F
stands for the value 15
.
If you now have the address 0xC
, then the 13th element of the memory (decimal 12) is meant. Hopefully you have not forgotten that in computer systems counting starts from 0
. I know that makes it confusing. But we have to get used to it.
That should be enough about memory at this point. We will look at it in more detail in a separate post. But for now, we know enough to understand pointers.
Declare a pointer and get an address
Now let’s look at how we can use pointers. Of course, since these are also variables, you have to declare them first.
Pointers point to an address in memory. At this address it expects a value of a certain data type. This is the PointedType, which precedes the PointerVariableName. So that the Pointer is to be distinguished clearly from a conventional variable, a Dereferenz-Operator *
stands between type and name. As is well known, the declaration is terminated with a semicolon ;
.
// listing1: pointer declarartion
// PointedType * PointerVariableName = reserved memory address to point to nowhere;
int * pointToInt = nullptr;
Just like all variables, the pointer gets assigned a random value if we don’t initialize it. But we don’t want it to point to any place in memory. Or worse, it points to an address range that doesn’t exist. That will crash your application.
Therefore we initialize it with a null pointer nullptr
. This ensures that the assigned pointer points to no valid, but also to no invalid address. In C++ there is an extra fixed value reserved for it and this allows pointer variables to be initialized safely.
And what address does my variable have?
You can find out with the reference operator &
. If you call a variable normally, its value is returned. However, if you prefix the variable name with &
, you get the address in memory where the variable is located.
In lisitng2 you can see the call to a variable and its address.
// listing2: determining the memory address
#include <iostream>
int main()
{
int intVar = 12;
const double doubleConst = 7.56;
// get the value
std::cout << "Value of integer variable is: " << intVar << std::endl;
std::cout << "Size of integer variable is: " << sizeof(intVar) << " Byte" << std::endl;
std::cout << "Value of double constant is: " << doubleConst << std::endl;
std::cout << "Size of double constant is: " << sizeof(doubleConst) << " Byte" << std::endl;
// Get the address in memory
std::cout << "Integer variable is located at: " << &intVar << std::endl;
std::cout << "Double constant is located at: " << &doubleConst << std::endl;
return 0;
}
# Output
Value of integer variable is: 12
Size of integer variable is: 4 Byte
Value of double constant is: 7.56
Size of double constant is: 8 Byte
Integer variable is located at: 0x7fff160b788c
Double constant is located at: 0x7fff160b7890
Earlier we said that an address contains one byte of data. You can see this very nicely in listing2. The integer variable inVar
is 4 bytes and 4 address blocks, after that is the next variable doubleConst
.
Working with pointers and accessing data
Intermediate state: We can now declare pointers and determine the address of variables. In addition, we know that pointers are variables in which addresses are stored.
Let’s combine these things and store in a pointer an address that we get using the Reference operator.
// listing3: using a pointer to store an address
#include <iostream>
int main()
{
// decalring a variable
int intVar = 12;
// declaring a pointer to type int and initialize to the address
int* pointToInt = &intVar;
std::cout << "Value of integer variable is: " << intVar << std::endl;
std::cout << "Value of integer pointer is: " << pointToInt << std::endl;
return 0;
}
# Output
Value of integer variable is: 12
Value of integer pointer is: 0x7ffeda83004c
Not so exciting at all. The output of listing3 is quite similar to listing2. The only difference is that the address of the integer variable is stored in a pointer variable and can be called there at any time.
Did you notice that the addresses change every time the program runs? That’s because your computer redistributes the data in memory each time it runs. Sometimes in the same place; but often somewhere else.
“Great, now I know the address of a variable…. But actually I’m only interested in its value…”
For this there is the dereference operator *
. With this you can access the value at the address to which the pointer points.
// listing4: access to the value
#include <iostream>
int main()
{
// decalring a variable
int intVar = 12;
// declaring a pointer to type int and initialize to the address
int* pointToInt = &intVar;
std::cout << "Value of intVar = " << intVar << std::endl;
std::cout << "Address of intVar = " << &intVar << std::endl;
std::cout << "Value of pointToInt: " << pointToInt << std::endl;
std::cout << "Stored value at the address pointToInt points to: " << *pointToInt << std::endl;
return 0;
}
# Output
Value of intVar = 12
Address of intVar = 0x7fff604b42cc
Value of pointToInt: 0x7fff604b42cc
Stored value at the address pointToInt points to: 12
If you use the dereference operator *
, then the application jumps to this address in memory and since it is an integer pointer, it takes 4 bytes (the size of an integer) starting there. You can already see how important the data types are here.
And you must not forget to initialize a pointer. Otherwise it will contain a random address to which your application may not have access correction. Your program will crash and you will get an Access Violation
.
Manipulating data and the size of a pointer
So far we have only used pointers to retrieve data and information. But we can also assign values to them.
// listing5: assigning a value to a pointer
#include <iostream>
int main()
{
int yourAge;
double yourHeight;
int* pointsToAge = &yourAge;
double* pointsToHeight = &yourHeight;
// store input at the memory pointed to
std::cout << "How old are you?" << std::endl;
std::cin >> *pointsToAge;
std::cout << "How tall are you (in m)?" << std::endl;
std::cin >> *pointsToHeight;
// displaying the values of the variables
std::cout << "You are "<< yourAge << " years old and "<< yourHeight << " meter tall." << std::endl;
return 0;
}
In listing5 the user’s input is not passed to a normal variable, but to a pointer. This in turn points to the address of the variable. For the assignment of the value to succeed, you have to dereference the address with the *
operator.
The output of the variables yourAge
and yourHeight
shows that the input via the dereferenced pointer stores the value in the memory area of the two variables. So you can use pointers to read and manipulate values in memory.
From your point of view, using pointers or variables may not make too much difference now. For the system, however, it does. This becomes clear when you look at the used memory.
// listing6: size of a pointer
#include <iostream>
int main()
{
std::cout << "Size of char: " << sizeof(char) << std::endl;
std::cout << "Size of pointer to char: " << sizeof(char*) << std::endl << std::endl;
std::cout << "Size of int: " << sizeof(int) << std::endl;
std::cout << "Size of pointer to int: " << sizeof(int*) << std::endl << std::endl;
std::cout << "Size of double: " << sizeof(double) << std::endl;
std::cout << "Size of pointer to double: " << sizeof(double*) << std::endl;
return 0;
}
# Output
Size of char: 1
Size of pointer to char: 8
Size of int: 4
Size of pointer to int: 8
Size of double: 8
Size of pointer to double: 8
We know that each datatype has a certain size. The char
data type occupies one byte in memory, an integer int
occupies 4 bytes and so on. Also the pointer variable has a fixed size in memory. And this must be able to capture every memory address. With 64 bit processors 8 bytes are needed for this. This is how much memory a pointer occupies.
It does not matter to which variable it points and which data type it has. The memory requirement of the pointer does not change. You can see this wonderfully in listing6.
Effect of increment and decrement operators
Finally, here’s a question that you may not have asked yourself. But do me the favor and deal with it for a tiny moment before you read the answer.
What happens when you increment (++) or decrement (–) a pointer?
If you don’t have a guess, just try it out!
// listing7: incrementing / decrementing a pointer
#include <iostream>
int main()
{
int anInt = 32;
int* pointsToInt = &anInt;
std::cout << "Address of anInt " << pointsToInt << std::endl;
pointsToInt++; // incrementing a pointer
std::cout << "Increased pointer " << pointsToInt << std::endl;
pointsToInt--; // decrementing a pointer
std::cout << "Decreased pointer " << pointsToInt << std::endl;
return 0;
}
# Output
Address of anInt 0x7fffffffd5fc
Increased pointer 0x7fffffffd600
Decreased pointer 0x7fffffffd5fc
A Pointer contains an address. In listing7 it is the address of the integer variable anInt
. For example, suppose that the memory address of the variable pointed to by the pointer pointsToInt
is 0x3c
.
The integer itself is 4 bytes in size. Thus the variable occupies a memory block with the four addresses 0x3c
, 0x3d
, 0x3e
,0x3f
.
What happens now if you increment the pointer?
It will not be incremented by one place from 0x3c
to 0x3d
. Then it would point into the middle of the data set of the variable and give useless information. But you surely already knew that yourself.
No, with the increment operator ++
the address of the pointer is increased by the size of the data type it points to. In our case these are the 4 bytes of integers.
The compiler interprets the increment statement on a pointer as a request to point to the following value after our integer variable. In doing so, it assumes that it is the same data type again. Thus, the compiler ensures that the pointer always points to the beginning of a set of data and never to the middle or end.
As you can see in listing7, the pointer pointsToInt
is incremented and then points to an address four bytes away. For our example, this means that it is incremented from 0x3c
to 0x40
.
The decrement operator --
has of course the same effect on a pointer, only in the opposite direction. Here the address is reduced by the size of the data type.
Dynamic memory allocation
Let’s declare an array. To do this, let’s briefly refresh our knowledge. An array frames multiple variables of the same data type in successive blocks of memory. So for the declaration we need the data type, the name of the array and the number of its elements.
// listing8: array declaration
int anArray[25];
Nothing new for you. But this creates two problems:
- you are limited to a fixed number of elements
- you always occupy the memory for all elements, even if you use less
An array is static. That means, it is fixed at compile time and remains unchangeable during program runtime. Unfortunately, this means that you are not using the system’s resources optimally.
To optimize memory usage based on user requirements, you need a dynamic allocation of memory capacities - Dynamic Memory Allocation. This means allocating more memory when it is needed and releasing it when there is less demand.
In the case of arrays, C++ offers you the solution in the form of vectors. But we can achieve dynamic memory allocation also with the help of pointers.
The two operators new
and delete
help us to do this. The operator new
lets you allocate new memory blocks. Usually it is used in such a way that it either returns the address in the form of a pointer if the memory request is successful or throws an exception otherwise.
To use new
, you must first specify the data type of the variable for which memory is to be freed. The address is then stored in a pointer. listing9 shows you how the whole thing looks for one and for several integer values at once.
// listing9: requesting memory with "new" operator
// request memory for one element
int* pointToNewInt = new int;
// request memory for a block of five integers
int* pointToFiveInt = new int[5];
So you can easily free a block for several variables of the same data type. Keep in mind, however, that there is no guarantee that your request for new memory will be successful and may even cause your program to crash. It depends on the current state of the system and the availability of memory.
// listing10: memory allocation fail
int main()
{
// request a large number of memory space
int* pointsToLargeArray = new int [0x1fffffffff];
// use the allocated memory
delete[] pointsToLargeArray;
return 0;
}
#Output
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
Aborted (core dumped)
Since C++11 you can easily catch such cases with std::nothrow. std::nothrow
is a constant used to resolve the overloads of throwing and non-throwing allocation functions.
That is, no exception is thrown on failed memory allocation. The pointer is assigned a nullptr
instead.
// listing11: std::nothrow
#include <iostream>
int main()
{
// request a large number of memory space
int* pointsToLargeArray = new(std::nothrow) int [0x1fffffffff];
if (pointsToLargeArray == nullptr)
{
std::cout << "Allocation returned nullptr\n";
}
else
{
// use the allocated memory
delete[] pointsToLargeArray;
}
return 0;
}
#Output
Allocation returned nullptr
Free memory again
If you don’t need the memory anymore, you should free it. Otherwise it will remain reserved for your application and cannot be used for anything else. Of course you can act selfish or inconsiderate now and say:
“The main thing is that my application has memory. I don’t give a…”
But this will reduce the available memory for the whole system and in the worst case may even slow down your application in execution. Unused but reserved memory is also called Memory leak and should always be avoided.
Therefore we release the memory with the delete
operator. Every memory allocation with new
should be deallocated by delete
. To do this, simply apply the operator to the pointer of the memory to be freed.
// listing12: releasing memory with "delete" operator
// releasing memory of one element
delete pointToNewInt;
// releasing memory of a block of several elements
delete[] pointToFiveInt;
Here delete
does not care about the data type. It is not interested in the number of elements in the memory block. You just have to tell it to release the whole block by appending square brackets []
.
There is one restriction to note. With delete
you can only free addresses in a pointer that have been allocated before with new
. The operator cannot be applied to other pointers.
This sticks
This was our first contact with pointers / pointers. Not so easy to understand at first, but extremely important! Somehow it is a variable, but actually it points to another one in memory. But it has a memory location itself.
We can now declare Pointers and assign memory addresses to it. For this we have learned about reference operator *
, reference operator &
. We now also know how to use it correctly and manipulate data with it.
In addition, we have looked at its size in memory and what happens when we increment or decrement it.
Now we’ve earned a break before we want to distinguish references from pointers. After all, the two are easy to confuse.
I wish you maximum success!
Sources
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017