C Programming Fundamentals
A comprehensive C programming course covering algorithms, pointers, memory, file I/O, the preprocessor, translation units, linkage, callbacks, alignment, aliasing, and ABI boundaries.
The topic sequence follows the 2013-2014 introductory programming and systems-programming coursework. Later language-standard changes are included only where they clarify the subject; C's core semantics are not retroactively described through newer conveniences.
Unit 1: Introduction to Programming
Problem solving
Programming is the process of turning a problem into an exact sequence of operations that a computer can execute. Writing source code is only one of the later steps. The problem must first be understood, its inputs and outputs identified, and a solution method designed.
A useful sequence is:
Define the problem
↓
Identify inputs and outputs
↓
Design the algorithm
↓
Choose data structures
↓
Write the program
↓
Compile and run
↓
Test
↓
Evaluate the resultCorrect syntax alone does not make a program correct. A program should solve the intended problem, cover the required cases, have defined behavior for invalid input, use resources appropriately and remain understandable enough to maintain.
Algorithm
An algorithm is a finite and unambiguous sequence of operations that transforms defined input into the required output. Its steps must be executable, terminate, behave predictably under the stated conditions and cover the cases required by the problem.
Finding the largest of three values can be expressed without committing to any programming language:
largest <- a
if b > largest
largest <- b
if c > largest
largest <- c
return largestThe algorithm is independent of C or C++; the language is the implementation vehicle.
Pseudocode
Pseudocode expresses an algorithm without forcing it into the syntax of one language:
average(list):
total <- 0
for each item in list:
total <- total + item
return total / number_of_itemsIt should make control flow, data relationships and operation order visible. It does not need to compile.
Assignment
Assignment can be represented as:
x <- 5and in C as:
x = 5;The = token is assignment, not mathematical equality. The right-hand expression is evaluated and its result is stored in the object on the left. Therefore:
x = x + 1;is meaningful: the old value is read, incremented, and the result is written back.
Selection and loops
C provides if, else and switch for selection, and for, while and do-while for repetition. The choice of construct should reflect the structure of the problem rather than merely shorten the source code.
if (temperature < 0) {
/* freezing risk */
} else {
/* no freezing risk */
}A pre-tested loop uses while; a known iteration scheme often fits for; a post-tested loop uses do-while.
Functions
A function encapsulates a named operation:
int max2(int a, int b)
{
return a > b ? a : b;
}Functions reduce duplication, split a program into units, improve testability and separate interfaces from implementations.
Flowcharts and programming languages
Flowcharts can be useful for small algorithms. Their traditional symbols represent start/end, processing, decisions, input/output and flow. Nassi-Shneiderman diagrams express sequence, selection and repetition as structured blocks. They remain pedagogically useful, although larger programs are usually reasoned about through source code, pseudocode, state diagrams and other models.
C exposes low-level details such as addresses and object representation with relatively little runtime machinery. C++ grew historically from C but is more than C with classes; it has its own type system, object model, templates, resource-management idioms and standard library.
Unit 2: Structure of a C Program
Minimal program and main
A hosted C program can begin with:
int main(void)
{
return 0;
}Two common standard forms are:
int main(void)and:
int main(int argc, char *argv[])void main() is not the portable standard form for a hosted C program.
Translation process
A simplified toolchain is:
Source code
↓
Preprocessing
↓
Compilation
↓
Assembly
↓
Object code
↓
Linking
↓
Executable programA compiler driver may combine these stages behind one command:
cc program.c -o programUnderstanding the stages matters when diagnosing preprocessing errors, missing declarations, unresolved external symbols or ABI mismatches.
Header files
Standard library declarations are made visible through headers such as:
#include <stdio.h>Project headers are commonly included with quotes:
#include "my_header.h"Headers should normally contain declarations and definitions that are safe to include from multiple translation units; ordinary externally linked object definitions belong in a .c file.
Comments
Traditional C comments use:
/* comment */C99 standardized line comments:
// commentA useful comment explains purpose, constraint or non-obvious reasoning instead of repeating the statement below it.
Fundamental data types
Core types include:
char
short
int
long
long long
float
double
long double
_Bool / boolThe exact width of ordinary integer types is implementation-dependent within the constraints of the standard. sizeof(int) is commonly 4 on contemporary systems but C does not require int to be exactly 32 bits.
When an exact-width integer type exists, <stdint.h> can provide names such as:
int32_t
uint32_t
int64_t
uint64_tsizeof(char) is always 1 by definition. A C byte is not defined as universally eight bits; CHAR_BIT reports the implementation's number of bits per byte and is at least 8.
C23 made bool, true and false core-language spellings. Older code commonly obtains Boolean support through <stdbool.h> and _Bool.
Declarations, initialization and constants
int count;
double average;
char grade;Automatic local objects must not be read before they have an initialized value. Initialization can be explicit:
int count = 0;
double pi = 3.141592653589793;Literal constants include integers, floating-point values, character constants and string literals. A read-only object can be declared with const:
const int max_count = 100;In C, a const object is not automatically an integer constant expression in every context. Preprocessor constants are also possible:
#define MAX_COUNT 100Macros are textual preprocessing constructs and therefore do not carry normal object type semantics.
Scope, storage duration and linkage
A block-scope automatic variable exists according to its storage duration and is named only within its scope:
void f(void)
{
int x = 10;
}A file-scope object can have external linkage. Excessive global state introduces hidden dependencies and should be used deliberately.
static has different effects depending on context. At block scope it gives an object static storage duration while retaining block scope. At file scope it gives an identifier internal linkage:
static int value;extern can declare an externally linked object defined elsewhere:
extern int global_counter;The historical register storage-class specifier was an optimization hint. Modern optimizing compilers perform register allocation themselves; register is not a dependable performance mechanism.
Unit 3: Operators and Program Control
Arithmetic and integer division
C's arithmetic operators include:
+ - * / %Integer division discards the fractional part toward zero:
7 / 2produces 3. With floating operands:
7.0 / 2.0produces 3.5.
Increment, decrement and compound assignment
++i;
--i;
i++;
i--;Prefix and postfix forms differ when the value of the expression itself matters. Code should avoid modifying the same scalar object multiple times in an expression when sequencing does not make the behavior well-defined.
Compound assignment includes:
x += 5;
x -= 2;
x *= 3;
x /= 4;It expresses update operations directly and evaluates the left operand according to the compound-assignment rules rather than being merely textual substitution.
Comparison, logical and bitwise operators
Comparison operators are ==, !=, <, >, <= and >=. Assignment and comparison are distinct:
x = 5; /* assignment */
x == 5 /* comparison */Logical operators are:
&& logical AND
|| logical OR
! logical NOT&& and || short-circuit. This makes guards such as the following valid:
if (p != NULL && *p > 0) {
...
}Bitwise operators are:
& bitwise AND
| bitwise OR
^ bitwise XOR
~ complement
<< left shift
>> right shiftThey are central to masks, flags, protocols, device registers and compact representations. Logical && and bitwise & are not interchangeable.
if, conditional operator and switch
if selects a path based on a scalar condition. Nested conditions are legal, but excessive nesting makes invariants difficult to see. The conditional operator:
condition ? value1 : value2is an expression and is useful when the result is naturally a value.
switch selects among integral or enumeration case labels:
switch (command) {
case 1:
...
break;
case 2:
...
break;
default:
...
break;
}Unless control is deliberately transferred, falling through from one case to another should be explicit and understandable.
for, while and do-while
A for loop separates initialization, condition and iteration expression:
for (int i = 0; i < n; ++i) {
...
}A while loop is suitable when continuation depends on a condition not naturally described as a counter. do-while evaluates its condition after the body and therefore executes the body at least once.
break exits the nearest loop or switch. continue skips to the next loop iteration. goto exists and has legitimate low-level uses, especially structured cleanup in C, but should not replace ordinary structured control flow.
Unit 4: Arrays and Strings
One-dimensional arrays
int values[5];creates five int elements indexed from 0 through 4. Array bounds are not automatically checked by the language. Access outside the array is undefined behavior.
Initialization can be explicit:
int values[] = {10, 20, 30, 40};Within the same scope where the object is truly an array, element count can be computed as:
sizeof values / sizeof values[0]This does not work after an array parameter has adjusted to a pointer.
Multidimensional arrays
int matrix[3][4];is an array of three arrays of four int. C stores ordinary multidimensional arrays contiguously in row-major order. Matrix operations should respect dimensions and avoid integer overflow in index calculations.
A matrix addition requires matching dimensions. Matrix multiplication requires the number of columns of the left matrix to match the number of rows of the right matrix; each result element is a dot product.
C strings
A C string is a sequence of char terminated by a zero byte:
char text[] = "hello";The array contains the five visible characters plus \0. This convention makes correct capacity management essential.
Reading text safely
An unbounded input routine must not be used with a fixed buffer. fgets accepts the buffer capacity:
char line[128];
if (fgets(line, sizeof line, stdin) != NULL) {
...
}The retained newline, end-of-file behavior and input truncation must be handled according to the application's contract.
String library operations
<string.h> provides functions such as:
strlen
strcpy / strncpy
strcat / strncat
strcmp
memcpy
memmove
memset
memcmpTheir preconditions matter. strcpy and strcat require a destination with enough space. strncpy is not a general safe-string replacement because its null-termination behavior depends on the source length. Buffer size should be tracked explicitly.
strlen counts characters before the terminating zero byte and therefore requires a valid null-terminated string. strcmp performs lexicographic byte comparison and returns a value less than, equal to or greater than zero; code should not depend on a particular nonzero magnitude.
Unit 5: Pointers and Memory
Pointer concept
A pointer stores an address suitable for referring to an object or function of the corresponding pointer type:
int x = 10;
int *p = &x;&x obtains the address of x, and *p accesses the pointed-to object:
*p = 20;Null pointers
A null pointer represents no object/function target:
int *p = NULL;Dereferencing a null pointer is undefined behavior. A pointer must also refer to a live object with a compatible effective type and sufficient bounds before it can safely be dereferenced.
Pointer arithmetic
Pointer arithmetic is defined relative to array objects. If p points to an array element, p + 1 points to the next element, advancing by sizeof *p bytes at the machine-address level. Valid arithmetic is constrained to the array object and the one-past-the-end position; the one-past pointer may be formed and compared but not dereferenced.
Arrays and pointers
In many expressions an array expression converts to a pointer to its first element, but an array is not itself a pointer. This distinction is visible in sizeof, object lifetime and assignment rules.
for (int *p = values; p != values + n; ++p) {
use(*p);
}is one pointer-based traversal pattern.
Pointer arrays and pointers to pointers serve different layouts:
char *argv[];
char **p;Their type relationships should be understood rather than inferred from syntax alone.
Dynamic allocation
<stdlib.h> provides:
malloc
calloc
realloc
freeExample:
int *values = malloc(n * sizeof *values);
if (values == NULL) {
/* allocation failure */
}Before multiplying element count by element size, robust code considers whether the multiplication can overflow size_t.
calloc allocates space for an array and initializes all bytes to zero. Byte-wise zero is not a portable guarantee for every possible semantic representation in C, although it produces zero for the ordinary integer types and null characters.
A safe realloc pattern keeps the original pointer until success is known:
int *tmp = realloc(values, new_count * sizeof *values);
if (tmp != NULL) {
values = tmp;
}If realloc fails for a nonzero requested size, the original allocation remains valid.
Common memory errors include leaks, double free, use-after-free, out-of-bounds access, returning pointers to expired automatic objects and using uninitialized pointers. These are semantic failures; successful compilation does not make them safe.
Unit 6: Functions
Declarations and definitions
A prototype makes a function interface visible before use:
int sum(int a, int b);The definition provides the body:
int sum(int a, int b)
{
return a + b;
}Consistent declarations across translation units are essential for type-correct calls and ABI compatibility.
void functions and parameter passing
A function that does not return a value can use void:
void print_status(int status)
{
...
}C passes arguments by value. To let a function modify a caller-owned object, the caller passes its address:
void set_zero(int *value)
{
if (value != NULL) {
*value = 0;
}
}This is still value passing: the pointer value itself is copied.
Array parameters adjust to pointer parameters, so the length or another boundary normally has to be passed separately:
int sum_array(const int *values, size_t count);const communicates that the function does not modify elements through that pointer. It is an interface guarantee at the type level, not a statement that the underlying object can never be modified through any alias.
Static local state and recursion
A static local retains its stored value across calls. It can be useful but introduces hidden state that affects reentrancy and concurrency.
Recursion is a natural representation for some tree, divide-and-conquer and mathematical problems. Every recursive design needs a base case and progress toward it. Stack depth is finite; tail recursion is not guaranteed to be optimized by C implementations.
Unit 7: Structures, Unions and Bit-fields
Structures
A structure groups named fields:
struct point {
int x;
int y;
};typedef can introduce a convenient alias:
typedef struct point Point;Initialization may be positional or designated where supported by the applicable C standard:
struct point p = { .x = 10, .y = 20 };A structure pointer uses ->:
p_ptr->xStructures can be passed by value, but passing a pointer is often appropriate for large objects or in-place modification. Arrays of structures provide contiguous records and are often cache-friendly for record-oriented traversal.
Layout and padding
Structure fields may contain padding inserted to satisfy alignment requirements. Therefore sizeof(struct type) is not necessarily the sum of member sizes. Raw structure bytes are not automatically a stable disk or network format because padding, alignment, endianness and member representation can vary.
Unions
A union overlays members in one storage region:
union value {
int i;
double d;
};Its size is sufficient for its largest member plus any required alignment. Unions are useful in tagged representations when the program separately records which alternative is active.
Bit-fields
Bit-fields can describe compact fields in an implementation-defined layout:
struct flags {
unsigned ready : 1;
unsigned mode : 3;
};Allocation order, packing and some type details are implementation-defined. Bit-fields are therefore poor substitutes for explicitly encoded portable wire formats.
Unit 8: File I/O and the Standard Library
Opening and closing files
fopen returns a FILE * stream:
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
...
}Common modes include read, write, append and their update/binary variants. The exact combination determines whether an existing file is truncated, preserved or required to exist.
A successfully opened stream should be closed when ownership ends:
if (fclose(fp) != 0) {
...
}Character, line and formatted I/O
fgetc/fputc handle characters. fgets reads a bounded line fragment. fprintf and fscanf perform formatted I/O; format strings are part of the type contract and must match argument types.
Input parsing should test return values. A failed conversion is different from end-of-file, and partial input can leave unread characters in the stream.
Binary I/O
fread and fwrite move arrays of bytes or objects:
size_t n = fwrite(buffer, sizeof buffer[0], count, fp);Writing a struct directly to a file can be valid for a private, same-build temporary format, but it is not a portable serialization scheme. Stable formats should define field widths, byte order, encoding and versioning explicitly.
File position and file operations
fseek, ftell and rewind support file positioning subject to the stream mode and platform rules. remove and rename provide standard file operations, but atomicity and replacement semantics can still depend on the operating environment.
Character, mathematics, date and time libraries
<ctype.h> classifies and converts character codes. Except for EOF, values passed to functions such as isalpha should be representable as unsigned char.
<math.h> provides mathematical functions such as sqrt, sin, cos, log and pow. Floating-point domain/range behavior and error reporting must be considered where numerical robustness matters.
<time.h> provides calendar and processor-time facilities. Date/time work should distinguish elapsed monotonic time from civil/calendar time; the standard library's facilities and platform APIs have different guarantees.
Unit 9: The C Preprocessor
Preprocessing
Preprocessing occurs before translation of the resulting C tokens into the program proper. Directives begin with # at the preprocessing level.
Object-like macros
#define BUFFER_SIZE 4096performs token substitution. Parenthesized typed constants, const objects or enumeration constants are often preferable when ordinary language semantics can express the requirement.
Function-like macros
#define SQUARE(x) ((x) * (x))requires defensive parentheses, but even then SQUARE(i++) has multiple side effects because the argument is substituted more than once. Inline functions are often safer when type semantics and single evaluation are desired.
Stringification and token pasting
# stringifies a macro argument and ## pastes preprocessing tokens. These features are powerful for metaprogramming and generated declarations but can make diagnostics and control flow harder to follow.
#undef and conditional compilation
A macro can be removed with:
#undef NAMEConditional compilation uses directives such as:
#if
#ifdef
#ifndef
#elif
#else
#endifThey are useful for platform differences, feature configuration and build-time selection. Excessive conditional compilation can fragment one source file into many hard-to-test variants.
Header guards
A traditional guard is:
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* declarations */
#endifIt prevents repeated inclusion from producing duplicate declarations or definitions. Many compilers also support #pragma once, but the macro guard is the portable language-toolchain convention.
Unit 10: Translation Units, Linkage, and Call Boundaries
A C program is not merely the concatenation of its source files. After preprocessing, each .c file is compiled as a separate translation unit; object files are then combined by the linker into an executable or library. This distinction is essential when separating name visibility from object lifetime.
Internal and external linkage
Whether a file-scope name can be referenced from another translation unit is determined by linkage. A file-scope object or function declared static is limited to that translation unit. extern declares an object or function whose definition is provided elsewhere.
static int local_counter;
extern int shared_counter;These keywords participate in the program's linkage contract. In multi-file programs, placing a definition in a header is fundamentally different from placing a declaration there.
Function pointers and callbacks
A function pointer allows behavior to be selected as data:
int apply(int x, int (*op)(int)) {
return op(x);
}This mechanism is common in comparators, callbacks, state machines, and hardware-abstraction layers where indirection is needed without a larger runtime abstraction. The pointed-to function type must match the call boundary; calling through an incompatible function type can produce undefined behavior.
Alignment, aliasing, and object representation
An object's address must satisfy the alignment required by its type. Misaligned access may be slower on some architectures and invalid on others. Compilers may also assume, under defined rules, that pointers of unrelated types do not alias the same object. Code that violates strict aliasing may appear plausible at source level yet behave differently after optimization.
When the byte representation of an object must be inspected, character-type access provides a defined route for examining its object representation. Byte order and numeric value are separate concepts; endianness and alignment must not be conflated.
The ABI boundary
The C language standard defines source-language semantics, not the full binary interface. Register usage for parameters, stack-frame layout, symbol naming, alignment, and calling conventions belong to the platform's Application Binary Interface (ABI).
The layers can be read as:
C source
↓
translation unit
↓
object file
↓
ABI and symbols
↓
linking
↓
executable programReliable low-level reasoning therefore requires the source language, compiler assumptions, and platform ABI to be treated as related but distinct contracts.
Defined Behaviour, Lifetime, and Bounds in C
C gives the programmer unusually direct control over representation and data movement. The same property makes the language's defined-behaviour boundaries part of correctness. A correct C program must reason not only about the algorithm but also about object lifetime, bounds, alignment, integer ranges, and pointer validity.
An array is not a pointer
An array object contains contiguous elements. In many expression contexts its name is converted to a pointer to the first element, but the array itself is not a pointer object. This distinction matters for sizeof, &array, function parameters, and multidimensional arrays.
int a[10];
size_t n = sizeof a / sizeof a[0];This calculation works where a is an actual array object. When an array declarator is used as a function parameter, the parameter is adjusted to a pointer and the same sizeof expression no longer reports the element count. Robust interfaces carry the pointer and length together.
Object lifetime and dangling pointers
An automatic local object ceases to exist when its lifetime ends. Returning its address does not extend that lifetime. Likewise, accessing storage after free() is a use-after-free defect.
Dynamic ownership should be explicit: who allocates, who releases, and how cleanup occurs on error paths? Double free and memory leak are different failures and require different controls.
Integer and size calculations
size_t is the unsigned type used for object sizes. If externally influenced values are multiplied as count * element_size, the multiplication can wrap before malloc() is called. Allocation may then succeed for a much smaller buffer while later logic assumes the original large count. Size arithmetic should be checked before allocation.
Signed integer overflow is generally undefined behaviour in C. Unsigned arithmetic wraps modulo its range, but defined wraparound is not automatically a safe application result.
Expression side effects
Code that depends on multiple modifications of the same object inside a complex expression can cross sequencing rules and create undefined or surprising behaviour. Splitting order-dependent side effects into explicit statements is usually more portable and reviewable than compressing them into a clever expression.
const, volatile, and atomicity
const restricts modification through a particular access path; it does not automatically place data in ROM or prevent every alias from modifying the underlying object. volatile is relevant when accesses must remain observable to the abstract machine for hardware/environment interaction; it is not a thread synchronization or atomicity mechanism. Concurrent shared state requires the language's atomic and synchronization facilities.
A practical C-interface rule
For every pointer parameter, make the contract explicit: may it be NULL, how many elements are valid, may the function write, who owns the storage, may the function retain the pointer, and can an error leave partial output? Unspecified answers to these questions are a common source of memory-safety defects.
Undefined behavior, portability, and bounds
A C program can compile successfully while still having undefined behavior. Out-of-bounds access, use-after-lifetime, certain signed overflows, and invalid pointer operations are examples. "It works on my machine" is not evidence that such behavior is valid.
Portable code avoids accidental assumptions about width and alignment. Fixed-width integer types are useful for external formats, API return values must be checked, and buffer length should be part of the function contract.
Pointer arithmetic is best treated as a controlled low-level tool. The caller should be able to tell which memory a function may access, how much data it expects, and whether ownership changes.
Verifiability in C programs
In C, “it runs” is not the same as “its behavior is defined.” Correctness should be evaluated against the language rules for object lifetime, types, bounds, and expression evaluation rather than by successful compilation alone.
Portability checks benefit from multiple compilers and high warning levels. Memory issues can be investigated with tools such as AddressSanitizer and UndefinedBehaviorSanitizer, but a clean run does not prove correctness for all inputs. Bounds and ownership contracts still need explicit review.
Low-level examples should state platform assumptions such as integer width, endianness, alignment, operating-system APIs, and compiler extensions. This separates standard C semantics from implementation-specific behavior.
Reading Arrays, Pointers, Lifetime, and Size Together
Arrays and pointers often look similar in C expressions, but they are not the same concept. int a[10] defines an actual array object, and sizeof(a) in that scope returns the total byte size of the array. In many expressions the array name is converted to a pointer to its first element; an int a[] function parameter is likewise adjusted to a pointer parameter. A different sizeof result inside the function is therefore not a contradiction in the language—the type is no longer an array there.
A pointer is valid for access for more reasons than merely being non-null. The pointed-to object must still be within its lifetime, the address must satisfy alignment requirements, and the access must remain within the permitted object bounds. Returning the address of an automatic local object leaves a pointer to an object whose lifetime has ended. Storage obtained by malloc has a different lifetime that lasts until free; the numerical address may appear unchanged afterwards, but the pointer no longer grants a valid access.
C passes function arguments by value. To modify an object owned by the caller, its address can be passed explicitly:
void increment(int *p) {
if (p != NULL) {
++*p;
}
}The pointer itself is still copied by value, but the copied value identifies the same object, so that object can be modified.
These distinctions matter beyond syntax questions. Array bounds, object lifetime, ownership, and undefined behaviour belong to the same memory model. Reliable C reasoning asks not only “what does this expression do?” but also “does the referenced object still exist, and is this access defined within its bounds?”
References
- Brian W. Kernighan; Dennis M. Ritchie. The C Programming Language. Prentice Hall, 1988.
- ISO/IEC. ISO/IEC 9899:2011 Programming Languages - C. International Organization for Standardization, 2011. Source
- ISO/IEC. ISO/IEC 9899:2024 Information technology — Programming languages — C. International Organization for Standardization, 2024. Source
- K. N. King. C Programming: A Modern Approach. W. W. Norton, 2008.