Study Notes on LearnCpp (Part I - C++ Basic)


Source: LearnCpp.com by Alex

Introduction to programming languages

Optional Reading: Difference between compiled and interpreted languages?

Interpreters and compilers have complementary strengths and weaknesses, it’s becoming increasingly common for language runtimes to combine elements of both. Java’s JVM is a good example of this. The Java itself is compiled to byte code, and then directly to machine code.

Also, another key point from this article is that languages are not compiled or interpreted. They are not a nature of the languages. C code is compiled, but there are C interpreters available that make it easier to debug or visualize the code.

Introduction to C++

History of C++

  • 1979: Bjarne Stroustrup at Bell Labs started developing C++
  • 1998: Ratified by the ISO committee in 1998
  • 2003: C++03
  • 2011: C++11 (a huge number of new capabilities)
  • 2014: C++14
  • 2017: C++17

C and C++’s philosophy

The underlying design philosophy of C and C++ can be summed up as "trust the programmer", which is both wonderful and dangerous. Although we have the freedom, it’s important to know the things you should not do with C++.

History of “bug”

The term bug was first used by Thomas Edison back in the 1870s! However, the term was popularized in the 1940s when engineers found an actual moth stuck in the hardware of an early computer, causing a short circuit. Both the log book in which the error was reported and the moth are now part of the Smithsonian Museum of American History.

Compilers, linkers, and the libraries

For complex projects, some development environments use a makefile, which is a file that describes how to build a program.

C++ Basics

This section covers too many C stuff. I just write down something important to notice and recall!

std::endl vs ‘\n’

Using std::endl can be a bit inefficient, as it actually does two jobs: it moves the cursor to the next line, and it “flushes” the output (makes sure that it shows up on the screen immediately; output the buffer).

When writing text to the console using std::cout, std::cout usually flushes output anyway (and if it doesn’t, it usually doesn’t matter), so having std::endl flush is rarely important.

The ‘\n’ character doesn’t do the redundant flush, so it performs better.

  • << - insertion operator
  • >> - extraction operator. The input must be stored in a variable to be used.

Expressions

Initialization can be used to give a variable a value at the point of creation. C++ supports 3 types of initialization:

  • copy initialization
  • direct initialization
  • uniform initialization.

An expression is a combination of literals, variables, operators, and explicit function calls (not shown above) that produce a single output value. When an expression is executed, each of the terms in the expression is evaluated until a single value remains (this process is called evaluation). That single value is the result of the expression.

Expression statements

Statements are used when we want the program to perform an action. Expressions are used when we want the program to calculate a value.

Certain expressions (like x = 5) are useful by themselves. However, we mentioned above that expressions must be part of a statement, so how can we use these expressions by themselves?

Fortunately, we can convert any expression into an equivalent statement (called an expression statement). An expression statement is a statement that consists of an expression followed by a semicolon. When the statement is executed, the expression will be evaluated (and the result of the expression will be discarded).

1
2
3
4
5
6
int x;  // this statement does not contain an expression (this is just a variable definition)
5 * 6;
5 < 6;

std::cout << x; // hint: operator << is a binary operator
// If operator<< is a binary operator, then std::cout must be the left-hand operand, and x must be the right-hand operand. Since that's the entire statement, this must be an expression statement.

Functions and Files

C++ does not define whether function calls evaluate arguments left to right or vice-versa.

Function parameters and variables defined inside the function body are called local variables. The time in which a variable exists is called its lifetime. Variables are created and destroyed at runtime, which is when the program is running. A variables scope determines where it can be accessed. When a variable can be accessed, we say it is in scope. When it can not be accessed, we say it is out of scope. Scope is a compile-time property, meaning it is enforced at compile time.

Refactoring is the process of breaking down a larger function into many smaller, simpler functions.

Whitespace refers to characters used for formatting purposes. In C++, this includes spaces, tabs, and newlines.

A definition actually implements the (for functions and types) or instantiates (for variables) an identifier. A declaration is a statement that tells the compiler about the existence of the identifier. In C++, all definitions serve as declarations. Pure declarations are declarations that are not also definitions (such as function prototypes).

Namespace

In C++, a namespace is a grouping of identifiers that is used to reduce the possibility of naming collisions. It turns out that std::cout‘s name isn’t really std::cout. It’s actually just cout, and std is the name of the namespace that identifier cout is part of. In modern C++, all of the functionality in the C++ standard library is now defined inside namespace std (short for standard).

When you use an identifier that is defined inside a namespace (such as the std namespace), you have to tell the compiler that the identifier lives inside the namespace.

  • Explicit namespace qualifier std::
    This is the safest way to use cout, sine there’s no ambiguity.

  • Using namespace std (and why to avoid it!!!)

    A using directive tells the compiler to check a specified namespace when trying to resolve an identifier that has no namespace prefix. So in the above example, when the compiler goes to determine what identifier cout is, it will check both locally (where it is undefined) and in the std namespace (where it will match to std::cout).

    1
    2
    3
    4
    5
    6
    #include <iostream>
    using namespace std;
    int main() {
    cout << "Hello World!";
    return 0;
    }

    Many texts, tutorials, and even some compilers recommend or use a using directive at the top of the program. However, used in this way, this is a bad practice, and highly discouraged.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream> // imports the declaration of std::cout
    using namespace std; // makes std::cout accessible as "cout"

    int cout() { // declares our own "cout" function
    return 5;
    }

    int main() {
    cout << "Hello, world!"; // Compile error! Which cout do we want here? The one in the std namespace or the one we defined above?

    return 0;
    }

bool & Literals

1
2
3
4
5
int main() {
bool flag; // remember to initialize it!
std::cout << flag << "\n"; // 1 or 0
return 0;
}

In C++, bool is treated as 0 or 1.

C++ has two kinds of constants: literal and symbolic. In this lesson, we’ll cover literals.

Just like variables have a type, all literals have a type too. The type of a literal is assumed from the value and format of the literal itself.

You can use literal suffixes to change the default type of a literal if its type is not what you want.

1
2
float f1 = 5.0f;  // since the type of 5.0 is double
float f2 = 4.1; // result in a loss of precision

In C++14, we can assign binary literals by using 0b prefix:

1
2
3
4
5
int bin(0);
bin = 0b1; // assign binary 0000 0001 to the variable
bin = 0b11; // assign binary 0000 0011 to the variable
bin = 0b1010; // assign binary 0000 1010 to the variable
bin = 0b11110000; // assign binary 1111 0000 to the variable

Because long literals can be hard to read, C++14 also adds the ability to use a quotation mark ' as a digit separator. In Java, we use _ instead.

const

1
2
int bin = 0b1011'0010;  // assign binary 1011 0010 to the variable
long value = 2'132'673'462; // much easier to read than 2132673462

To make a variable constant, simply put the const keyword either before or after the variable type, like so:

1
2
const double gravity { 9.8 }; // preferred use of const before type
int const sidesInSquare { 4 }; // okay, but not preferred

Although C++ will accept const either before or after the type, we recommend using it before the type because it better follows standard English language convention where modifiers come before the object being modified (e.g. a green ball, not a ball green).

Const variables must be initialized when you define them, and then that value can not be changed via assignment. Otherwise, it will cause a compile error:

1
const double gravity;  // compiler error

Note that const variables can be initialized from non-const values:

1
2
3
4
std::cout << "Enter your age: ";
int age;
std::cin >> age;
const int usersAge (age); // usersAge can not be changed

Const is used most often with function parameters:

1
2
3
void printInteger(const int myValue) {
std::cout << myValue << "\n";
}

Does two things:

  • let the person calling the function know that the function will not change the value of myValue
  • it ensures that the function doesn’t change the value of myValue

However, with parameters passed by value, like the above, we generally don’t care if the function changes the value of the parameter, since it’s just a copy that will nbe destroyed later. Thus, we usually don’t const parameters passed by value.

Runtime constants are those whose initialization values can only be resolved at runtime (when your program is running). Variables such as myValue above is a runtime constant, because the compiler can’t determine their values at compile time. myValue depends on the value passed into the function (which is only known at runtime).

In most cases, it doesn’t matter whether a constant value is runtime or compile-time. However, there are a few odd cases where C++ requires a compile-time constant instead of a run-time constant (such as when defining the length of a fixed-size array — we’ll cover this later). Because a const value could be either runtime or compile-time, the compiler has to keep track of which kind of constant it is.

To help provide more specificity, C++11 introduced new keyword constexpr, which ensures that the constant must be a compile-time constant:

1
2
3
4
5
6
7
8
9
10
constexpr double gravity (9.8);
// ok, the value of 9.8 can be resolved at compile-time
constexpr int sum = 4 + 5;
// ok, the value of 4 + 5 can be resolved at compile-time

std::cout << "Enter your age: ";
int age;
std::cin >> age;
constexpr int myAge = age;
// not okay, age can not be resolved at compile-time
  • Rule: Any variable that should not change values after initialization and whose initializer is known at compile-time should be declared as constexpr.
  • Rule: Any variable that should not change values after initialization and whose initializer is not known at compile-time should be declared as const.

Naming your const variables: Some programmers prefer to use all upper-case names for const variables. Others use normal variable names with a k prefix. However, we will use normal variable naming conventions, which is more common. Const variables act exactly like normal variables in every case except that they can not be assigned to, so there’s no particular reason they need to be denoted as special.

Symbolic constants: A symbolic constant is a name given to a constant literal value. There are two ways to declare symbolic constants in C++. One of them is good, and one of them is not.

Bad: Using object-like macros with a substitution parameter as symbolic constants.

A better solution: Use const variables, or better, constexpr

That way using symbolic constants, if you ever need to change them, you only need to change them in one place.

Side effects in inc / dec

A function or expression is said to have a side effect if it modifies some state (e.g. any stored information in memory), does input or output, or calls other functions that have side effects.

Usually, they are useful:

1
2
3
x = 5; // the assignment operator modifies the state of x
++x; // operator++ modifies the state of x
std::cout << x; // operator<< modifies the state of the console

However, side effects can also lead to unexpected results:

1
2
3
4
5
6
7
8
9
10
11
int add(int x, int y) { return x + y; }

int main() {
int x = 5;
int value = add(x, ++x);
// is this 5 + 6, or 6 + 6? It depends on what order your compiler evaluates the function arguments in

std::cout << value;
// value could be 11 or 12, depending on how the above line evaluates!
return 0;
}

C++ does not define the order in which function arguments are evaluated. Note that this is only a problem because one of the argument to function add() has a side effect.

Another popular example:

1
2
3
4
5
6
7
int main() {
int x = 1;
x = x++;
std::cout << x;

return 0;
} // the output is undefined
  • If the ++ is applied to x before the assignment, the answer will be 1 (postfix operator++ increments x from 1 to 2, but it evaluates to 1, so the expression becomes x = 1).

  • If the ++ is applied to x after the assignment, the answer will be 2 (this evaluates as x = x, then postfix operator++ is applied, incrementing x from 1 to 2).

There are other cases where C++ does not specify the order in which certain things are evaluated, so different compilers will make different assumptions.

Please don’t ask why your programs that violate the above rule produce results that don’t seem to make sense. That’s what happens when you write programs that have “undefined behavior”. :)

Bit flags & bit masks

In the majority of cases, this is fine — we’re usually not so hard-up for memory that we need to care about 7 wasted bits. However, in some storage-intensive cases, it can be useful to “pack” 8 individual boolean values into a single byte for storage efficiency purposes. This is done by using the bitwise operators to set, clear, and query individual bits in a byte, treating each as a separate boolean value. These individual bits are called bit flags.

Defining bit flags in C++14

In order to work with individual bits, we need to have a way to identify the individual bits within a byte, so we can manipulate those bits (turn them on and off). This is typically done by defining a symbolic constant to give a meaningful name to each bit used. The symbolic constant is given a value that represents that bit.

Because C++14 supports binary literals, this is easiest in C++14:

1
2
3
4
5
6
7
8
9
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 0b0000'0001; // represents bit 0
const unsigned char option1 = 0b0000'0010; // represents bit 1
const unsigned char option2 = 0b0000'0100; // represents bit 2
const unsigned char option3 = 0b0000'1000; // represents bit 3
const unsigned char option4 = 0b0001'0000; // represents bit 4
const unsigned char option5 = 0b0010'0000; // represents bit 5
const unsigned char option6 = 0b0100'0000; // represents bit 6
const unsigned char option7 = 0b1000'0000; // represents bit 7

Defining bit flags in C++11 or earlier

Because C++11 doesn’t support binary literals, we have to use other methods to set the symbolic constants. There are two good methods for doing this. Less comprehensible, but more common, is to use hexadecimal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Define 8 separate bit flags (these can represent whatever you want)
const unsigned char option0 = 0x1; // hex for 0000 0001
const unsigned char option1 = 0x2; // hex for 0000 0010
const unsigned char option2 = 0x4; // hex for 0000 0100
const unsigned char option3 = 0x8; // hex for 0000 1000
const unsigned char option4 = 0x10; // hex for 0001 0000
const unsigned char option5 = 0x20; // hex for 0010 0000
const unsigned char option6 = 0x40; // hex for 0100 0000
const unsigned char option7 = 0x80; // hex for 1000 0000

// This can be a little hard to read. One way to make it easier is to use the left-shift operator to shift a bit into the proper location.
const unsigned char option0 = 1 << 0; // 0000 0001
const unsigned char option1 = 1 << 1; // 0000 0010
const unsigned char option2 = 1 << 2; // 0000 0100
const unsigned char option3 = 1 << 3; // 0000 1000
const unsigned char option4 = 1 << 4; // 0001 0000
const unsigned char option5 = 1 << 5; // 0010 0000
const unsigned char option6 = 1 << 6; // 0100 0000
const unsigned char option7 = 1 << 7; // 1000 0000

Using bit flags to manipulate bits

The next thing we need is a variable that we want to manipulate. Typically, we use an unsigned integer of the appropriate size (8 bits, 16 bits, 32 bits, etc… depending on how many options we have).

1
unsigned char myflags = 0; // all bits turned off to start

To set a bit (turn on)

We use bitwise OR equals (operator |=):

1
2
3
4
5
6
myflags |= option4; // turn option 4 on
myflags |= (option4 | option5); // turn on multiple bits
// myflags = 0000 0000 (we initialized this to 0)
// option4 = 0001 0000
// -------------------
// result = 0001 0000

Turn bits off

1
2
3
4
5
6
myflags &= ~option4;
myflags &= ~(option4 | option5);
// myflags = 0001 1100
// ~option4 = 1110 1111
// --------------------
// result = 0000 1100

Toggle a bit state

We use bitwise XOR:

1
2
myflags ^= option4; // flip option4 from on to off, or vice versa
myflags ^= (option4 | option5); // flip options 4 and 5 at the same time

Determining if a bit is on or off

1
2
3
4
if (myflags & option4)
std::cout << "myflags has option 4 set";
if (!(myflags & option5))
std::cout << "myflags does not have option 5 set";

Here is an actual example for a game we might write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Define a bunch of physical/emotional states
const unsigned char isHungry = 1 << 0; // 0000 0001
const unsigned char isSad = 1 << 1; // 0000 0010
const unsigned char isMad = 1 << 2; // 0000 0100
const unsigned char isHappy = 1 << 3; // 0000 1000
const unsigned char isLaughing = 1 << 4; // 0001 0000
const unsigned char isAsleep = 1 << 5; // 0010 0000
const unsigned char isDead = 1 << 6; // 0100 0000
const unsigned char isCrying = 1 << 7; // 1000 0000

unsigned char me = 0; // all flags/options turned off to start
me |= isHappy | isLaughing; // I am happy and laughing
me &= ~isLaughing; // I am no longer laughing

// Query a few states (we'll use static_cast<bool> to interpret the results as a boolean value rather than an integer)
std::cout << "I am happy? " << static_cast<bool>(me & isHappy) << '\n';
std::cout << "I am laughing? " << static_cast<bool>(me & isLaughing) << '\n';

Why are bit flags useful?

Astute readers will note that the above myflags example actually doesn’t save any memory. 8 booleans would normally take 8 bytes. But the above example uses 9 bytes (8 bytes to define the bit flag options, and 1 bytes for the bit flag)! So why would you actually want to use bit flags?

Bit flags are typically used in two cases:

  • When you have many sets of identical bitflags.

  • Imagine you had a function that could take any combination of 32 different options. One way to write that function would be to use 32 individual boolean parameters:

    1
    2
    3
    void someFunction(bool option1, bool option2, bool option3, bool option4, bool option5, bool option6, bool option7, bool option8, bool option9, bool option10, bool option11, bool option12, bool option13, bool option14, bool option15, bool option16, bool option17, bool option18, bool option19, bool option20, bool option21, bool option22, bool option23, bool option24, bool option25, bool option26, bool option27, bool option28, bool option29, bool option30, bool option31, bool option32);
    // ↓
    void someFunction(unsigned int options);

An introduction to std::bitset

All of this bit flipping is exhausting, isn’t it? Fortunately, the C++ standard library comes with functionality called std::bitset that helps us manage bit flags.

To create a std::bitset, you need to include the bitset header, and then define a std::bitset variable indicating how many bits are needed. The number of bits must be a compile time constant.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bitset>

// Note that with std::bitset, our options correspond to bit indices, not bit patterns
const int option0 = 0;
const int option1 = 1;
const int option2 = 2;
const int option3 = 3;
const int option4 = 4;
const int option5 = 5;
const int option6 = 6;
const int option7 = 7;

std::bitset<8> bits; // we need 8 bits
std::bitset<8> bits(option1 | option2);
// start with option 1 and 2 turned on
std::bitset<8> morebits(0x3);
// start with bit pattern 0000 0011

std::bitset provides 4 key functions:

  • test() allows us to query whether a bit is a 0 or 1
  • set() allows us to turn a bit on (this will do nothing if the bit is already on)
  • reset() allows us to turn a bit off (this will do nothing if the bit is already off)
  • flip() allows us to flip a bit from a 0 to a 1 or vice versa

Bit masks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const unsigned int redBits   = 0xFF'00'00'00;
const unsigned int greenBits = 0x00'FF'00'00;
const unsigned int blueBits = 0x00'00'FF'00;
const unsigned int alphaBits = 0x00'00'00'FF;

std::cout << "Enter a 32-bit RGBA color value in hexadecimal (e.g. FF7F3300): ";
unsigned int pixel;
std::cin >> std::hex >> pixel; // std::hex allows us to read in a hex value

// use bitwise AND to isolate red pixels, then right shift the value into the range 0-255
unsigned char red = (pixel & redBits) >> 24;
unsigned char green = (pixel & greenBits) >> 16;
unsigned char blue = (pixel & blueBits) >> 8;
unsigned char alpha = pixel & alphaBits;

std::cout << "Your color contains:\n";
std::cout << static_cast<int>(red) << " of 255 red\n";
std::cout << static_cast<int>(green) << " of 255 green\n";
std::cout << static_cast<int>(blue) << " of 255 blue\n";
std::cout << static_cast<int>(alpha) << " of 255 alpha\n";

Variable Scope and More Types

Local variables, scope, and duration

When discussing variables, it’s useful to separate out the concepts of scope and duration.

  • A variable’s scope determines where a variable is accessible.
  • A variable’s duration determines where it is created and destroyed. The two concepts are often linked.

Variables defined inside a function are called local variables. Local variables have automatic duration, which means they are created (and initialized, if relevant) at the point of definition, and destroyed when the block they are defined in is exited. Local variables have block scope (also called local scope), which means they enter scope at the point of declaration and go out of scope at the end of the block that they are defined in.

1
2
3
4
5
6
7
8
9
10
11
int main() { // outer block
int n(5); // n created and initialized here

{ // begin nested block
double d(4.0); // d created and initialized here
} // d goes out of scope and is destroyed here

// d can not be used here because it was already destroyed!

return 0;
} // n goes out of scope and is destroyed here

Note that a variable inside a nested block can have the same name as a variable inside an outer block. When this happens, the nested variable “hides” the outer variable. This is called name hiding or shadowing.

Shadowing is something that should generally be avoided, as it is quite confusing!

Rule: Avoid using nested variables with the same names as variables in an outer block. Variables should be defined in the most limited scope possible.

Global variables and linkage

Variables declared outside of a function are called global variables. Global variables have static duration, which means they are created when the program starts and are destroyed when it ends. Global variables have file scope (also informally called global scope or global namespace scope), which means they are visible until the end of the file in which they are declared.

Similar to how variables in an inner block with the same name as a variable in an outer block hides the variable in the outer block, local variables with the same name as a global variable hide the global variable inside the block that the local variable is declared in. However, the global scope operator (::) can be used to tell the compiler you mean the global version instead of the local version.

1
2
3
4
5
6
7
int value(5);

int main() {
int value = 7;
value++; // local
::value++; // global
}

However, having local variables with the same name as global variables is usually a recipe for trouble, and should be avoided whenever possible. By convention, many developers prefix global variable names with g_ to indicate that they are global. This both helps identify global variables as well as avoids naming conflicts with local variables.

Internal and external linkage via the static and extern keywords

In addition to scope and duration, variables have a third property: linkage. A variable’s linkage determines whether multiple instances of an identifier refer to the same variable or not.

(Strong & weak symbols?)

A variable with no linkage can only be referred to from the limited scope it exists in. Normal local variables are an example of variables with no linkage. Two local variables with the same name but defined in different functions have no linkage — each will be considered an independent variable.

A variable with internal linkage is called an internal variable (or static variable). Variables with internal linkage can be used anywhere within the file they are defined in, but can not be referenced outside the file they exist in.

A variable with external linkage is called an external variable. Variables with external linkage can be used both in the file they are defined in, as well as in other files.

If we want to make a global variable internal (able to be used only within a single file), we can use the static keyword to do so.

Similarly, if we want to make a global variable external (able to be used anywhere in our program), we can use the extern keyword to do so:

1
2
3
4
extern double g_y(9.8); 
// g_y is external, and can be used by other files

// Note: those other files will need to use a forward declaration to access this external variable (extern)

By default, non-const variables declared outside of a function are assumed to be external. However, const variables declared outside of a function are assumed to be internal.

Note that this means the extern keyword has different meanings in different contexts:

  • In some contexts, extern means “give this variable external linkage”.
  • In other contexts, extern means “this is a forward declaration for an external variable that is defined somewhere else”.

Function linkage

Functions have the same linkage property that variables do. Functions always default to external linkage, but can be set to internal linkage via the static keyword.

Function forward declarations don’t need the extern keyword. The compiler is able to tell whether you’re defining a function or a function prototype by whether you supply a function body or not.

The one-definition rule and non-external linkage

Forward declarations and definitions, we noted that the one-definition rule says that an object or function can’t have more than one definition, either within a file or a program.

However, it’s worth noting that non-extern objects and functions in different files are considered to be different entities, even if their names and types are identical. This makes sense, since they can’t be seen outside of their respective files anyway.

Global symbolic constants

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef CONSTANTS_H
#define CONSTANTS_H

// define your own namespace to hold constants
namespace Constants {
const double pi(3.14159);
const double avogadro(6.0221413e23);
const double my_gravity(9.2);
// m/s^2 -- gravity is light on this planet
// ... other related constants
}
#endif

This duplication of variables isn’t really that much of a problem (since constants aren’t likely to be huge), but changing a single constant value (not names) would require recompiling every file that includes the constants header, which can lead to lengthy rebuild times for larger projects.

We can avoid this problem by turning these constants into const global variables, and changing the header file to hold only the variable forward declarations:

constants.cpp:

1
2
3
4
5
6
7
namespace Constants {
// actual global variables
extern const double pi(3.14159);
extern const double avogadro(6.0221413e23);
extern const double my_gravity(9.2);
// m/s^2 -- gravity is light on this planet
}

constants.h:

1
2
3
4
5
6
7
8
9
10
#ifndef CONSTANTS_H
#define CONSTANTS_H

namespace Constants {
// forward declarations only
extern const double pi;
extern const double avogadro;
extern const double my_gravity;
} // use of g_ is not necessary
#endif

However, there are a couple of downsides to doing this. First, these constants are now considered compile-time constants only within the file they are actually defined in (constants.cpp), not anywhere else they are used. This means that outside of constants.cpp, they can’t be used anywhere that requires a compile-time constant (constexpr) (such as for the length of a fixed array, something we talk about in chapter 6). Second, the compiler may not be able to optimize these as much.

Given the above downsides, we recommend defining your constants in the header file. If you find that for some reason those constants are causing trouble, you can move them into a .cpp file as per the above as needed.

Why (non-const) global variables are evil?

If you were to ask a veteran programmer for one piece of advice on good programming practices, after some thought, the most likely answer would be, “Avoid global variables!”. And with good reason: global variables are one of the most abused concepts in the language. Although they may seem harmless in small academic programs, they are often hugely problematic in larger ones.

But before we go into why, we should make a clarification. When developers tell you that global variables are evil, they’re not talking about ALL global variables. They’re mostly talking about non-const global variables.

One of the reasons to declare local variables as close to where they are used as possible is because doing so minimizes the amount of code you need to look through to understand what the variable does. Global variables are at the opposite end of the spectrum — because they can be used anywhere, you might have to look through a significant amount of code to understand their usage.

Global variables also make your program less modular and less flexible. A function that utilizes nothing but its parameters and has no side effects is perfectly modular. Modularity helps both in understanding what a program does, as well as with reusability. Global variables reduce modularity significantly.

Rule: Use local variables instead of global variables whenever reasonable, and pass them to the functions that need them.

So what are very good reasons to use non-const global variables?

There aren’t many. In many cases, there are other ways to solve the problem that avoids the use of non-const global variables. But in some cases, judicious use of non-const global variables can actually reduce program complexity, and in these rare cases, their use may be better than the alternatives.

For example, if your program uses a database to read and write data, it may make sense to define the database globally, because it could be needed from anywhere. Similarly, if your program has an error log (or debug log) where you can dump error (or debug) information, it probably makes sense to define that globally, because you’re mostly likely to only have one log and it could be used anywhere. A sound library would be another good example: you probably don’t want to pass this to every function that needs it. Since you’ll probably only have one sound library managing all of your sounds, it may be better to declare it globally, initialize it at program launch, and then treat it as read-only thereafter.

If you do find a good use for a non-const global variable, a few useful bits of advice will minimize the amount of trouble you can get into.

  • First, prefix all your global variables with g_, and/or put them in a namespace, both to reduce the chance of naming collisions and raise awareness that a variable is global.

  • Second, instead of allowing direct access to the global variable, it’s a better practice to “encapsulate” the variable.

  • Third, when writing a standalone function that uses the global variable, don’t use the variable directly in your function body. Pass it in as a parameter, and use the parameter. That way, if your function ever needs to use a different value for some circumstance, you can simply vary the parameter. This helps maintain modularity.

    1
    2
    3
    4
    5
    6
    double instantVelocity(int time) {
    return g_gravity * time;
    }
    double instantVelocity(int time, double gravity) {
    return gravity * time;
    }

Static duration variables

The static keyword is one of the most confusing keywords in the C++ language (maybe with the exception of the keyword class). This is because it has different meanings depending on where it is used.

Just like we use “g_” to prefix global variables, it’s common to use “s_” to prefix static (static duration) variables. Note that internal linkage global variables (also declared using the static keyword) get a “g_”, not a “s_”.

Static variables offer some of the benefit of global variables (they don’t get destroyed until the end of the program) while limiting their visibility to block scope. This makes them much safer for use than global variables.

Scope, duration, and linkage summary

A variable’s duration determines when it is created and destroyed.

  • automatic duration
  • static duration
  • dynamic duration

An identifier’s linkage determines whether multiple instances of an identifier refer to the same identifier or not.

  • no linkage (the identifier only refers to itself)
  • internal linkage (can be accessed anywhere within the file)
  • external linkage (the file, or the other files via forward declaration)

Namespaces

Problem (this is why namespaces are introduced):

foo.h

1
2
3
int doSomething(int x, int y) {
return x + y;
}

goo.h

1
2
3
int doSomething(int x, int y) {
return x - y;
}

main.cpp

1
2
3
4
5
6
7
8
#include "foo.h"
#include "goo.h"
#include <iostream>

int main() {
std::cout << doSomething(4, 3) << "\n";
return 0;
}

What is a namespace?

A namespace defines an area of code in which all identifiers are guaranteed to be unique. By default, global variables and normal functions are defined in the global namespace.

foo.h:

1
2
3
4
5
namespace Foo {
int doSomething(int x, int y) {
return x + y;
}
}

Multiple namespace blocks with the same name allowed

It’s legal to declare namespace blocks in multiple locations (either across multiple files, or multiple places within the same file). All declarations within the namespace block are considered part of the namespace.

add.h:

1
2
3
4
5
namespace BasicMath {
int add(int x, int y) {
return x + y;
}
}

subtract.h:

1
2
3
4
5
namespace BasicMath {
int subtract(int x, int y) {
return x - y;
}
}

The standard library makes extensive use of this feature, as all of the different header files included with the standard library have their functionality inside namespace std.

Nested namespaces and namespace aliases

1
2
3
4
5
6
7
namespace Foo {
namespace Goo {
const int g_x = 5;
}
}
// main
std::cout << Foo::Goo::g_x;

In C++17, nested namespaces can also be declared this way:

1
2
3
4
5
namespace Foo::Goo { // left 2 right
const int g_x = 5;
}
// main
std::cout << Foo::Goo::g_x;

Because typing the fully qualified name of a variable or function inside a nested namespace can be painful, C++ allows you to create namespace aliases.

1
2
3
4
5
6
7
8
namespace Foo {
namespace Goo {
const int g_x = 5;
}
}
namespace Boo = Foo::Goo; // Boo now refers to Foo::Goo
// main
std::cout << Boo::g_x; // This is really Foo::Goo::g_x

It’s worth noting that namespaces in C++ were not designed as a way to implement an information hierarchy — they were designed primarily as a mechanism for preventing naming collisions.

In general, you should avoid nesting namespaces if possible, and there are few good reasons to nest them more than 2 levels deep. However, in later lessons, we will see other related cases where the scope resolution operator needs to be used more than once.

Using statements

If you’re using the standard library a lot, typing “std::” before everything you use from the standard library can become repetitive. C++ provides some alternatives to simplify things, called using statements.

The using declaration

1
2
using std::cout; // this using declaration tells the compiler that cout should resolve to std::cout
cout << "Hello world!"; // so no std:: prefix is needed here!

This doesn’t save much effort in this trivial example, but if you are using cout a lot inside of a function, a using declaration can make your code more readable. Note that you will need a separate using declaration for each name you use (e.g. one for std::cout, one for std::cin, and one for std::endl).

The using directive

1
2
using namespace std; // this using directive tells the compiler that we're using everything in the std namespace!
cout << "Hello world!"; // so no std:: prefix is needed here!

For illustrative purposes, let’s take a look at an example where a using directive causes ambiguity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace a {
int x(10);
}

namespace b {
int x(20);
}

int main() {
using namespace a;
using namespace b;
std::cout << x << '\n'; // error
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int cout() {
return 5;
}

int main() {
using namespace std; // makes std::cout accessible as "cout"
cout << "Hello, world!"; // uh oh! Which cout do we want here? The one in the std namespace or the one we defined above?
// error
return 0;
}

// but we can use:
int main() {
// 1
std::cout << "Hello, world!";
// 2
using std::cout;
cout << "Hello, world!";
}

Many new programmers put using directives into the global scope. This pulls all of the names from the namespace directly into the global scope, greatly increasing the chance for naming collisions to occur. This is considered bad practice.

Rule: Avoid “using” statements outside of a function (in the global scope).

Suggestion: We recommend you avoid “using directives” entirely.

It’s because there’s no way to cancel the “using namespace XXX”.

The best you can do is intentionally limit the scope of the using statement from the outset using the block scoping rules.

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
{
using namespace Foo;
// calls to Foo:: stuff here
} // using namespace Foo expires

{
using namespace Goo;
// calls to Goo:: stuff here
} // using namespace Goo expires

return 0;
}

Of course, all of this headache can be avoided by explicitly using the scope resolution operator (::) in the first place.

Random number generation

Computers are generally incapable of generating random numbers. Instead, they must simulate randomness, which is most often done using pseudo-random number generators.

A pseudo-random number generator (PRNG) is a program that takes a starting number (called a seed), and performs mathematical operations on it to transform it into some other number that appears to be unrelated to the seed. It then takes that generated number and performs the same mathematical operation on it to transform it into a new number that appears unrelated to the number it was generated from. By continually applying the algorithm to the last generated number, it can generate a series of new numbers that will appear to be random if the algorithm is complex enough.

A short program that generates 100 pseudo-random numbers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

unsigned int PRGN() {
static unsigned int seed = 5323;
// Take the current seed and generate a new value from it
// Due to our use of large constants and overflow, it would be
// hard for someone to casually predict what the next number is
// going to be from the previous one.
seed = 8253729 * seed + 2396403;

// Take the seed and return a value between 0 and 32767
return seed % 32768;
}

int main() {
// Print 100 random numbers
for (int count = 1; count <= 100; ++count) {
std::cout << PRNG() << "\t";
if (count % 5 == 0) {
std::cout << "\n";
}
}
}

What is a good PRNG?

  • The PRNG should generate each number with approximately the same probability.
  • The method by which the next number in the sequence is generated shouldn’t be obvious or predictable.
  • The PRNG should have a good dimensional distribution of numbers.
  • All PRNGs are periodic, which means that at some point the sequence of numbers generated will eventually begin to repeat itself.

std::rand() is a mediocre PRNG

The algorithm used to implement std::rand() can vary from compiler to compiler, leading to results that may not be consistent across compilers. Most implementations of rand() use a method called a Linear Congruential Generator (LCG). If you have a look at the first example in this lesson, you’ll note that it’s actually a LCG, though one with intentionally picked poor constants. LCGs tend to have shortcomings that make them not good choices for most kinds of problems.

For applications where a high-quality PRNG is useful, I would recommend Mersenne Twister (or one of its variants), which produces great results and is relatively easy to use. Mersenne Twister was adopted into C++11, and we’ll show how to use it later in this lesson.

Although you can create a static local std::mt19937 variable in each function that needs it (static so that it only gets seeded once), it’s a little overkill to have every function that need a random number generator seed and maintain its own local generator. A better option in most cases is to create a global random number generator (inside a namespace!). Remember how we told you to avoid non-const global variables? This is an exception (also note: std::rand() and std::srand() access a global object, so there’s precedent for this).

1
2
3
4
5
6
7
8
9
10
11
#include <random> // for std::mt19937
#include <ctime> // for std::time
namespace MyRandom {
// Initialize our mersenne twister with a random seed based on the clock (once at system startup)
std::mt19937 mersenne(static_cast<unsigned int>(std::time(nullptr)));
}

int getRandomNumber(int min, int max) {
std::uniform_int_distribution<> die(min, max); // we can create a distribution in any function that needs it
return die(MyRandom::mersenne); // and then generate a random number from our global generator
}

A perhaps better solution is to use a 3rd party library that handles all of this stuff for you, such as the header-only Effolkronium's random library. You simply add the header to your project, #include it, and then you can start generating random numbers via Random::get(min, max).

1
2
3
4
5
6
7
8
9
#include <iostream>
#include "random.hpp"
using Random = effolkronium::random_static;
int main() {
std::cout << Random::get(1, 6) << '\n';
std::cout << Random::get(1, 10) << '\n';
std::cout << Random::get(1, 20) << '\n';
return 0;
}

std::cin, extraction, and dealing with invalid text input

When the user enters input in response to an extraction operation, that data is placed in a buffer inside of std::cin. A buffer (also called a data buffer) is simply a piece of memory set aside for storing data temporarily while it’s moved from one place to another. In this case, the buffer is used to hold user input while it’s waiting to be extracted to variables.

When the extraction operator is used, the following procedure happens:

  • If there is data already in the input buffer, that data is used for extraction.
  • If the input buffer contains no data, the user is asked to input data for extraction (this is the case most of the time). When the user hits enter, a ‘\n’ character will be placed in the input buffer.
  • operator >> extracts as much data from the input buffer as it can into the variable (ignoring any leading whitespace characters, such as spaces, tabs, or ‘\n’).
  • Any data that can not be extracted is left in the input buffer for the next extraction.

Extraction succeeds if at least one character is extracted from the input buffer. Any unextracted input is left in the input buffer for future extractions. For example:

1
2
int x;
std::cin >> x;

If the user enters “5a”, 5 will be extracted, converted to an integer, and assigned to variable x. “a\n” will be left in the input stream for the next extraction. Extraction fails if the input data does not match the type of the variable being extracted to. For example:

1
2
3
// current buffer: "a\n"
int x;
std::cin >> x;

There are three basic ways to do input validation:

  • Inline (as the uesr types)
    • Prevent the user from typing invalid input the first place.
    • Unfortunately, std::cin does not support this style of validation.
  • Post-entry (after the user types)
    • Let the user enter whatever they want into a string, then validate whether the string is correct, and if so, convert the string to the final variable format.
    • Let the user enter whatever they want, let std::cin and operator>> try to extract it, and handle the error cases.

Types of invalid text input:

  • Error A: Input extraction succeeds but the input is meaningless to the program (e.g. entering ‘k’ as your mathematical operator).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // For A
    char getOperator() {
    while (true) {
    std::cout << Enter: << "\n";
    char op;
    std::cin >> op;

    if (op == '+' || op == '-' || op == '*' || op == '/')
    return op;
    else
    std::cout << "Oops! Try again!" << "\n";
    }
    }
  • Error B: Input extraction succeeds but the user enters additional input (e.g. entering ‘*q hello’ as your mathematical operator).

    1
    2
    3
    4
    // For B
    // Enter a double value: 5*7
    // Enter one of the following: +, -, *, or /: Enter a double value: 5 * 7 is 35
    std::cin.ignore(32767, '\n'); // clear (up to 32767) characters out of the buffer until a '\n' character is removed

    Since the last character the user entered must be a ‘\n’, we can tell std::cin to ignore buffered characters until it finds a newline character (which is removed as well).

  • Error C: Input extraction fails (e.g. trying to enter ‘q’ into a numeric input).

    Now consider the following execution of the calculator program:

    1
    Enter a double value: a

    You shouldn’t be surprised that the program doesn’t perform as expected, but how it fails is interesting:

    1
    2
    Enter a double value: a
    Enter one of the following: +, -, *, or /: Enter a double value:

    and the problem suddenly ends.
    This looks pretty similar to the extraneous input case, but it’s a little different. Let’s take a closer look.

    When the user enters ‘a’, that character is placed in the buffer. Then operator>> tries to extract ‘a’ to variable x, which is of type double. Since ‘a’ can’t be converted to a double, operator>> can’t do the extraction. Two things happen at this point: ‘a’ is left in the buffer, and std::cin goes into “failure mode”.

    Once in ‘failure mode’, future requests for input extraction will silently fail. Thus in our calculator program, the output prompts still print, but any requests for further extraction are ignored. The program simply runs to the end and then terminates (without printing a result, because we never read in a valid mathematical operation).

    Fortunately, we can detect whether an extraction has failed and fix it:

    1
    2
    3
    4
    5
    if (std::cin.fail()) {
    // yep, so let's handle the failure
    std::cin.clear(); // put us back in 'normal' operation mode
    std::cin.ignore(32767, '\n'); // and remove the bad input
    }

    Combine A, B, and C:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    double getDouble() {
    while (true) {
    std::cout << "Enter a double value: ";
    double x;
    std::cin >> x;

    if (std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(32767, '\n');
    } else {
    std::cin.ignore(32767, '\n');
    return x;
    }
    }
    }

    Note: Prior to C++11, a failed extraction would not modify the variable being extracted to. This means that if a variable was uninitialized, it would stay uninitialized in the failed extraction case. However, as of C++11, a failed extraction due to invalid input will cause the variable to be zero-initialized. Zero initialization means the variable is set to 0, 0.0, “”, or whatever value 0 converts to for that type.

  • Error D: Input extraction succeeds but the user overflows a numeric value.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int main() {
    std::int16_t x { 0 }; // x is 16 bits, holds from -32768 to 32767
    std::cout << "Enter a number between -32768 and 32767: ";
    std::cin >> x;

    std::int16_t y { 0 }; // y is 16 bits, holds from -32768 to 32767
    std::cout << "Enter another number between -32768 and 32767: ";
    std::cin >> y;

    std::cout << "The sum is: " << x + y << '\n';
    return 0;
    }
    1
    2
    Enter a number between -32768 and 32767: 40000
    Enter another number between -32768 and 32767: The sum is: 32767

    In the above case, std::cin goes immediately into “failure mode”, but also assigns the closest in-range value (>= C++11) to the variable. Consequently, x is left with the assigned value of 32767. Additional inputs are skipped, leaving y with the initialized value of 0. We can handle this kind of error in the same way as a failed extraction.

Conclusion

For each point of text input, consider:

  • Could extraction fail?
  • Could the user enter more input than expected?
  • Could the user enter meaningless input?
  • Could the user overflow an input?

The following code will test for and fix failed extractions or overflow:

1
2
3
4
5
if (std::cin.fail()) { // has a previous extraction failed or overflowed?
// yep, so let's handle the failure
std::cin.clear(); // put us back in 'normal' operation mode
std::cin.ignore(32767,'\n'); // and remove the bad input
}

The following will also clear any extraneous input:

1
std::cin.ignore(32767,'\n'); // and remove the bad input

Finally, use loops to ask the user to re-enter input if the original input was invalid (meaningless).

Arrays, Strings, Pointers, and References

String & Arrays

One important point to note is that C-style strings follow all the same rules as arrays. This means you can initialize the string upon creation, but you can not assign values to it using the assignment operator after that!

1
2
char myString[] = "string"; // ok
myString = "rope"; // not ok!

Bad practice:

1
2
3
4
char name[255]; // declare array large enough to hold 255 characters
std::cout << "Enter your name: ";
std::cin >> name;
std::cout << "You entered: " << name << '\n';

In the above program, we’ve allocated an array of 255 characters to name, guessing that the user will not enter this many characters. Although this is commonly seen in C/C++ programming, it is poor programming practice, because nothing is stopping the user from entering more than 255 characters (either unintentionally, or maliciously).

The recommended way of reading strings using cin is as follows:

1
2
3
4
char name[255]; // declare array large enough to hold 255 characters
std::cout << "Enter your name: ";
std::cin.getline(name, 255);
std::cout << "You entered: " << name << '\n';

This call to cin.getline() will read up to 254 characters into name (leaving room for the null terminator!). Any excess characters will be discarded. In this way, we guarantee that we will not overflow the array!

Note the difference between strlen() and std::size(). strlen() prints the number of characters before the null terminator, whereas std::size (or the sizeof() trick) returns the size of the entire array, regardless of what’s in it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <cstring>
#include <iterator> // for std::size

int main() {
char name[20] = "Alex"; // only use 5 characters (4 letters + null terminator)
std::cout << "My name is: " << name << '\n';
std::cout << name << " has " << strlen(name) << " letters.\n";
std::cout << name << " has " << std::size(name) << " characters in the array.\n"; // use sizeof(name) / sizeof(name[0]) if not C++17 capable

return 0;
// My name is: Alex
// Alex has 4 letters.
// Alex has 20 characters in the array.
}

Don’t use C-style strings

It is important to know about C-style strings because they are used in a lot of code. However, now that we’ve explained how they work, we’re going to recommend that you avoid them altogether whenever possible! Unless you have a specific, compelling reason to use C-style strings, use std::string (defined in the header) instead. std::string is easier, safer, and more flexible. In the rare case that you do need to work with fixed buffer sizes and C-style strings (e.g. for memory-limited devices), we’d recommend using a well-tested 3rd party string library designed for the purpose instead.

Pointers

What good are pointers?

At this point, pointers may seem a little silly, academic, or obtuse. Why use a pointer if we can just use the original variable?

It turns out that pointers are useful in many different cases:

  1. Arrays are implemented using pointers. Pointers can be used to iterate through an array (as an alternative to array indices).
  2. They are the only way you can dynamically allocate memory in C++. This is by far the most common use case for pointers.
  3. They can be used to pass a large amount of data to a function in a way that doesn’t involve copying the data, which is inefficient.
  4. They can be used to pass a function as a parameter to another function.
  5. They can be used to achieve polymorphism when dealing with inheritance.
  6. They can be used to have one struct/class point at another struct/class, to form a chain. This is useful in some more advanced data structures, such as linked lists and trees.

So there are actually a surprising number of uses for pointers. But don’t worry if you don’t understand what most of these are yet. Now that you understand what pointers are at a basic level, we can start taking an in-depth look at the various cases in which they’re useful, which we’ll do in subsequent lessons.

Pointers convert to boolean false if they are null, and boolean true if they are non-null. Therefore, we can use a conditional to test whether a pointer is null or not:

1
2
3
4
5
6
double *ptr = 0 ;
// pointers convert to boolean false if they are null, and boolean true if they are non-null
if (ptr)
cout << "ptr is pointing to a double value.";
else
cout << "ptr is a null pointer.";

Best practice: Initialize your pointers to a null value if you’re not giving them another value.

In C++, there is a special preprocessor macro called NULL (defined in the header). This macro was inherited from C, where it is commonly used to indicate a null pointer.

The value of NULL is implementation defined, but is usually defined as the integer constant 0. Note: as of C++11, NULL can be defined as nullptr instead (which we’ll discuss in a bit).

Best Practice: Because NULL is a preprocessor macro with an implementation defined value, avoid using NULL (sure?).

Note that the value of 0 isn’t a pointer type, so assigning 0 (or NULL, pre-C++11) to a pointer to denote that the pointer is a null pointer is a little inconsistent. In rare cases, when used as a literal argument, it can even cause problems because the compiler can’t tell whether we mean a null pointer or the integer 0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cstddef> // for NULL

void print(int x) {
std::cout << "print(int): " << x << '\n';
}

void print(int *x) {
if (!x)
std::cout << "print(int*): null\n";
else
std::cout << "print(int*): " << *x << '\n';
}

int main() {
int *x { NULL };
print(x); // calls print(int*) because x has type int*
print(0); // calls print(int) because 0 is an integer literal
print(NULL); // likely calls print(int), although we probably wanted print(int*)
return 0;
}

To address the above issues, C++11 introduces a new keyword called nullptr. nullptr is both a keyword and an rvalue constant, much like the boolean keywords true and false are.

1
int *ptr = nullptr;

C++ will implicitly convert nullptr to any pointer type. So in the above example, nullptr is implicitly converted to an integer pointer, and then the value of nullptr assigned to ptr. This has the effect of making integer pointer ptr a null pointer.

C++11 also introduces a new type called std::nullptr_t (in header ). std::nullptr_t can only hold one value: nullptr! While this may seem kind of silly, it’s useful in one situation. If we want to write a function that accepts only a nullptr argument, what type do we make the parameter? The answer is std::nullptr_t.

In all but two cases (which we’ll cover below), when a fixed array is used in an expression, the fixed array will decay (be implicitly converted) into a pointer that points to the first element of the array. (But a pointer is still not an array though)

Arrays in structs and classes don’t decay

Finally, it is worth noting that arrays that are part of structs or classes do not decay when the whole struct or class is passed to a function. This yields a useful way to prevent decay if desired, and will be valuable later when we write classes that utilize arrays.

For optimization purposes, multiple string literals may be consolidated into a single value. For example:

These are two different string literals with the same value. The compiler may opt to combine these into a single shared string literal, with both name1 and name2 pointed at the same address. Thus, if name1 was not const, making a change to name1 could also impact name2 (which might not be expected). Actually, if there is no const modifier, name1 can’t be changed still.

Rule: Feel free to use C-style string symbolic constants if you need read-only strings in your program, but always make them const!

By outputting char or const char std::cout will assume you are going to print a string instead of an address (int *). While this is great 99% of the time, it can lead to unexpected results. Consider the following case:

1
2
3
char c = 'Q';
std::cout << &c;
// Q╠╠╠╠╜╡4;¿■A

Why did it do this? Well, it assumed &c (which has type char*) was a string. So it printed the ‘Q’, and then kept going. Next in memory was a bunch of garbage. Eventually, it ran into some memory holding a 0 value, which it interpreted as a null terminator, so it stopped. What you see may be different depending on what’s in memory after variable c.

C++ supports three basic types of memory allocation, of which you’ve already seen two.

  • Static memory allocation happens for static and global variables. Memory for these types of variables is allocated once when your program is run and persists throughout the life of your program.
  • Automatic memory allocation happens for function parameters and local variables. Memory for these types of variables is allocated when the relevant block is entered, and freed when the block is exited, as many times as necessary.
  • Dynamic memory allocation is the topic of this article.

To allocate a single variable dynamically, we use the scalar (non-array) form of the new operator:

1
2
3
new int;
int *ptr1 = new int(5); // direct initialization
int *ptr2 = new int{ 5 }; // uniform initialization

If it wasn’t before, it should now be clear at least one case in which pointers are useful. Without a pointer to hold the address of the memory that was just allocated, we’d have no way to access the memory that was just allocated for us!

When we are done with a dynamically allocated variable, we need to explicitly tell C++ to free the memory for reuse. For single variables, this is done via the scalar (non-array) form of the delete operator:

1
2
3
// assume ptr has previously been allocated with operator new
delete ptr; // return the memory pointed to by ptr to the operating system
ptr = nullptr; // set ptr to be a null pointer (use nullptr instead of 0 in C++11)

The delete operator does not actually delete anything. It simply returns the memory being pointed to back to the operating system. The operating system is then free to reassign that memory to another application (or to this application again later).

Note that deleting a pointer that is not pointing to dynamically allocated memory may cause bad things to happen.

A pointer that is pointing to deallocated memory is called a dangling pointer. Dereferencing or deleting a dangling pointer will lead to undefined behavior.

Rule: Set deleted pointers to 0 (or nullptr in C++11) unless they are going out of scope immediately afterward.

Operator new can fail

When requesting memory from the operating system, in rare circumstances, the operating system may not have any memory to grant the request with.

By default, if new fails, a bad_alloc exception is thrown. If this exception isn’t properly handled (and it won’t be, since we haven’t covered exceptions or exception handling yet), the program will simply terminate (crash) with an unhandled exception error.

In many cases, having new throw an exception (or having your program crash) is undesirable, so there’s an alternate form of new that can be used instead to tell new to return a null pointer if memory can’t be allocated. This is done by adding the constant std::nothrow between the new keyword and the allocation type:

1
int *value = new (std::nothrow) int; // value will be set to a null pointer if the integer allocation fails

Note that if you then attempt to dereference this memory, undefined behavior will result (most likely, your program will crash). Consequently, the best practice is to check all memory requests to ensure they actually succeeded before using the allocated memory.

1
2
3
4
int *value = new (std::nothrow) int;
if (!value) {
std::cout << "Could not allocate memory" << "\n";
}

Deleting a null pointer has no effect. Thus, there is no need for the following:

1
2
if (ptr)
delete ptr;

Memory leaks

Memory leaks happen when your program loses the address of some bit of dynamically allocated memory before giving it back to the operating system. When this happens, your program can’t delete the dynamically allocated memory, because it no longer knows where it is. The operating system also can’t use this memory, because that memory is considered to be still in use by your program.

Dynamically allocated memory effectively has no scope. That is, it stays allocated until it is explicitly deallocated or until the program ends (and the operating system cleans it up, assuming your operating system does that). However, the pointers used to hold dynamically allocated memory addresses follow the scoping rules of normal variables. This mismatch can create interesting problems.

1
2
3
4
5
6
7
8
9
10
11
12
void doSomething() {
int *ptr = new int;
}

// another example
int value = 5;
int *ptr = new int; // allocate memory
ptr = &value; // old address lost, memory leak results

// another example
int *ptr = new int;
ptr = new int; // old address lost, memory leak results

Dynamically allocating arrays

In addition to dynamically allocating single values, we can also dynamically allocate arrays of variables. Unlike a fixed array, where the array size must be fixed at compile time, dynamically allocating an array allows us to choose an array length at runtime.

To allocate an array dynamically, we use the array form of new and delete (often called new[] and delete[]):

1
2
3
int *array = new int[length];
delete[] array;
// Essentially, the new[] operator is called, even though the [] isn't placed next to the new keyword.

One often asked question of array delete[] is, “How does array delete know how much memory to delete?” The answer is that array new[] keeps track of how much memory was allocated to a variable, so that array delete[] can delete the proper amount. Unfortunately, this size/length isn’t accessible to the programmer (which means we need to keep track of the length if we want to access the size of a dynamically allocating array).

Dynamic arrays are almost identical to fixed arrays, but remember to delete[] it

If you want to initialize a dynamically allocated array to 0, the syntax is quite simple:

1
int *array = new int[length]();

Prior to C++11, there was no easy way to initialize a dynamic array to a non-zero value (initializer lists only worked for fixed arrays). This means you had to loop through the array and assign element values explicitly.

1
2
3
4
5
6
int *array = new int[5];
array[0] = 9;
array[1] = 7;
array[2] = 5;
array[3] = 3;
array[4] = 1;

However, starting with C++11, it’s now possible to initialize dynamic arrays using initializer lists!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int fixedArray[5] = { 9, 7, 5, 3, 1 };
// initialize a fixed array in C++03
int *array = new int[5] { 9, 7, 5, 3, 1 };
// initialize a dynamic array in C++11
int fixedArray[5] { 9, 7, 5, 3, 1 };
// initialize a fixed array in C++11

char fixedArray[14] { "Hello, world!" };
// initialize a fixed array in C++11

char *array = new char[14] { "Hello, world!" };
// doesn't work in C++11
// If you have a need to do this, dynamically allocate a std::string instead (or allocate your char array and then strcpy the string in).

int *dynamicArray1 = new int[] {1, 2, 3};
// not okay: implicit size for dynamic arrays!

Resizing arrays (not okay)

Dynamically allocating an array allows you to set the array length at the time of allocation. However, C++ does not provide a built-in way to resize an array that has already been allocated. It is possible to work around this limitation by dynamically allocating a new array, copying the elements over, and deleting the old array. However, this is error prone, especially when the element type is a class (which have special rules governing how they are created).

Consequently, we recommend avoiding doing this yourself.

Fortunately, if you need this capability, C++ provides a resizable array as part of the standard library called std::vector. We’ll introduce std::vector shortly.

Pointers and const

Pointing to const value:

1
2
3
4
5
6
7
int value = 5;
const int *ptr = &value; // ptr points to a "const int"
value = 6; // the value is non-const when accessed through a non-const identifier

int value = 5;
const int *ptr = &value; // ptr points to a "const int"
*ptr = 6; // error - ptr treats its value as const, so changing the value through ptr is not legal

Const pointers:

1
2
int value = 5;
int *const ptr = &value;

Const pointer to a const value:

1
2
int value = 5;
const int *const ptr = &value;

References

l-values and r-values

In C++, variables are a type of l-value (pronounced ell-value). An l-value is a value that has an address (in memory). Since all variables have addresses, all variables are l-values. The name l-value came about because l-values are the only values that can be on the left side of an assignment statement. When we do an assignment, the left hand side of the assignment operator must be an l-value. Consequently, a statement like 5 = 6; will cause a compile error, because 5 is not an l-value. The value of 5 has no memory, and thus nothing can be assigned to it. 5 means 5, and its value can not be reassigned. When an l-value has a value assigned to it, the current value at that memory address is overwritten.

The opposite of l-values are r-values (pronounced arr-values). An r-value refers to any value that can be assigned to an l-value. r-values are always evaluated to produce a single value. Examples of r-values are literals (such as 5, which evaluates to 5), variables (such as x, which evaluates to whatever value was last assigned to it), or expressions (such as 2 + x, which evaluates to the value of x plus 2).

1
2
x = 7;
x = x + 1;

In this statement, the variable x is being used in two different contexts. On the left side of the assignment operator, “x” is being used as an l-value (variable with an address). On the right side of the assignment operator, x is being used as an r-value, and will be evaluated to produce a value (in this case, 7). When C++ evaluates the above statement, it evaluates as: x = 7 + 1;

The key takeaway is that on the left side of the assignment, you must have something that represents a memory address (such as a variable). Everything on the right side of the assignment will be evaluated to produce a value.

Note: const variables are considered non-modifiable l-values.

Three basic variable types:

  • Normal variables
  • Pointers
  • Reference variables

A reference is a type of C++ variable that acts as an alias to another object or value.

C++ supports three kinds of references:

  • References to non-const values (typically just called “references”, or “non-const references”), which we’ll discuss in this lesson.

    1
    2
    3
    4
    5
    6
    int value = 5; // normal integer
    int &ref = value; // reference to variable value

    int x = 5; // normal integer
    int &y = x; // y is a reference to x
    int &z = y; // z is also a reference to x

    Using the address-of operator on a reference returns the address of the value being referenced:

    1
    2
    cout << &value; // prints 0012FF7C
    cout << &ref; // prints 0012FF7C
  • References to const values (often called “const references”), which we’ll discuss in the next lesson.

  • C++11 added r-value references, which we cover in detail in the chapter on move semantics.

References must be initialized.

References to non-const values can only be initialized with non-const l-values. They can not be initialized with const l-values or r-values.

References can not be reassigned

Once initialized, a reference can not be changed to reference another variable. Consider the following snippet:

1
2
3
4
5
int value1 = 5;
int value2 = 6;

int &ref = value1; // okay, ref is now an alias for value1
ref = value2; // assigns 6 (the value of value2) to value1 -- does NOT change the reference!

Note that the second statement may not do what you might expect! Instead of reassigning ref to reference variable value2, it instead assigns the value from value2 to value1 (which ref is a reference of).

References as function parameters

References are most often used as function parameters. In this context, the reference parameter acts as an alias for the argument, and no copy of the argument is made into the parameter. This can lead to better performance if the argument is large or expensive to copy.

In lesson 6.8 — Pointers and arrays we talked about how passing a pointer argument to a function allows the function to dereference the pointer to modify the argument’s value directly.

References work similarly in this regard. Because the reference parameter acts as an alias for the argument, a function that uses a reference parameter is able to modify the argument passed in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

// ref is a reference to the argument passed in, not a copy
void changeN(int &ref) {
ref = 6;
}

int main() {
int n = 5;
std::cout << n << '\n';
changeN(n);
// note that this argument does not need to be a reference
std::cout << n << '\n';
return 0;
}

Best practice: Pass arguments by non-const reference when the argument needs to be modified by the function.

The primary downside of using non-const references as function parameters is that the argument must be a non-const l-value. This can be restrictive.

Using references to pass C-style arrays to functions

One of the most annoying issues with C-style arrays is that in most cases they decay to pointers when evaluated. However, if a C-style array is passed by reference, this decaying does not happen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // Note: You need to specify the array size in the function declaration
void printElements(int (&arr)[4]) {
int length = sizeof(arr) / sizeof(arr[0]);
// we can now do this since the array won't decay
for (int i = 0; i < length; ++i) {
std::cout << arr[i] << std::endl;
}
}

int main() {
int arr[] = { 99, 20, 14, 80 };
printElements(arr);
return 0;
}

References as shortcuts

A secondary (much less used) use of references is to provide easier access to nested data. Consider the following struct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Something {
int value1;
float value2;
};

struct Other {
Something something;
int otherValue;
};
Other other;
int &ref = other.something.value1;
// instead of using:
other.something.value1 = 5;
// we can use
ref = 5;

References vs pointers

References and pointers have an interesting relationship — a reference acts like a pointer that is implicitly dereferenced when accessed (references are usually implemented internally by the compiler using pointers). Thus given the following:

1
2
3
int value = 5;
int *const ptr = &value;
int &ref = value;

*ptr and ref evaluate identically. As a result, the following two statements produce the same effect:

1
2
*ptr = 5;
ref = 5;

Because references must be initialized to valid objects (cannot be null) and can not be changed once set, references are generally much safer to use than pointers (since there’s no risk of dereferencing a null pointer). However, they are also a bit more limited in functionality accordingly.

If a given task can be solved with either a reference or a pointer, the reference should generally be preferred. Pointers should only be used in situations where references are not sufficient (such as dynamically allocating memory).

References to r-values extend the lifetime of the referenced value

Normally r-values have expression scope, meaning the values are destroyed at the end of the expression in which they are created.

1
std::cout << 2 + 3; // 2 + 3 evaluates to r-value 5, which is destroyed at the end of this statement

However, when a reference to a const value is initialized with an r-value, the lifetime of the r-value is extended to match the lifetime of the reference.

1
2
3
int somefcn() {
int &ref = 2 + 3; // error
}
1
2
3
4
5
int somefcn() {
const int &ref = 2 + 3; // normally the result of 2+3 has expression scope and is destroyed at the end of this statement
// but because the result is now bound to a reference to a const value...
std::cout << ref; // we can use it here
} // and the lifetime of the r-value is extended to here, when the const reference dies

Const references as function parameters

References used as function parameters can also be const. This allows us to access the argument without making a copy of it, while guaranteeing that the function will not change the value being referenced.

1
2
3
4
// ref is a const reference to the argument passed in, not a copy
void changeN(const int &ref) {
ref = 6; // not allowed, ref is const
}

References to const values are particularly useful as function parameters because of their versatility. A const reference parameter allows you to pass in a non-const l-value argument, a const l-value argument, a literal, or the result of an expression:

1
2
3
4
int a = 1;
printIt(a); // non-const l-value
const int b = 2;
printIt(b); // const l-value

To avoid making unnecessary, potentially expensive copies, variables that are not pointers or fundamental data types (int, double, etc…) should be generally passed by (const) reference.

Fundamental data types should be passed by value, unless the function needs to change them.

Rule: Pass non-pointer, non-fundamental data type variables (such as structs) by (const) reference.

1
2
3
4
5
6
7
8
struct Person {
int age;
double weight;
};
Person person; // In C++, we don't need "struct" word.
Person *ptr = &person;
(*ptr).age = 5;
ptr->age = 5;

Rule: When using a pointer to access the value of a member, use operator-> instead of operator. (the . operator)

For-each loops

C++11 introduces a new type of loop called a for-each loop (also called a range-based for loop) that provides a simpler and safer method for cases where we want to iterate through every element in an array (or other list-type structure).

1
2
for (element_declaration : array)
statement;

Because element_declaration should have the same type as the array elements, this is an ideal case in which to use the auto keyword, and let C++ deduce the type of the array elements for us.

Copying array elements can be expensive, and most of the time we really just want to refer to the original element. Fortunately, we can use references for this:

1
2
3
int array[5] = { 9, 7, 5, 3, 1 };
for (auto &element : array)
std::cout << element << ' ';

And, of course, it’s a good idea to make your element const if you’re intending to use it in a read-only fashion.

Rule: In for-each loops element declarations, if your elements are non-fundamental types, use references or const references for performance reasons.

For-each doesn’t work with pointers to an array

In order to iterate through the array, for-each needs to know how big the array is, which means knowing the array size. Because arrays that have decayed into a pointer do not know their size, for-each loops will not work with them!

1
2
3
4
5
6
int sumArray(int array[]) { // array is a pointer
int sum = 0;
for (const auto &number : array) // compile error, the size of array isn't known
sum += number;
return sum;
}

Similarly, dynamic arrays won’t work with for-each loops for the same reason.

Multidimensional arrays

1
2
3
int **array = new int[10][5];  // won't work
int (*array)[5] = new int[10][5]; // ok
auto array = new int[10][5]; // In C++11, you can use.

Unfortunately, this relatively simple solution doesn’t work if the right-most array dimension isn’t a compile-time constant.

Intro to std::array

Introduced in C++11, std::array provides fixed array functionality that won’t decay when passed into a function. std::array is defined in the array header, inside the std namespace.

1
2
3
#include <array>
std::array<int, 3> myArray1;
std::array<int, > myArray2 = { 9, 7, 5, 3, 1 }; // error, cannot omit

Just like the native implementation of fixed arrays, the length of a std::array must be set at compile time.

std::array supports a second form of array element access (the at() function) that does bounds checking:

1
2
3
std::array<int, 5> myArray { 1, 2, 3, 4, 5 };
myArray.at(1) = 6;
myArray.at(9) = 10; // array element 9 is invalid, will throw error

In the above example, the call to array.at(1) checks to ensure array element 1 is valid, and because it is, it returns a reference to array element 1. We then assign the value of 6 to this. However, the call to array.at(9) fails because array element 9 is out of bounds for the array. Instead of returning a reference, the at() function throws an error that terminates the program (note: It’s actually throwing an exception of type std::out_of_range — we cover exceptions in chapter 15). Because it does bounds checking, at() is slower (but safer) than operator[].

std::array will clean up after itself when it goes out of scope, so there’s no need to do any kind of cleanup.

1
myArray.size();
1
2
3
4
5
6
7
8
9
void printLength(const std::array<double, 5> &myArray) {
std::cout << "length: " << myArray.size();
}

int main() {
std::array<double, 5> myArray { 9.0, 7.2, 5.4, 3.6, 1.8 };
printLength(myArray);
return 0;
}

Also note that we passed std::array by (const) reference. This is to prevent the compiler from making a copy of the std::array when the std::array was passed to the function (for performance reasons).

Rule: Always pass std::array by reference or const reference

Manually indexing std::array via size_type

1
2
3
std::array<int, 5> myArray { 7, 3, 1, 9, 5 };
for (int i = 0; i < myArray.size(); ++i)
std::cout << myArray[i] << ' ';

The answer is that there’s a likely signed/unsigned mismatch in this code! Due to a curious decision, the size() function and array index parameter to operator[] use a type called size_type, which is defined by the C++ standard as an unsigned integral type. Our loop counter/index (variable i) is a signed int. Therefore both the comparison i < myArray.size() and the array index myArray[i] have type mismatches.

Interestingly enough, size_type isn’t a global type (like int or std::size_t). Rather, it’s defined inside the definition of std::array (C++ allows nested types). This means when we want to use size_type, we have to prefix it with the full array type (think of std::array acting as a namespace in this regard). In our above example, the fully-prefixed type of “size_type” is std::array::size_type!

Therefore, the correct way to write the above code is as follows:

1
2
3
4
5
6
7
for (std::array<int, 5>::size_type i = 0; i < myArray.size(); ++i) {
// foo
}
// or
using index_t = std::array<int, 5>::size_type;
for (index_t i = 0; i < myArray.size(); ++i)
// foo

In all common implementations of std::array, size_type is a typedef for std::size_t. So it’s somewhat common to see developers use size_t instead. While not technically correct, in almost all implementations, this will work:

1
2
for (std::size_t i = 0; i < myArray.size(); ++i)
// foo

A better solution is to avoid manual indexing of std::array in the first place. Instead, use range-based for loops (or iterators) if possible.

Intro to std::vector

Introduced in C++03, std::vector provides dynamic array functionality that handles its own memory management. This means you can create arrays that have their length set at runtime, without having to explicitly allocate and deallocate memory using new and delete. std::vector lives in the header.

1
2
3
4
5
#include <vector>
// no need to specify length at initialization
std::vector<int> array;
std::vector<int> array2 = { 9, 7, 5, 3, 1 }; // use initializer list to initialize array
std::vector<int> array3 { 9, 7, 5, 3, 1 }; // use uniform initialization to initialize array (C++11 onward)
1
2
3
array[6] = 2; // no bounds checking
array.at(7) = 3; // does bounds checking
array = { 9, 8, 7 }; // In C++11

Just like with std::array, size() returns a value of nested type size_type (full type in the above example would be std::vector::size_type), which is an unsigned integer.

1
2
3
// resize an array
std::vector<int> array { 0, 1, 2 };
array.resize(5); // set size to 5

There are two things to note here. First, when we resized the array, the existing element values were preserved! Second, new elements are initialized to the default value for the type (which is 0 for integers).

Resizing a vector is computationally expensive, so you should strive to minimize the number of times you do so.

Compacting bools

std::vector has another cool trick up its sleeves. There is a special implementation for std::vector of type bool that will compact 8 booleans into a byte! This happens behind the scenes, and doesn’t change how you use the std::vector.

1
2
3
4
5
std::vector<bool> array { true, false, false, true, true };
std::cout << "The length is: " << array.size() << '\n';

for (auto const &element: array)
std::cout << element << ' ';

Functions

Parameters vs Arguments

In common usage, the terms parameter and argument are often interchanged. However, for the purposes of further discussion, we will make a distinction between the two:

A function parameter (sometimes called a formal parameter) is a variable declared in the function declaration: void foo(int x).

An argument (sometimes called an actual parameter) is the value that is passed to the function by the caller: foo(5).

Rule: When passing an argument by reference, always use a const reference unless you need to change the value of the argument

References to pointers

It’s possible to pass a pointer by reference, and have the function change the address of the pointer entirely:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void foo(int *&ptr) { // pass pointer by reference
// (int *) &ptr
ptr = nullptr; // this changes the actual ptr argument passed in, not a copy
}

int main() {
int x = 5;
int *ptr = &x;
std::cout << "ptr is: " << (ptr ? "non-null" : "null") << '\n'; // prints non-null
foo(ptr);
std::cout << "ptr is: " << (ptr ? "non-null" : "null") << '\n'; // prints null

return 0;
}

When to use pass by reference:

  • When passing structs or classes (use const if read-only).
  • When you need the function to modify an argument.
  • When you need access to the type information of a fixed array.

When not to use pass by reference:

  • When passing fundamental types that don’t need to be modified (use pass by value).

There is also pass by address.

When to use pass by address:

  • When passing built-in arrays (if you’re okay with the fact that they’ll decay into a pointer).
  • When passing a pointer and nullptr is a valid argument logically.

Return by address is often used to return dynamically allocated memory to the caller:

1
2
3
4
5
int* allocateArray(int size) {
return new int[size];
}
int *array = allocateArray(25);
delete[] array;

Just like return by address, you should not return local variables by reference. Consider the following example:

1
2
3
4
int& doubleValue(int x) {
int value = x * 2;
return value; // return a reference to value here
} // value is destroyed here

Return by reference is typically used to return arguments passed by reference to the function back to the caller. In the following example, we return (by reference) an element of an array that was passed to our function by reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Returns a reference to the index element of array
int& getElement(std::array<int, 25> &array, int index) {
// we know that array[index] will not be destroyed when we return to the caller (since the caller passed in the array in the first place!)
return array[index]; // so it's okay to return it by reference
}

int main() {
std::array<int, 25> array;
// Set the element of array with index 10 to the value 5
getElement(array, 10) = 5;
std::cout << array[10] << '\n';
return 0;
}

Lifetime extension doesn’t save dangling references

1
2
3
4
5
6
const int& returnByReference() {
return 5;
}
int main() {
const int &ref = returnByReference(); // runtime error
}

In the above program, returnByReference() is returning a const reference to a value that will go out of scope when the function ends. This is normally a no-no, as it will result in a dangling reference. However, we also know that assigning a value to a const reference can extend the lifetime of that value. So which takes precedence here? Does 5 go out of scope first, or does ref extend the lifetime of 5?

The answer is that 5 goes out of scope first, then ref extends the lifetime of the dangling reference. Lifetime extension only works when the object going out of scope is going out of scope in the same block (e.g. because it has expression scope). It does not work across function boundaries.

Use tuple to return multiple values

1
2
3
4
5
6
7
8
9
10
11
12
std::tuple<int, double> returnTuple() { // return a tuple that contains an int and a double
return std::make_tuple(5, 6.7);
// use std::make_tuple() as shortcut to make a tuple to return
}

int main() {
int a;
double b;
std::tie(a, b) = returnTuple(); // put elements of tuple in variables a and b
std::cout << a << ' ' << b << '\n';
return 0;
}

As of C++17, a structured binding declaration can be used to simplify splitting multiple returned values into separate variables:

1
2
3
4
5
6
int main() {
auto [a, b] = returnTuple(); // used structured binding declaration to put results of tuple in variables a and b
std::cout << a << ' ' << b << '\n';

return 0;
}

Using a struct is a better option than a tuple if you’re using the struct in multiple places. However, for cases where you’re just packaging up these values to return and there would be no reuse from defining a new struct, a tuple is a bit cleaner since it doesn’t introduce a new user-defined data type.

Inline functions

Rule: Be aware of inline functions, but modern compilers should inline functions for you as appropriate, so there isn’t a need to use the keyword.

Inline functions are exempt from the one-definition per program rule

In previous chapters, we’ve noted that you should not implement functions (with external linkage) in header files, because when those headers are included into multiple .cpp files, the function definition will be copied into multiple .cpp files. These files will then be compiled, and the linker will throw an error because it will note that you’ve defined the same function more than once.

However, inline functions are exempt from the rule that you can only have one definition per program, because of the fact that inline functions do not actually result in a real function being compiled — therefore, there’s no conflict when the linker goes to link multiple files together.

This may seem like an uninteresting bit of trivia at this point, but next chapter we’ll introduce a new type of function (a member function) that makes significant use of this point.

Even with inline functions, you generally should not define global functions in header files.

Function overloading

Function return types are not considered for uniqueness

A function’s return type is NOT considered when overloading functions. (Note for advanced readers: This was an intentional choice, as it ensures the behavior of a function call or subexpression can be determined independently from the rest of the expression, making understanding complex expressions much simpler. Put another way, we can always determine which version of a function will be called based solely on the arguments. If return values were included, then we wouldn’t have an easy syntactic way to tell which version of a function was being called — we’d also have to understand how the return value was being used, which requires a lot more analysis).

1
2
3
4
5
6
7
8
9
10
11
int getRandomValue();
double getRandomValue();
// the compiler will flag this as an error

// solution
// 1
int getRandomInt();
double getRandomDouble();
// 2 - not recommended
void getRandomValue(int &out);
void getRandomValue(double &out);

Typedefs are not distinct, since they don’t introduce new types. The following two declarations of Print() are considered identical:

1
2
3
typedef char *string;
void print(string value);
void print(char *value);

How function calls are matched with overloaded functions

Making a call to an overloaded function results in one of three possible outcomes:

  1. A match is found. The call is resolved to a particular overloaded function.
  2. No match is found. The arguments can not be matched to any overloaded function.
  3. An ambiguous match is found. The arguments matched more than one overloaded function.

When an overloaded function is called, C++ goes through the following process to determine which version of the function will be called:

First, C++ tries to find an exact match. This is the case where the actual argument exactly matches the parameter type of one of the overloaded functions. For example:

1
2
void print(char *value);
void print(int value); // print(0); exact match

Although 0 could technically match print(char) (as a null pointer), it exactly matches print(int) (matching char would require an implicit conversion). Thus print(int) is the best match available.

Secondly, if no exact match is found, C++ tries to find a match through promotion. To summarize,

  • Char, unsigned char, and short is promoted to an int.
  • Unsigned short can be promoted to int or unsigned int, depending on the size of an int
  • Float is promoted to double
  • Enum is promoted to int
1
2
void print(char *value);
void print(int value); // print('a'); match this one

Thirdly, if no promotion is possible, C++ tries to find a match through standard conversion. Standard conversions include:

  • Any numeric type will match any other numeric type, including unsigned (e.g. int to float)
  • Enum will match the formal type of a numeric type (e.g. enum to float)
  • Zero will match a pointer type and numeric type (e.g. 0 to char*, or 0 to float)
  • A pointer will match a void pointer
1
2
3
4
struct Employee; // defined somewhere else
void print(float value);
void print(Employee value);
print('a'); // 'a' converted to match print(float)

Finally, C++ tries to find a match through user-defined conversion. Although we have not covered classes yet, classes (which are similar to structs) can define conversions to other types that can be implicitly applied to objects of that class. For example, we might define a class X and a user-defined conversion to int.

1
2
3
4
5
class X; // with user-defined conversion to int
void print(float value);
void print(int value);
X value; // declare a variable named value of type class X
print(value); // value will be converted to an int and matched to print(int)

Ambiguous matches

Default parameters

1
2
3
4
5
6
7
8
void printValues(int x, int y=10) {
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
}
int main() {
printValues(1); // y will use default parameter of 10
printValues(3, 4); // y will use user-supplied value 4
}

A function can have multiple default parameters:

1
2
3
void printValues(int x=10, int y=20, int z=30) {
std::cout << "Values: " << x << " " << y << " " << z << '\n';
}

Note that it is impossible to supply an argument for parameter z without also supplying arguments for parameters x and y. This is because C++ does not support a function call syntax such as printValues(,,3). This has two major consequences:

  • All default parameters must be the rightmost parameters. The following is not allowed:

    1
    void printValue(int x=10, int y); // not allowed
  • If more than one default parameter exists, the leftmost default parameter should be the one most likely to be explicitly set by the user.

Default parameters can only be declared once

Once declared, a default parameter can not be redeclared. That means for a function with a forward declaration and a function definition, the default parameter can be declared in either the forward declaration or the function definition, but not both.

1
2
3
4
void printValues(int x, int y=10);
void printValues(int x, int y=10) {
// error: redefinition of default parameter
}

Default parameters can only be declared once

Once declared, a default parameter can not be redeclared. That means for a function with a forward declaration and a function definition, the default parameter can be declared in either the forward declaration or the function definition, but not both.

Default parameters and function overloading

Functions with default parameters may be overloaded. For example, the following is allowed:

1
2
void print(std::string string);
void print(char ch=' ');

If the user were to call print(), it would resolve to print(‘ ‘), which would print a space.

However, it is important to note that default parameters do NOT count towards the parameters that make the function unique. Consequently, the following is not allowed:

1
2
void printValues(int x);
void printValues(int x, int y=20);

If the caller were to call printValues(10), the compiler would not be able to disambiguate whether the user wanted printValues(int) or printValues(int, 20) with the default value.

Function Pointers

Note that the type (parameters and return type) of the function pointer must match the type of the function. Here are some examples of this:

1
2
3
4
5
6
7
8
9
10
11
// function prototypes
int foo();
double goo();
int hoo(int x);

// function pointer assignments
int (*fcnPtr1)() = foo; // okay
int (*fcnPtr2)() = goo; // wrong -- return types don't match!
double (*fcnPtr4)() = goo; // okay
fcnPtr1 = hoo; // wrong -- fcnPtr1 has no parameters, but hoo() does
int (*fcnPtr3)(int) = hoo; // okay

Unlike fundamental types, C++ will implicitly convert a function into a function pointer if needed (so you don’t need to use the address-of operator (&) to get the function’s address). However, it will not implicitly convert function pointers to void pointers, or vice-versa.

One interesting note: Default parameters won't work for functions called through function pointers. Default parameters are resolved at compile-time (that is, if you don’t supply an argument for a defaulted parameter, the compiler substitutes one in for you when the code is compiled). However, function pointers are resolved at run-time. Consequently, default parameters can not be resolved when making a function call with a function pointer. You’ll explicitly have to pass in values for any defaulted parameters in this case.

Providing default functions

If you’re going to allow the caller to pass in a function as a parameter, it can often be useful to provide some standard functions for the caller to use for their convenience. For example, in the selection sort example above, providing the ascending() and descending() function along with the selectionSort() function would make the callers life easier, as they wouldn’t have to rewrite ascending() or descending() every time they want to use them.

You can even set one of these as a default parameter:

1
2
// Default the sort to ascending sort
void selectionSort(int *array, int size, bool (*comparisonFcn)(int, int) = ascending);

Making function pointers prettier with typedef or type aliases

1
2
typedef bool (*validateFcn)(int, int);
bool validate(int x, int y, validateFcn pfcn) // clean

In C++11, you can instead use type aliases to create aliases for function pointers types:

1
2
using validateFcn = bool(*)(int, int); // type alias
bool validate(int x, int y, validateFcn pfcn) // clean

This reads more naturally than the equivalent typedef, since the name of the alias and the alias definition are placed on opposite sides of the equals sign.

Using std::function in C++11

Introduced in C++11, an alternate method of defining and storing function pointers is to use std::function, which is part of the standard library header. To define a function pointer using this method, declare a std::function object like so:

1
2
3
4
5
6
#include <functional>
bool test(int, int) {
// foo
}
bool validate(int x, int y, std::function<bool(int, int)> fcn); // std::function method that returns a bool and takes two int parameters
<bool(int, int)> myFunc = test;

The stack and the heap

Stack overflow

The stack has a limited size, and consequently can only hold a limited amount of information. On Windows, the default stack size is 1MB. On some unix machines, it can be as large as 8MB. If the program tries to put too much information on the stack, stack overflow will result. Stack overflow happens when all the memory in the stack has been allocated — in that case, further allocations begin overflowing into other sections of memory.

Here is an example program that will likely cause a stack overflow. You can run it on your system and watch it crash:

1
int stack[100000000];

Another example:

1
2
3
4
5
6
7
8
void foo() {
foo();
}

int main() {
foo();
return 0;
}

std::vector capacity and stack behavior

Although this is the most useful and commonly used part of std::vector, std::vector has some additional attributes and capabilities that make it useful in some other capacities as well.

Length vs capacity

1
int *array = new int[10] { 1, 2, 3, 4, 5 };

We would say that this array has a length of 10, even though we’re only using 5 of the elements that we allocated.

However, what if we only wanted to iterate over the elements we’ve initialized, reserving the unused ones for future expansion? In that case, we’d need to separately track how many elements were “used” from how many elements were allocated. Unlike a built-in array or a std::array, which only remembers its length, std::vector contains two separate attributes: length and capacity. In the context of a std::vector, length is how many elements are being used in the array, whereas capacity is how many elements were allocated in memory.

Taking a look at an example from the previous lesson on std::vector:

1
2
3
4
5
std::vector<int> array { 0, 1, 2 };
array.resize(5); // set length to 5
std::cout << "The length is: " << array.size() << '\n';
std::cout << "The length is: " << array.size() << '\n'; // 5
std::cout << "The capacity is: " << array.capacity() << '\n'; // 5

In this case, the resize() function caused the std::vector to change both its length and capacity. Note that the capacity is guaranteed to be at least as large as the array length (but could be larger), otherwise accessing the elements at the end of the array would be outside of the allocated memory!

Why differentiate between length and capacity? std::vector will reallocate its memory if needed, but like Melville’s Bartleby, it would prefer not to, because resizing an array is computationally expensive. Consider the following:

1
2
3
4
5
6
7
8
9
10
std::vector<int> array;
array = { 0, 1, 2, 3, 4 }; // okay, array length = 5
std::cout << "length: " << array.size() << " capacity: " << array.capacity() << '\n';

array = { 9, 8, 7 }; // okay, array length is now 3!
std::cout << "length: " << array.size() << " capacity: " << array.capacity() << '\n';

// output
// length: 5 capacity: 5
// length: 3 capacity: 5

Array subscripts and at() are based on length, not capacity

Vectors may allocate extra capacity

When a vector is resized, the vector may allocate more capacity than is needed. This is done to provide some “breathing room” for additional elements, to minimize the number of resize operations needed.

Handling errors, cerr and exit

Problem: When a function is called, the caller may have passed the function parameters that are semantically meaningless.

1
2
3
void printString(const char *cstring) {
std::cout << cstring;
}

Can you identify the assumption that may be violated? The answer is that the caller might pass in a null pointer instead of a valid C-style string. If that happens, the program will crash. Here’s the function again with code that checks to make sure the function parameter is non-null:

1
2
3
4
5
void printString(const char *cstring) {
// Only print if cstring is non-null
if (cstring)
std::cout << cstring;
}

cerr is a mechanism that is meant specifically for printing error messages. cerr is an output stream (just like cout) that is defined in . Typically, cerr writes the error messages on the screen (just like cout), but it can also be individually redirected to a file.

Assert and static_assert

An assert statement is a preprocessor macro that evaluates a conditional expression at runtime.

1
2
3
4
5
6
7
8
#include <cassert> // for assert()

int getArrayValue(const std::array<int, 10> &array, int index) {
// we're asserting that index is between 0 and 9
assert(index >= 0 && index <= 9); // this is line 6 in Test.cpp

return array[index];
}

Making your assert statements more descriptive

Fortunately, there’s a little trick you can use to make your assert statements more descriptive. Simply add a C-style string description joined with a logical AND:

1
assert(found && "Car could not be found in database");

Here’s why this works: A C-style string always evaluates to boolean true. So if found is false, false && true = false. If found is true, true && true = true. Thus, logical AND-ing a string doesn’t impact the evaluation of the assert.

NDEBUG and other considerations

The assert() function comes with a small performance cost that is incurred each time the assert condition is checked. Furthermore, asserts should (ideally) never be encountered in production code (because your code should already be thoroughly tested). Consequently, many developers prefer that asserts are only active in debug builds. C++ comes with a way to turn off asserts in production code:

1
2
#define NDEBUG 
// all assert() calls will now be ignored to the end of the file

Static_assert

C++11 adds another type of assert called static_assert. Unlike assert, which operates at runtime, static_assert is designed to operate at compile time, causing the compiler to error if the condition is not true. If the condition is false, the diagnostic message is printed.

Here’s an example of using static_assert to ensure types have a certain size:

1
2
static_assert(sizeof(long) == 8, "long must be 8 bytes");
static_assert(sizeof(int) == 4, "int must be 4 bytes");

A few notes. Because static_assert is evaluated by the compiler, the conditional part of a static_assert must be able to be evaluated at compile time. Because static_assert is not evaluated at runtime, static_assert statements can also be placed anywhere in the code file (even in global space).

In C++11, a diagnostic message must be supplied as the second parameter. In C++17, providing a diagnostic message is optional.

Ellipsis (and why to avoid them)

The best way to learn about ellipsis is by example. So let’s write a simple program that uses ellipsis. Let’s say we want to write a function that calculates the average of a bunch of integers. We’d do it like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <cstdarg> // needed to use ellipsis

// The ellipsis must be the last parameter
// count is how many additional arguments we're passing
double findAverage(int count, ...) {
double sum = 0;

// We access the ellipsis through a va_list, so let's declare one
va_list list;

// We initialize the va_list using va_start. The first parameter is
// the list to initialize. The second parameter is the last non-ellipsis
// parameter.
va_start(list, count);

// Loop through all the ellipsis arguments
for (int arg=0; arg < count; ++arg)
// We use va_arg to get parameters out of our ellipsis
// The first parameter is the va_list we're using
// The second parameter is the type of the parameter
sum += va_arg(list, int);

// Cleanup the va_list when we're done.
va_end(list);

return sum / count;
}
0%