C++ Notes
nullptr, or multiple pointers pointing to the same memory where one of them deletes it and the rest become dangling.nullptr: int* p{};nullptr immediately after delete. This gives you a chance to check validity before using it again.delete.nullptr.nullptr as well.int* owner { new int{5} }; // owner (master pointer) int* slave { owner }; // non-owner pointer (same address) if (owner != nullptr) { std::cout << *slave << '\n'; // safe to use because owner is valid } else { std::cout << "memory already released!\n"; } delete owner; // only owner calls delete owner = nullptr; slave = nullptr; // reset non-owner too to prevent dangling
#include <iostream> int main() { // uninitialized pointer (safe way) int* p1{}; // nullptr, safe // int* p1; // dangling (junk address), dangerous // deleted pointer (reset immediately after delete) int* p2 = new int{10}; delete p2; p2 = nullptr; // prevents dangling // multiple pointers: owner model int* owner { new int{5} }; // master pointer int* slave { owner }; // non-owner, read/write only if (owner != nullptr) { std::cout << *slave << "\n"; // prints: 5 } delete owner; // only owner deletes owner = nullptr; slave = nullptr; // reset to prevent dangling return 0; }
new operator fails to allocate heap memory, usually because the system is out of memory.new throws an exception of type std::bad_alloc.The two code examples below intentionally exhaust memory for learning purposes. Do not run them normally. Infinite loops with heap allocation and no delete will freeze the system, not just crash the one program.
new, which throws on failure.try block and catch std::exception&.e.what() returns a message describing the failure.#include <iostream> #include <new> int main() { try { while (true) { int* block { new int[1'000'000] }; // ~4 MB per iteration std::cout << "Allocated memory\n"; } } catch (std::exception& e) { std::cout << "Memory allocation failed: " << e.what() << '\n'; } return 0; }
new that does not throw exceptions.nullptr instead of throwing.#include <iostream> #include <new> int main() { while (true) { int* block { new (std::nothrow) int[1'000'000] }; // no exception on failure if (block == nullptr) { std::cout << "Memory allocation failed\n"; // caught via nullptr check break; } std::cout << "Allocated memory\n"; } return 0; }
true when it contains a valid, non-null address, false when it contains nullptr.if statement.int* p{}; if (p) { std::cout << "pointer contains VALID address\n"; } else { std::cout << "pointer contains INVALID address\n"; }
Calling delete on a nullptr multiple times is completely safe, the standard guarantees it's a no-op. What's unsafe is calling delete on a pointer that still holds an address to already-freed memory, which is exactly the case the null-reset habit is meant to prevent.
#include <iostream> int main() { int* p{}; // nullptr if (p) { std::cout << "pointer contains VALID address\n"; } else { std::cout << "pointer contains INVALID address\n"; // prints this } // deleting nullptr multiple times is completely safe (standard guarantees it) delete p; // safe: no-op delete p; // safe: no-op delete p; // safe: no-op return 0; }
| Capability | Stack Array | Dynamic Array |
|---|---|---|
std::size() | Works | Compilation error, the array has decayed to a pointer. |
| Range-based for-each loop | Works | Not possible, the compiler needs to know the size. |
sizeof() | Returns the full array's size | Returns the size of the pointer, not the array. |
| Length tracking | Automatic | Not stored anywhere. The programmer must track it manually. |
When a dynamic array is created, it immediately decays into a pointer to its first element. Only the base address is kept, the array's length is completely lost the moment that happens, which is exactly why every row above breaks the same way.
double* marks { new double[10] }; // uninitialized elements int* marks { new (std::nothrow) int[10]{} }; // zero-initialized, nothrow double* marks { new (std::nothrow) double[5]{1,2,3,4,5} }; // partially initialized
| Expression | Meaning |
|---|---|
marks | Address of element 0, the base address. |
*marks | Value of the first element. |
(marks + i) | Address of element i. |
*(marks + i) | Value at element i. |
marks[i] | Value at element i, same as above. |
&marks[i] | Address of element i, same as marks + i. |
So marks[i] == *(marks + i) and &marks[i] == (marks + i) always hold, indexing is really just pointer arithmetic wearing a friendlier syntax.
Use delete[], not delete. Using plain delete on an array is undefined behavior.
delete[] marks; marks = nullptr;
int n = 5; int* arr = new int[n]; is valid, and size_t n = 5; double* marks = new double[n]; is valid too.constexpr values work.#include <iostream> int main() { int size = 5; // runtime variable allowed for heap arrays double* marks = new (std::nothrow) double[size]{1, 2, 3, 4, 5}; std::cout << "Base address (marks): " << marks << '\n'; // prints: address std::cout << "First element (*marks): " << *marks << "\n\n"; // prints: 1 // accessing elements via index and pointer arithmetic for (int i = 0; i < size; ++i) { std::cout << "marks[" << i << "] = " << marks[i] << " | address = " << (marks + i) << '\n'; } std::cout << "\nEquivalence check:\n"; std::cout << "(marks + 2) = " << (marks + 2) << '\n'; // address of element 2 std::cout << "*(marks + 2) = " << *(marks + 2) << '\n'; // prints: 3 std::cout << "marks[2] = " << marks[2] << '\n'; // prints: 3 (same) std::cout << "&marks[2] = " << &marks[2] << '\n'; // address of element 2 (same) delete[] marks; // delete[] for arrays, not delete marks = nullptr; return 0; }
sizeof(type) bytes per step, not 1 byte.int* p; p + 1 // moves forward by sizeof(int) bytes = 4 bytes double* q; q + 1 // moves forward by sizeof(double) bytes = 8 bytes
When you write base_address + index, the compiler silently does base_address + (index * sizeof(type)). Pointer arithmetic is element-based, not byte-based, the compiler handles the scaling for you every time.
| Valid | Invalid |
|---|---|
| ptr + n // move forward n elements ptr - n // move backward n elements ++ptr // move to next element --ptr // move to previous element ptr1 - ptr2 // distance in elements (same array only) |
ptr + ptr // NOT allowed ptr * 2 // NOT allowed ptr / 2 // NOT allowed void* math // arithmetic on void* not allowed in standard C++ |
An array name acts like a pointer to its first element: arr == &arr[0], both give the address of element 0.
int arr[5]{10,20,30,40,50}; arr // address of element 0 arr + 1 // address of element 1 *(arr + 2) // value of element 2 = 30
arr and &arr store the same numeric address, the start of the array.arr is of type int*. Adding 1 moves by 1 int, 4 bytes.&arr is of type int (*)[10], a pointer to the entire array object. Adding 1 moves past the whole array, 10 ints, 40 bytes.A classic trick that exploits the type difference between arr and &arr to compute the number of elements at compile time.
int arr[] = {0,1,2,3,4,5,6,7,8,9}; int size = *(&arr + 1) - arr;
arr is type int*, pointing to element 0. &arr is type int(*)[10], pointing to the entire array object. Both hold the same numeric address, but arithmetic scales differently because of their types.&arr is a pointer to the whole array of 10 ints, adding 1 jumps past the entire array. On a system where int is 4 bytes, that's 40 bytes forward. The result is a pointer to the memory location just past arr[9], which is conceptually arr[10].int[10]. That object immediately decays into a pointer to its first element, so *(&arr + 1) behaves as an int* pointing to what would be arr[10].int* gives the number of elements between them, not raw bytes. The compiler divides the byte difference by sizeof(int) automatically. Result: (address of arr[10]) minus (address of arr[0]) equals 40 bytes divided by 4, which is 10 elements.This trick works only for stack arrays, and only within the same scope where the array was declared. The moment it's passed to a function, &arr behaves as int**, because the compiler no longer knows the array size. For heap arrays, &arr doesn't point to the entire array object at all, so the trick doesn't apply there, for heap arrays, always store the size in a separate variable.
#include <iostream> int main() { int arr[]{10, 20, 30, 40, 50}; // arr and &arr hold the same address but have different types std::cout << "arr = " << arr << '\n'; // address of element 0 std::cout << "&arr = " << &arr << '\n'; // same numeric address, different type std::cout << "arr + 1 = " << (arr + 1) << '\n'; // moves by sizeof(int) = 4 bytes std::cout << "&arr + 1 = " << (&arr + 1) << '\n'; // moves by sizeof(int[5]) = 20 bytes // pointer arithmetic equivalences std::cout << "\nPointer arithmetic:\n"; std::cout << "*(arr + 0) = " << *(arr + 0) << '\n'; // prints: 10 std::cout << "*(arr + 2) = " << *(arr + 2) << '\n'; // prints: 30 std::cout << "arr[2] = " << arr[2] << '\n'; // prints: 30 (same as above) std::cout << "&arr[2] = " << &arr[2] << '\n'; // address of element 2 // size trick using type difference between arr and &arr int size = *(&arr + 1) - arr; std::cout << "\nSize via trick: " << size << '\n'; // prints: 5 // dynamic array: pointer arithmetic only, must track size manually int* darr = new int[5]{1, 2, 3, 4, 5}; std::cout << "\nDynamic array:\n"; std::cout << "*(darr + 3) = " << *(darr + 3) << '\n'; // prints: 4 std::cout << "darr[3] = " << darr[3] << '\n'; // prints: 4 (same) delete[] darr; darr = nullptr; return 0; }
That's the full pointer picture: how they fail silently, how to keep them from failing at all, how arrays and pointers are really the same thing underneath, and one genuinely clever compile-time trick to close it out. Next up, references, the safer cousin of everything covered across these two posts.