C • 2026-10-10

C Programming Tutorial: Variables, Pointers, Arrays, and Memory Basics

Understand C syntax, types, control flow, functions, arrays, strings, pointers, dynamic memory, structs, file I/O, and safe memory practices with complete programs.

Understand C syntax, types, control flow, functions, arrays, strings, pointers, dynamic memory, structs, file I/O, and safe memory practices with complete programs.

Developer workspace with code on a screen
Build your programming knowledge one practical example at a time.

What Is C?

C is a compiled, general-purpose language widely used in operating systems, embedded software, databases, and performance-sensitive libraries. It gives programmers direct control over memory, so correct pointer and buffer handling is essential.

Why learn C

  • It teaches how programs interact with memory and the machine.
  • Many other languages (C++, Java, C#, JavaScript) borrow its syntax.
  • It is the foundation of operating systems and embedded devices.

How C programs are built

  1. Preprocess: #include and #define lines are expanded.
  2. Compile: source code becomes object code.
  3. Link: object files and libraries are combined into an executable.
  4. Run: the operating system loads and executes the program.

Hello, world and how to compile it

#include <stdio.h>

int main(void) {
    printf("Hello, C!\n");
    return 0;
}

Compiling with warnings enabled

cc -Wall -Wextra -Wpedantic -std=c11 main.c -o main
./main

Types, Variables, and Operators

  • Integer types: char, short, int, long, long long, with unsigned variants.
  • Floating-point types: float and double.
  • Use size_t for sizes and array indexes, and fixed-width types such as int32_t from <stdint.h> when exact size matters.
  • Always initialize variables; reading an uninitialized variable is undefined behavior.
  • Integer division truncates: 7 / 2 is 3. Cast to double for fractional results.
  • Signed integer overflow is undefined behavior; check ranges before arithmetic.

Types, sizes, and casting

#include <stdio.h>
#include <stdint.h>

int main(void) {
    int apples = 7;
    int people = 2;
    double share = (double)apples / people;      // 3.5, not 3

    uint8_t small = 255;
    int32_t exact = -42;

    printf("share = %.2f\n", share);
    printf("sizeof(int) = %zu bytes\n", sizeof(int));
    printf("small = %u, exact = %d\n", (unsigned)small, exact);
    return 0;
}

Control Flow and Input

  • if/else, switch, for, while, and do...while control execution.
  • switch cases need break to avoid falling through to the next case.
  • Prefer fgets over gets (removed) or unbounded scanf("%s") for reading text.
  • Always check return values of input functions.

Reading a number safely and branching

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char line[64];
    printf("Enter your score: ");

    if (fgets(line, sizeof line, stdin) == NULL) {
        fprintf(stderr, "No input\n");
        return 1;
    }

    int score = (int)strtol(line, NULL, 10);

    if (score >= 90)      puts("Grade A");
    else if (score >= 75) puts("Grade B");
    else                  puts("Keep practicing");

    for (int i = 1; i <= 3; ++i) {
        printf("Round %d\n", i);
    }
    return 0;
}

Functions and Header Files

  • Declare a function's prototype before use so the compiler can check calls.
  • C passes arguments by value; pass a pointer to let a function change the caller's variable.
  • Use const on pointer parameters that the function only reads.
  • Put prototypes in .h files with include guards and definitions in .c files.
  • Mark file-private helpers static.

Prototype, definition, and a header file

// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int clamp(int value, int low, int high);

#endif

// math_utils.c
#include "math_utils.h"

int clamp(int value, int low, int high) {
    if (value < low)  return low;
    if (value > high) return high;
    return value;
}

// main.c
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("%d\n", clamp(150, 0, 100));   // 100
    return 0;
}

Arrays and Strings

  • Arrays store elements of one type in contiguous memory.
  • C does not check array bounds; the programmer must keep indexes valid.
  • An array passed to a function decays to a pointer, so also pass its length.
  • A C string is a char array ending with a null terminator '\0'.
  • Use bounded functions such as snprintf and strncpy carefully, and always leave room for the terminator.

Arrays and string basics

#include <stdio.h>
#include <string.h>

double average(const int values[], size_t count) {
    if (count == 0) return 0.0;
    long long sum = 0;
    for (size_t i = 0; i < count; ++i) sum += values[i];
    return (double)sum / (double)count;
}

int main(void) {
    int scores[] = {82, 91, 76, 88};
    size_t count = sizeof scores / sizeof scores[0];
    printf("Average: %.2f\n", average(scores, count));

    char name[16];
    snprintf(name, sizeof name, "%s %d", "User", 42);
    printf("%s (length %zu)\n", name, strlen(name));
    return 0;
}

Pointers

A pointer stores an address. Pointers let functions modify caller data, work with arrays, and build dynamic data structures.

  • &x gives the address of x; *p dereferences pointer p.
  • A NULL pointer points to nothing; never dereference it.
  • Pointer arithmetic moves by whole elements: p + 1 advances by sizeof *p bytes.
  • Only dereference pointers that refer to valid, live objects.
  • Do not return the address of a local variable; it is destroyed when the function ends.

Pointers, swap, and pointer arithmetic

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 3, y = 9;
    swap(&x, &y);
    printf("x=%d y=%d\n", x, y);          // x=9 y=3

    int nums[] = {10, 20, 30};
    int *p = nums;                         // points to nums[0]
    printf("%d %d\n", *p, *(p + 2));       // 10 30

    int *none = NULL;
    if (none == NULL) puts("pointer is NULL");
    return 0;
}

Dynamic Memory: malloc and free

  • malloc allocates memory on the heap and returns NULL on failure; always check it.
  • calloc allocates and zeroes memory; realloc resizes a block.
  • Every successful allocation must be released with exactly one free.
  • Set pointers to NULL after freeing to avoid accidental reuse.
  • Common bugs: memory leaks, double free, use after free, and buffer overflows.

Allocating and freeing a dynamic array

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t n = 5;
    int *data = malloc(n * sizeof *data);
    if (data == NULL) {
        fprintf(stderr, "Out of memory\n");
        return 1;
    }

    for (size_t i = 0; i < n; ++i) data[i] = (int)(i * i);

    int *bigger = realloc(data, 10 * sizeof *data);
    if (bigger == NULL) {          // original block still valid
        free(data);
        return 1;
    }
    data = bigger;

    printf("data[4] = %d\n", data[4]);

    free(data);
    data = NULL;
    return 0;
}

Structs and Custom Types

  • struct groups related values under one name.
  • Use typedef to avoid repeating the struct keyword.
  • Pass large structs by pointer, using const when they are read-only.
  • Access members with . on values and -> on pointers.

A struct with functions and heap allocation

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[32];
    int age;
    double gpa;
} Student;

void print_student(const Student *s) {
    printf("%s (%d) GPA %.2f\n", s->name, s->age, s->gpa);
}

int main(void) {
    Student a = {"Asha", 20, 3.8};
    print_student(&a);

    Student *b = malloc(sizeof *b);
    if (!b) return 1;
    snprintf(b->name, sizeof b->name, "%s", "Ravi");
    b->age = 21;
    b->gpa = 3.4;
    print_student(b);
    free(b);
    return 0;
}

File I/O

  • fopen returns NULL on failure; always check it.
  • Use fgets, fprintf, fread, and fwrite for reading and writing.
  • Close every file with fclose.
  • Use perror to print a helpful system error message.

Writing and reading a text file

#include <stdio.h>

int main(void) {
    FILE *out = fopen("notes.txt", "w");
    if (!out) { perror("fopen"); return 1; }
    fprintf(out, "line one\nline two\n");
    fclose(out);

    FILE *in = fopen("notes.txt", "r");
    if (!in) { perror("fopen"); return 1; }

    char line[128];
    while (fgets(line, sizeof line, in)) {
        printf("read: %s", line);
    }
    fclose(in);
    return 0;
}

Common Bugs and How to Catch Them

  • Buffer overflow: writing past the end of an array. Use bounded functions and check lengths.
  • Use after free: reading memory after free. Set pointers to NULL after freeing.
  • Memory leak: forgetting to free. Pair each allocation with a free path.
  • Uninitialized variables: always initialize before reading.
  • Off-by-one errors: remember arrays are zero-indexed.
  • Format string mismatches: match printf specifiers to argument types.

Using sanitizers and warnings to find bugs

cc -g -Wall -Wextra -Wpedantic -std=c11 -fsanitize=address,undefined main.c -o main
./main

# Memory checking on Linux:
valgrind --leak-check=full ./main

Best Practices

  • Compile with -Wall -Wextra -Wpedantic and fix every warning.
  • Check the result of malloc, fopen, and input functions.
  • Prefer size_t for sizes and indexes; pass array lengths explicitly.
  • Use const wherever data is not modified.
  • Keep functions short and give them clear ownership rules for memory.
  • Never use unsafe functions such as gets; avoid unbounded strcpy and sprintf.
  • Test with sanitizers and a memory checker regularly.

Complete Example: Calculating an Average Safely

Compile with a C compiler such as GCC or Clang. For example: cc -Wall -Wextra -Wpedantic -std=c11 main.c -o main.

Calculate an average safely

#include <stdio.h>
#include <stddef.h>

double average(const int values[], size_t count) {
    if (count == 0) {
        return 0.0;
    }

    long long sum = 0;
    for (size_t i = 0; i < count; ++i) {
        sum += values[i];
    }

    return (double)sum / (double)count;
}

int main(void) {
    const int scores[] = {82, 91, 76, 88};
    const size_t count = sizeof scores / sizeof scores[0];

    printf("Average: %.2f\n", average(scores, count));
    return 0;
}

Frequently Asked Questions

Is C difficult to learn?

C requires careful attention to types, pointers, and memory, but learning it builds a strong understanding of how programs interact with memory.

What is a pointer?

A pointer is an object that stores the address of another object or function, subject to C's pointer rules.

Why compile with warnings?

Compiler warnings can catch suspicious conversions, missing declarations, and other mistakes before the program runs.

What is the difference between malloc and calloc?

malloc allocates uninitialized memory, while calloc allocates memory for an array and sets all bytes to zero.

What is undefined behavior?

It means the C standard places no requirements on what happens, such as reading out of bounds or overflowing a signed integer. The program might crash, appear to work, or behave unpredictably.

What is the difference between C and C++?

C++ started from C but adds classes, templates, a larger standard library, and many other features. Idiomatic C and idiomatic C++ look quite different.

← Back to All Articles