C Fundamentals - Part 2

C Fundamentals - Part 2

Introduction to the most powerful low-level programming language C.
This article sums up working with files, arrays, strings, memory management, functions, pointers and structures.
Our motto is - less talk, more code (or less theory, more examples)
cover
<-- Read first part
 
Covered subjects:
Working with files | Pointers | Arrays | Dynamic allocation | Strings | Functions | Structures

Working with files

Using files for testing

Before we get into reading and writing into a file in C, let me mention something about working with files for C programming first. To know how to work with files in C can be very handy not only for certain tasks that requires it, like generating files, writing contents into a file or create temporary files... it can also be very useful for testing the actual program. For example, our calculator from my previous article, when we run the program to see how it works, we test all possible input cases we can think of - addition, subtraction, multiplication and division, giving it some random numbers to check if it works correctly. Also testing how it handles certain cases like dividing by zero and invalid inputs... Now imagine doing this kind of "testing" every time we do minor changes to the code and run the program. That would be time consuming as hell. One of the programmer's job is to think of the most effective solution to any problem. And so, we can just write these test cases into some test files and pass those files to our program as the input:

// input-test.txt (testing data)
5+20
1243 - 200
3*100
150/6
13/0
10/3
10 what

// output-test.txt (expected output of the program)
The sum of 5 and 20 is 25
The difference of 1243 and 200 is 1043
The product of 3 and 100 is 300
The quotient of 150 and 6 is 25.000000
Division by zero!
The quotient of 10 and 3 is 3.333333
Wrong operand!

(In Bash (UNIX shell)), to pass the input-test.txt file to our program as input:

./a.out < input-test.txt

And to test, whether the output of our program is same of different from the expected one in output-test.txt, we can use diff program:

diff output.txt <(./a.out < test.txt)

If we want to write all the program's output into a file, we just do:

./a.out > output.txt

 Okay, I digressed a bit. Back to the main topic. 

Reading and writing into files

The basic way to work with files in C is to use the data type FILE * (the * stands for pointer, for more information about pointers scroll down).
Opening a file:

FILE * fw = fopen("file1.txt", "w"); // opens file1.txt for writing
FILE * fr = fopen("file2.txt", "r"); // opens file2.txt for reading

First argument of the function fopen() defines the file to be opened. We can open the file for either reading or writing, which is defined in the second argument ("w" for writing, "r" for reading).

Basic functions to work with the file:

char c = getc(fr);    // reading a character from the file, similar to getchar()
putc(c, fw);          // writing a character to the file, similar to putchar()
fscanf(fr, "%d", &i); // formated reading from the file, similar to scanf()
fprintf(fw, "%d", i); // formated writing to the file, similar to printf()

It's important to always close the file after we finish our work with it.

fclose(fr);

Simple example of a program, that reads the first character from file DATA.txt and writes that character into RESULT.txt as equivalent ASCII number.

#include <stdio.h>

int main() {
    FILE *fr, *fw;
    char c;

    fr = fopen("DATA.txt", "r");
    fw = fopen("RESULT.txt", "w");

    c = getc(fr);
    fprintf(fw, "%d", c);

    fclose(fr);
    fclose(fw);

    return 0;
}

It's good to mention the end-of-file indicator (EOF or feof()).

// reads the whole content of file in fr
// and writes this content into a file in fw
// feof() stands for end-of-file indicator
// could be also replaced with EOF constant:
// while ((c = getc(fr)) != EOF) {...}
while (c = getc(fr), !feof(fr)) { 
    putc(c, fw);
}

Also keep in mind that when you open or close a file you were working with, it's not always guaranteed that this action will succeed. The file you are trying to open might not exist or you don't have the permission to etc... there are many various reasons why it wouldn't have succeeded. So it's a good habit to always check if the file was successfully opened and closed and proceed depending on it's results.

// if fopen() fails, it returns NULL
if ((fr = fopen("DATA.txt", "r")) == NULL) {
    printf("File DATA.txt failed to open.\n");
    return 1;
}

...

// if fclose() fails, it returns EOF
if (fclose(fr) == EOF) {
    printf("File couldn't be closed.");
    return 1;
}

 Pointers

Pointers are the core, the heart and soul of C. Unless you understand pointers and know how to work with them, you can never claim yourself as being a C programmer or not even being someone with the actual knowledge of C (anyone can write hello world).

So, what is a pointer? To put it simply, the difference between a normal variable and pointer is that a variable stores some kind of a value whereas a pointer stores the memory address of a certain variable (every variable has allocated space in memory, this space has an address). To be more concrete, a pointer points to the first address of the allocated memory of a certain variable. It's easier to understand from examples.

#include <stdio.h>

int main() {

    int *p_i; // declaration of pointer p_i that will point to an integer
    int i = 10; // declaration of integer i

    p_i = &i; // assigning the address of i (not the value) to pointer p_i (for example address 0x1)

    i = 15; // change the value of i

    printf("%d\n", *p_i); // prints 15 (the value stored in address 0x1 is 15)
    printf("%p\n", p_i); // prints the address of i (0x1 for example)
    printf("%p\n", &i); // equivalent to the printf() above, prints the address of i

    *p_i = 30; // changing the value in 0x1 to 30

    printf("%d\n", *p_i); // prints 30 (the value stored in address 0x1)
    printf("%d\n", i); // prints 30 (i is stored in 0x1 of which value we changed)

    return 0;
}

Note: When printing an address using the %p specifier, we should correctly cast the argument to (void*)

printf("%p\n", (void*)&i);

The reason why pointers are the heart of C is because we use them everywhere. From working with arrays, functions, strings to making the program faster and more efficient with the right use of pointers.

Arrays

When working with arrays, we have to keep in mind that array indexes are counted from 0 (there are many cringy jokes on the internet about this).

int x[10];    // defines an array x of 10 integers
x[0] = 5;     // first element is at index 0
x[9] = 150;   // last element is at index 9

When we create an array, we actually create a pointer that points to the address of the first element of the allocated array. In other words, pointers are actually arrays with undefined sizes (the size can be allocated dynamically, see Dynamic Arrays bellow).

Note: Not really, but it's fine to think of it that way when working with arrays.

#include <stdio.h>

int main() {
    
    int x[2];
    int *p_x;

    p_x = x; // p_x and x points exactly to the same address

    p_x[0] = 150;
    x[1] = 13;

    /* These callings are equivalent (prints 150) */
    printf("%d\n", x[0]);
    printf("%d\n", *x);
    printf("%d\n", p_x[0]);
    printf("%d\n", *p_x);

    /* These callings are equivalent (prints 13) */
    printf("%d\n", x[1]);
    printf("%d\n", *(x+1));
    printf("%d\n", p_x[1]);
    printf("%d\n", *(p_x+1));

    return 0;
}

Static Arrays

Static arrays, or statically allocated arrays are the classic arrays where we define their sizes in the code and work with them as they are.

#include <stdio.h>
#define MAX 150

int main() {
    
    int x[10];      // defines array x of 10 elements
    int y[MAX];     // defines array y of 150 elements
    int z[5] = { 5, 3, 8, 4, 6 }; // explicit declaration

    // filling all elements of array x with value 0
    for (int i = 0; i < 10; i++) {
        x[i] = 0;
    }

    // filling all elements of array y with user input
    for (int i = 0; i < MAX; i++) {
        scanf("%d", &y[i]);
    }

    // printing all elements of array y
    for (int i = 0; i < MAX; i++) {
        printf("%d ", y[i]);
    }

    // prints 5 3 8 4 6
    for (int i = 0; i < 5; i++) {
        printf("%d ", z[i]);
    }

    return 0;
}

Dynamic Arrays

Or dynamically allocated arrays are arrays, that we allocate during the program run and can change their allocated sizes during the process as well. These arrays, unlike statically allocated ones,  allocates only what's needed. If we create an array and we don't know how many elements we should allocate beforehand, we should allocate them dynamically as if we allocate too little, it won't work correctly (overflow) and if we allocate too much, the program will eat too much memory for nothing.

To define a dynamically allocated array, we create an empty pointer first:

int *p_arr;

After that, we can allocate certain block of memory with malloc() or calloc() function from stdlib.h:

#include <stdlib.h> // contains malloc/calloc/realloc functions
...
p_arr = (int *) malloc(10 * sizeof(int));
// or equivalently
p_arr = (int *) calloc(10, sizeof(int));

We have just allocated enough memory for 10 integers (10 times size of integer), in other words, we have just created an integer array p_arr with the size of 10 elements. Accessing these elements can be done the same way as with static arrays:

p_arr[0] = 10;
p_arr[9] = 150;

And after we finish working with our dynamic array or we don't need it anymore, it's good to always free it's allocated memory:

free(p_arr); // basically deletes the array

Let's try it on a simple example - a program, that reads all input as integers and returns their sum. Since we don't know how many numbers will be inputted, we will let the user tell us first:

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

int main() {
    int *p_arr;
    int count = 0;
    int sum = 0;

    printf("How many numbers to be sum up: ");
    if (scanf("%d", &count) != 1) {
        printf("Invalid input.\n");
        return 1;
    }

    p_arr = (int*) malloc(count * sizeof(int));

    printf("Enter %d numbers:\n", count);
    for (int i = 0; i < count; i++) {
        if (scanf("%d", &p_arr[i]) != 1) {
            printf("Invalid input.\n");
            return 1;
        }
    }

    for (int i = 0; i < count; i++) {
        sum += p_arr[i];
    }

    printf("Sum of all entered numbers is: %d\n", sum);
    free(p_arr);
    return 0;
}

See the above output:

How many numbers to be sum up: 5
Enter 5 numbers:
1
2
3
4
5
Sum of all entered numbers is: 15

Of course, if we wanted to just sum up all the numbers, we could have came up with a better method and didn't need to use any array at all and it would have been more efficient (no need to allocate any memory). But this example is about arrays so of course I will demonstrate it on them.

Let's take a look on an example where not even the user himself knows how many numbers he will give us to sum up. In this case, we have no other choice but to allocate the memory on the go. Let's allocate 10 numbers first and every time the user input exceeds it, we allocate additional 10 numbers again:

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

int main() {
    int *p_arr;             // our dynamic array
    int x;                  // user inputted number
    int maxSize = 0;        // maximum allocated size
    int currentSize = 0;    // current amount of filled elements in our array
    int sum = 0;            // resulted sum

    printf("Enter numbers:\n");
    while (scanf("%d", &x) == 1) { // while we read the input
        if (currentSize >= maxSize) { // if the current amount of elements is already at the maximum
            int *tmp; // we create a temporary array

            maxSize += 10; // extends the maximum allocated size
            tmp = (int *) malloc(maxSize * sizeof(int)); // allocate tmp with this size

            for (int i = 0; i < currentSize; i++) { // fill our tmp with our current p_arr data
                tmp[i] = p_arr[i];
            }

            free(p_arr); // free the memory of p_arr array
            p_arr = tmp; // and assign our tmp to it (p_arr becomes tmp)
        }

        p_arr[currentSize++] = x; // if everything is fine, add an input into our array
    }

    for (int i = 0; i < maxSize; i++) {
        sum += p_arr[i];
    }

    printf("Sum of all entered numbers is: %d\n", sum);
    free(p_arr); // don't forget to free even if it's at the end of the program
    return 0;
}

Looks complicated? Don't worry. This example was just to help you understand how resizing (reallocating) a dynamically allocated array during the program's run works. C's stdlib.h has a function called realloc(), that actually does the exact same thing as we did above.

Let's rewrite the program above to our new version using realloc():

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

int main() {
    int *p_arr;             
    int x;                  
    int maxSize = 0;        
    int currentSize = 0;    
    int sum = 0;            

    printf("Enter numbers:\n");
    while (scanf("%d", &x) == 1) { 
        if (currentSize >= maxSize) { 
            maxSize += 10;
            p_arr = (int*) realloc(p_arr, maxSize * sizeof(int));
        }
        p_arr[currentSize++] = x;
    }

    for (int i = 0; i < maxSize; i++) {
        sum += p_arr[i];
    }

    printf("Sum of all entered numbers is: %d\n", sum);
    free(p_arr);
    return 0;
}

Much better right?

And finally, the same as with the file opening and closing, it's a good habit to check if memory allocation was successful as well. In case the memory is full or for some other reasons the allocation fails and you proceed to work with it as if it didn't, your program might eventually crash.

int *p_arr = (int*) malloc(10 * sizeof(int)); // allocate memory
if (!p_arr) { // if it fails
    free(p_arr); // free it's space
    printf("Allocation failed.\n"); // return error
    return 1;
}

Multidimensional Arrays

An array is basically 1D - one dimensional. It's elements are stored linearly. We can, however, declare and work with 2D (table/grid/matrix) and more dimensional arrays as well.

When declaring a 2D array, we define an array of which each of it's element is actually another array.

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

int main(void) 
{
    int x[10][10]; // 2D array of 10*10 elements
    int y[10][10][5]; // 3D array of 10*10*5 elements

    int z[3][2] = {
        {1, 2},
        {3, 4},
        {5, 6}
    }; // explicit declaration of 2D array of 3*2 elements

    int **dynamic; // this is gonna be dynamically allocated 2D array using pointer to pointer

    dynamic = (int**) malloc(3 * sizeof(int*)); // allocated 3 pointers
    for (int i = 0; i < 3; i++) {
        dynamic[i] = (int*) malloc(2 * sizeof(int)); // allocate 2 integers to each pointer
    } // finally, we have allocated 3*2 elements

    // print values of z
    printf("Values of z:\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 2; j++) {
            printf("%d ", z[i][j]);
            dynamic[i][j] = z[i][j] * 2; // filling 'dynamic' array
        }
        printf("\n");
    }

    // print values of our dynamic array
    printf("Values of dynamic:\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 2; j++) {
            printf("%d ", dynamic[i][j]);
        }
        printf("\n");
    }

    for (int i = 0; i < 3; i++) { // free memory of each element
        free(dynamic[i]);
    }
    free(dynamic); // finally free the memory of the whole array

    return 0;
}

output:

Values of z:
1 2 
3 4 
5 6 
Values of dynamic:
2 4 
6 8 
10 12 

Strings

If you read the first part of this serial, you already know that C doesn't have explicit data type for strings. Instead, strings are represented as an array of characters in C. Or in other words, strings are represented as:

// string "Hello" is equal to
char str[6] = { 'H', 'e', 'l', 'l', 'o', '\0' }; // '\0' indicates the-end-of-string (similar to EOF in files)

 String declaration can be done in an easier way than the one above:

char str1[6] = "Hello"; // compiler knows it's a string and will add '\0' automatically
char str2[] = "Hello, world!"; // compiler will allocate the size automatically during this initialization
const char *str3 = "Hello, my beautiful world!"; // constant string

C also has some handy functions to work with strings stored in the header file string.h, the functions are:

#include <string.h>
...
strlen(str); // returns the length of given string
strcpy(str, "Hello"); // copies the content of the second argument string to the string from first argument (copies "Hello" to str)
strcat(str, " world"); // appends the content of the second argument string to the string from first argument (if str was "Hello", it will be "Hello world" now)
strchr(str, 'e'); // searches for the character (second argument) in a string (first argument) and returns a pointer to it's location if found or NULL if not
strcmp(str1, str2); // compares 2 strings and returns 0 - identical, >0 - str1 is larger, <0 str2 is larger (lexicographically)
strstr(str1, str2); // searches for a substring (second argument) in a string (first argument) and returns a pointer to it's location if found or NULL if not

What happens when we create a string of 5 characters (char str[] = "Hello";) and fill it with a string of 12 characters? (strcpy(str, "Hello, world");)

Well, the same thing as if we wanted to copy an array of 14 elements into an array of 6 elements. It overflows and the program might eventually crash. The good way to avoid this is to use the secure versions of these functions:

strncpy(str, "Hello, world!", 6); // same as strcpy but copies up to 6 characters of the second argument string
// we can eventually use it as
strncpy(str1, str2, strlen(str1));

Functions

Why should we use functions? Because we are programmers and we write legible, well-structured and non-redundant code. For this purpose, we use functions in C. We have been working with functions all this time (printf(), getc(), fopen(), strcpy(), malloc(), strlen(), ...) but it would be good to be able to write our own functions as well.

Function declaration has this format:

datatype function_name(parameters) {
    body of the function
}

Example of a function that returns the larger of 2 given numbers:

int larger(int x, int y) {
    if (x > y) return x;
    else return y;
}

Calling our function in main() program:

#include <stdio.h>

int larger(int x, int y) {
    if (x > y) return x;
    else return y;
}

int main() {
    int x = 150;
    int y = 85;
    int z = larger(x, y);

    printf("%d\n", x); // prints 150
    printf("%d\n", larger(64, 88)); // prints 88

    return 0;
}

When we pass variables as function arguments this way, the function creates it's own local copy of the variable and works with the copy. So even if we were to change the value of a variable with the same name inside the function, it won't affect the original one:

void playWithNumbers(int x) { // a procedure (function without return value)
    x = 989456; // local variable x doesnt affect the variable x in main() function
}

int main() {
    int x = 5;
    playWithNumbers(x);
    printf("%d\n", x); // prints 5
    return 0;
}

It's safe as it doesn't affect out variables, we don't have to worry about variable names being the same in both functions etc. However, it can also be slow as it has to copy the content of the variable into the local one (which is perfectly fine for small data types like numbers). Imagine copying a large string or large data structure (see Structures bellow) every time we call the function. In some cases, it's better to pass not the value of the variable, but it's address instead and work with pointers to these addresses inside the function instead of their copies. The function then can change the value of our outside variable as well. Check it out on an example of a function, that will swap the values of 2 given variables:

#include <stdio.h>

void swap(int *p_x, int *p_y) { // create local pointers
    int tmp;
    tmp = *p_x;
    *p_x = *p_y;
    *p_y = tmp;
}

int main() {
    int x = 5;
    int y = 40;

    swap(&x, &y); // pass addresses of our variables

    printf("x: %d\n", x); // prints x: 40
    printf("y: %d\n", y); // prints y: 5
    
    return 0;
}

Structures

And finally the last topic of this serial. Structure is a special data type composed of various elements of any datatype (unlike arrays where every element has to be of the same datatype). If you are familiar with OOP (object oriented programming), you can view structures as something similar to objects.

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

struct person { // first way to define a structure
    char name[10];
    int age;
    double height;
    double weight;
};

typedef struct { // second way
    char name[10];
    char race[20];
    int age;
} dog;

int main() {
    struct person Peter;

    strncpy(Peter.name, "Peter", 10);
    Peter.age = 18;
    Peter.height = 168.5;
    Peter.weight = 64.2;

    dog Rex; // no need to put struct at the start thanks to typedef

    strncpy(Rex.name, "Rex", 10);
    strncpy(Rex.race, "German Shepherd", 20);
    Rex.age = 5;
    
    printf("===== Person 1 =====\n");
    printf("Name: %s\n", Peter.name);
    printf("Age: %d\n", Peter.age);

    printf("===== Dog 1 =====\n");
    printf("Name: %s\n", Rex.name);
    printf("Race: %s\n", Rex.race);

    return 0;
}

Output:

===== Person 1 =====
Name: Peter
Age: 18
===== Dog 1 =====
Name: Rex
Race: German Shepherd

Because structures can be really large, we usually work with pointers to them (to their addresses) than with their actual contents (for example when passing structures to functions - see Functions above).

struct Person *p_peter = &peter;
(*p_peter).age = 18; // accessing the attribute of the structure through pointer p_peter
p_peter->age = 18; // can be actually written this way as well

 

Thank you for reading up to this point. Again, if you have any questions, comments or found any errors regarding this article, contact us directly via our Facebook.