Back to blog

Phase 1: C++ Fundamentals - Getting Started with Modern C++

cppc++programmingfundamentalssystems
Phase 1: C++ Fundamentals - Getting Started with Modern C++

Welcome to Phase 1

Welcome to the first phase of your Modern C++ learning journey! If you're coming from another programming language—Python, Java, Go, JavaScript, or any other—you'll find many familiar concepts here. The goal of this phase is to get you comfortable with Modern C++ syntax and understand how C++ approaches fundamental programming concepts.

This is not a "what is a variable?" tutorial. Instead, we'll focus on C++-specific syntax, conventions, and patterns that differentiate it from other languages you may know. We'll write Modern C++ from day one, avoiding legacy patterns that plague older codebases.

Time commitment: 1-2 weeks, 1-2 hours daily Prerequisite: Programming experience in any language

What You'll Learn

By the end of Phase 1, you'll be able to:

✅ Set up a complete C++ development environment
✅ Understand C++ compilation model (preprocessor, compiler, linker)
✅ Use C++ type system effectively
✅ Write modern code with auto and decltype
✅ Understand references vs pointers
✅ Use const and constexpr correctly
✅ Write functions with proper signatures
✅ Work with namespaces
✅ Handle basic I/O and strings


Setting Up Your Environment

1. Choose and Install a Compiler

C++ is a compiled language. You need a compiler to turn your source code into executable programs.

Recommended Compilers:

CompilerPlatformNotes
GCC (g++)Linux, macOS, WindowsMost popular, excellent standards support
Clang (clang++)Linux, macOS, WindowsExcellent error messages, fast
MSVCWindowsBest Windows integration

Installation by Platform:

Linux (Ubuntu/Debian):

sudo apt update
sudo apt install build-essential  # Installs GCC
# Or for Clang:
sudo apt install clang

macOS:

# Install Xcode Command Line Tools (includes Clang)
xcode-select --install
 
# Or install GCC via Homebrew
brew install gcc

Windows:

  • MSVC: Install Visual Studio Community with "Desktop development with C++"
  • GCC: Install MSYS2 and run pacman -S mingw-w64-x86_64-gcc
  • Clang: Install via Visual Studio or LLVM installer

Verify installation:

# GCC
g++ --version
 
# Clang
clang++ --version
 
# MSVC (in Developer Command Prompt)
cl

2. Choose an IDE or Editor

Visual Studio Code (Recommended for learning):

  • Install VS Code
  • Install extensions: "C/C++" and "CMake Tools"
  • Lightweight, cross-platform, free

CLion (Recommended for professional use):

  • CLion by JetBrains
  • Best-in-class C++ IDE
  • Paid (free for students)

Visual Studio (Windows):

  • Best Windows integration
  • Excellent debugger
  • Free Community edition

Other options:

  • Qt Creator - Great for Qt development
  • Vim/Neovim - With proper plugins
  • Eclipse CDT - Free, cross-platform

3. Your First C++ Program

Create a file named hello.cpp:

#include <iostream>
 
int main() {
    std::cout << "Hello, Modern C++!" << std::endl;
    return 0;
}

Compile and run:

# GCC
g++ -std=c++20 -o hello hello.cpp
./hello
 
# Clang
clang++ -std=c++20 -o hello hello.cpp
./hello
 
# MSVC (Developer Command Prompt)
cl /std:c++20 hello.cpp
hello.exe

What's happening:

  • #include <iostream> - Includes the I/O stream library (preprocessor directive)
  • int main() - Entry point for C++ programs (must return int)
  • std::cout - Standard output stream (from namespace std)
  • << - Stream insertion operator
  • std::endl - Newline + flush buffer
  • return 0 - Success exit code (0 means success)

4. Compiler Flags for Modern C++

Always use these flags during development:

# GCC/Clang recommended flags
g++ -std=c++20 -Wall -Wextra -Wpedantic -Werror main.cpp -o main
 
# What they mean:
# -std=c++20    Use C++20 standard (or c++17, c++23)
# -Wall         Enable "all" common warnings
# -Wextra       Enable extra warnings
# -Wpedantic    Enforce strict standard compliance
# -Werror       Treat warnings as errors (highly recommended!)
 
# MSVC equivalent
cl /std:c++20 /W4 /WX main.cpp

Pro tip: Always compile with warnings as errors (-Werror or /WX). This prevents bad habits and catches bugs early.


Understanding the Compilation Model

Unlike interpreted languages (Python, JavaScript), C++ has a multi-stage compilation process:

The Build Process

Source Code (.cpp) → Preprocessor → Compiler → Assembler → Linker → Executable

1. Preprocessing (#include, #define, #ifdef):

  • Textual substitution
  • Header files are literally copy-pasted
  • Macros are expanded

2. Compilation:

  • Syntax and semantic analysis
  • Type checking
  • Generates object files (.o or .obj)

3. Linking:

  • Combines object files
  • Resolves external references
  • Produces final executable

Example:

# See preprocessor output
g++ -E hello.cpp -o hello.i
 
# Compile to object file (no linking)
g++ -c hello.cpp -o hello.o
 
# Link object file to executable
g++ hello.o -o hello

Header Files vs Source Files

C++ separates declarations (headers) from definitions (source files):

// math_utils.hpp (header file)
#ifndef MATH_UTILS_HPP
#define MATH_UTILS_HPP
 
int add(int a, int b);  // Declaration
 
#endif
// math_utils.cpp (source file)
#include "math_utils.hpp"
 
int add(int a, int b) {  // Definition
    return a + b;
}
// main.cpp
#include "math_utils.hpp"
#include <iostream>
 
int main() {
    std::cout << add(5, 3) << std::endl;  // Uses declaration
    return 0;
}

Compile with multiple files:

g++ -std=c++20 main.cpp math_utils.cpp -o program

Note: C++20 introduces modules as a modern alternative to headers. We'll cover them in the Modern Features post.


C++ Type System

C++ is statically typed and strongly typed. Every variable has a fixed type at compile time.

Fundamental Types

#include <cstdint>  // For fixed-width types
 
// Integers
int count = 42;           // Usually 32 bits
short small = 100;        // At least 16 bits
long bigger = 100000L;    // At least 32 bits
long long huge = 10000000000LL;  // At least 64 bits
 
// Fixed-width integers (recommended for portability)
int8_t tiny = 127;        // Exactly 8 bits
int16_t medium = 32767;   // Exactly 16 bits
int32_t standard = 42;    // Exactly 32 bits
int64_t large = 9223372036854775807LL;  // Exactly 64 bits
 
// Unsigned variants
unsigned int positive = 100u;
uint32_t alsoPositive = 100u;
 
// Floating-point
float price = 19.99f;     // 32-bit (note 'f' suffix)
double precise = 3.14159265358979;  // 64-bit (most common)
long double veryPrecise = 3.14159265358979323846L;  // Extended precision
 
// Character
char letter = 'A';        // 8 bits
wchar_t wide = L'Ω';      // Wide character
char16_t utf16 = u'€';    // UTF-16 character
char32_t utf32 = U'🎉';   // UTF-32 character
char8_t utf8 = u8'A';     // UTF-8 character (C++20)
 
// Boolean
bool isActive = true;     // true or false

Key differences from other languages:

  • int size is platform-dependent (use int32_t for guarantees)
  • char is a numeric type (can do arithmetic)
  • No implicit boolean conversion in conditionals (must be explicit)

Type Sizes and Limits

#include <iostream>
#include <limits>
#include <cstdint>
 
int main() {
    std::cout << "int size: " << sizeof(int) << " bytes\n";
    std::cout << "int min: " << std::numeric_limits<int>::min() << "\n";
    std::cout << "int max: " << std::numeric_limits<int>::max() << "\n";
 
    std::cout << "double size: " << sizeof(double) << " bytes\n";
    std::cout << "double min: " << std::numeric_limits<double>::lowest() << "\n";
    std::cout << "double max: " << std::numeric_limits<double>::max() << "\n";
 
    return 0;
}

Type Inference with auto

Modern C++ supports type inference with auto (C++11):

auto count = 42;        // int
auto price = 19.99;     // double (NOT float!)
auto letter = 'A';      // char
auto message = "Hello"; // const char* (NOT std::string!)
auto flag = true;       // bool
 
// auto is particularly useful for complex types
std::vector<std::pair<std::string, int>> data;
// Instead of:
std::vector<std::pair<std::string, int>>::iterator it1 = data.begin();
// Write:
auto it2 = data.begin();  // Much cleaner!

When to use auto:

// Good: Type is obvious from right side
auto name = std::string("Alice");
auto numbers = std::vector<int>{1, 2, 3};
auto result = someFunction();  // If return type is clear from context
 
// Good: Type is complex
auto it = container.begin();
auto [key, value] = somePair;  // Structured bindings (C++17)
 
// Avoid: Type is not obvious
auto x = getValue();  // What type is this?
auto y = a + b;       // Depends on types of a and b

decltype - Type Deduction

decltype deduces the type of an expression without evaluating it:

int x = 42;
decltype(x) y = 100;  // y is int
 
double pi = 3.14;
decltype(pi) e = 2.71;  // e is double
 
// Useful with auto for complex return types
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}

nullptr - The Null Pointer

Always use nullptr (C++11), never NULL or 0:

int* ptr = nullptr;  // Modern way (type-safe)
// int* ptr = NULL;  // Old way (just a macro for 0)
// int* ptr = 0;     // C way (ambiguous)
 
void process(int value);
void process(int* ptr);
 
process(0);        // Calls process(int) - ambiguous!
process(NULL);     // Might call process(int) - problematic!
process(nullptr);  // Calls process(int*) - correct!

Variables and Constants

Variable Declaration and Initialization

C++ offers multiple ways to initialize variables:

// Copy initialization (pre-C++11)
int a = 42;
std::string name = "Alice";
 
// Direct initialization
int b(42);
std::string name2("Bob");
 
// Uniform/Brace initialization (C++11, recommended)
int c{42};
std::string name3{"Charlie"};
std::vector<int> nums{1, 2, 3, 4, 5};
 
// Brace initialization prevents narrowing conversions
int x{3.14};  // Error! Won't compile
int y = 3.14; // Compiles but silently truncates to 3

Recommendation: Prefer brace initialization {} for safety (prevents narrowing conversions).

const - Runtime Constants

const creates read-only variables:

const int maxSize = 100;
// maxSize = 200;  // Error: cannot modify const
 
const std::string greeting = "Hello";
// greeting = "Hi";  // Error
 
// const with pointers (tricky!)
int value = 42;
const int* ptr1 = &value;     // 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
 
// Remember: read right-to-left
// ptr1: pointer to (const int)
// ptr2: (const pointer) to int

constexpr - Compile-Time Constants

constexpr (C++11) creates constants evaluated at compile time:

constexpr int size = 100;        // Must be computable at compile time
constexpr double pi = 3.14159;
 
// constexpr functions (computed at compile time when possible)
constexpr int square(int x) {
    return x * x;
}
 
constexpr int result = square(5);  // Computed at compile time: 25
int runtime = 5;
int dynamic = square(runtime);     // Computed at runtime
 
// C++17: constexpr if
template<typename T>
auto getValue(T t) {
    if constexpr (std::is_integral_v<T>) {
        return t * 2;
    } else {
        return t;
    }
}
 
// C++20: constexpr std::vector and std::string
constexpr std::array<int, 3> arr = {1, 2, 3};  // OK since C++11
// constexpr std::vector<int> vec = {1, 2, 3}; // OK since C++20

consteval - Always Compile-Time (C++20)

consteval int compileTimeOnly(int x) {
    return x * x;
}
 
constexpr int a = compileTimeOnly(5);  // OK: compile time
// int runtime = 5;
// int b = compileTimeOnly(runtime);   // Error! Must be compile time

constinit - Compile-Time Initialized (C++20)

constinit int globalValue = 42;  // Must be initialized at compile time
                                  // But can be modified at runtime

Summary:

  • const - Cannot be modified (runtime or compile time)
  • constexpr - Can be computed at compile time
  • consteval - Must be computed at compile time
  • constinit - Must be initialized at compile time, but mutable

References vs Pointers

Understanding references and pointers is crucial for C++.

References

A reference is an alias for an existing variable:

int value = 42;
int& ref = value;  // ref is an alias for value
 
ref = 100;  // Modifies value!
std::cout << value << std::endl;  // Prints 100
 
// References must be initialized
// int& badRef;  // Error: references must be initialized
 
// References cannot be reseated
int other = 200;
ref = other;  // This assigns 200 to value, doesn't make ref refer to other

Pointers

A pointer stores the memory address of a variable:

int value = 42;
int* ptr = &value;  // ptr stores address of value
 
std::cout << ptr << std::endl;   // Prints address (e.g., 0x7fff5fbff8ac)
std::cout << *ptr << std::endl;  // Dereference: prints 42
 
*ptr = 100;  // Modifies value through pointer
std::cout << value << std::endl;  // Prints 100
 
// Pointers can be null
int* nullPtr = nullptr;
 
// Pointers can be reseated
int other = 200;
ptr = &other;  // Now ptr points to other

When to Use What

FeatureReferencePointer
Must be initializedYesNo
Can be nullNoYes
Can be reseatedNoYes
Syntaxref (no dereferencing)*ptr (must dereference)

Guidelines:

  • Prefer references when you don't need null or reseating
  • Use pointers when null is a valid state or when working with dynamic memory
  • Use const references for read-only access to avoid copies
// Pass by value (copy)
void passByValue(std::string s) {
    // s is a copy - modifications don't affect original
}
 
// Pass by reference (can modify)
void passByRef(std::string& s) {
    // s is the original - modifications affect it
}
 
// Pass by const reference (read-only, no copy)
void passByConstRef(const std::string& s) {
    // s is the original, but can't be modified
    // Most common for "input" parameters
}
 
// Pass by pointer (can be null)
void passByPointer(std::string* s) {
    if (s) {  // Check for null
        // Use *s to access the string
    }
}

Control Flow

Conditional Statements

if-else:

int score = 85;
 
if (score >= 90) {
    std::cout << "Grade: A\n";
} else if (score >= 80) {
    std::cout << "Grade: B\n";
} else if (score >= 70) {
    std::cout << "Grade: C\n";
} else {
    std::cout << "Grade: F\n";
}

if with initializer (C++17):

// Before C++17
{
    auto it = map.find(key);
    if (it != map.end()) {
        // use it
    }
}
 
// C++17: if with initializer
if (auto it = map.find(key); it != map.end()) {
    // it is scoped to this if-else block
    std::cout << it->second << std::endl;
}
// it is not accessible here

Ternary operator:

int score = 85;
std::string result = score >= 60 ? "Pass" : "Fail";

switch statement:

int day = 3;
std::string dayName;
 
switch (day) {
    case 1:
        dayName = "Monday";
        break;
    case 2:
        dayName = "Tuesday";
        break;
    case 3:
        dayName = "Wednesday";
        break;
    default:
        dayName = "Unknown";
        break;
}

switch with initializer (C++17):

switch (auto value = getValue(); value) {
    case 1: /* ... */ break;
    case 2: /* ... */ break;
    default: break;
}

[[fallthrough]] attribute (C++17):

switch (value) {
    case 1:
    case 2:
        handleOneOrTwo();
        [[fallthrough]];  // Intentional fallthrough
    case 3:
        handleThree();
        break;
    default:
        break;
}

Loops

for loop:

for (int i = 0; i < 5; ++i) {
    std::cout << i << " ";
}
// Output: 0 1 2 3 4

Range-based for loop (C++11):

std::vector<int> numbers = {1, 2, 3, 4, 5};
 
// Read-only iteration
for (const auto& num : numbers) {
    std::cout << num << " ";
}
 
// Modifying elements
for (auto& num : numbers) {
    num *= 2;  // Doubles each element
}
 
// With init-statement (C++20)
for (std::vector<int> v = {1, 2, 3}; auto& num : v) {
    std::cout << num << " ";
}

while loop:

int count = 0;
while (count < 5) {
    std::cout << count << " ";
    ++count;
}

do-while loop:

int count = 0;
do {
    std::cout << count << " ";
    ++count;
} while (count < 5);

Loop control:

for (int i = 0; i < 10; ++i) {
    if (i == 3) continue;  // Skip this iteration
    if (i == 7) break;     // Exit loop
    std::cout << i << " ";
}
// Output: 0 1 2 4 5 6

Functions

Functions in C++ can be standalone (unlike Java where everything is in a class).

Basic Function Syntax

// Function declaration (prototype)
int add(int a, int b);
 
// Function definition
int add(int a, int b) {
    return a + b;
}
 
// void function (no return value)
void printMessage(const std::string& msg) {
    std::cout << msg << std::endl;
}
 
int main() {
    int sum = add(5, 3);
    printMessage("Hello!");
    return 0;
}

Parameter Passing

// Pass by value (copy)
void byValue(int x) {
    x = 100;  // Only modifies local copy
}
 
// Pass by reference (can modify)
void byReference(int& x) {
    x = 100;  // Modifies original
}
 
// Pass by const reference (read-only, efficient for large objects)
void byConstRef(const std::string& s) {
    // Cannot modify s
    std::cout << s << std::endl;
}
 
// Pass by pointer
void byPointer(int* x) {
    if (x) {
        *x = 100;  // Modifies original
    }
}
 
int main() {
    int a = 42;
    byValue(a);      // a is still 42
    byReference(a);  // a is now 100
 
    std::string msg = "Hello";
    byConstRef(msg); // Efficient, no copy
 
    int b = 42;
    byPointer(&b);   // b is now 100
 
    return 0;
}

Default Arguments

void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!" << std::endl;
}
 
greet("Alice");           // "Hello, Alice!"
greet("Bob", "Hi");       // "Hi, Bob!"
 
// Default arguments must be rightmost
void process(int a, int b = 10, int c = 20);  // OK
// void process(int a = 5, int b, int c);     // Error!

Function Overloading

Same function name, different parameters:

int add(int a, int b) {
    return a + b;
}
 
double add(double a, double b) {
    return a + b;
}
 
int add(int a, int b, int c) {
    return a + b + c;
}
 
// Usage
int sum1 = add(5, 10);        // Calls first overload
double sum2 = add(5.5, 10.5); // Calls second overload
int sum3 = add(5, 10, 15);    // Calls third overload

inline Functions

Suggests to the compiler to insert function body at call site:

inline int square(int x) {
    return x * x;
}
 
// Note: Modern compilers decide inlining automatically
// Use inline mainly for small functions in headers

constexpr Functions

Can be evaluated at compile time:

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
 
constexpr int fact5 = factorial(5);  // Computed at compile time: 120
 
int runtime = 5;
int factRuntime = factorial(runtime);  // Computed at runtime

[[nodiscard]] Attribute (C++17)

Warn if return value is ignored:

[[nodiscard]] int calculateResult() {
    return 42;
}
 
[[nodiscard("Error code must be checked")]] int mayFail() {
    // ...
    return 0;
}
 
int main() {
    calculateResult();   // Warning: ignoring return value
    int x = calculateResult();  // OK
    return 0;
}

[[maybe_unused]] Attribute (C++17)

Suppress unused variable warnings:

void process([[maybe_unused]] int debugLevel) {
    // debugLevel might only be used in debug builds
#ifdef DEBUG
    std::cout << "Debug level: " << debugLevel << std::endl;
#endif
}

Namespaces

Namespaces prevent name collisions:

namespace graphics {
    void draw() {
        std::cout << "Drawing graphics\n";
    }
 
    struct Point {
        int x, y;
    };
}
 
namespace audio {
    void draw() {  // Same name, different namespace
        std::cout << "Drawing audio waveform\n";
    }
}
 
int main() {
    graphics::draw();  // Calls graphics::draw()
    audio::draw();     // Calls audio::draw()
 
    graphics::Point p{10, 20};
 
    return 0;
}

using Declarations and Directives

// using declaration - brings specific name into scope
using std::cout;
using std::endl;
cout << "Hello" << endl;  // No std:: prefix needed
 
// using directive - brings all names from namespace (avoid in headers!)
using namespace std;
cout << "Hello" << endl;  // All std:: names available
 
// Best practice: use declarations, not directives
// Never use "using namespace" in header files!

Nested Namespaces (C++17)

// Before C++17
namespace company {
    namespace project {
        namespace module {
            void function() { }
        }
    }
}
 
// C++17
namespace company::project::module {
    void function() { }
}

Anonymous/Unnamed Namespaces

Internal linkage (like static in C):

namespace {
    void internalHelper() {
        // Only visible in this translation unit
    }
 
    int internalVariable = 42;
}

Inline Namespaces (C++11)

Useful for versioning:

namespace mylib {
    inline namespace v2 {
        void process() { /* v2 implementation */ }
    }
 
    namespace v1 {
        void process() { /* v1 implementation */ }
    }
}
 
mylib::process();     // Calls v2 (inline namespace)
mylib::v1::process(); // Calls v1 explicitly
mylib::v2::process(); // Calls v2 explicitly

Basic I/O

Console Output with iostream

#include <iostream>
 
int main() {
    // Basic output
    std::cout << "Hello, World!" << std::endl;
 
    // Chaining output
    int age = 25;
    std::string name = "Alice";
    std::cout << "Name: " << name << ", Age: " << age << std::endl;
 
    // endl vs '\n'
    std::cout << "With endl" << std::endl;  // Flushes buffer
    std::cout << "With newline\n";          // Faster, no flush
 
    // Error output
    std::cerr << "Error message" << std::endl;
 
    return 0;
}

Console Input

#include <iostream>
#include <string>
 
int main() {
    // Reading integers
    int age;
    std::cout << "Enter your age: ";
    std::cin >> age;
 
    // Reading strings (single word)
    std::string name;
    std::cout << "Enter your name: ";
    std::cin >> name;  // Reads until whitespace
 
    // Reading entire line
    std::cin.ignore();  // Clear newline from previous input
    std::string fullName;
    std::cout << "Enter your full name: ";
    std::getline(std::cin, fullName);  // Reads entire line
 
    std::cout << "Hello, " << fullName << "! Age: " << age << std::endl;
 
    return 0;
}

Stream Manipulators

#include <iostream>
#include <iomanip>
 
int main() {
    int num = 42;
    double pi = 3.14159265358979;
 
    // Number formatting
    std::cout << std::hex << num << std::endl;  // 2a (hexadecimal)
    std::cout << std::dec << num << std::endl;  // 42 (decimal)
    std::cout << std::oct << num << std::endl;  // 52 (octal)
 
    // Floating-point precision
    std::cout << std::fixed << std::setprecision(2) << pi << std::endl;  // 3.14
    std::cout << std::scientific << pi << std::endl;  // 3.14e+00
 
    // Width and fill
    std::cout << std::setw(10) << std::setfill('0') << num << std::endl;  // 0000000042
 
    // Boolean formatting
    bool flag = true;
    std::cout << std::boolalpha << flag << std::endl;  // true (instead of 1)
 
    return 0;
}

C++20/23: std::format and std::print

Modern formatting (like Python f-strings):

#include <format>  // C++20
#include <print>   // C++23
 
int main() {
    std::string name = "Alice";
    int age = 25;
    double height = 5.8;
 
    // std::format (C++20)
    std::string msg = std::format("Name: {}, Age: {}, Height: {:.1f}",
                                   name, age, height);
    std::cout << msg << std::endl;
 
    // std::print (C++23) - even simpler!
    std::print("Name: {}, Age: {}\n", name, age);
    std::println("With automatic newline: {}", name);
 
    // Format specifiers
    std::print("{:>10}", 42);     // Right-align, width 10
    std::print("{:0>10}", 42);    // Zero-padded
    std::print("{:#x}", 255);     // Hex with 0x prefix: 0xff
    std::print("{:.2f}", 3.14159); // 2 decimal places: 3.14
 
    return 0;
}

Strings

C-Style Strings (Avoid)

// C-style strings (avoid in modern C++)
const char* cstr = "Hello";  // Pointer to char array
char arr[] = "World";        // Character array
 
// Problems with C-strings:
// - No bounds checking
// - Manual memory management
// - Easy to make mistakes
// - Verbose operations (strlen, strcmp, strcpy, etc.)

std::string (Use This!)

#include <string>
 
int main() {
    // Creating strings
    std::string s1 = "Hello";
    std::string s2("World");
    std::string s3{5, 'x'};  // "xxxxx"
 
    // Concatenation
    std::string greeting = s1 + ", " + s2 + "!";
 
    // Appending
    greeting += " How are you?";
    greeting.append(" Fine, thanks.");
 
    // Length
    std::cout << greeting.length() << std::endl;  // or .size()
 
    // Accessing characters
    char first = greeting[0];        // No bounds checking
    char safe = greeting.at(0);      // Throws if out of bounds
 
    // Substrings
    std::string sub = greeting.substr(0, 5);  // "Hello"
 
    // Finding
    size_t pos = greeting.find("World");
    if (pos != std::string::npos) {
        std::cout << "Found at: " << pos << std::endl;
    }
 
    // Comparison
    if (s1 == "Hello") {
        std::cout << "Equal!" << std::endl;
    }
 
    // Iteration
    for (char c : greeting) {
        std::cout << c;
    }
 
    return 0;
}

std::string_view (C++17)

Non-owning reference to a string (avoids copies):

#include <string_view>
 
// Before C++17: this creates a copy
void processOld(const std::string& s) {
    std::cout << s << std::endl;
}
 
// C++17: no copy, works with std::string AND const char*
void processNew(std::string_view sv) {
    std::cout << sv << std::endl;
}
 
int main() {
    std::string str = "Hello, World!";
    const char* cstr = "Hello, C-string!";
 
    processNew(str);   // No copy
    processNew(cstr);  // No copy (and no std::string allocation!)
    processNew("Literal");  // Works with literals too
 
    // String view operations
    std::string_view sv = "Hello, World!";
    std::string_view hello = sv.substr(0, 5);  // "Hello"
    std::string_view world = sv.substr(7, 5);  // "World"
 
    // Warning: string_view doesn't own the data!
    std::string_view dangerous() {
        std::string temp = "Don't do this";
        return temp;  // Dangling reference! temp is destroyed
    }
 
    return 0;
}

When to use std::string_view:

  • Function parameters for read-only access
  • When you don't need to own or modify the string
  • When performance matters (avoids allocations)

When NOT to use:

  • When you need to store the string
  • When the source might be destroyed
  • When you need to modify the string

Arrays

C-Style Arrays (Avoid in Most Cases)

// Fixed-size, stack-allocated
int arr[5] = {1, 2, 3, 4, 5};
 
// Problems:
// - Decays to pointer when passed to functions
// - No bounds checking
// - No size information at runtime
// - Cannot be returned from functions safely

std::array (Fixed-Size, Modern)

#include <array>
 
int main() {
    // Create array
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
 
    // Size is known
    std::cout << "Size: " << arr.size() << std::endl;
 
    // Access elements
    arr[0] = 10;         // No bounds checking
    arr.at(0) = 10;      // Throws if out of bounds
    arr.front();         // First element
    arr.back();          // Last element
 
    // Iteration
    for (const auto& elem : arr) {
        std::cout << elem << " ";
    }
 
    // Fill with value
    arr.fill(0);
 
    // Works with STL algorithms
    std::sort(arr.begin(), arr.end());
 
    // Can be passed to functions without decay
    void process(std::array<int, 5>& a);  // Size is part of type!
 
    return 0;
}

std::vector (Dynamic-Size)

#include <vector>
 
int main() {
    // Create vector
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::vector<int> vec2(10);        // 10 elements, all 0
    std::vector<int> vec3(10, 42);    // 10 elements, all 42
 
    // Add elements
    vec.push_back(6);
    vec.emplace_back(7);  // Constructs in place (more efficient)
 
    // Size and capacity
    std::cout << "Size: " << vec.size() << std::endl;
    std::cout << "Capacity: " << vec.capacity() << std::endl;
 
    // Reserve capacity (avoid reallocations)
    vec.reserve(100);
 
    // Access
    vec[0];        // No bounds checking
    vec.at(0);     // Throws if out of bounds
    vec.front();   // First element
    vec.back();    // Last element
 
    // Remove elements
    vec.pop_back();              // Remove last
    vec.erase(vec.begin());      // Remove first
    vec.clear();                 // Remove all
 
    // Check if empty
    if (vec.empty()) {
        std::cout << "Vector is empty" << std::endl;
    }
 
    return 0;
}

We'll dive much deeper into STL containers in the dedicated STL post.


Putting It All Together: Example Program

Let's build a simple grade calculator that uses everything we've learned:

#include <iostream>
#include <string>
#include <vector>
#include <numeric>   // for std::accumulate
#include <algorithm> // for std::min_element, std::max_element
 
// Constants
constexpr int PASSING_GRADE = 60;
constexpr int MAX_GRADE = 100;
constexpr int MIN_GRADE = 0;
 
// Function to get letter grade
[[nodiscard]] char getLetterGrade(int score) {
    if (score >= 90) return 'A';
    if (score >= 80) return 'B';
    if (score >= 70) return 'C';
    if (score >= 60) return 'D';
    return 'F';
}
 
// Function to validate grade
[[nodiscard]] bool isValidGrade(int grade) {
    return grade >= MIN_GRADE && grade <= MAX_GRADE;
}
 
// Function to calculate statistics
struct Statistics {
    double average;
    int min;
    int max;
    bool allPassing;
};
 
[[nodiscard]] Statistics calculateStats(const std::vector<int>& grades) {
    if (grades.empty()) {
        return {0.0, 0, 0, false};
    }
 
    double sum = std::accumulate(grades.begin(), grades.end(), 0.0);
    double avg = sum / static_cast<double>(grades.size());
 
    int minGrade = *std::min_element(grades.begin(), grades.end());
    int maxGrade = *std::max_element(grades.begin(), grades.end());
 
    bool allPass = std::all_of(grades.begin(), grades.end(),
                               [](int g) { return g >= PASSING_GRADE; });
 
    return {avg, minGrade, maxGrade, allPass};
}
 
// Function to print results
void printResults(std::string_view studentName, const std::vector<int>& grades) {
    std::cout << "\n=== Grade Report for " << studentName << " ===\n\n";
 
    std::cout << "Individual Grades:\n";
    for (size_t i = 0; i < grades.size(); ++i) {
        std::cout << "  Test " << (i + 1) << ": "
                  << grades[i] << " (" << getLetterGrade(grades[i]) << ")\n";
    }
 
    auto [average, min, max, allPassing] = calculateStats(grades);
 
    std::cout << "\nStatistics:\n";
    std::cout << "  Average: " << average << " (" << getLetterGrade(static_cast<int>(average)) << ")\n";
    std::cout << "  Highest: " << max << "\n";
    std::cout << "  Lowest:  " << min << "\n";
    std::cout << "  Status:  " << (allPassing ? "All tests passed!" : "Some tests failed") << "\n";
}
 
int main() {
    std::string name;
    std::cout << "Enter student name: ";
    std::getline(std::cin, name);
 
    std::vector<int> grades;
    std::cout << "Enter grades (negative number to finish):\n";
 
    int grade;
    while (std::cin >> grade && grade >= 0) {
        if (isValidGrade(grade)) {
            grades.push_back(grade);
        } else {
            std::cerr << "Invalid grade (must be 0-100). Skipping.\n";
        }
    }
 
    if (grades.empty()) {
        std::cout << "No valid grades entered.\n";
        return 1;
    }
 
    printResults(name, grades);
 
    return 0;
}

What this program demonstrates:

  • constexpr compile-time constants
  • [[nodiscard]] attribute for return values
  • Function overloading and various return types
  • const references for read-only parameters
  • std::string_view for efficient string passing
  • std::vector for dynamic arrays
  • Range-based for loops
  • Structured bindings (C++17)
  • Lambda expressions
  • Standard library algorithms

Common Mistakes to Avoid

1. Forgetting to Initialize Variables

int x;  // Uninitialized - contains garbage!
if (x > 0) { /* undefined behavior! */ }
 
// Always initialize
int y = 0;
int z{};  // Value-initialized to 0

2. Using == in Conditionals with Assignment

int x = 5;
if (x = 10) { /* Bug! This assigns 10 to x, then checks if x is truthy */ }
 
// Correct
if (x == 10) { /* Comparison */ }

3. Forgetting break in Switch

switch (value) {
    case 1:
        doSomething();
        // Fall-through bug! Use [[fallthrough]] if intentional
    case 2:
        doSomethingElse();
        break;
}

4. Returning References to Local Variables

int& dangerous() {
    int local = 42;
    return local;  // Dangling reference! local is destroyed
}
 
// Return by value for local variables
int safe() {
    int local = 42;
    return local;  // OK: value is copied
}

5. Ignoring Compiler Warnings

# Always compile with warnings
g++ -Wall -Wextra -Werror main.cpp
 
# Don't suppress warnings - fix them!

Summary and Key Takeaways

Compilation: C++ is compiled (preprocessor → compiler → linker)
Types: Use modern types (int32_t, std::string, std::vector)
auto: Use for complex types, but be explicit when type isn't obvious
const vs constexpr: const = can't change, constexpr = compile-time
References: Prefer references over pointers when possible
nullptr: Always use nullptr, never NULL or 0
Strings: Use std::string and std::string_view, avoid C-strings
Arrays: Use std::array (fixed) or std::vector (dynamic)
Warnings: Always compile with -Wall -Wextra -Werror
Modern features: Use C++17/20 features for cleaner code


Next Steps

In the next post, we'll dive into Object-Oriented Programming in C++:

  • Classes and objects
  • Constructors and destructors
  • RAII (Resource Acquisition Is Initialization)
  • Inheritance and polymorphism
  • Operator overloading

You now have the foundation needed to understand how C++ builds upon these basics with powerful OOP features.

Practice suggestions:

  1. Rewrite a simple program from another language in C++
  2. Experiment with different compiler flags
  3. Use Compiler Explorer to see generated assembly
  4. Try the example program and extend it with new features

Happy coding! 🚀


This post is part of the Modern C++ Learning Roadmap series.

Related Posts:

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.