Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Pointers Deep Dive
Pointers are C’s superpower. They enable direct memory manipulation, efficient data structures, and system-level programming. While they have a reputation for being difficult, the core concept is simple: a pointer is a variable that stores a memory address, like a sticky note with a house number on it. The sticky note is not the house — it just tells you where the house is. If you come from Python or JavaScript, you have already used pointers — you just did not know it. Every time you pass an object to a function in Python, you are passing a reference (which is a pointer with guardrails). C gives you the same thing, minus the guardrails.Pointers in C
At its core, a pointer is just a number—the address of a byte in memory.Why Does C Have Pointers?
The Fundamental Problem
Before learning pointer syntax, understand why they exist: The problem: How do you write efficient, low-level code? In high-level languages, you work with “values”:- Pass a 1MB struct to a function? Copy all 1MB
- Return a large array? Copy it back
- Modify a parameter? Can’t affect the original
- Pass a pointer (8 bytes) instead of copying 1MB
- Return a pointer instead of copying data
- Modify data through a pointer to affect the original
- Implement efficient data structures (linked lists, trees)
- Interface with hardware (memory-mapped I/O)
- Implement system calls (kernel needs to modify user memory)
- Build operating systems and drivers
Pointer Fundamentals
Memory Model
Pointer Types
Pointer Arithmetic
Pointer arithmetic is one of the most confusing yet powerful features of C. It allows you to navigate memory based on the type of data you are pointing to.What’s Allowed
Pointer Arithmetic Visualized
p + 1, the compiler doesn’t add 1 byte to the address. Instead, it adds sizeof(*p) bytes. This is why pointer arithmetic “just works” with arrays:
- Enables efficient array traversal
- Allows treating arrays as pointers
- Foundation for dynamic data structures
- Critical for understanding
sizeofand alignment
Arrays and Pointers
Array Decay
In C, arrays and pointers are closely related but NOT identical. The most important concept to grasp is “array decay” — the automatic conversion of an array name into a pointer to its first element. Think of it like this: if you give someone your home address, they know where you live but they do not know how many rooms your house has. That “loss of size information” is exactly what happens when an array decays to a pointer.Multi-dimensional Arrays
Pointers to Pointers
Array of Pointers vs Pointer to Array
Const Correctness
Void Pointers
Avoid* is C’s way of saying “I’m pointing to something, but I’m not going to tell you what type it is.” It is the foundation of generic programming in C — the same pattern that languages like Java use generics and Rust uses trait objects for. The standard library’s qsort, bsearch, and pthread_create all use void* to work with arbitrary data types.
The tradeoff: you gain flexibility but lose type safety. The compiler cannot check that you are casting back to the correct type. This is where discipline and documentation matter.
Generic Container with void*
The Restrict Keyword
Therestrict keyword is a promise you make to the compiler: “I guarantee that this pointer is the only way to access this memory region.” It is not a runtime check — it is a contract. If you break this contract, the compiler will generate wrong code silently.
Why does this matter? Without restrict, the compiler must assume that any two pointers might point to overlapping memory. This forces it to reload values from memory after every store, preventing many powerful optimizations.
Function Pointers
Common Pitfalls
Dangling Pointers
A dangling pointer is like a hotel room key for a room that has already been cleaned and reassigned to another guest. The key still “works” — it opens a door — but whatever you find behind that door is unpredictable and definitely not what you left there.Null Pointer Dereference
Wild Pointers
A wild pointer is an uninitialized pointer that contains whatever garbage bits happened to be in that stack location before. Dereferencing it is like dialing a random phone number and shouting instructions — you have no idea who is on the other end or what damage you might cause.Exercises
Pointer Puzzle
Given
int a = 5, *p = &a, **pp = &p;, trace what each of these expressions evaluates to: *p, **pp, *pp, &p, &a, p[0].Custom memcpy
Implement your own
void *my_memcpy(void *dest, const void *src, size_t n) using only pointer operations.Reverse Array In-Place
Write
void reverse(int *arr, size_t n) using only pointer arithmetic (no array indexing []).Generic Sorting
Write a sorting function that accepts array, size, element size, and comparison function pointer, like
qsort.Next Up
Memory Layout & Segments
Understand how programs use memory
Interview Deep-Dive
What is the strict aliasing rule, and how can violating it cause bugs that only appear at -O2?
What is the strict aliasing rule, and how can violating it cause bugs that only appear at -O2?
Strong Answer:
- The strict aliasing rule (C99 6.5/7) says that an object must only be accessed through a pointer to its declared type (or a compatible type, or
char*). Accessing anintthrough afloat*is undefined behavior. The compiler is allowed to assume that pointers of different types never alias the same memory, which enables aggressive optimizations like reordering loads and stores. - At
-O0, the compiler generates naive code that reads from memory every time, so aliasing violations often “work.” At-O2, the compiler caches values in registers across stores to unrelated pointer types. If you write*(float*)&my_int = 3.14f;and then readmy_int, the compiler may return the old value ofmy_intbecause it assumes thefloat*write could not have affected theintvariable. - The safe alternatives are: use
memcpyfor type punning (compilers optimize it to zero overhead), use a union (defined behavior in C but not C++), or access throughchar*/unsigned char*which is explicitly allowed to alias anything. - Real-world example: network code that casts a
char[]receive buffer to astruct ip_header*technically violates strict aliasing. Usingmemcpyto copy the bytes into a properly typed struct is both correct and equally fast after optimization.
restrict keyword relate to aliasing, and when should you use it?Follow-up Answer:restrictis a promise you make to the compiler: “This pointer is the only way to access the memory it points to during its lifetime.” This goes beyond strict aliasing — even twoint*pointers might alias the sameint, and the compiler must account for that. Withrestrict, the compiler knows they do not overlap and can keep values in registers across stores, vectorize loops, and reorder operations freely.memcpyusesrestricton both parameters (source and destination must not overlap);memmovedoes not. Misusingrestrict(passing overlapping buffers to arestrict-qualified function) is undefined behavior that produces silently wrong results — not a crash, just incorrect output.
Explain the difference between 'char *s = "hello"' and 'char s[] = "hello"'. What goes wrong if you confuse them?
Explain the difference between 'char *s = "hello"' and 'char s[] = "hello"'. What goes wrong if you confuse them?
Strong Answer:
char *s = "hello"makessa pointer that points to a string literal stored in read-only memory (the.rodatasection). The pointer itself lives on the stack (or as a global), but the string data is shared and immutable. Writings[0] = 'J'is undefined behavior — on most systems it causes a segfault because the page is mapped read-only.char s[] = "hello"creates an array of 6 bytes on the stack (5 characters plus the null terminator) and copies the literal’s contents into it. The array is fully mutable.s[0] = 'J'changes it to “Jello” with no issues.- The subtle trap:
sizeof(s)is different. For the pointer,sizeof(s)is 8 (pointer size on 64-bit). For the array,sizeof(s)is 6 (the full array including null terminator). This matters when computing buffer sizes. - In production, always declare string literal pointers as
const char *s = "hello". This way the compiler warns you if you try to modify the literal, and it documents the intent. Code that omits theconstis a red flag in code review.
- When an array name appears in most expressions (including as a function argument), it “decays” to a pointer to its first element. Inside the called function, the parameter is a pointer —
sizeofreturns the pointer size (8 bytes), not the array size. This is a design decision from C’s origins: arrays were designed to be lightweight and interchangeable with pointers for efficiency. The practical consequence is that you must always pass the array size as a separate parameter. Forgetting this is the root cause of countless buffer overflows. Modern C practice is to pair every array parameter with asize_tlength parameter, and some coding standards enforce this via naming conventions likevoid process(const int *data, size_t data_len).
A function returns a pointer to a local variable. Describe exactly what happens and how you would detect this bug.
A function returns a pointer to a local variable. Describe exactly what happens and how you would detect this bug.
Strong Answer:
- When a function returns, its stack frame is deallocated — the stack pointer moves back up. The memory where the local variable lived is not zeroed or protected; it simply becomes “available” for the next function call to reuse. The returned pointer now points to this reclaimed memory. Dereferencing it may return the original value (by luck, if no other function has overwritten that stack slot yet), garbage from a subsequent function call, or crash with a segfault if the page has been unmapped (rare for stack memory).
- This is undefined behavior, and the insidious part is that it often “works” in simple test cases (the stack slot has not been reused yet) and only fails in production when a different call pattern overwrites the memory.
- Detection:
-Wreturn-local-addr(GCC) and-Wreturn-stack-address(Clang) catch the obvious cases at compile time. AddressSanitizer (-fsanitize=address) catches it at runtime by placing red zones around stack variables and detecting access after the function returns. Valgrind’s memcheck also flags this as “use of uninitialized value” in many cases. In code review, any function returning a pointer should be scrutinized: the pointer must refer to heap memory (malloc), static/global memory, or memory owned by the caller (passed in as a parameter).
- Return a heap-allocated object (caller is responsible for
free). Return by value for small structs (C copies the struct into the caller’s stack frame — efficient for structs up to ~64 bytes). Accept a caller-provided buffer as a parameter (void get_name(char *buf, size_t buf_size)). Use a static local variable (safe but not thread-safe and not reentrant — each call overwrites the previous result). Of these, the caller-provided buffer pattern is the most common in production C code because it gives the caller full control over allocation lifetime and strategy.
How does qsort work internally, and what is the subtle bug in the common comparison function 'return a - b'?
How does qsort work internally, and what is the subtle bug in the common comparison function 'return a - b'?
Strong Answer:
qsorttakes an array, its length, element size, and a comparison function pointer. It uses the comparison function to order arbitrary data types without knowing their structure. Internally, most implementations use a hybrid algorithm: introsort (quicksort with fallback to heapsort when recursion depth gets too deep) for the general case, with insertion sort for small subarrays. Sinceqsortoperates onvoid*, it usesmemcpyor byte-level swaps to move elements, which is slower than type-aware sorting but fully generic.- The classic bug in
return *(int*)a - *(int*)bis signed integer overflow. IfaisINT_MAXandbis-1, the subtraction producesINT_MAX - (-1) = INT_MAX + 1, which overflows a signed int (undefined behavior). The result wraps toINT_MIN(a negative number), tellingqsortthatINT_MAX < -1, which is wrong. This corrupts the sort order. - The safe comparison pattern is
return (a > b) - (a < b), which returns -1, 0, or 1 without any arithmetic that could overflow. This is the pattern you should always use.
qsort use void* instead of being type-specific, and what is the performance cost?Follow-up Answer:- C has no templates or generics at the language level, so
qsortmust work withvoid*for universality. The cost is twofold: every comparison requires a function pointer call (which the compiler cannot inline, defeating branch prediction and instruction cache locality), and every swap is a byte-by-bytememcpyinstead of a single register move. Benchmarks show that a hand-written type-specific sort can be 2-5x faster thanqsorton the same data. In performance-critical code, you either write your own sort or use a macro-generated sort (the Linux kernel’ssort()function avoids function pointer overhead by inlining the comparison).