Book Image

C++ Fundamentals

By : Antonio Mallia, Francesco Zoffoli
Book Image

C++ Fundamentals

By: Antonio Mallia, Francesco Zoffoli

Overview of this book

C++ Fundamentals begins by introducing you to the C++ compilation model and syntax. You will then study data types, variable declaration, scope, and control flow statements. With the help of this book, you'll be able to compile fully working C++ code and understand how variables, references, and pointers can be used to manipulate the state of the program. Next, you will explore functions and classes — the features that C++ offers to organize a program — and use them to solve more complex problems. You will also understand common pitfalls and modern best practices, especially the ones that diverge from the C++98 guidelines. As you advance through the chapters, you'll study the advantages of generic programming and write your own templates to make generic algorithms that work with any type. This C++ book will guide you in fully exploiting standard containers and algorithms, understanding how to pick the appropriate one for each problem. By the end of this book, you will not only be able to write efficient code but also be equipped to improve the readability, performance, and maintainability of your programs.
Table of Contents (9 chapters)
C++ Fundamentals
Preface

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:

Figure 1.7: Scope of a variable

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;
}