Initialization

It is recommended to initialize value upon creation with a values. We can choose to add specific value, or default zero initialization. Multiple values can be initialized at the same time

// default initialization without value
int width
int x, y;

Copy-Initialization

Initialization inherited from C language. It copies the value to the variable. Not commonly used because it is less efficient form of initialization. It is best practice to initialize a single variable on a line.

int width = 5; // copy-initialization

Direct-initialization

Introduced for more efficient initialization of complex objects. It was superseded by direct-list-initialization.

int d { 7 };   // direct-list-initialization (initial value in braces)

Direct List Initialization

Modern way to initialize objects in C++. One of main benefits of this initialization is due to narrowing conversions.

If you try to list-initialize a variable using a value that the variable can not safely hold, compiler will raise an error. Narrowing convention only applies to the list initialization, any subsequent assignment is allowed.

int width { 5 };    // direct-list-initialization of initial value 5 into variable width (preferred)

int height = { 6 }; // copy-list-initialization of initial value 6 into variable height (rarely used)

int w1 { 4.5 }; // compile error: list-init does not allow narrowin conversion - fraction can't be saved in int

w1 = 4.5;  // assignemnt is allowed

int width {}; // value-initialization / zero-initialization to value 0

int x ( 5 ), y (6)

IOSTREAM

iostream is used by importing it as a library

#include <iostream>

std::count

Character output prints characters to console as standard output. Characters are not directly written to standard output, but they are buffered and flushed periodically.

Newline

There are 2 common ways to move cursor to next line.

  • std::endl - adds a new line character and flushes the buffer. Useful when we want to explicitly flush the buffer.
  • "\n" - adds a new line character without flushing the buffer. More efficient than std::endl. It is preferred to use " and helps avoid inadvertent multicharacter literals.

std::cin

std::cin (which stands for “character input”) reads input from keyboard. Typically used to input data from console to variable

    std::cout << "Hello " << "World" << std::endl; // print Hello world! to console
    std::cout << "Enter your age:"
    
    int x {};
    std::cin >> x;

Functions

Simple example of using functions.

#include <iostream>

int getUserInput() {
    std::cout << "Enter an integer: ";
    int input;
    std::cin >> input;

    return input;
}


int main() {
    std::cout << "Wellcome to first program" << "\n";

    const int x { getUserInput()};
    const int y { getUserInput() };

    std::cout << x << " * " << y << " = " << x * y << "\n";

    return EXIT_SUCCESS;
}

Value Parameters

Value parameters are passed to a function by value. They are copied into the matching parameter.

#include <iostream>

void printValues(int x, int y)
{
    std::cout << x << '\n';
    std::cout << y << '\n';
}

Preprocessor

Preprocessor accepts code files and produces a translation unit. Translation unit is prepared code that is the input to the compiler. Some tasks done by the preprocessor are:

  • Strips out comments
  • Ensures each file ends with a new line
  • Processes #include

Preprocessor directive

Also called a directive, it is an instruction that start # and ends with a new line character (/n) instead of semicolon ;. These directives tell the preprocessor to perform certain text manipulation tasks.

#include

The #include directive is replaced by the preprocessor with the content of included file.

#define

#define creates a macro that defines how input text is converted into replacement text.

#include <iostream>

#define COLOR "green"

int main()
{
    std::cout << "My name is: " << MY_NAME << '\n';

    return 0;
}

Header Files

Typically header files are used to propagate one or more several declaration into a code file. They should follow name of the cpp implementation. Following ODR (One Definition Rule), no implementation should be defined in the header file.

// mathutil.h
int multiply(int x, int y);

// mathutil.cpp
#include "mathutil.h"

int multiply(int x, int y) {
    return x * y;
}

//main.cpp
#include <iostream>
#include "mathutil.h"

int main() {
    int x { 2 }
    int y { 4 }
    std::cout << x << " * " << y << " = " << multiply(x, y) << "\n";

    return 0;
}

Header files can contain another header files.

// file: foo.h 
#include <string_view> // required to use std::string_view

std::string_view getApplicationName(); // std::string_view used here

Header File Best Practices

  • Header guards should be always included
  • No variables are defined
  • Has the same name as its associated c++ file
  • Contains only declaration of function contained in its associated cpp file
  • Includes all header files for functionality it needs
  • Includes only what it requires
  • No .cpp files should be included
  • Includes documentation on what something does and how to use the header. Describe how the code works in source files.

Constants

Constant is a named variable that can not be changed. There is no specific naming convention.

Best Practice

  • Variable should be constant wherever possible
  • Values for parameters should not be constants
const int size { 10 }

Literals

Meaning of literal values can not be redefined. For example, value 3 can never be different.

Literal values have their type. It is typically deduced from the value. Sometimes we need to change the default type assigned to the literal. This can be done with Suffixes.

    std::cout << 5u << '\n'; // 5u is type unsigned int

C Strings

C Strings are string literals placed between two double (")quotes. We use single quotes (') for char literals. Strings are not fundamental type in C++. They are inhereted from C language and are called C strings.

  • Each string terminates with a special character, null terminator \0.
  • Strings are constant types created at the beginning

Control Flow

if

if (condition) {
   true_statement;
}
else {
    false_statement;
}

Single line statements can avoid parenthesis.

if (age >= minDrinkingAge) purchaseBeer();
else std::cout << "No drinky for you\n".

switch

Switch expression must evaluate to integral type.

void printDigitName(int x)
{
    switch (x)
    {
    case 1:
        std::cout << "One";
        break;
    case 2:
        std::cout << "Two";
        break;
    case 3:
        std::cout << "Three";
        break;
    default:
        std::cout << "Unknown";
        break;
    }
}

while

While uses loop variable to identify terminating condition. It always should be a signed as unsigned integers can lead to unpredictable results.

    int count{ 1 };
    while (count <= 10)
    {
        std::cout << count << ' ';
        ++count;
    }

    std::cout << "done!\n";

Sometime we use infinite loops intentionally.

while (true)
{
  // this loop will execute forever
}

do while

 do
    statement; // can be a single statement or a compound statement
while (condition);

for

Statement for is prefered to while when there is obvious loop variable.

for (init-statement; condition; end-expression)
   statement;

It is possible to emit any or all for statements.

int i{ 0 };
for ( ; i < 10; ) // no init-statement or end-expression
{
    std::cout << i << ' ';
    ++i;
}

Compound Data Types

C++ supports the following compound types:

  • Functions
  • C-style Arrays
  • Pointer types:
    • Pointer to object
    • Pointer to function
  • Pointer to member types:
    • Pointer to data member
    • Pointer to member function
  • Reference types:
    • L-value references
    • R-value references Enumerated types:
    • Unscoped enumerations
    • Scoped enumerations Class types:
    • Structs
    • Classes
    • Unions

lvalue

Expression that evaluates to an identifiable object or function (or bit-field). Entities with identities can be accessed via an identifier, reference, or pointer, and typically have a lifetime longer than a single expression or statement. They can be modifiable or non-modifiable (aka constants)

rvalue

It is an expression that is not an lvalue. Rvalue expressions evaluate to a value. For example, we can break following statement int x = 10; into, x - lvalue and 10 - rvalue.

lvalue vs rvalue

Following are rules of thumb to distinguish lvalues from rvalues:

  • lvalue (locator value): Has a identifiable memory address. If you can take its address using the ampersand (&) operator, it’s an lvalue. It usually has a name and persists beyond a single expression.
  • Rvalue expressions are those that evaluate to values, including literals and temporary objects that do not persist beyond the end of the expression.
int return5()
{
    return 5;
}

int main()
{
    int x{ 5 }; // 5 is an rvalue expression
    const double d{ 1.2 }; // 1.2 is an rvalue expression

    int y { x }; // x is a modifiable lvalue expression
    const double e { d }; // d is a non-modifiable lvalue expression
    int z { return5() }; // return5() is an rvalue expression (since the result is returned by value)

    int w { x + 1 }; // x + 1 is an rvalue expression
    int q { static_cast<int>(d) }; // the result of static casting d to an int is an rvalue expression

    return 0;
}

Lvalue References

Also called as a references act as an reference for existing values such as variables. The type of a reference determine what type of object it can reference. Lvalue reference type is identified by a ampersand & in the type specifier for example int&.

We can create a variables holding lvalue references. We can use reference to modify object that is being referenced.

#include <iostream>

int main()
{
    int x { 5 }; // normal integer variable
    int& ref { x }; // ref is now an alias for variable x

    std::cout << x << ref << '\n'; // print 55

    x = 6; // x now has value 6

    std::cout << x << ref << '\n'; // prints 66

    ref = 7; // the object being referenced (x) now has value 7

    std::cout << x << ref << '\n'; // prints 77

    return 0;
}

Lifetime of a lvalue and its reference is independent. It can lead to a dangling reference when a object being reffered to is dead while its reference is still alive. This can lead to undefined behavior.

References are not objects and they don’t occupy memory. Wherever possible, they are replaced by compiler with the referenced value.

Passing an argument to a function can be done by:

  • copy a value - copying the value
  • pass a value - passing a reference to the method instead of the value

Pass by Reference

Primitive types are often as small as few bytes. For those it is more efficient to pass the value instead of passing their reference. This is not the case for more complex types such as strings. We want to limit unnecessary creating copies of classes wherever possible.

Passing by reference is defined by declaring a reference rather than a value type parameter. When a value is passed to the function it is bound to the reference instead to the value.

#include <iostream>

void printMe(int& xRef, int x) {
    std::cout << &xRef << " - " << &x << "\n";
}

void addOne(int& x) {
    ++x; // modifies object
}

int main() {
    int x { 5 };
    std::cout << &x << "\n";
    printMe(x, x);
    addOne(x);
    std::cout << x << "\n";
}
// Output:
// 0x16b66f56c
// 0x16b66f56c - 0x16b66f544
// 6

In above example you can see that the x in the main function has the same address as the xRef in printMe function. Because x in printMe function was copied it has different address.

Pass by reference enable us to modify the underlying value. Because the reference enables us to mutate the value, we can not pass references to constants. This linmits our use of references.

Pointers

We use & to get address to value - once we have an address, we can use a dereference operator * to access to access the value at given address.

#include <iostream>

int main() {
    int x { 5 };
    int* ref { &x };

    std::cout  << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";
    return 0;
}

A pointer is an object that holds a memory address as its value.

Similar to references, we use a pointer type by adding a start as a postfix to the value type, for example int* pointer holds address to int value.

A pointer is a variable, hence it is not initialized by default. Uninitialized pointer is called a wild pointer, containing a garbage address. It is a best practice to always initialize a pointer to a known value.

Pointer assignment can:

  • Change what it is pointing at
    • By assigning a new address to the pointer
  • Change the value being pointed at
    • By assigning a dereferenced pointer a new value
#include <iostream>

int main() {

    int x { 5 };
    int* ref { &x };

    std::cout  << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";

    *ref = 6;

    std::cout  << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";

    int y { 7 };
    ref = &y;

    std::cout  << "x=" << x << "\nmemory_address=" << ref << "\nref_val=" << *ref << "\n";
}

References and Pointers

Pointers behave similarly to referfences. They store address to a value and can be dereferenced to the value. They are not the same and have following differences:

  Pointer Reference
Holds memory address explicitly internally
Indirection is managed manually (*, &) by compiler
Can be null Yes No
can be reseated Yes No
Syntax overhead *ptr, ptr-> No
Is a class Yes No

It is preffered to use lvalue references instead of pointers. Here is a decision table to help with choice:

  • Is null a valid state? → Pointer
  • Can the target change? → Pointer
  • Interfacing with C? → Pointer
  • Otherwise → Reference

Pass by Reference and Address

When dealing with objects larger than few bytes, it is more efficient to pass them by reference, or address.

#include <iostream>
#include <string>

void printByValue(std::string val) // The function parameter is a copy of str
{
    std::cout << val << '\n'; // print the value via the copy
}

void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
    std::cout << ref << '\n'; // print the value via the reference
}

void printByAddress(const std::string* ptr) // The function parameter is a reference that binds to str
{
    std::cout << *ptr << '\n'; // print the value via the reference
}

int main()
{
    std::string str{ "Hello, world!" };

    printByValue(str); // pass str by value, makes a copy of str
    printByReference(str); // pass str by reference, does not make a copy of str
    printByAddress(&str);

    return 0;
}

References are idiomatic to C++, hence we should use pass by reference unless there is a specific reason to pass an address. Some of the benefits of pass by reference are:

  • The argument always exists - we don’t need to check nullability
  • The syntax is cleaner - the caller can pass variable normally

We use pass by addresss when:

  • We want to communicate that the argument can be null
  • We need to reseat the pointer - point the pointer to another object, e.g. in linked list

The modern C++ guideline (C++ Core Guidelines: F.16/F.60) sums it up well: use a reference when the parameter must exist; use a pointer when it’s optional.