HSHimanshu Sharma
Basic C & C++

C language for beginners: programs, types, pointers, structs and files

C language for beginners: programs, types, pointers, structs and files Advertisement

C is more than fifty years old and still runs the world's operating systems, databases, embedded devices and programming-language interpreters. Python, PHP and many other languages are themselves written in C. Learning it teaches you what other languages hide: how memory is laid out, what a pointer is, what "compiling" actually does. It is a small language, so you can learn the whole thing, but it gives you no safety net, so you must be careful. This guide takes you from your first program to structs and files. Every program below was compiled with clang -Wall -Wextra -std=c11 and run on macOS; they behave the same with gcc.

A short history

C was created by Dennis Ritchie at Bell Labs in the early 1970s, evolving from the earlier language B, mainly to rewrite the Unix operating system in a language that was not tied to one machine. It was described in the book The C Programming Language (Kernighan and Ritchie, 1978) and later standardised (ANSI C in 1989, then C99, C11, C17 and C23). C is a general-purpose, procedural, statically typed, compiled language. "Procedural" means programs are built from functions that operate on data; "statically typed" means every variable has a type fixed at compile time; "compiled" means a compiler translates your source into machine code before the program runs.

Your first program

#include <stdio.h>

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

Save it as hello.c, then compile and run it:

$ cc hello.c -o hello
$ ./hello
Hello, C!

Line by line: #include <stdio.h> pulls in the declaration of printf. main is where the program starts; int says it returns a number to the operating system, and return 0 means success. printf prints text, and \n is a newline. Statements end with a semicolon, blocks use curly braces, and C is case-sensitive.

What the compiler does

Compiling is four steps, which cc runs for you: the preprocessor handles lines starting with # (it pastes in header files and expands macros), the compiler turns C into assembly, the assembler makes an object file, and the linker joins object files and libraries into the final program. When you see an error such as undefined reference, it is the linker; expected ';' is the compiler. Always compile with warnings on (-Wall -Wextra) and read them.

Data types and variables

Type Typical size Example
char 1 byte 'A'
short, int 2, 4 bytes 42
long, long long 8 bytes (platform dependent) 9000000000LL
float, double 4, 8 bytes 3.14, 2.718281828

Sizes depend on the platform; use sizeof to find them. Variables must be declared before use, and a local variable that is not initialised holds garbage, so always give it a value.

#include <stdio.h>
#include <limits.h>

int main(void) {
    int age = 30;
    double price = 19.99;
    char grade = 'A';
    printf("%d %.2f %c\n", age, price, grade);
    printf("sizes: char=%zu int=%zu long=%zu double=%zu\n",
           sizeof(char), sizeof(int), sizeof(long), sizeof(double));
    printf("int max = %d\n", INT_MAX);
    printf("7/2 = %d, 7%%2 = %d, 7/2.0 = %.1f\n", 7 / 2, 7 % 2, 7 / 2.0);
    return 0;
}
30 19.99 A
sizes: char=1 int=4 long=8 double=8
int max = 2147483647
7/2 = 3, 7%2 = 1, 7/2.0 = 3.5

Notice that dividing two integers gives an integer (7 / 2 is 3); make one operand a double to get a fractional result. The format characters in printf must match the types: %d int, %ld long, %f double, %c char, %s string, %zu for sizes, %% for a literal percent sign. A mismatch is undefined behaviour, and the compiler's warnings catch it.

Reading input

scanf reads formatted input. It needs the address of the variable, written with &:

#include <stdio.h>

int main(void) {
    int a, b;
    printf("Enter two numbers: ");
    if (scanf("%d %d", &a, &b) != 2) {
        printf("Invalid input\n");
        return 1;
    }
    printf("Sum = %d, product = %d\n", a + b, a * b);
    return 0;
}

With 12 5 typed as input it printed Enter two numbers: Sum = 17, product = 60. Always check the return value of scanf: it is the number of items read. Reading a whole line safely is done with fgets(buffer, sizeof buffer, stdin), which never writes past the end of the buffer, unlike the dangerous gets, which was removed from the language.

Operators and control flow

C has arithmetic (+ - * / %), comparison (== != < > <= >=), logical (&& || !), bitwise (& | ^ ~ << >>) and assignment operators (= += -= ++ --). Any non-zero value is true and zero is false. The classic mistake is if (x = 5) (assignment) instead of if (x == 5).

#include <stdio.h>

int main(void) {
    int marks = 72;
    if (marks >= 75) printf("distinction\n");
    else if (marks >= 60) printf("first class\n");
    else printf("pass or fail\n");

    for (int i = 1; i <= 5; i++) printf("%d ", i * i);
    printf("\n");

    int n = 5, fact = 1;
    while (n > 1) fact *= n--;
    printf("5! = %d\n", fact);

    int day = 3;
    switch (day) {
        case 1: printf("Mon\n"); break;
        case 3: printf("Wed\n"); break;
        default: printf("other\n");
    }
    return 0;
}
first class
1 4 9 16 25 
5! = 120
Wed

switch falls through to the next case unless you break. break leaves a loop and continue skips to the next iteration, as in most languages (compare the Python flow control post).

Functions

A function has a return type, a name, parameters and a body. C passes arguments by value: the function gets a copy.

#include <stdio.h>

int square(int n) { return n * n; }

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

long long factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }

int main(void) {
    int x = 3, y = 9;
    swap_wrong(x, y);
    printf("after swap_wrong: %d %d\n", x, y);
    swap(&x, &y);
    printf("after swap:       %d %d\n", x, y);
    printf("%d %lld\n", square(7), factorial(15));
    return 0;
}
after swap_wrong: 3 9
after swap:       9 3
49 1307674368000

swap_wrong changed copies, so nothing happened. swap received pointers (addresses) and changed the original variables. That is the point of pointers, explained next. A function must be declared before it is called, so larger programs put declarations (prototypes) in header files.

Pointers

A pointer is a variable that holds a memory address. &x gives the address of x; *p follows a pointer to the value it points to.

#include <stdio.h>

int main(void) {
    int x = 10;
    int *p = &x;
    *p = 25;
    printf("x = %d, *p = %d, p points to x: %s\n", x, *p, p == &x ? "yes" : "no");

    int a[4] = {10, 20, 30, 40};
    int *q = a;
    printf("%d %d %d\n", *q, *(q + 2), q[3]);
    printf("elements between: %ld\n", (long)(&a[3] - &a[0]));
    return 0;
}
x = 25, *p = 25, p points to x: yes
10 30 40
elements between: 3

Arrays and pointers are closely related: the name of an array becomes a pointer to its first element, and *(q + 2) is the same as q[2]. Pointer arithmetic moves in units of the element size. Dangers to remember: dereferencing an uninitialised pointer or NULL crashes the program; using a pointer to a variable that no longer exists (a "dangling pointer") is undefined behaviour; going past the end of an array corrupts memory silently.

Arrays and strings

An array holds a fixed number of elements of one type. C has no built-in string type: a string is an array of char ending with a zero byte '\0'.

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

int main(void) {
    int marks[5] = {70, 85, 60, 90, 75};
    int sum = 0, max = marks[0];
    for (int i = 0; i < 5; i++) {
        sum += marks[i];
        if (marks[i] > max) max = marks[i];
    }
    printf("sum=%d avg=%.1f max=%d\n", sum, sum / 5.0, max);

    char name[20] = "Asha";
    char copy[20];
    strcpy(copy, name);
    strcat(copy, " Sharma");
    printf("%s has %zu chars; name==copy? %s\n", copy, strlen(copy), strcmp(name, copy) == 0 ? "yes" : "no");

    char word[] = "hello";
    for (int i = 0, j = (int)strlen(word) - 1; i < j; i++, j--) {
        char t = word[i]; word[i] = word[j]; word[j] = t;
    }
    printf("reversed: %s\n", word);
    return 0;
}
sum=380 avg=76.0 max=90
Asha Sharma has 11 chars; name==copy? no
reversed: olleh

Use strcmp to compare strings (comparing with == compares addresses). strcpy and strcat do not check sizes, so make sure the destination is large enough, or use snprintf and strncpy carefully. Buffer overflows are the cause of a great many security bugs.

Structures

A struct groups related variables of different types, which is how you model a record such as a student.

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

struct Student {
    int roll;
    char name[30];
    float marks;
};

void print_student(const struct Student *s) {
    printf("%d %-8s %.1f\n", s->roll, s->name, s->marks);
}

int main(void) {
    struct Student list[3] = {
        {1, "Asha", 82.5f}, {2, "Ravi", 67.0f}, {3, "Meena", 91.25f}
    };
    struct Student best = list[0];
    for (int i = 1; i < 3; i++)
        if (list[i].marks > best.marks) best = list[i];
    for (int i = 0; i < 3; i++) print_student(&list[i]);
    printf("top: %s\n", best.name);
    return 0;
}
1 Asha     82.5
2 Ravi     67.0
3 Meena    91.2
top: Meena

Use . on a struct and -> on a pointer to a struct. (91.25 printed as 91.2 because %.1f rounds the binary value, which is exactly representable here and rounds to even; floating point rounding can surprise.) Passing const struct Student * avoids copying the struct and promises not to change it.

Dynamic memory

Arrays declared in a function live on the stack and have a fixed size. For sizes known only at run time, ask the system for memory with malloc and give it back with free:

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

int main(void) {
    int n = 5;
    int *data = malloc(n * sizeof *data);
    if (data == NULL) { fprintf(stderr, "out of memory\n"); return 1; }
    for (int i = 0; i < n; i++) data[i] = (i + 1) * 10;

    int *bigger = realloc(data, 8 * sizeof *data);
    if (bigger == NULL) { free(data); return 1; }
    data = bigger;
    for (int i = n; i < 8; i++) data[i] = (i + 1) * 10;

    for (int i = 0; i < 8; i++) printf("%d ", data[i]);
    printf("\n");
    free(data);
    data = NULL;
    return 0;
}
10 20 30 40 50 60 70 80 

The rules: check for NULL; every malloc needs exactly one free; never use memory after freeing it; never free twice. A memory leak is allocated memory that is never freed. Tools such as Valgrind (Linux) and AddressSanitizer (-fsanitize=address, in clang and gcc) find these mistakes; compile your test builds with it.

File handling

fopen opens a file and returns a FILE *; always check it for NULL, and fclose when done. Modes: "r" read, "w" write (erases), "a" append, with b for binary.

#include <stdio.h>

struct Student { int roll; char name[30]; float marks; };

int main(void) {
    struct Student s[2] = {{1, "Asha", 82.5f}, {2, "Ravi", 67.0f}};

    FILE *f = fopen("students.dat", "wb");
    if (!f) { perror("open for write"); return 1; }
    fwrite(s, sizeof s[0], 2, f);
    fclose(f);

    FILE *g = fopen("students.dat", "rb");
    if (!g) { perror("open for read"); return 1; }
    struct Student r;
    while (fread(&r, sizeof r, 1, g) == 1)
        printf("%d %s %.1f\n", r.roll, r.name, r.marks);
    fclose(g);

    FILE *missing = fopen("nothere.txt", "r");
    if (!missing) perror("nothere.txt");
    return 0;
}
1 Asha 82.5
2 Ravi 67.0
nothere.txt: No such file or directory

Binary files store the raw bytes of the struct, which is fast but not portable between systems with different layouts. Text files (fprintf, fscanf, fgets) are portable and human-readable, so prefer them for data you may share.

Putting it together: a small student database

A classic exercise is a program with a menu that can create, read, update and delete records (CRUD) kept in a file or an array, with search and sort. Build it in stages: first an array of structs in memory with functions add_student, list_students, find_student and delete_student; then save and load the array with fwrite/fread; then add input validation. It uses everything in this post: structs, arrays, functions, pointers, files and loops.

Common mistakes in C

  • Forgetting & in scanf, or using the wrong format character.
  • Using = instead of ==.
  • Reading or writing past the end of an array (no error, just corruption).
  • Using uninitialised variables.
  • Forgetting the '\0' terminator, or copying into a buffer that is too small.
  • Comparing strings with ==.
  • Leaking memory, or using freed memory.
  • Ignoring compiler warnings.

What to do next

Practise with small programs: prime numbers, sorting, a calculator, a matrix program. Then compare with another language: the C++ language overview builds on C, and the Java beginner series shows a memory-managed language. If you want the surrounding concepts first (what an operating system and a text editor are), read fundamentals of technology.

Advertisement