stack vs. heap

StackHeap
Speedfast — just moves a pointerslower — the allocator has to find and track a free block
Sizelimited (a few MB, fixed at thread start)limited only by available memory
Layoutcontiguous, grows/shrinks automatically with scopenot contiguous between separate allocations
Lifetimetied to scope — deallocated automatically when a variable goes out of scopetied to new/delete — an object made with new stays alive until explicitly deleted, even after the pointer that made it goes out of scope
The heap's manual lifetime is the whole reason smart pointers exist: forget one delete and you leak memory; call delete twice, or on a stack address, and you crash.

float *ptr;
*ptr = 4.0;          // undefined behavior — ptr points nowhere in particular yet

ptr = new float;     // now ptr owns a float on the heap
*ptr = 4.0;          // fine
delete ptr;          // your responsibility — nothing does this for you

int x = 5;
int *stackPtr = &x;
delete stackPtr;     // runtime error: free(): invalid pointer — x was never on the heap
            

size_t and the fixed-width integer types

The motivation isn't obscure — it's portability across time, not just across platforms. size_t is defined as "whatever type sizeof returns," which the standard guarantees is large enough to represent the size of the largest object the target platform can allocate. Code written to say "the size of a thing is a size_t" keeps compiling correctly whether that's 32 bits today or 128 bits in thirty years — code that hardcodes int or long for a size doesn't have that guarantee.
TypeUse it when
size_tyou mean "the size of an object" — container sizes, indices, sizeof results
uint8_t, int32_t, etc.you need an exact bit width — parsing a binary file format, matching a hardware register layout, anything externally defined
plain intyou just want "a reasonably fast whole number" and don't care about the exact width — the natural word size of the machine

const-correctness

A const member function promises not to modify the object it's called on — the compiler enforces the promise. A const object can only call const member functions, but non-const objects can call either. Making a function const whenever it doesn't need to mutate state isn't pedantry: it lets the compiler catch accidental mutation, and it lets const objects use the function at all.

class Account {
  mutable int accessCount = 0;   // allowed to change even from a const function
  double balance = 0;
public:
  double getBalance() const {
    accessCount++;                // OK — accessCount is mutable
    // balance = 0;                // error: assignment of member in read-only object
    return balance;
  }
};

const Account a;
a.getBalance();                  // OK — const object, const function
            
mutable is the escape hatch: it marks a specific member as changeable even through a const function — typically for bookkeeping (a cache, a counter, a mutex) that doesn't represent the object's logical state.
Full worked example, including what happens when a non-const function tries the same mutations: TopNotchNote/cpp/fundamentals_const_correctness.cpp

char, strings, and arrays

A C-string is really just a pointer to the first byte of a contiguous block, terminated by a '\0' byte. std::string hides all of that, but it's worth seeing once, because the terminator is exactly why C-string bugs happen.

const char *name = "bab\0ak";
std::cout << name;              // prints "bab" — printing stops at the first '\0', not at the end of the literal

char pr[] = "babak";
sizeof(pr);                      // 6 — 5 letters + the trailing '\0', stored inline in the array

char *ptr = "babak";
sizeof(ptr);                     // 8 — that's the size of a pointer, not the string; use strlen(ptr) for the string's length
            
Unlike a Python string, std::string is mutable — indexing and modifying in place work the way they do on a std::vector. Prefer it over raw char* in new code; it owns its own memory and won't overrun a fixed buffer.
str.substr(pos, len) | https://en.cppreference.com/w/cpp/string/basic_string/substr | extract a substring starting at pos, len characters long |'fu1'
str.at(pos) | https://en.cppreference.com/w/cpp/string/basic_string/at | bounds-checked character access; throws std::out_of_range instead of undefined behavior |'fu2'
str1.compare(str2) | https://en.cppreference.com/w/cpp/string/basic_string/compare | lexicographic comparison; == works too and is more readable |'fu3'

arrays vs. pointers in a function signature

These two declarations are identical as function parameters — the array decays to a pointer the moment it crosses the function boundary, so the function has no way to know the array's size from the parameter itself.

void f(int *a)   { /* ... */ }
void f(int a[])  { /* ... */ }   // exactly the same signature as above

int arr[5];                       // size fixed at declaration
int arr2[] = {1, 2, 3};           // size inferred from the initializer — arr2[0] is 1

void f(int a[]) { /* sizeof(a) here is sizeof(int*), not the array's byte size */ }
            
Because of this decay, arrays and inheritance don't mix. Indexing an array is just pointer arithmetic: array[i] is *(array + i), computed as array + i * sizeof(element_type). If you pass an array of Derived to a function expecting an array of Base, the compiler uses sizeof(Base) for that arithmetic — wrong if sizeof(Derived) != sizeof(Base), which it almost always is. See Inheritance & Polymorphism for why this rules out ever treating arrays polymorphically.

narrowing conversions

Brace initialization ({ }) refuses conversions that could lose information — a useful guard rail that plain = initialization doesn't give you.

int a = 3.14;      // silently truncates to 3 — legal, easy to miss
int b{3.14};        // compile error: narrowing conversion of '3.14' from 'double' to 'int'
int c{3};            // fine — no narrowing, 3 is exactly representable as an int
            

unions, briefly

A union lets several members share the same memory — its size is the size of its largest member, and writing one member overwrites the others. Modern C++ rarely needs this directly (std::variant is the type-safe replacement), but it still shows up in low-level code that needs to reinterpret the same bytes as two different types, or in a function that returns one of several types by reference to a shared location.

union Value {
  int asInt;
  float asFloat;
};

Value v;
v.asInt = 42;
// v.asFloat now reads those same 4 bytes reinterpreted as a float — not a meaningful float value
            

internal vs. external linkage

Internal linkage means a name is visible only inside the translation unit (roughly: the one .cpp file, after preprocessing) that defines it. External linkage means it's visible to the whole program — every object file the linker combines. A free function or global variable is external by default; marking it static at file scope makes it internal, i.e. invisible to the linker outside that file. This is covered in depth, with a worked static-member example, in constexpr & static.

where to go from here

Pointers & References — the mechanics behind everything above that touches the heap.
Classes & Constructors — const-correctness applied to a full class.
C++ Best Practices — struct padding and sizeof, once you're comfortable with the basics here.
Regular Expressions (std::regex) — pattern matching over the std::string type introduced here.

reference

cppreference — std::size_t
cppreference — list initialization (narrowing)