Skip to main content

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

Types & Variables

Primitive Types

Critical: int is NOT always 32 bits. On embedded systems it might be 16 bits. Use int32_t when you need exactly 32 bits.

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

Array Decay: When you pass an array to a function, it decays to a pointer. You LOSE size information. Always pass size as a separate parameter.

Multi-dimensional Arrays


Strings

Strings in C are just null-terminated character arrays. There is no String 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 an int 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.
Optimization: Order struct members from largest to smallest to minimize padding. This is especially important in hot paths and large arrays.

Bit Fields


Unions

Unions share memory between members—only one is valid at a time.

Enums


Compilation Model


Quick Exercises

1

Type Sizes

Write a program that prints the size of all basic types on your system. Note which are different from what you expected.
2

Struct Padding

Create a struct with 3 members of different sizes. Use sizeof and offsetof to visualize the padding.
3

String Manipulation

Implement a safe string_concat function that takes a destination buffer, its size, and two source strings, returning true if concatenation succeeded.
4

Tagged Union

Implement a JSON-like value type using a tagged union that can hold null, bool, int, double, string, or error.

Next Up

Build Systems & Toolchain

Master GCC, Make, CMake, and the compilation process

Interview Deep-Dive

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)1 evaluates to false because -1 is converted to UINT_MAX (4294967295 on a 32-bit system), and 4294967295 > 1.
  • This is a real security vulnerability. In code like if (user_length < buffer_size) where user_length is 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 subsequent memcpy(buf, src, user_length) interprets -1 as SIZE_MAX bytes, causing a massive buffer overflow.
  • The mitigation is to always check for negative values before mixed comparisons, use -Wsign-compare and -Wconversion compiler flags, and prefer size_t (unsigned) for all sizes and indices.
Follow-up: A junior developer argues that -Wall would catch this. Is that correct?Follow-up Answer:
  • Not entirely. -Wall includes -Wsign-compare on GCC, which catches the comparison case, but it does not catch all implicit signed-to-unsigned conversions in arithmetic. You need -Wconversion and -Wsign-conversion for 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.
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 to struct {'{'}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 sizeof and offsetof to 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.
Follow-up: How do you use _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.
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 catch old_style(1, 2, 3) even if the function expects zero arguments.
  • int modern_style(void) explicitly declares that the function takes no arguments. Calling modern_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 write int main() instead of int main(void) often have not internalized the distinction.
Follow-up: What are calling conventions, and why do they matter when linking C code with assembly or other languages?Follow-up Answer:
  • 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.