← BackC30 Q

C

Pointers, memory, and the machine underneath

30 questions | entry to principal | answers written to be said out loud, not read

Entry

What is the difference between a pointer and an array in C?

An array is a block of contiguous storage, and its name decays to a pointer to the first element in almost every expression, which is why people say they are the same thing. They are not: sizeof on a real array gives me the size of the whole block, while sizeof on a pointer gives me the size of an address, usually 8 bytes on a 64 bit machine. An array name is also not something I can reassign, and the moment I pass an array into a function all I really receive is a pointer, so the length has to travel with it as a separate argument.

How does C represent a string?

A C string is just a char array whose end is marked by a NUL terminator, the zero byte, and nothing anywhere records the length for me. That means strlen walks the whole string every single time it is called, so a loop that calls it on each iteration is accidentally quadratic. It also means every buffer needs 1 extra byte for that terminator, and if I ever lose it, every string function in the library keeps reading straight past the end of my memory.

What is the difference between the stack and the heap?

Locals and function parameters live on the stack, which is really just a pointer that moves as frames get pushed and popped, so allocation costs almost nothing and cleanup happens automatically when the function returns. Anything I get from malloc lives on the heap instead, survives past the function that created it, and is my job to free. The classic beginner bug is returning the address of a local, because that stack frame is gone the instant the function returns even though the pointer still looks perfectly valid.

What does free actually do?

malloc asks the allocator for a block of a given size and hands me back a pointer, or NULL if it fails, and the memory I get is uninitialized garbage unless I used calloc. free does not erase anything and usually does not hand memory back to the operating system either, it simply returns the block to the allocator so a later malloc can reuse it. My variable still holds the same address after the call, which is exactly why I set the pointer to NULL immediately afterwards.

What is a union and when would you use one?

A union stores all of its members in the same block of memory, so its size is the size of the largest member and writing 1 member overwrites the others. I use it when a value is genuinely 1 of several shapes, and I always pair it with a separate tag field recording which member is currently valid, because the union itself tracks nothing. Reading a member I did not write is type punning, which C99 does allow through a union, but it is a thing I want to be deliberate about rather than accidental.

What does the preprocessor do, and why does every header need an include guard?

The preprocessor is a plain text substitution pass that runs before the compiler ever sees the file, expanding #include, macros and conditionals. Include guards exist because a header pulled in twice would redefine its types, so I wrap every header in #ifndef, #define and #endif with a unique name, or use #pragma once where I know the toolchain supports it. I keep macros to a minimum because they have no types and no scope, so a max macro that evaluates its argument twice will happily double an increment passed into it.

What belongs in a header file versus a .c file?

A header declares what exists, the prototypes, types and macros other files need, and the .c source file holds the actual definitions. The rule I follow is that a header should contain declarations only, because a definition in a header gets duplicated in every file that includes it and the linker then complains about multiple definitions. I also include a module's own header first inside its source file, so the compiler catches any drift between the prototype and the real definition instead of leaving it for link time.

How does error handling work in C without exceptions?

Most library calls signal failure through their return value, NULL from fopen or -1 from read, and then set the global errno to say why. The discipline is that I check the return value first and only then look at errno, because a successful call is allowed to leave errno holding a stale value from something earlier. I turn it into a message with strerror or perror, and in my own code I return an int status and keep a single cleanup block at the end of the function so nothing leaks on the error path.

Junior

How does pointer arithmetic work, and where does it stop being defined?

Pointer arithmetic works in units of the pointed to type rather than in bytes, so adding 1 to an int * moves the address forward by 4 bytes on a typical machine. That is also why subtracting 2 pointers into the same array gives me an element count instead of a byte count. The rule I keep in my head is that arithmetic is only defined inside one array, plus the position 1 past the end, so walking a pointer off the front of a buffer or comparing pointers from 2 unrelated allocations is undefined even when it appears to work.

What does const mean in different positions in a pointer declaration?

I read const declarations right to left. const char *p is a pointer to a constant character, so I can move the pointer but never write through it, while char * const p is a constant pointer to mutable data, and const char * const p locks both. In real code I put const on the function parameters I promise not to modify, because it documents intent and lets the compiler catch an accidental write instead of me finding it later.

What does static do at file scope versus inside a function?

static means 2 different things depending on where I write it. At file scope it gives a variable or a function internal linkage, so the symbol stays private to that translation unit and will not collide with a same named symbol in another file. Inside a function it changes storage duration instead: the variable lives for the whole program, keeps its value between calls, and is zero initialized exactly once, which is handy for a counter and a real problem the moment 2 threads call that function.

What are the gotchas with sizeof?

sizeof is a compile time operator rather than a function, and it does not evaluate its operand, so sizeof(i++) leaves i completely alone. The gotcha that catches people is that it reports the size of the declared type, so it gives me the full array size inside the function where the array was declared but only the pointer size once that array has been passed somewhere else. It also yields a size_t, which is unsigned, so comparing it against a signed length can turn a negative number into an enormous positive one.

What is a dangling pointer and why is use after free so dangerous?

A dangling pointer still holds an address after the thing it pointed at is gone, either because I freed it or because the stack frame it lived in returned. Using it is a use after free, and the ugly part is that it usually appears to work fine, right up until the allocator hands that block to somebody else and my write quietly corrupts unrelated state. My habits are to set the pointer to NULL right after free, to keep ownership obvious about who frees what, and to run everything under AddressSanitizer so it fires the moment it happens.

What does volatile tell the compiler?

volatile tells the compiler that a variable can change outside the normal flow of the program, so it has to reload it from memory on every read instead of caching it in a register or optimizing the whole loop away. The genuine uses are memory mapped hardware registers, a variable written by a signal handler, and a local that has to survive setjmp. What it does not give me is atomicity or any ordering guarantee between threads, and assuming otherwise is the single most common misuse I run into.

How do you tell a linker error from a compiler error?

A compiler error means 1 translation unit does not make sense on its own, a syntax error or a type mismatch or a missing declaration, and it points at a file and a line. A linker error means every file compiled fine but a symbol could not be resolved, so undefined reference means I declared it and never defined it, or I forgot to add the object file or the library to the link line. Multiple definition is the opposite problem, usually a non static definition sitting in a header, and the giveaway for both is that a linker error has no line number to point at.

Senior

Why is a struct sometimes bigger than the sum of its fields?

The compiler inserts padding bytes between struct members so each one lands on its natural alignment boundary, which is why a struct holding a char, an int and another char comes out at 12 bytes instead of 6. Because of that I never memcmp 2 structs and never write one straight to a file or a socket, since those padding bytes hold whatever garbage was in memory. When layout genuinely matters I order members from largest to smallest to shrink the padding, and I serialize field by field rather than trusting the compiler's layout.

How do you find and prevent memory leaks in a C program?

A leak is memory I allocated and then lost the last pointer to, so nothing can ever free it, and in a long running daemon that is what eventually gets the process killed. I find them with valgrind --leak-check=full or an AddressSanitizer build with LeakSanitizer enabled, both of which give me the allocation stack trace for every block still live at exit. The habit that prevents most of them is deciding who owns each allocation at the API boundary, and using a single cleanup label with goto so every early return unwinds the same way.

Why is strcpy considered dangerous?

strcpy copies until it hits the NUL terminator in the source and has no idea how big the destination is, so anything longer writes straight past the end of the buffer. On the stack that overwrite can reach the saved return address, which is the classic stack smashing exploit, and even when it is not exploitable it silently corrupts whatever happened to sit next in memory. I use snprintf with an explicit destination size and check the return value, because strncpy is not the safe drop in replacement people assume, it does not always terminate the string.

What is a function pointer and what do you use it for?

A function pointer holds the address of a function, so I can pass behaviour around as data, which is how qsort takes a comparison callback and how a driver exposes a table of operations. The declaration syntax is the part people trip over, int (*fp)(int, int), and the parentheses matter, because without them I have declared a function returning a pointer instead. I mostly reach for them to build dispatch tables that replace a long switch, and I always check the pointer is not NULL before calling through it.

What does undefined behaviour actually mean in C?

Undefined behaviour does not mean the compiler picks something reasonable, it means the standard places no requirements at all on the program, so the optimizer is entitled to assume it never happens and delete the code that only makes sense if it does. That is why a null check written after a dereference can vanish completely, and why the same source works in a debug build and breaks at higher optimization. I treat any UB as a real bug even when the output looks correct, and I build with -fsanitize=undefined so the compiler tells me before a customer does.

What happens when an integer overflows in C?

Signed integer overflow is undefined behaviour, while unsigned arithmetic is defined to wrap, and that difference matters far more than people expect. Because signed overflow is UB, the compiler may assume x + 1 > x always holds and optimize away the exact check I wrote to catch it, so I test before the operation, not after, or use a builtin like __builtin_add_overflow. The other trap is mixing a signed length with an unsigned size_t, where a negative value becomes an enormous positive one and sails straight past a bounds check.

What are integer promotions and the usual arithmetic conversions?

Anything narrower than int is promoted to int before arithmetic happens, so 2 char values added together are really added as int, and ~c on an unsigned char does not do what most people expect. The usual arithmetic conversions then push mixed operands toward the wider type and toward unsigned, which is how comparing a signed int against an unsigned value silently converts the signed side and turns a negative number into a huge one. I make the conversion explicit with a cast whenever types are mixed, and I turn on -Wconversion and -Wsign-compare so the compiler shows me the ones I missed.

How do you set, clear and test bits safely?

The moves I use constantly are x & (1u << n) to test a bit, x |= (1u << n) to set it, x &= ~(1u << n) to clear it, and x ^= (1u << n) to toggle it. The trap is the shift itself: shifting by more than the width of the type is undefined, and 1 << 31 on a 32 bit signed int is already wrong, so I write 1u and use unsigned types for anything bit twiddling. I also mask before I shift down when pulling a field out, and I never assume >> on a signed value fills with 0.

Staff

What does the restrict qualifier promise?

restrict on a pointer parameter is a promise to the compiler that, for the lifetime of that pointer, the object it points at is only reached through that pointer. That lets the optimizer keep a value in a register across a write instead of reloading after every store in case the 2 pointers alias, which is exactly why memcpy is declared with it and memmove is not. It is completely unchecked, so if I lie and the buffers really do overlap the result is undefined, and I only reach for it in a hot loop where I have measured the difference.

What is endianness and when does it matter?

Endianness is the byte order a machine uses for a multi byte integer, little endian storing the least significant byte first, which is what x86 and most ARM do, and big endian being the network byte order used on the wire. It only shows up when bytes cross a boundary, so I care about it in file formats, network protocols, and anything I memcpy into a struct. I convert explicitly with htonl and ntohl or by shifting bytes into place myself, rather than casting a byte buffer to an int * and hoping the 2 machines agree.

When would you use fork instead of threads?

fork gives me a whole new process with its own copy of the address space, so the child cannot corrupt the parent's memory and a crash only takes down 1 of them, but every bit of sharing has to go through a pipe or shared memory and the switches cost more. Threads share 1 address space, which makes sharing free and switching cheap, and that is exactly what makes them dangerous, because any thread can corrupt any other thread's data. I pick processes for isolation and fault tolerance, threads for tight data sharing, and I remember that after a fork in a threaded program only the calling thread survives in the child, so I stick to async signal safe calls until I exec.

What is a data race in C, and how do you deal with one?

A data race is 2 threads touching the same memory with at least 1 write and no synchronization between them, and in C11 terms that is undefined behaviour rather than merely a wrong value, so the compiler may already have optimized on the assumption it cannot happen. I fix it with a pthread_mutex_t around the shared state, or an _Atomic type when it is a single counter or flag, and I keep the critical section small while still covering the whole invariant. I always lock in a fixed global order to avoid deadlock, and I run the tests under ThreadSanitizer, because a race that never fires on my laptop will fire in production.

Principal

Why is volatile the wrong tool for sharing data between threads?

volatile is not a threading primitive, and treating it as one is the bug I see most often in otherwise senior code. It stops the compiler caching a value in a register, but it does not stop the compiler or the CPU from reordering the accesses around it, and it provides no atomicity at all, so a volatile counter still loses increments under contention. What I actually want is an _Atomic type with an explicit memory order, or a mutex, both of which carry the barriers that make a thread's writes visible to another in the order I expect. The only places volatile really belongs are hardware registers and a signal handler flag, where sig_atomic_t is the right type.

How do you talk to a hardware peripheral register from C?

On a microcontroller a peripheral register is just a memory address, so I declare it as a pointer to volatile uint32_t, and the volatile is what stops the compiler from folding away a write it thinks is dead or hoisting a poll out of a loop. The parts people miss are that some registers are write only or clear on read, so a read modify write against a status register can destroy flags, and that the compiler and the core are both free to reorder 2 accesses to different addresses, which is why I need a barrier between the data write and the trigger write. I also keep interrupt handlers short, share state with the main loop only through a sig_atomic_t style flag or a lock free ring buffer, and never call malloc from an interrupt.

Fast recall

pointer = an address into memory | dangling pointer = points at freed memory | NUL terminator = zero byte ending a string | static = internal linkage or persistent storage | volatile = reload from memory every read | restrict = promise of no aliasing | padding = bytes inserted for alignment | undefined behaviour = optimizer may assume anything | translation unit = one source file after preprocessing | linker = resolves symbols across objects | size_t = unsigned type for sizes | data race = unsynchronized concurrent access

BH·C·github.com/bunlongheng/study