A Student’s C Book I

Level 1. Introduction to C

1. Hello, World!

What if I told you that you could talk to your computer via text files? Yeah, that’s right. Opening up a text file is like opening your messaging app to talk to your friend. The sorts of things that you can write on a messaging app, such as asking whether your friend will come to the university tomorrow or asking for the solution for homework that the teacher gave during one of yesterday’s classes, are also possible to do with your computer. However, there is a small but surely expected catch: you have to open up a new text file on your computer instead of an app on your phone, and you have to learn the computer language to be able to speak to it. I mean, what else would one expect? If you were to talk to people from different countries, you wouldn’t have been able to use your mother tongue all the time. As we sometimes need to learn foreign human spoken languages to be able to communicate with other people, now you need to learn the computer’s language to communicate with your computer. The language is called C.

Computers talk with commands.

Humans tend to usually be polite when they talk to each other. They avoid shouting, screaming, and giving other people commands. Here’s a typical conversation that might be held between two adults:

* Hello, John. How have you been?
* Would you like to go to the library today?
^ Hi, Nina. I am fine, thanks for asking. What about you?
* I am trying to be fine, too. Have just been trying to understand what this C stuff is… 😬
^ Yeah, sure. I also crave to understand how this thing works. 😅
^ I’ll see you in the library then.

This kind of conversation between humans is too complex and chaotic for computers to comprehend. Computers aren’t as smart as us (yet). Since they have limited “intelligence” compared to us, they need to be given concrete and rigorous commands. This is the same as interacting with kids; you don’t explain every little detail why you “commanded” an 8-year-old kid to not come home later than 8 pm. You can try to convey your reasoning, but it just might not get through. So, if John and Nina were just a couple of computers, then the conversation could have gone as follows instead:

* Hello, John! Answer “fine!” if you have been fine lately!
^ Hello, Nina! fine! Answer “fine!” if you have been fine lately!
* Answer “yes!” if you would like to go to the library to study C today!
^ yes!

Asking “How have you been?” nicely in human language is the same as commanding to shout “fine” if John feels alright. In the third line, Nina didn’t mention “fine.”, so John automatically understood that Nina wasn’t fine. Also, pay attention to how they did not talk over each other; instead, they spoke one line at a time. From the third person’s view, they may seem like a bunch of angry, stupid computers shouting at each other for no reason, but for them, this is the normal way of talking. So, we have to get used to it for now. At least, it is much more concise and faster than ours.

Computers have their own command vocabulary.

Again, there is nothing unexpected here, as we have words in the Azerbaijani vocabulary, and computers have their own words in their C vocabulary. It shouldn’t be surprising that their vocabulary contains only a few words. Here are a few examples that are translated from our language to pseudo (fake) computer language:

In English, we say: Hey Nina, could you calculate the area of a rectangle with sides 3 cm and 4 cm?
In C, we say: Multiply 3 by 4!
(because computers are dumb, remember? you can’t just ask it to do math; you have to teach it.)

In English, we say: Hey John, pick a number – an integer, and I’ll try to guess it.
In C, we say: Make up an empty place in your mind for a number and hold a random integer there!
(humans remember the number they pick without needing to remind themselves that they have to store it somewhere in their brain, but computers need to be told every single step of the process.)

The exact words that the computers use look like this in reality:

C keywordsTranslation to English
intWhen you would like to talk about integers (for example, -23 or 0 or 889)
float/doubleWhen you would like to talk about the real number (for example, -23.45 or 0.0 or 3.1415)
charWhen you would like to talk about symbols (for example, the letter ‘a’ or the digit ‘1’ or the ‘!’ sign)
printfWhen you would like to display something on your screen
includeWhen you would like to remind the computer of the things that it can already do (for example, computers don’t understand printf if you don’t remind them by using include first)

Hello, World!

Now, it is time to tell the computer to print (display) a greeting message on the screen. Here’s the code for it:

#include <stdio.h>

int main(){
    printf("Hello, World!");
    return 0;

}

A code or a computer program is a bunch of sentences written in the computer language. In the code shown above, the first line (reading #include <stdio.h>) is to remind the dumb computer that it can display stuff on the screen. Then, when the computer reads the third line (reading printf(“Hello, World!”);), it displays “Hello, World!” on your screen. We will not focus on the other lines for now, but you’ll quickly learn what those mean, and soon you’ll realize that it is all a bunch of simple commands that your computer needs to be told by you, a programmer, to do interesting and fun stuff.

I should probably also mention that the program above should be saved in a file (using text files is the first step for communicating with computers, remember?). Then you should follow these steps:

  1. For simplicity, let’s save the file as code.c (note that the file extension must be .c and not .txt)
  2. Now, you need compile the program by typing the following command on the terminal and pressing ENTER or RETURN: gcc -o code code.c (this tells your computer to read the text inside code.c and inform you if there is anything on that file that it does not understand; if everything is understandable by the computer, it will give you another file named code)
  3. Finally, you can execute (run) the program by typing the following and pressing ENTER or RETURN on your terminal: ./code (in contrast to step 2, this line tells your computer to execute or perform the things mentioned in the code.c file)

These steps must be done in the right order. The first step basically puts your commands in the code.c file into the computer’s memory by saving the file. The second step is to make sure that your commands are understandable by the computer because if not, the computer will complain about it so that you can go back and fix the file. Finally, the third step is executed after the second step is successful (that is, a new file named code is generated by the computer, which contains the translated version of your commands). 

2. Basics

Your computer can memorize things

One of the things that you can make your computer remember is integer numbers. As one would do in math, you can assign numbers to variables (usually to reuse them later on when needed). Roughly speaking, this is how computers remember things. You use a variable to make a computer remember a number. You may ask why there needs to be a variable to remember a number. Well, let’s imagine that you were to be given multiple facts to memorize. For simplicity, I’ll only assume two of them: Alice is 3 years old, and Bob is 5 years old. Now, you are asked about Bob’s age. If you didn’t associate 3 and 5 with the names Alice and Bob, respectively, then you wouldn’t be able to answer this question with certainty. So, your mind probably did something similar to what we do in math — use variables to represent those numbers, that is, Alice = 3 and Bob = 5. That’s why you were able to answer the question correctly and with certainty: because you knew that Bob’s age had to be a number assigned to the variable named Bob and not Alice. In fact, we always either implicitly (in our mind) or explicitly (in math) use abstract variable names to represent numbers. For this reason, computers are the same.

One little detail about computers memorizing different things is that they also need to be explicitly told the type of thing they are about to memorize. For example, to make your computer memorize an integer 3 as the age of Alice, you need to tell three pieces of information to the computer:

  1. the thing to be memorized is an integer;
  2. the variable name it is associated with is Alice;
  3. the integer is 3.

All three sentences above are packed into a single statement in the computer C language, as shown below.

int Alice = 3;

The general pattern to make your computer memorize something is the following:

[Type_Of_The_Thing_To_Memorize] [Variable_Name] = [The_Thing_To_Memorize];

It is also worth mentioning that to recall the thing memorized (e.g., the integer 3), we can and will use the associated variable (e.g., Alice) — that is, asking the question “What number was the variable Alice equal to?” is enough to get the integer 3 as the answer from the computer. Now, it should be obvious why there needs to be variables and not just stand-alone numbers. If we did not have any variable or any association for the integer 3, then the only way to make the computer recall that number and say it back to you would be by asking the following question: What number was 3 equal to? That would be pretty dumb and meaningless, right? In real life, this would look like the scenario where you don’t remember someone’s phone number, but you also don’t know who you want to call; it would be impossible for you to ask for any help from other people to find the phone number since no one would know whose phone number you are looking for. The only solution would be to ask for a phone number that is exactly the same as the phone number you are looking for. This is obviously paradoxical because if you knew the phone number in the first place, why would you try to ask it or “re-remember” it? Obviously, this is not how the recalling works. You can only recall things when there are certain associations that relate those things to some other things that you already know and can remember easily. Okay, okay… Enough with the philosophy. Let’s see what other types of things computers can memorize and recall.

Data TypesDescription
intinteger type
float
double
real number type with less precision
real number type with more precision
charcharacter or symbol type (e.g., ‘a’ or ‘3’ or ‘?’)
unsigned int0 and positive integer (i.e., natural number) type
long intbig integer type
long doublebig real number type with more precision
short intsmall integer type
long long intvery big integer type
unsigned short intsmall natural number type
unsigned long intbig natural number type
unsigned long long intvery big integer type
signed intsame as int
signed short intsame as short int
signed long intsame as long int
Equality in programming is not the same as equality in mathematics

You can reassign different numbers to the same variable in programming. Keep in mind that the variable’s content (its value) will change only after a new assignment statement (i.e., a statement assigning a new number to the same variable). This can be illustrated as follows:

int Alice = 3; // initial value of Alice is 3
Alice = 4; // Alice has the value 4 now
int Bob = 10; // Alice still equals 4
Alice = 7; // Alice's value is updated to 7, and Bob still holds 10
Bob = 11; // Alice has the same value 7 
Bob = 20; // Alice has the same value 7
Alice = 30;

Your computer remembers only the last updates on every variable. In between two updates or assignments, the variable’s value stays the same as it was indicated on the right-hand side (RHS) of the previous assignment statement. That is why we did not see any change in Alice‘s value from line 4 up until line 7.

Now that you know about variable reassignments in C, it is time to blow up your mind for the first time. The following code is perfectly correct in C:

int age = 3;
age = age + 2;

Wait..? Whaaat?! Yes, you read that right. The code snipped given above is perfectly correct because the equality operator in C programming is not the same as the equality sign in mathematics. In math, equality between a variable x and an expression E in general stands for “x and E are evaluated to the same final number or result.” In programming, it actually means something different: x now holds the value evaluated from E. So, saying “let x=4” and then “x=4+1” would be incorrect in math because you always define your variables ones, and their values are not supposed to change during the mathematical operations. However, in programming, variables can be modified to hold different values than what they initially held, and therefore, the same x = 4 + 1; statement in programming would mean that the variable x now holds its old value incremented by one. If we want to increment the old value by one but do not know what the old value is, we can instead write x = x+1;.

In mathematics, we don’t typically have this notion of redefining things because we use ordered sets to keep track of each definition for each variable. For example, if we needed to simulate the variable reassignment shown previously (i.e., x = x + 1;) in mathematics, we would do something like this instead: \text{let}\ x_0 = 4 and x_1 = x_0 + 1. Here, x_0 represents the initial value of x, and x_1 the new incremented value. Then we could also use the initial or new value by using these indexed variables. However, we would not be able to use the initial value of x after reassigning it to a new number in programming; that is, that information would be lost. In mathematics, unlike the C programming, there is no information loss in this regard. That’s why we use ordered sets (indexed variables) to “modify” variables.

Assignment in C programming works the way it does because of the memory efficiency. Since programming can be considered applied math, we don’t want to store every step of the evolution of a variable unnecessarily. Instead, the default behavior is to put the new number back in the same old place of the variable, which makes the same variable hold a different number afterward. However, if we needed to do it in a mathy style where each modification is stored along with the initial assignment, we could also do it by using the concept of arrays that I will now talk about in this post. You’ll learn about them a few posts later. Please just be patient.

To wrap up this subsection, just remember the following. It is assumed that variables in math are all conceptual and, hence, not tied to any physical location in the real world. In contrast, variables in C are tied to actual little memory cells in your computer. That is why saying “let x=3” means defining x to be the same as the number 3 in math, and saying x=3; means putting the number 3 in the variable x‘s memory cell in programming. Due to this subtle difference, every new reassigned value to the same variable makes its original content be deleted from that particular memory location permanently, whereas reassignments to the exact same variable aren’t even possible nor needed in mathematics because we can store infinitely many of them (a mathematical variable is not tied to any place physical). If you are still asking the “But why?” question, then go search about how classical mathematics is non-constructive and declarative and how C programming is constructive and imperative.

Your computer can “talk” and “listen”

In the previous post, we saw that two computer bots were talking to each other in a precise and somewhat rude way (that’s fine). But how can we make our computer talk to us instead of other weirdos? An even better question is, how can WE talk to our computers… instead of other weirdos? You know, these are important questions if we are going to live our lives with our computers for many, many years. They say being able to communicate properly is the key to a happy marriage.

You have already seen one function that enables the computer to talk by displaying a message on the screen. Yes, that’s the good ol’ printf() function. Now, it is time to learn how a computer can listen to what we have to say to it. The function for that is scanf() function that scans a user’s input to the computer by using a keyboard. So, printf() prints and scanf() scans. Makes sense. However, when the computer waits for user input, it also needs to be told where to keep or store the entered input in the memory. Pretty stupid, right? Computers have memory, but they also need to be told to use it for the good. At this point, we have to remind ourselves that our computers are pretty “stupid” for these obvious reasons.

The printf(…) function

Your computer does not have a mouth like we do. So, the simplest way of making your computer “talk” to you is by getting it to display something on its screen so that we can read what it says. The command that makes the computer display something on the screen is the printf() function. It is a printing function, so the name makes sense. It has multiple parameters (this is to say, it takes multiple arguments upon the function calling). However, you can just give it a single argument, and it will still work fine. In that argument, you should specify what you want the computer to print/display on the screen. For example, if you wanted your computer to print Hello, World! on the screen, then you would do something like this inside the main() function:

printf("Hello, World!");

Note that the thing that you want to be printed on the screen must be in between a pair of double quotes (). If you did not put it in between ” ” (as in printf(Hello, World!);), it would not work and give you an error. The reason is that whenever you type anything that is not in between the double quotes, your computer does not treat it as plain, innocent text. Instead, it tries to interpret it either as a data type, a variable name, a number, a function, or a correct mixture of these or something else that directly commands the computer to do something. In our case, neither Hello, and World! inside the printf() function is a valid statement and actually causes the computer to think that they are two separate things due to the white space in between. On the other hand, printf("Hello, World!"); is perfectly understandable by the computer because it treats Hello, World! as a whole and as something that is neither a data type, a number, a function, a mixture of these, nor another command; it rather treats it as a plain text given as an argument to the printf() function. Then, the printf() function displays this text on your computer’s screen. The printf() function is not a known command to the C program by default. To make things work as expected, you should also tell where your computer can find this function by using the #include keyword as shown below:

#include <stdio.h>

int main(){
    printf("Hello, World!");
    return 0;
}

What about the other arguments that we did not provide to the printf() function? What are they useful for? Well, I’ll demonstrate with an example. Let’s assume that there is a variable x in your program, and you would like to print the value it holds in the computer’s memory. How would you do it? Here is what would not work:

#include <stdio.h>

int main(){
    int age = 34;
    printf("age");
    return 0;
}

The code above would just produce the following output on your screen:

age

Remember how putting quotes around symbols made your computer interpret it as plain text and not a variable name? That’s the reason “age” is printed and not its value “34”. If you think that we should get rid of those quotes, then (to let the computer know that it is the variable name and not an innocent piece of text), you are halfway there. The only thing, other than removing those double quotes, that needs to be done here is to tell the computer what type of variable we are trying to print the value of. This is done by passing a proper so-called format string as the first argument of the printf() function. We cannot just put int keyword as the first argument because it is a direct data type and not the format stringthat corresponds to the integer data type. This is how the printf() function works… It expects a format string and not the data type directly. As our variable could be intfloatdouble, or char, there is a different format string that corresponds to each one. The list of the format strings for different data types is given below in Table 2.

Data TypesFormat String
int“%d”
float
double
“%f”
“%lf”
char“%c”
unsigned int“%u”
long int“%ld”
long double“%Lf”
short int“%d”
long long int“%lld”
unsigned short int“%u”
unsigned long int“%zu”
unsigned long long int“%lld”
signed int“%d”
signed short int“%d”
signed long int“%ld”

Since the first argument of the printf() function always needs to be a format string, which is also a plain text (since it uses a pair of quotes), the variable (whose value we want to be printed on the screen) has to be put as the second argument. This is done as shown below:

#include <stdio.h>

int main(){
    int age = 43;
    printf("%d", age);
    return 0;
}

This code will produce the following output:

43

Now, we can make it more human-readable by also inserting some human-readable text inside the format string:

#include <stdio.h>

int main(){
    int age = 43;
    printf("My variable (age) has the value %d. It works!", age);
    return 0;
}

This code will produce the following output:

My variable (age) has the value 43. It works!

Now, you would probably be surprised if I told you that you can actually keep adding as many arguments as you want into the printf() function as long as their types are specified in the format string. Let’s look at a simple example to demonstrate this real quickly:

#include <stdio.h>

int main(){
    int age = 43;
    printf("My variable (age) has the value %d. My height is %f meters.", age, 1.8);
    return 0;
}

The code above will produce the following output:

My variable (age) has the value 43. My height is 1.8 meters.

Now, let’s have more printf‘s and see what happens:

#include <stdio.h>

int main(){
    int age = 43;
    printf("My variable (age) has the value %d. My height is %f meters.", age, 1.8);
    char c = 'A';
    printf("My first name starts with letter %c. My surname starts with %c.", c, 'B');
    return 0;
}

The code above will produce the following output:

My variable (age) has the value 43. My height is 1.8 meters. My first name starts with letter A. My surname starts with B.

It prints them on the same line… Why? Because computers are dumb. That’s why. If you don’t specify when to go to the new line on your screen it will keep staying on the same line forever. To specify a new line in the format string, you have to use \n character. Let’s look at an example:

#include <stdio.h>

int main(){
int age = 43;
printf("My variable (age) has the value %d. My height is %f meters.\n", age, 1.8);
    char c = 'A';
    printf("My first name starts with letter %c.\nMy surname starts with %c.", c, 'B');
    return 0;
}

This code will produce the following output:

My variable (age) has the value 43. My height is 1.8 meters.
My first name starts with letter A.
My surname starts with B.

Notice how \n and the character M (in the text “My surname…”) has no white space in between them. If we put a white space in between, then the text “My surname starts with…” would not only be printed in a new line, but also that new line would start with a white space and then the text following it. The output would look like this:

My variable (age) has the value 43. My height is 1.8 meters.My first name starts with letter A. My surname starts with B.

All this being said about the printf() function, you should now be able to predict the outcome of the following piece of code easily:

#include <stdio.h>

int main(){
    int age = 43;
    printf("Hello, World!");
    printf(" My %s %c%c %d.\n", "age", 'i', 's', age);
    printf("\nBye%cbye...\n", ' ');
    return 0;
}

If you compiled and ran the C program above, the output on your screen would look like this:

Hello, World! My age is 43.



Bye bye...

The computer would first print the “Hello, World!” string (text) without putting a new line at the end (so we are still on the same line), then the second printf(" My...", ...); would cause putting a single white space after the first string (“Hello, World!”) and then print “My age is 43.\n” on the screen. Notice that the string “age” was substituted for the format string “%s” in the first argument of the printf() function, the single characters ‘i’ and ‘s’ were substituted for two consecutive “%c” and “%c” (thus resulting in having a substring “is”), and the value of the age variable was substituted for “%d” (that is why the second argument was put in between the double quotes and the last one was not). Then, the new line character ‘\n’ was put at the end of the current line, meaning that the cursor has moved one line down. Reading the last printf("\nBye%cbye...\n", ' '); statement, another new line is immediately put (so now we have moved two lines down from the initial string), and the single white space character ‘ ‘ was substituted for the format string “%c” right in between “Bye” and “bye” (after the formatting/substitution, the string becomes “Bye bye”). Another new line is added at the end, but since the line was filled with any visible characters/symbols, we are not going to see anything in the last line (as is the case for the second line between the “Hello, World!…” and “Bye bye…” lines).

The scanf(…) function

As the printf() function you just learned about, the scanf() function needs to know what type of user input the computer is expecting (i.e., the so-called format string). It could be an integer, a real number, a single character (like ‘a’, a single white space ‘ ‘, or a new line character ‘\n’), or something else. However, we cannot just put the keywords int for an integer, float or double for a real number, char for a single character as an argument to this function. It expects them in a special format called a format string. The format string tells the scanf() function the type of input the user will enter (not the context of the input, just the type). However, we also need to remind the computer to not delete the input right after the user enters it and to keep it in some memory location for a while so that it can be used later on. That is going to be the second argument to this function: the memory address to be used by the computer to save (memorize) the user input. But how do we pass an address as an argument to a function in C? Let’s see.

Memory in computers is indexed from 0 to some relatively big number M so that a specific memory address can be used by programmers. If you remember from the previous post, we should use a special keyword like intfloat, etc., to tell the computer that we are going to work with the numbers and we need storage to save the results. For example, whenever a programmer writes int a; in a C program, that commands the computer to allocate storage in the memory for a single integer called a. Telling your computer about the type of variable (without telling what is stored in its memory) is called variable declaration. When the programmer then writes a=1 in the same program after the variable declaration, it shouts at the computer to store/save the integer 1 at that location. Telling your computer which value to hold in its memory for the first time is called variable definition. Then, if the programmer wrote a+2, after the definition of a, the computer would be able to calculate this expression mathematically and reach the number 3 as the result of this calculation. It is also worth mentioning that it would not save this result anywhere in its memory since we didn’t tell it to do so, and therefore, we would lose the result immediately without being able to print it, let’s say.

The scanf() function takes multiple arguments. The first argument specifies the type of thing the user is expected to enter via the keyboard. It could be an integer, a real number (it is actually called a floating-point number in programming), a character, or something else. You probably should remember that there were some special keywords for these types already, as shown in Table 1. However, the first argument of the scanf() function expects to see not one of those data types but instead a so-called format string that can represent any one of them. The format strings for different types are shown in Table 2. The second argument is the variable with which the user input is associated. That is to say, after the user types an input on the terminal and presses the Enter or Return key on the keyboard, the input will be assigned to the variable specified in the second argument of the scanf() function.

Let’s say we would like the computer to ask the user to enter his age as an integer. The following code would work just fine for this example:

#incude <stdio.h>

int main(){
    int age;
    scanf("%d", &age);
    return 0;

}

Notice how we passed “%d” as the format string since it is the one that corresponds to integers, and the ampersand (&) sign in front of the age variable. The “%d” indicates that the inputted value will be stored in a variable of type int, and the & sign tells the computer to store the user input in the memory address of the agevariable as opposed to some random place. But why didn’t we just put age as the second argument instead of &age? Now, I have more explaining to do. The short answer is, again, that computers are dumber than how smart you think they are or should be. A bit longer answer is that computers have internal memory (you might have heard the term “RAM” by now), and they use their little memory cells to store values. This is what memorization is all about in computers. Storing a value in some empty memory cell is analogous to memorizing that underlying value. Each of these memory cells is indexed from 0 to some large number. By using these indices, your computer is able to manipulate the values stored at those memory locations. Figure 1 depicts a computer memory for a better understanding.

Figure 1. Computer memory.

But wait! Is there any way for the C programmers to access those indices? The answer is yes. We have already talked about the importance of having variables to recall those values from the computer’s memory. Frankly speaking, those variables are just names for the memory addresses (the terms “memory address,” “memory location,” and “memory index” refer to the same concept). We use names whenever we do not want to and need to deal with the memory addresses (because, you know, putting the number 31 in the age variable seems more natural to humans than trying to put that value in the memory address 3425238 and then trying to remember this address). That’s why when you type something like int a = 1; your computer picks a “random” memory address (let’s say 442133) and writes the value 1 into that memory cell. When you need to recall the value you assigned to the variable a, your computer goes into the memory address 442133 and fetches its content (the value 1) for you. However, there are certain cases where you might encounter a function in C that works with the raw memory addresses instead of the beautiful, human-readable variable names. There are strong reasons for that, but I am not going to talk about them here as the “Pointers” section is specifically dedicated to this matter. The scanf() function is one of those functions. Its first argument is the format string, and all the other arguments that you may put in this function explicitly need to be memory addresses (of your variables) and not the variable names. That’s where the ampersand sign (&) comes into play. This operator tells your computer that you are interested in passing the raw memory address of your variable. So, while typing age refers to the thing that is stored at the memory address of the variable age, typing &age refers to that memory address where the age is stored. If we typed scanf("%d", age); then your computer would complain because age does not refer to the memory address. It refers to some integer number. Instead, typing scanf("%d", &age); would solve this issue because now your computer knows that whatever the user inputs via the keyboard, it will be stored at the particular memory location &age (whose human-readable name is age). 

But how come we can type int age = 23; but not scanf(“%d”, age);? The magic is in the = operator in the first statement. Since we cannot use it inside the scanf() function, we do not have the privilege of passing the variable name as opposed to its memory address. However, whenever we can and do use the equality sign in between a variable name (such as age) and a value (such as 23), the computer deals with the ampersand conversion automatically to ease our job. Otherwise, we would have to manually put 23 into the memory address of age by hand, which is time-consuming.

Compiling and Running programs

You have probably wondered, even if it was for a very brief moment, why we always need to first compile C programs and then run them? As a matter of fact, what is the difference between compiling and running a C program? Okay, we will have to extend our analogies further to understand these processes. Running a C program means executing the instructions/statements/commands specified in the source code. This process is also called execution of the program or run-time phase. However, your computer is not actually capable of executing the instructions directly from the C code. Now, what I am about to see may surprise you a bit: C is not the native language of a computer, meaning that it is actually an intermediary language that is easy for both programmers and computers to understand by using some sort of language translation tools. In our case, human programmers do not need any such external translation tool because we are smart enough to do the translation from the natural language to the C language by ourselves. As you are learning C by reading this tutorial is a practical proof of this. However, your computer is dumb. No hard feelings; mine is dumb, too. It needs an external tool (a program or software) to translate a text presented in a C language to the one that it understands in its own language. “But what is the native language of computers?” you might ask. It is non-ironically called the machine language. It is a language where you only use binary numbers as the means of communication. I will not go into the specifications of the machine language in this post, but I will say this to help you see the big picture here: gcc is one of those translator programs that converts a C code to a machine-native code/text. This translation process is called compilation. That is why we need to first compile our programs before making the machine actually execute the instructions specified in it. In the particular case of translator programs that translate programs to the machine language, we call them compilers. So, gcc is a C compiler as fasm is an Assembly compilerg++ is a C++ compiler, and so on. Now, let’s see how we can use gcc compiler to compile/translate our C programs.

gcc -o [EXECUTABLE_NAME] [SOURCE_CODE]

The template shown above illustrates a simple use case of the gcc compiler. According to this usage, when we type gcc -o translated_version_of_my_code code.c on the terminal and hit ENTER or RETURN, the gcc will read the source code inside the file named code.c and it will translate it to the machine-readable version (since this version of the code is executable by a computer, it is also called an executable). Here, -o [EXECUTABLE_NAME] is to tell gcc to rename our executable to whatever we specify after the -o flag. In our case, gcc will save the executable as translated_version_of_my_code. To run this executable (i.e., to make the computer to read it and execute the specified instructions), you should then type ./[EXECUTABLE_NAME], which, in our case, is ./translated_version_my_code and then hit ENTER or RETURN on the terminal once again. Now, the computer will start executing the program. Let’s see an example.

Linux (Ubuntu)

Open up your terminal and type the following command, and hit RETURN:

gedit

This will open up a new empty text file. Copy-paste the following code into that text file: 

#incude <stdio.h>

int main(){
    int age = 3;
    printf("Alice's age is %d.", age);
    return 0;
}

Save the file as example.c and go back to your previously opened terminal again. Now, type the following and hit RETURN:

gcc -o exec example.c

This command will create a new machine-readable file (the so-called executable) named exec in the same directory. Type the following and hit RETURN:

./exec

You will see the following output on your screen:

Alice's age is 3.

Congratulations! That was it. Now, you can use these steps to compile and run your programs throughout this tutorial series. Hope you’ll enjoy the ride.

MacOS

Open up your terminal and type the following command, and hit RETURN:

gedit

This will open up a new empty text file. Copy-paste the following code into that text file: 

#incude <stdio.h>

int main(){
    int age = 3;
    printf("Alice's age is %d.", age);
    return 0;
}

Save the file as example.c and go back to your previously opened terminal again. Now, type the following and hit RETURN:

gcc -o exec example.c

This command will create a new machine-readable file (the so-called executable) named exec in the same directory. Type the following and hit RETURN:

./exec

You will see the following output on your screen:

Alice's age is 3.

Congratulations! That was it. Now, you can use these steps to compile and run your programs throughout this tutorial series. Hope you’ll enjoy the ride.

Windows (don’t use it!)

No, really – Don’t use Windows!

3. Functions

I receive Inputs, You receive Output

What is a function? Roughly speaking, in the mathematical sense, it is a mapping from some input space to some output space such that no two different output values correspond to the same input value. In programming, we also call inputs the arguments given to a function. In fact, we even make a slight distinction between the arguments and the parameters of the functions. We call variable inputs, used in the output expression of the function, the arguments, and exact valued inputs, used in the final computation of the function’s output, the arguments. Let’s now recall a simple mathematical function as an example:

    \[f(x) = x + 1\]

A computation function (i.e., a function in programming) is not too much different than a mathematical one. It has all the components that the mathematical one has:

  1. A name (it is named f in our example)
  2. Parameters or input variables (it is just a single parameter named x in our example)
  3. A (function) body (it is the expression on the RHS, which is x+1 in our example)

There are even more similarities. As one is able to get the output of the function by putting a number (i.e., an argument) in between the opening and closing parentheses that come right after the function name, the exact same can be done in programming using the C language.

    \[f(5) \text{ in math gives 6}\]

    \[f(5) \text{ in programming also gives 6}\]

We can even use additional variables to store the function output:

    \[y = f(5)\ \text{in math (now, y = 6)}\]

    \[int y = f(5);\ \text{in programming (now, y = 6)}\]

There is a slight difference between defining a mathematical and a computational function, though. While the writing f(x) = x + 1 on a piece of paper is enough for a mathematical definition of a function, we should type the following in a .c text file for the computation definition of the same function:

int f(int x){ return x + 1; }

First of all, we have to specify two things that were not included in the mathematical definition: (1) the type of each input parameter and (2) the type of output value. Assuming that the domain and codomain is the set of integers, it now becomes

int f(int x) = x + 1

Notice the keyword int in front of the function name. This keyword is put in front of the function name to tell the computer that the output value of the function f is an integer number. The second int keyword inside the parentheses tells the computer the type of parameter x

Second of all, we use the equals sign (=) for variable-value assignments in programming. Instead of using the equality operator, programmers have decided to put everything inside a pair of opening and closing curly braces after specifying all the function parameters. This is shown below:

int f(int x){ x + 1 }

The last couple of changes to be made in order to get to the correct computational definition of our function in C are the following:

  1. We have to type the special word return in front of the expression that is going to be the output of the function. That is, int f(int x){ return x + 1 }
  2. C expects to see a semicolon (;) to make sure that our statement is properly closed (as one is expected to put a dot at the end of each finished sentence in English). That is, int f(int x){ return x + 1; }

So, the final and complete version of the computation definition of our simple function is given as follows:

int f(int x){ return x + 1; }

It is also worth mentioning that the output value of functions in programming is also called their returned value or simply a return value. Now, if we wanted to define a simple addition function \mathtt{add}(x, y) = x + y that works for real numbers in C, it would look like this:

float add(float x, float y){ return x + y; }

Since the data type in front of the function name indicates the type of the function’s output, it had to be float. We can logically infer this because we know the output expression, that is, x + y. Since both x and y are real numbers (they are called floating-point numbers in programming) that have the type float, adding two floating-point numbers will surely result in another floating-point number.

Simple pattern matching

Functions in C can do more than just returning a value. For example, a computational function could have its own set of (local) variables that can only be used by the function itself. Instead of returning x + 1 directly, something as shown below is possible:

int f(int x){ int y = x + 1; return y; }

This function behaves the same as its previous version. The difference is that this version memorizes a value computed from x + 1 and then returns/outputs the memorized value, whereas the first version just outputs the computed final value without memorizing it. In this case, it is meaningless to memorize the output value before returning it, but in the following case, it might be useful:

int f(int x){ int y = x + 1; printf("%d + 1 = %d", x, y); return y; }

In the version shown above, when the computer executes/calls this function, it will first memorize the final output value in the variable named y, then display its value on the screen, and finally return it as the output. Notice how both parameter x and the local variable y are used multiple times within the function. This is totally fine because inside the function’s scope (i.e., function body), x will hold the number provided as an input argument, and y will hold the value of x+1 unless you explicitly modify their values later on.

As a side note, I should also mention that C does not care whether all the statements (i.e., your commands to the computer that can also be thought of as sentences in programming) are typed on a single line or multiple lines according to their original order. It also does not care about the number of multiple white spaces in between the symbols. For example, for C language, all the following statements mean the same:

int my_x;
int     my_x;
        int              my_x           ;

However, you should keep in mind that while there is no difference between putting a single white space or more in between the symbols, there is certainly a difference between putting one or more white spaces and not putting any. For example, int my_x; is not the same as int my x; for this exact reason. In the former case, my_x is the name of your integer variable, and in the latter case, my is the variable name, and the computer will complain about x because it will now understand what this symbol represents as it has been separated by a whitespace from the beginning of your variable name. Also note that the different numbers of more than one white space only matter inside a pair of double quotes (for example, printf(“a b”); is not the same as printf(“a     b”);). To demonstrate how C doesn’t care about the lines, let’s also rewrite our infamous f(x) = x + 1 function:

int f (int x) {
    int y = x + 1;
    return y;
}

Yeah, this looks much better now. It is more readable than putting everything on a single line and then indefinitely scrolling your window horizontally like a true freak. It is better for us that C does not care about such irrelevant white spaces and lines because these things should not affect how our function (or program in general) must behave. The order, however, matters. For example, we cannot write return y; on the line above the int y = x + 1; since the computer starts reading our program from the top left and it moves from left to right on the current line, then goes to the beginning of the next line right after the previous one and does the same there until it finishes reading the whole code. This is also how we read books, papers, and so on. That is why order matters. Imagine reading a page of a math book where the author speaks about how addition and integration work before mentioning anything about the numbers.

Pattern matching in C is simple and yet powerful enough to get the job done. Imagine the following writings on a piece of paper from one of your math classes:

    \[f(x) = x + 1\]

    \[g(x, y) = f(x) + f(-2) \cdot f(y)\]

    \[a = 3\]

    \[g(2, a^2) = ?\]

You could easily calculate the answer for the last line as all the information you need is given above it. Moreover, to calculate the output of g(2, a^2), you would perform a simple pattern matching (even though you might not realize it). The pattern matching would go as follows:

  1. g(2, a^2) uses a^2 as the second argument which is defined right above as a = 3. So, we need to calculate g(2, 3^2) which is also equal to g(2, 9);
  2. g(2, 9) refers to the function definition g(x, y), and therefore x = 2 and y = 9;
  3. Since we know that g(x, y) = f(x) + f(-2) \cdot f(y), we can substitute the x and y with their corresponding values. That is, g(2, 4) = f(2) + f(-2) \cdot f(9);
  4. The definition of f is also given to us in the first line, so, we can calculate f(2) = 3, f(-2) = -1, and f(9) = 10 by simply substituting its parameter x by 2, -2, and 9, respectively;
  5. Now, the output expression of g(2, 4) becomes g(2, 4) = f(2) + f(-2) \cdot f(9) = 3 + (-1) \cdot 10 = -7;
  6. Therefore, g(2, a^2) = -7.

This simple pattern matching by using variable-value substitutions is what happens in C programming as well. Let’s see a quick example in C (we’ll assume that the numbers can be real numbers):

#include <stdio.h>

double f (double x) {
    double y = x + 1;
    return y;
}

double g(double x, double y){
    double constant = f(-2);
    return f(x) + f(y) + constant;
}

int main(){
    double a;
    printf("Please enter a = ");
    scanf("%lf", &a);
    double output = g(2, a*a); // we don't have square operator, so, we use multiplication
    printf("g(2, a*a) = %lf", output);
    return 0;
}

When the C program shown above is about to be executed by your computer, it will start reading the file line by line and from left to right first to ensure that everything is in the correct order and there are no syntactic errors in your writing. Then, if the program seems to be correct, it will be able to execute it starting from the main() function. So, the execution will go like this, starting from the first line inside the main() function:

  1. double a; tells the computer that we have a variable that holds a real number, but the value is not known yet.
  2. printf(“Please enter a = “); tells the computer to display “Please enter a = ” on the screen. This an optional step to let the user know that a value of a is expected from him/her.
  3. scanf(“%lf”, &a); tells the computer to get a real number from the user via the keyboard and keep it inside the variable named a. After the user enters a number, the computer will know what number the (double) a holds, and therefore, its value will become known.
  4. double output = g(2, a*a); tells the computer to compute the function g(2, a^2) by using the pattern matching and all the information about the function definitions given above the main() function.
  5. printf(“g(2, a*a) = %lf”, output); tells the computer to display the text “g(2, a*a) = [some value here depending on the user input]” on the screen for the user to know the answer. For example, if the user entered the number 3 on the terminal, the output would exactly look like this: g(2, a*a) = -7
  6. return 0; tells the computer to return integer 0 as the output value of the main() function.

Hopefully, everything I have covered up until now is clear to you. If you still have some doubts about some parts of the post, make sure to open up a text editor and start playing with the examples given in this post. Rewrite them by hand (without using a copy-paste functionality), play with them, change them, run them, and make sure you are practicing along with reading this tutorial. You will learn C by practicing a lot. I mean A LOT!

Function calling and Recursion

Given the function definition f(x) = x + 1, one is surely able to “use” this function with any arbitrary real number as the argument. For example, one could use f(-3) in any expression, and that would be perfectly fine by the rules of mathematics because f is a well-defined function that maps -3 to -2. We get the mapped output (-2 in our example) by computing the output expression of the underlying function (x+1 in our example) with the given input (-3 in our example). This process is also called function calling or function invocation in programming. We invoke/call a C function by typing the function name, followed by the arguments. Let’s take a look at the following example:

double f(double x){
    return x + 1;
}

int main(){
    double y_1 = f(-3); // function call with argument -3, output is -2.0
    double y_2 = f(45.2); // function call with argument 45.2, output is 46.2
    double pi = 3.1415;
    double y_pi = f(pi); // function call with argument 3.1415, output is 4.1415
    return 0;
}

Now, imagine we have a bit complicated function whose output values are computed in terms of its other output values. We can take the factorial function as an example. If we let the \mathtt{facto}: \mathbb{N} \rightarrow \mathbb{N} denote the factorial function, then we can define it as follows:

    \[\mathtt{facto}(n) = \begin{cases}1,& \quad\text{if}\ n = 0, \\n \cdot \mathtt{facto}(n-1),& \quad\text{if}\ n > 0\end{cases}\]

This function uses its own output values for different inputs whenever n > 0. In programming, we say, the function calls itself, and a function calling itself is called a recursive function

unsigned int facto(unsigned int n){
    if(n == 0){
        return 1;
    }
    return n * facto(n-1);
}

Another recursive function is the Fibonacci function that generates the Fibonacci series 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 45, \cdots. Mathematically speaking, this function is defined as \mathtt{fib}_{n+1} = \mathtt{fib}_{n} + \mathtt{fib}_{n-1} where \mathtt{fib}_0 = 0 and \mathtt{fib}_1 = 1. In C, we can define it as follows:

unsigned int fib(unsigned int n){
    if(n < 2){
        return n;
    }
    return fib(n-1) + fib(n-2);
}

Recursion is a very powerful concept in programming. It sometimes eases the job of programmers multiple folds. However, sometimes, it causes longer run times (due to the slow program execution) and may even result in crashes due to the reasons that I will not talk about in this post. Such things are going to be covered in the future post(s) of this tutorial series.

4. Control Flow

Branching is a strategy to make perfect plans in the imperfect or incomplete world.

The control flow of a program execution is the path of sequential instructions the computer executes while running the program. So far, you have seen a linear and non-branching control flow where the computer executes each instruction in the main() function. However, we can change this behavior and make the computer sometimes skip some instructions or decide between multiple execution paths depending on some conditions. Before moving to the next section, I would like to mention one thing about a new data type you will need to keep in mind for this tutorial. The new data type I would like to introduce is the boolean or bool type. The boolean type is used to represent logical truth values, such as true and false. All conditional expressions (e.g., 3 < 5, 4 > 4, and so on) in C are evaluated to one of these boolean values. I mean, a condition in math must either be true or false at the end of the day. It cannot be neither, and it cannot be both. In C programming, the truth values are represented as numbers. false is always represented as the number 0, and true is represented as any non-0 number. With this being said, you can now start reading the next sections more comfortably.

Branching on a condition

Let’s say that according to the statistics on the population heights, the average height for men is 178 cm. So, anyone above 178 cm is considered taller than the average, and anyone below that is considered to have a relatively shorter height. Now, let’s say you want to help people to know whether they are higher, average, or shorter than the average. Because you know the average height, anytime a random person would ask you if he is taller than the average, you would ask him his height and compare his height to the average, and then you would say things like “you’re taller”, “you’re average”, and “you’re shorter” if his height is above the average, equal to it, and below the average, respectively. What you say to the person in front of you depends on his height, obviously. Now, you would like to write a C program that does exactly what you were doing. You need to tell the computer when to print what sentence. For that, you need to use branches. Branching means executing different commands or instructions depending on whether a given condition is satisfied or unsatisfied. In C language, one way of branching is by using the if block. Here is what the generic syntax for it looks like:

if ( CONDITION is satisfied ) {
    // do something
}

I will give you an example right away:

if(3 > 7) {
    printf("3 is greater than 7");
}

If you put the code snipped above inside the main function in a blank C program and ran it, it would not print “3 is greater than 7” on the screen because the computer would check the condition (i.e., if 3 > 7) and know that it is false. Since the condition was not satisfied, it would not execute the statements inside the if block (i.e., statements within the if‘s scope or body).

if-else block

We can specify the things the computer needs to do when a given condition is satisfied or true. Can we also specify what to do if the underlying condition is not true (i.e., false)? The answer is yes. All we have to do is to use the logical inverse of the condition in another if block. For example, if the initial condition is 3 < 7, then the logical opposite of this is another condition reading 3 \geq 7 (in programming, we denote the mathematical \geq inequality operator by using >= and the \leq operator by using <=). Let’s look at a simple example that uses conditions on a variable:

if(x < 5) {
    printf("x is less than 5");
}
if(x >= 5){
    printf("x is not less than 5");
}

The order of the if blocks does not matter here since the conditions are mutually exclusive (i.e., whenever one of them is true, then the other one must be false, and vice versa). So, only one condition will be true at any given run-time, and hence, only one printf() will be executed. Let’s suppose that x = 3. The computer will first check the first if block’s condition (i.e., if x < 5) and proceed to print “x is less than 5” since the condition 3 < 5 is true. Then, the computer will check the second if block’s condition (i.e., if 3 >= 5) and skip printing the “x is not less than 5” string since 3 is not bigger than or equal to 5. If we switched the places of the two if blocks, nothing would change in the output. Here is the new snippet in the opposite order:

if(x >= 5) {
    printf("x is not less than 5");
}
if(x < 5){
    printf("x is less than 5");
}

The computer would skip the first block’s printf() and execute the second block’s printf() function, and a a result, the same text “x is less than 5” would appear on the screen regardless of the order. Again, this is true because the conditions are mutually exclusive. In the code snippet given below, both printf() functions would be executed if x = 4:

int x;
if(x >= 3) {
    printf("x is not less than 3");
}
if(x < 5){
    printf("x is less than 5");
}

In the code snippet above, “x is not less than 4” would be printed if and only if x \geq 4, and “x is less than 5” would be printed if and only if x < 5. This is obvious. However, there are specific intervals of x for which either only one of the strings is printed or both of them are printed. To find the intervals, we should analyze the values and the mutually non-exclusive conditions. Both conditions (x \geq 4 and x < 5) are satisfied whenever x \in [3, 5), and since x must be an integer (by looking at its type int), the interval practically is x \in [3, 4] (more rigorously, x \in \{3, 4\}). Then we have two other intervals, x \in (-\infty, 3) and x \in [5, +\infty). In the former interval, only the “x is less than 5” string would be printed, and in the latter interval, only the “x is not less than 3” string would be printed. Now, we understand the behavior of this little C code better than we did before.

Whenever you find yourself in a similar situation where you need to tell the computer to do one thing if some condition holds and another thing when the same condition does not hold, you can use if-else blocks instead of two consecutive if blocks, as shown previously. The else block always has to follow the if block, and the statements inside the else block are going to be executed if and only if the condition of the initial if block does not hold. Let’s rewrite the previous code snipped using the if-else block for demonstration:

if(x < 5){
    printf("x is less than 5");
}
else{
    printf("x is not less than 5");
}

Notice how the else block does not have any conditional expression attached to it. This is because the computer automatically goes inside the else body whenever the condition attached to its preceding if block  is false.

else-if block

Now, we would like to detect and print something depending on whether x < 5, or x > 5, or x = 5 (in programming, single equality sign is used only for assignments and not conditionals, so, we need to use double equals == for checking if the left-hand side equals the right-hand side of the expression). We could do this as shown below:

if(x < 5){
    printf("x is less than 5");
}
else{
    if(x > 5){
        printf("x is bigger than 5");
    }
    else{
        printf("x is equal to 5");
    }
}
goto statement

There is actually a statement in the C language that you can use to change the control flow of the program execution arbitrarily in a very flexible and somewhat dangerous way. The statement I am talking about is the goto statement. Here’s how it works:

  1. You put a label before a piece of code that will be executed when some condition is true;
  2. Somewhere appropriate in the code, you put an if block checking whether some condition is met, and you put a goto statement in the if body by using the same label you put before the piece of code that needed to be executed whenever the condition was satisfied.

For example,

#include <stdio.h>
int main(){
    int age;
    scanf("%d", &age);

    if(age < 18){
        goto MY_LABEL; // the goto statement
    }
    else{
        printf("You are an adult.\n");
        goto END; // another goto statement
    }

MY_LABEL: // this is a label
    printf("You are not an adult.\n");
    
END: // this is also a label
    return 0;
}

Let’s take a careful look at the full code C program shown above. There are two goto statements and two labels. The goto statement, when executed by the computer, makes the computer jump directly to the given label’s location in the program and continue the execution from that point. I should also mention that to put these labels in the code, all you have to do is type an arbitrary title (it cannot begin with special characters, such as ?, !, #, %, ^, -, +, (, ), *, &, @, ~, <, >, etc., or numbers but could be in small or capital letters and contain numbers and the _ character) and then put a colon (:) after it. If we analyze the code above, we will see that the first if block has a single goto statement in its body that jumps to the label named MY_LABEL. Jumping to the MY_LABEL location in the program means that the computer will start to execute the last printf() statements (i.e., it will print “You are not an adult.”) after which it will go the next return 0; statement, which is under the label named END. Recall that the labels do not affect the program flow on their own without any goto statements; they are just location specifiers to be used with the goto statements. It is the goto statements that make the execution flow of the program change, not the labels themselves. So, if the age is less than 18, the computer will jump to MY_LABEL to print “You are not an adult.” and then finish by returning 0. If the age is not less than 18 (i.e., the else block), then it will print “You are an adult.” and jump to END to finish by returning 0. If we did not make the jump to where the return 0; statement was (i.e., the label named END), the computer would go to the next instruction after the else block (and that would result in printing “You are not an adult.”), which is not the correct behavior that we expect to happen.

Branching back is called Looping

As you just learned about the goto statement, you may have probably thought how powerful (and potentially dangerous) that statement was. Unlike the if-else blocks, we can either jump forward or backward by using a goto statement. This capability makes goto a very powerful statement. Since we can jump backward in the code depending on some condition, we can write the following without breaking any rules of the C language:

#include <stdio.h>
int main(){
    int i = 0;
    
DO_THIS_10_TIMES:
    printf("%d\n", i);
    i = i + 1;
    if(i < 10){
        goto DO_THIS_10_TIMES;
    }
    printf("End.");

    return 0;
}

The program given above would produce the following output:

0
1
2
3
4
5
6
7
8
9
End.

We cannot produce such a behavior shown above by using only if-else statements. The if-else statements always jump forward in the program while choosing one of two or more possible execution paths. However, we need to jump backward if we want to re-execute the same set of instructions over and over again for some number of times.

Jumping back in the program execution to re-execute the same piece of code multiple times is called looping. Loops are an effective way for programmers to not repeat themselves in an utterly inelegant way while writing programs. If one needs to write a program that prints “Hello, World!\n” 5 times or a program that counts from 43 to 320, loops can be used to avoid typing out the whole thing that has a very obvious repetitive pattern to it. Although goto can be used for this purpose, it is potentially very unhinged and error-prone. The reason it is potentially dangerous is that you can jump anywhere (forward or backward) in the program, and the execution flow will continue sequentially from that point forward unless another goto is encountered. The fact that a human programmer needs to keep remembering and/or rereading the whole different sections of the code over and over again makes the use of goto potentially dangerous. It can cause a lot of “stupid” bugs because humans are not the best when it comes to keeping track of every little detail in different parts of the code simultaneously. Due to this issue, there are more hinged and sane looping mechanisms that are considered to be safe to use. Now, you’ll see what safe looping looks like.

while() loop

The first safe looping mechanism includes the use of while block. This block is very similar to the if block. In fact, it is exactly the same as the if block syntactically. Semantically, the difference between them is that the if block executes the statements present in its body only once whenever the corresponding condition is met, whereas the while block executes the statements present in its body as long as the corresponding condition is met. Let’s take a look at the generic format of the while block.

while ( CONDITION ) {
    // while body: do something
}

To demonstrate how it works in practice, let’s write a simple program that prints the current iteration number and the string “Hello, World!” in a new line 10 times.

#include <stdio.h>

int main(){
    int i = 0;
    while(i < 10) {
        printf("Iteration: %d; ", i+1); 
        printf("Hello, World!\n");
        i = i + 1;
    }
    return 0;
}

The program shown above produces the following output when executed:

Iteration: 1; Hello, World!
Iteration: 2; Hello, World!
Iteration: 3; Hello, World!
Iteration: 4; Hello, World!
Iteration: 5; Hello, World!
Iteration: 6; Hello, World!
Iteration: 7; Hello, World!
Iteration: 8; Hello, World!
Iteration: 9; Hello, World!
Iteration: 10; Hello, World!

Obviously, you could do the same without using any sort of loop by writing printf(“Iteration: 1; Hello, World!\n”); followed by printf(“Iteration: 2; Hello, World!\n”); followed by another printf(“Iteration: 3; Hello, World!\n”);, and so on. This is possible, although very inelegant and error-prone. However, what you could not have done is to get the number of iterations dynamically from the user during the program execution and print “Hello, World!” text that many times on the screen. This is because you do not know which number the user would input while you are writing your program before the execution phase. That’s when using a loop is unavoidable. Consider the following piece of code:

#include <stdio.h>
int main(){
    int n = 0;
    scanf("%d", n);

    int old = 0, curr = 1;
    int iter = 1;
    while(iter < n){
        int tmp = curr;
        curr = curr + old;
        old = tmp;
        iter = iter + 1;
    }
    
    if(n == 0){
        curr = 0;
    }
    printf("fib(%d) = %d", n, curr);
    return 0;
}

The program given above prints the Fibonacci number at the index specified by the user. For example, if the user input was the number 4, then the output would be 3. Let’s try to unfold the while loop and understand how the computer would come up with the correct Fibonacci number.

n = 4;
old = 0; curr = 1;
iter = 1;
check if (1 < 4) (true) {
    int tmp = curr(1);          // tmp = 1;
    curr = curr(1) + old(0);    // curr = 1;
    old = tmp(1);               // old = 1;
    iter = iter(1) + 1;         // iter = 2;
}
check if (2 < 4) (true) {
    int tmp = curr(1);          // tmp = 1;
    curr = curr(1) + old(1);    // curr = 2;
    old = tmp(1);               // old = 1;
    iter = iter(2) + 1;         // iter = 3;
}
check if (3 < 4) (true) {    int tmp = curr(2);          // tmp = 2;
    curr = curr(2) + old(1);    // curr = 3;
    old = tmp(2);               // old = 2;
    iter = iter(3) + 1;         // iter = 4;
}check if (4 < 4) (false) {    // END OF THE WHILE LOOP}
if (n == 0) (false) { ... }print on screen: fib(4) = curr(3);

As you have already learned about this, the computer stores only the last updates made on each variable. That is why the variables oldcurr, and iter only refer to the numbers that they have been reassigned to the most recently. The semantic meaning of the variable old is to refer to the \mathtt{fib}_{n-1}, and the variable curr is to refer to the current \mathtt{fib}_n. Since \mathtt{fib}_{n+1} = \mathtt{fib}_n + \mathtt{fib}_{n-1}, we manipulated the old and curr variables to refer to different successive pairs of Fibonacci numbers in the sequence. This process can be once again illustrated as shown below:

old = 0, curr = 1 (Fibonacci sequence: 0, 1, 1, 2, 3, 5, ...)
old = 1, curr = 1 (Fibonacci sequence: 0, 1, 1, 2, 3, 5, ...)
old = 1, curr = 2 (Fibonacci sequence: 0, 1, 1, 2, 3, 5, ...)
old = 2, curr = 3 (Fibonacci sequence: 0, 1, 1, 2, 3, 5, ...)

Since to computer the next number in the sequence, the current number, and the number that comes right before it in the sequence is enough according to the Fibonacci formula, we update the curr variable to point to the current variable and the old variable to point to the number the comes right before it. As we compute the next number by doing curr = curr + old; inside the whileblock, now the curr variable points to the next number, and since its value is lost now, we keep it in another temporary variable tmp by writing int tmp = curr;. We use this temporary value (i.e., the value that the curr used to hold) to reassign the old variable to the one that used to be stored in the curr variable before the update, and hence, we write old = tmp;. Finally, we increment the iter by 1 in order to indicate the current iteration, which also refers to an index in the Fibonacci sequence.

Now you know how the while block works, it is time to mention one last thing about it. There is a slightly different version of while that is called the do-while block. The difference between them is that the former first checks the condition and then proceeds to execute the body if the condition is true whereas the latter first proceeds to execute the body (i.e., whether the condition in the while part is true or false) and then checks the condition in order to decide whether to re-execute the body again or break out of the loop. Its syntax is as follows:

do {
    // do something repeatedly
} while ( CONDITION );

So, even though the condition given to the do-while block is false initially, it will still execute the body once because the condition-checking part comes after the body (unlike the while block).

for( ; ; ) loop

Another way of looping in C is by using the for block. It is similar to the while block, and the main difference is that the for block may also deal with the temporary variable definitions and manipulations related to the condition based on whose truth value its body is executed. It has the following abstract syntax:

for ( VARIABLE_DEFINITIONS ; CONDITION ; VARIABLE_MANIPULATIONS ) {
    // do something repeatedly as long as the CONDITION holds
}

To illustrate it on a simple, practical example, let’s suppose that we would like to print the “Hello, World!\n” string 10 times by using the for loop. The integer that we will need to declare and define is going to be iterated from 0 to 10, exclusive. With the while loop, we would need to define this variable (let’s name it iter) before the condition of the block (outside the while block), which is iter < 10. Then, inside the while body we would need to write iter = iter + 1 as the last instruction to increment its value by one each time “Hello, World!\n” is printed onto the screen. These two things (i.e., the temporary variable definition – int iter = 0; and the variable manipulation – incrementing iter by one) can be moved inside the for block along with where the condition (i.e., iter < 10) is put. This is done as shown below.

for(int iter=0; iter<10; iter=iter+1){
    printf("Hello, World!\n");
}

It is worth mentioning that the VARIABLE_MANIPULATIONS that are passed right after the CONDITION of the for block are going to be executed only after all the instructions in the for body are executed. Since the VARIABLE_DEFINITIONS, CONDITION, and the VARIABLE_MANIPULATIONS are all optional (when the CONDITION is not passed, the for loop assumes that the non-existing condition is automatically true and, therefore, executes its body indefinitely as infinitum), we could also rewrite the code snippet shown above like this:

int iter = 0;
for( ; iter<10; ){
    printf("Hello, World!\n");
    iter = iter + 1;
}

This now looks more like a while block. Also note that how the iter = iter + 1; line is put after the last instruction in the original for block’s body (that is, the printf(“Hello, World\n”); statement). Let’s now look at a different example.

#include <stdio.h>

int main(){
    int i = 0;
    for(int i=0; i<10; i=i+1){
        if(i%2 == 0){
            printf("%d is even\n", i);
        }
        else{
            printf("%d is odd\n", i);
        }
    }
    return 0;
}

This program will produce the following output:

0 is even
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd

If you wonder about the % operator, it stands for the modulo or remainder obtained from the division of two numbers. So, 3%2 is 1 because the remainder is 1, and 6%2 is 0 for the likewise reason. 

Breaking out of the loop

There is an instruction called break that breaks out of any sort of whiledo-while, and for loop. It does not work for the loops made possible by the goto statements. How the break works is pretty simple and very straight forward. The break statement breaks/jumps out of the loop in whose body it is directly present. Let’s see the following example.

while(1){
    printf("Hello, World!");
    break;
}

The code snippet above prints the “Hello, World!” string only once and then goes out of the loop. Although the condition is always a non-zero number, which is evaluated to the boolean value of true automatically, the break instruction, when executed by the computer, will cause the termination of this otherwise infinite loop. Let’s see another example where break will cause the inner loop to break but not the outer one.

for(int i=0; i<3; i=i+1){
    while(1){
        printf("Hello, World!");
        break;
    }
    printf("\n---\n");
}

The code snippet shown above produces the following output:

Hello, World!
---
Hello, World!
---
Hello, World!
---

The “Hello, World!” string is printed only three times because the break statement causes only the inner while loop to terminate immediately after printing. The reason it does not affect the outer for loop in this example is that it has been directly typed inside the while loop and, therefore, decides only the fate of the while loop. In contrast, we can break out of the outer for loop as shown below:

for(int i=0; i<3; i=i+1){
    int j = 0;
    while(1){
        printf("Hello, World!");
        break;
    }
    if(i == 1){
        break;
    }
    printf("\n---\n");
}

The output would look like this:

Hello, World!
---
Hello, World!

Notice how the “Hello, World!” string is printed only twice. This is because of the if condition inside the for loop that breaks when i equals 1 — that is, starts from 0 and the “Hello, World!” string is printed, then it is incremented to 1 and the same string is printed and the for loop terminates before i is incremented to 2). Also, notice how the “\n—\n” string is printed only once, and there are no triple dashes after the last “Hello, World!” unlike the previous example. This is because the break statement inside the if block is executed before the following printf(“\n—\n”); instruction when i is equal to 1, and this causes the computer to jump out of the loop even though there might still be some instruction left to be executed for the current iteration.

As one last example, let’s look at the following program:

#include <stdio.h>

int is_prime(unsigned int num){
    int divisor_found = 0;
    for(unsigned int i=2; i<num; i=i+1){
        if(num % i == 0){
            divisor_found = 1;
            break;
        }
    }
    return 1 - divisor_found;
}

int main(){
    unsigned int min, max;
    printf("Enter min number: ");
    scanf("%d", &min);
    printf("Enter max number: ");
    scanf("%d", &max);
   
    for(unsigned int num=min; num<max; num=num+1){
        if(is_prime(num)){
            printf("%u is prime\n", num);
            break;
        }
        else{
            printf("%u is not prime\n", num);
        }
    }
   return 0;
}

Before looking at the correct output, think about it by yourself as an exercise. Do you think you have found the correct answer? Go ahead and compare yours with the one shown below.

Enter the min number: 8
Enter the max number: 200
8 is not prime
9 is not prime
10 is not prime
11 is prime

So, the behavior of the program above is the print numbers starting from the min value (given by the user) until either a prime number has been found or the max value (also given by the user) has been reached without any primes in between the [\mathtt{min}, \mathtt{max}). It should be evident that the last printed number must always either be a prime or the max value itself.

Skipping an iteration

You have already learned about the break statement. It breaks out of the loops. Now, it is time to learn about another statement that just skips an iteration instead of breaking or jumping out of the loop that contains the statement. This statement is called continue. Whenever executed, it makes the computer to skip one iteration of the current loop. To be more clear, skipping an iteration means jumping to the beginning of the next iteration from the place the continue instruction is executed in the current iteration. Let’s see a quick example.

for(int i=0; i<4; i=i+1){
    printf("Beginning of iteration %d\n", i);
    if(i >= 2){
        continue;
    }
    printf("End of iteration %d\n", i);
}

The code snippet above produces the following output:

Beginning of iteration 0
End of iteration 0
Beginning of iteration 1End of iteration 1Beginning of iteration 2Beginning of iteration 3

Notice how the “End of iteration 2” and “End of iteration 3” strings are missing from the output. It is because when reaches values above or equal to 2, the if block is executed, which has a single continue statement. As soon as this statement is executed, the execution flow jumps to the beginning of the next iteration without executing the instructions that came after the executed continuestatement. However, for the first couple of iterations, everything is printed since the if condition is false (i.e., when i=0and i=1), resulting in skipping the continue instruction. Like the break statement, the continue statement does not affect outer loops either. Let’s see another simple example to demonstrate this.

for(int i=0; i!=2; i=i+1){
    for(int j=0; j<3; j=j+1){
        printf("i=%d, j=%d\n", i, j);
        continue;
        printf("This is never printed.\n");
    }
    printf("This is printed.\n");
}

The code snippet above produces the following output.

i=0, j=0
i=0, j=1
i=0, j=2
This is printed.
i=1, j=0
i=1, j=1
i=1, j=2
This is printed.

The “This is never printed.\n” string is not printed since there is an unconditional continue statement that is going to be executed in each iteration of the inner for loop. However, this statement does not affect the outer for loop, and that is why we see the “This is printed.” text on the screen.

To demonstrate the difference between the break and continue statements explicitly, let’s look at the example shown below.

for(int i=0; i<3; i=i+1){
    printf("Beginning of iteration %d\n", i);
    break;
    printf("End of iteration %d\n", i);
}

for(int i=0; i<3; i=i+1){
    printf("Beginning of iteration %d\n", i);
    continue;
    printf("End of iteration %d\n", i);
}

The first for loop will produce the following output:

Beginning of iteration 0

In contrast, the second for loop will produce the following output:

Beginning of iteration 0
Beginning of iteration 1
Beginning of iteration 2

I hope the distinction is obvious from this example. break jumps out of the loop, whereas continue jumps out of the iteration within the loop.

5. Pointers

How to think about a computer’s memory

You may have already had a suspicious look at your personal computer at home and wondered how that thing’s memory system works. Well, here is how you should think about it: a computer’s memory is like a tape with many individual cells on it. When you declare an arbitrary variable x (that is, int x;), a random cell is allocated to hold x‘s value. When you define x by initializing it with some value (that is, x=3;), that value is written onto the corresponding cell on the tape. In the meantime, your computer refers to that variable x by the index of its tape cell — that is, it does not actually know about the human-readable variable name x when the program is run after the compilation. In fact, it is the compiler (e.g., gcc, clang, etc.) that translates the human-readable names to machine-readable memory/tape indices. The machine-readable version of your C program, which is also called an executable or a binary file, may seem somewhat more complicated at first glance but is easier to understand once you know the basics. It is outside the scope of this tutorial series, so I will not cover it in detail here. However, I still think it is necessary for you to familiarize yourself with it just a little bit to understand today’s topic well.

Figure 1. A simple illustration of a computer memory as a tape with 100 cells.

Computers can only read files in binary1. What this sentence means is that it cannot understand a text file written in a human-understandable language such as English, Italian, or Azerbaijani. Not only those languages, but it cannot understand any piece of text that uses any character other than 0 and 1. This is what reading in binary means — you can only understand 0s and 1s… and nothing else. Since a C program is a text file that has human-readable characters and keywords, such as int, main, printf, return, and so on, the compiler needs to translate all of these into an executable or a binary file with 0s and 1s. For example, the compiler needs to translate a statement like int x = 3; into something like “11000111 11110101 00000011” that is machine-readable (i.e., readable by the computer). The compiler knows how to translate C statements into binary because it knows what your computer is capable of doing when it reads certain binary words (i.e., a binary string with 8 zeros and ones). For example, there is a special binary word 11000111 that your computer understands as “writing something down onto some tape cell.” We’ll call this word the mov instruction2. The next two words that come after the mov instruction determine the value to be written and the index of the tape cell, respectively. So, the binary sentence/statement “11000111 11110101 00000011” is understood by the computer as “writing the value 3 (00000011 in binary) into the memory location 11110101 (the tape cell corresponding to the variable x)”. Although there is much more to the machine-readable instructions and executable binary files, I will stop right here because I think you now have just enough understanding of these matters to understand what the pointers are. However, before moving into the pointers, there is one last thing that I want to cover. I would like to show you how memory is managed in a computer by using instructions such as mov.

Now you know that saying something as simple as int x = 3; is read by the computer as 110001111111010100000011. Recall that in the binary instruction, the second word is the address of the cell on the tape that is translated by the compiler because, naturally, the computer must know where to put the value 3. In contrast, when you type something like int y = x;, you don’t actually want to assign x‘s address on the tape to y; instead, you want to assign x‘s value to y. Due to this, when you put x on the left-hand side (LHS) of an assignment (e.g., x=3;), the value evaluated from the right-hand side (RHS) is written (by using the mov instruction) onto the corresponding tape cell; even if some other value might have written to the same place previously, the new value replaces the old. When you put x on the RHS (e.g., int y=x; or z=x+1;), its value is used to evaluate the expression, not the location or cell number on the tape. When you define a function with parameters (e.g., void f(int a, double b){ ... }), one empty tape cell is allocated for each parameter of the function. When you pass x into a function as an argument (e.g., f(x, 3.14);), x‘s value on the tape is copied to the cell corresponding to the relevant function parameter — so, functions work with their own set of tape cells specifically allocated for their parameters. That’s why modifying a variable’s value in a function, into which it is directly passed as an argument, is not possible. All of these are illustrated in the figures below.

Memory address of my variable

You have already learned a few things about computer memory and how values are stored in memory cells indexed/pointed by certain memory addresses. In programming, we call these memory addresses pointers since they point to specific memory locations. As you might have already remembered from the previous posts, you can get the memory address of your variable by putting the ampersand sign (&) in front of it. Recall that your variable is an alias for some memory address, which can be interpreted as both the address and the value stored in it, depending on the case. A variable has two possible interpretations:

  1. If something is assigned to a variable, then the memory address of the variable is extracted and used by the computer. That is, avariables on the LHS of an assignment statement is used by the computer to identify its location/pointer to store the RHS in.
  2. In any other case (e.g., a variable appearing on the RHS of an assignment), the variable’s value is used by the computer.

Since a variable (such as int a; float f; char c;) in C always occupies some memory address, it is called an L-value in a more technical term. In contrast, values (such as 5, -13.4, ‘a’) in C have only one possible interpretation — that is, there is no memory address associated with the value alone, so it is always treated as just a value. The more technical term for it is an R-value. So, the difference between an L-value and an R-value is that an L-value is a stand-alone “object” that has its own memory location and content, whereas an R-value has just the content without a memory address. This is actually not quite right. Everything is stored somewhere in the computer’s memory, but some places are just prohibited for the programmer to access as if it was the regular memory we can access with the ampersand operator. That’s what we mean when we say an r-value doesn’t have an address, i.e., its address is inaccessible through the ampersand operator. For the record, these inaccessible places where the intermediary values are stored are called registers.

Since every variable is an L-value in C when we use a variable on the LHS of the assignment statement, its address is used, and when we use a variable on the RHS of the assignment statement or in any other circumstances (e.g., passing it to some function as an argument), its value is used. To explicitly indicate the memory address of the variable whose value is used only (e.g., because it appears on the RHS of the assignment or it is passed to a function as an argument), we use the unary ampersand operator. The ampersand followed by a variable will give us the memory address of that variable (i.e., the pointer), which is an index (i.e., a positive integer). Since this index is just a value and hence not stored anywhere accessible to us, it is an R-value. That’s why we cannot use it on the LHS of the assignment statement, as shown below:

int x, y;
&x = &y; // error, &x is r-value but LHS must l-value

We used to pass addresses of variables to the scanf() function as it required working with the raw pointers and not the values. However, we did not see what the actual memory addresses look like. Well, a pointer is actually an index (positive integer) that can be printed by using the “%p” format string as can be seen below:

int val = 1;
printf("&val = %p", &val);

If there is a variable to store a pointer value, then we should also have a data type for that variable to store the pointer itself in some other memory address. The data type to store a pointer/address of a variable of type T (e.g., int, float, char, etc.) is T* type (e.g., int*, float*, char*, etc.). For example,

int x = 1;
int* ptr = &x;
ptr = x; // wrong! ptr = 1 and not the address of x

Memory addresses of different data types usually require different pointer types as the computer needs to know what type of data the pointer points to. It needs to know this information so that you can use the pointer to manipulate the content of the pointed memory block. To read the content of the memory block pointed by a pointer, you need to put the dereferencing operator (*) in front of the pointer. The * operator tells the computer to get you the actual content stored in the address/pointer that is being dereferenced by it. This is illustrated in the code snippet below:

int val = 34;
int* addr = &val;
printf("%d is stored at address %p\n", *addr, addr);
int val2 = *addr;
printf("%d = %d\n", val, val2);

You can also modify the content of the memory block pointed by a pointer. After the modification, any variable previously used as an alias to recall the content of the same underlying memory address can be used to recall the modified content. This can be done as shown below:

int val = 1;
int* addr = &val;
*val = 2;
printf("val = %d", val);

Since &val is a pointer to an integer and has the type int*, we can also dereference it directly without storing it elsewhere. In other words, * operator followed by an ampersand cancels out (that is, *&x = x) since the dereferenced address of a variable is equal to the very same variable by loghical implication. This can be shown as follows:

int val = 8;
printf("before: %d at address %p\n", *&val, &val);
*&val = 5; // same as val = 5;
printf("after: %d at address %p\n", *&val, &val);

Let’s also take a look at the following example:

int val1 = 1;
int val2 = val1;
int* addr1 = &val1;
int* addr2 = &val2;

printf("(val1 address) %p != %p (val2 address)\n", addr1, addr2);
printf("but (val1 value) %d = %d (val2 value)\n", val1, val2);

val2 = 2;
printf("val1 = %d or *%p = %d\n", val1, addr1, *addr1);
printf("val2 = %d or *%p = %d\n", val2, addr2, *addr2);

*addr1 = -7;
printf("val1 = %d or *%p = %d\n", val1, addr1, *addr1);
printf("val2 = %d or *%p = %d\n", val2, addr2, *addr2);

Notice how the val1 and val2 addresses (i.e., &val1 and &val2, respectively) are not the same while holding the same value 1. This is because they are physically stored at different locations in your computer’s memory, but the contents of both memory locations are the same due to val2 = val1; assignment. Moreover, such an assignment does not imply anything like &val2 = &val1, and in fact, what it implies is just the action of copying &val1‘s content into the &val2 memory location. This is also why changing val2‘s value to 2 (i.e., val2 = 2;) does not affect the val1‘s value, and changing val1‘s value (i.e., *addr1 = -7;) does not change the val2‘s value from 2 to -7.

Why does scanf() require a pointer?

We can use variables to store numbers or characters in the computer’s memory. For example,

int age = 4;
printf("current age: %d\n", age);
age = age + 1;
printf("after one year: %d\n", age);

Why the hell does scanf() require a pointer, then? If you remember from one of the previous posts, computers compute the output of a function by using a substitution. Not only that, but you also learned in the first subsection of this post that the function parameters are allocated separately on the memory, and the variables/values passed as arguments are copied to those locations to be then used by the function. Let’s look at the following example:

void f(int x){
    printf("x in f() is at %p and equals %d\n", &x, x);
}
void g(int x){
    x = 6;
    printf("x in g() is at %p and equals %d\n", &x, x);
}
int main(){
    int x = 2;
    printf("x in main() is at %p and equals %d\n", &x, x);
    f(x);
    g(x);
    printf("x in main() still equals %d\n", x);
}

Although the variable name x was used in all three functions (i.e., f(), g(), and main()), each one of them was used as an alias for a different memory location, meaning that changing one x‘s value could not possibly affect the other. Don’t get confused, though. The notion of local variables lets programmers not run out of variable names too quickly, as the same variable name can be reused for a different purpose in another C function. Due to this, each local variable is actually an alias for a different memory address. A variable is considered local (for a function f()) if it is a parameter of f() or a variable declared inside the function’s body. So, notice how x‘s value in the main() function did not change from 2 to 6 even though we called the function g(x); before printing its value. This should now explain the output you would see on your screen if you ran this program; it should look like something shown below:

x in main() is at 0xa843bb9fff024eec and equals 2
x in f() is at 0xa843bb9ffd024b04 and equals 2
x in g() is at 0xa843bb9ffd034bfa and equals 6
x in main() still equals 2

So, how could we change a variable’s memory content by passing it as an argument to some function and modifying its value inside that function? Is it even possible to do something like this? The answer is yes, of course. Instead of passing the value that is stored in the variable’s memory location (say, 0xa843bb9fff024eec), we should pass the memory location of that variable as an argument. Although the memory address 0xa843bb9fff024eec is also going to be copied as a value to the function’s parameter’s location in the memory, we can now manipulate the contents of that memory address by dereferencing it. This can be done as shown below.

void h(int* addr){
    printf("addr in h() is at %p and equals %p\n", &addr, addr);
    printf("inside the addr (%p) is stored a value %d\n", addr, *addr);
    *addr = 6; // notice how we don't need to directly assign by typing `addr = 6;`
    printf("before returning from h() the address %p now holds a new value %d\n", addr, *addr);
}
int main(){
    int x = 2;
    printf("x in main() is at %p and equals %d\n", &x, x);
    h(&x);
    printf("x in main() now equals %d\n", x);
    return 0;
}

This will produce an output similar to the one shown below:

x in main() is at 0xa843bb9fff024eec and equals 2
addr in h() is at 0xa843bb9f0a0c46a1 and equals 0xa843bb9fff024eec
inside the addr (0xa843bb9fff024eec) is stored a value 2
before returning from h() the address 0xa843bb9fff024eec now holds a new value 6
x in main() now equals 6

Nice! We just managed to change the x‘s value in initialized in one function (i.e., main()) by passing it to another function (i.e., h()). We already knew that whatever we pass into functions as arguments in C is just getting copied to the function’s “private” parameter variables in memory, so inside the function’s body, we would not be able to put any new values into the address of the variable whose value we wanted to be changed. Instead, we could only change the function’s own set of variables, which are not the same as the ones passed in terms of the memory address (although the names might be the same). This was the reason we had no other way but just to pass the variable’s address directly as an argument so that modifying its content would actually make the change globally and absolutely. Since the scanf() function needs to write whatever value the user enters via the keyboard into a variable, it also needs to take its address to make the modifications on the memory.

Pointer arithmetic

Pointers are potentially big numbers/indices that refer to the memory locations of the memory cells in your computer. That is why we can apply some arithmetic operations on them and hopefully we will make sense out of the resulting numbers. For example, if we know the memory address of some variable x, then we could effectively access the memory contents of its left and right adjacent cells by dereferencing (&x-1) and (&x+1), respectively. Another example is, given two variables x, followed by y, we can also find the memory address in the middle by typing &x + (&y – &x)/2. This is all pointer arithmetic, i.e., arithmetic on memory addresses of variables and not the values stored on those addresses. Let’s see a quick example:

int x = 3;
int y = 7;
int z = -23;
printf("&x = %p\n", &x);
printf("&y = %p\n", &y);
printf("&z = %p\n", &z);
printf("[%d][%d][%d]", *(&y-1), *(&y), *(&y+1));

If you see the numbers [-23][7][3] in the output, you might think that if the computer chooses truly random addresses for each variable, then what are the odds of the numbers -23 and 3 randomly stored on those addresses or those addresses actually being consequent in the memory. Well, actually, I never told you that all of the addresses are picked in a truly random way by your computer. In fact, that’s what we observe here. The memory addresses of all three variables are adjacent to each other in memory in the order in which they appear in the code. However, there is a little caveat: such consequent addresses are allocated from higher index to lower index. That’s why &y-1 equals &z and not &x (likewise, &y+1 equals &x and not &z). With this being said, now you might naturally wonder about dereferencing (&z-1) since this address is not within the declared variables in the program given above. If you dereference that memory address by inserting *(&z-1) inside a printf statement into the code, it may result in one of the two possible outcomes:

  1. A “random” number may appear on the screen indicating that the memory address (&z-1) has been used by the computer to store that number for some reason;
  2. Your program crashes with the segmentation fault indicating that the computer does not allow you to access the memory you did not declare by using a variable for some safety reasons.

If you are coming from almost any other existing programming language, this may feel like magic. I mean, accessing a variable by using the pointer pointing to another variable? Pretty magical. Question: Do C programmers usually use pointers for this purpose? Answer: No. In fact, this is something that you should not do in your C program since it is not easily readable, and it may easily lead to very shady bugs during the program execution. If you want to access the memory content of a variable, just use the same variable to do so. No need to get fancy at the cost of getting fired from your C job.

6. Arrays

Hold my integers

Imagine a case where you need to work with tabular-like data of the ages of 50 people. How would you store these numbers in the computer’s memory? Would you type out 50 different variable names one by one, such as age1, age2, age3, etc., and then assign values to them? You could absolutely do that. However, there is a better way! It is by using an array…

An array is a bunch of elements that are located in the memory consecutively. That is to say, the memory addresses of any ordinary variables, such as age2 and age3, did not need to be consecutive in the memory, whereas every successive pair of elements in an array is guaranteed to be consecutive in the memory as well. You might ask, what is the advantage of using an array over multiple variables? Well, there are several advantages:

  • Instead of typing out 50 different variable names by hand, we now have to type only one (that is, the array variable);
  • Programmers like to keep things organized. If you have 50 names and 50 ages to work with in your C project, you might as well represent them as two separate chunks of memory blocks;
  • Finally, pointer arithmetic can be used to jump between the array elements very conveniently.

An array in a C program can be declared by using a pair of square brackets as shown below:

unsigned int ages[50];

That will tell the computer to allocate 50 positive integers in consecutive order in the memory. In fact, you can test this by using the pointer arithmetic since the variable ages is actually going to be used as an alias for the memory address of the first unsigned integer. This can be done as follows:

unsigned int ages[50] = {0}; // initialize all ages to 0
for(int i=0; i<50; i=i+1){
    ages[i] = 2*i + 1;  // ages[0]=1, ages[1]=3, ages[2]=5, ...
    printf("ages[%d] = %u and is at address %p\n", i, ages[i], &ages[i]);
}

Since the memory addresses are consecutive, we could easily replace &ages[i] with &ages[0]+i and ages[i] with *(&ages[0]+i), and everything would still work the same as before. Note that the difference between &ages[i] and &ages[0]+i is that the former is a pointer to the element at index i, and the latter is the result of the pointer arithmetic that reads as adding number i to the pointer that points to the first element of the array (i.e., the element at index 0). The two must be the same because of the fact that the array is consecutively stored in the memory. This can be simply visualized as follows:

addresses:       0   1  2  3   4  5  6  7 ... 53  54 55  56  57
memory cells:  ['a'][3][ ][-2][1][3][5][7]...[99][ ][ ]['?'][0]...
array index:                   0  1  2  3 ... 49
        the array begins here--^     ^--let's pick ages[2]=5

In the example shown above, the elements of our array (i.e., 1, 3, 5, 7, …, 99) are stored consecutively starting from the memory address 4. The first element of the array is always at an index 0, so the address of the first element is &ages[0], and if you printed its value (given our hypothetical memory layout shown above), it would be 4. So, from this picture, it should be obvious that &ages[2] (which is 6) is also equal to &ages[0]+2 (which is 4+2=6). Moreover, this is true for any index, i.e., &ages[i] equals &ages[0]+i. Recall that the array variable itself is also a pointer to the first element of the array, and therefore, age is a memory address that is equal to &ages[0]. With that being said, we can rewrite the code snippet above as shown below.

unsigned int ages[50] = {0};
for(int i=0; i<50; i=i+1){
    *(ages+i) = 2*i+1;
    printf("ages[i] = %u and is located at %p\n", i, *(ages+i), ages+i);
}

Working with 50 numbers got easier with the help of arrays. Since we did not have any dimension other than age, we used a single-dimensional or 1D array. Now, let’s assume that we have to store the following tabular data by using an array:

IndexAgeHeight
023174
110140
216180
345179
.
.
.
.
.
.
.
.
.
4934156

In this case, having just 50 integers won’t cut it; we have to allocate 100 integers, i.e., 50 to store ages and 50 for heights. That’s when we can use a two-dimensional or 2D array where the second dimension will be used to hold a pair of numbers (i.e., age and height). This can be done as follows:

unsigned int data[50][2] = {0}; // 50 * 2 = 100 integers in total
data[0][0] = 23; data[0][1] = 174;
data[1][0] = 10; data[1][1] = 140;
data[2][0] = 16; data[2][1] = 180;
data[3][0] = 45; data[3][1] = 179;
// ...
data[49][0] = 34; data[49][1] = 156;

In the code snippet above, each data[i] holds two integers (i.e., age and height, respectively) in a single row indicated by the last dimension (i.e., unsigned int data[50][2]) while the first dimension is indexed by the number of people we have. Since data[i] (where 0 \leq i \leq 49) refers to the (i+1)-th row, we can further access the age or the height information by using the second index 0 \leq j \leq 1, that is data[i][j] (data[i][0] is the age given in the (i+1)-th row and data[i][1] is the height given in the (i+1)-th row).

I should probably also tell you that we can change the order of the dimensions as long as we have storage for 100 integers. Let’s try to imagine the following declaration: unsigned int data[2][50]. By using this declaration, we could say that now the first dimension indicates the columns instead of the rows, and the second dimension indicates the rows of our table instead of the columns. data[0] holds all 50 ages, and data[1] holds all 50 heights, and to access/modify the individual age and height numbers, the second index must be used (data[0][j] refers to the age given in the (j+1)-th row and data[0][j] refers to the height given in the (j+1)-th row). Let’s see how we would initialize such an array in C:

unsigned int data[2][50] = {0}; // 2 * 50 = 100 integers in total
data[0][0] = 23; data[1][0] = 174;
data[0][1] = 10; data[1][1] = 140;
data[0][2] = 16; data[1][2] = 180;
data[0][3] = 45; data[1][3] = 179;
// ...
data[0][49] = 34; data[1][49] = 156;

You may already get the feeling for the arrays in C. They are useful for representing tabular data, and they can scale easily to any arbitrary number of dimensions you want. You can create an n-dimensional array by putting n opening-closing brackets [ ] after the array variable (e.g., float arr[4][3][6][2][5]; for a 5D array that holds 4*3*6*2*5=720 floating-point numbers). However, there is a catch: an array can only hold a single type of elements, meaning that if you declare a float array, then it can only hold floating-point numbers and not characters, for example. So, you cannot mix different types in a single array. To illustrate the issue, let’s suppose that you should store the following table in the computer’s memory:

IndexAgeHeightWeightFirst NameLast Name
02317478.3BobSurfers
11014059.9NickBostrom
21618065.8KatherineCillian
34517995.5BillLohm
.
.
.
.
.
.
.
.
.
493415680.0AlexBeckham

We can use an array of positive integers to store age and height information, an array of floats to store weight information, and an array of strings to store the first and last names. This could be done as shown below.

// table1[0] represents ages and table1[1] represents heights
unsigned int table1[2][50] = {0};

// table2 represents weights
float table2[50] = {0};

// table3[0] represents first names and table3[1] represents last names
// let's imagine the max name length can be 32 characters
char table3[2][50][32] = {0};

With all this being said, there is one thing left to be mentioned in this section. That’s the array initialization. Instead of initializing an array to all 0s (i.e., by assigning them to {0} in the declaration), you can put any arbitrary numbers inside the curly braces to initialize your array however you want. Let’s see the examples shown below.

float weights[3] = {45.3, 66.7, 89.0};      // now weights[0] = 45.3; weights[1] = 66.7; weights[2] = 89.3
int numbers[] = {3, 6, -2, 0};              // same as numbers[4] = {...};
char first_name[] = {'B', 'o', 'b', '\0'};  // it's recommended to put '\0' as the last character
char* last_name = "Marley";                 // same as last_name[] = {'M', 'a', 'r', 'l', 'e', 'y', '\0'};

The last two initializations are string3 initializations, where we can put a bunch of characters together in a character (char) array to represent words or any text in general. When we do not indicate the size of the array (that is, the number of elements) inside the square brackets [] during the initialization, the compiler infers it automatically by looking at the elements inside the curly braces { ... }. For example, saying int numbers[] = {3, 6, -2, 0}; is perfectly understood by the compiler because we have provided all the elements of the numbers array inside the curly braces. Initializing strings is a bit different than initializing other built-in types, such as integers or floating-point numbers. In C, it is a good practice to end a character array or a string with a null-terminator. A null-terminator is the special character '\0', and it is used to indicate the end of the string. When we use a string initialization by using double quotes (e.g., char* last_name = "Marley";), the compiler automatically puts the null-terminator at the end. Having a null-terminator in our string also implies that its size is actually one more than what it needed to contain initially. That is to say, the first_name string contains 4 characters instead of 3, and the last_name string contains 7 characters instead of 6.

The size of my array

An array in C is a pointer to its first element and, hence, does not contain the size information by itself. This means that just by knowing the first element or its address, you cannot know the number of elements in the whole array. For this reason, C programmers usually pass the size information along with the array pointer to functions that need them. Since array variables are also the pointers to the first elements, one can also pass them to functions as pointers. Here’s a quick example:

#include <stdio.h>

double sum(double arr[], unsigned int size){
    double out = 0.0;
    for(unsigned int i=0; i<size; i=i+1){
        out = out + arr[i];
    }
    return out;
}

double mult(double* arr, unsigned int size){
    double out = 1.0;
    for(unsigned int i=0; i<size; i=i+1){
        out = out * arr[i];
    }
    return out;
}

int main(){
    double* numbers = {-2, 1, 3.5};
    unsigned int size = sizeof(numbers) / sizeof(double); // size = 24 / 8 = 3
    printf("sum of [-2, 1, 3.5] is %lf\n", sum(numbers, size));
    printf("multiplication of [-2, 1, 3.5] is %lf\n", mult(numbers, size));
    return 0;
}

Notice the use of the sizeof operator in the main() function. This operator returns the size of its argument in bytes. For example, in a 64-bit machine, sizeof(char) gives us 1, sizeof(int) gives us 4, sizeof(float) gives us 4, sizeof(double) gives us 8, sizeof(char*) gives us 8 bytes, and so on. When its argument is an array, this operator behaves a bit differently. If the array passed as an argument to the sizeof operator is in the same scope where the sizeof operator has been used, it returns the number of bytes the array contains. On the other hand, if the array passed as an argument is not in the same scope, then the sizeof operator treats it as a pointer and returns the size of the pointer (i.e., 8 bytes in 64-bit machines) and not the whole array. That’s why, to get the actual number of elements in the numbers array, we had to divide its size in bytes (i.e., sizeof(number))by the size of its type (i.e., sizeof(double)). Moreover, we passed this size variable to the other functions because passing an array to a function as an argument always causes it to be treated as a pointer inside the function’s body and, hence, using the sizeof operator on an array argument would return 8 bytes all the time instead of the array’s actual size. We can test this easily as follows:

#include <stdio.h>

void test(char* name){
    printf("sizeof(name) = %zu bytes in test()\n", sizeof(name));
}

int main(){
    char* name = "Bob"; // has 4 characters - 'B', 'o', 'b', and '\0'; each character is 1 byte
    printf("sizeof(name) = %zu bytes in main()\n", sizeof(name));
    test(name);
    
    int x = 7;
    int* ptr = &x;
    printf("sizeof(int*) = %zu and sizeof(ptr) = %zu\n", sizeof(int*), sizeof(ptr));
    printf("sizeof(int) = %zu and sizeof(*ptr) = %zu\n", sizeof(int), sizeof(*ptr));
    return 0;
}

This would produce the following output on your screen:

sizeof(name) = 4 bytes in main()
sizeof(name) = 8 bytes in test()
sizeof(int*) = 8 and sizeof(ptr) = 8
sizeof(int) = 4 and sizeof(*ptr) = 4

Since the ptr variable has the type of int*, sizeof(ptr) and sizeof(int*) are equal. Moreover, since dereferencing ptr (i.e., *ptr)gives us an int, sizeof(*ptr) and sizeof(int) are also equal. This works for any type in C.

7. Data Structures

All variables in one place

Being able to represent integers, floating-point numbers, characters, and pointers (i.e., memory addresses) is cool. But what if you wanted to represent more complex and/or abstract objects? To illustrate it with a practical example, let’s suppose that we want to represent a person by storing his/her age, gender, heigh, weight, and full name. As a side note, we already know that we can represent age and height as a couple of unsigned integers, gender as a single character, weight as a floating-point number, and full name as a string (i.e., character array). So, the following code snipped seems like a good fit.

unsigned int age, height;
char gender;
float weight;
char name[100];

It would be fair to use the variable declarations shown above if you wanted to represent a single person. What if you needed to deal with 50 different people’s data? You may say that we can just use arrays for each variable whose indices refer to different people. This can be written as shown below.

unsigned int age[50], height[50];
char gender[50];
float weight[50];
char names[50][100];

This would work well technically. However, it does not seem very readable or robust enough for future code modifications. I mean, the fact that we don’t have a single variable named person that holds all of the necessary information together is what makes it harder to work with. For example, let’s consider passing the person’s data to another function. With what we currently have in our hands, we would need to do something like this:

void func(unsigned int age, char gender, unsigned int height, float weight, char* name);

If you decided to remove the age field or add another field to the person object later on, you would need to modify the removed/added fields in all the places that used to use these fields (e.g., functions taking all the person’s data as arguments). This is just one of the dozens of problems with the current approach that we have taken with the representation of a person object. In order to avoid these annoying and error-prone issues, it would be better to have the ability to create one’s own custom data type named person.

It is indeed possible to create your own data type that can be used to represent more complex abstract objects that may have many internal details (such as the person object having age, gender, and so on). For this, we use data structures! The below is given the generic template to create your own data structure or a custom data type in other words:

struct TAG {
    TYPE VARIABLE;
    // 2nd variable with any type
    // 3rd variable with any type
    // ...
};

With the help of a custom data structure, we can now easily represent the person abstraction in C as shown below.

struct Person {
    unsigned int age, height;
    char gender;
    float weight;
    char name[100];
};

You can initialize an object of any custom type declared as a struct by initializing each of its fields separately. One simple way of doing it is as follows:

int main(){
    struct Person person;
    person.age = 13;
    person.height = 150;
    person.gender = 'm';
    person.weight = 65.7;
    strcpy(person.name, "Bob Marley");
    return 0;
}

There are certain data structures that are well-known because of how useful they have been in solving many real-world problems. Now, we are going to take a look at a couple of them and see how they can be implemented in C.

Example: Stack

Stack is a well-known data structure that represents a last-in-first-out (LIFO) operation on a blob of data. How LIFO stack works is similar to how you put dishes on top of each other in the kitchen as you wash them and how you always take the top dish when you need to use a new dish. The similarity between the two is that the last dish you put on top of the stack of clean dishes is the first one you are going to take when you need it, and likewise, you put elements on top of each other in the stack, and you can only take out the top one when you need to. In more technical terms, putting an element on top of the stack is called pushing into the stack, and taking the top element out is called popping from the stack. Let’s define the stack data structure now.

struct Stack{
    int nums[100];
    unsigned int size;
};

The data structure shown above represents a stack that can be used to push/pop integers into/from the nums array. Moreover, it has a certain predefined capacity, i.e., holding up to 100 integers. The stack has two types of essential operations as mentioned previously: pushing an element to the top and popping an element from the top. These operations can be implemented as C functions as shown below.

void stack_push(struct Stack* stk, int num){
    if(stk->size < 100){
        stk->nums[stk->size] = num;
        stk->size += 1;
    }
}

void stack_pop(struct Stack* stk){
    if(stk->size > 0){
        stk->size -= 1;
    }
}

stack_push() function pushes an integer (the second argument num) into the stack (the first argument stk). The elements of the stack are stored in an array named nums, where the index 0 denotes the bottom element in the stack’s array; therefore, the index size-1 denotes the element on the top. The top index (stk->size-1) is used to store an integer val, and then the size is incremented by one because the stack now has one new element. However, an element is pushed if and only if there is an empty space in the stack’s array, i.e., less than 100 elements already present in the stack’s array.

stack_pop() function removes an element from the top of the stack, i.e., removing an element whose index is stk->size-1. To make sure that the top element is removed from the stack, we just decrement the stack size by 1; so, the new top element is now pointed by the index old stack size minus two. It is also worth mentioning that an element can be removed from the stack if and only if there is at least one element already in it, and therefore, we have to check if stk->size > 0.

One last thing is to pay attention to the first argument of the stack functions being a stack pointer, not the stack object itself. If we passed the stack object itself, it would be copied to another memory location in your computer, and hence, making any modifications to the copied version would have no effect on the original stack object. That’s why we need to pass the memory address of the original stack to these functions. While it is true that with the current implementation, the memory addresses of the original stack object are getting copied into other locations in the memory, dereferencing the copied pointer values gives us the original stack object since the copied values are the original stack’s memory address.

Now, we can use our stack data structure as shown below. Do not forget to initialize your stack with the correct size information before using it.

#include <stdio.h>

struct Stack{
    int nums[100];
    unsigned int size;
};

int main(){
    struct Stack stack;
    stack.size = 0;      // you MUST initialize the stack size to 0 before using it
    
    stack_push(&stack, 3);    // stack now has 1 number:  [3]
    stack_push(&stack, 2);    // stack now has 2 numbers: [3, 2]
    stack_push(&stack, 92);   // stack now has 3 numbers: [3, 2, 92]
    
    printf("stack size: %u\n", stack.size);
    for(unsigned int i=0; i<stack.size; i+=1){
        printf("index %u: %d\n", i, stack.nums[i]);
    }

    stack_pop(&stack);    // stack now has 2 numbers: [3, 2]
    stack_pop(&stack);    // stack now has 1 number:  [3]

    printf("stack size: %u\n", stack.size);
    for(unsigned int i=0; i<stack.size; i+=1){
        printf("index %u: %d\n", i, stack.nums[i]);
    }

    stack_pop(&stk);    // stack now has no element: []
    stack_pop(&stk);    // no change has been made since stk.size = 0
    return 0;
}

Example: Naive Queue

Queue is another well-known data structure that works similarly to the stack in the sense that it is a container used to hold elements. Unlike the stack, the queue follows first-in-first-out (FIFO) logic. As LIFO is analogous to the dish-washing/using process, FIFO is analogous to waiting in line to make a purchase in the store. For simplicity, we assume that whoever enters the store first is also the first one on the line making a purchase. This is what a queue is. You push elements to the back of the queue, and you pop elements from the front. Let’s see how a naive queue data structure can be defined.

struct Queue{
    int nums[100];
    unsigned int size;
};

The Queue data structure shown above can be used to hold integers. Here, index 0 denotes the front of the queue and the last index size-1 denotes the front. As already mentioned, the two most important operations on queues are pushing an element to the back of the queue’s array and popping an element from the front of the queue’s array. Let’s see how these functions can be implemented in C.

void queue_push(struct Queue* que, int num){
    if(que->size < 100){
        que->nums[que->size] = num;
        que->nums += 1;
    }
}

void queue_pop(struct Queue* que){
    if(que->size > 0){
        for(unsigned int i=1; i<que->size; i+=1){
            que->nums[i-1] = que->nums[i];
        }
        que->size -= 1;
    }
}

Now, we can use our simple queue data structure in our C program. Do not forget to initialize the size of the queue before starting to push/pop numbers. Otherwise, your program may behave buggy, resulting in unexpected and weird outputs.

#include <stdio.h>

struct Queue{
    int nums[100];
    unsigned int size;
};

void queue_push(struct Queue* que, int num){
}

void queue_pop(struct Queue* que){
}

int main(){
    struct Queue que;
    que.size = 0;        // always initialize size to 0 before using the queue

    queue_push(&que, 30);  // que now has 1 element:  [30]
    queue_push(&que, 20);  // que now has 2 elements: [30, 20]
    queue_push(&que, 97);  // que now has 3 elements: [30, 20, 97]

    printf("queue size: %u\n", que.size);
    for(unsigned int i=0; i<que.size; i+=1){
        printf("index %u: %d\n", i, que.nums[i]);
    }
    
    queue_pop(&que);  // que now has 2 elements: [20, 97]
    queue_pop(&que);  // que now has 1 element:  [97]

    printf("queue size: %u\n", que.size);
    for(unsigned int i=0; i<que.size; i+=1){
        printf("index %u: %d\n", i, que.nums[i]);
    }

    queue_pop(&que);  // que now has no element: []
    queue_pop(&que);  // no change has been made since (que.size > 0) is false
    return 0;
}

Leave a Reply

Your email address will not be published. Required fields are marked *