C++ Fundamentals
The type and memory model underneath everything else in this track.
Beginner
| Stack | Heap | |
|---|---|---|
| Speed | fast — just moves a pointer | slower — the allocator has to find and track a free block |
| Size | limited (a few MB, fixed at thread start) | limited only by available memory |
| Layout | contiguous, grows/shrinks automatically with scope | not contiguous between separate allocations |
| Lifetime | tied to scope — deallocated automatically when a variable goes out of scope | tied to new/delete — an object made with new stays alive until explicitly deleted, even after the pointer that made it goes out of scope |
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 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.
| Type | Use it when |
|---|---|
size_t | you 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 int | you just want "a reasonably fast whole number" and don't care about the exact width — the natural word size of the machine |
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.
'\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
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.
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 */ }
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.
{ }) 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
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
.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.