C++
C++ is a multi-paradigm, general-purpose programming language that provides high-level abstractions while maintaining low-level control over system resources. It emphasizes performance, efficiency, and fine-grained memory management, making it ideal for systems programming, game development, and performance-critical applications.
| Paradigms | Typing | Memory Management | Execution |
|---|---|---|---|
| Procedural Object Oriented Functional | Strong Static | Manual | Compiled |
#include <iostream>
int main(int argc, char* argv[])
{
std::cout << "Hello, World!" << std::endl;
return 0;
}
Table of Contents
- 1 Backgrounds
- 2 Toolchain
- 3 Compilation Process
- 4 Syntax
- 5 Structure
- 6 Comments
- 7 Namespaces
- 8 Preprocessor Directives
- 9 Variables
- 10 Data Types
- 11 Literals
- 12 Operators
- 13 Memory Management
- 14 Control Flow Structures
- 15 Functions
- 16 Lambda Expressions
- 17 Classes
- 18 Operation Overloading
- 19 Standard Template Library (STL)
1 Backgrounds
1.1 Resources
- Comprehensive reference documentation: C++ Reference
- Official C++ standards committee website: ISO C++ Standard
- Beginner-friendly tutorial: cplusplus.com Tutorial
- In-depth learning resource with exercises: Learn C++
- Best practices by Bjarne Stroustrup and Herb Sutter: C++ Core Guidelines
1.2 Advantages and Disadvantages
| Advantages | Disadvantages |
|---|---|
| Exceptional runtime performance | Manual memory management required |
| Fine-grained control over hardware | Steep learning curve |
| Zero-cost abstractions | Complex syntax and semantics |
| Large ecosystem and community | Longer compilation times |
| Backward compatibility with C | Verbose error messages |
| Multi-paradigm support | Undefined behavior pitfalls |
| Wide platform support | No official and built-in toolchain |
1.3 History
- C++ was developed in 1979 by Bjarne Stroustrup at Bell Telephone Laboratories
- It was intended as an extension of C, introducing object-oriented programming features
- Originally called “C with Classes”
- The first formal standard (C++98) was published in 1998 by the International Organization for Standardization (ISO)
- C evolved separately with the C99 standard in 1999, introducing features that diverged from C++
- This created some incompatibilities between the two languages
- Major C++ standard updates published by ISO were:
- C++03 (2003): Bug fixes and minor improvements to C++98
- C++11 (2011): Major modernization with lambdas, smart pointers, move semantics, and more
- Considered to be the foundation of modern C++
- C++14 (2014): Refinements and library additions
- C++17 (2017): Structured bindings,
std::optional,std::variant, parallel algorithms - C++20 (2020): Concepts, modules, coroutines, ranges
- C++23 (2023):
std::expected,std::mdspan, improved lambda features - C++26 (expected 2026): Work in progress with reflection and other proposals
2 Toolchain
- C++ is only a language specification and therefore doesn’t come with an official toolchain
- To use C++, various tools must be installed that together form a development environment
2.1 Compilers
- Compilers implement C++ and are therefore required
- Major C++ compilers include:
- GCC/G++: GNU Compiler Collection, widely used on Linux and cross-platform
- Clang: LLVM-based compiler, known for fast compilation and helpful error messages
- MSVC: Microsoft Visual C++, native compiler for Windows
- Intel C++ Compiler: Optimized for Intel processors
2.2 Standard Library Implementations
- Different compilers come with different standard library implementations:
- libstdc++: GNU’s implementation, shipped with GCC/G++
- libc++: LLVM’s implementation, shipped with Clang
- MSVC STL: Microsoft’s implementation, shipped with MSVC
2.3 Build Systems
- Common C++ build systems include:
- Make: Traditional build tool using Makefiles
- CMake: Cross-platform meta-build system that generates native build files
- Ninja: Fast build system often used with CMake
- Meson: Modern build system focusing on speed and usability
2.4 Debuggers
- Common C++ debuggers include:
- GDB: GNU Debugger, standard debugger for Linux
- LLDB: LLVM debugger, works well with the Clang compiler
- Visual Studio Debugger: Integrated debugger for Windows
2.5 Package Managers
- Common C++ package managers include:
- vcpkg: Cross-platform package manager by Microsoft
- Conan: Decentralized package manager with binary package support
- CPM: CMake-based package manager
3 Compilation Process
graph TD
source_files[Source files] --> |passed to| preprocessor[Preprocessor];
header_files[Header files] --> |passed to| preprocessor;
preprocessor --> |If preprocessor error| stop[Stop];
preprocessor --> |produces| translation_units[Translation Units];
translation_units --> |passed to| compiler[Compiler];
compiler --> |If compiler error| stop;
compiler --> |compiles| object_files[Object files];
object_files --> |passed to| linker[Linker];
library_files[Library files] --> |passed to| linker;
linker --> |links into| executable_binary[Executable Binary];
linker --> |If linker error| stop;
- Preprocessor: Produces translation units from source and header files
- All preprocessor directives are executed
- Translation units are self-contained C++ source files that result from preprocessing each source file with its included headers
- Each source file becomes one translation unit after preprocessing
- This step is canceled if a preprocessor directive is incorrect
- Compiler: Produces binary object files from translation units
- Translation units are translated into machine code
- Each translation unit can be compiled independently (separate compilation)
- Object files contain machine code along with metadata such as symbol tables, relocation information, and debugging data
- Object files typically get the file extension
.o(Unix/Linux) or.obj(Windows) - This step is canceled if a translation unit contains a syntax error or semantic error
- Linker: Produces a single executable binary file from object and library files
- All object files and library files are linked together
- Resolves external symbol references (functions and variables declared in one file and defined in another)
- The executable gets no extension on Unix/Linux or
.exeon Windows - This step is canceled if references can’t be resolved (e.g., undefined references or multiple definitions)
4 Syntax
4.1 Whitespace
- Whitespace characters include spaces, tabs, newlines, and carriage returns
- Whitespaces are mostly ignored during compilation
- They serve as separators between tokens (identifiers, literals, keywords, and operators)
- Multiple consecutive whitespaces are treated as a single separator
- Exception: Whitespace within string literals and character literals is preserved
- Exception: Preprocessor directives must start with
#at the beginning of a line
- Comments are treated as whitespace by the compiler
- At least one whitespace is required to separate adjacent keywords or identifiers
int x=10; // valid: operators don't require whitespace
int x = 10; // valid: multiple whitespaces treated as one
intx = 10; // invalid: missing whitespace between keyword and identifier
"Hello World" // whitespace preserved in string literal
4.2 Statements
- Statements are instructions that perform actions
- Most statements must end with a semicolon
;as a terminator- Exceptions: Compound statements, control flow structures, function/class definitions
- Different types of statements:
- Expression statements: Evaluate an expression followed by
; - Declaration statements: Declare variables or functions
- Compound statements (blocks): Multiple statements enclosed in
{} - Control flow statements: Conditionals, loops, jumps
- Expression statements: Evaluate an expression followed by
- Block statements create their own scope
- Variables declared inside a block are only accessible within that block
- Blocks are treated as single statements in control structures
- Empty statements (just
;) are valid but typically indicate a logic error
int x; // declaration statement
x = 10; // expression statement
{ int y = 5; } // block statement (y is only accessible within the block)
if (x > 0) ; // empty statement (likely unintended)
4.3 Identifiers
- Identifiers are names used for variables, functions, classes, namespaces, etc.
- Rules for valid identifiers:
- Must start with a letter (
a-z,A-Z) or underscore (_) - May contain letters, digits (
0-9), and underscores - Cannot be C++ keywords (e.g.,
int,class,if,for) - Are case-sensitive:
myVar,MyVar, andMYVARare different identifiers - Have no length limit (though most compilers guarantee at least 1024 characters)
- Must start with a letter (
- Reserved naming patterns that are reserved for compiler and standard library implementations:
- Identifiers starting with underscore followed by uppercase letter (
_Name) - Identifiers containing double underscores anywhere (
__name,my__var) - Identifiers starting with underscores in the global namespace (
_global)
- Identifiers starting with underscore followed by uppercase letter (
// valid identifiers
int age;
int _count;
int value123;
int myVariableName;
// invalid identifiers
int 2fast; // starts with digit
int my-var; // contains hyphen
int class; // reserved keyword
int my var; // contains space
// case sensitivity
int value = 10;
int Value = 20; // different variable
int VALUE = 30; // another different variable
4.4 Keywords
- Keywords are reserved words with special meaning in C++ that cannot be used as identifiers
- C++ has approximately 90+ keywords that are reserved across all versions
- The following reserved keywords do exist:
alignasalignofandand_eqasmautobitandbitorboolbreakcasecatchcharchar8_tchar16_tchar32_tclasscomplconceptconstconstevalconstexprconstinitconst_castcontinueco_awaitco_returnco_yielddecltypedefaultdeletedodoubledynamic_castelseenumexplicitexportexternfalsefloatforfriendgotoifinlineintlongmutablenamespacenewnoexceptnotnot_eqnullptroperatororor_eqprivateprotectedpublicregisterreinterpret_castrequiresreturnshortsignedsizeofstaticstatic_assertstatic_caststructswitchtemplatethisthread_localthrowtruetrytypedeftypeidtypenameunionunsignedusingvirtualvoidvolatilewchar_twhilexorxor_eq
5 Structure
5.1 Scopes
- A scope is a region of code where a name (identifier) is valid and accessible
- Block statements (code enclosed in
{}) create their own scopes- Scopes can be nested within other scopes
- The program itself forms the global scope, which contains all other scopes
- A name is visible at a given point in the code if:
- It was declared earlier in the current scope, or
- It was declared in an outer scope
- Inner scopes can hide names from outer scopes, which is called shadowing
int x = 10; // global scope
void foo()
{
int y = 20; // function scope
{
int z = 30; // nested block scope
y = 10; // variables from outer scopes are accessible
int x = 5; // shadows global x
}
}
5.2 Entry Point
Every program must contain a main function as the entry point for execution:
// main with command-line arguments
int main(int argc, char* argv[])
{
// code goes here
return 0;
}
// main without command-line arguments (when not needed)
int main()
{
// code goes here
return 0;
}
- The parameter
argcprovides the count of command-line arguments - The parameter
argvprovides the actual command-line arguments as an array of C-stringsargv[0]is always the program nameargv[1]throughargv[argc-1]are the user-provided arguments
- The return value is the program’s exit status code
0conventionally indicates success- Non-zero values indicate various error conditions
- If no
returnstatement is present,main()implicitly returns0
5.3 Header and Source Files
- C++ code is typically organized into header files and source files
- Header files contain:
- Function declarations
- Class declarations
- Template definitions (must be in headers)
- Inline function definitions
- Constants and type aliases
- Source files contain:
- Function definitions
- Class member function definitions
- Global variable definitions
- This separation allows for interface/implementation separation
- Header files contain:
- Extensions don’t affect compilation, but conventions exist:
- Source files:
.cpp(most common),.cc,.cxx,.c++ - Header files:
.hpp(C-compatible),.h,.hh,.hxx
- Source files:
Best practices:
- Definitions should be kept in
.cppfiles and declarations in.hppfiles - Use forward declarations to reduce header dependencies when possible
5.4 Project Structure
- Common project directory conventions:
src/: Source files (.cpp)include/: Public header files (.hpp)lib/: External library files (.a,.so,.lib,.dll)build/: Build artifacts (object files, intermediate files)bin/: Final executable binariestest/: Test files and test executablesdocs/: Documentationexamples/: Example code demonstrating usage
6 Comments
- Comments are text annotations in source code that are ignored by the compiler
- Used for documentation, explanations, and temporarily disabling code
6.1 Single-Line Comments
- Single-line comments begin with
//and continue until the end of the line
// This is a full-line comment explaining the code below
int x = 10; // This is an inline comment explaining this variable
// Comments can span multiple lines by repeating the // prefix
// Line 1 of the comment
// Line 2 of the comment
Best practices:
- Single-line comments should be used for brief explanations, inline annotations, and quick notes
6.2 Multi-Line Comments
- Multi-line comments begin with
/*and end with*/ - Multi-line comments can span multiple lines without repeating comment markers
- Cannot be nested:
/* /* nested */ */will end at the first*/
/* This is a multi-line comment
that spans several lines.
It ends when the closing marker appears. */
/*
* Common formatting style with asterisks
* for improved readability in longer comments
*/
int y = 20; /* Can also be used inline */
Best practices:
- Multi-line comments should be used for longer explanations, function/class descriptions, or block documentation
6.3 Documentation Comments
- Documentation comments are a special comment format used by documentation generators
- They provide structured documentation for functions, classes, and parameters
- Common formats:
///- Triple-slash style/** ... */- JavaDoc style
- Common documentation tags:
@brief- Brief description@param- Parameter description@return- Return value description@note- Additional notes@see- Cross-references
// Triple-slash style example
/// @brief Calculates the sum of two integers
/// @param a The first integer
/// @param b The second integer
/// @return The sum of a and b
int add(int a, int b)
{
return a + b;
}
// JavaDoc style example
/**
* @brief Represents a 2D point in Cartesian coordinates
*
* This class provides basic operations for working with points,
* including distance calculations and coordinate access.
*
* @note Coordinates are stored as double-precision floating-point values
*/
class Point
{
public:
/**
* @brief Constructs a point at the specified coordinates
* @param x The x-coordinate (default: 0.0)
* @param y The y-coordinate (default: 0.0)
*/
Point(double x = 0.0, double y = 0.0);
/**
* @brief Calculates the distance from this point to another
* @param other The other point
* @return The Euclidean distance between the two points
*/
double distanceTo(const Point& other) const;
private:
double m_x; ///< The x-coordinate of the point
double m_y; ///< The y-coordinate of the point
};
7 Namespaces
- Namespaces provide a way to organize code and prevent name collisions
- Identifiers within a namespace can be accessed from outside using the scope resolution operator
:: - Multiple namespace blocks with the same name are considered part of the same namespace
- Namespaces can be nested and aliased for convenience
Best practices
- Organize your code into logical namespaces that reflect project structure
7.1 Basic Namespace Usage
// defining a namespace
namespace math
{
int add(int a, int b)
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
}
// split definitions - these belong to the same namespace
namespace math
{
int subtract(int a, int b)
{
return a - b;
}
}
// access element of namespace from of it
int result = math::add(5, 3);
// bring specific identifier into current scope
using math::add;
int result2 = add(5, 3); // no namespace prefix needed
int result3 = math::multiply(2, 4); // other identifiers still need prefix
// bring entire namespace into current scope
using namespace math;
int result4 = subtract(10, 5); // all identifiers accessible without prefix
Best practices:
- Don’t use
using namespacein header files - it pollutes all files that include the header - Avoid
using namespace std;in production code to prevent name collisions
7.2 Nested Namespaces
// explicit nested namespace
namespace company
{
namespace project
{
namespace module
{
void function() {}
}
}
}
// simplified nested namespace
namespace company::project::module
{
void function() {}
}
// access nested namespace
company::project::module::function();
// namespace alias for convenience
namespace cpm = company::project::module;
cpm::function();
Best practices:
- Use namespace aliases for deeply nested namespaces to improve readability
7.3 Anonymous Namespaces
- Anonymous namespaces are used to have only internal linkage
- This means that their content is only visible inside their translation unit
- This is helpful to provide encapsulation in APIs
namespace
{
int helperFunction() { return 42; }
const int SECRET = 123;
}
int publicFunction()
{
// can use directly within same file
return helperFunction();
}
Best practices:
- Use anonymous namespaces instead of
staticfor internal linkage
8 Preprocessor Directives
- Preprocessor directives are executed by the preprocessor before compilation
- They begin with
#and do not require semicolons - Processed as text substitution before the compiler sees the code
8.1 Include Directives
// include standard library header (searches system include paths)
#include <iostream>
#include <vector>
// include local header (searches current directory first)
#include "myheader.h"
#include "utilities/helper.h"
Best practices:
- Put
#includedirectives at the beginning of files - Always include what you use (don’t rely on transitive includes)
8.2 Include Guards
// traditional include guards to prevent multiple inclusion
#ifndef MYHEADER_H
#define MYHEADER_H
// header content here...
#endif
// modern include guards to prevent multiple inclusion
#pragma once
Best practices:
- Use
#pragma oncein all header files (simpler and faster)
8.3 Macros
// define simple constant macro
#define PI 3.14159
#define MAX_SIZE 100
// function-like macros (text substitution, not type-safe)
#define SQUARE(x) ((x) * (x)) // parentheses prevent precedence issues
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// using macros
int area = SQUARE(5); // expands to ((5) * (5))
int maximum = MAX(10, 20); // expands to ((10) > (20) ? (10) : (20))
// undefine macros
#undef PI
#undef SQUARE
Best practices:
- Prefer
constvariables andinlinefunctions over macros when possible - Use all caps for macro names to distinguish them from regular code
- Always use parentheses in macro expressions to avoid precedence issues
- Avoid complex macro logic and use templates or functions instead
8.4 Conditional Compilation
// compile code only if macro is defined
#ifdef DEBUG
std::cout << "Debug mode enabled" << std::endl;
#endif
// compile code only if macro is not defined
#ifndef NDEBUG
assert(x > 0);
#endif
// include conditionally
#if defined(WINDOWS)
#include <windows.h>
#elif defined(LINUX)
#include <unistd.h>
#else
#error "Unsupported platform"
#endif
// check macro values
#define VERSION 2
#if VERSION >= 2
// new feature code
#else
// legacy code
#endif
Best practices:
- Use conditional compilation for platform-specific or debug code
8.5 Predefined Macros
// standard predefined macros
__FILE__ // current source file name
__LINE__ // current line number
__DATE__ // compilation date
__TIME__ // compilation time
__cplusplus // C++ standard version (e.g., 201703L for C++17)
std::cout << "Error at " << __FILE__ << ":" << __LINE__ << std::endl;
8.6 Error and Warning Generation
// generate compiler error with message
#error "This configuration is not supported"
// generate compiler warning with message (non-standard, compiler-specific)
#warning "This feature is deprecated"
9 Variables
- Variables are named storage locations that hold values of a specific data type
- They must be declared with a type before use and can be modified during program execution
// declaration without initialization
int x;
// undefined behavior
x + 1;
// copy initialization (makes temporary copies for classes)
int y = 10;
// direct initialization (makes no temporary copies for classes)
int z(20);
// uniform initialization (prevents type narrowing)
int a{30};
// list initialization (alternative syntax for uniform initialization)
int b = {40};
// assignment
x = 10;
y = 12;
// initialize constant that is immutable (must be initialized)
const int MAX_SIZE = 100;
// type inference with auto
auto value = 42; // int
auto pi = 3.14; // double
auto name = "John"; // const char*
// save variable on the heap
int* heapVar = new int(100);
delete heapVar; // must manually free memory
Best practices:
- Identifiers of variables with primitive data types should be written in camel case
- Variables should always be initialized to avoid undefined behavior
- Prefer uniform initialization
{}to prevent narrowing conversions - Use
constfor variables that should not be modified - Use
autowhen the type is obvious from context to reduce verbosity - Create variables close to first use
- Prefer automatic storage (stack) over dynamic storage (heap) when possible
10 Data Types
- Data types determine how much memory is allocated and how the bits are interpreted for a value
10.1 Primitive Data Types
- Primitive data types represent single values and have predefined sizes
10.1.1 Integer Types
- Integer types represent whole numbers that can be either signed or unsigned
- Sizes of integer types vary by platform but follow guaranteed minimums
| Keyword | Byte Size | Signedness |
|---|---|---|
bool | At least 1 | N/A |
char | At least 1 | Implementation-defined |
signed char | At least 1 | Signed |
unsigned char | At least 1 | Unsigned |
shortshort int | At least 2 | Signed |
unsigned shortunsigned short int | At least 2 | Unsigned |
int | At least 4 | Signed |
unsigned int | At least 4 | Unsigned |
long | At least 8 | Signed |
unsigned long | At least 8 | Unsigned |
long longlong long int | At least 8 | Signed |
unsigned long longunsigned long long int | At least 8 | Unsigned |
int8_t | 1 | Signed |
uint8_t | 1 | Unsigned |
int16_t | 2 | Signed |
uint16_t | 2 | Unsigned |
int32_t | 4 | Signed |
uint32_t | 4 | Unsigned |
int64_t | 8 | Signed |
uint64_t | 8 | Unsigned |
size_t | System Size | Unsigned |
ptrdiff_t | System Size | Signed |
boolandchardon’t represent integers, but are represented internally as such themselves- Thereby
boolrepresents boolean values - Thereby
charis also a character type
- Thereby
Best practices:
intshould be used for general-purpose integers when size doesn’t matter- Fixed-width types (
int32_t,uint64_t, etc.) should be used when exact size is relevant size_tshould be used for sizes, indices and countsshortshould be used overshort int,longoverlong intandlong longoverlong long intunsignedtypes should be used for bit manipulation and when negative values are meaninglessptrdiff_tshould be used for pointer arithmetic
10.1.2 Floating-Point Types
- Floating-point types represent real numbers with fractional parts
- Sizes and precisions of floating-point types vary by platform but follow guaranteed minimums
- Floating-point types follow the IEEE 754 standard on most platforms
- Therefore they aren’t guaranteed to be precise
| Keyword | Byte Size | Precision (decimal digits) |
|---|---|---|
float | At least 4 | ~6-9 |
double | At least 8 | ~15-17 |
long double | At least 8 | ~15-21 |
Best practices:
doubleshould be used instead offloat
10.1.3 Character Types
- Character types represent individual characters
- These support various character encodings as ASCII, UTF-8, UTF-16 and UTF-32
| Keyword | Byte Size | Purpose |
|---|---|---|
char | 1 | Basic character (ASCII/UTF-8) |
wchar_t | At least 2 | Wide character |
char8_t | 1 | UTF-8 character |
char16_t | 2 | UTF-16 character |
char32_t | 4 | UTF-32 character |
10.2 Compound Data Types
- Compound data types consist of any number of primitive data types or even other compound data types
10.2.1 Arrays
- Arrays are fixed-size collections storing multiple elements of the same type in contiguous memory
- They are zero-indexed
- Their size must be known at compile time
- They decay to pointers when passed to functions
- These were introduced in C, but more modern approaches for them were introduced since then in C++
- This is why they are sometimes referred to as C-style arrays
// declare array
int x[5];
// initialize arrays
int y1[5] = {1, 2, 3, 4, 5}; // explicit size and values
int y2[] = {1, 2, 3, 4, 5}; // size inferred from initializer (5 elements)
int y3[5] = {1, 2}; // partial initialization (remaining elements zero-initialized)
int y4[5] = {}; // zero-initialization for all elements
// access elements
y1[0] == 1; // read first element
y1[1] = 5; // modify second element
y1[1] == 5; // verify modification
y1[100]; // undefined behavior
// pointer arithmetic (arrays decay to pointers)
int* ptr = y1; // pointer to first element
*(y1 + 2) == 3; // access third element via pointer arithmetic
ptr[2] == 3; // equivalent using subscript notation
// multi-dimensional array
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
matrix[1][2] == 7; // access element at row 1, column 2
Best practices:
- Always initialize arrays to avoid undefined behavior from garbage values
- Prefer
std::array(fixed size) orstd::vector(dynamic size) over C-arrays - Never access elements beyond array bounds
- Use
.at()withstdcontainers for bounds checking
- Use
- Store array size separately or use
std::size()when passing arrays to functions
10.2.2 Strings
- Strings are null-terminated character arrays (they end with
\0)
// use string literals (immutable, stored in read-only memory)
const char* name1 = "John"; // pointer to string literal
// mutable character arrays
char name2[] = "John"; // array with null terminator
char name3[] = {'J', 'o', 'h', 'n', '\0'}; // explicit initialization
char name4[10] = "John"; // array with extra space
// string manipulation
#include <cstring>
std::strlen(name2); // get length (4, excludes null terminator)
std::strcpy(name4, "Jane"); // copy string (unsafe, no bounds checking)
std::strcat(name4, " Doe"); // concatenate strings (unsafe)
std::strcmp(name2, name4); // compare strings (returns 0 if equal)
Best practices:
- Prefer
std::stringover C-style strings for safety and convenience - Use
.at()instead of[]when bounds checking is needed - Pass strings by
constreference to functions:void func(const std::string& str) - Avoid C-string functions (
strcpy,strcat, etc.) due to buffer overflow risks
10.2.3 Structures
- Structures are user-defined types that group related data members of different types
- Their members are publicly accessible by default (unlike classes)
- They are typically used for Plain Old Data (POD) types or simple data aggregates
// define structure
struct Point
{
int x;
int y;
};
// declare structure variable
Point p1;
// define structure and declare variable simultaneously
struct Rectangle
{
int width;
int height;
} rect1, rect2;
// initialize structure
struct Person
{
std::string name;
int age;
double height;
};
// aggregate initialization
Person p1{"Alice", 30, 1.65}; // uniform initialization (preferred)
Person p2 = {"Bob", 25, 1.80}; // copy-list-initialization
// designated initializers
Person p3{.name = "Charlie", .age = 35, .height = 1.75}; // can specify in any order
Person p4{.age = 40, .name = "Diana"}; // omitted members are zero-initialized
// default initialization (members have indeterminate values)
Person p5; // uninitialized members
// direct member access
p1.name = "Alice Smith";
p1.age = 31;
int currentAge = p1.age;
// pointer to structure
Person* ptr = &p1;
ptr->name = "Alicia"; // arrow operator for pointers
(*ptr).age = 32; // equivalent using dereference and dot operator
// structure with constructor method
struct Employee
{
std::string name;
int id;
double salary;
// default constructor
Employee()
: name(""), id(0), salary(0.0)
{}
// parameterized constructor with initialization list
Employee(std::string n, int i, double s)
: name(n), id(i), salary(s)
{}
// destructor
~Employee()
{
// cleanup if needed
}
};
Employee emp1; // calls default constructor
Employee emp2("John", 1001, 50000.0); // calls parameterized constructor
Employee emp3{"Jane", 1002, 55000.0}; // uniform initialization
// nested structure
struct Address
{
std::string street;
std::string city;
int zipCode;
};
struct Customer
{
std::string name;
Address address; // nested structure
};
Customer c1{"Alice", {"123 Main St", "Springfield", 12345}};
c1.address.city = "Shelbyville";
Best practices:
- Identifiers of structures should be written in Pascal case
- Always initialize structures to avoid undefined behavior with uninitialized members
- Prefer uniform initialization
{}over copy-list-initialization=to prevent narrowing conversions - Use designated initializers for clarity when initializing complex structures
- Use structures for simple data aggregates
- Use classes when adding significant behavior or invariants
- Avoid manual memory management within structures
- Prefer smart pointers or RAII wrappers
10.2.4 Unions
- Unions are special data structures where all members share the same memory location
- Only one of their members can hold a value at any given time
- Their size equals the size of the largest member
// define union
union Data
{
int intValue; // 4 bytes
float floatValue; // 4 bytes
char charValue; // 1 byte
};
// declare union variable
Data d1;
// define union and declare variable simultaneously
union Value
{
int i;
double d;
} v1, v2;
// initialize union (initializes first member by default)
Number n1 = {42}; // initializes i
Number n2{3.14f}; // initializes i with float converted to int (C++11)
// designated initialization
Number n3{.f = 3.14f}; // explicitly initialize f
Number n4{.d = 2.718}; // explicitly initialize d
// access members
n1.i = 100; // set integer value
n1.f = 3.14f; // overwrites integer value, now f is active
// n1.i is now undefined!
int x = n1.i;
// track which member of union is active
enum class DataType { INT, FLOAT, DOUBLE };
struct TaggedData
{
DataType type; // discriminator
union
{
int intValue;
float floatValue;
double doubleValue;
} value;
};
TaggedData data;
data.type = DataType::INT;
data.value.intValue = 42;
// define union with constructor and destructor method
union FlexibleData
{
int i;
double d;
// constructor for int
FlexibleData(int val) : i(val) {}
// constructor for double
FlexibleData(double val) : d(val) {}
// destructor
~FlexibleData() {}
};
FlexibleData fd1(42); // initializes i
FlexibleData fd2(3.14); // initializes d
Best practices:
- Identifiers of unions should be written in Pascal case
- Prefer
std::variantover unions for type-safe alternatives - Always track which member is active using a discriminator
- Never read from a member that wasn’t the last one written
- Use unions only when necessary for memory optimization or interfacing with C code
- Avoid unions with constructors/destructors unless necessary
10.2.5 Enums
- Enums are user-defined types representing a set of named integer constants
- They provide type safety and readability for representing discrete values
// basic enumeration (values start at 0 and increment by 1)
enum Color
{
Red, // 0
Green, // 1
Blue // 2
};
// enumeration with custom values
enum Status
{
Success = 0,
Warning = 1,
Error = -1,
Critical = 100
};
// usage
Color c1 = Red; // enumerators are in surrounding scope
Color c2 = Color::Green; // can also use scope resolution
int value = Blue; // implicit conversion to int (value = 2)
// scoped enumeration
enum class Direction
{
North, // 0
South, // 1
East, // 2
West // 3
};
// scoped enumerations with custom values
enum class HttpStatus
{
OK = 200,
NotFound = 404,
ServerError = 500
};
// usage of scoped enumerations
Direction dir1 = Direction::North; // must use scope resolution
int value = static_cast<int>(Direction::South); // explicit conversion required
// specify underlying integer type of enumeration
enum class Byte : uint8_t
{
Min = 0,
Max = 255
};
Best practices:
- Identifiers of enumerations should be written in Pascal case
- Enumerator names should be written in Pascal case or CONSTANT_CASE depending on convention
- Prefer
enum classover plainenumto avoid naming conflicts and implicit conversions - Specify underlying type explicitly when size matters or for forward declarations
- Use enums for representing a fixed set of related constants (states, options, error codes)
10.3 Data Type Management
- Multiple utilities for controlling type properties, conversions, and compile-time type manipulation exist
10.3.1 Constants
- Constants are immutable values that cannot be modified after initialization
- Multiple levels of compile-time evaluation available
// primitive runtime constants
const double PI = 3.14159;
const int MAX_SIZE = 100;
// compound type runtime constants
const int* ptr1 = &MAX_SIZE; // pointer to const int (can't modify *ptr1)
int* const ptr2 = &value; // const pointer to int (can't modify ptr2)
const int* const ptr3 = &value; // const pointer to const int (can't modify either)
// runtime reference constants
const int& ref = MAX_SIZE; // reference to const int
const char* names[] = {"John", "Jane"}; // array of pointers to const char
const int values[] = {1, 2, 3, 4, 5}; // array of const int
// constant compile-time evaluation
constexpr int ARRAY_SIZE = 10;
constexpr double PI = 3.14159265359;
constexpr int square(int x) { return x * x; }
int arr[ARRAY_SIZE]; // use compile-time constant
int area = square(5); // evaluate at compile time if possible
// compile-time constant classes
class Point
{
public:
int x, y;
constexpr Point(int x_, int y_) : x(x_), y(y_) {} // constexpr constructor
constexpr int getX() const { return x; } // constexpr method
};
constexpr Point p(10, 20); // created at compile time
constexpr int xCoord = p.getX(); // evaluated at compile time
Best practices:
- Use constant case for compile-time constants and configuration values
- Prefer
constexproverconstwhen values can be computed at compile time - Declare variables as
constwhen they shouldn’t be modified after initialization - Use
constreferences for function parameters to avoid unnecessary copies
10.3.2 Volatile
- The
volatilekeyword tells the compiler that a variable’s value may change at any time without any action being taken by the code- This prevents the compiler from optimizing away reads or writes to that variable
- The variable’s actual value is always read from memory rather than being cached in a register
// hardware register that can change due to external hardware
volatile int hardwareRegister = 0;
// variable shared with signal handler
volatile sig_atomic_t signalFlag = 0;
// memory-mapped I/O
volatile uint8_t* ioPort = reinterpret_cast<volatile uint8_t*>(0x1000);
*ioPort = 0xFF; // write to hardware
// reading volatile variable always fetches from memory
while (hardwareRegister == 0) {
// compiler will not optimize this away
// always reads the actual value from memory
}
// volatile pointer variations
volatile int* ptr1; // pointer to volatile int
int* volatile ptr2; // volatile pointer to int
volatile int* volatile ptr3; // volatile pointer to volatile int
Best practices:
- Use
volatilefor hardware registers in embedded systems - Use
volatilefor variables modified by signal handlers - Use
volatilefor memory-mapped I/O operations - Do NOT use
volatilefor thread synchronization - use atomics or mutexes instead volatiledoes not provide thread safety or memory ordering guarantees- Combine with appropriate types like
sig_atomic_tfor signal-safe operations
10.3.3 Type Aliases
- Type aliases create alternative names for existing types
// traditional type aliases
typedef const char* cstring;
cstring myString = "Hello!";
// modern type aliases
using cstring = const char*;
cstring myOtherString = "Hello!";
Best practices:
- Prefer
usingovertypedeffor consistency and template support - Use type aliases to simplify complex type declarations
- Create aliases for frequently used container types
- Name aliases descriptively to convey their purpose
10.3.4 Size and Alignment
- Data types are aligned to ensure that data is stored in memory at addresses matching their size or specific boundaries
- This improving access speed and preventing hardware errors
// size of types in bytes (traditional)
size_t intSize = sizeof(int);
size_t doubleSize = sizeof(23.1);
// size of types in bytes (modern)
#include <iterator>
size_t boolSize = std::size(bool);
size_t charSize = std::size('r');
// check type sizes at compile time
static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");
static_assert(sizeof(void*) == 8, "requires 64-bit platform");
// alignment of types
size_t intAlign = alignof(int); // typically 4
size_t doubleAlign = alignof(4.31); // typically 8
// force alignment of type
alignas(16) int x;
Best practices:
- Use
std::size()instead of manual sizeof calculations for arrays - Be aware that
sizeof(array_parameter)gives pointer size, not array size - Use
sizeoffor buffer allocation and memory calculations - Remember that structure sizes include padding for alignment
- Use
static_assertto verify size assumptions at compile time
10.3.5 Type Conversion and Casting
- Type conversions transform values from one type to another
- Conversion can be implicit (automatic) or explicit (manual)
// automatic numeric promotions (smaller to larger)
int i = 10;
long l = i; // int to long
double d = i; // int to double
// automatic arithmetic conversions (mixed-type operations)
int x = 5;
double y = 2.5;
double result = x + y; // x promoted to double, result is 7.5
// automatic narrowing conversions (can lose data)
int truncated = 3.14; // 3.14 truncated to 3
int overflow = 300;
char c = overflow; // undefined behavior if overflow occurs
// automatic pointer conversions
Derived* derived = new Derived();
Base* base = derived; // upcast (derived to base) - always safe
// automatic boolean conversions
int num = 42;
if (num) { } // int to bool: non-zero is true
int* ptr = nullptr;
if (!ptr) { } // pointer to bool: non-null is true
// explicit compile-time conversions
int i = 10;
double d = static_cast<double>(i); // int to double
int j = static_cast<int>(3.14); // double to int (truncates to 3)
Derived* derived = static_cast<Derived*>(base); // unchecked downcast
//explicit C-style conversions (tries multiple cast types in sequence)
float f = (float)i; // C-style cast
float g = float(i); // functional cast notation
// explicit run-time conversion for safe downcasting
class Base {};
class Derived : public Base {};
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
if (derived) {
// safe to use derived methods
} else {
// cast failed
}
// remove const modifier
const int* constPtr = new int(42);
int* mutablePtr = const_cast<int*>(constPtr);
// add const modifier
int* ptr = new int(10);
const int* cptr = const_cast<const int*>(ptr); // or just: const int* cptr = ptr;
// remove volatile modifier
volatile int hardware = 0;
int normal = const_cast<int&>(hardware); // removes volatile qualifier
// bit-level conversion
int i = 42;
float* f = reinterpret_cast<float*>(&i); // reinterpret int bits as float
// *f has undefined value, not 42.0!
Best practices:
- Prefer C++ casts (
static_cast, etc.) over C-style casts for clarity and safety - Use
static_castfor well-defined conversions (numeric, pointer upcast) - Use
dynamic_castfor safe downcasting in polymorphic hierarchies - Avoid
const_castunless interfacing with const-incorrect APIs - Avoid
reinterpret_castexcept for low-level system programming - Make implicit conversions explicit when clarity is important
- Avoid narrowing conversions or use explicit checks
- Enable compiler warnings for implicit conversions (
-Wconversion)
10.3.6 Type Inference
- Type inference allows the compiler to deduce types automatically
// automatic type deduction
auto i = 42; // int
auto d = 3.14; // double
auto c = 'x'; // char
auto s = "hello"; // const char*
// use type of other value
int i = 42;
decltype(i) j = i; // int
// function template deduction
template<typename T>
void process(T value) {}
process(42); // T deduced as int
process(3.14); // T deduced as double
process("hello"); // T deduced as const char*
// decompose tuples and pairs
std::pair<int, std::string> getData() {
return {42, "hello"};
}
// decompose structures
struct Point { int x; int y; };
Point p{10, 20};
auto [x, y] = p; // x: int, y: int
Best practices:
- Use
autofor complex type names - Use
autoto avoid redundancy - Use
decltype(auto)when you need to preserve exact return types - Use structured bindings for clearer tuple/pair decomposition
- Don’t use
autowhen type is not obvious from context - Consider explicit types for numeric literals if precision matters
- Enable warnings for auto type deductions that might surprise (
-Wauto-type)
10.3.7 Generic Programming (Templates)
- Templates enable writing code that works with any type
- They provide compile-time polymorphism without runtime overhead
// function template
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b)
{
return a + b;
}
// explicit function template instantiation
int i = max<int>(5, 10); // T = int
double d = max<double>(3.5, 2.1); // T = double
// function template argument deduction
int j = max(5, 10); // T deduced as int
auto k = max(3.5, 2.1); // T deduced as double
auto result = add(5, 3.14); // returns double (8.14)
// class template
template <typename T>
class Container
{
private:
T value;
public:
Container(T v) : value(v) {}
T get() const { return value; }
void set(T v) { value = v; }
};
// explicit class template instantiation
Container<int> intContainer(42);
Container<std::string> strContainer("hello");
// class template argument deduction
Container c1(42); // Container<int> deduced
Container c2("text"); // Container<const char*> deduced
// variadic template arguments
template <typename... Args>
void print(Args... args)
{
(std::cout << ... << args) << std::endl;
}
// define concept (constraint)
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;
// instantiate concept
template <Numeric T>
T multiply(T a, T b)
{
return a * b;
}
multiply(5, 10); // OK: int is numeric
multiply(3.14, 2.0); // OK: double is numeric
// multiply("a", "b"); // Error: string is not numeric
// define concept with requires clause
template <typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
};
// instantiate concept with requires clause
template <Comparable T>
T clamp(T value, T min, T max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
// use multiple requires clauses
template <typename T>
requires Numeric<T> && Comparable<T>
T average(T a, T b)
{
return (a + b) / 2;
}
Best practices:
- Rely on template argument deduction to reduce verbosity
- Document template requirements clearly or use concepts
- Avoid excessive template complexity
- Consider providing explicit instantiations for common types
- Use variadic templates for flexible parameter counts
11 Literals
- Literals are fixed values written directly in source code
- Their types are determined by their syntax and suffixes
- They are evaluated at compile time
11.1 Integer Literals
- Integer literals represent whole number values
- Their default type is
int, but suffixes can change the type
// decimal literals (base 10)
42; // int
0; // int
1000; // int
// octal literals (base 8)
052; // int (42 in decimal)
0777; // int (511 in decimal)
// hexadecimal literals (base 16)
0x2A; // int (42 in decimal)
0xFF; // int (255 in decimal)
0XABCD; // int (43981 in decimal)
// binary literals (base 2)
0b101010; // int (42 in decimal)
0B1111; // int (15 in decimal)
// integer suffixes (case-insensitive)
42U; // unsigned int
42L; // long
42UL; // unsigned long
42LL; // long long
42ULL; // unsigned long long
// digit separators for readability
1'000'000; // 1000000
0b1010'0101; // binary with separator
0x00FF'ABCD; // hexadecimal with separator
Best practices:
- Use digit separators
'for large numbers to improve readability - Use hexadecimal for bit masks and flags
- Use binary literals for bit patterns when clarity is important
- Avoid octal literals as they can be confused with decimal (leading zero)
- Use suffixes to prevent unexpected type conversions
11.2 Floating-Point Literals
- Floating-point literals represent real numbers with fractional parts
- Their default type is
double, but suffixes can change the type
// basic floating-point literals
3.14; // double
0.5; // double
.5; // double (0.5)
5.; // double (5.0)
123.456; // double
// scientific notation (exponent: E or e)
1e10; // double (10000000000.0)
2.5e-3; // double (0.0025)
6.02E23; // double (602000000000000000000000.0)
// floating-point suffixes
3.14F; // float (or 3.14f)
3.14L; // long double (or 3.14l)
// digit separators for readability
1'000.5; // 1000.5
6.022'140'76e23; // Avogadro's number with separators
// hexadecimal floating-point literals
0x1.2p3; // double (9.0 in decimal: 1.125 * 2^3)
0x1.fp10; // double (1984.0 in decimal)
Best practices:
- Use
Fsuffix for float literals to avoid implicit double-to-float conversions - Prefer
doubleoverfloatfor general-purpose floating-point arithmetic - Use scientific notation for very large or very small numbers
- Be aware that floating-point literals are approximations
11.3 Boolean Literals
- Boolean literals represent truth values
true; // bool (1)
false; // bool (0)
// usage
bool isValid = true;
bool hasError = false;
11.4 Character Literals
- Character literals represent single characters enclosed in single quotes
- Type is
charby default, but prefixes can change that
// basic character literals
'a'; // char
'Z'; // char
'5'; // char
'@'; // char
' '; // space character
// escape sequences for special characters
'\n'; // newline
'\t'; // horizontal tab
'\r'; // carriage return
'\b'; // backspace
'\f'; // form feed
'\v'; // vertical tab
'\0'; // null character
'\\'; // backslash
'\''; // single quote
'\"'; // double quote
'\?'; // question mark (avoid trigraphs)
// numeric escape sequences
'\x41'; // hexadecimal (character 'A')
'\101'; // octal (character 'A')
// wide character literals
L'A'; // wchar_t
L'\u00E9'; // wchar_t (é)
// UTF character literals
u8'a'; // char (UTF-8)
u'A'; // char16_t (UTF-16)
U'A'; // char32_t (UTF-32)
// universal character names (Unicode code points)
'\u0041'; // char ('A')
'\U00000041'; // char ('A')
Best practices:
- Use escape sequences for non-printable characters
- Prefer named escape sequences (
\n,\t) over numeric codes for readability - Use Unicode escape sequences (
\u,\U) for international characters - Be aware that character literals are integers and can be used in arithmetic
11.5 String Literals
- String literals represent sequences of characters enclosed in double quotes
- Type is array of
const charwith null terminator by default
// basic string literals
"Hello"; // const char[6] (includes '\0')
"Hello, World!"; // const char[14]
""; // empty string: const char[1] (just '\0')
// string literals with escape sequences
"First line\nSecond line"; // multi-line text
"Path\\to\\file"; // backslashes
"He said \"Hello\""; // quotes within string
// string concatenation (adjacent literals are merged)
"Hello, " "World!"; // equivalent to "Hello, World!"
"This is a long string "
"split across multiple "
"lines for readability";
// wide string literals
L"Hello"; // const wchar_t[6]
// UTF string literals
u8"Hello"; // const char[6] (UTF-8)
u"Hello"; // const char16_t[6] (UTF-16)
U"Hello"; // const char32_t[6] (UTF-32)
// raw string literals (escape sequences not interpreted)
R"(C:\path\to\file)"; // no need to escape backslashes
R"(Line 1
Line 2
Line 3)"; // preserves newlines
R"delimiter(String with "quotes" and )parentheses)delimiter"; // custom delimiter
// string literal suffixes
"text"s; // std::string
u8"text"s; // std::u8string
u"text"s; // std::u16string
U"text"s; // std::u32string
// multi-line string literals using raw strings
const char* json = R"({
"name": "John",
"age": 30,
"city": "New York"
})";
Best practices:
- Use raw string literals for paths, regex patterns, and embedded code to avoid escaping
- Use
std::string(with"text"ssuffix) instead ofconst char*for easier manipulation - Prefer UTF-8 encoding (
u8"") for international text - Break long string literals across multiple lines using adjacent string concatenation
- Use custom delimiters in raw strings when the string contains
)"sequence
11.6 Pointer Literals
// null pointer literal
nullptr; // std::nullptr_t
// usage
int* ptr = nullptr;
void func(int* p) { }
func(nullptr);
// C-style null pointers
int* oldStyle1 = NULL;
int* oldStyle2 = 0;
Best practices:
- Always use
nullptrinstead ofNULLor0for null pointers nullptris type-safe and prevents ambiguous overload resolution
12 Operators
- Operators perform operations on operands (values, variables, or expressions)
- Operators have precedence rules that determine evaluation order
- Operators can be overloaded for custom types
12.1 Operator Precedence and Parentheses
- Operators are evaluated according to precedence rules (e.g.,
*before+) - Parentheses
()override default precedence and force evaluation of enclosed expressions first - Use parentheses to clarify intent even when not strictly necessary
// multiplication has higher precedence than addition
4 + 3 * 2 == 10; // evaluated as: 4 + (3 * 2)
// parentheses change evaluation order
(4 + 3) * 2 == 14;
// multiple levels of parentheses
((2 + 3) * 4) - 5 == 15;
// use parentheses for clarity even when not required
if ((x > 0) && (y < 10)) { } // clearer than: if (x > 0 && y < 10)
Best practices:
- Use parentheses to clarify complex expressions
- Don’t rely on memorizing precedence rules for readability
12.2 Arithmetic Operators
- Arithmetic operators perform mathematical operations on numeric values
- Result types depend on operand types (integer vs floating-point)
- They may cause overflow, underflow, or undefined behavior with invalid operations
| Operation | Operator | syntax | Example |
|---|---|---|---|
| Addition | + | x + y | 3 + 4 == 7; |
| Unary Plus | + | +x | +(5) == 5; |
| Subtraction | - | x - y | 4 - 3 == 1; |
| Unary Minus | - | -y | -(4) == -4; |
| Multiplication | * | x * y | 3 * 2 == 6; |
| Division | / | x / y | 3.0 / 2.0 == 1.5; |
| Integer Division | / | x / y | 3 / 2 == 1; |
| Modulo | % | x % y | 11 % 4 == 3; |
| Pre-Increment | ++ | ++x | int x = 3; ++x == 4; |
| Post-Increment | ++ | x++ | int x = 3; x++ == 3; |
| Pre-Decrement | -- | --x | int x = 3; --x == 2; |
| Post-Decrement | -- | x-- | int x = 3; x-- == 3; |
// integer division truncates toward zero
7 / 2 == 3; // not 3.5
-7 / 2 == -3; // not -3.5
// floating-point division
7.0 / 2.0 == 3.5;
7 / 2.0 == 3.5; // mixed types promote to double
// modulo operator (remainder after integer division)
10 % 3 == 1;
-10 % 3 == -1; // sign matches dividend
10 % -3 == 1;
// modulo only works with integer types
// 5.5 % 2.0; // compilation error
// increment/decrement operators
int x = 5;
int a = ++x; // x becomes 6, then a = 6 (pre-increment)
int b = x++; // b = 6, then x becomes 7 (post-increment)
// division by zero
int result = 10 / 0; // undefined behavior
double d = 10.0 / 0.0; // may produce infinity (implementation-defined)
Best practices:
- Be explicit with floating-point literals to avoid unintended integer division:
x / 2.0notx / 2 - Prefer pre-increment
++iover post-incrementi++for better performance with complex types - Check for division by zero before dividing
- Be aware that modulo with negative numbers may produce unexpected results
12.3 Comparison Operators
- Comparison operators compare values and return boolean results (
trueorfalse) - All comparison operators have lower precedence than arithmetic operators
| Operation | Operator | syntax | Example |
|---|---|---|---|
| Equality | == | x == y | 4 == 4 == true; |
| Inequality | != | x != y | 3 != 4 == true; |
| Greater | > | x > y | 4 > 3 == true; |
| Greater-Equals | >= | x >= y | 4 >= 3 == true; |
| Less | < | x < y | 3 < 4 == true; |
| Less-Equals | <= | x <= y | 3 <= 4 == true; |
// comparing floating-point values requires caution
double a = 0.1 + 0.2;
a == 0.3; // may be false due to floating-point precision
std::abs(a - 0.3) < 0.0001; // better approach with epsilon
// comparing pointers
int x = 5;
int* p1 = &x;
int* p2 = &x;
p1 == p2; // true (same address)
// comparing C++-strings
std::string s1 = "hello";
std::string s2 = "hello";
s1 == s2; // true (compares content)
const char* c1 = "hello";
const char* c2 = "hello";
c1 == c2; // may be true or false (compares addresses, not content)
std::strcmp(c1, c2) == 0; // correct way to compare C-strings
Best practices:
- Use
std::abs(a - b) < epsilonfor floating-point comparisons - Use
std::strcmp()orstd::stringfor string comparisons, not==onchar* - Be aware that
==compares pointer addresses, not pointed-to values
12.4 Logical Operators
- Logical operators perform boolean logic operations on truth values
- Logical AND and OR use short-circuit evaluation (right operand may not be evaluated)
- Their result is always
booltype
| Operation | Operator | syntax | Example |
|---|---|---|---|
| AND | && | x && y | true && true == true; |
| OR | ││ | x ││ y | true ││ false == true; |
| NOT | ! | !x | !false == true; |
// short-circuit evaluation with AND
if (ptr != nullptr && ptr->value > 0) { // safe: ptr checked first
// ptr->value only evaluated if ptr != nullptr
}
// short-circuit evaluation with OR
if (x == 0 || y / x > 10) { // safe: division only if x != 0
// y / x only evaluated if x != 0
}
// logical NOT
bool isValid = true;
!isValid == false;
// common patterns
if (!ptr) { } // check if pointer is null
if (!vec.empty()) { } // check if container has elements
// has higher precedence than && and ||
!x && y; // equivalent to: (!x) && y
x || y && z; // equivalent to: x || (y && z)
Best practices:
- Rely on short-circuit evaluation for null checks and bounds checking
- Put cheaper/faster conditions first in
&&chains - Put more likely-true conditions first in
||chains - Use parentheses to clarify complex logical expressions
12.5 Bitwise Operators
- Bitwise operators manipulate individual bits of integer values
- They only work with integer types (not floating-point)
- Left shift multiplies by powers of 2, right shift divides by powers of 2
| Operation | Operator | syntax | Example |
|---|---|---|---|
| Bitwise AND | & | x & y | 0b0110 & 0b0011 == 0b0010; |
| Bitwise OR | │ | x │ y | 0b0110 │ 0b0011 == 0b0111; |
| Bitwise NOT | ~ | ~x | ~0b0110 == 0b...11111001; |
| Bitwise XOR | ^ | x ^ y | 0b0110 ^ 0b0011 == 0b0101; |
| Left Shift | << | x << n | 0b0011 << 2 == 0b1100; |
| Right Shift | >> | x >> n | 0b1100 >> 2 == 0b0011; |
// bit masking to check specific bits
uint8_t flags = 0b10110100;
if (flags & 0b00000100) { // check if bit 2 is set
// bit 2 is set
}
// set specific bits
flags |= 0b00001000; // set bit 3
// clear specific bits
flags &= ~0b00000100; // clear bit 2
// toggle specific bits
flags ^= 0b00000001; // toggle bit 0
// shifts as multiplication/division by powers of 2
int x = 5;
x << 1 == 10; // multiply by 2^1 = 2
x << 3 == 40; // multiply by 2^3 = 8
x >> 1 == 2; // divide by 2^1 = 2 (truncates)
// XOR swap algorithm
int a = 5, b = 10;
a ^= b; // a = a ^ b
b ^= a; // b = b ^ (a ^ b) = original a
a ^= b; // a = (a ^ b) ^ original a = original b
// check if number is power of 2
bool isPowerOf2 = (x != 0) && ((x & (x - 1)) == 0);
Best practices:
- Use bitwise operators for flags, permissions, and bit manipulation
- Use shifts for efficient multiplication/division by powers of 2
- Prefer named constants for bit masks
- Be careful with right shift on negative signed integers
- Use unsigned types for bitwise operations to avoid sign extension issues
12.6 Assignment Operators
- Assignment operators are assigning values to variables
- Therefore the left operand must always be a variable
| Operation | Operator | syntax | Example |
|---|---|---|---|
| Assignment | = | x = y | x = 3; x == 3; |
| Addition Assignment | += | x += y | x = 3; x += 4; x == 7; |
| Subtraction Assignment | -= | x -= y | x = 4; x -= 3; x == 1; |
| Multiplication Assignment | *= | x *= y | x = 3; x *= 4; x == 12; |
| Division Assignment | /= | x /= y | x = 3.0; x /= 2.0; x == 1.5; |
| Integer Division Assignment | /= | x /= y | x = 3; x /= 2; x == 1; |
| Modulo Assignment | %= | x %= y | x = 11; x %= 4; x == 3; |
| Bitwise AND Assignment | &= | x &= y | x = 0b01; x &= 0b11; x == 0b01; |
| Bitwise OR Assignment | │= | x │= y | x = 0b01; x │= 0b11; x == 0b11; |
| Bitwise XOR Assignment | ^= | x ^= y | x = 0b01; x ^= 0b11; x == 0b10; |
| Left Shift Assignment | <<= | x <<= y | x = 0b01; x <<= 1; x == 0b10; |
| Right Shift Assignment | >>= | x >>= y | x = 0b10; x >>= 1; x == 0b01; |
12.7 Ternary Operator
- The ternary operator is the only ternary (three-operand) operator in C++
- It provides a concise way to write simple if-else expressions
Syntax: condition ? value_if_true : value_if_false
// basic usage
int x = 3;
const char* answer = x > 10 ? "x is greater than 10" : "x is 10 or less";
// assign different values based on condition
int max = (a > b) ? a : b;
// can be used in function arguments
print((x > 0) ? "positive" : "non-positive");
// both branches must have compatible types
int result = condition ? 42 : 3.14; // OK: double promoted
auto value = flag ? 100 : "text"; // Error: incompatible types
// nested ternary (discouraged for readability)
int category = score >= 90 ? 1 :
score >= 80 ? 2 :
score >= 70 ? 3 : 4;
// equivalent if-else (more readable for complex logic)
int category;
if (score >= 90) category = 1;
else if (score >= 80) category = 2;
else if (score >= 70) category = 3;
else category = 4;
Best practices:
- Use for simple conditional assignments
- Avoid nesting ternary operators deeply (use if-else for complex logic)
- Add parentheses around condition for clarity:
(x > 0) ? a : b - Both branches should be simple expressions, not complex statements
- Consider using if-else if the ternary operator reduces readability
12.8 Comma Operator
- The comma operator
,evaluates multiple expressions sequentially - It returns the value of the rightmost expression
- It has the lowest precedence of all operators
// basic usage
int x = (a = 1, b = 2, a + b); // x = 3, a = 1, b = 2
// common use in for loops
for (int i = 0, j = 10; i < j; i++, j--) {
// i increments, j decrements each iteration
}
// multiple expressions evaluated left to right
int result = (printf("Hello"), 42); // prints "Hello", returns 42
// comma in function arguments is NOT the comma operator
func(a, b); // separate arguments, not comma operator
func((a, b)); // comma operator, only b is passed to func
Best practices:
- Main legitimate use is in for loop initialization/iteration
- Avoid in most other contexts as it reduces readability
- Parentheses are required to use comma operator in most contexts
13 Memory Management
- C++ requires manual memory management, in which the following memory locations are relevant
- Stack: Automatic storage for local variables (fast, limited size, auto-cleanup)
- Heap: Dynamic storage requiring manual allocation/deallocation (slower, larger)
13.1 Pointers
- Pointers are variables that store memory addresses of other variables
- They enable indirect access to values, dynamic memory allocation, and efficient passing of large objects
// declare uninitialized pointer
int* a;
// initialize null pointers (points to nothing)
int* b1 = nullptr; // modern C++
int* b2 = NULL; // C-style
int* b3 = 0; // old C++ style
// get memory address of variable
int c = 10;
int* pc = &c; // pc now holds the address of c
// dereference pointer to access/modify value
int value = *pc; // read value at address (value = 10)
*pc = 20; // modify value at address (c now = 20)
// pointer to const (cannot modify pointed-to value)
const int x = 5;
const int* ptr1 = &x; // pointer to const int
// *ptr1 = 10; // Error: cannot modify const value
ptr1 = &c; // can point to different address
// const pointer (cannot change address)
int* const ptr2 = &c; // const pointer to int
*ptr2 = 30; // can modify value
// ptr2 = &x; // Error: cannot change pointer address
// const pointer to const (neither value nor address can change)
const int* const ptr3 = &x;
// *ptr3 = 10; // Error: cannot modify value
// ptr3 = &c; // Error: cannot change address
// pointers to pointers (multiple levels of indirection)
int g = 1;
int* h = &g; // pointer to int
int** i = &h; // pointer to pointer to int
int j = **i; // dereference twice to get value
// pointer arithmetic (only valid with arrays/contiguous memory)
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // points to first element
int* ptr2 = ptr + 2; // points to third element (arr[2])
int value1 = *ptr; // 10
int value2 = *(ptr + 1); // 20
int value3 = ptr[2]; // 30 (subscript notation)
ptrdiff_t distance = ptr2 - ptr; // difference in elements (2)
// pointer comparison
if (ptr < ptr2) { } // valid for pointers to same array
if (ptr == nullptr) { } // check for null pointer
// void pointers (generic pointer type)
void* vptr = &c; // can point to any type
// int val = *vptr; // Error: cannot dereference void*
int* iptr = static_cast<int*>(vptr); // must cast before dereferencing
int val = *iptr;
// function pointers
int add(int a, int b)
{
return a + b;
}
int (*funcPtr)(int, int) = &add; // pointer to function
int result = funcPtr(3, 4); // call function through pointer
// alternative function pointer syntax
using BinaryOp = int(*)(int, int);
BinaryOp op = add;
int result2 = op(5, 6);
Best practices:
- Always initialize pointers
- Use
nullptrwhen pointers shouldn’t point to a valid address yet - Prefer
nullptroverNULLor0for type safety and clarity - Check for null before dereferencing
- Use
constcorrectly to express intent and prevent bugs - Prefer references over pointers when the value must always be valid
- Prefer smart pointers over raw pointers for ownership management
- Set pointers to
nullptrafter deleting to avoid dangling pointers
13.2 References
- References are aliases for existing variables
- Unlike pointers, references cannot be null, cannot be reassigned, and don’t require dereferencing
// initialize reference (must be initialized at declaration)
int x = 3;
int& y = x; // y is now an alias for x
// use reference (automatically dereferenced)
y = 5; // modifies x
int z = y; // reads x
// references cannot be reassigned
int a = 10;
y = a;
// const reference (cannot modify referenced value)
const int& ref = x;
int value = ref; // can read
// ref = 10; // Error: cannot modify through const reference
// reference to const can bind to temporary values
const int& temp = 42; // temporary lifetime extended
// int& temp2 = 42; // Error: non-const reference cannot bind to temporary
// reference parameters (avoid copying large objects)
struct LargeObject { int data[1000]; };
void process(const LargeObject& obj) { // passed by reference, not copied
// use obj without copying overhead
}
// reference return values (allow chaining)
int& getElement(int* arr, int index) {
return arr[index]; // return reference to array element
}
int array[5] = {1, 2, 3, 4, 5};
getElement(array, 2) = 10; // modify array[2] directly
// dangling reference (undefined behavior)
int& getDanglingRef() {
int local = 42;
return local; // Error: returns reference to local variable!
}
// int& bad = getDanglingRef(); // undefined behavior
Best practices:
- Use references instead of pointers when the value must always be valid (never null)
- Use
constreferences for function parameters to avoid unnecessary copies - Never return references to local variables
- Prefer references over pointers for cleaner syntax and safety
13.3 Dynamic Memory Allocation
- Dynamic memory is allocated on the heap at runtime, allowing flexible lifetimes and sizes
- The heap has much more memory than the stack but is slower to access
// allocate single object on heap
int* x = new int; // uninitialized
int* y = new int(10); // initialized to 10
int* z = new int{15}; // uniform initialization
// delete single object
delete y;
y = nullptr;
// allocate array on heap
int* arr = new int[10]; // uninitialized array
int* arr2 = new int[10](); // zero-initialized array
int* arr3 = new int[3]{1, 2, 3}; // initialized with values
// delete array
delete[] arr;
arr = nullptr;
// allocation failure handling
try {
int* huge = new int[1000000000000]; // may throw std::bad_alloc
} catch (const std::bad_alloc& e) {
// handle allocation failure
}
// construct object at specific address
#include <new>
alignedStorage buffer;
void* ptr = &buffer;
int* obj = new(ptr) int(42); // construct int at existing address
obj->~int(); // must manually call destructor
Best practices:
- Avoid
new[], usestd::vector,std::array, orstd::unique_ptr<T[]>instead - Always match
newwithdeleteandnew[]withdelete[] - Set raw pointers to
nullptrafter deletion
14 Control Flow Structures
- Control flow determines the order in which statements are executed in a program
- By default, statements execute sequentially from top to bottom
- Control flow structures alter this default execution order based on conditions, loops, or explicit jumps
14.1 Conditions
- Conditions allow branching of control flow to execute statements based on boolean expressions
#include <iostream>
int x = 10;
// basic if statement
if (x < 10)
{
std::cout << "x is less than 10" << std::endl;
}
// if-else statement
if (x < 10)
{
std::cout << "x is less than 10" << std::endl;
}
else
{
std::cout << "x is 10 or greater" << std::endl;
}
// if-else chain
if (x < 10)
{
std::cout << "x is less than 10" << std::endl;
}
else if (x > 10)
{
std::cout << "x is greater than 10" << std::endl;
}
else
{
std::cout << "x is 10" << std::endl;
}
// single-line if statement
if (x > 0) std::cout << "positive" << std::endl;
// if statement with initializer statement
if (int value = getValue(); value > 0)
{
// value is in scope here
std::cout << "Value: " << value << std::endl;
}
// value is out of scope here
// condition with declaration
if (auto ptr = getPointer())
{
// ptr is non-null here
ptr->doSomething();
}
Best practices:
- Always use braces
{}even for single-line statements to prevent errors - Keep conditions simple and readable, extract complex logic to named boolean variables
- Order conditions from most likely to least likely for better performance
- Avoid deep nesting and prefer early returns or guard clauses
- Use
ifwith initializers to limit variable scope
14.2 Loops
- Loops repeatedly execute a block of statements until a condition becomes false
#include <iostream>
#include <vector>
// while loop: condition checked before each iteration
int x = 0;
while (x < 10)
{
std::cout << x << std::endl;
x++;
}
// do-while loop: condition checked after each iteration
int y = 0;
do
{
std::cout << y << std::endl;
y++;
}
while (y < 10);
// for loop: initialization, condition, and increment in one line
for (int i = 0; i < 10; i++)
{
std::cout << i << std::endl;
}
// for loop with multiple variables
for (int i = 0, j = 10; i < j; i++, j--)
{
std::cout << i << " " << j << std::endl;
}
// infinite loop (requires break to exit)
for (;;)
{
if (shouldExit()) break;
doWork();
}
// for-each loop: iterate over containers and arrays
int arr[5] = {1, 2, 3, 4, 5};
for (int element : arr)
{
std::cout << element << std::endl;
}
// range-based for with auto type deduction
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
for (auto name : names)
{
std::cout << name << std::endl;
}
// range-based for with reference (avoid copying, allow modification)
std::vector<int> values = {1, 2, 3, 4, 5};
for (auto& value : values)
{
value *= 2; // modifies original elements
}
// range-based for with const reference (avoid copying, prevent modification)
for (const auto& name : names)
{
std::cout << name << std::endl; // efficient, no copy
}
// continue: skip remaining code in current iteration and start next iteration
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0) {
continue; // skip even numbers
}
std::cout << i << std::endl;
}
// break: exit loop immediately
for (int i = 0; i < 10; i++)
{
if (i == 5) {
break; // exit loop when i reaches 5
}
std::cout << i << std::endl; // prints 0 to 4
}
// for loop with initializer
for (auto values = getData(); auto& val : values)
{
std::cout << val << std::endl;
}
Best practices:
- Use
forloops when iteration count is known - Use
whileloops when condition is complex or iteration count is unknown - Use
do-whileloops when loop body must execute at least once - Prefer range-based for loops for iterating over containers
- Use
const auto&in range-based for loops to avoid unnecessary copies - Use
auto&in range-based for loops when modifying elements - Avoid modifying loop control variables inside the loop body
- Keep loop bodies simple; extract complex logic to functions
- Prefer
continueover nested if statements for clarity
14.3 Switches
- Switch statements provide multi-way branching based on the value of an integral expression
#include <iostream>
// basic switch with break statements
int x = 2;
switch (x)
{
case 0:
std::cout << "x is 0" << std::endl;
break;
case 1:
std::cout << "x is 1" << std::endl;
break;
case 2:
std::cout << "x is 2" << std::endl;
break;
default: // executes when no case matches
std::cout << "x is something else" << std::endl;
break; // optional in last case, but recommended for consistency
}
// intentional fall-through (executes multiple consecutive cases)
int countdown = 3;
switch (countdown)
{
case 3:
std::cout << "3..." << std::endl;
[[fallthrough]]; // attribute to indicate intentional fall-through
case 2:
std::cout << "2..." << std::endl;
[[fallthrough]];
case 1:
std::cout << "1..." << std::endl;
[[fallthrough]];
case 0:
std::cout << "Blast off!" << std::endl;
break;
default:
std::cout << "Invalid countdown value" << std::endl;
}
// switch with multiple cases for same code
char grade = 'B';
switch (grade)
{
case 'A':
case 'a':
std::cout << "Excellent!" << std::endl;
break;
case 'B':
case 'b':
std::cout << "Good!" << std::endl;
break;
case 'C':
case 'c':
std::cout << "Average" << std::endl;
break;
default:
std::cout << "Unknown grade" << std::endl;
}
// switch with initializer statement
switch (int value = getValue(); value)
{
case 1:
std::cout << "One" << std::endl;
break;
case 2:
std::cout << "Two" << std::endl;
break;
default:
std::cout << "Other: " << value << std::endl;
}
// value is out of scope here
// switch with variable declarations in cases
switch (x)
{
case 1:
{ // braces create scope for variable
int temp = x * 2;
std::cout << temp << std::endl;
break;
}
case 2:
{
int temp = x * 3; // same variable name, different scope
std::cout << temp << std::endl;
break;
}
default:
break;
}
Best practices:
- Always include
breakat the end of each case unless fall-through is intentional - Use
[[fallthrough]]attribute to document intentional fall-through - Always include a
defaultcase for completeness and debugging - Place
defaultcase last for readability - Use braces
{}in cases that declare variables - Prefer switch over long if-else chains for integral/enum comparisons
- Use enumerations with switch for type-safe value sets
- Enable compiler warnings for missing enum cases (
-Wswitch) in switches
14.4 Jumps
- Jump statements unconditionally transfer control to another part of the program
// basic goto usage
goto myLabel;
std::cout << "This is skipped" << std::endl;
myLabel:
std::cout << "Jumped here" << std::endl;
// goto cannot jump into scopes
goto skip;
{
int x = 10;
skip: // Error: jumping into scope
std::cout << x << std::endl;
}
// legitimate use case: breaking out of nested loops
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
if (found)
goto cleanup; // break both loops at once
}
}
cleanup:
// cleanup code here
// better alternative using flags
bool done = false;
for (int i = 0; i < 10 && !done; i++)
{
for (int j = 0; j < 10; j++)
{
if (found)
{
done = true;
break;
}
}
}
Best practices:
- Avoid
goto, use structured control flow instead - Prefer early returns, break, continue, or flags over
goto - Extract nested loops into functions to avoid needing
goto - Only consider
gotofor error cleanup in C-style code or performance-critical nested loops - If using
goto, always jump forward, never backward - Use descriptive labels that indicate the target purpose
14.5 Exception Handling
- Exceptions are errors that are thrown at runtime
- Exception handling provides a mechanism to respond to the runtime errors
-
Exceptions separate error handling code from normal program flow
- common standard exceptions:
std::logic_error: errors in program logicstd::invalid_argumentstd::domain_errorstd::length_errorstd::out_of_range
std::runtime_error: errors detectable at runtimestd::range_errorstd::overflow_errorstd::underflow_error
std::bad_alloc: memory allocation failurestd::bad_cast: failed dynamic_cast with references
#include <iostream>
#include <stdexcept>
#include <string>
// throw exception explicitly
throw std::runtime_error("Something went wrong");
// create custom exception
class CustomException : public std::exception
{
private:
std::string message;
public:
CustomException(const std::string& msg) : message(msg) {}
const char* what() const noexcept override
{
return message.c_str();
}
};
// basic try-catch block
try
{
int x = 10;
if (x < 0)
throw std::runtime_error("Negative value not allowed");
std::cout << "Value: " << x << std::endl;
}
catch (const std::runtime_error& e)
{
std::cout << "Runtime error: " << e.what() << std::endl; // access exception message
}
// catch multiple exception types
try
{
// code that may throw different exceptions
throw std::invalid_argument("Invalid input");
}
catch (const std::invalid_argument& e)
{
std::cout << "Invalid argument: " << e.what() << std::endl;
}
catch (const std::runtime_error& e)
{
std::cout << "Runtime error: " << e.what() << std::endl;
}
catch (const std::exception& e) // base class catches all standard exceptions
{
std::cout << "Exception: " << e.what() << std::endl;
}
catch (...) // catches any exception, including non-standard types
{
std::cout << "Unknown exception caught" << std::endl;
}
// rethrow exceptions
try
{
try
{
throw std::runtime_error("Original error");
}
catch (const std::runtime_error& e)
{
std::cout << "Caught and rethrowing..." << std::endl;
throw; // rethrow the same exception
}
}
catch (const std::runtime_error& e)
{
std::cout << "Caught rethrown exception: " << e.what() << std::endl;
}
// function promises not to throw exceptions
int safeDivide(int a, int b) noexcept
{
return (b != 0) ? a / b : 0;
}
// conditional noexcept
template <typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a))))
{
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
{
std::cout << "Error: " << e.what() << std::endl;
}
// function try blocks for guards in constructors
class Resource
{
public:
Resource(int value)
try : member(value)
{
if (value < 0)
throw std::invalid_argument("Negative value");
}
catch (const std::exception& e)
{
std::cout << "Constructor exception: " << e.what() << std::endl;
throw; // must rethrow in constructor catch block
}
private:
int member;
};
Best practices:
- Catch exceptions by const reference to avoid object slicing and unnecessary copies
- Order catch blocks from most specific to most general exception types
- Use standard exception classes when appropriate
- Derive from
std::exceptionfor custom exceptions - Implement
what()in custom exceptions to provide error descriptions - Use
throw;to rethrow the original exception (preserves exception type) - Use
noexceptfor functions that guarantee no exceptions (enables compiler optimizations) - Don’t throw exceptions in destructors (causes program termination if during stack unwinding)
- Don’t catch exceptions you cannot handle and let them propagate to higher levels
- Avoid using
catch (...)unless you rethrow or log, as it hides errors - Don’t use exceptions for normal control flow as they are for exceptional conditions
- Document which exceptions a function may throw
- Consider using
std::optional,std::expected, or error codes for expected error cases
15 Functions
- Functions are reusable blocks of code that perform specific tasks and can take parameters and return values
- Functions must be declared before use (forward declaration) and can be defined separately
// forward declaration (prototype)
int sum(int x, int y);
// definition
int sum(int x, int y)
{
return x + y;
}
// function with no parameters or return value
void printMessage()
{
std::cout << "Hello" << std::endl;
}
// function call
int result = sum(10, 4); // result == 14
printMessage();
// pass by value (copies the argument)
void increment(int x)
{
x++; // only modifies local copy
}
// pass by reference (allows modification of original)
void increment(int& x)
{
x++; // modifies original variable
}
// pass by const reference (read-only, no copy overhead)
void printVector(const std::vector<int>& vec)
{
for (int val : vec) {
std::cout << val << " ";
}
}
// pass by pointer (can be nullptr)
void maybeModify(int* ptr)
{
if (ptr != nullptr) {
*ptr = 42;
}
}
// same function name, different parameter types or counts (function overloading)
int add(int x, int y)
{
return x + y;
}
double add(double x, double y)
{
return x + y;
}
int add(int x, int y, int z)
{
return x + y + z;
}
add(5, 3); // calls int version, returns 8
add(5.5, 3.2); // calls double version, returns 8.7
add(1, 2, 3); // calls three-parameter version, returns 6
// default parameters (must be rightmost and declared only once)
int power(int base, int exponent = 2);
int power(int base, int exponent)
{
int result = 1;
for (int i = 0; i < exponent; i++) {
result *= base;
}
return result;
}
power(3); // uses default, returns 9 (3^2)
power(3, 3); // returns 27 (3^3)
// multiple return values using std::pair or std::tuple
std::pair<int, int> divmod(int dividend, int divisor)
{
return {dividend / divisor, dividend % divisor};
}
auto [quotient, remainder] = divmod(17, 5); // structured binding
// return by reference
int& getElement(std::vector<int>& vec, size_t index)
{
return vec[index]; // returns reference to element
}
// trailing return type for better readability
auto multiply(int x, int y) -> int
{
return x * y;
}
// inline suggests the compiler to replace function calls with the function body
inline int max(int a, int b)
{
return (a > b) ? a : b;
}
// constexpr functions can be evaluated at compile time
constexpr int factorial(int n)
{
return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr int result = factorial(5); // computed at compile time
int runtime = factorial(n); // can still be called at runtime
// constexpr functions must have literal return type
constexpr double circleArea(double radius)
{
return 3.14159 * radius * radius;
}
// noexcept indicates a function won't throw exceptions
int safeDivide(int a, int b) noexcept
{
return (b != 0) ? a / b : 0;
}
Best practices:
- Use
constreferences for read-only parameters to avoid unnecessary copies - Pass small types by value and large types by const reference
- Use
noexceptfor functions that won’t throw exceptions to enable optimizations - Prefer early returns to reduce nesting depth
- Avoid functions with too many parameters (consider grouping into a struct)
- Mark compile-time computable functions as
constexprwhen possible - Use
[[nodiscard]]attribute for functions whose return value shouldn’t be ignored - Document preconditions, postconditions, and exceptions in comments
- Prefer references over pointers when the argument must always be valid
16 Lambda Expressions
- Lambda expressions are functions that can be used as expressions instead of statements
- Therefore they can be passed as values to variables and functions
- Thereby they don’t have a data type associated with them and require the
autokeyword
- Lambda expressions can capture identifiers in their scope with
=and&- Thereby
=is used to make identifiers in the current scope available to the statements in the lambda function by copying their values - Thereby
&is used to make identifiers in the current scope available to the statements in the lambda function per reference - These can be used as prefixes in combination with identifiers to specify only them
- Thereby multiple of these can be used in a comma separated list
- Thereby
// define lambda expression
auto add = [](int a, int b) -> int { return a + b; };
// call lambda expressions
add(3, 4) == 7;
[](int a, int b) -> int { return a + b; }(3, 4) == 7;
// use lambda functions with captures
int x = 3;
int y = 4;
int z = 5;
[=, &z]() -> void { z = x + y; }();
17 Classes
- Classes encapsulate variables and functions inside a single data structure
- Therefore they provide a uniform interface for users
- Thereby their variables are called properties and their functions are called methods
17.1 Creation
#include <iostream>
// declare class without its members
class Foo;
// define class with its members
class FooBar
{
// properties (variables) of class
int x = 10;
// methods (functions) of class
void do()
{
std::cout << "Something!" << std::endl;
}
};
// declare class with its members
class Bar
{
int x;
void do();
};
// define members of class
Bar::x = 10;
Bar::void do()
{
std::cout << "Something!" << std::endl;
}
Best practices:
- Identifiers of classes should be written in Pascal case
- Properties should be initialized to avoid the usage of undefined members
17.2 Instantiation
- Classes can be instantiated into objects, which have their class as data type
- These objects have their own set of members that are defined in their class
- Inside classes the current object can be referenced with the
thispointer variable
#include <iostream>
// define class with constructor
class Foo
{
public:
int x;
int y;
// constructor method that will be called to instantiate objects
Foo(int in1, int in2)
{
this->x = in1;
this->y = in2;
}
};
// initialize object
Foo foo(3, 4);
// define class with default constructor
class Bar
{
public:
int x;
int y;
// constructor method that will be called when no constructor was called explicitly
Bar()
{
this->x = 0;
this->y = 0;
}
};
// declare object (calls default constructor)
Bar bar;
// define object
bar = Bar(3, 4);
// define class with initialization list
class FooBar
{
public:
int x;
int y;
FooBar(int in1, int in2)
// initialization list that will initialize object with passed values
// (otherwise object will be initialized with default values first)
// (this also allows to set constant properties on construction)
: x(in1), y(in2)
{}
};
// define deconstructors
class BarFoo
{
public:
int x;
int y;
BarFoo(int in1, int in2)
: x(in1), y(in2)
{}
// destructor method that will be called when the object is deleted
~BarFoo()
{
std::cout << "Bye!" << std::endl;
}
};
// access members of object
foo.x = 10;
foo.do();
Best practices:
- Identifiers of objects should be written in camel case
- Constructors and deconstructors should always be defined, even if they are only standard constructors and deconstructors
- Constructors should always use initialization lists to assign arguments to properties
17.3 Const Correctness
- Methods can be marked as constant to signify that they don’t mutate their object
- The marking of all non-mutating methods as constant is called const correctness
- Constant objects and constant references can only call constant methods
class Math
{
public:
int value = 0;
int add(int x, int y) const
{
return x + y;
}
};
// method can be called on constant object because it is constant itself
const Math m();
m.add(1, 2);
// also applies to const reference to const object
const Math& ref = m;
ref.add(1, 2);
Best practices:
- Methods should always be defined as constant when they don’t mutate their object
17.4 Access Modifiers
- The visibility of class members from outside their class can be configured with access modifiers
- These are used like labels to apply them to every member after them
- The following access modifiers do exist:
private: Members are only visible inside their class (active per default)protected: Members are only visible inside their class and derivations of that classpublic: Members are visible everywhere
- Classes can define friend functions and classes that can access their private and protected members
- These aren’t inherited by derived classes
// apply access modifiers
class Foo
{
private: // can be omitted because its active per default
int x;
void do();
protected:
double y:
void some();
public:
char z:
void thing();
};
// define friends of class
class Bar
{
private:
int x = 0;
int y = 0;
public:
friend void barfoo(int in); // define signature of friend function
friend class FooBar; // define signature of friend class
};
void barfoo(int in) {} // friend function of `Bar`
class FooBar; // friend class of `Bar`
Best practices:
- Friends of classes should be avoided to ensure encapsulation
17.5 Inheritance
- Classes can be derived from other classes as their base to inherit all of their members
- Derivations can be modified with access modifiers to configure the access modifiers for all derived members
public: Doesn’t change the access modifiers of derived membersprotected: Changes derived public members to protected membersprivate: Changes derived members to private members
- Derived classes are treated as the same data type as their base class, which is why they can be substituted by them in variables and function calls
- Thereby they’re losing all members that aren’t part of the substituted base class
- This can be bypassed with pointers for base classes to which derivations are assigned instead
- Thereby they’re losing all members that aren’t part of the substituted base class
// define derivation
class Foo
{
public:
int x;
Foo(int in)
: x(in)
{}
};
class Bar : public Foo
{
public:
int y;
// call and pass arguments to constructor of base class
Bar(int in1, int in2)
: Foo(in1), y(in2)
{}
};
// override derived methods
class Foobar
{
public:
// define method as overridable
virtual void do();
};
class Barfoo : public Foobar
{
public:
// override derived method
void do() override
{}
};
// substitute base class with derived class
Foo* foobar = new Bar(3, 4);
Best practices:
- Derivations should use the access modifier
publicto not modify the access modifiers of their derived members
18 Operation Overloading
- Operators are defined as functions that are called upon the usage of their operator with their operands as arguments
- Therefore they can be overloaded for different operands like other functions
- Operators can be overloaded inside classes as methods
- Thereby the object itself is used as first parameter automatically and therefore the method only takes the right operand as argument
// overload binary operator
struct Foo
{
int x;
int y;
};
Foo operator +(const Foo& a, const Foo& b) // pass left and right operand to operation
{
return Foo{a.x + b.x, a.y + b.y}; // return result of operation
}
// overload unary operators
struct Bar
{
int x;
int y;
};
Bar& operator ++(Bar& in) // pass only operand
{
in.x++;
in.y++;
return in;
}
Bar operator ++(Bar& in, int) // dummy parameter for non-existing right operand
{
Bar temp = in;
in.x++;
in.y++;
return temp;
}
// overload operator as method
class Foobar
{
public:
int x = 0;
int y = 0;
Foobar(int x, int y)
: x(x), y(y)
{}
Foobar operator +(const Foobar& in) // pass only right operand, left operand is object itself
{
return Foobar(this->x + in.x, this->y + in.y);
}
};
Best practices:
- Dummy parameters should be keywords of data types to guarantee that they aren’t used
- Operator overloads should be used to make reference data types compatible with operators
- Operator Overloads for classes should be friend functions or methods of these so they can access their private members
19 Standard Template Library (STL)
- The standard template library (STL) is C++’s standard library and provides additional data types, classes and functions
- Its elements must be imported with
includedirectives and are all part of thestdnamespace
- Its elements must be imported with
19.1 Containers
- The STL provides multiple container data structures as classes
19.1.1 C++-Arrays
- C++-arrays are static sized wrappers for raw arrays
#include <array>
// declare C++-array
std::array<int, 5> foo;
// initialize C++-arrays
std::array<int, 5> bar = {1, 2, 3, 4, 5};
// access elements of C++-array
bar[1] = 1;
bar.at(3) == 4; // can throw std::out_of_range exception
bar.front() == 1;
bar.back() == 5;
// get size of C++-array
bar.size() == 5;
// get pointer to raw arrays
bar.data();
19.1.2 C++-Strings
- C++-Strings are dynamically sized wrappers for raw strings
- Thereby they have a capacity to store the wrapped string and reallocate their memory when the string extends that former capacity
#include <string>
// declare C++-string
std::string letters;
// initialize C++-string
std::string str = "Hello";
// define C++-string
letters = "test";
letters.assign("testing");
letters.assign(5, 'x'); letters == "xxxxx";
// construct c++-strings
std::string foo("Foo Bar");
std::string bar(5, 'a'); letters == "aaaaa";
// get length of C++-string
std::string len = "Hello";
len.length() == 5;
len.size() == 5;
len.empty() == false;
// get substring from C++-string
std::string subStr = "Hello";
subStr.substr(2, 3) == "llo";
// get starting positions of substring in C++-string
std::string startPos = "Hello";
startPos.find("ell") == 1;
startPos.find("xo") == std::string::npos;
std::string::npos == -1;
// replace substring in C++-string
std::string replacing = "Hello, World!";
replacing.replace(7, 5, "There") == "Hello, There!";
// remove substring from C++-string
std::string removal = "HI!!!";
removal.erase(2, 3) == "HI";
removal.erase() == "";
// insert substring into C++-string
std::string inserting = "Hello";
inserting.insert(5, ", World!") == "Hello, World!";
// concatenate strings to C++-strings
std::string concatenating = "Hello";
concatenating.append(", World!") == "Hello, World!";
concatenating.push_back('!') == "Hello!";
// comparing c++-strings
comparing.compare("Hello") == 0;
std::string comparing = "Hello";
comparing == "world" == false;
comparing < "world" == true;
comparing >= "world" == false;
// swap C++-strings
std::string first = "second";
std::string second = "first";
first.swap(second);
// manage memory of c++-string
std::string memory = "Hello";
memory.capacity(); // get capacity of C++-string in bytes
memory.max_size(); // get maximal possible capacity of C++-string in bytes
memory.reserve(100); // set capacity of C++-string in bytes
// get pointer to inner raw string of C++-string
std::string wrapped = "Hello";
const char* raw = wrapped.c_str();
Best practices:
- C++-strings should always be used instead of raw strings due to their enhanced security and useability
19.1.3 Vectors
- Vectors are dynamically sized wrappers for raw arrays
- Thereby they have a capacity to store the wrapped array and reallocate their memory when the array extends that former capacity
#include <vector>
// declare vector
std::vector<int> foo;
// initialize vector
std::vector<int> bar = {1, 2, 3, 4, 5};
// construct vectors
std::vector<int> foobar(5, 0);
// append element to vector
foobar.push_back(0);
// remove last element from vector and return it
foobar.pop_back();
// remove all elements from vector
foobar.clear();
// access elements of vectors
bar[1] = 1;
bar.at(3) == 4; // can throw std::out_of_range exception
bar.front() == 1;
bar.back() == 5;
// get number of elements in vector
bar.size() == 5;
// manage memory of vector
foo.capacity(); // get capacity of vector in elements
foo.max_size(); // get maximal possible capacity of vector in elements
foo.reserve(100); // set capacity of vector in elements
Best practices:
- C++-arrays should be used instead of vectors whenever practicable due to more efficient memory management
- Vectors should be created using initializations or their constructor to allocate needed memory once and initialize their values
- Memory for vectors should be allocated preparatory with the method
reserveto reduce the number of memory allocations
19.1.4 Sets
- Sets are unordered data structures that can’t contain duplicates
- Every containing duplicate is discarded
#include <set>
// declare set
std::set<int> foo;
// initialize set
std::set<int> bar = {1, 2, 3, 4, 5};
// access elements of set
bar.find(1) == 1;
// add elements to set
foobar.insert(0);
// remove element from set
foobar.erase(1);
19.1.5 Pairs
- Pairs are data structures to store exactly two values
#include <utility>
// declare pair
std::pair<char, int> duo;
// initialize pairs
std::pair<char, int> foo = {'a', 1};
std::pair<char, int> bar = std::make_pair('b', 2);
// access elements of pair
foo.first == 'a';
foo.second == 1;
19.1.6 Maps
- Maps are unordered lists of key-value pairs
- Thereby their key-value pairs are implemented as pairs
- In these the first value is the map’s key
- In these the second value is the map’s value of the key
- Thereby their keys must be unique
- Thereby their key-value pairs are implemented as pairs
#include <map>
// declare map
std::map<char, int> foo;
// initialize map
std::map<char, int> bar = {{'a', 1}, {'b', 2}, {'c', 3}};
// access elements of map
bar['a'] == 1;
bar['d'] = 4;
bar['d'] == 4;
bar.find('a'); // get iterator for map to specified key-value pair
// add elements to map
foo.insert(std::make_pair('a', 1));
foo.emplace('b', 2);
foo['c'] = 3;
// remove elements from maps
foo.erase('a'); // specify key of key-value pair to remove
foo.clear(); // remove all elements
19.1.7 Iterators
- Containers use iterator objects to iterate through them
- These are wrappers for pointers pointing to elements of these containers
#include <algorithm>
#include <array>
// get iterators to elements of container
std::array arr<int> = {1, 2, 3, 4, 5};
std::array::iterator firstElement = arr.begin();
std::array::iterator lastElement = arr.end();
// remove elements in container with iterators
arr.erase(firstElement, lastElement);
// iterate through container with iterator
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it)
{
int element = *it;
}
// apply function to all elements of a container
int doubleIt(int num)
{
return 2 * num;
}
std::vector<int> evens = {2, 4, 6, 8};
std::transform(
evens.begin(), // iterator to start of apply
evens.end(), // iterator to end of apply
evens.begin(), // iterator to where to store results of apply
doubleIt // function to apply
);
// remove elements from containers while iterating through them based on predicate function
bool isVocal(char c)
{
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}
std::vector<char> letters = {'a', 'b', 'c', 'd', 'e', 'f'};
// push removed elements to end and return iterator to first pushed element
auto end = std::remove_if(letters.begin(), letters.end(), isVocal);
letters.erase(end, letters.end()); // remove pushed elements
// sort elements in containers
std::vector<int> rand = {5, 1, 8, 7, 0};
std::sort(rand.begin(), rand.end()); // ascending sort
std::sort(rand.begin(), rand.end(), [](int a, int b) { return a < b; }); // custom ordering
Best practices:
- The function
remove_ifin combination with the methoderaseshould be used to remove elements of containers to avoid iterator invalidation
19.2 IO
19.2.1 Terminal
#include <iostream>
#include <string>
#include <fstream>
// output string representations of values to stdout
std::cout << "Hello" << ", " << "What's your Name? " << std::endl; // > Hello, What's your Name?\n
std::cout << "Numbers: " << 1 << ", " << 2 << ", " << 3 << std::endl; // > Numbers: 1, 2, 3\n
// output chunks of string representations to the terminal
std::cout.width(3); // set width of chunks
std::cout << "1234567" << std::endl; // > 123\n456\n7 \n
// input whitespace separated tokens from stdin
std::string first, last;
std::cin >> first >> last;
// input entire line from stdin
std::string input;
std::getline(std::cin, input); // save read line into variable
// output string representations to stderr
std::cerr << "Error " << 404 << ": Not Found" << std::endl; // > Error 404: Not Found\n
Best practices:
endlshould be used instead of the\ncharacter for system independent newlines- Error messages should be printed with
cerr
19.2.2 Files
#include <iostream>
#include <string>
// output string representations of values to file
std::ofstream fout("path/to/file.txt"); // create output stream object to file
fout << "Hello" << ", " << "What's your Name? " << std::endl; // > Hello, What's your Name?\n
fout << "Numbers: " << 1 << ", " << 2 << ", " << 3 << std::endl; // > Numbers: 1, 2, 3\n
// input whitespace separated tokens from file
std::ifstream fin1("path/to/file.txt"); // create input stream object to file
std::string first, last;
fin1 >> first >> last;
// input entire line from file
std::ifstream fin2("path/to/file.txt"); // create input stream object to file
std::string input;
std::getline(fin2, input); // save read line into variable
// check whether file stream is open
std::ifstream file("path/to/file.txt");
file.is_open() == true; // may be false if opening failed
// close file stream
file.close();
Best practices:
- File stream objects should be checked for successful opening with the
is_openmethod - File stream objects should be closed with the
closemethod before their deletion
19.2.3 Manipulators
- Output streams can be manipulated by inserting stream manipulator objects into them
- These manipulate the entire stream from the point of their insertion until another inserted manipulator cancels them out
#include <iostream>
#include <iomanip>
// flush stdout buffer
std::cout << "Hello, " << std::flush;
// insert OS independent newline
std::cout << "Line 1" << std::endl;
std::cout << "Line 2" << std::endl;
// set base of numbers to decimal, octal and hexadecimal
int number = 255;
std::cout << std::dec << number << std::endl; // > 255
std::cout << std::oct << number << std::endl; // > 377
std::cout << std::hex << number << std::endl; // > ff
// set base of numbers to anything
std::cout << std::setbase(10) << number << std::endl; // > 255
std::cout << std::setbase(8) << number << std::endl; // > 377
std::cout << std::setbase(16) << number << std::endl; // > ff
// set notation of real numbers to scientific notation
double pi = 3.14159;
std::cout << std::scientific << pi << std::endl;
// output real number with a fixed number of 3 decimal places
std::cout << std::fixed << std::setprecision(3) << pi << std::endl;
// output real number with a total of 5 significant digits (default format)
std::cout << std::defaultfloat << std::setprecision(5) << pi << std::endl;
Best practices:
- Flushing of output streams should be used carefully due to potential performance losses
19.3 System
#include <cstdlib>
#include <ctime>
// interrupt program with custom status code
exit(1);
// get system time
std::time_t currentTime = std::time(nullptr);
19.4 Math
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <random>
// get absolute value (available for every number type)
abs(-34.5) == 34.5;
abs(17) == 17;
// raise to power
double power1 = std::pow(10.0, 3.0);
double power2 = std::pow(10.0, 3.0);
// calculate square root
double square1 = std::sqrt(10.0);
double square2 = std::sqrt(10.0);
// set system seed for random number generation
srand(42); // set constant seed to always get same random number for reproducibility
srand(time(0)); // set ever changing seed to always get different random numbers
// generate random signed integer
int random = rand();
// modern random number generation engine and distribution
std::mt19937 rng(42); // fixed seed for reproducibility
std::uniform_int_distribution<int> dist(1, 6);
int dice = dist(rng); // 1..6
// clamp a number between a lower and upper bound (available for every number type)
double min = 10.0;
double max = 20.0;
std::clamp(9.5, min, max) == 10.0;
std::clamp(45.1, min, max) == 20.0;
std::clamp(14.87, min, max) == 14.87;
Best practices:
- The argument for the
srandfunction should be thetimefunction to set an ever changing system seed - The
srandfunction should only be used once with the same argument at the start of the program to avoid conflicting behaviors
19.5 Character Manipulation
#include <cctype>
// manipulate characters
std::tolower('A') == 'a';
std::toupper('a') == 'A';
// check character properties
std::islower('a') == true;
std::isupper('A') == true;
std::isalpha('a') == true;
std::isdigit('1') == true;
std::isalnum('a') == true;
std::isspace(' ') == true;
19.6 Smart Pointers
- Smart pointers are wrappers for raw pointers
- These are keeping track of their pointed values and references to them from other smart pointers
- These are managing the creation and deletion of their pointed values
Best practices:
- Smart pointers should always be used instead of pointers and references due to their enhanced security and usability
- Use raw pointers and references only for non-owning access
- Avoid
new/deletefor ownership.
19.6.1 Shared Pointers
- Shared pointers are smart pointers that are keeping track of all instances of them pointing to the same memory address
- Thereby the pointed value is deleted when all shared pointers pointing to it are deleted
#include <memory>
// initialize shared pointers
std::shared_ptr<int> foo(new int(1));
std::shared_ptr<int> bar = std::make_shared<int>(2);
// dereference shared pointer
*foo;
// declare and define shared pointer
std::shared_ptr<int> foobar; // null pointer
foobar = std::shared_ptr<int>(new int(3));
Best practices:
- Use shared pointers only when shared ownership is required
- Shared pointers should be initialized with the
make_sharedfunction due to their enhanced performance and security