March 27, 202616 min read

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 beginners programming systems tutorial
Ad 336x280

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 you printf, scanf, and other I/O functions. The .h means it's a header file.
  • int main(void) -- the entry point. int means it returns an integer. void means it takes no arguments. Returning 0 signals success to the operating system.
  • printf("Hello, World!\n") -- prints text. \n is a newline character. Unlike C++ or Python, printf doesn'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:

SpecifierType
%dint
%ldlong
%fdouble/float
%.2fdouble with 2 decimal places
%cchar
%sstring (char array)
%ppointer (memory address)
%xhexadecimal
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
Think of it like a house. The house is the variable (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:

FunctionPurpose
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
Buffer safety is your problem. 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:

  • malloc returns NULL if 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. Every malloc needs a matching free.
  • After free(), set the pointer to NULL to 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.

math_utils.h:
#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. Use printf 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, fprintf for 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, socket for interacting with the operating system.
  • Debugging tools -- GDB for stepping through code, Valgrind for memory analysis, AddressSanitizer for catching bugs.
C is not a comfortable language. It doesn't hold your hand. But it rewards you with an understanding of computing that no other language provides. Every abstraction in every higher-level language -- garbage collection, dynamic arrays, hash maps, virtual dispatch -- you can now understand from the ground up, because you've seen the raw materials they're built from.

Explore more tutorials and guides at CodeUp.

Ad 728x90