Master the foundations of systems programming, memory management, and low-level computer science with C.
February 2026
C is often called a “high-level assembly language.” This description, while reductive, captures the essence of the language: it provides the abstractions necessary for structured programming while maintaining a transparent mapping to the underlying hardware. Unlike languages like Java or Python, which run on a Virtual Machine (JVM) or through an interpreter, C is designed around the concept of an Abstract Machine that closely mirrors the von Neumann architecture.
When you write C, you are not writing for a specific CPU (like an Intel i9 or an Apple M3); you are writing for the C Abstract Machine. The C Standard (ISO/IEC 9899) defines how this machine behaves.
Key characteristics of the C Abstract Machine include:
A C program undergoes a rigorous transformation process before the hardware can execute it. Understanding this pipeline is critical for debugging linker errors and optimizing build times.
cpp)The preprocessor handles directives starting with #. It performs text substitution, includes header files, and handles conditional compilation. It does not understand C syntax; it is essentially a sophisticated “find-and-replace” engine.
cc1)The compiler proper translates the preprocessed C code into assembly language specific to the target architecture (x86, ARM, RISC-V). This is where syntax checking, type checking, and optimization occur.
as)The assembler converts the human-readable assembly instructions into machine code (binary). The result is an Object File, which contains machine instructions but may have unresolved references to functions or variables defined in other files.
ld)The linker resolves those external references. It combines multiple object files and static libraries into a single executable. It also maps the logical addresses in the object files to final memory addresses.
In a modern operating system, every running C process is given a virtual address space. This space is typically organized into several segments:
| Segment | Description | Lifetime |
|---|---|---|
| Text | The actual machine instructions (read-only). | Program Duration |
| Data | Global and static variables initialized by the programmer. | Program Duration |
| BSS | Global and static variables uninitialized (set to zero). | Program Duration |
| Heap | Memory allocated at runtime via malloc or calloc. | Manual |
| Stack | Local variables and function call frames. | Function Scope |
In the C Abstract Machine, the environment calls a specific function to begin execution. While this usually looks like int main(void), the signature can vary depending on whether you need command-line arguments.
/* The signature for a program that ignores arguments */ int (void) { return 0; }
Perhaps the most important concept in C is Undefined Behavior. If your code violates the rules of the Abstract Machine (e.g., dereferencing a null pointer or accessing an array out of bounds), the C standard says anything can happen. The compiler is not required to catch these errors. This is the “double-edged sword” of C: absolute power and absolute responsibility.
Waiting for signal...
A C program is a sequence of Tokens. The compiler’s lexer breaks your source code into these basic units: Keywords, Identifiers, Constants, String Literals, and Punctuators.
int MyVar; and int myvar; refer to two distinct memory locations.C’s type system is designed to expose the underlying hardware’s capabilities. Unlike managed languages, C types often have implementation-defined sizes.
The size of int is typically the “natural” size of the processor’s word (32-bits on most modern systems). However, the standard only guarantees minimum ranges:
| Type | Minimum Size | Guaranteed Range |
|---|---|---|
char | 8 bits | -127 to 127 (or 0 to 255) |
short | 16 bits | -32,767 to 32,767 |
int | 16 bits | -32,767 to 32,767 |
long | 32 bits | -2,147,483,647 to 2,147,483,647 |
long long | 64 bits | -(2^63 - 1) to (2^63 - 1) |
C uses the IEEE 754 Standard for floating-point arithmetic.
float: Single precision (usually 32-bit).double: Double precision (usually 64-bit).long double: Extended precision (often 80-bit or 128-bit).stdint.h SolutionBecause primitive sizes vary by architecture, systems programming requires Fixed-Width Types. Since the C99 standard, <stdint.h> provides types that are guaranteed to be the same size everywhere.
#include <stdint.h>
int32_t fixed_int; // Exactly 32 bits
uint64_t large_unsig; // Exactly 64 bits, unsigned
intptr_t pointer_int; // Integer large enough to hold a pointer
C performs Integer Promotion: any type smaller than an int (like char or short) is converted to an int before arithmetic operations.
The “Usual Arithmetic Conversions” rule:
When mixing types (e.g., int + double), C promotes the “smaller” type to the “larger” one to prevent precision loss. However, converting a double to an int causes truncation, where the fractional part is simply discarded.
int a = 5;
float b = 2.0;
// What is the resulting type of (a / b)?
// Answer: What happens when you add 1 to the maximum possible value of a signed integer?
Waiting for signal...
In C, an Expression is a sequence of operators and operands that specifies a computation. Understanding the nuances of how these expressions are evaluated is the difference between writing robust code and creating subtle, non-deterministic bugs.
Common arithmetic operators (+, -, *, /, %) behave as expected for floating-point and unsigned integers. However, signed integer arithmetic carries risks:
(-5) / 2 results in -2.%): Requires integer operands. The identity (a/b)*b + a%b == a always holds in C.C is the language of choice for drivers and embedded systems because of its direct support for bit-level manipulation.
| Operator | Description | Common Use Case |
|---|---|---|
& | Bitwise AND | Masking bits (clearing specific bits). |
| | Bitwise OR | Setting bits. |
^ | Bitwise XOR | Toggling bits or simple swaps. |
~ | Bitwise NOT | One’s complement (inverting all bits). |
<< | Left Shift | Multiply by power of 2. |
>> | Right Shift | Divide by power of 2 (Behavior for signed is implementation-defined). |
Waiting for signal...
Logical AND (&&) and OR (||) are guaranteed to evaluate from left-to-right. They use Short-Circuit Evaluation:
A && B, if A is false, B is never evaluated.A || B, if A is true, B is never evaluated.This property is frequently used to guard against null pointer dereferences:
if (ptr != NULL && ptr->value > 10) { ... }
Precedence determines which operator is applied first in an expression like a + b * c. Associativity determines the direction of evaluation for operators of the same precedence (e.g., a - b - c is (a - b) - c).
A “Sequence Point” is a point in time where all “side effects” (like variable assignments) from previous evaluations are guaranteed to be complete.
;) is a sequence point.&&, ||, and , operators are sequence points.CRITICAL RULE: Between two sequence points, an object’s value shall be modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored. Failure to follow this results in Undefined Behavior.
int i = 5;
// Is this code valid or undefined behavior?
i = i++ + 1;
// Answer: The ?: operator is C’s only ternary operator. It is an expression, not a statement, meaning it returns a value and can be used on the right-hand side of an assignment.
int max = (a > b) ? a : b;
Control flow determines the path execution takes through a program. In C, branching is achieved primarily through if-else constructs and switch statements. Unlike high-level languages with dedicated bool types, C treats logic as a numerical property.
In C, there is no native boolean type in the core language (prior to C99’s <stdbool.h>). The rules for truth are simple:
NULL pointer.if (5) { /* This will always execute */ }
if (0) { /* This will never execute */ }
if (-1) { /* This will execute! */ }
if-else ConstructThe if statement evaluates an expression. If it is non-zero, the following block executes.
When nesting if statements, an else always associates with the nearest preceding if that doesn’t have an else. This can lead to logic errors if braces are omitted.
if (a > 0)
if (b > 0)
do_thing();
else // This else belongs to (b > 0), not (a > 0)!
do_other_thing();
Best Practice: Always use curly braces {} to avoid ambiguity and improve maintainability.
switch Statement and Jump TablesThe switch statement is used for multi-way branching based on an integer constant.
switch (expression) {
case CONSTANT_1:
// statement
break;
case CONSTANT_2:
// statement
break;
default:
// statement
}
When a switch has many cases, the compiler often optimizes it using a Jump Table. Instead of checking every condition sequentially (as in an if-else if chain), the CPU can jump directly to the correct code block using an offset in an array of addresses. This makes switch statements in terms of time complexity in many scenarios.
Unlike modern languages (like Swift or Go), C cases “fall through” by default. If you omit the break keyword, execution continues into the next case. This is occasionally useful for mapping multiple inputs to the same output:
switch(input) { case 'y': case 'Y': confirmed = 1; ; case 'n': confirmed = 0; break; }
Modern CPUs use Branch Predictors to guess the outcome of if statements before they are fully evaluated. This allows the CPU to pre-fetch instructions.
Waiting for signal...
Iteration allows a program to repeat a block of code while a condition is met. C provides three primary loop constructs: while, do-while, and for.
while vs. do-whileThe fundamental difference lies in when the condition is evaluated.
while: Pre-test loop. The body may execute zero times.do-while: Post-test loop. The body is guaranteed to execute at least once. This is often used for input validation or state machines where an action must be performed before its result can be checked.int i = 10;
while (i < 5) { /* Never runs */ }
do {
/* Runs exactly once */
} while (i < 5);
for LoopThe for loop is syntactically sugar for a while loop but is much more expressive. It consists of three expressions:
C’s comma operator allows you to include multiple expressions where only one is expected. This is particularly useful in for loops for managing multiple counters.
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i: %d, j: %d\n", i, j);
}
break and continuebreak: Immediately terminates the innermost loop.continue: Skips the remainder of the current iteration and jumps to the condition/stepper evaluation.Warning: Overusing break and continue can lead to “spaghetti code.” Use them judiciously to handle exceptional cases rather than as primary control logic.
In systems programming, infinite loops are often intentional (e.g., an OS kernel idle loop or an embedded control loop).
for (;;) { /* The idiomatic C infinite loop */ }
while (1) { /* Also common */ }
High-performance C relies on the compiler to optimize loops. Loop Unrolling is a technique where the compiler replicates the loop body multiple times to reduce the overhead of the condition check and the branch.
Manually unrolling:
// Standard
for (int i = 0; i < 4; i++) { process(i); }
// Unrolled (conceptually)
process(0); process(1); process(2); process(3);
Modern compilers like GCC and Clang will do this automatically if they determine it will improve performance without blowing up the code size (cache pressure).
int i = 0; while (i < 3) { printf("%d", i); ; }
Waiting for signal...
In C, a function is a named block of code that performs a specific task. They are the primary tool for Decomposition—breaking complex problems into manageable, testable units.
A C function consists of:
void).C is a single-pass compiler (conceptually). If you call a function before the compiler has seen its definition, it won’t know the types of the arguments or the return value.
.c file.// Prototype
int square(int x);
int main() {
int res = square(5); // Compiler knows 'square' takes an int and returns an int
}
// Definition
int square(int x) {
return x * x;
}
Every time a function is called, a new Stack Frame (or Activation Record) is pushed onto the Call Stack. This frame contains:
When the function returns, its stack frame is “popped,” and its local memory is effectively reclaimed (this is why local variables are called Automatic variables).
C follows a strict Pass-by-Value model. When you pass a variable to a function, the function receives a copy of the data.
To modify a variable from within a function, you must pass the address of the variable (using pointers). Even then, C is still passing the value of the address.
Waiting for signal...
A function that calls itself is Recursive. Each recursive call adds a new frame to the stack. If the recursion is too deep (or infinite), it will exhaust the stack memory, resulting in a Stack Overflow.
int factorial(int n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive step
}
print_message(void) {
printf("Hello\n");
}{}. They exist only while that block is executing.In C, an Array is a collection of elements of the same type, stored in a contiguous block of memory. This physical contiguity is what makes arrays extremely fast: calculating the address of an element at index i is a simple arithmetic operation.
One of the most confusing aspects of C is the relationship between arrays and pointers. In most expressions, an array name decays into a pointer to its first element.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 'arr' decays to &arr[0]
Because of this, the bracket notation arr[i] is actually syntactic sugar for pointer arithmetic:
arr[i] is identical to *(arr + i).
C is a “trust the programmer” language. It does not check if an index is within the bounds of an array at runtime.
arr[10] on an array of size 5 will simply read whatever happens to be in memory at that offset.int vals[5] = {1, 2, 3};
// Which expression is equivalent to vals[2]?
// Answer: C stores multi-dimensional arrays in Row-Major Order. This means the elements of the first row are stored first, followed by the second row, and so on.
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
In memory, this looks like a single flat sequence: 1, 2, 3, 4, 5, 6.
When you pass an array to a function, you are actually passing a pointer to the first element. The function has no way of knowing the size of the array unless you pass it as a separate argument.
Waiting for signal...
Modern C allows you to initialize specific elements of an array by their index, which is incredibly useful for sparse arrays or configuration tables:
int settings[100] = { [0] = 1, [50] = 5, [99] = -1 };
In this example, all other elements are automatically initialized to zero.
In C, there is no built-in string type. A string is simply an array of characters (char) where the end of the string is marked by a special character called the Null Terminator ('\0', which has an ASCII value of 0).
Because of this design, a string of length always requires bytes of storage.
When you write "Hello", you are creating a String Literal.
char *p = "Hello"; p[0] = 'h';) results in Undefined Behavior, often a segmentation fault.To have a modifiable string, you must copy it into an array:
char modifiable[] = "Hello"; // Copies "Hello" into stack memory
modifiable[0] = 'h'; // Perfectly valid
<string.h> FunctionsC provides a standard library for string manipulation. However, these functions are notorious for being unsafe if not used with extreme care.
| Function | Description | Risk |
|---|---|---|
strlen() | Returns length (excluding \0). | time complexity; slow for long strings. |
strcpy() | Copies one string to another. | Buffer Overflow: Doesn’t check if destination is large enough. |
strcmp() | Compares two strings lexically. | Returns 0 if equal, not a boolean true/false. |
strcat() | Appends one string to another. | Also prone to buffer overflows. |
If you try to store 10 characters in an array of size 5, C will happily write the extra 5 characters into whatever follows the array in memory. This can overwrite return addresses or other variables, allowing attackers to hijack program execution.
char buffer[6];
// How many 'actual' characters can this buffer safely hold?
// Answer: Because strings are arrays, we can iterate through them using pointer arithmetic. This is often faster than indexing and is the idiomatic way to write string functions in C.
Waiting for signal...
snprintfIn modern C development, functions like strcpy are often banned in favor of snprintf or strncpy, which allow you to specify the maximum number of bytes to write.
char dest[10];
snprintf(dest, sizeof(dest), "%s", "This is a very long string");
// 'dest' will contain "This is a\0" (safe truncation)
In C, a Pointer is a variable whose value is a memory address.
Think of RAM as a massive array of bytes. Each byte has a unique numerical index called its address. When you declare int x = 5;, the compiler allocates space in that array to hold the value 5. A pointer to x simply holds that numerical index.
& and *&): Returns the memory address of an object.*): Accesses the value at the address held by a pointer.int x = 42;
int *p = &x; // 'p' holds the address of 'x'
printf("%d", *p); // Goes to the address in 'p' and reads the value (42)
*p = 100; // Goes to the address in 'p' and overwrites it with 100
Why do we need different types of pointers (like int*, char*, double*) if every pointer just holds an address?
The type of a pointer tells the compiler:
p + 1 jumps by 4 bytes for an int* but only 1 byte for a char*).NULL PointerA NULL pointer is a pointer that points to “nothing” (usually address 0).
NULL pointer is Undefined Behavior and usually triggers a Segmentation Fault.int y = 10; int *ptr = &y; // How do we change the value of y to 20 using only ptr? = 20;
If pointers are so dangerous, why does C use them so heavily?
Waiting for signal...
Remember that in C, the name of an array decodes to the address of its first element. Thus, int *p = arr; is shorthand for int *p = &arr[0];.
As established, adding 1 to a pointer does not add one byte to the address; it adds one unit of the underlying type.
If p is an int* and an int is 4 bytes:
p + 1 increases the address by 4.
This allows for incredibly efficient iteration over contiguous structures like arrays.
In C, we can have pointers that point to other pointers. This is commonly used for:
int **).char **argv is an array of strings (where each string is char*).int x = 10;
int *p = &x;
int **pp = &p; // pp -> p -> x
printf("%d", **pp); // Output: 10
A function’s code resides in memory, just like data. A Function Pointer stores the address of the entry point of a function. This enables Callbacks and higher-order programming in C.
Syntax: Return_Type (*Pointer_Name)(Parameter_Types);
int add(int a, int b) { return a + b; }
// declare a pointer to a function taking two ints and returning one int
int (*operation)(int, int) = add;
int result = operation(5, 3); // result is 8
void *The void * type is a Generic Pointer. It can point to any data type, but it cannot be dereferenced directly because the compiler doesn’t know the size or type of the underlying data.
To use the data, you must “cast” it back to a specific type.
int x = 5;
void *vp = &x;
// printf("%d", *vp); // ERROR: Invalid
printf("%d", *(int*)vp); // Correct: Cast to int* then dereference
void *ptr; int x = 10; ptr = &x; // How to read x into 'val'? int val = ;
Pointer Aliasing occurs when two pointers point to the same memory location. This can confuse the compiler’s optimizer unless the restrict keyword is used.
Dangling Pointers are pointers that point to memory that has been freed or is out of scope.
int* get_ptr() {
int x = 10;
return &x; // WARNING: x is on the stack and will be destroyed!
}
Waiting for signal...
So far, we have used Automatic Storage (local variables on the stack). However, the stack has limitations:
The Heap is a large pool of memory that exists independently of function calls. We can request memory from the heap at any time and it stays allocated until we explicitly release it.
<stdlib.h>C provides four primary functions for managing heap memory:
| Function | Purpose | Key Detail |
|---|---|---|
malloc(size) | Allocates size bytes. | Memory is uninitialized (contains garbage). |
calloc(n, size) | Allocates n elements of size. | Memory is zero-initialized. |
realloc(ptr, size) | Resizes an existing block. | May move the block to a new address. |
free(ptr) | Releases the block. | Using the pointer after free is Undefined Behavior. |
int *p = malloc(10 * sizeof(int));malloc returned NULL (which happens if the system is out of memory).free(p);Waiting for signal...
In languages like Python or Java, a Garbage Collector cleans up after you. In C, you are the garbage collector.
A leak occurs when you lose the pointer to an allocated block without calling free(). The memory remains “reserved” but unusable, eventually crashing the system if it happens in a loop.
Dereferencing a pointer after it has been passed to free(). The memory might have been re-assigned to something else, causing silent data corruption.
Calling free() on the same pointer twice. This usually crashes the program immediately as it corrupts the heap’s internal metadata.
int *p = malloc(10 * sizeof(int)); // We need more space! int *temp = (p, 20 * sizeof(int)); if (temp != NULL) p = temp;
Over time, frequent allocations and deallocations can leave “holes” in the heap—small blocks of free memory that are too small to satisfy new requests. High-performance systems often use Custom Allocators (like jemalloc or mimalloc) to mitigate this.
C provides three primary ways to create custom types: Structures, Unions, and Enumerations. These allow you to group related data into logical entities.
A struct is a block of memory that holds multiple variables (members) of different types.
struct Player {
char name[32];
int score;
float health;
};
. vs ->.) for direct instances.->) for pointers to instances. ptr->x is shorthand for (*ptr).x.This is a critical systems concept. A struct’s size is not always the sum of its parts. Compilers insert “padding” bytes to ensure members are aligned with the CPU’s word boundaries for faster access.
struct Mixed {
char c; // 1 byte
// 3 bytes of padding inserted here!
int i; // 4 bytes
};
// sizeof(struct Mixed) is likely 8, not 5.
Optimization Tip: Reorder struct members from largest to smallest to minimize padding.
A union is a special type where all members share the same starting memory address. The size of the union is the size of its largest member.
Use unions for:
union Data {
int i;
float f;
} u;
u.i = 42;
// Now u.f also contains the bit pattern of 42 interpreted as a float.
In embedded programming, we often need to packet data into specific bits to save space or match hardware registers.
struct Flags {
unsigned int is_active : 1; // 1 bit
unsigned int error_code : 3; // 3 bits
unsigned int reserved : 4; // 4 bits
};
// Total size: 1 byte (plus possible padding to int size)
union Example {
char a;
int b;
};
// If sizeof(char) is 1 and sizeof(int) is 4,
// what is sizeof(union Example)?
// Answer: enum)Enums provide a way to define integer constants with human-readable names, improving code clarity.
enum Status { IDLE, RUNNING, ERROR = -1 };
enum Status current = IDLE; // 'current' is effectively 0
Waiting for signal...
Every variable in C has two properties that define its behavior:
autoThe default for local variables. They are stored on the Stack and have Automatic Storage Duration (destroyed when the block ends).
staticThe static keyword has two distinct meanings depending on where it is used:
.c file and cannot be accessed by other files via extern.externUsed to declare a variable or function that is defined in another Translation Unit (file). It gives the variable External Linkage.
// file1.c
int global_count = 10;
// file2.c
extern int global_count; // Accesses the variable in file1.c
registerA hint to the compiler to store the variable in a CPU Register instead of RAM for faster access. Modern compilers are so good at register allocation that this is rarely used today, except in extremely tight loops.
constIndicates that the variable’s value cannot be changed after initialization. This allows the compiler to perform optimizations and move data to read-only memory.
volatile: The Embedded EssentialThe volatile qualifier tells the compiler: “This variable can change at any time without this code doing anything.”
volatile, the compiler might optimize away “redundant” reads, failing to see the hardware change.volatile int *sensor = (int*)0x40001234;
while (*sensor == 0) { /* Wait for hardware event */ }
// Without volatile, the compiler might turn this into an infinite loop!
Qualifiers can be applied to the pointer itself or the data it points to. This distinction is vital for API design:
| Declaration | Meaning |
|---|---|
const int *p | Pointer to a constant integer (Data cannot change). |
int * const p | Constant pointer to an integer (Address cannot change). |
const int * const p | Constant pointer to a constant integer (Nothing can change). |
int get_next_id() { int id = 0; return ++id; }
Waiting for signal...
The C Preprocessor (CPP) is a separate program that runs before the actual compilation. It does not understand C syntax; it performs Token-based Text Substitution. Directives for the preprocessor always start with a #.
#include#include <file>: Searches the system include directories (Standard Library).#include "file": Searches the local project directory first, then fallback to system paths.The Include Guard Pattern: To prevent a header from being included multiple times (which causes “redefinition” errors), every header file should use an include guard:
#ifndef MY_HEADER_H
#define MY_HEADER_H
// Your declarations here
#endif
#defineMacros allow you to define symbols that the preprocessor will swap for their replacement text wherever they appear.
#define MAX_BUFFER 1024
Macros can take arguments. However, because they are just text replacement, they are dangerous.
#define SQUARE(x) x * x
// SQUARE(5 + 1) becomes 5 + 1 * 5 + 1 = 11 (Wrong!)
// Fixed: #define SQUARE(x) ((x) * (x))
The preprocessor can “strip out” code blocks based on specific conditions. This is the primary way C achieves Portability across different Operating Systems.
#ifdef _WIN32
// Windows-specific code
#elif __linux__
// Linux-specific code
#endif
The preprocessor provides two special operators:
# (Stringification): Converts a macro argument into a string literal.## (Token Pasting): Merges two tokens into one.#define DEBUG_PRINT(var) printf(#var " = %d\n", var)
int speed = 60;
DEBUG_PRINT(speed); // Expands to: printf("speed" " = %d\n", speed);
#ifndef HEADER_H HEADER_H // ... code ... #endif
inlineModern C often prefers inline functions over macros because:
inline functions obey Scope rules.Waiting for signal...
In C, we do not interact directly with files. Instead, we interact with Streams. A stream is a uniform interface for reading and writing data, regardless of whether that data is coming from a hard drive, a keyboard, or a network socket.
The Standard Library provides three streams by default:
stdin: Standard Input (Keyboard)stdout: Standard Output (Console)stderr: Standard Error (Console, unbuffered)FILE *To work with a custom file, we use a FILE pointer. This pointer manages a buffer and tracks the current position in the file.
FILE *fp = fopen("filename.txt", "mode");
Common Modes:
"r": Read (fails if file doesn’t exist)."w": Write (overwrites existing file)."a": Append (writes to the end)."rb", "wb": Binary modes (prevents OS from altering line endings).Opening a file can fail (missing file, permission denied). Always check if the pointer is NULL.
There are three ways to move data through a stream:
| Level | Input | Output | Usage |
|---|---|---|---|
| Character | fgetc() | fputc() | Fine-grained parsing. |
| Line | fgets() | fputs() | Reading text safely. |
| Formatted | fscanf() | fprintf() | Structured data. |
| Block | fread() | fwrite() | Large binary blocks (Fastest). |
A file pointer maintains an internal “cursor.” You can move this cursor manually:
fseek(fp, offset, origin): Move the cursor.ftell(fp): Get the current cursor position.rewind(fp): Jump back to the start.char buffer[100]; FILE *fp = fopen("data.txt", "r"); if (fp != ) { fgets(buffer, 100, fp); fclose(fp); }
For performance, C does not write every byte to disk immediately. It stores them in a Buffer and writes them in chunks.
fflush(fp): Forces the buffer to be written to disk immediately.fclose(fp): Automatically flushes and closes the stream.Waiting for signal...
In Text Mode, some systems automatically convert \n to \r\n and vice-versa. In Binary Mode ("rb", "wb"), the data is moved exactly as-is, which is crucial for images, executables, and custom data structures.
The C Standard Library (often referred to as libc) is a collection of headers and binaries that provide essential functionality. It is the bridge between your portable C code and the underlying Operating System.
Instead of writing your own sort, C provides highly optimized generic algorithms.
qsort: The Generic Sorterqsort can sort any array of any type. It uses a Comparison Callback to determine the order of elements.
void qsort(void *base, size_t nitems, size_t size,
int (*compar)(const void *, const void *));
bsearch: Binary SearchWorks on sorted arrays to find an element in time.
exit(status): Terminates the program normally. A status of 0 indicates success.abort(): Terminates the program abnormally (often generates a core dump for debugging).atexit(callback): Registers a function to be called automatically when the program exits. This is perfect for cleaning up resources or logging.void cleanup() { printf("Cleaning up...\\n"); }
int main() {
atexit(cleanup);
return 0; // cleanup() is called automatically
}
Avoid atoi—it has no error handling. Use the strto... family instead:
strtol: String to long.strtod: String to double.These functions provide a pointer to the “first character that couldn’t be converted,” allowing you to detect malformed input.
rand(): Returns a pseudo-random integer.srand(seed): Seeds the random number generator. Usually seeded with time(NULL).Note: rand() is not cryptographically secure. For security applications, use OS-specific APIs like /dev/urandom or BCryptGenRandom.
int cmp(const void *a, const void *b) { return (*(int*)a - *(int*)b); } // To sort an array of 5 ints: qsort(arr, 5, sizeof(int), );
Waiting for signal...
<math.h>: sin, cos, pow, sqrt, ceil, floor. Note: You often need to link with -lm.<limits.h>: INT_MAX, CHAR_BIT, LLONG_MIN. Use these to write portable code that doesn’t make assumptions about bit-widths.Arrays are fast for access, but they are rigid:
A Linked List is a sequence of Nodes scattered throughout the heap. Each node knows only two things: its data and where the next node is.
To build a node, we need a structure that contains a pointer to itself. This is called a self-referential structure.
struct Node {
int data; // The payload
struct Node *next; // Pointer to the next node
};
The “Head” is just a standard pointer that stores the address of the first node. If the list is empty, head is NULL.
To move through the list, we use a temporary pointer. We never move the head pointer itself during traversal, or we will lose the list!
struct Node *current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
Inserting at the head is an operation regardless of the list size.
new_node->next to the current head.head to point to the new_node.// To delete a list, we must free every node. struct Node *tmp; while (head != NULL) { tmp = head; head = ; free(tmp); }
Linked lists are powerful but come with costs:
next pointer.Waiting for signal...
An Abstract Data Type defines what a structure does, but not how it is implemented in memory. In C, we can implement the same ADT using either static arrays or dynamic linked lists.
A stack follows the Last-In, First-Out principle. Imagine a stack of dinner plates: you can only add or remove the plate at the top.
An array is the fastest way to build a stack if you know the maximum size in advance.
#define MAX 100
int stack[MAX];
int top = -1;
void push(int val) {
if (top < MAX - 1) stack[++top] = val;
}
int pop() {
if (top >= 0) return stack[top--];
return -1; // Error
}
A queue follows the First-In, First-Out principle. Like a line at a supermarket, the person who arrived first is served first.
If you use a simple array for a queue, “dequeuing” from the front leaves wasted space at the beginning. To solve this, we use a Circular Buffer where the tail wraps back to the beginning of the array when it reaches the end.
| Implementation | Advantage | Disadvantage |
|---|---|---|
| Array | access, cache-friendly. | Fixed size, potential overflow. |
| Linked List | Dynamic size, no overflow. | but cache-unfriendly (allocations). |
// if top initialized to -1 void push(int x) { stack[] = x; }
The CPU itself uses a stack to manage function calls. When main() calls sum(), the current state of main is pushed onto the Hardware Stack. When sum finishes, it is popped, and execution returns to main.
Waiting for signal...
C has no try-catch blocks or native exception handling. In the C model, errors are ordinary values. They are not exceptional events that interrupt the flow; they are expected outcomes that must be checked and handled explicitly by the programmer.
There are three main ways a C function signals failure:
Functions return an int. Typically, 0 means success, and negative values relate to specific error types.
if (calculate_physics() != 0) {
handle_error();
}
Functions that return pointers return NULL to signify failure (e.g., malloc, fopen).
errnoFound in <errno.h>. When a system level function fails, it sets a global integer variable errno. You can translate this number into a human-readable string using strerror().
Warning: errno is only valid immediately after a failed call. Many functions do not clear errno on success, so a successful call won’t overwrite a previous error.
goto for CleanupWhile generally discouraged, the goto statement is widely considered “the right way” to handle cleanup in complex functions with multiple failure points. This prevents the “Arrow Anti-pattern” of nested if statements.
int process_file() {
int cleanup_needed = 0;
if (open_file() != 0) goto fail;
cleanup_needed = 1;
if (allocate_buffer() != 0) goto fail;
// ... logic ...
return 0;
fail:
if (cleanup_needed) close_file();
return -1;
}
setjmp and longjmpFound in <setjmp.h>, this is C’s version of a “Non-local Goto.” It allows you to jump directly from a deeply nested function back up to a previous state in the call stack.
setjmp(env): Saves the current CPU state (registers, stack pointer).longjmp(env, val): Restores that state, making it look like setjmp just returned val.Use Case: This is how basic exception handling is implemented in low-level frameworks.
#include <string.h> #include <errno.h> // How to get a string for the current error? char *msg = (errno);
Robust C code assumes inputs are malicious and functions will fail.
assert(ptr != NULL); from <assert.h> to catch logic errors during development.Waiting for signal...
When your C program runs, it lives in User Space. It is isolated from other programs and from the hardware itself by the CPU’s memory protection hardware. If you want to perform any action that affects the outside world (like writing to a file or sending a network packet), you must cross the boundary into Kernel Space.
A system call is the mechanism for a program to request a service from the operating system kernel.
syscall on x86-64 or svc on ARM).Functions like printf() or fopen() are not system calls themselves. They are part of the C Standard Library, which performs buffering and formatting before eventually calling the true system-level functions (like write() or open() on POSIX systems).
In kernel drivers or embedded C, we interact with hardware via Memory-Mapped I/O (MMIO). Specific memory addresses are physically wired to hardware devices.
// Force the compiler to read/write the actual memory address
// every single time, because the hardware could change it.
volatile uint32_t *led_control = (uint32_t *)0x40021018;
*led_control = 0x01; // Turn on a physical LED
The ABI is the low-level contract between the compiler and the machine. It defines:
struct padding is handled.Understanding the ABI is essential for writing assembly code that calls C functions, or vice versa.
// used to prevent optimization for MMIO uint32_t *reg = (uint32_t*)0x1234;
In systems programming, we often deal with Interrupts. An interrupt is a hardware signal that tells the CPU to stop what it’s doing and jump to a specific function called an Interrupt Service Routine (ISR).
Technically, a function call has overhead (pushing to the stack). For performance-critical system code, we use the inline keyword. This asks the compiler to copy the function body directly into the calling site, eliminating the call overhead while keeping the code modular.
Waiting for signal...
Mastering C means mastering the machine. By understanding how the preprocessor, compiler, and linker collaborate with the Operating System, you gain the power to build the tools that empower all other software. From here, the journey continues into OS Development, Compiler Design, and High-Performance Systems.