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.
C Syntax Speed Run
You know how to program. Let’s map your existing knowledge to C in record time. If you have written Python, JavaScript, Java, or Go, most of the control flow will look familiar. The key differences: C gives you no garbage collection (you manage memory yourself), no built-in strings (just null-terminated character arrays), no exceptions (you check return values), and no runtime safety checks (the program trusts that you know what you are doing). These differences are not limitations — they are the reason C code can run as fast as the hardware allows.Time: 4-6 hours
Goal: Write C code confidently
Assumed: You understand variables, functions, loops, arrays from another language
Goal: Write C code confidently
Assumed: You understand variables, functions, loops, arrays from another language
Types & Variables
Primitive Types
Type Sizes
Integer Promotion & Conversions
C’s implicit type conversions are one of the most common sources of subtle bugs. The rules are well-defined but counterintuitive. The general principle: when you mix types in an expression, the compiler “promotes” smaller types to larger ones. But when you mix signed and unsigned, the signed value gets converted to unsigned — and that conversion can turn a negative number into a huge positive number.Control Flow (Quick Reference)
The Comma Operator
Functions
Declaration vs Definition
Pass by Value (Always!)
Static Functions
Inline Functions (C99)
Arrays
Basic Arrays
Multi-dimensional Arrays
Strings
Strings in C are just null-terminated character arrays. There is noString type, no .length() method, no bounds checking. A “string” is a char* pointing to the first byte of a sequence that ends with a '\0' byte. Every string operation must manually account for that null terminator, and every buffer that holds a string must have room for it. Forgetting the null terminator or writing past the buffer end is the root cause of the majority of C security vulnerabilities in the wild.
If you come from Python where s = "hello" + " world" just works, C strings will feel primitive. They are. That is the price of zero-overhead abstractions — and the reason safe string handling is a core skill, not an afterthought.
Structs
Basic Structs
Struct Memory Layout & Padding
The compiler inserts invisible “padding” bytes between struct members to ensure each field starts at a memory address that is a multiple of its size. Why? Because most CPUs can only read anint from an address that is a multiple of 4. An unaligned access is either slow (x86, which silently handles it in hardware) or a crash (ARM, which raises a bus error). The compiler pads to prevent this.
This means the order of struct fields affects the struct’s total size. Same fields, different order, different memory footprint.
Bit Fields
Unions
Unions share memory between members—only one is valid at a time.Enums
Compilation Model
Quick Exercises
Type Sizes
Write a program that prints the size of all basic types on your system. Note which are different from what you expected.
Struct Padding
Create a struct with 3 members of different sizes. Use
sizeof and offsetof to visualize the padding.String Manipulation
Implement a safe
string_concat function that takes a destination buffer, its size, and two source strings, returning true if concatenation succeeded.Next Up
Build Systems & Toolchain
Master GCC, Make, CMake, and the compilation process
Interview Deep-Dive
What happens when you compare a signed int with an unsigned int in C? Why is this dangerous?
What happens when you compare a signed int with an unsigned int in C? Why is this dangerous?
Strong Answer:
- When a signed and unsigned integer appear in the same expression, the signed value is implicitly converted to unsigned before the comparison. If the signed value is negative, it wraps around to a very large unsigned value. For example,
(int)-1 < (unsigned int)1evaluates to false because-1is converted toUINT_MAX(4294967295 on a 32-bit system), and4294967295 > 1. - This is a real security vulnerability. In code like
if (user_length < buffer_size)whereuser_lengthis a signed int from untrusted input, an attacker can pass-1, which passes the check (because it becomes a huge unsigned value), and then the subsequentmemcpy(buf, src, user_length)interprets-1asSIZE_MAXbytes, causing a massive buffer overflow. - The mitigation is to always check for negative values before mixed comparisons, use
-Wsign-compareand-Wconversioncompiler flags, and prefersize_t(unsigned) for all sizes and indices.
-Wall would catch this. Is that correct?Follow-up Answer:- Not entirely.
-Wallincludes-Wsign-compareon GCC, which catches the comparison case, but it does not catch all implicit signed-to-unsigned conversions in arithmetic. You need-Wconversionand-Wsign-conversionfor broader coverage. Even then, some cases slip through. The belt-and-suspenders approach is compiler warnings plus explicit validation of all untrusted signed values before they participate in unsigned operations.
Explain struct padding and alignment in C. How would you minimize wasted space?
Explain struct padding and alignment in C. How would you minimize wasted space?
Strong Answer:
- The CPU accesses memory most efficiently when data is aligned to its natural boundary: a 4-byte int must sit at an address divisible by 4, an 8-byte double at an address divisible by 8. The compiler inserts invisible padding bytes between struct members to satisfy these constraints. The struct itself is padded at the end to a multiple of its largest member’s alignment.
- For example,
struct {'{'}char a; int b; char c;{'}'}on a typical 64-bit system is 12 bytes (1 + 3 padding + 4 + 1 + 3 padding), not 6. Reordering tostruct {'{'}int b; char a; char c;{'}'}yields 8 bytes (4 + 1 + 1 + 2 padding) — a 33% reduction. - The general rule: order members from largest to smallest alignment. Use
sizeofandoffsetofto verify. For arrays of millions of structs (particle systems, database rows), this padding waste multiplies — saving 4 bytes per struct across 10 million structs saves 40 MB of memory and dramatically improves cache utilization. - When you need exact byte-level control (network protocols, file formats), use
__attribute__((packed))or#pragma pack(1), but be aware that packed structs cause unaligned memory access, which is slow on x86 and crashes on ARM.
_Static_assert to enforce struct layout assumptions at compile time?Follow-up Answer:_Static_assert(sizeof(struct NetworkPacket) == 8, "NetworkPacket must be exactly 8 bytes");will fail compilation if the struct is not the expected size. This is essential for binary formats and network protocols where the on-wire representation must match exactly. You can also assert offsets:_Static_assert(offsetof(struct Packet, flags) == 4, "flags must be at offset 4");. This catches breakage immediately when someone adds a field or changes a type, rather than producing silent data corruption at runtime.
What is the difference between 'int old_style()' and 'int modern_style(void)' in C?
What is the difference between 'int old_style()' and 'int modern_style(void)' in C?
Strong Answer:
int old_style()is an old-style (K&R) function declaration that says nothing about the parameters. It accepts any number of arguments of any type without a compiler warning. This is a holdover from pre-ANSI C and is a source of subtle bugs — the compiler will not catchold_style(1, 2, 3)even if the function expects zero arguments.int modern_style(void)explicitly declares that the function takes no arguments. Callingmodern_style(42)produces a compile-time error, which is what you want.- In C23, the empty parentheses
()in a function declaration finally means “no parameters” (matching C++ behavior), but until your codebase targets C23 exclusively, always use(void)for parameterless functions. This is a common interview signal: candidates who writeint main()instead ofint main(void)often have not internalized the distinction.
- A calling convention specifies how arguments are passed (registers vs. stack, which registers, what order), who cleans up the stack (caller vs. callee), and which registers must be preserved across calls. On x86-64 Linux (System V ABI), the first six integer arguments go in rdi, rsi, rdx, rcx, r8, r9; floating-point in xmm0-xmm7; return value in rax. On Windows x64, the convention is different (rcx, rdx, r8, r9). If you link C code with hand-written assembly or a library compiled with a different convention, arguments land in the wrong registers and you get silent data corruption or crashes. This is also why
extern "C"exists in C++ — it tells the compiler to use C’s calling convention and name mangling rules so C++ code can interoperate with C libraries.