C++ Notes
int, char) and non-primitive types (objects, arrays).*: int* px, double* py, long double* pz.int* p, int *p, int * p are all the same.&: int* p{&num}; stores the address of num in p (braces preferred).int* p{} is the same as int* p{nullptr}.sizeof(int*) == sizeof(double*) == sizeof(int**). 4 bytes on a 32-bit system, 8 bytes on a 64-bit system.* binds to the variable, not the base type: int* p{}, q{}; declares one pointer and one plain int, which is very misleading to read.p prints the stored address, p = &x assigns the address of x to p.*, dereferencing, works with the data at that address: *p prints the value stored at that address, *p = 23 writes 23 into the memory at that address.* to the type: int* is a pointer to int, int** is a pointer to int*, int*** is a pointer to int**.int*** stores the address of an int**, which stores the address of an int*, which stores the address of an int.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 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.
| Declaration | Modify value (*p = 5)? | Rebind pointer (p = &y)? | Use when |
|---|---|---|---|
const int* p(same as int const* p) | No | Yes | You want to protect the data through this pointer, but still allow it to point elsewhere later. |
int* const p | Yes | No | The pointer's identity must stay fixed, but mutating the data through it is fine. |
const int* const p | No | No | Both 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.
#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; }
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.
"Hello, World!" is an array of const char stored in read-only memory.char* message{"Hello, World!"};, because the literal is actually const char[], and assigning it to a non-const pointer is unsafe.const char* message{"Hello, World!"};. This pointer cannot be used to modify the data, but message[index] still works fine for reading any character.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.
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 [].
#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; }
| Region | What Lives There |
|---|---|
| Text section | The binary machine code of the program. Read-only. |
| Data section | Global and static variables. |
| Stack | Local variables and function call frames. Managed automatically, with a fixed size decided at compile time. |
| Heap | Dynamic memory allocation. Controlled entirely by the programmer using new and delete. Larger and flexible. |
| System / kernel region | Protected 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.
| Stack Memory | Heap 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. |
new creates a memory block on the heap, not a named variable.delete. Failing to do so causes a memory leak.nullptr to prevent a dangling pointer.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
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)
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.
#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; }
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.