Post

C Programming Basics

Tutorial on C Programming

C Programming Basics

Your First Steps in C Programming: A Beginner’s Tutorial

Welcome to the world of C programming! C is a powerful, versatile, and foundational programming language that has influenced countless other languages. Developed in the early 1970s by Dennis Ritchie at Bell Labs, it’s known for its efficiency, low-level memory access, and widespread use in operating systems (like Linux and Windows), embedded systems, game development, and high-performance computing.

If you’re new to programming or coming from a higher-level language, C might seem a bit different, but mastering its basics provides a solid understanding of how computers actually work. Let’s dive in!

Who is this tutorial for? Absolute beginners with little to no programming experience, or those familiar with other languages looking to understand C fundamentals.

What will you learn?

  • How to set up your environment (briefly).
  • The structure of a basic C program.
  • Variables and fundamental data types.
  • Basic input and output operations.
  • Essential operators.
  • Control flow (making decisions and looping).
  • The basics of functions.
  • A glimpse into arrays and pointers.

1. Setting Up Your C Environment

Before you can write and run C code, you need a C compiler. A compiler translates your human-readable C code into machine code that your computer can execute.

  • GCC (GNU Compiler Collection): The most common C compiler, available for free on Linux, macOS, and Windows (often via MinGW or Cygwin).
    • Linux: Usually pre-installed or installable via package manager (e.g., sudo apt update && sudo apt install build-essential on Debian/Ubuntu).
    • macOS: Install Xcode Command Line Tools (xcode-select --install in Terminal).
    • Windows: Install MinGW-w64 (Minimalist GNU for Windows) or use WSL (Windows Subsystem for Linux).
  • IDE (Integrated Development Environment): Software that bundles a text editor, compiler, debugger, and other tools. Popular choices include:
    • Visual Studio Code (VS Code): Free, highly extensible, works on all platforms (needs C/C++ extension and a compiler like GCC).
    • Code::Blocks: Free, open-source, cross-platform IDE specifically for C/C++.
    • Visual Studio (Windows): Powerful IDE from Microsoft (Community edition is free).
    • Online Compilers: Websites like Replit, OnlineGDB, or Programiz Online C Compiler let you write and run C code directly in your browser – great for starting quickly!

For this tutorial, we’ll assume you have access to a compiler like GCC.

2. Your First C Program: “Hello, World!”

It’s a tradition! Let’s write a program that prints “Hello, World!” to the screen.

1
2
3
4
5
6
7
8
9
10
11
// 1. Include Header File
#include <stdio.h>

// 2. The main Function - Program execution begins here
int main() {
    // 3. Use printf to display output
    printf("Hello, World!\n");

    // 4. Return 0 to indicate successful execution
    return 0;
}

Let’s break it down:

  1. #include <stdio.h>: This is a preprocessor directive. It tells the compiler to include the contents of the standard input/output header file (stdio.h). This file contains declarations for functions like printf (for printing output) and scanf (for reading input).
  2. int main() { ... }: This is the main function. Every C program must have a main function, as it’s the starting point of execution.
    • int: Specifies that the main function will return an integer value to the operating system (by convention, 0 means success).
    • (): Parentheses indicate it’s a function. Sometimes you might see int main(void) which explicitly states it takes no arguments.
    • { ... }: Curly braces define the beginning and end of the function’s code block.
  3. printf("Hello, World!\n");: This is a statement calling the printf function.
    • printf: The function used to print formatted output to the console.
    • "Hello, World!\n": The string literal (text) to be printed.
    • \n: An escape sequence representing a newline character. It moves the cursor to the next line after printing.
    • ;: The semicolon marks the end of the statement. Almost every statement in C ends with a semicolon.
  4. return 0;: This statement exits the main function and returns the value 0 to the operating system, indicating that the program ran successfully.

Compiling and Running:

  1. Save the code above in a file named hello.c.
  2. Open your terminal or command prompt.
  3. Navigate to the directory where you saved the file.
  4. Compile the code using GCC:
    1
    
    gcc hello.c -o hello
    
    • gcc: Invokes the compiler.
    • hello.c: The input source file.
    • -o hello: Specifies the name of the output executable file (you can name it anything).
  5. Run the compiled program:
    • On Linux/macOS: ./hello
    • On Windows: hello.exe or .\hello.exe

You should see Hello, World! printed on your screen. Congratulations!

3. Variables and Data Types

Variables are named containers used to store data. In C, you must declare a variable before using it, specifying its data type.

Common Data Types:

  • int: Stores whole numbers (integers). Example: 10, -5, 0.
  • float: Stores single-precision floating-point numbers (numbers with decimal points). Example: 3.14, -0.5.
  • double: Stores double-precision floating-point numbers (more precision than float). Example: 3.14159265.
  • char: Stores a single character. Example: 'A', 'a', '!', ' '. Note the single quotes.

Declaration and Initialization:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

int main() {
    // Declaration
    int age;
    float price;
    char initial;

    // Initialization (assigning an initial value)
    age = 30;
    price = 99.95;
    initial = 'J';

    // Declaration and Initialization together
    double pi = 3.14159;
    int count = 0;

    // Using printf to display variable values
    // Format Specifiers: %d (int), %f (float/double), %c (char)
    printf("Age: %d\n", age);
    printf("Price: %.2f\n", price); // %.2f prints float/double with 2 decimal places
    printf("Initial: %c\n", initial);
    printf("Count: %d\n", count);
    printf("Pi: %f\n", pi); // %f can be used for double too

    return 0;
}

Format Specifiers: Notice printf uses placeholders like %d, %f, %c. These are format specifiers that tell printf what type of data to expect and how to format it.

4. Basic Input/Output (I/O)

We’ve seen printf for output. To get input from the user, we use the scanf function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

int main() {
    int userAge;
    float userPrice;
    char userInitial;

    // Prompt the user for input
    printf("Enter your age: ");
    // Read an integer input from the user
    scanf("%d", &userAge); // IMPORTANT: Note the '&' symbol!

    printf("Enter a price: ");
    scanf("%f", &userPrice);

    // Be careful when mixing scanf for numbers and characters
    // It's often better to read characters separately or handle whitespace
    printf("Enter your first initial: ");
    // Notice the space before %c to consume any leftover newline from previous scanf
    scanf(" %c", &userInitial);

    // Display the values entered by the user
    printf("\nYou entered:\n");
    printf("Age: %d\n", userAge);
    printf("Price: %.2f\n", userPrice);
    printf("Initial: %c\n", userInitial);

    return 0;
}

Crucial Point about scanf: Notice the & symbol before the variable names (&userAge, &userPrice, &userInitial). This is the address-of operator. scanf needs to know the memory address where it should store the input value. We’ll touch on addresses again when we mention pointers. Forgetting the & with scanf (except when reading strings into character arrays) is a very common beginner mistake!

5. Operators

C provides various operators to perform operations:

  • Arithmetic: + (addition), - (subtraction), * (multiplication), / (division), % (modulo - remainder of division).
    1
    2
    3
    4
    5
    6
    7
    
    int a = 10, b = 3;
    int sum = a + b; // 13
    int diff = a - b; // 7
    int product = a * b; // 30
    int quotient = a / b; // 3 (integer division truncates)
    int remainder = a % b; // 1
    float preciseQuotient = (float)a / b; // 3.333... (cast 'a' to float for float division)
    
  • Relational: Used for comparison, result in 1 (true) or 0 (false).
    • == (Equal to)
    • != (Not equal to)
    • > (Greater than)
    • < (Less than)
    • >= (Greater than or equal to)
    • <= (Less than or equal to)
      1
      2
      3
      
      int x = 5, y = 10;
      printf("x == y? %d\n", x == y); // 0 (false)
      printf("x < y? %d\n", x < y);   // 1 (true)
      
  • Logical: Combine or negate boolean conditions.
    • && (Logical AND - true if both sides are true)
    • || (Logical OR - true if at least one side is true)
    • ! (Logical NOT - inverts true to false, false to true)
      1
      2
      3
      
      int isRaining = 1; // Treat 1 as true
      int haveUmbrella = 0; // Treat 0 as false
      printf("Go out? %d\n", !isRaining || haveUmbrella); // 0 (false, because it's raining AND no umbrella)
      
  • Assignment: = (Assign value), +=, -=, *=, /=, %= (Shorthand operations, e.g., x += 5 is x = x + 5).

6. Control Flow

Control flow statements allow you to alter the execution path based on conditions or repeat blocks of code.

a) if, else if, else (Conditional Execution)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
    int score = 75;

    if (score >= 90) {
        printf("Grade: A\n");
    } else if (score >= 80) {
        printf("Grade: B\n");
    } else if (score >= 70) {
        printf("Grade: C\n");
    } else if (score >= 60) {
        printf("Grade: D\n");
    } else {
        printf("Grade: F\n");
    }

    return 0;
}

b) Loops (Repetition)

  • for loop: Used when you know the number of iterations in advance.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    #include <stdio.h>
    int main() {
        // Print numbers from 1 to 5
        // for (initialization; condition; increment/decrement)
        for (int i = 1; i <= 5; i++) {
            printf("%d ", i); // Output: 1 2 3 4 5
        }
        printf("\n");
        return 0;
    }
    
  • while loop: Used when the loop continues as long as a condition is true. The condition is checked before each iteration.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    #include <stdio.h>
    int main() {
        int count = 0;
        while (count < 3) {
            printf("Looping... ");
            count++; // Important: update the counter to avoid infinite loop
        }
        printf("\nDone.\n"); // Output: Looping... Looping... Looping... Done.
        return 0;
    }
    
  • do-while loop: Similar to while, but the condition is checked after the loop body executes. Guarantees the loop runs at least once.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #include <stdio.h>
    int main() {
        int input;
        do {
            printf("Enter a positive number: ");
            scanf("%d", &input);
        } while (input <= 0); // Keep asking until positive number is entered
    
        printf("You entered: %d\n", input);
        return 0;
    }
    

7. Functions

Functions allow you to break down your code into smaller, reusable, and manageable blocks. We’ve already used main and printf. Let’s define our own.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

// 1. Function Declaration (Prototype) - Tells the compiler about the function
int add(int num1, int num2); // Takes two ints, returns an int

// The main function
int main() {
    int a = 10;
    int b = 20;
    int sum;

    // 3. Function Call
    sum = add(a, b); // Pass 'a' and 'b' as arguments

    printf("The sum of %d and %d is %d\n", a, b, sum); // Output: The sum of 10 and 20 is 30

    return 0;
}

// 2. Function Definition - Contains the actual code
int add(int num1, int num2) { // 'num1' and 'num2' are parameters
    int result = num1 + num2;
    return result; // Return the calculated value
}

Key Parts:

  1. Declaration (Prototype): Placed before main (or in a header file). Tells the compiler the function’s name, return type, and parameter types.
  2. Definition: The actual implementation of the function’s logic.
  3. Call: Invoking the function from another part of the program (like main), passing arguments (values like a and b) that correspond to the function’s parameters (num1 and num2).

8. A Glimpse into Arrays and Pointers

Arrays: Store multiple values of the same data type in a contiguous block of memory. Accessed using an index (starting from 0).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

int main() {
    // Declare an integer array of size 5
    int numbers[5];

    // Initialize elements
    numbers[0] = 10;
    numbers[1] = 20;
    numbers[2] = 30;
    numbers[3] = 40;
    numbers[4] = 50;
    // numbers[5] = 60; // ERROR: Index out of bounds! (Valid indices are 0 to 4)

    // Access and print an element
    printf("The element at index 2 is: %d\n", numbers[2]); // Output: 30

    // Initialize during declaration
    float temps[] = {25.5, 26.1, 24.9}; // Size inferred from initializers

    // Loop through an array
    printf("Temperatures: ");
    for (int i = 0; i < 3; i++) { // Need to know the size (3 here)
        printf("%.1f ", temps[i]);
    }
    printf("\n");

    return 0;
}

Pointers (Brief Intro): A pointer is a variable that stores the memory address of another variable. They are fundamental in C for tasks like dynamic memory allocation, efficient function arguments, and working with complex data structures.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
    int var = 10;
    int *ptr; // Declare a pointer variable 'ptr' that can point to an int

    ptr = &var; // Assign the address of 'var' to 'ptr' using '&' (address-of operator)

    printf("Value of var: %d\n", var);
    printf("Address of var: %p\n", &var); // %p is format specifier for addresses
    printf("Value stored in ptr (address of var): %p\n", ptr);
    printf("Value pointed to by ptr (*ptr): %d\n", *ptr); // Use '*' (dereference operator) to get the value at the address

    // Change value using the pointer
    *ptr = 25;
    printf("New value of var (changed via pointer): %d\n", var); // Output: 25

    return 0;
}

Pointers are a deeper topic, but understanding the & (address-of) and * (dereference) operators is the first step. Remember how scanf needed the &? It needed the address to store the value!

9. Comments

Use comments to explain your code. The compiler ignores them.

1
2
3
4
5
6
7
// This is a single-line comment

/*
This is a
multi-line
comment.
*/

Strings (In Depth)

In many programming languages, strings are a built-in fundamental data type. In C, however, a string is essentially treated as an array of characters that follows a specific convention: it must end with a special character called the null terminator (\0). This null character signals the end of the string.

Working with strings in C often involves direct memory manipulation (via arrays and pointers) and using standard library functions designed for these null-terminated character arrays.

1. Representing Strings: Null-Terminated Character Arrays

The most common way to handle strings is using an array of char.

1
2
char message[20]; // Declares a character array that can hold up to 19 characters + the null terminator.
char greeting[] = "Hello"; // Declares and initializes an array. Size is automatically determined (6: H, e, l, l, o, \0).

Let’s visualize greeting:

Index012345
Value‘H’‘e’‘l’‘l’‘o’\0
  • "Hello": This string literal automatically includes the null terminator (\0) when used to initialize an array.
  • Size: The greeting array needs 6 elements to store the 5 characters of “Hello” plus the final \0. If you declare char greeting[5] = "Hello";, you won’t have space for the null terminator, which will cause problems with standard string functions.

2. Declaring and Initializing Strings

There are several ways to create strings:

a) Character Array Initialization:

1
2
3
4
5
6
7
8
// Size automatically determined (including \0)
char str1[] = "Welcome";

// Explicit size (must be large enough for content + \0)
char str2[50] = "Learning C is fun!";

// Initializing character by character (MANUALLY add \0)
char str3[] = {'C', ' ', 'i', 's', ' ', 'f', 'u', 'n', '!', '\0'}; // Don't forget \0!

b) Using Pointers (String Literals):

1
char *str4 = "This is a string literal.";
  • Important Distinction: When you use char *str = "...", str is a pointer that points to the memory location where the string literal "..." is stored. This memory area is often read-only. Attempting to modify the string through this pointer (e.g., str4[0] = 'X';) leads to undefined behavior (often a crash).
  • In contrast, strings declared as arrays (char str1[] = "...") are stored in memory that can usually be modified (e.g., str1[0] = 'W'; is typically okay).

Rule of Thumb: If you need to modify the string later, declare it as an array (char name[size]; or char name[] = "...";). If it’s a constant string you only need to read, a char * pointer is acceptable (and sometimes more efficient).

3. The Importance of the Null Terminator (\0)

Standard C string functions (like those for copying, concatenating, or finding the length) rely entirely on the null terminator (\0) to know where the string data ends. If a character array that you intend to use as a string lacks a \0, these functions will read past the end of your intended data, accessing invalid memory, which leads to crashes or incorrect results.

1
2
3
char bad_string[3] = {'B', 'A', 'D'}; // NO null terminator!
// printf("%s\n", bad_string); // DANGEROUS! printf will read past the array bounds.
// strlen(bad_string);        // DANGEROUS! strlen will keep counting until it finds a \0 somewhere in memory.

Always ensure your character arrays have enough space for and contain the \0 if you plan to treat them as strings.

4. String Manipulation Functions (<string.h>)

C provides a standard library string.h containing many useful functions for working with null-terminated strings. You need to include it: #include <string.h>

a) strlen() - Finding String Length

Calculates the length of a string excluding the null terminator.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>

int main() {
    char myString[] = "Example"; // Length is 7
    size_t len = strlen(myString); // size_t is an unsigned integer type suitable for sizes

    printf("String: %s\n", myString);
    printf("Length: %zu\n", len); // Use %zu format specifier for size_t

    return 0;
}

Output:

1
2
String: Example
Length: 7

b) strcpy() and strncpy() - Copying Strings

Used to copy one string into another character array.

  • strcpy(destination, source);
    • Copies the string from source (including \0) into destination.
    • DANGER: strcpy performs no bounds checking. If source is longer than destination can hold (including \0), it will write past the end of the destination buffer, causing a buffer overflow – a major security risk and common cause of crashes. Avoid strcpy unless you are absolutely certain the destination is large enough.
  • strncpy(destination, source, n);
    • Copies at most n characters from source to destination.
    • Safer, but tricky: If source has n or more characters (without \0), strncpy will not null-terminate destination. If source has fewer than n characters, it copies the \0 and pads the rest of destination (up to n characters) with \0.
    • Best Practice: Always manually ensure null termination after using strncpy if there’s a chance the source was truncated.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "Copy this!";
    char dest1[50];
    char dest2[10]; // Too small for src!
    char dest3[10];

    // Using strcpy (safe here because dest1 is large enough)
    strcpy(dest1, src);
    printf("dest1 after strcpy: %s\n", dest1);

    // Using strncpy (safer)
    strncpy(dest3, src, sizeof(dest3) - 1); // Copy at most 9 chars (leave space for \0)
    dest3[sizeof(dest3) - 1] = '\0';      // MANUALLY ensure null termination
    printf("dest3 after strncpy: %s\n", dest3); // Output: Copy this

    // --- DANGER ZONE ---
    // strcpy(dest2, src); // <-- !! BUFFER OVERFLOW !! Don't do this! Crashes likely.
    // printf("dest2 after unsafe strcpy: %s\n", dest2);

    return 0;
}

c) strcat() and strncat() - Concatenating Strings

Appends one string to the end of another.

  • strcat(destination, source);
    • Appends a copy of source to the end of destination. The first character of source overwrites the \0 at the end of destination.
    • DANGER: Like strcpy, performs no bounds checking. If destination doesn’t have enough space for its original content plus the content of source plus the final \0, it will cause a buffer overflow. Avoid strcat unless absolutely certain the destination is large enough.
  • strncat(destination, source, n);
    • Appends at most n characters from source to the end of destination.
    • It always null-terminates the result (provided destination had space for its original content plus at least one appended character plus the final \0).
    • Safer, but you need to calculate n carefully to avoid truncation or overflow. n should be less than sizeof(destination) - strlen(destination) - 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>

int main() {
    char greeting[50] = "Hello, "; // Ensure enough space initially
    char name[] = "Alice";
    char place[20] = "Welcome "; // Less space
    char city[] = "Wonderland";

    // Using strcat (safe here)
    strcat(greeting, name); // Appends "Alice"
    printf("strcat result: %s\n", greeting); // Output: Hello, Alice

    // Using strncat (safer)
    // Calculate remaining space: sizeof(place) - strlen(place) - 1
    size_t remaining_space = sizeof(place) - strlen(place) - 1;
    strncat(place, city, remaining_space);
    // place might be "Welcome Wonderland" if space permits, or truncated like "Welcome Wonderlan"
    printf("strncat result: %s\n", place);

    return 0;
}

d) strcmp() and strncmp() - Comparing Strings

Compares two strings lexicographically (like in a dictionary).

  • strcmp(str1, str2);
  • strncmp(str1, str2, n); (Compares at most n characters)

Return Value:

  • 0 if str1 is equal to str2.
  • < 0 (negative value) if str1 comes before str2 alphabetically.
  • > 0 (positive value) if str1 comes after str2 alphabetically.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <string.h>

int main() {
    char pass1[] = "secret123";
    char pass2[] = "secret123";
    char pass3[] = "Secret123"; // Different case
    char pass4[] = "secretABC";

    if (strcmp(pass1, pass2) == 0) {
        printf("'%s' and '%s' are identical.\n", pass1, pass2);
    }

    if (strcmp(pass1, pass3) != 0) {
        printf("'%s' and '%s' are different (case matters).\n", pass1, pass3);
    }

    if (strcmp(pass1, pass4) > 0) { // 's' > 'A'
         printf("'%s' comes after '%s'.\n", pass1, pass4);
    }

    // Using strncmp - compare first 6 characters
    if (strncmp(pass1, pass4, 6) == 0) {
        printf("The first 6 characters of '%s' and '%s' match.\n", pass1, pass4);
    }

    return 0;
}

5. String Input/Output

a) Printing Strings: Use printf with the %s format specifier.

1
2
char message[] = "Printing strings is easy.";
printf("%s\n", message);

b) Reading Strings:

  • scanf("%s", buffer);
    • Reads a sequence of non-whitespace characters from input and stores it in the buffer array.
    • It automatically adds the \0 terminator.
    • Stops reading at the first whitespace character (space, tab, newline).
    • DANGER: Like strcpy, scanf("%s", ...) performs no bounds checking. If the user types more characters than the buffer can hold (before hitting whitespace), it causes a buffer overflow. Avoid scanf("%s", buffer) without width limits.
    • Slightly Safer scanf: scanf("%19s", buffer); - Reads at most 19 characters into buffer (assuming buffer has size 20), leaving space for \0. Better, but still stops at whitespace.
  • fgets(buffer, size, stdin);
    • The recommended safe way to read string input.
    • Reads characters from the specified input stream (stdin for standard input/keyboard) into buffer.
    • Reads at most size - 1 characters, ensuring space is left for the \0.
    • Stops reading when it encounters a newline (\n) or reaches the size limit.
    • Includes the newline character (\n) in the buffer if it was read before hitting the size limit. This often needs to be handled (removed).
    • Returns NULL on error or end-of-file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <string.h> // For strcspn

int main() {
    char name[50]; // Buffer to hold the name

    printf("Enter your full name: ");

    // Read using fgets (safer)
    if (fgets(name, sizeof(name), stdin) != NULL) {
        // Remove trailing newline, if present
        name[strcspn(name, "\n")] = '\0'; // Finds the first newline and replaces it with null

        printf("Hello, %s!\n", name);
    } else {
        printf("Error reading input.\n");
    }

    return 0;
}

(Note: strcspn(name, "\n") returns the index of the first newline character in name. Replacing that character with \0 effectively removes the newline read by fgets.)

Key Takeaways for C Strings

  • Strings are null-terminated (\0) character arrays.
  • Be mindful of buffer sizes to prevent overflows. Always allocate enough space for your string data plus the \0.
  • Prefer safer functions like strncpy, strncat, and fgets over their unsafe counterparts (strcpy, strcat, scanf("%s", ...)).
  • Manually ensure null termination when using functions like strncpy that might not add it automatically.
  • Remember the difference between char array[] = "..." (modifiable) and char *ptr = "..." (points to potentially read-only literal).
  • Include <string.h> for string manipulation functions.
  • Handle the trailing newline character often included by fgets.

We’ve covered the fundamentals, including variables, control flow, basic functions, arrays, and strings. Now, let’s delve into more powerful features of C that allow for greater flexibility and control: Structures, File Input/Output, a deeper look at Pointers, and Dynamic Memory Allocation.

Structs: Creating Custom Data Types

Often, you need to group related data items together, even if they are of different types. For example, to represent a student, you might need their ID (integer), name (string/char array), and GPA (float). C’s struct (structure) allows you to create your own custom data types to bundle these items.

a) Defining a Structure:

You define a structure using the struct keyword, followed by a tag name (like Student), and then curly braces {} containing the member variables (fields) with their data types.

1
2
3
4
5
6
// Define a structure named 'Student'
struct Student {
    int studentID;
    char name[100]; // Character array for the name
    float gpa;
}; // Don't forget the semicolon here!

This definition creates a blueprint or template for a Student type. It doesn’t allocate any memory yet.

b) Declaring Structure Variables:

Once defined, you can declare variables of this structure type, just like you declare int or float variables.

1
2
struct Student student1; // Declares a variable 'student1' of type 'struct Student'
struct Student student2, student3; // Declare multiple variables

c) Accessing Structure Members:

You access the individual members (fields) within a structure variable using the dot (.) operator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <string.h> // For strcpy

// Define the structure
struct Student {
    int studentID;
    char name[100];
    float gpa;
};

int main() {
    // Declare a structure variable
    struct Student student1;

    // Assign values to members using the dot operator
    student1.studentID = 101;
    strcpy(student1.name, "Alice Wonderland"); // Use strcpy for strings
    student1.gpa = 3.85;

    // Access and print members
    printf("Student ID: %d\n", student1.studentID);
    printf("Name: %s\n", student1.name);
    printf("GPA: %.2f\n", student1.gpa);

    // You can also initialize during declaration (C99+)
    struct Student student2 = {102, "Bob The Builder", 3.5};
    printf("\nStudent ID: %d\n", student2.studentID);
    printf("Name: %s\n", student2.name);
    printf("GPA: %.2f\n", student2.gpa);

    return 0;
}

d) Pointers to Structures:

You can have pointers that point to structure variables. Accessing members through a pointer uses the arrow (->) operator instead of the dot (.).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>

struct Student { /* ... definition as above ... */ };

int main() {
    struct Student student1 = {101, "Alice Wonderland", 3.85};
    struct Student *ptrStudent; // Declare a pointer to a Student struct

    ptrStudent = &student1; // Assign the address of student1 to the pointer

    // Access members using the arrow operator (->) with the pointer
    printf("ID (via pointer): %d\n", ptrStudent->studentID);
    printf("Name (via pointer): %s\n", ptrStudent->name);
    printf("GPA (via pointer): %.2f\n", ptrStudent->gpa);

    // The arrow operator is shorthand for (*ptrStudent).member
    printf("GPA (alternative pointer syntax): %.2f\n", (*ptrStudent).gpa);

    return 0;
}

e) typedef with Structs:

Using typedef can create an alias for struct YourStructName, making declarations cleaner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Define the struct and create an alias 'Point' using typedef
typedef struct {
    int x;
    int y;
} Point; // 'Point' is now an alias for the struct type

int main() {
    Point p1 = {10, 20}; // Use the alias directly
    Point p2;
    p2.x = 5;
    p2.y = -3;

    printf("P1: (%d, %d)\n", p1.x, p1.y);
    printf("P2: (%d, %d)\n", p2.x, p2.y);

    return 0;
}

Structs are essential for organizing complex data and are the building blocks for more advanced data structures like linked lists and trees.

File I/O: Reading From and Writing To Files

Your C programs often need to interact with files to read data or store results persistently. C provides a set of functions (mostly in stdio.h) for file input/output (I/O).

a) The FILE Pointer:

File operations revolve around a special pointer type: FILE *. This pointer holds information about the file being accessed (like its location, current position, etc.).

1
FILE *filePointer; // Declare a FILE pointer

b) Opening a File (fopen)

Before you can read or write, you must open the file using fopen().

FILE *fopen(const char *filename, const char *mode);

  • filename: A string containing the name (and optionally path) of the file.
  • mode: A string specifying how you want to open the file. Common modes:
    • "r": Open for reading (file must exist).
    • "w": Open for writing (creates file if it doesn’t exist, truncates to zero length/overwrites if it does).
    • "a": Open for appending (creates file if it doesn’t exist, writes are added to the end).
    • "rb", "wb", "ab": Like r, w, a, but in binary mode (important for non-text files).
    • "r+", "w+", "a+": Open for both reading and writing.

Crucially: fopen returns a FILE pointer if successful, or NULL if an error occurs (e.g., file not found in "r" mode, no permissions). Always check the return value!

1
2
3
4
5
6
FILE *outfile = fopen("output.txt", "w");
if (outfile == NULL) {
    perror("Error opening file for writing"); // perror prints a system error message
    return 1; // Indicate an error occurred
}
// ... proceed to write to the file ...

c) Writing to a File:

  • fprintf(filePointer, format_string, ...);: Writes formatted output to the specified file, similar to printf.
  • fputc(char, filePointer);: Writes a single character.
  • fputs(string, filePointer);: Writes a string (stops at \0, doesn’t add a newline).
1
2
3
4
5
fprintf(outfile, "Hello from C!\n");
int value = 123;
fprintf(outfile, "The value is: %d\n", value);
fputs("This is another line.\n", outfile);
fputc('X', outfile);

d) Reading from a File:

  • fscanf(filePointer, format_string, ...);: Reads formatted input from the file, similar to scanf. Needs addresses (&) for variables. Returns the number of items successfully read, or EOF (End Of File).
  • fgetc(filePointer);: Reads a single character (returns int, or EOF at end-of-file/error).
  • fgets(buffer, size, filePointer);: Reads a line of text (or up to size-1 characters) into buffer. Includes newline (\n) if read. Safer than fscanf for reading strings. Returns NULL at end-of-file/error.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FILE *infile = fopen("input.txt", "r");
if (infile == NULL) {
    perror("Error opening file for reading");
    return 1;
}

char lineBuffer[256];
int readValue;

// Read using fscanf
if (fscanf(infile, "%d", &readValue) == 1) { // Check if one item was read
    printf("Read value using fscanf: %d\n", readValue);
}

// Read lines using fgets
printf("Reading lines with fgets:\n");
while (fgets(lineBuffer, sizeof(lineBuffer), infile) != NULL) {
    printf("Read line: %s", lineBuffer); // fgets includes \n, so no extra \n needed here
}

e) Closing a File (fclose)

When you’re finished with a file, you must close it using fclose().

int fclose(FILE *filePointer);

  • Flushes any buffered output to the disk.
  • Releases system resources associated with the file.
  • Returns 0 on success, EOF on error.
1
2
fclose(outfile);
fclose(infile);

Forgetting to close files can lead to data loss (output might stay in buffers) and resource leaks.

Example: Write and Read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h> // For exit()

int main() {
    FILE *fp;
    char textToWrite[] = "This is line 1.\nThis is line 2.";
    char buffer[100];

    // --- Writing to file ---
    fp = fopen("mydata.txt", "w");
    if (fp == NULL) {
        perror("Error opening file for writing");
        exit(1);
    }
    printf("Writing to file...\n");
    fprintf(fp, "%s\n", textToWrite);
    fprintf(fp, "Adding a number: %d\n", 42);
    fclose(fp); // Close after writing
    printf("Finished writing.\n\n");


    // --- Reading from file ---
    fp = fopen("mydata.txt", "r");
     if (fp == NULL) {
        perror("Error opening file for reading");
        exit(1);
    }
    printf("Reading from file using fgets:\n");
    while(fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer); // Print line read (includes newline)
    }
    fclose(fp); // Close after reading
    printf("\nFinished reading.\n");

    return 0;
}

Pointers (In Depth)

We introduced pointers as variables storing memory addresses. Let’s explore their capabilities further.

a) Pointer Arithmetic:

You can perform limited arithmetic operations on pointers. When you add an integer n to a pointer ptr, the pointer is advanced by n * sizeof(*ptr) bytes in memory. This means it points n elements forward, based on the size of the data type it points to. Similarly for subtraction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    int *ptr = numbers; // Pointer points to the first element (numbers[0])

    printf("ptr points to address: %p, value: %d\n", ptr, *ptr); // Address of numbers[0], value 10

    // Advance the pointer by 1 element (adds sizeof(int) bytes to the address)
    ptr++;
    printf("ptr+1 points to address: %p, value: %d\n", ptr, *ptr); // Address of numbers[1], value 20

    // Advance by 2 more elements
    ptr = ptr + 2; // equivalent to ptr += 2;
    printf("ptr+3 points to address: %p, value: %d\n", ptr, *ptr); // Address of numbers[3], value 40

    // Subtracting pointers (gives the number of elements between them)
    int *ptr_start = numbers;
    int *ptr_end = &numbers[4]; // Pointer to the last element
    ptrdiff_t diff = ptr_end - ptr_start; // ptrdiff_t is a type for pointer differences
    printf("Elements between ptr_end and ptr_start: %td\n", diff); // Output: 4

    return 0;
}

Valid Operations: Addition/subtraction of an integer, subtracting two pointers (pointing within the same array), comparison (<, >, ==, etc.). Invalid Operations: Adding two pointers, multiplication, division.

b) Pointers and Arrays:

There’s a very strong connection:

  • An array name, when used in many contexts (like being passed to a function or assigned to a pointer), decays into a pointer to its first element.
  • array_name is equivalent to &array_name[0].
  • array_name[i] is equivalent to *(array_name + i).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

void printArray(int *arr, int size) { // Function accepting a pointer to int
    printf("Array elements: ");
    for (int i = 0; i < size; i++) {
        // Both notations work:
        // printf("%d ", arr[i]);
        printf("%d ", *(arr + i)); // Access using pointer arithmetic + dereference
    }
    printf("\n");
}

int main() {
    int data[] = {5, 10, 15, 20};
    int size = sizeof(data) / sizeof(data[0]);

    printf("Value at data[1]: %d\n", data[1]);     // Array notation
    printf("Value at *(data + 1): %d\n", *(data + 1)); // Pointer notation

    printArray(data, size); // Pass the array name (decays to pointer)

    return 0;
}

c) Pointers and Functions:

  • Pass by Reference: As seen with scanf and our swap example (if we wrote one), passing a pointer allows a function to modify the original variable whose address was passed. This simulates “pass by reference”.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    #include <stdio.h>
    
    void increment(int *value_ptr) { // Accepts a pointer to an integer
        (*value_ptr)++; // Increment the value AT the address pointed to
    }
    
    int main() {
        int counter = 10;
        printf("Counter before: %d\n", counter);
        increment(&counter); // Pass the ADDRESS of counter
        printf("Counter after: %d\n", counter); // Output: 11
        return 0;
    }
    
  • Returning Pointers: Functions can return pointers. However, NEVER return a pointer to a local variable of that function. Local variables cease to exist when the function returns, making the returned pointer invalid (a dangling pointer). Pointers returned should typically point to dynamically allocated memory (see next section) or static/global variables, or be pointers passed into the function.

    1
    2
    3
    4
    5
    6
    7
    
    // DANGEROUS - DO NOT DO THIS:
    /*
    int* createLocalVar() {
        int localVar = 100;
        return &localVar; // Returning address of variable that will be destroyed!
    }
    */
    

Dynamic Memory Allocation (<stdlib.h>)

Sometimes, you don’t know how much memory you need at compile time, or you need data to persist beyond the scope of a single function. C allows you to manually allocate and deallocate memory from a pool called the heap using functions from <stdlib.h>.

a) malloc (Memory Allocation):

void *malloc(size_t size);

  • Allocates a block of size bytes on the heap.
  • Returns a void* (generic pointer) to the beginning of the allocated block.
  • Returns NULL if allocation fails (e.g., out of memory). Always check for NULL!
  • The allocated memory block is uninitialized (contains garbage values).
  • You need to cast the void* to the appropriate pointer type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int *dynamicArray;
int numElements = 10;

// Allocate memory for 10 integers on the heap
dynamicArray = (int *)malloc(numElements * sizeof(int)); // Calculate total bytes needed

if (dynamicArray == NULL) {
    fprintf(stderr, "Memory allocation failed!\n");
    exit(1);
}

// Now you can use dynamicArray like a regular array (up to numElements)
dynamicArray[0] = 100;
*(dynamicArray + 1) = 200; // Pointer arithmetic works too

b) calloc (Contiguous Allocation):

void *calloc(size_t num, size_t size);

  • Allocates memory for an array of num elements, each of size bytes.
  • Initializes the allocated memory block to all zeros.
  • Returns void* or NULL (check return value).
1
2
3
4
5
6
float *floatArray;
int count = 5;

floatArray = (float *)calloc(count, sizeof(float)); // Allocate 5 floats, initialized to 0.0

if (floatArray == NULL) { /* ... handle error ... */ }

c) realloc (Re-allocation):

void *realloc(void *ptr, size_t newSize);

  • Changes the size of a previously allocated memory block (pointed to by ptr).
  • ptr must be NULL or a pointer returned by a previous malloc, calloc, or realloc call.
  • Tries to resize the existing block. If not possible, allocates a new block, copies the contents from the old block (up to the minimum of the old and new sizes), frees the old block, and returns a pointer to the new block.
  • Returns NULL if reallocation fails (the original block remains allocated). Always use a temporary pointer to store the result of realloc.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int *arr = (int *)malloc(5 * sizeof(int));
if (!arr) { /* error */ }

// ... use arr ...

// Need more space, try to resize to hold 10 ints
int *temp = (int *)realloc(arr, 10 * sizeof(int));
if (temp == NULL) {
    // Reallocation failed! arr still points to the original 5 ints.
    fprintf(stderr, "Reallocation failed\n");
    // Decide how to handle this - maybe free(arr) and exit?
    free(arr); // Don't leak the original memory
    exit(1);
} else {
    // Reallocation successful, update the main pointer
    arr = temp;
    // Now arr can hold 10 ints (first 5 have old values, next 5 are uninitialized)
    arr[9] = 99;
}

d) free (Deallocation):

void free(void *ptr);

  • Releases a block of memory previously allocated with malloc, calloc, or realloc, returning it to the heap.
  • ptr must be NULL or a pointer returned by one of the allocation functions that hasn’t already been freed.
  • Crucial: For every malloc/calloc/realloc, there should eventually be a corresponding free when the memory is no longer needed. Failure to free memory leads to memory leaks.
1
2
3
4
5
6
7
8
free(dynamicArray); // Release the memory allocated earlier
dynamicArray = NULL; // Good practice: set pointer to NULL after freeing

free(floatArray);
floatArray = NULL;

free(arr); // Release the reallocated block
arr = NULL;

Common Dynamic Memory Errors:

  • Memory Leak: Forgetting to free allocated memory. The program consumes more and more memory over time.
  • Dangling Pointer: Using a pointer after the memory it pointed to has been freed. Leads to crashes or unpredictable behavior. Setting pointers to NULL after free helps prevent this.
  • Double Free: Calling free more than once on the same memory block. Leads to corruption.
  • Invalid Free: Calling free on a pointer that was not obtained from malloc/calloc/realloc (e.g., address of a local variable).

Dynamic memory allocation gives you great power but requires careful management to avoid errors.

Happy Coding!!

This post is licensed under CC BY 4.0 by the author.