C Programming for Beginners: The Language Behind Everything
Learn C from scratch. Variables, functions, pointers, arrays, strings, structs, and memory management explained step by step with real examples.
C is the most influential programming language ever created. Linux, Windows, macOS, PostgreSQL, Python's interpreter, Git, the SQLite database engine, most embedded systems, and the firmware in your microwave -- all written in C. It was created in 1972, and more than fifty years later it's still in the top three most-used languages worldwide.
Why learn C when newer languages exist? Because C teaches you what computers actually do. There's no garbage collector hiding memory management. No runtime interpreting your code. You write instructions, the compiler turns them into machine code, and the CPU runs them. Understanding C makes you a better programmer in every other language, because every other language is ultimately running on top of what C pioneered.
It's also a small language. The core fits in your head. What makes it challenging isn't complexity -- it's responsibility. C trusts you completely, which means it lets you make mistakes that other languages prevent. That's a feature, not a bug. Let's learn it.
Setting Up GCC
GCC (GNU Compiler Collection) is the standard C compiler. It's free and available everywhere.
Linux:sudo apt install gcc # Ubuntu/Debian
sudo dnf install gcc # Fedora
macOS:
xcode-select --install
This installs Apple's Clang compiler, which works the same way for our purposes.
Windows: Install MinGW-w64 via MSYS2, or use WSL (Windows Subsystem for Linux) to get a full Linux environment.
Verify:
gcc --version
Hello World
Create hello.c:
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
Compile and run:
gcc hello.c -o hello
./hello
What's happening:
#include-- includes the Standard I/O library. This gives youprintf,scanf, and other I/O functions. The.hmeans it's a header file.int main(void)-- the entry point.intmeans it returns an integer.voidmeans it takes no arguments. Returning0signals success to the operating system.printf("Hello, World!\n")-- prints text.\nis a newline character. Unlike C++ or Python,printfdoesn't add a newline automatically.
printf and scanf
printf is how you output text. It uses format specifiers to embed values:
#include <stdio.h>
int main(void) {
int age = 25;
double height = 5.9;
char grade = 'A';
char name[] = "Alice";
printf("Name: %s\n", name);
printf("Age: %d\n", age);
printf("Height: %.1f feet\n", height);
printf("Grade: %c\n", grade);
// Multiple values in one printf
printf("%s is %d years old and %.1f feet tall.\n", name, age, height);
return 0;
}
Common format specifiers:
| Specifier | Type |
|---|---|
%d | int |
%ld | long |
%f | double/float |
%.2f | double with 2 decimal places |
%c | char |
%s | string (char array) |
%p | pointer (memory address) |
%x | hexadecimal |
scanf reads input:
#include <stdio.h>
int main(void) {
int age;
double weight;
printf("Enter your age: ");
scanf("%d", &age);
printf("Enter your weight: ");
scanf("%lf", &weight);
printf("You are %d years old and weigh %.1f kg.\n", age, weight);
return 0;
}
Notice the & before age in scanf. This passes the address of the variable so scanf knows where to store the value. Forget it and you'll get a segfault or corrupted data. We'll explain addresses fully when we cover pointers.
Variables and Types
C has a small set of types:
#include <stdio.h>
int main(void) {
// Integer types
int count = 42;
short small = 100;
long big = 1000000L;
unsigned int positive_only = 300;
// Floating point
float pi_approx = 3.14f;
double pi = 3.14159265358979;
// Character
char letter = 'A';
char newline = '\n';
// There is no boolean type in C89/C90
// C99 added _Bool, and <stdbool.h> gives you bool/true/false
#include <stdbool.h>
bool is_ready = true;
printf("int: %d\n", count);
printf("char: %c (ASCII: %d)\n", letter, letter);
printf("double: %.10f\n", pi);
return 0;
}
C doesn't have a string type. Strings are arrays of characters terminated by a null byte ('\0'). More on that later.
Operators
Standard arithmetic and comparison:
int a = 10, b = 3;
printf("%d + %d = %d\n", a, b, a + b); // 13
printf("%d / %d = %d\n", a, b, a / b); // 3 (integer division!)
printf("%d %% %d = %d\n", a, b, a % b); // 1 (modulo)
// Integer division truncates. 10/3 is 3, not 3.333
// For decimal results, cast to double:
printf("%.2f\n", (double)a / b); // 3.33
Watch out for integer division. 10 / 3 gives 3, not 3.33. If you want the decimal result, at least one operand must be a floating-point type.
Control Flow
if / else
int temperature = 72;
if (temperature > 85) {
printf("It's hot.\n");
} else if (temperature > 65) {
printf("It's nice.\n");
} else {
printf("It's cold.\n");
}
Loops
// for loop
for (int i = 0; i < 5; i++) {
printf("%d ", i);
}
printf("\n");
// while loop
int countdown = 5;
while (countdown > 0) {
printf("%d... ", countdown);
countdown--;
}
printf("Go!\n");
// do-while
int input;
do {
printf("Enter a positive number: ");
scanf("%d", &input);
} while (input <= 0);
Functions
#include <stdio.h>
// Function declarations (prototypes) go before main
int add(int a, int b);
void print_line(int length);
int factorial(int n);
int main(void) {
printf("Sum: %d\n", add(10, 20));
print_line(30);
printf("5! = %d\n", factorial(5));
return 0;
}
// Function definitions
int add(int a, int b) {
return a + b;
}
void print_line(int length) {
for (int i = 0; i < length; i++) {
printf("-");
}
printf("\n");
}
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
Function prototypes (declarations before main) tell the compiler what functions exist, their return types, and their parameters. The actual implementation (definition) can come later.
Arrays
#include <stdio.h>
int main(void) {
// Declaration and initialization
int scores[5] = {90, 85, 78, 92, 88};
// Access by index (zero-based)
printf("First: %d\n", scores[0]);
printf("Last: %d\n", scores[4]);
// Modify
scores[2] = 80;
// C arrays don't know their own size. You track it yourself.
int size = sizeof(scores) / sizeof(scores[0]);
// Calculate average
int sum = 0;
for (int i = 0; i < size; i++) {
sum += scores[i];
}
double average = (double)sum / size;
printf("Average: %.1f\n", average);
return 0;
}
Critical detail: C does no bounds checking. If you access scores[10] on a 5-element array, C won't stop you. You'll read (or write) whatever memory happens to be there. This is a buffer overflow, and it's the source of countless bugs and security vulnerabilities.
Passing arrays to functions
#include <stdio.h>
// Arrays decay to pointers when passed to functions
// You must pass the size separately
double average(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return (double)sum / size;
}
int main(void) {
int numbers[] = {10, 20, 30, 40, 50};
int len = sizeof(numbers) / sizeof(numbers[0]);
printf("Average: %.1f\n", average(numbers, len));
return 0;
}
When you pass an array to a function, C actually passes a pointer to the first element. The function has no way to know the array's size, which is why you always pass the size as a separate parameter.
Pointers: Step by Step
This is the concept that separates C from higher-level languages. Take it slow.
What is a pointer?
Every variable lives at a memory address. A pointer is a variable that stores a memory address.
#include <stdio.h>
int main(void) {
int x = 42;
// &x gives the address of x
printf("Value of x: %d\n", x);
printf("Address of x: %p\n", (void*)&x);
// A pointer variable stores an address
int *ptr = &x;
printf("ptr holds address: %p\n", (void*)ptr);
printf("Value at that address: %d\n", *ptr); // Dereferencing
// Modify x through the pointer
*ptr = 100;
printf("x is now: %d\n", x); // 100
return 0;
}
Two operators to remember:
&variable-- "address of" -- returns the memory address where that variable is stored*pointer-- "dereference" -- returns the value stored at the address the pointer holds
x). The address written on the house is &x. A piece of paper with that address written on it is the pointer (ptr). Going to the address on the paper and looking at the house is dereferencing (*ptr).
Why pointers matter
Functions in C receive copies of their arguments. Without pointers, a function can't modify the caller's variables:
#include <stdio.h>
// This DOESN'T work -- it modifies a copy
void swap_broken(int a, int b) {
int temp = a;
a = b;
b = temp;
}
// This DOES work -- it modifies through pointers
void swap(int a, int b) {
int temp = *a;
a = b;
*b = temp;
}
int main(void) {
int x = 10, y = 20;
swap_broken(x, y);
printf("After swap_broken: x=%d, y=%d\n", x, y); // Still 10, 20
swap(&x, &y);
printf("After swap: x=%d, y=%d\n", x, y); // Now 20, 10
return 0;
}
Pointer arithmetic
int numbers[] = {10, 20, 30, 40, 50};
int *ptr = numbers; // Points to first element
printf("%d\n", *ptr); // 10
printf("%d\n", *(ptr + 1)); // 20
printf("%d\n", *(ptr + 2)); // 30
ptr++;
printf("%d\n", *ptr); // 20 (ptr now points to second element)
Adding 1 to a pointer advances it by the size of the type it points to. For int, that's typically 4 bytes. This is how array indexing works under the hood: arr[i] is equivalent to (arr + i).
Strings
In C, strings are null-terminated character arrays:
#include <stdio.h>
#include <string.h>
int main(void) {
// String literal (stored in read-only memory)
char greeting[] = "Hello";
// This is actually: {'H', 'e', 'l', 'l', 'o', '\0'}
printf("%s\n", greeting);
printf("Length: %lu\n", strlen(greeting)); // 5 (doesn't count '\0')
// Common string.h functions
char dest[50];
strcpy(dest, "Hello"); // Copy
strcat(dest, ", World!"); // Concatenate
printf("%s\n", dest); // "Hello, World!"
// Comparison
if (strcmp("abc", "abc") == 0) {
printf("Strings are equal\n");
}
// You CANNOT compare strings with ==
// == compares addresses, not contents
char a[] = "hello";
char b[] = "hello";
if (a == b) {
printf("This won't print\n"); // Different addresses
}
if (strcmp(a, b) == 0) {
printf("This will print\n"); // Same contents
}
return 0;
}
Key string.h functions:
| Function | Purpose |
|---|---|
strlen(s) | Length (excluding null terminator) |
strcpy(dest, src) | Copy src into dest |
strncpy(dest, src, n) | Copy at most n characters (safer) |
strcat(dest, src) | Append src to dest |
strcmp(a, b) | Compare: returns 0 if equal |
strchr(s, c) | Find first occurrence of char c |
strcpy doesn't check if the destination is large enough. Use strncpy or snprintf in real code.
Structs
Structs let you group related data:
#include <stdio.h>
#include <string.h>
struct Book {
char title[100];
char author[50];
double price;
int pages;
};
void print_book(const struct Book *book) {
printf("%s by %s ($%.2f, %d pages)\n",
book->title, book->author, book->price, book->pages);
}
int main(void) {
struct Book book1;
strcpy(book1.title, "Dune");
strcpy(book1.author, "Frank Herbert");
book1.price = 9.99;
book1.pages = 688;
// Or initialize directly
struct Book book2 = {"1984", "George Orwell", 8.99, 328};
print_book(&book1);
print_book(&book2);
return 0;
}
The -> operator accesses a member through a pointer. book->title is shorthand for (*book).title.
You can use typedef to avoid writing struct everywhere:
typedef struct {
char name[50];
int age;
} Person;
Person alice = {"Alice", 25}; // No need for 'struct' keyword
Dynamic Memory
Sometimes you don't know how much memory you need at compile time. malloc allocates memory at runtime:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("How many numbers? ");
scanf("%d", &n);
// Allocate memory for n integers
int numbers = malloc(n sizeof(int));
if (numbers == NULL) {
printf("Memory allocation failed!\n");
return 1;
}
// Use it like an array
for (int i = 0; i < n; i++) {
numbers[i] = i * 10;
}
for (int i = 0; i < n; i++) {
printf("%d ", numbers[i]);
}
printf("\n");
// Free the memory when done
free(numbers);
return 0;
}
Critical rules:
mallocreturnsNULLif allocation fails. Always check.sizeof(int)gives the size of an int in bytes. Don't hardcode sizes.free()releases the memory back to the system. Everymallocneeds a matchingfree.- After
free(), set the pointer toNULLto avoid using freed memory.
calloc is like malloc but initializes memory to zero:
int *arr = calloc(10, sizeof(int)); // 10 ints, all initialized to 0
Header Files and Multi-File Programs
Real C programs span multiple files. Header files (.h) declare interfaces, source files (.c) implement them.
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(int arr[], int size);
#endif
math_utils.c:
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return (double)sum / size;
}
main.c:
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("3 + 4 = %d\n", add(3, 4));
printf("3 * 4 = %d\n", multiply(3, 4));
int scores[] = {90, 85, 78, 92, 88};
printf("Average: %.1f\n", average(scores, 5));
return 0;
}
Compile:
gcc main.c math_utils.c -o program
./program
The #ifndef / #define / #endif pattern is called an include guard. It prevents the header from being included twice, which would cause duplicate definition errors.
Building Something: A Contact Book
Let's put it all together:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_NAME 50
#define MAX_PHONE 20
typedef struct {
char name[MAX_NAME];
char phone[MAX_PHONE];
} Contact;
typedef struct {
Contact *contacts;
int count;
int capacity;
} ContactBook;
ContactBook* create_book(int initial_capacity) {
ContactBook *book = malloc(sizeof(ContactBook));
if (!book) return NULL;
book->contacts = malloc(initial_capacity * sizeof(Contact));
if (!book->contacts) {
free(book);
return NULL;
}
book->count = 0;
book->capacity = initial_capacity;
return book;
}
int add_contact(ContactBook book, const char name, const char *phone) {
if (book->count >= book->capacity) {
int new_capacity = book->capacity * 2;
Contact *new_contacts = realloc(book->contacts,
new_capacity * sizeof(Contact));
if (!new_contacts) return 0;
book->contacts = new_contacts;
book->capacity = new_capacity;
}
strncpy(book->contacts[book->count].name, name, MAX_NAME - 1);
book->contacts[book->count].name[MAX_NAME - 1] = '\0';
strncpy(book->contacts[book->count].phone, phone, MAX_PHONE - 1);
book->contacts[book->count].phone[MAX_PHONE - 1] = '\0';
book->count++;
return 1;
}
void list_contacts(const ContactBook *book) {
if (book->count == 0) {
printf("No contacts.\n");
return;
}
for (int i = 0; i < book->count; i++) {
printf("%d. %s - %s\n", i + 1,
book->contacts[i].name, book->contacts[i].phone);
}
}
Contact find_contact(const ContactBook book, const char *name) {
for (int i = 0; i < book->count; i++) {
if (strcmp(book->contacts[i].name, name) == 0) {
return &book->contacts[i];
}
}
return NULL;
}
void free_book(ContactBook *book) {
free(book->contacts);
free(book);
}
int main(void) {
ContactBook *book = create_book(4);
if (!book) {
printf("Failed to create contact book.\n");
return 1;
}
add_contact(book, "Alice", "555-0101");
add_contact(book, "Bob", "555-0102");
add_contact(book, "Carol", "555-0103");
printf("All contacts:\n");
list_contacts(book);
printf("\nSearching for Bob...\n");
Contact *found = find_contact(book, "Bob");
if (found) {
printf("Found: %s - %s\n", found->name, found->phone);
} else {
printf("Not found.\n");
}
free_book(book);
return 0;
}
This program demonstrates structs, dynamic memory, pointers, realloc for growing arrays, and proper cleanup. It's the kind of code that teaches you how data structures work from the inside.
Common Pitfalls
Segfaults. Short for "segmentation fault." It means you accessed memory you don't own -- dereferencing a NULL pointer, writing past the end of an array, or using memory after freeing it. Useprintf debugging or a tool like Valgrind to track these down.
Buffer overflows. Writing more data than a buffer can hold. char name[10]; strcpy(name, "a very long name"); writes past the end of name, corrupting whatever memory comes after it. Always check sizes. Use strncpy and snprintf.
Memory leaks. Every malloc needs a free. If you allocate memory in a loop without freeing it, your program's memory usage grows until the OS kills it. Valgrind is invaluable here: valgrind ./your_program shows every leak.
Using uninitialized variables. C doesn't zero-initialize local variables. int x; printf("%d", x); prints garbage. Always initialize your variables.
Forgetting the null terminator. Strings must end with '\0'. If you're building strings manually, make sure to include it, and make sure your buffer is one byte larger than the string content.
Returning pointers to local variables. Local variables are destroyed when the function returns. A pointer to them becomes a dangling pointer:
// WRONG
int* bad_function(void) {
int x = 42;
return &x; // x is destroyed after return, pointer is dangling
}
// RIGHT
int* good_function(void) {
int *x = malloc(sizeof(int));
*x = 42;
return x; // Caller must free this
}
What to Learn Next
You now understand the fundamentals of C. The path forward:
- Data structures -- implement linked lists, stacks, queues, and hash tables from scratch. C forces you to understand how they actually work.
- File I/O --
fopen,fread,fwrite,fprintffor reading and writing files. - Preprocessor -- macros, conditional compilation, more about
#define. - Make and build systems -- Makefiles for compiling multi-file projects.
- System calls --
fork,exec,pipe,socketfor interacting with the operating system. - Debugging tools -- GDB for stepping through code, Valgrind for memory analysis, AddressSanitizer for catching bugs.
Explore more tutorials and guides at CodeUp.