← Back to all posts

07 C++ Notes

Pointers, Part 1: Declaration and Memory

C++ Notes


Declaration and Use

Introduction

Declaration

Using a Pointer

Pointers to Pointers

Trick: "star kills p." The rule is that stars must never outnumber the p-levels. Each star strips one level of indirection. Given int x{23}, int* p{&x}, int** pp{&p}, int*** ppp{&pp}: *p behaves like x (gives value 23), *pp behaves like p (gives address of x), *ppp behaves like pp (gives address of p), and ***ppp behaves like x (gives value 23). "Behaves like" means both the type and the data it contains are equivalent.

Const Pointers

const binds to whatever is on its left. If nothing is on the left, it binds to the right. This one rule decides what exactly ends up const.

DeclarationModify value (*p = 5)?Rebind pointer (p = &y)?Use when
const int* p
(same as int const* p)
NoYesYou want to protect the data through this pointer, but still allow it to point elsewhere later.
int* const pYesNoThe pointer's identity must stay fixed, but mutating the data through it is fine.
const int* const pNoNoBoth the identity and the data must stay fixed for the rest of the pointer's life.

The object itself never becomes const, only your view through that particular pointer does. Another pointer with a non-const view can still modify the exact same data. For example: int* p2{new int{10}}; and const int* const q{p2};, then *p2 = 20; is allowed, but *q = 20; is not, even though p2 and q point at the same memory.


Code Example: main.cpp

C++
#include <iostream>

int main() {
    int x{23};
    std::cout << "x        = " << x  << '\n'; // prints: 23
    std::cout << "&x       = " << &x << '\n'; // prints: address of x

    int* p{&x};
    std::cout << "p        = " << p   << '\n'; // prints: address of x (same as &x)
    std::cout << "*p       = " << *p  << '\n'; // prints: 23

    *p = 50; // write through pointer into x
    std::cout << "x after  = " << x  << '\n'; // prints: 50 (x was changed via pointer)

    int** pp{&p};
    std::cout << "pp       = " << pp   << '\n'; // prints: address of p
    std::cout << "*pp      = " << *pp  << '\n'; // prints: address of x (same as p)
    std::cout << "**pp     = " << **pp << '\n'; // prints: 50

    int*** ppp{&pp};
    std::cout << "ppp      = " << ppp    << '\n'; // prints: address of pp
    std::cout << "*ppp     = " << *ppp   << '\n'; // prints: address of p (same as pp)
    std::cout << "**ppp    = " << **ppp  << '\n'; // prints: address of x (same as p)
    std::cout << "***ppp   = " << ***ppp << '\n'; // prints: 50

    int* q{};             // null pointer (safe, points nowhere)
    int* nullPtr{nullptr}; // same as above
    std::cout << "q        = " << q       << '\n'; // prints: 0
    std::cout << "nullPtr  = " << nullPtr << '\n'; // prints: 0

    std::cout << "sizeof(int*)    = " << sizeof(int*)    << '\n'; // prints: 8 (64-bit)
    std::cout << "sizeof(int**)   = " << sizeof(int**)   << '\n'; // prints: 8
    std::cout << "sizeof(double*) = " << sizeof(double*) << '\n'; // prints: 8 (all pointers same size)

    return 0;
}

Pointer to Char

Char Pointer Basics

A char pointer works like any other pointer: char* p creates a pointer to char, p = &x stores the address of a char variable, p gives the address, and *p gives the char data at that address.

String Literal and Char Pointer

cout treats a char* specially. When it points to a null-terminated character sequence, it prints the entire string rather than the address. For any other pointer type, it prints the address as usual. If you actually want the address of a char pointer, cast it: (void*)p.

Const Pointer vs Char Array

Both of these print as text through cout, since both refer to null-terminated character sequences: const char* message{"Hello, World!"} and char message[]{"Hello, World!"}. The difference is what you can actually do with them.

const char*char[]
Data cannot be modified through the pointer, since the underlying literal lives in read-only memory.
Data can be modified freely by index, since it's a real, writable array on the stack.

For a modifiable char string, always use an actual char array with [].


Code Example: main.cpp

C++
#include <iostream>

int main() {
    char x{'A'};
    char* p{&x};
    std::cout << "p (address): " << (void*)p << '\n'; // cast to void* to print address (otherwise cout prints string)
    std::cout << "*p (data):   " << *p       << "\n\n"; // prints: A

    // string literal: stored in read-only memory, const char* is required
    const char* message{"Hello, World!"};
    std::cout << "message:    " << message    << '\n';  // prints: Hello, World!
    std::cout << "*message:   " << *message   << '\n';  // prints: H (first char)
    std::cout << "message[1]: " << message[1] << "\n\n"; // prints: e

    // char array: data is modifiable
    char message2[]{"Hello, World!"};
    std::cout << "message2:    " << message2    << '\n'; // prints: Hello, World!
    std::cout << "*message2:   " << *message2   << '\n'; // prints: H
    std::cout << "message2[1]: " << message2[1] << '\n'; // prints: e
    message2[0] = 'h';
    message2[7] = 'w';
    std::cout << "message2 (after changes): " << message2 << '\n'; // prints: hello, world!

    return 0;
}

Program Memory Map

Why Virtual Memory Exists

Process and Memory Map

MMU: Address Translation

Memory Map Regions

RegionWhat Lives There
Text sectionThe binary machine code of the program. Read-only.
Data sectionGlobal and static variables.
StackLocal variables and function call frames. Managed automatically, with a fixed size decided at compile time.
HeapDynamic memory allocation. Controlled entirely by the programmer using new and delete. Larger and flexible.
System / kernel regionProtected memory used by the OS. Programs cannot access this directly.

For a C++ programmer, stack and heap are the two regions that actually matter day to day, they're the ones you're constantly making decisions about, which is exactly what the next section is about.


Dynamic Memory Allocation

Stack vs Heap

Stack MemoryHeap Memory
Smaller, fixed, and automatic.
Size decided at compile time, the compiler knows exactly how many bytes a function or scope needs.
Runtime uses a stack pointer to track the top of the stack (LIFO-based).
When a function or scope ends, the stack pointer moves back by the known number of bytes, releasing memory automatically.
The developer isn't in full control of memory lifetime.
Pointers are optional. Stack variables can be accessed directly by name.
Larger in size, flexible, and runtime-based.
Size is decided during program execution, not at compile time.
No fixed stack pointer exists to rewind heap memory automatically.
The developer is in full control of memory lifetime.
Pointers are mandatory to access heap memory, there are no names for heap objects.
new and delete operators are used.

How Dynamic Allocation Works

C++
int* p{};        // pointer on stack, initialized to nullptr
p = new int;    // heap memory for 1 int allocated; address assigned to p
*p = 34;        // write value into heap memory
std::cout << p;  // prints the address stored in p
std::cout << *p; // prints the value at that address (34)
delete p;        // destroys data and releases heap memory
p = nullptr;     // prevents dangling pointer

Heap Initialization Forms

C++
int* p{new int};      // heap int allocated, value uninitialized (garbage)
int* p{new int{}};    // heap int allocated, value-initialized to 0
int* p{new int(33)};  // heap int allocated, initialized to 33
int* p{new int{33}};  // heap int allocated, brace-initialized to 33 (preferred)

Memory Leaks

Do not write into an uninitialized pointer (int* p;), the compiler won't warn you, and the junk address may belong to the OS or some other part of memory entirely. Don't write into a nullptr either. And don't call delete twice on the same pointer before assigning it to new memory, if two pointers point to the same memory, only one of them should be the owner that actually calls delete.


Code Example: main.cpp

C++
#include <iostream>

int main() {
    // stack variables (automatic, named, no manual cleanup needed)
    int x = 10;
    int y = 20;
    std::cout << "Stack variables:\n";
    std::cout << "x = " << x << "\n"; // prints: 10
    std::cout << "y = " << y << "\n"; // prints: 20

    // heap memory (dynamic, unnamed, must be managed manually)
    int* p{};      // pointer on stack, nullptr
    p = new int;  // heap block allocated (no variable name)
    *p = 34;       // write into heap memory
    std::cout << "\nHeap memory:\n";
    std::cout << "Address in p: " << p  << "\n"; // prints: some address
    std::cout << "Value at p:   " << *p << "\n"; // prints: 34
    delete p;     // free heap memory
    p = nullptr;  // reset to prevent dangling pointer

    // different initialization forms
    int* a = new int;      // uninitialized (garbage)
    int* b = new int{};    // value-initialized to 0
    int* c = new int(33);  // initialized to 33
    int* d = new int{33};  // brace-initialized to 33
    std::cout << "\nInitialization forms:\n";
    std::cout << "*a (garbage): " << *a << "\n"; // unpredictable
    std::cout << "*b {}:        " << *b << "\n"; // prints: 0
    std::cout << "*c (33):      " << *c << "\n"; // prints: 33
    std::cout << "*d {33}:      " << *d << "\n"; // prints: 33
    delete a; a = nullptr;
    delete b; b = nullptr;
    delete c; c = nullptr;
    delete d; d = nullptr;

    return 0;
}

Closing

That's how pointers work and where the memory they point at actually lives, from a single address all the way up to the OS deciding what stack and heap even mean. Next up, part 2: everything that goes wrong with pointers, and how to keep it from going wrong.

← Previous 06 - Arrays Next → 08 - Pointers, Part 2: Safety and Arithmetic