C/C++ Patterns
Source: https://cs61.seas.harvard.edu/site/2025/Patterns/
Silence Intentional Unused Variables
Compile warning-free, but make intentional omissions explicit.
void* m61_malloc(size_t sz, const char* file, int line) {
(void) file, (void) line; // silence warnings
return malloc(sz);
}
Use this when a parameter will be used later, or is required by an interface but
not used by this implementation. The (void) cast says:
“I know this value is unused.”
For unused static functions in modern C++:
[[maybe_unused]] static void helper() {
}
Sized Integer Types
Include:
#include <cstdint>
Common choices:
uint8_t,int8_t: exactly 8 bits.uint16_t,int16_t: exactly 16 bits.uint32_t,int32_t: exactly 32 bits.uint64_t,int64_t: exactly 64 bits.uintptr_t,intptr_t: integer type the same size as a pointer.size_t: unsigned type for object sizes; returned bysizeof.ssize_t: signed counterpart with the same width assize_t.ptrdiff_t: type of pointer subtraction, as inp2 - p1.off_t: file sizes and file offsets; can be larger than memory-sized types.
Rule of thumb: choose a type that matches the domain. Use size_t for memory
object sizes, ptrdiff_t for pointer differences, fixed-width types when the
bit width matters, and uintptr_t only when you really need pointer-sized
integer behavior.
Linked List Basics
Terminology:
node: one item in the list.head: first node, ornullptrwhen empty.tail: last node, ornullptrwhen empty.next: moves head-to-tail.prev: moves tail-to-head.trav: conventional traversal variable name.
Linked lists support O(1) insertion/deletion when you already have the relevant node, but search is usually O(N).
Doubly Linked List With Head
Supports O(1):
- push front
- erase known node
struct node {
node* next;
node* prev;
};
struct list {
node* head = nullptr;
};
void list_push_front(list* l, node* n) {
n->next = l->head;
n->prev = nullptr;
if (l->head) {
l->head->prev = n;
}
l->head = n;
}
void list_erase(list* l, node* n) {
if (n->next) {
n->next->prev = n->prev;
}
if (n->prev) {
n->prev->next = n->next;
} else {
l->head = n->next;
}
}
Key edge case: erasing the head needs l->head = n->next.
Doubly Linked List With Head And Tail
Supports O(1):
- push front
- push back
- erase known node
struct list {
node* head = nullptr;
node* tail = nullptr;
};
void list_push_front(list* l, node* n) {
n->next = l->head;
n->prev = nullptr;
if (l->head) {
l->head->prev = n;
} else {
l->tail = n;
}
l->head = n;
}
void list_push_back(list* l, node* n) {
n->next = nullptr;
n->prev = l->tail;
if (l->tail) {
l->tail->next = n;
} else {
l->head = n;
}
l->tail = n;
}
void list_erase(list* l, node* n) {
if (n->next) {
n->next->prev = n->prev;
} else {
l->tail = n->prev;
}
if (n->prev) {
n->prev->next = n->next;
} else {
l->head = n->next;
}
}
Extra invariant: if the list has one node, that node is both head and tail.
Every insertion and deletion must preserve this.
Erasing While Iterating
Problem: erasing an element invalidates the iterator pointing to it.
Pattern:
- If you keep the element, increment the iterator.
- If you erase the element, assign the iterator returned by
erase.
void remove_even_items(std::map<std::string, int>& map) {
for (auto it = map.begin(); it != map.end(); ) {
if (it->second % 2 == 0) {
it = map.erase(it);
} else {
++it;
}
}
}
Do not write ++it after erase(it) unless the API specifically says that
remains valid.
Growable Arrays: Size And Capacity
Maintain:
size: number of initialized elements.capacity: number of allocated slots.
Invariant:
size <= capacity
Push-back rule:
- If
size < capacity, store the new element. - If
size == capacity, grow first. - Growth strategy: double capacity, with a small initial capacity such as 8.
struct vector_of_T {
T* data = nullptr;
size_t size = 0;
size_t capacity = 0;
};
T* vector_get(vector_of_T* v, size_t i) {
assert(i < v->size);
return &v->data[i];
}
void vector_push_back(vector_of_T* v, T new_element) {
if (v->size == v->capacity) {
size_t new_capacity = v->capacity ? v->capacity * 2 : 8;
v->data = (T*) realloc(v->data, sizeof(T) * new_capacity);
v->capacity = new_capacity;
}
v->data[v->size] = new_element;
++v->size;
}
Why doubling matters: growing by a fixed amount causes too many reallocations and copies. Doubling makes allocation overhead logarithmic in the maximum size reached.
In normal C++, prefer std::vector<T>. Learn this pattern to understand why
std::vector is efficient and how capacity-based growth works.