C++ Cheatsheet

C vs. C++ Functionality

C Interoperability

C++ is a superset of C, but some C functionality has been superceded and should not typically be used in C++ code. One exception to this rule is when interacting with C code, sometimes we are forced to use C functionality.

malloc and free

In C++, malloc and free should never be used. The new and delete operators should be used instead. These are type-safe, and they also ensure that the appropriate constructor and destructor is called.

Program Structure

Header Files

Header files (e.g. .h, .hpp) typically contain function declarations, while source files (e.g. .cpp, .cc) typically contain function definitions.

Source files can include header files in order to gain access to the declarations therein. After compilation, it is the responsibility of the linker to supply the appropriate definition for each referenced declaration; a header can tell the compiler that a function exists, but the compiler does not know where it is defined.

In theory everything could be defined in header files but this would drastically increase the compile time.

Include Guards

Each symbol (function, variable, etc.) can only be defined once (otherwise how can the linker know which version is the correct one?).

This is problematic when definitions are present in header files, as the same header files may be included multiple times within a program.

The solution for this is to use include guards:

#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H

// header file contents

#endif

Memory Management

Memory Management

The practice of relying on constructor and destructors for memory management is sometimes referred to as Resource Acquisition Is Initialization (RAII).

Memory leaks are still possible if we forget to call delete. One solution to this is to use smart pointers, which call new and delete automatically based on reference counting.

Heap vs. Stack

Using the new operator creates an object on the heap, but by default objects are created on the stack. Stack-allocated objects are automatically deleted (and their destructor called) when they go out of scope, making them much easier and safer to use.

Stack-allocated objects are also cheaper to create and access, since their memory address can be determined at compile time. However, the stack size is very limited compared to the heap, so it should not be used for large objects.

Basic Syntax & Keywords

Right-Left Rule

The right-left rule allows us to reliably interpret complex type declarations. It is defined as follows:

  1. Start reading the declaration from the identifier.
  2. Read to the right until a closing bracket is reached.
  3. Read to the left until the matching bracket is found.
  4. Repeat from step 2, starting outside the brackets, until the whole declaration has been parsed.

Example:

int * (* (*fp1) (int) ) [10];
Start from the variable name:                       -> fp1
Read right, hit a bracket; go left to find *        -> is a pointer
Read right again outside the brackets; find (int)   -> to a function that takes an int
Hit another bracket; go left to find *              -> and returns a pointer
Read right again outside the brackets; find [10]    -> to an array of 10
Read left to find *                                 -> pointers to
Keep reading left to find int                       -> ints

More information can be found here.

const

Variables, parameters and methods should be denoted as const wherever possible to prevent accidental modification. const methods of a class can only call other const methods.

This is generally appropriate for getter methods (unless they return a reference to a member variable, because that would enable modification of that object).

East / west const

There is ongoing debate about whether the const keyword should be placed to the left or to the right of a variable declaration. The former is arguably more natural, but the latter is more consistent.

constexpr

constexpr should be used for variables and functions whose values can be determined at compile time. These can then be used in the declarations of const variables.

noexcept

Functions that should never throw an exception can be marked noexcept. This is a useful form of documentation, and it can allow certain optimisations by the compiler. Should such a function throw an exception, the program will terminate.

inline

inline tells the compiler not to produce an error if a function definition is encountered multiple times, on the assumption that the function is defined in every translation unit where it is used, and that each definition is exactly the same. This is rarely needed, but can be appropriate for small functions defined in header files.

More information can be found here.

rvalues

An rvalues is a temporary value, i.e. a value without a name, that exists only within the context of a statement.

For example:

// The value in parentheses is an rvalue
f(x + 4);

// The value returned here is an rvalue
return 16;

rvalue References (&&)

Functions can be declared to handle rvalue parameters explicitly. For example:

void foo(int a); // regular function (1)
void foo(int&& a); // function with rvalue parameter (2)

int x = 5;
foo(x); // calls (1)
foo(5); // calls (2)

Pointers & References

Address-of Operator (&)

The address-of operator returns the memory address of a variable. This is useful for creating pointers, since we have to assign them the memory address of the variable we want them to point to.

int p = 10;
int *q = &p;  // q now points to p

Dereference Operator (*)

Dereferencing a pointer means getting the value that is stored in the memory location held by a pointer.

int p = 10;
int *q = &p;
std::cout << *q << "\n";  // prints 10

Dot (.) and Arrow (->) Operators

The dot is used to access an object's members, when that object is stored directly in a variable.

The arrow is used when accessing an object's members, when we have a pointer to the object. This dereferences the pointer before accessing the member.

In other words, foo->bar() is the same as (*foo).bar().

Pointers vs. References

A reference (&) is an alias for another variable. This is useful for providing access to a variable across scope boundaries. References are generally safer to use than raw pointers because they are always initialised to a value. References cannot be reassigned after creation.

Raw pointers (*) are useful in cases where reassignment is desirable, or where the value may be nullptr. A pointer that points to a destroyed object is known as a dangling pointer, and dereferencing it causes undefined behaviour.

More information can be found here.

Pointer Declarations

Misleading Syntax

It is better to place the * (or &) next to the variable name, to prevent misleading declarations like this:

int* p, q;

In this example, only p is actually a pointer.

This ambiguity can be resolved with the help of a typedef. In this example, both p and q are pointers:

typedef int * IntPointer;
IntPointer p, q;

Pointers to Arrays

int tiles[32][4];
int (*p)[4] = tiles;    // p is a pointer to an array of 4 ints;
                        //  (`tiles` is equivalent to `tiles[0]`)
int *q[5];              // q is a pointer to an array of 5 ints

Function Pointers

Function pointers are declared using:

return_type (*variable_name)(function_params);

For example, here p is a pointer to a function that takes 2 floats and returns a pointer to a char:

char * (*p)(float, float);

Smart Pointers

TODO

TODO.

Collections

vector

TODO.

map

TODO.

Iterators

TODO.

Templates

TODO

TODO.

Classes

Special Member Functions

C++ defines a number of special member functions. The compiler can usually generate them automatically, but they can also be declared explicitly. They are:

  • Default constructor (no parameters)
  • Destructor
  • Copy constructor
  • Copy assignment operator
  • Move constructor (C++ 11)
  • Move assignment operator (C++ 11)

Rule of Five

The Rule of Five states that if any of these special member functions (except for the default constructor) are explicitly declared, then all of them should be declared, because there is probably some manual resource management taking place that will have implications for all of these operations.

Constructors

All non-static fields - even fundamental types - should be initialised by an object's constructorto prevent undefined values.

Member Initializer Lists

For performance reasons, initializer lists are the preferred way to initialise an object's fields.

Foo(const int x, const int y)
    : x(x),
      y(y))
{
    // Constructor implementation goes here;
    // the fields 'x' and 'y' have already been populated
}

Destructors

An object's destructor is automatically called when the object goes out of scope or gets deleted.

~Foo(); // destructor

The default destructor has an empty body.

Copy Constructor

When an object is passed to - or returned from - a function, it is implicitly copied. This is done by invoking the object's copy constructor:

Foo(const Foo &objectToCopy); // copy constructor

Foo f;
bar(f); // calls Foo's copy constructor

The default copy constructor just copies each of the object's non-static members. If any of these are themselves class types, their own copy constructors will be invoked.

Copy Assignment Operator

The copy assignment operator is called when an existing object is assigned to a new variable:

Foo& operator=(const Foo &objectToCopy); // copy assignment operator

Foo f;
Foo g = f; // calls Foo's copy assignment operator

The default copy assignment operator performs a copy-assignment operation on each of the object's non-static members. If any of these are themselves class types, their own copy-assignment operators will be invoked; for basic types, the built-in assignment operator is used.

Move Constructor

The move constructor is called when an object is initialised from a temporary value, e.g. an rvalue, or when std::move is invoked:

Foo(Foo&& rhs); // move constructor

Foo create()
{
    Foo f;
    return f;
}

Foo g(create()); // Calls Foo's move constructor

Unlike the copy constructor, the move constructor is permitted to "steal" resources from the original object.

The default move constructor just moves each of the object's non-static members. If any of these are themselves class types, their own move constructors will be invoked.

More information on move semantics can be found here.

Move Assignment Operator

The move assignment operator is similar to the move constructor, but it is used when assigning a temporary value to a new variable.

Foo& operator=(Foo&& rhs); // move assignment operator

Foo create()
{
    Foo f;
    return f;
}

Foo g = create(); // Calls Foo's move constructor

Inheritance

Virtual Destructors

TODO.

Pure Virtual

TODO.

Object Slicing

TODO.

Lambda Functions

TODO

TODO.

Published 09/03/2021