Appendix A

Intermediate C++ Review

This appendix provides a quick review of intermediate C++ concepts used throughout the book. The concepts roughly correspond to typical topics in Computer Science 1 and 2 courses. If you are rusty with C++, you should spend extra time reviewing this material.

References, Pointers, and Arrays

Although references, pointers, and arrays might seem like separate concepts, they are closely related. Furthermore, because pointers are often stumbling points for C++ programmers, it’s worthwhile to spend some time reviewing their intricacies.

References

A reference is a variable that refers to another variable that already exists. To denote a variable as a reference, add an & immediately after the type. For example, here is how you can declare r as a reference to the already existing integer i:

int i = 20;
int& r = i; // r refers to i

By default, functions pass parameters by value (pass-by-value), meaning that when you call a function, the parameter copies to a new variable. When passing by value, modifications to parameters do not persist beyond the function call. For example, here is an (incorrect) implementation of a Swap function that swaps two integers:

void Swap(int a, int b)
{
   int temp = a;
   a = b;
   b = temp;
}

The problem with Swap is that a and b are copies of the parameters, which means the function cannot truly swap the parameters as desired. To solve this problem, you should instead declare the parameters of Swap as references to integers:

void Swap(int& a, int& b)
{
   // (The body of the function is identical to the previous)
}

When passing a parameter by reference (pass-by-reference), any changes made to that parameter within the function will persist after the function ends.

One caveat is that because a and b are now references to integers, they must reference existing variables. You can’t pass in temporary values for the parameters. For example, Swap(50,100) is invalid because 50 and 100 are not declared variables.

warning

PASS-BY-VALUE IS DEFAULT: By default, all parameters in C++, even objects, pass by value. In contrast, languages like Java and C# default to passing objects by reference.

Pointers

To understand pointers, it first helps to remember the way computers store variables in memory. During program execution, entering a function automatically allocates memory for local variables in a segment of memory called the stack. This means that all local variables in a function have memory addresses known to the C++ program.

Table A.1 shows code snippets and possible locations of their variables in memory. Notice that each variable has an associated memory address. The table shows the memory addresses in hexadecimal simply because that’s the typical notation for memory addresses.

Table A.1 Variable Storage

Code

Variable

Memory Address

Value

int x = 50; x 0xC230 50
int y = 100; y 0xC234 100
int z = 200; z 0xC238 200

The address-of operator (also &) queries the address of a variable. To get the address of a variable, place a & in front of the variable. For example, given the code in Table A.1, the following code outputs the value 0xC234:

std::cout << &y;

A pointer is a variable that stores an integral value corresponding to a memory address. The following line declares the pointer p that stores the memory address of the variable y:

int* p = &y;

The * after the type signifies a pointer. Table A.2 shows the pointer p in action. Note that, just like any other variable, p has both a memory address and a value. But because p is a pointer, its value corresponds to the memory address of y.

Table A.2 Variable Storage (with Pointers)

Code

Variable

Memory Address

Value

int x = 50;

x

0xC230

50

int y = 100;

y

0xC234

100

int z = 200;

z

0xC238

200

int* p = &y;

p

0xC23C

0xC234

The * operator also dereferences a pointer. Dereferencing a pointer accesses the memory “pointed to” by the pointer. For example, the last line in Table A.3 changes the value of y to 42. This is because dereferencing p goes to the memory address 0xC234, which corresponds to the location of y in memory. Thus, writing the value 42 at this memory address overwrites the value of y.

Table A.3 Variable Storage (with Dereferencing)

Code

Variable

Memory Address

Value

int x = 50;

x

0xC230

50

int y = 100;

y

0xC234

42

int z = 200;

z

0xC238

200

int* p = &y;

p

0xC23C

0xC234

*p = 42;

Unlike references, which must refer to something, pointers can point to nothing. A pointer that points to nothing is a null pointer. To initialize a pointer as null, use the nullptr keyword, as in the following code:

char* ptr = nullptr;

Dereferencing a null pointer crashes the program. The error message varies depending on the operating system, but typically “access violation” or “segmentation fault” errors occur when dereferencing a null pointer.

Arrays

An array is a collection of multiple elements of the same type. The following code declares an array of 10 integers called a and then sets the first element in the array (index 0) to 50:

int a[10];
a[0] = 50;

By default, the elements in an array are uninitialized. While you could manually initialize each element in an array, it’s more convenient to use either the initializer syntax or a loop. The initializer syntax uses braces, like this:

int fib[5] = { 0, 1, 1, 2, 3 };

Alternatively, you could use a loop. The following initializes each of the 50 elements in array to 0:

int array[50];
for (int i = 0; i < 50; i++)
{
   array[i] = 0;
}

warning

ARRAYS DON’T BOUND CHECK: Requesting invalid indices can lead to memory corruption and other errors. Several tools exist to help find bad memory accesses, such as the AddressSanitizer tool available in Xcode.

C++ stores arrays contiguously in memory. This means that the data for index 0 is right next to the data for index 1, which is right next to index 2, and so on. Table A.4 shows an example of a five-element array in memory. Keep in mind that the variable array (without the subscript) references the memory address of index 0 (in this case, 0xF2E0). Because of this, you can pass a single-dimensional array to a function via a pointer.

Table A.4 An Array in Memory

Code

Variable

Memory Address

Value

int array[5] = {
   2, 4, 6, 8, 10
};

array[0]

0xF2E0

2

array[1]

0xF2E4

4

array[2]

0xF2E8

6

array[3]

0xF2EC

8

array[4]

0xF2F0

10

You also can declare multidimensional arrays. For example, the following code creates a 2D array of floats with four rows and four columns:

float matrix[4][4];

To pass a multidimensional array into a function, you must explicitly specify the dimensions, like this:

void InvertMatrix(float m[4][4])
{
   // Code here...
}

Dynamic Memory Allocation

As discussed earlier, memory allocation for local variables is automatic in C++. These variables end up in memory on the stack. This is great for temporary variables and function parameters. However, local variables are sometimes not enough.

First, the stack has a limited amount of memory available—typically much less than the amount of memory an average program might want to use. For example, the Microsoft Visual C++ compiler has a default stack size of 1 MB. Such a small amount of memory won’t be enough for all but the simplest games.

Second, local variables have a fixed lifetime. They are only available from the point of declaration until the end of the containing scope. This scope is typically within a function, as global variables are stylistically undesirable.

In dynamic memory allocation, the programmer controls allocation and deallocation of variables in memory. Dynamic allocations go into the heap, which is a separate part of memory. The heap is much larger in size compared to the stack (several gigabytes on current machines), and data on the heap persists until either the programmer deletes the data or the program ends.

Recall that in C++, the new and delete operators allocate and deallocate memory on the heap. The new operator allocates memory for the requested type of variable, and for classes and structs, it calls the constructor. The delete operator performs the opposite: It calls the destructor for class/struct types and deallocates the memory for the variable.

For example, this code dynamically allocates memory for a single int variable:

int* dynamicInt = new int;

To free the memory of a dynamically allocated variable, use delete:

delete dynamicInt;

Forgetting to delete a dynamically allocated variable causes a memory leak, meaning that the memory is unusable for the remaining life span of the program. For programs that run for a long time, small memory leaks accumulate over time and eventually cause the heap to run out of memory. If the heap runs out of memory, the program will almost always crash soon afterward.

Of course, dynamically allocating just a single integer doesn’t take advantage of all the available memory on the heap. You can also dynamically allocate arrays:

char* dynArray = new char[4*1024*1024];
dynArray[0] = 32; // Set the first element to 32

Note that when dynamically allocating an array, you place square brackets immediately after the type and specify the size there. Unlike with a statically allocated array, with a dynamically allocated array, you can specify a size at runtime.

To delete a dynamically allocated array, use delete[]:

delete[] dynArray;

Assorted Class Topics

Recall that C++ supports object-oriented programming through classes. This section assumes familiarity with the basics of classes in C++: classes versus objects, how to declare a class with member variables and functions, the constructor, and inheritance and polymorphism. It instead focuses on certain topics that cause some issues when using classes in C++.

References, const, and Classes

Passing objects by value to functions is inefficient. This is because copying the object can be expensive, especially if the object has a lot of data. Thus, a best practice is to pass objects by reference.

However, one issue with references is that they allow the function to modify the parameter. For example, suppose an Intersects function takes in two Circle objects and returns whether these circles intersect. If the function takes in these circles by reference, it could choose to modify the centers or radii of the circles.

The solution is to instead use a constant (const) reference. A const reference guarantees that the function can only read from the reference but not write to it. So, a more correct declaration of Intersects uses const references:

bool Intersects(const Circle& a, const Circle& b);

You can also mark member functions as const member functions to guarantee that a member function does not modify member data. For example, a GetRadius function for Circle shouldn’t modify member data, which means it should be a const member function. To denote that a member function is const, add the const keyword immediately after the closing parenthesis of the function declaration, as in Listing A.1.

Listing A.1 Circle Class with const Member Function


class Circle
{
public:
   float GetRadius() const { return mRadius }
   // Other functions omitted
   // ...
private:
   Point mCenter;
   float mRadius;
};


To summarize, the best practices when it comes to references, const, and classes are as follows:

  • Pass non-basic types by reference, const reference, or pointers, to avoid making copies.

  • Pass by const reference when a function does not need to modify a reference parameter.

  • Mark member functions that don’t modify data as const.

Dynamic Allocation of Classes

Just as with any other type, you can dynamically allocate classes. Listing A.2 shows a declaration for a Complex class that encapsulates a real part and an imaginary part.

Listing A.2 A Complex Class


class Complex
{
public:
   Complex(float real, float imaginary)
      : mReal(real)
      , mImaginary(imaginary)
   { }
private:
   float mReal;
   float mImaginary;
};


Notice how the constructor for Complex takes two parameters. To dynamically allocate an instance of Complex, you must pass in these parameters:

Complex* c = new Complex(1.0f, 2.0f);

As with dynamic allocation of other types, the new operator returns a pointer to the dynamically allocated object. Given a pointer to an object, the -> operator accesses any public members. For example, if the Complex class had a public Negate member function that takes no parameters, the following would call the function on the object c:

c->Negate();

You can also dynamically allocate arrays of objects. This works only if the class has a default constructor (a constructor that takes in no parameters). This is because there’s no way to specify constructor parameters when dynamically allocating an array. If you don’t define any constructors for a class, C++ automatically creates a default constructor for you. However, if you declare a constructor that takes in parameters, then C++ will not automatically create a default constructor. In this case, if you want a default constructor you must declare it yourself. In the case of Complex, because you declared a non-default constructor, there is no default constructor.

Destructors

Suppose you needed to dynamically allocate arrays of integers several times throughout a program. Rather than manually write this code repeatedly, it might make sense to encapsulate this functionality inside a DynamicArray class, as in Listing A.3.

Listing A.3 A Basic Declaration for DynamicArray


class DynamicArray
{
public:
   // Constructor takes in size of element
   DynamicArray(int size)
      : mSize(size)
      , mArray(nullptr)
   {
      mArray = new int[mSize];
   }
   // At function used to access an index
   int& At(int index) { return mArray[index]; }
private:
   int* mArray;
   int mSize;
};


With this DynamicArray class, you could create a dynamic array with 50 elements by using the following code:

DynamicArray scores(50);

However, as previously discussed, every call to new must have a matching call to delete. In this case, DynamicArray dynamically allocates an array in its constructor, but there’s no matching delete[] anywhere. This means that when the scores object goes out of scope, there is a memory leak.

The solution is to use another special member function called the destructor. The destructor is a member function that automatically runs when destroying the object. For objects allocated on the stack, this happens when the object goes out of scope. For dynamically allocated objects, the delete call on the object also invokes the destructor.

The destructor always has the same name as the class, except prefaced with a tilde (~). So for DynamicArray, this is the destructor:

DynamicArray::~DynamicArray()
{
   delete[] mArray;
}

If you add this destructor, when scores goes out of scope, the destructor deallocates mArray, eliminating the memory leak.

The Copy Constructor

The copy constructor is a special constructor that creates an object as a copy of another object of the same type. For example, suppose you declare the following Complex object:

Complex c1 = Complex(5.0f, 3.5f);

You could then instantiate a second instance of Complex as a copy of c1:

Complex c2(c1);

In most cases, C++ provides a copy constructor implementation if the programmer doesn’t declare one. This default copy constructor directly copies all member data from the original object to the new object. For Complex, this works perfectly fine; for example, it means that c2.mReal and c2.mImaginary directly copy from the corresponding members of c1.

However, for classes with pointers to data, such as DynamicArray, directly copying member data doesn’t give the desired result. Suppose you run the following code:

DynamicArray array(50);
DynamicArray otherArray(array);

With the default copy constructor, you directly copy the mArray pointers rather than copying the underlying dynamically allocated array. This means that if you next modify otherArray, you also modify array at the same time! Figure A.1 illustrates this problematic behavior, called a shallow copy.

Figure represents a shallow copy of mArray.

Figure A.1 A shallow copy of mArray

If the default copy constructor is insufficient, as is the case for DynamicArray, you must declare a custom copy constructor:

DynamicArray(const DynamicArray& other)
   : mSize(other.mSize)
   , mArray(nullptr)
{
   // Dynamically allocate my own data
   mArray = new int[mSize];
   // Copy from other's data
   for (int i = 0; i < mSize; i++)
   {
      mArray[i] = other.mArray[i];
   }
}

Note that the only parameter to the copy constructor is a const reference to another instance of the class. The implementation in this case dynamically allocates a new array and then copies over the data from the other DynamicArray. This is as a deep copy because the two objects now have separate underlying dynamically allocated arrays.

In general, classes that dynamically allocate data should implement the following member functions:

  • A destructor to free the dynamically allocated memory

  • A copy constructor to implement deep copies

  • An assignment operator (discussed in the next section), also to implement deep copies

If it’s necessary to implement any of these three functions, then you should implement all three. This problem is so common in C++ that developers coined the term rule of three to remember it.

note

In the C++11 standard, the rule of three expands to the rule of five, as there are two additional special functions (the move constructor and the move assignment operator). While this book does use some C++11 features, it does not use these additional functions.

Operator Overloading

C++ gives programmers the ability to specify the behavior of built-in operators for custom types. For example, you can define how the arithmetic operators work for the Complex class. In the case of addition, you can declare the + operator as follows:

friend Complex operator+(const Complex& a, const Complex& b)
{
   return Complex(a.mReal + b.mReal,
                  a.mImaginary + b.mImaginary);
}

Here, the friend keyword means that operator+ is a standalone function that can access Complex’s private data. This is a typical declaration signature for binary operators.

After overloading the + operator, you can use it to add two complex objects, like so:

Complex result = c1 + c2;

You can also override binary comparison operators. The only difference is that these operators return a bool. For example, the following code overloads the == operator:

friend bool operator==(const Complex& a, const Complex& b)
{
   return (a.mReal == b.mReal) &&
      (a.mImaginary == b.mImaginary);
}

You can also overload the = operator (or assignment operator). As with the copy constructor, if you don’t specify an assignment operator, C++ gives you a default assignment operator that performs a shallow copy. So, you usually only need to overload the assignment operator in the “rule of three” case.

There’s one big difference between an assignment operator and a copy constructor. With a copy constructor, you’re constructing a new object as a copy of an existing one. With an assignment operator, you’re overwriting an already existing instance of an object. For example, the following code invokes the assignment operator for DynamicArray on the third line because the first line previously constructed a1:

DynamicArray a1(50);
DynamicArray a2(75);
a1 = a2;

Because the assignment operator overwrites an already existing instance with new values, it needs to deallocate any previously dynamically allocated data. For example, this is the correct implementation of the assignment operator for DynamicArray:

DynamicArray& operator=(const DynamicArray& other)
{
   // Delete existing data
   delete[] mArray;
   // Copy from other
   mSize = other.mSize;
   mArray = new int[mSize];
   for (int i = 0; i < mSize; i++)
   {
      mArray[i] = other.mArray[i];
   }
   // By convention, return *this
   return *this;
}

Note that the assignment operator is a member function of the class, not a standalone friend function. Also, by convention, the assignment operator returns a reference to the reassigned object. This allows for code (albeit ugly code) that chains assignments, such as the following:

a = b = c;

You can override nearly every operator in C++, including the subscript [] operator, new, and delete. However, with great power comes great responsibility. You should try to overload an operator only when it’s clear what the operator does. It makes sense that the + operator does addition, but you should avoid reassigning the meaning of an operator.

For example, some math libraries override | and ^ for the dot and cross products, even though | and ^ are the bitwise OR and bitwise XOR for integral types. Overusing operator overloading in this manner leads to code that is difficult to understand. Amusingly, the C++ library itself breaks this best practice: Streams overload the >> and << operators for input and output (which are bitshift operators for integral types).

Collections

A collection provides a way to store elements of data. The C++ Standard Library (STL) provides many different collections, and so it’s important to understand when to utilize which collections. This section discusses the most commonly used collections.

Big-O Notation

Big-O notation describes the rate at which an algorithm scales as the problem size scales. This rate is also known as the time complexity of the algorithm. You can use Big-O to understand the relative scaling of specific operations on collections. For example, an operation with a Big-O of O(1) means that regardless of the number of elements in the collection, the operation will always take the same amount of time. On the other hand, a Big-O of O(n) means that the time complexity is a linear function of the number of elements.

Table A.5 lists the most common time complexities, from the fastest to the slowest. Algorithms that are exponential or slower are too slow to see real use beyond very small problem sizes.

Table A.5 Common Time Complexities in Big-O Notation (Fastest to Slowest)

Big-O

Described As

Examples

O(1)

Constant

Insertion into the front of a linked list, array indexing

O(log n)

Logarithmic

Binary search (given an already sorted collection)

O(n)

Linear

Linear search

O(n log n)

“n log n”

Merge sort, quick sort (average case)

O(n2)

Quadratic

Insertion sort, bubble sort

O(2n)

Exponential

Integer factorization

O(n!)

Factorial

Brute-forcing the traveling salesperson problem

Although Big-O notation says how an algorithm scales, for certain problem sizes, algorithms with worse time complexity may perform better. For example, a quick sort has an average time complexity of O(n log n), while an insertion sort has a time complexity of O(n2). However, for small problem sizes (such as n<20), the insertion sort has a faster execution time because it does not use recursion. Thus, it’s as important to consider the actual execution performance of an algorithm as its specific use case.

Vector

A vector is a dynamic array that automatically resizes based on the number of elements in the collection. To insert elements into a vector, use the push_back (or emplace_back) member function. This adds an element to the end (back) of the vector. For example, the following code declares a vector of floats and then adds three elements at the end of the vector:

// #include <vector> to use std::vector
std::vector<float> vecOfFloats;
vecOfFloats.push_back(5.0f);  // Contents: { 5.0f }
vecOfFloats.push_back(7.5f);  // Contents: { 5.0f, 7.5f }
vecOfFloats.push_back(10.0f); // Contents: { 5.0f, 7.5f, 10.0f }

Once the vector has elements, you can use array subscript notation to access specific elements in the vector. So given the vector from the preceding snippet, vecOfFloats[2] accesses the third element in the vector, yielding 10.0f.

In the long run, insertion into the back of a vector averages out to O(1). However, because a vector exists in one contiguous block of memory, as in Figure A.2, insertion at an arbitrary position in the vector is O(n). Because of this, you should avoid arbitrary insertion into a vector. But an advantage of this contiguous memory layout is that accessing an element at an index is O(1).

A rectangular box with five sections read “2, 4, 6, 8, and 10.”

Figure A.2 The internal memory layout of a vector is contiguous, as with arrays

Linked List

A linked list is a collection that stores each element at a separate location in memory and links them together with pointers. The std::list collection allows for insertion to both the front and the back of the list. Use the push_front (or emplace_front) function to insert into the front, and push_back (or emplace_back) for the back. The following code creates a linked list of integers and inserts a handful of elements:

// #include <list> to use std::list
std::list<int> myList;
myList.push_back(4);
myList.push_back(6);
myList.push_back(8);
myList.push_back(10);
myList.push_front(2);

Figure A.3 illustrates myList after completing all the insertions. Note that, by definition, the elements in the linked list are not next to each other in memory. One advantage of a linked list is that insertion to either end of the list is O(1). If you have a pointer to an element in the list, you can also insert before or after that element in O(1) time.

Figure represents myList with elements inserted into it.

Figure A.3 myList with elements inserted into it

However, one disadvantage of a linked list is that accessing the nth element of the list is O(n). For this reason, the implementation of std::list does not allow indexing via array subscripting.

Queues

A queue exhibits first-in, first-out (FIFO) behavior, much like waiting in a line at a store. With a queue, you cannot remove elements in any arbitrary order. With a queue, you must remove elements in the same order in which they were added. Although many books use enqueue to reference insertion into a queue and dequeue to reference removal from a queue, the implementation of std::queue uses push (or emplace) for insertion and pop for removal. To access the element at the front of the queue, use front.

The following code inserts three elements into a queue and then removes each element from the queue, outputting the values:

// #include <queue> to use std::queue
std::queue<int> myQueue;
myQueue.push(10);
myQueue.push(20);
myQueue.push(30);
for (int i = 0; i < 3; i++)
{
   std::cout << myQueue.front() << ' ';
   myQueue.pop();
}

Because queues operate in a FIFO manner, the above code outputs the following:

10 20 30

The std::queue implementation guarantees O(1) time complexity for insertion, accessing the front element, and removal.

Stack

A stack exhibits last-in, first-out (LIFO) behavior. For example, if you add the elements A, B, and C to a stack, you can only remove them in the order C, B, A. You use the push (or emplace) function to add an element onto the stack and the pop function to remove an element from the stack. The top function accesses the element on the “top” of the stack. The following code shows std::stack in action:

// Include <stack> to use std::stack
std::stack<int> myStack;
myStack.push(10);
myStack.push(20);
myStack.push(30);
for (int i = 0; i < 3; i++)
{
   std::cout << myStack.top() << ' ';
   myStack.pop();
}

Because of the LIFO behavior of a stack, the above code outputs the following:

30 20 10

As with queue, the major operations for std::stack all have constant time complexity.

Maps

A map is an ordered collection of {key, value} pairs, sorted by key. Each key in the map must be unique. Because a map has both a key type and a value type, you must specify both types when declaring a map. The recommended way to add an element to a map is with the emplace function, which takes in the key and values as parameters. For example, the following code creates a std::map of months, where the key is the number of the month and the value is the string name of the month:

// #include <map> to use std::map
std::map<int, std::string> months;
months.emplace(1, "January");
months.emplace(2, "February");
months.emplace(3, "March");
// ...

The easiest way to access an element from a map is by using the [] operator and passing in the key. For example, the following would output February:

std::cout << months[2];

However, this syntax works as expected only if the key is in the map. To determine if a key is in a map, use the find function. This returns an iterator to the element, if found. (We discuss iterators in a moment.)

Internally, the std::map implementation uses a balanced binary search tree. This means that std::map can find an element by key in O(log n) (logarithmic) time. Insertion and removal from the map are also logarithmic. Furthermore, due to the binary search tree, looping over the contents of a map is in ascending order of the keys.

Hash Maps

While a regular map maintains an ascending order of the keys, a hash map is unordered. In exchange for the lack of ordering, insertion, removal, and search are all O(1). Thus, in cases where you need a map but don’t need ordering, a hash map yields better performance than a regular map.

The C++ hash map std::unordered_map has the same functions as std::map, just without any guaranteed ordering. To use the hash map class, use #include <unordered_map>.

Iterators, Auto, and Range-Based For Loops

For looping over all the elements in a vector you can use the same syntax as for looping over an array. However, many of the other C++ STL collections, such as list and map, do not support this array syntax.

One way to loop over these other containers is with an iterator, which is an object that helps traverse the collection. Every C++ STL collection supports iterators. Each collection has a begin function that returns an iterator to the first element and an end function that returns an iterator to the last element. The type of the iterator is the type of the collection followed by ::iterator. For example, the following code creates a list and then uses an iterator to loop over each element in the list:

std::list<int> numbers;
numbers.emplace_back(2);
numbers.emplace_back(4);
numbers.emplace_back(6);
for (std::list<int>::iterator iter = numbers.begin();
   iter != numbers.end();
   ++iter)
{
   std::cout << *iter << std::endl;
}

Note that the iterator is dereferenced with *, the same way a pointer is dereferenced. The syntax for looping over other collections with iterators is similar.

In the case of a map, the iterator actually points to a std::pair. So, given an iterator to an element in a map, you must use first and second to access the key and value, respectively. Returning to the months map from earlier, you can get an iterator to an element and output its data with the following code:

// Get an iterator to the element with the key 2
std::map<int, std::string> iter = months.find(2);
if (iter != months.end()) // This is only true if found
{
   std::cout << iter->first << std::endl; // Outputs 2
   std::cout << iter->second << std::endl; // Outputs February
}

Typing out the long type names for iterators is annoying. C++11 provides the auto keyword to help reduce this pain. auto tells the compiler to deduce the type of a variable for you, based on the assigned value. For example, because the begin function returns an iterator of a very specific type, auto can deduce that correct type. There is no performance penalty for using auto, though some programmers find the code harder to understand.

Using auto, you can rewrite the list loop as follows:

// auto is deduced to be std::list<int>::iterator
for (auto iter = numbers.begin();
   iter != numbers.end();
   ++iter)
{
   std::cout << *iter << std::endl;
}

The code in this book uses auto only when it provides a benefit in readability.

Even with auto, the code for looping via iterators is clunky. Many other programming languages provide a foreach construct for looping over collections. C++11 has a similar construct called a range-based for loop. To loop over the numbers list with a range-based for loop, use the following syntax:

for (int i : numbers)
{
   // i stores the element for the current loop iteration
   std::cout << i << std::endl;
}

This loop makes a copy of each element in the list as it iterates over it. However, you can also pass by reference if you want to modify elements in the collection. Similarly, you can use const references.

You can also use auto for the type when writing a range-based for loop. However, as with using an explicit type, this makes a copy of each element. However, you can also use const and & with auto, if needed.

One disadvantage of a range-based for loop is that you can’t add or remove elements in the collection during the loop. So, if you need this behavior, you must use another type of loop.

Additional Reading

There are many excellent resources available online to help you learn and practice the fundamentals of C++. One such website is LearnCPP.com, which contains a very in-depth progression of topics. If you prefer traditional books, you should see Stephen Prata’s book, which provides coverage of the basics. Eric Roberts’s book covers both the fundamentals of C++ and relevant data structures.

Both of Scott Meyers’s books are great resources for best practices in C++. They are short reads that provide many tips on how to achieve maximum effectiveness from C++ code.

There is also a great deal of information available on the C++ Standard Library. Bjarne Stroustrup, the creator of C++, devotes a large section of his book to the C++ collection implementations.

LearnCpp.com. Last modified April 28, 2016. http://www.learncpp.com.

Meyers, Scott. Effective C++, 3rd edition. Boston: Addison-Wesley, 2005.

Meyers, Scott. Effective Modern C++. Sebastopol: O’Reilly Media, 2014.

Prata, Stephen. C++ Primer Plus, 6th edition. Upper Saddle River: Addison-Wesley, 2012.

Roberts, Eric. Programming Abstractions in C++. Boston: Pearson, 2014.

Stroustrup, Bjarne. The C++ Programming Language, 4th edition. Upper Saddle River: Pearson, 2013.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.88.165