Pointers and References
In the previous section, variables have been defined as portions of memory that can be accessed by their name. In this way, the programmer does not need to remember the memory location and size that's reserved, but can conveniently refer to the variable name.
In C++, the way to retrieve the actual memory address of a variable is done by preceding the variable name with an ampersand sign (&), also known as the address-of operator.
The syntax to use the concept of the address-of operator is as follows:
&variable_name
Using this in code will return the physical memory address of the variable.
Pointers
A data structure that's capable of storing a memory address in C++ is known as a pointer. A pointer always points to an object of a specific type, and because of that we need to specify the type of the object that's pointed to when declaring the pointer.
The syntax to declare a pointer is as follows:
type * pointer_name;
Multiple declarations in the same statement are also possible when it comes to a pointer, but it is important to remember that an asterisk (*) is needed for each pointer declaration. An example of multiple pointer declaration is as follows:
type * pointer_name1, * pointer_name2, *...;
When the asterisk is specified only for the first declaration, the two variables will have different types. For example, in the following declaration, only the former is a pointer:
type * pointer_name, pointer_name;
Note
Independently of the pointed variable type, a pointer will always occupy the same size in memory. This derives from the fact that the memory space needed by the pointer is not related to a value stored by the variable, but to a memory address that is platform-dependent.
Intuitively, a pointer assignment has the same syntax as any other variable:
pointer_name = &variable_name;
The previous syntax will copy the memory address of the variable_name variable into the pointer named pointer_name.
The following code snippet will first initialize pointer_name1 with the memory address of variable_name, and then it initializes pointer_name2 with the value stored in pointer_name1, which is the memory address of variable_name. As a result, pointer_name2 will end up pointing to the variable_name variable:
type * pointer_name1 = &variable_name; type * pointer_name2 = pointer_name1;
The following implementation is invalid:
type * pointer_name1 = &variable_name; type * pointer_name2 = &pointer_name1;
This time, pointer_name2 would be initialized with the memory address of pointer_name1, resulting in a pointer that points to another pointer. The way to point a pointer to another pointer is to use the following code:
type ** pointer_name;
Two asterisks (*) indicate the type that's pointed is now a pointer. In general, the syntax simply requires an asterisk (*) for each level of indirection in the declaration of the pointer.
To access the actual content at a given memory address, it is possible to use the dereference operator (*), followed by the memory address or a pointer:
type variable_name1 = value; type * pointer_name = &variable_name1; type variable_name2 = *pointer_name;
The value contained by variable_name2 is the same as the one contained by variable_name1. The same applies when it comes to assignment:
type variable_name1 = value1; type * pointer_name = &variable_name1; *pointer_name = value2;
References
Unlike a pointer, a reference is just an alias for an object, which is essentially a way to give another name to an existing variable. The way to define a reference is as follows:
type variable_name = value; type &reference_name = variable_name;
Let's examine the following example:
#include <iostream> int main() { int first_variable = 10; int &ref_name = first_variable; std::cout << "Value of first_variable: " << first_variable << std::endl; std::cout << "Value of ref_name: " << ref_name << std::endl; } //Output Value of first_variable: 10 Value of ref_name: 10
We can identify three main differences with pointers:
Once initialized, a reference remains bound to its initial object. So, it is not possible to reassign a reference to another object. Any operations performed on a reference are actually operations on the object that has been referred.
Since there is not the possibility to rebind a reference, it is necessary to initialize it.
References are always associated with a variable that's stored in memory, but the variable might not be valid, in which case the reference should not be used. We will see more on this in the Lesson 6, Object-Oriented Programming.
It is possible to define multiple references to the same object. Since the reference is not an object, it is not possible to have a reference to another reference.
In the following code, given that a is an integer, b is a float, and p is a pointer to an integer, verify which of the variable initialization is valid and invalid:
int &c = a; float &c = &b; int &c; int *c; int *c = p; int *c = &p; int *c = a; int *c = &b; int *c = *p;
The const Qualifier
In C++, it is possible to define a variable whose value will not be modified once initialized. The way to inform the compiler of this situation is through the const keyword. The syntax to declare and initialize a const variable is as follows:
const type variable_name = value;
There are several reasons to enforce immutability in a C++ program, the most important ones being correctness and performance. Ensuring that a variable is constant will prevent the compilation of code that accidentally tries to change that variable, preventing possible bugs.
The other reason is that informing the compiler about the immutability of the variable allows for optimizing the code and logic behind the implementation of the code.
Note
After creating an object, if its state remains unchanged, then this characteristic is known as immutability.
An example of immutability is as follows:
#include <iostream> int main() { const int imm = 10; std::cout << imm << std::endl; //Output: 10 int imm_change = 11; std::cout << imm_change << std::endl; //Output: 11 imm = imm_change; std::cout << imm << std::endl; //Error: We cannot change the value of imm }
An object is immutable if its state doesn't change once the object has been created. Consequently, a class is immutable if its instances are immutable. We will learn more about classes in Lesson 3, Classes.
Modern C++ supports another notion of immutability, which is expressed with the constexpr keyword. In particular, it is used when it is necessary for the compiler to evaluate the constant at compile time. Also, every variable declared as constexpr is implicitly const.
The previous topic introduced pointers and references; it turns out that even those can be declared as const. The following is pretty straightforward to understand, and its syntax is as follows:
const type variable_name; const type &reference_name = variable_name;
This syntax shows how we can declare a reference to an object that has a const type; such a reference is colloquially called a const reference.
References to const cannot be used to change the object they refer to. Note that it is possible to bind a const reference to a non-const type, which is typically used to express that the object that's been referenced will be used as an immutable one:
type variable_name; const type &reference_name = variable_name;
However, the opposite is not allowed. If an object is const, then it can only be referenced by a const reference:
const type variable_name = value; type &reference_name = variable_name; // Wrong
An example of this is as follows:
#include <iostream> int main() { const int const_v = 10; int &const_ref = const_v; //Error std::cout << const_v << std::endl; //Output: 10 }
Just like for references, pointers can point to the const object, and the syntax is also similar and intuitive:
const type *pointer_name = &variable_name;
An example of this is as follows:
#include <iostream> int main() { int v = 10; const int *const_v_pointer = &v; std::cout << v << std::endl; //Output: 10 std::cout << const_v_pointer << std::endl; //Output: Memory location of v }
const object addresses can only be stored in a pointer to const, but the opposite is not true. We could have a pointer to const point to a non-const object and, in this case, like for a reference to const, we are not guaranteed that the object itself will not change, but only that the pointer cannot be used to modify it.
With pointers, since they are also objects, we have an additional case, which is the const pointer. While for references saying const reference is just a short version of reference to const, this is not the case for the pointer and has a totally different meaning.
Indeed, a const pointer is a pointer that is itself constant. Here, the pointer does not indicate anything about the pointed object; it might be either const or non-const, but what we cannot change instead is the address pointed to by the pointer once it has been initialized. The syntax is as follows:
type *const pointer_name = &variable_name;
As you can see, the const keyword is placed after the * symbol. The easiest way to keep this rule in mind is to read from right to left, so pointer-name > const > * > type can be read as follows: pointer-name is a const pointer to an object of type type. An example of this is as follows:
#include <iostream> int main() { int v = 10; int *const v_const_pointer = &v; std::cout << v << std::endl; //Output: 10 std::cout << v_const_pointer << std::endl; //Output: Memory location of v }
Note
Pointer to const and const to pointer are independent and can be expressed in the same statement:
const type *const pointer_name = &variable_name;
Note
The preceding line indicates that both the pointed object and the pointer are const.
The Scope of Variables
As we have already seen, variable names refer to a specific entity of a program. The live area of the program where this name has a particular meaning is also called a scope of a name. Scopes in C++ are delimited with curly braces, and this area is also called a block. An entity that's declared outside of any block has a global scope and is valid anywhere in the code:
The same name can be declared in two scopes and refers to different entities. Also, a name is visible once it is declared until the end of the block in which it is declared.
Let's understand the scope of global and local variables with the following example:
#include <iostream> int global_var = 100; //Global variable initialized int print(){ std::cout << global_var << std::endl; //Output: 100 std::cout << local_var << std::endl; //Output: Error: Out of scope } int main() { int local_var = 10; std::cout << local_var << std::endl; //Output: 10 std::cout << global_var << std::endl; //Output: 100 print(); //Output:100 //Output: Error: Out of scope }
Scopes can be nested, and we call the containing and contained scope the outer and inner scope, respectively. Names declared in the outer scope can be used in the inner one. Re-declaration of a name that was initially declared in the outer scope is possible. The result will be that the new variable will hide the one that was declared in the outer scope.
Let's examine the following code:
#include <iostream> int global_var = 1000; int main() { int global_var = 100; std::cout << "Global: "<< ::global_var << std::endl; std::cout << "Local: " << global_var << std::endl; } Output: Global: 1000 Local: 100
In the next chapter, we will explore how to use local and global variables with functions.
In the following code, we will find the values of all the variables without executing the program.
The following program shows how variable initialization works:
#include <iostream> int main() { int a = 10; { int b = a; } const int c = 11; int d = c; c = a; }