C++ Basics


  • Description: A note on the C++ basics — constants, declarations, tokens, storage qualifiers, initialization forms, enums, attributes, and alignment
  • My Notion Note ID: K2A-B1-1
  • Created: 2018-09-14
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Constants and Immutability

1.1 const vs #define

  • Both define constants but behave very differently. Prefer const / constexpr (C++11) in modern code.
Aspect const #define
Type safety Has a type; compiler performs type checking No type; simple text substitution
Scope Respects scope rules Global text replacement
Debugging Visible in debugger Replaced before compilation; invisible to debugger
Testability Can be used in unit tests and assertions Cannot be inspected programmatically
#define MAX_SIZE 100              // No type, no scope, no safety
const int kMaxSize = 100;         // Type-safe, scoped, debuggable
constexpr int kMaxSize2 = 100;    // C++11: compile-time constant

1.2 Pointer to const vs const Pointer

  • const position relative to * decides what's immutable.
int value = 42;

// Pointer to const: cannot modify the pointed-to value through this pointer
const int* p1 = &value;       // OK
int const* p2 = &value;       // Same as above (equivalent syntax)
// *p1 = 10;                  // ERROR: cannot modify through pointer-to-const

// Const pointer: cannot change what the pointer points to
int* const p3 = &value;       // Must be initialized
// p3 = &other;               // ERROR: cannot reassign const pointer
*p3 = 10;                     // OK: can modify the value

// Const pointer to const: both are immutable
const int* const p4 = &value; // Must be initialized
// *p4 = 10;                  // ERROR
// p4 = &other;               // ERROR
  • Mnemonic: read right-to-left. const int* = "pointer to const int." int* const = "const pointer to int."

2. Declarations

2.1 Variable Declaration Gotchas

  • Multi-variable declarations on one line — easy to misread.
  1. Only the last variable in a chain receives the initializer:

    // Only 'index' is initialized to 0; 'column' and 'row' are uninitialized
    int column, row, index = 0;
    
    // All three are initialized to 0
    int column = 0, row = 0, index = 0;
    
    // Chain assignment also works
    int column, row, index;
    column = index = row = 0;
    
  2. * binds only to the immediately following identifier:

    int* a, b, c;
    // This is NOT three pointers. It is equivalent to:
    //   int* a;   -- pointer to int
    //   int b;    -- plain int
    //   int c;    -- plain int
    
    // To declare three pointers:
    int *a, *b, *c;
    
    // Or better yet, one declaration per line:
    int* a;
    int* b;
    int* c;
    

2.2 Tokens

  • Token = smallest element the compiler recognizes. Six categories:
  1. Keywords — reserved words (if, class, virtual, ...)
  2. Identifiers — user-defined names (variables, functions, classes)
  3. Constants — literals (42, 3.14, 'a', true)
  4. String literals"hello"
  5. Special symbols — punctuation/delimiters ([] () {} , ; * = #)
  6. Operators+, -, <<, >

2.3 Comma Operator

  • Evaluates all operands left-to-right, returns the last.
  • Rarely used intentionally — avoid for clarity.
int a = (1, 2, 3);  // a == 3; all expressions evaluated, last one returned

3. Storage Qualifiers

3.1 mutable

  • Allows a non-static class member to be modified inside a const member function.

Why mutable is needed

  • const member fn promises logical const (observable state unchanged), not bitwise const (no bytes change).
  • Internal bookkeeping not part of observable state — caches, lazy-init flags, mutexes, debug counters.
  • Modifying bookkeeping in const method is logically fine, but type system can't tell logical-const from bitwise-const.
  • mutable = escape hatch: "OK to modify in const method, not part of observable state."
  • Without mutable:
    • Drop const from method → contagious; every const-ref caller breaks.
    • const_cast → legal but ugly, easy to misuse.

Common use cases

  1. Memoization / caches — cache an expensive computation; cached value is internal, not observable.
  2. Lazy initialization — defer constructing a heavy member until first access.
  3. Mutex inside const methods — locking requires the mutex itself to be mutable.
  4. Reference-counted handlesstd::shared_ptr's control block updates a count even when the pointee is const.
  5. Debug instrumentation — access counters, timing data, etc.

Example

#include <iostream>

class Example {
public:
    Example(int val) : val_(val) {}

    int getVal() const {
        // This is a const method, but we can modify mutable members
        ++access_count_;
        return val_;
    }

    int getAccessCount() const { return access_count_; }

private:
    int val_;
    mutable int access_count_ = 0;  // Can be modified in const methods
};

int main() {
    const Example ex(5);
    std::cout << ex.getVal() << std::endl;         // 5 (access_count_ = 1)
    std::cout << ex.getVal() << std::endl;         // 5 (access_count_ = 2)
    std::cout << ex.getAccessCount() << std::endl; // 2
    return 0;
}
  • getVal() is const — returned value unchanged across calls.
  • access_count_ tick-up = internal, not observable → mutable correctly allows it.

3.2 volatile

  • volatile — value may change outside program flow.
  • Compiler must read/write from memory each access. No register caching, no eliding "redundant" reads.
volatile int hardware_register;  // Value may change outside program control

When to use volatile

  • 3 legitimate uses, all involve memory modified outside the standard C++ execution model:
  1. Memory-mapped I/O — hardware registers in the program's address space (status flags, device control).
  2. Signal handlers — flag set in handler, polled in main flow (use volatile std::sig_atomic_t).
  3. setjmp / longjmp — locals modified between setjmp and longjmp must be volatile to be reliably observable.

Example: signal handler flag

#include <csignal>
#include <iostream>

volatile std::sig_atomic_t signal_received = 0;

void handler(int) {
    signal_received = 1;  // safe to assign inside a signal handler
}

int main() {
    std::signal(SIGINT, handler);

    while (!signal_received) {
        // Without volatile, the compiler could observe that nothing in the
        // visible flow modifies signal_received and turn this into an
        // infinite loop. volatile forces a fresh read every iteration.
    }
    std::cout << "Caught signal, exiting." << std::endl;
    return 0;
}

When NOT to use volatile

  • Not thread safety. No atomic reads/writes, no inter-thread ordering, no inter-thread visibility.
  • For threads: std::atomic<T>, std::mutex, std::condition_variable.
// WRONG: volatile does not make this thread-safe
volatile int counter = 0;
// ++counter from multiple threads is still a race condition

// CORRECT
std::atomic<int> counter{0};
// ++counter is atomic across threads
  • Rule of thumb: volatile talks to the outside world (hardware, signals, OS jumps); std::atomic talks to other threads.

3.3 extern

  • Declares var/fn defined in another translation unit. Compiler looks up at link time, not in current TU.
// file1.cpp -- definition
int myVar = 42;

// file2.cpp -- declaration (uses the variable defined in file1.cpp)
extern int myVar;

int main() {
    std::cout << myVar << std::endl;  // 42
    return 0;
}
  • extern "C" — C linkage when calling C functions from C++.
extern "C" {
    void c_library_function(int arg);
}

4. Initialization Forms

  • Many init forms in C++ — not equivalent.
int a;            // default initialization (uninitialized for built-ins)
int b = 0;        // copy initialization
int c(0);         // direct initialization
int d{0};         // direct-list (brace) initialization
int e = {0};      // copy-list initialization
int f{};          // value initialization (zero-initialized)

std::vector<int> v1;            // default ctor
std::vector<int> v2(5);         // FIVE zero-initialized ints
std::vector<int> v3{5};         // ONE element with value 5  (initializer_list wins!)
std::vector<int> v4{5, 10};     // TWO elements: 5, 10
std::vector<int> v5(5, 10);     // FIVE elements: 10, 10, 10, 10, 10

struct Point { int x; int y; };
Point p{1, 2};                  // aggregate initialization
Point q = {.x = 1, .y = 2};     // designated initializers (C++20)

Prefer {} (brace) in modern C++:

  1. No narrowing conversionsint x{3.14}; is error; int x = 3.14; silently truncates.
  2. Avoids most vexing parseWidget w(Foo()); declares a function; Widget w{Foo{}}; constructs an object.
  3. Uniform syntax — aggregates, classes, arrays.
  • Trap: class with initializer_list ctor → brace prefers it. std::vector<int> v{5}; = 1 element (=5), not 5-element vector. Use () for count-based ctor.

5. Enums

  • 2 kinds: traditional (unscoped) and scoped (enum class, C++11).
// Unscoped enum (C-style) — names leak into the surrounding scope, implicitly converts to int
enum Color { Red, Green, Blue };
int c = Red;                   // OK: implicit conversion
if (c == 0) { /* ... */ }

// Scoped enum (enum class) — names are scoped, no implicit conversion
enum class Priority { Low, Medium, High };
Priority p = Priority::High;
// int n = p;                  // ERROR: no implicit conversion
int n = static_cast<int>(p);   // OK: explicit

Always prefer enum class in modern code:

  1. No name leakage — Priority::High, not just High.
  2. No implicit conversions to/from int.
  3. Specify underlying type — enum class Flags : uint8_t { ... }.

Bitmask enums — opt back into bitwise operators:

enum class Perm : unsigned { Read = 1, Write = 2, Exec = 4 };

constexpr Perm operator|(Perm a, Perm b) {
    return static_cast<Perm>(static_cast<unsigned>(a) | static_cast<unsigned>(b));
}

Perm p = Perm::Read | Perm::Write;

6. Attributes

  • Standardized annotations (C++11+) for diagnostics/optimizations.
Attribute Meaning
[[nodiscard]] Warn if the return value is ignored
[[nodiscard("reason")]] (C++20) Warn with a specific message
[[deprecated]] Warn on use
[[deprecated("use foo instead")]] With message
[[maybe_unused]] Suppress unused-variable warnings
[[fallthrough]] Acknowledge intentional switch fallthrough
[[likely]] / [[unlikely]] (C++20) Branch prediction hints
[[no_unique_address]] (C++20) Allow zero-size empty subobjects
[[nodiscard]] int compute() { return 42; }
compute();   // warning: ignoring return value

[[deprecated("use compute() instead")]]
int legacy_compute();

void process(int x, [[maybe_unused]] int debug_id) {
    // debug_id only used in debug builds; no warning in release
}

switch (n) {
    case 1: do_one();
    [[fallthrough]];
    case 2: do_two_or_one(); break;
    case 3: do_three(); break;
}

if (cond) [[likely]] {
    // hot path
}
  • [[nodiscard]] on factories/fallible ops — catches "forgot to check return" bugs at compile time. Most useful in practice.

7. Alignment and std::byte

  • alignof queries, alignas requests memory alignment.
alignof(int);                  // typically 4 (4-byte alignment)
alignof(double);               // typically 8

struct alignas(64) CacheLine {  // aligned to a 64-byte cache line
    int data[16];
};

alignas(16) char buffer[256];   // 16-byte aligned buffer

Common uses:

  1. Cache-line alignment — avoid false sharing in multi-threaded code.
  2. SIMD intrinsics — require 16- or 32-byte alignment.
  3. Hardware — specific alignment (e.g., DMA buffers).
  • std::byte (C++17, <cstddef>) — type-safe byte for raw memory, not arithmetic.
  • No implicit conversion to/from char / int.
#include <cstddef>

std::byte b{0xFF};             // ok
b = std::byte{0x10};           // ok
// int n = b;                   // ERROR: no implicit conversion
int n = std::to_integer<int>(b);   // explicit: 16
b |= std::byte{0x01};          // bitwise ops are defined
  • Use for opaque memory — serialization buffers, generic byte streams, hashing input.
  • Use unsigned char if you need arithmetic on bytes.

8. Aggregate Types and POD

  • Layout/behavior classifications. Matter for serialization, ABI compatibility, memcpy-based copying.

Aggregate types

  • Aggregate = array, or class with:
    1. No user-declared (or inherited) constructors.
    2. No private/protected non-static data members.
    3. No virtual functions.
    4. No virtual or non-public bases (since C++17).
  • Brace-init + designated initializers (C++20):
struct Point { int x; int y; };
Point p{1, 2};                   // aggregate init
Point q = {.x = 1, .y = 2};      // designated init (C++20)

Trivially copyable

  • Trivially copyable — copy/move/destruct all trivial (compiler-generated, no user logic).
  • Safe to memcpy.

Standard layout

  • Standard layout:
    1. All non-static data members same access control.
    2. No virtual functions or virtual bases.
    3. C-compatible struct interpretation safe.

POD (Plain Old Data)

  • POD = trivial + standard layout.
  • Trivial is stricter than trivially-copyable — also requires trivial default ctor.
  • Byte-for-byte C-struct compatible. Use for binary serialization, OS APIs, network protocols.
Trait What it means
std::is_aggregate_v<T> Aggregate-initializable
std::is_trivially_copyable_v<T> Safe to memcpy
std::is_standard_layout_v<T> C-compatible layout
std::is_pod_v<T> (deprecated in C++20) Both trivial AND standard layout
struct A { int x; };                    // aggregate, trivially copyable, standard layout, POD
struct B { int x; B(int) {} };          // not aggregate (has ctor)
struct C { int x; private: int y; };    // not standard layout (mixed access)
struct D { virtual void f(); };         // not POD (has virtual)

static_assert(std::is_trivially_copyable_v<A>);
static_assert(!std::is_aggregate_v<B>);
  • C++20: std::is_pod deprecated. Use the precise trait — std::is_trivially_copyable_v or std::is_standard_layout_v. POD as a single concept is no longer the right vocabulary.