Learning Notes

cpp

C++

Contents

  1. C++ basics: objects, variables, initialization, input, and output
  2. Functions, namespaces, preprocessing, and multiple files
  3. Fundamental data types, literals, constants, and strings
  4. Operators and bit manipulation
  5. Scope, duration, linkage, and storage classes
  6. Control flow
  7. Type conversion
  8. Function overloading and templates
  9. References, pointers, passing arguments, dynamic memory, and arrays
  10. Structs, classes, constructors, access levels, and friends

1. C++ basics

Structure of a C++ program

The main function is the entry point of a C++ program. A program starts running from main().

#include <iostream>

int main()
{
    std::cout << "Hi\n";
    return 0;
}

Objects and variables

In C++ we can access memory via objects. An object represents a region of storage, such as memory or a CPU register.

We can initialize a variable in three ways:

int a = 5;   // copy initialization
int b(5);    // direct initialization
int c{ 5 };  // list initialization

List initialization is generally better because it does not allow narrowing conversions, so it can catch data loss and some mistakes at compile time. We can also do multiple initializations in one statement when needed.

For an unused variable, we can use [[maybe_unused]] before the declaration. This is useful when we want to avoid an unused-variable compiler warning.

[[maybe_unused]] int unusedValue{};

An uninitialized local variable can contain an indeterminate value. We should initialize variables when we declare them.

Input and output: iostream, cout, cin, and endl

The <iostream> header is needed for input and output.

  • std::cout means character output.
  • std::cin means character input.
  • << is the insertion operator when used with std::cout.
  • >> is the extraction operator when used with std::cin.
#include <iostream>

int main()
{
    int x{};

    std::cout << "Hi, this is value of x: " << x << '\n';
    std::cin >> x;
    std::cout << "Now x is: " << x << '\n';

    return 0;
}

std::cout is buffered. This means output is often stored in a buffer first and then sent to the console later.

std::endl writes a newline and flushes the output buffer, so it is slower than '\n'. We normally use '\n' and use std::endl only when we actually need a flush.

std::cin is also buffered, and we can use multiple extractions:

int x{};
int y{};

std::cin >> x >> y;

If we enter 5 7, the first extraction reads 5 and the second reads 7. A space, tab, or newline can separate them.

cin >> behavior — quick notes

  • std::cin >> reads one whitespace-delimited token and skips leading whitespace such as spaces, tabs, and newlines.
  • It does not wait for a fresh Enter when data is already in the input buffer.
  • So 1 2 typed on one line, or 1 and 2 typed on separate lines, work the same way for two std::cin >> int reads.
  • A leftover newline is mainly a problem when std::getline follows std::cin >>. Before getline, we can write:
#include <limits>

std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  • A failed extraction, for example entering abc for an int, sets the fail state and leaves the bad characters in the input buffer. Future reads fail until we fix it:
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  • Check std::cin.fail() after input that can be invalid.
  • One line to remember: >> stops at whitespace, consumes only what it reads, and never auto-clears errors.

Literals, parameters, and arguments

Literals are fixed values inserted directly into the source code. For example, 5, 1.2, and "Hello world!" are literals.

A parameter is a variable used in a function definition. An argument is the value passed by the caller.

int add(int a, int b) // a and b are parameters
{
    return a + b;
}

int result{ add(2, 3) }; // 2 and 3 are arguments

Forward declarations and naming conflicts

We can write a function declaration at the beginning of the program, then define the function after main().

int add(int a, int b); // forward declaration

int main()
{
    return add(2, 3);
}

int add(int a, int b)
{
    return a + b;
}

If two identifiers have the same name in the same scope, it produces a naming conflict. We can avoid such collisions by using local variables, different scopes, or namespaces. A common naming convention for a global variable is to start its name with g_, such as g_count.


2. Functions, namespaces, preprocessing, and multiple files

Namespaces and the scope resolution operator

A namespace is like creating a scope so that we can avoid name collisions. We use the scope resolution operator :: to access names inside a namespace.

namespace Rishi
{
    int age{ 5 };

    void bedtime()
    {
    }
}

int main()
{
    int age{ 10 };
    return Rishi::age;
}

We can define a namespace in global scope or inside another namespace. A namespace cannot be defined inside a function.

The preprocessor

The preprocessor runs before compilation and produces a translation unit, which is then compiled by the compiler. Linking happens after compilation: the linker combines object files and libraries into the final program. Together, preprocessing, compilation, and linking are normally called the build process.

Some important work of the preprocessor is:

  1. Remove comments.
  2. Resolve #include.
  3. Expand macros made with #define.

Things that start with # are usually called preprocessor directives. They are handled during preprocessing, and the preprocessor does not understand normal C++ syntax.

#include <iostream>

#define JOE

int main()
{
#ifdef JOE
    std::cout << 1 << '\n';
#endif
    return 0;
}

In the case of a macro, the identifier is replaced by its replacement text.

Header files and header guards

When making multiple files, for example add.cpp and main.cpp, we can put a function declaration in a header file. Then every file that needs the declaration can include the header.

The header file consists of a function declaration and a header guard.

// add.h
#ifndef ADD_H
#define ADD_H

int add(int a, int b);

#endif

Header guards stop the same header from being included more than once in one translation unit.


3. Fundamental data types, literals, constants, and strings

Fundamental data types

There are many fundamental data types. My notes divide them into these categories:

  1. void
  2. Null pointer type: std::nullptr_t
  3. Floating-point types
  4. Integer types
  5. bool
  6. Character types

Integer types

Integer types store whole numbers.

  • short int
  • int
  • long int
  • long long int

Any data type can hold 2n values, where n is the number of bits.

We can create an unsigned data type by writing unsigned.

unsigned int count{};

If we assign a negative value to an unsigned value, it wraps around modulo 2n. For example, converting -1 to an unsigned integer gives the highest value that type can store. This is unsigned wraparound, so we should avoid unsigned integers for normal arithmetic unless we really need their behaviour.

For signed integers, overflowing the valid range is undefined behavior.

Fixed-width integers and size_t

C++ only promises minimum ranges for types such as int, and the exact size can vary by system. Fixed-width types are useful when we need an exact size and want code that is less platform-dependent.

We use them through <cstdint>:

#include <cstdint>

std::int8_t smallNumber{};
std::uint8_t smallUnsignedNumber{};
std::int16_t mediumNumber{};
std::uint16_t mediumUnsignedNumber{};
std::int32_t number{};
std::uint32_t unsignedNumber{};
std::int64_t bigNumber{};
std::uint64_t bigUnsignedNumber{};

std::int8_t and std::uint8_t may behave like character types when printed. std::size_t is the unsigned type generally used for sizes and indexes.

Floating-point types

The floating-point types are:

  • float
  • double
  • long double

For scientific notation, we can write something like:

double x{ 8.0e10 };

By default, std::cout prints floating-point values with a precision of 6 digits. We can change that with std::setprecision(), which needs <iomanip>.

#include <iomanip>
#include <iostream>

std::cout << std::setprecision(10) << 1.23456789 << '\n';

When a floating-point literal has no suffix, it is double. A suffix f makes it float.

double d{ 200.5 };
float f{ 200.5f };

Floating-point values have a sign, exponent, and significand (also called mantissa). A finite number cannot store every possible real number, so rounding error happens. Floating-point also has special values such as infinity, NaN, and positive and negative zero.

Boolean values

We can do:

bool a{ true };
bool b{ false };
bool c{ !true };
bool d{ !false };
bool e{ 1 };
bool f{ 0 };
// bool g{ 2 }; // not allowed: narrowing conversion in list initialization

bool g = 2; // allowed with copy initialization and evaluates to true

When printing bool values:

std::cout << a << std::endl; // prints 1 if a is true and 0 if false

If we want to print true or false, we use:

std::cout << std::boolalpha; // manipulates cout; it does not output text itself
std::cout << a << '\n';
std::cout << std::noboolalpha; // reset back as before

Input of boolean values

We can use normal boolean input:

bool b{};
std::cin >> b;
// 0 becomes false and non-zero numeric input becomes true

If we want to take true and false as input, then we apply boolalpha to std::cin.

std::cin >> std::boolalpha;
std::cin >> b; // enter lowercase true or false; another word makes extraction fail
std::cin >> std::noboolalpha;

Characters and the null pointer type

Character types include:

  • char
  • wchar_t
  • char8_t
  • char16_t
  • char32_t

These are integral types, so they are stored as numbers in memory.

nullptr is the modern null pointer literal. Its type is std::nullptr_t.

Literals, suffixes, and numeral systems

We have suffixes we can add to literals:

#include <iostream>

int main()
{
    std::cout << 5 << '\n';  // 5 without a suffix is int by default
    std::cout << 5L << '\n'; // long
    std::cout << 5u << '\n'; // unsigned int

    return 0;
}

Numeral systems:

int hexVar{ 0x1A };    // hexadecimal: 1A is 26 in decimal
int octVar{ 055 };     // octal: 055 is 45 in decimal
int binVar{ 0b11010 }; // binary: 11010 is 26 in decimal, available since C++14

We can use ' to separate digits:

int mask{ 0b1011'0010 };

By default the data printed by std::cout is decimal, but we can use output manipulators:

#include <iostream>

int main()
{
    int x{ 12 };
    std::cout << x << '\n';          // decimal by default
    std::cout << std::hex << x << '\n'; // hexadecimal
    std::cout << x << '\n';          // still hexadecimal
    std::cout << std::oct << x << '\n'; // octal
    std::cout << std::dec << x << '\n'; // return to decimal
    std::cout << x << '\n';          // decimal

    return 0;
}

To output binary, there is a standard library class called std::bitset:

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<8> bits{ 0b0010'0010 };
    std::cout << bits << '\n';

    return 0;
}

Constants, the as-if rule, and compile-time programming

The as-if rule says the compiler can modify code in any way to optimize it as long as there is no observable change in the program's behavior.

It can do this through:

  1. Constant folding.
  2. Constant propagation.
  3. Dead code elimination.

Compile-time programming is a technique that shifts some execution of logic to build time, which can make the program faster at runtime.

Many compile-time features use constant expressions. A constant expression is an expression that can be evaluated at compile time. Expressions that must wait for runtime input are runtime expressions.

#include <iostream>

int getNumber()
{
    std::cout << "Enter a number: ";
    int y{};
    std::cin >> y; // can only execute at runtime

    return y;
}

int five()
{
    return 5; // the function itself is not constexpr, so a call to five() is a runtime expression
}

int main()
{
    5;                  // constant expression
    1.2;                // constant expression
    "Hello world!";     // constant expression
    5 + 6;              // constant expression
    1.2 * 3.4;          // constant expression
    8 - 5.6;            // constant expression
    sizeof(int) + 1;    // constant expression

    getNumber();        // runtime expression
    five();             // runtime expression
    std::cout << 5;     // runtime expression

    return 0;
}

const means that a variable cannot be modified after initialization. constexpr means the value is usable in a constant expression and requires compile-time evaluation when the context needs it. We use constexpr when we want the compiler to evaluate a value at compile time.

String

C-style strings are difficult to work with. Modern C++ gives us std::string and std::string_view. They are class types, not fundamental types.

We use std::string from the <string> header:

#include <iostream>
#include <string>

int main()
{
    std::string name{ "Rishi" };
    std::cout << name << '\n';

    name = "another text of any length";
    std::cout << name << '\n';

    return 0;
}

Behind the scene, std::string manages its own dynamic memory when needed. We can use it much like a normal variable.

Getting input can be a little problem because normal std::cin stops at whitespace. To get a whole sentence, we use std::getline.

#include <iostream>
#include <string>

int main()
{
    std::string text{};
    std::getline(std::cin >> std::ws, text);
    std::cout << text << '\n';

    return 0;
}

std::ws ignores leading whitespace before getline starts reading. It is an input manipulator, similar to how std::setprecision() is an output manipulator.

We can get the length of a string using stringVar.length() or stringVar.size(). It returns an unsigned size type and does not include the terminating null character used by c_str().

Normal double-quoted text is a string literal, which has C-style character-array behavior. We can make a std::string literal using the s suffix:

#include <string>

using namespace std::string_literals;

std::string name{ "Rishi"s };

Passing a std::string by value can copy its data, so for read-only function parameters we usually use std::string_view or const std::string&, depending on the situation.


4. Operators and bit manipulation

Side effects and the conditional operator

In C++, a side effect is any action that modifies the state of the execution environment or causes observable changes beyond computing and returning a value. Common examples include modifying a variable, writing to a file, taking user input, or printing to the console.

For example, in x = 5;, calculating 5 is the expression's value computation, while storing 5 in x is the side effect.

Understanding side effects is important when dealing with evaluation order. Avoid modifying and reading the same variable in one complicated expression.

a ? x : y is called the conditional operator, or arithmetic if. If a is true, the expression evaluates to x; otherwise, it evaluates to y.

Bit operations

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<8> bits{ 0b0000'0101 }; // 0000 0101
    bits.set(3);   // 0000 1101
    bits.flip(4);  // 0001 1101
    bits.reset(4); // 0000 1101

    std::cout << "All the bits: " << bits << '\n';
    std::cout << "Bit 3 has value: " << bits.test(3) << '\n';
    std::cout << "Bit 4 has value: " << bits.test(4) << '\n';

    return 0;
}

5. Scope, duration, linkage, and storage classes

Compound statements, scope, and shadowing

A compound statement is a block surrounded by braces:

{
    int x{};
}

Scope means where an identifier can be accessed in the source code.

There are three main types of scope in these notes:

  1. Global scope.
  2. Local scope.
  3. Block scope.

Variable shadowing means a higher-scope variable is hidden by another variable with the same name in a lower scope.

#include <iostream>

int g_foo{ 5 };

int main()
{
    std::cout << g_foo << '\n'; // prints 5

    int g_foo{ 10 };            // shadows the global variable
    std::cout << g_foo << '\n'; // prints 10

    return 0;
}

This happens when we reinitialize or redeclare a variable in a smaller scope.

Duration and linkage

Linkage tells us whether declarations of an identifier refer to the same entity across scopes or files.

  • Local variables have no linkage.
  • Global variables and functions can have internal or external linkage.
  • A global variable with internal linkage is also called an internal variable.

Storage duration means when the variable is created and destroyed:

  1. Automatic duration.
  2. Static duration.
  3. Dynamic duration, which comes from dynamic memory allocation.

Storage class specifiers are keywords used in variable and function declarations. They can affect storage duration and linkage.

Global variables

// Non-constant global variables
int g_x;                 // zero-initialized by default
int g_value{};           // explicitly value-initialized
int g_count{ 1 };        // explicitly initialized

// Const global variables
// const int g_y;        // error: const variables must be initialized
const int g_y{ 2 };

// Constexpr global variables
// constexpr int g_z;    // error: constexpr variables must be initialized
constexpr int g_z{ 3 };

By default, non-constant global variables have external linkage. To make one internal, we can use static:

static int g_foo{ 5 };

Global const and constexpr variables have internal linkage by default. Functions have external linkage by default.

// add.cpp
[[maybe_unused]] static int add(int x, int y)
{
    return x + y;
}
// main.cpp
#include <iostream>

int add(int x, int y); // a forward declaration does not make the static function accessible

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

This program will not link because add has internal linkage in add.cpp.

We can use extern for an external variable declaration, but we must be careful: an initialized non-const global with extern is a definition, not just a declaration.

Static local variables

When we create a local variable, it has automatic duration by default. If we use static with a local variable, its duration becomes static. It is created once, keeps its value after leaving its block, and is destroyed at program end.

#include <iostream>

void incrementAndPrint()
{
    static int s_value{ 1 }; // initializer runs only once
    ++s_value;
    std::cout << s_value << '\n';
} // s_value becomes inaccessible here but is not destroyed here

int main()
{
    incrementAndPrint();
    incrementAndPrint();
    incrementAndPrint();

    return 0;
}

Inline functions and variables

Calling a function normally has some call overhead. An inline function gives the compiler permission to replace a call with the function body when it is useful. It does not force the compiler to do so.

inline is also useful for allowing identical function or variable definitions in more than one translation unit.

Although we can make a constexpr variable externally linked, it must be initialized at its definition, so it cannot be separately forward declared as constexpr.

Old storage-class notes

The old types written in my copy are static, extern, register, and auto.

  • auto is now mainly used for type deduction in modern C++.
  • register is no longer useful in modern C++ and is deprecated.
  • static has several meanings depending on where it is used.
  • extern is used for external linkage declarations.

The picture below is a quick old-style storage-class reference. In modern C++, use the scope, duration, and linkage notes above as the main rule.

Storage classes reference


6. Control flow

Control statements

Control statements decide which statements execute. A basic switch looks like this:

switch (x)
{
case 1:
    return 1;
case 2:
    return 2;
default:
    return 0;
}

std::exit() stops the program immediately. We can give a function a name and call it when we need it.


7. Type conversion

Type conversion

There are implicit type conversions and explicit type conversions. When the compiler converts the type for us, it is implicit. When we ask for the conversion, it is explicit.

The static_cast operator is used for a normal explicit type cast:

static_cast<int>(5.5); // gives 5

Syntax:

static_cast<new_type>(expression)

The older C-style cast also exists:

int number{ (int)5.5 };

But in C++, we prefer static_cast<int>(5.5) because it clearly shows the intended conversion.

8. Function overloading and templates

Function overloading

Sometimes we might want to create two functions with the same name. For that, we need function overloading.

We can differentiate overloaded functions through the number of parameters and the types of parameters. Return type is not used to differentiate functions during overload resolution.

int add(int x, int y)
{
    return x + y;
}

int add(int x, int y, int z)
{
    return x + y + z;
}

The function signature is the parts of the function header used to differentiate functions. In C++, this includes the function name, number of parameters, parameter types, and function-level qualifiers. It does not include the return type.

The process of matching a function call to one overloaded function is called overload resolution. A simplified order is:

  1. Exact match.
  2. Numeric promotion.
  3. Numeric conversion.
  4. User-defined conversion.
  5. Give up with an error.

Resolving ambiguous matches

Explicitly cast ambiguous argument(s) to match the type of the function we want to call.

Sometimes we want to delete overloads:

#include <iostream>

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

void printInt(char) = delete; // calls halt compilation
void printInt(bool) = delete; // calls halt compilation

int main()
{
    printInt(5);      // okay
    // printInt('a'); // error
    // printInt(true); // error

    return 0;
}

If there are many non-matching types, we can use a deleted template:

template <typename T>
void printInt(T) = delete;

We can also define default parameters in a function. The important rule is that after one parameter has a default argument, every parameter to its right must also have a default argument.

Function templates

When doing function overloading, there are times when we need almost identical functions that only differ by type. In that case, we create a template. The compiler generates overloads from it, so we maintain only one template.

The initial function template is the primary template. Functions generated from it are instantiated functions.

template <typename T>
T add(T x, T y)
{
    return x + y;
}

When creating a primary function template, we use typename and a type called a template type parameter or template type.

To create a template function, add the template keyword:

template <typename T>
T add(T x, T y)
{
    return x + y;
}

The compiler can deduce template arguments from a call:

#include <iostream>

int main()
{
    std::cout << add(1, 2) << '\n'; // T is deduced as int
}

We can also specify the type:

std::cout << add<int>(1, 2) << '\n';

Ways to call a function template

For a simple template such as max:

template <typename T> // T is a placeholder
T max(T x, T y)
{
    return (x < y) ? y : x;
}

After using max(1, 2), the compiler can generate an int version:

// Conceptually, the generated function is like this:
int max(int x, int y)
{
    return (x < y) ? y : x;
}

This generated function is an instantiation. Implicit instantiation means it is instantiated when a function call needs it.

We can have multiple template types:

template <typename R, typename T1, typename T2>
R convert(T1 a, T2 b)
{
    return static_cast<R>(a + b); // calculation using a and b
}

// Called explicitly like: convert<int>(5.5f, 2.1f);
template <typename T1, typename T2>
auto add(T1 a, T2 b)
{
    return a + b; // return type is automatically deduced
}

Template argument deduction means that most of the time we do not need to write a template argument. For example, add(arg1, arg2) can let the function template deduce the parameter types itself.

We can mix normal values and template variables if the template parameter can be deduced. We can also have multiple template variables.

Overloading function templates

#include <iostream>

template <typename T>
auto add(T x, T y)
{
    return x + y;
}

template <typename T, typename U>
auto add(T x, U y)
{
    return x + y;
}

template <typename T, typename U, typename V>
auto add(T x, U y, V z)
{
    return x + y + z;
}

int main()
{
    std::cout << add(1.2, 3.4) << '\n';
    std::cout << add(5.6, 7) << '\n';
    std::cout << add(8, 9, 10) << '\n';

    return 0;
}

9. References, pointers, passing arguments, dynamic memory, and arrays

Pointers

A pointer is declared using . A pointer stores an address. When we access the data stored at the address in a pointer using , we call it dereferencing.

The * token can also be multiplication, so its meaning depends on the context. A token can have a defined meaning depending on whether it is a keyword, identifier, constant, literal, operator, or special symbol.

int value{ 5 };
int* ptr{ &value };

*ptr = 10; // dereferencing: value becomes 10

Passing by pointer

Passing by pointer means we pass an address from the calling side. It can change the original variable.

void work(int* a)
{
    *a = 11; // dereferencing
}

int main()
{
    int value{ 8 };
    work(&value); // passing address
}

Passing into a function

There are three main ways to pass a value into a function:

  1. By value.
  2. By reference.
  3. By pointer.

Pass by value

Pass by value is the default in C++. The data is copied into the parameter. Changes inside the function do not go outside the function.

void work(int a)
{
    a = 10;
}

Pass by reference

With pass by reference, changes inside the function are reflected outside. No copy is made.

void work(int& a)
{
    a = 10;
}

For a read-only value that should not be copied, we often pass by const reference:

class Person;

void print(const Person& person);

Pass by pointer

With pass by pointer, the pointer is an input and the function dereferences it to change the original object.

void work(int* a)
{
    *a = 11;
}

int main()
{
    int value{ 8 };
    work(&value);
}

For a class object, pass by pointer, reference, and value each have different syntax, but they all let us pass the object to a function or method of another class.

Dynamic memory allocation (DMA)

Dynamic memory allocation is the process of allocating memory for a variable during runtime instead of normal automatic allocation. DMA is usually done using the new and delete operators.

Normal local variables are generally allocated with automatic storage duration and are destroyed when they go out of scope. Dynamically allocated objects are allocated on the free store, often called the heap.

int* ptr{ new int };    // allocates an int and returns its address
int* ptr1{ new int{ 2 } };
int* ptr2{ new int(2) };

After we are done using the memory, we can delete it:

delete ptr;
ptr = nullptr; // set pointer to null after delete

A pointer that points to deallocated memory is a dangling pointer.

A memory leak happens when the program loses the memory address and there is no way to free the dynamically allocated memory. The memory can remain allocated until the program closes.

If there is no available space, new normally throws std::bad_alloc.

In modern C++, raw new and delete are usually avoided when a container or smart pointer can manage the memory safely. The raw examples are here because they are part of my notes.

Dynamically allocating arrays

We can dynamically allocate arrays like a normal scalar, but use new[] and delete[].

int* arr{ new int[5] };
delete[] arr;

int* numbers{ new int[5]{ 1, 2, 3, 4, 5 } };
delete[] numbers;

The array variable is a pointer to the first element, so normal indexing works:

arr[0] = 5;
arr[4] = 3;

There is no built-in way to resize a raw dynamic array after it is initialized. This is one reason we use std::vector instead when we need resizing.


10. Structs, classes, constructors, access levels, and friends

Structs, classes, and objects

A class is a user-defined type.

Student mike; // Student is the data type, mike is an instance of Student

An object is an instance of a type.

Classes can do information hiding with:

  • public
  • private
  • protected

They also give us the ability to create instances of a class.

Access specifiers

Each element of a class has an access level that determines who can access the member. C++ has three levels:

  • public
  • private
  • protected

Members of a struct are public by default. Members of a class are private by default.

Access functions are manually implemented methods that let us access or manipulate private data in a controlled way.

Access levels

Classes are not always aggregates. An aggregate is a type that allows direct member initialization using brace-list initialization.

For a class to be an aggregate, the important rules from my notes are:

  1. All data members are public.
  2. No user-declared constructors.
  3. No virtual base classes or virtual functions.
  4. No private or protected base classes.

Constructors and destructors

Constructors and destructors are special functions in a class.

A constructor is automatically called when an object is made. A destructor is automatically called when an object is destroyed, either by delete or when the object goes out of scope.

A constructor:

  • has the same name as the class;
  • has no return type;
  • is automatically called;
  • can be overloaded;
  • is used to initialize an object.

Types of constructor:

  1. Default constructor.
  2. Parameterized constructor.
  3. Copy constructor.

A default constructor takes no arguments and is usually used to initialize data members with default values.

class Person
{
    int height{};

public:
    Person() // default constructor
    {
    }
};

A parameterized constructor accepts arguments from outside and uses them to initialize data members. It needs to be public when objects should be created from outside the class.

A copy constructor is a special constructor that creates a new object by copying the data members of an existing object of the same class.

Member functions and member variables

Functions included in a class are called member functions. Functions that are not members of a class are called free functions.

Member functions can be invoked and created in any order, but data members are initialized in declaration order.

Member functions can also be overloaded, but the overloads should be in that class.

#include <iostream>

struct Date
{
    int year{};
    int month{};
    int day{};

    void print()
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

int main()
{
    Date today{ 2020, 10, 14 };

    today.day = 16; // member variables use the member selection operator .
    today.print();  // member functions also use .

    return 0;
}

Memory for a class is allocated when an object is created, not when the class itself is declared. Each normal data member has separate memory in each object.

Static data members are shared across all objects of a class. Normal data members get their own copy per object, but a static data member has one shared copy.

Const class objects and const member functions

We can create a const object of a class:

const Person person{};

If an object is const, we cannot modify its data members or call a non-const member function on it.

#include <iostream>

struct Date
{
    int year{};
    int month{};
    int day{};

    void print() const
    {
        std::cout << year << '/' << month << '/' << day;
    }
};

void doSomething(const Date& date)
{
    date.print(); // works because print is const
}

We can have a const and non-const overload:

#include <iostream>

struct Something
{
    void print()
    {
        std::cout << "non-const\n";
    }

    void print() const
    {
        std::cout << "const\n";
    }
};

Defining member functions outside the class

Member functions can be declared inside a class and defined outside it. The declaration needs to be inside the class, and the definition uses the scope resolution operator.

class Person
{
public:
    void will();
};

void Person::will()
{
}

The scope resolution operator should not be used when calling the member function through an object. We call it with the object:

Person person{};
person.will();

Friend functions and friend classes

Friend functions can access public, private, and protected data members of a class.

Friend functions are declared inside the class using the friend keyword. They can be member functions or non-member functions, and they are defined with the normal rules for the function.

#include <iostream>

class Accumulator
{
private:
    int m_data{ 5 };

    friend void print(const Accumulator& accumulator);
};

void print(const Accumulator& accumulator)
{
    std::cout << accumulator.m_data << '\n';
}

In this case, print is not a member function, but it can access the private data of Accumulator.

Friend classes and friend functions are not inherited.

A friend class can also be forward declared, declared as a friend inside another class, and then defined later.

A member function as a friend

#include <iostream>

class Storage;

class Display
{
private:
    bool m_displayIntFirst{};

public:
    Display(bool displayIntFirst)
        : m_displayIntFirst{ displayIntFirst }
    {
    }

    void displayStorage(const Storage& storage);
};

class Storage
{
private:
    int m_nValue{};
    double m_dValue{};

public:
    Storage(int nValue, double dValue)
        : m_nValue{ nValue }, m_dValue{ dValue }
    {
    }

    friend void Display::displayStorage(const Storage& storage);
};

void Display::displayStorage(const Storage& storage)
{
    if (m_displayIntFirst)
        std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
    else
        std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}

int main()
{
    Storage storage{ 5, 6.7 };
    Display display{ false };
    display.displayStorage(storage);

    return 0;
}