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:
| Compiler | Platform | Notes |
|---|---|---|
| GCC (g++) | Linux, macOS, Windows | Most popular, excellent standards support |
| Clang (clang++) | Linux, macOS, Windows | Excellent error messages, fast |
| MSVC | Windows | Best Windows integration |
Installation by Platform:
Linux (Ubuntu/Debian):
sudo apt update
sudo apt install build-essential # Installs GCC
# Or for Clang:
sudo apt install clangmacOS:
# Install Xcode Command Line Tools (includes Clang)
xcode-select --install
# Or install GCC via Homebrew
brew install gccWindows:
- 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)
cl2. 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.exeWhat's happening:
#include <iostream>- Includes the I/O stream library (preprocessor directive)int main()- Entry point for C++ programs (must returnint)std::cout- Standard output stream (from namespacestd)<<- Stream insertion operatorstd::endl- Newline + flush bufferreturn 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.cppPro 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 → Executable1. 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 helloHeader 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 programNote: 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 falseKey differences from other languages:
intsize is platform-dependent (useint32_tfor guarantees)charis 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 bdecltype - 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 3Recommendation: 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 intconstexpr - 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++20consteval - 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 timeconstinit - Compile-Time Initialized (C++20)
constinit int globalValue = 42; // Must be initialized at compile time
// But can be modified at runtimeSummary:
const- Cannot be modified (runtime or compile time)constexpr- Can be computed at compile timeconsteval- Must be computed at compile timeconstinit- 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 otherPointers
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 otherWhen to Use What
| Feature | Reference | Pointer |
|---|---|---|
| Must be initialized | Yes | No |
| Can be null | No | Yes |
| Can be reseated | No | Yes |
| Syntax | ref (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
constreferences 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 hereTernary 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 4Range-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 6Functions
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 overloadinline 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 headersconstexpr 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 explicitlyBasic 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 safelystd::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:
constexprcompile-time constants[[nodiscard]]attribute for return values- Function overloading and various return types
constreferences for read-only parametersstd::string_viewfor efficient string passingstd::vectorfor 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 02. 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:
- Rewrite a simple program from another language in C++
- Experiment with different compiler flags
- Use Compiler Explorer to see generated assembly
- 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:
- Go Pointers vs C++ Pointers - Compare C++ pointers with Go's safer approach
📬 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.