: An attempt to make computer machines run better

home | better linux | games | software | tutorials | reference | web log |

Tutorials: index | C | risc-v 32 assembly | risc-v 64 assembly | webassembly | x86_64 assembly | C 1 (old) | C 2 (old) | C 3 (old) | C 4 (old) | low-level graphics |


C Tutorial

The things you should learn

Tutorial 3: Making Improvements

 In the second tutorial we wrote a small program where the user must guess a random number generated by the computer. If you ran the program, you probably noticed that it is far from perfect. It is very user unfriendly and generates the same number every time. Let's fix these problems and learn some new things while we are at it.

User Friendliness
 First of all, if you write a program that is not user friendly, chances are that people won't want to use it. Basically, all we need to do is tell the user what is going on and what the user should do. As the program is now, we just expect the user to know what to do, which is logical since you are the only user. However, it is always a good exercise to assume that someday, somebody else is going to try your program. So let's add some text output into the program to prompt the user to guess again. We can do this with printf() just like we did before.

Programmer Friendliness
 On a similar note, and of about that same importance, the source code currently is just source code. There are some benefits to that, yet also some disadvantages ... significant disadvantages. It's a bit nice because now the only way you can understand the code is if you actually read it, which forces you to think more about your code and hence learn faster. However, it will take longer to understand even your own code, and if you make a mistake, you may have a more difficult time figuring out where your error is, which is of course important. Also, it makes it really hard for other people to understand your code. This becomes especially important when working on larger complicated projects. It is really better to be able to understand your code.
 Fortunately, we have whitespace and comments.
 Whitespace means newlines, spaces, and tabs. These are very important for programming because the compiler totally ignores them. This seems counter-intuitive at first because usually we think of programming as writing code for the computer to read, but the parts that the computer ignores are just as important (in a way). Using whitespace, you can write your code in a format which makes it easier for you (the programmer) and others (other programmers) to read and understand. You saw in our previous examples that every line of code inside the main function is indented out. This is not required in C, however, this practice allows programmers to easily see which code is part of a function simply by looking at how far it is indented. If we indent each block of code out with an additional tab, we can easily see which code is part of an if statement or contained in a loop.
 Comments, likewise, are totally ignored by the compiler. Comments allow us to write in plain English (or Korean or Chinese or Russian or Dothraki or any other language you might be able to type in) which will have no effect on the functionality of the program. We can use comments to explain what a piece of code is supposed to do. This becomes especially useful when writing large programs over long periods of time or where other people are going to be reading your code (or if you are doing any drunk programming).
 There are two types of comments. Single-Line Comments and Block Comments. Single-Line Comments start with // and continue to the next newline character. Since this is a comment and not real C code, it does not end with a semicolon. These type of comments are also sometimes called C++ Style Comments, but do not get confused by this, they are perfectly acceptable in C and have been part of the C standard since C99 (1999), and implemented in compilers before that. Do not hesitate to use Single-Line Comments. Block Comments are also called C Style Comments sometimes. A Block Comment starts with /* and ends with */. It does not matter how many lines this type of comment takes up, it always ends at the next */.
 I recommend using comments as much as possible. I also recommend using Single-Line Comments mostly, reserve Block Comments for special cases where they make sense. For example, if you are writing a comment that takes only a single line, it does not make sense to use a Block Comment, although, there are many programmers that do this.
 Comments also have a second use: debugging. If you have a particular line of code (or block of code) that you have written recently but your program is encountering some type of runtime error which you think may be caused by that line of code, you can use comments to test to see whether that code really causes the problem. You can change that line of code into a comment by simply adding a // on that line before the code, this will cause the compiler to ignore that line of code. This is known as commenting out code. Commented out code is ignored by the compiler and does not become part of the compiled executable, this allows you to test your program without that line of code. However, if you determine that line of code is not a problem, you can easily change that commented out code back into regular code by simply removing the // at the beginning of the line. This is one reason why Block Comments can be inconvenient. For instance, if you need to comment out a block of code rather than a single line, you can use a Block Comment to do so, yet that block of code contains another Block Comment somewhere, then your commented out block will end at the first */ that the compiler encounters, which is probably not where you intended your comment to end, which can cause more problems or sometimes lead you to incorrect conclusions about bugs in your code.

 We have talked about functions numerous times before, but now let's take the time to learn how to create our own function. Although we already learned this essentially because the main() function is a function, but now let's create another function which we can call from main().
 All functions are defined using the same form:
return type function name(argument list)
Where return type is a valid return type such as int or char or void. The function name could be almost anything, you get to decide what it should be, however, there are some rules. It must not contain any spaces (one word) and it must contain only regular English characters (A-z), numbers, and the underscore character. Also, it may not start with a number.
The argument list is a comma separated list of arguments. This must include the variable type as well as a name for each argument. These names must follow the same rules as functions and other variables.
 Like the if statement, the body of a function is contained in curly brackets ( { and } ).
 Writing functions is useful when you need to execute the same or similar code in many different places. For instance, we need to ask the user for input in two different places in our code. This was simple in the last example because it was only one line of code, but now we (should have) added code to prompt the user for input first. Instead of prompting the user with the same text and then getting an int from the command line, we can just call a function to do all that.  Let's try creating a function to prompt the user and get the response. We must define the function outside of the main function. Let's call the function "getGuess()" because the function will handle the user's guessing. We probably don't need any arguments for this function, so just don't put anything between the parenthesis. However, that is about all we can do for now, we can't decide on the return type yet because we still have another topic to cover before that, we need to talk about scope.

 You may be tempted to just copy the relevent code from main() and paste it into your newly created function and expect it to run. I can tell you for certain that it will not run. The problem is that we used a variable called "guess" in main() which we declared at the beginning of main(). In your new function, if you try to access or set guess, then you will get a compile error. The reason for this is because the scope of guess is limited to main(). The compiler only knows about guess inside of main() and getGuess() is defined outside of main(). A variable can be accessed only in the block of code where it was declared and any blocks nested inside that block. This is called the scope of the variable.
 Actually, almost everything in C has a scope, including functions. Usually we define functions on the top level outside of any other functions, so usually it is not evident, but functions do have a scope as well. Constants on the other hand, do not have scope in the same sense because they are handled by the preprocessor and not the compiler.
 This presents our program with a bit of a problem because new need to modify a variable in our new function, but that function is outside of the variable's scope. We can solve this problem by returning.

 We cannot access the variable from our new function, but there is a good way to solve this problem. Instead of setting that variable in the function, we can return the value that the user entered to the point where the function was called from.
 If we look at our function again, you may see we have not yet decided on a return type, because then we did not know what we would be returning from the function. However, now we do know. We need to return an int because we need an int to compare with the random number and the user will enter an int. So, now we know how to write our function, it should look like this so far:
int getGuess()
 printf("Enter your guess:\n");
 scanf("%d", &guess);
 However, like we discussed in the last section, we cannot access guess here because this is out of the scope of the variable. However, an easy way to fix this is to declare another variable inside our new function with the same name and type. It is very important here to remember that this new variable is NOT the same as the one in main(), they have the same name but they are two different variables. It is OK to use the same name because the scope of the two variables does not overlap, so there is no conflict.
 Also, since these two variables are not the same, setting guess in getGuess() has absolutely no effect on guess in main(). To get the value back to main() we need to return that value. This is done with the return statment, which has the form:
 return expression;
In this case, our expression is just the variable guess. So our final code for this function will look like this:
int getGuess()
 int guess;  
 printf("Enter your guess:\n");
 scanf("%d", &guess);

 return guess;

Calling Functions
 We have covered calling functions previously, however, now we need to call a function that returns a value. We have also done this before when we called rand(), although it may not have been clear to you then.
 Essentially, we want to do exactly the same thing we did then, but we do not want to apply the modulus operation to the value, and of course, we will be calling getGuess() instead of rand(). The code to call the function will like like this:
 guess = getGuess();
 This will call the function getGuess() and execute the code contained in that function and then set guess equal to the value returned from that function.

Internal States and Better Random Numbers
 In our program, we are calling the rand() function once per execution of our program. If you ran the program more than once, you might have noticed that the rand() function is actually returning the same value every time. This is because, like I said before, computers cannot create random numbers, rand() is only a pseudo-random number generator, which is just a mathematical algorithm. The same algorithm is executed each time your program is run, so of course it generates the same number each time. However, this is definitely not what we want. There is a solution though.
 rand() has what is called an internal state, which is some variable inside rand() that it remembers and can change. It uses this state to generate new numbers. Each time your program is run, it gets reset, but if you called rand() a second time in your program it would return a different number. However, since the internal state is reset each time the program is run, it will still return the same set of numbers each time. To get it to return something different each time we need to set rand()'s starting point to something different each time the program is run. This is called seeding the PRNG. The simplest way, and a common way to do this is to use the system time for this seed. Since the system time is specified in seconds, each time you run the program you will get different random numbers as long as you do not run the program more than once within one second (which is a reasonable assumption). Keep in mind that if you are relying on your random numbers for security purposes, this is a very poor method to use, but for our very simple usage, it is alright.
 To seed the random number generator, we can use another function called srand() (which stands for "seed random [generator]").
 srand() takes one int argument which is the number to seed the prng with. Now, like we already discussed, we want to seed it with the system time. We can get the system time with the time() function. This function is also part of the standard C library, but it's prototype is located in time.h, so, in order to use time(), we need to include this header, otherwise the compiler will not know how to call the function. The function prototype for time() looks like this:
time_t time(time_t *t);
 So we can see that time() returns a value of type time_t. This might be confusing at first because we never heard of a type called "time_t" before. However, time_t is actually just an integer, the same as int. We can see that the function takes one argument, which is a pointer to a time_t, time() will set the variable pointed to by the argument to the same value that it returns. However, in this case, it's not needed, so we can instead pass NULL to the function. NULL is a special value in C which is a pointer to sometime in memory that is not a valid value. You have to be really careful when using NULL because if you ever try to write to NULL, your program will crash. However, NULL is useful here because time() will check to see if it is NULL, and then not write anything, and instead just return the time.
 time() returns the amount of seconds since the epoch (Midnight, January 1st, 1970), which means it increases every second. So as long as you never call time() more than once per second, it will always give you something different. Since it always gives something different, we can use the output of time() to seed our random number generator. We could do something like this:
time_t t;
t = time(NULL);
 This will work perfectly, but there is a much nicer way to write it which only takes one line:
 This usage could be confusing at first, but once you think about it, it is easy to understand. time() will be executed first can it will return the time in seconds, instead of storing this in a variable, it just gets passed directly to srand(). With a decent optimizer, this won't actually be faster, but it is nicer to look at.
 Now, all that we have to do is call srand() in our program before we generate our first random number and we will get something different each time.

Allowing the Player to Lose
 Essentially, what we are making is a little game, and a game is always more fun (and frustrating) if you don't win every time. So we might want to set a limit on the number of guesses a player can make before the game ends. If the player can't guess the number before running out of guesses, then they lose.
 To implement this, we can use a new type of loop, called a for loop . A for loop is similar to the while loop in that it is a loop and that it has a condition that is continuously checked to determine if the loop should continue. However, the for loop also has a few additional features.
 For loops have three parts, there is an initialization part, a condition, and a part that is executed after each loop. This contrasts the while loop, which only has a condition. The for loop uses the following form:
for (initializer; condition; modification)
 //loop body
 The the for loop is executed, the first thing that happens is the initializer is executed. Usually, this is used to initialize a loop counter, which is a variable that will count how many times the loop body has been executed. The condition is exactly the same as the while loop. It is an expression which is evaluated each loop, if it evaluates to true, the loop will continue, if it evaluates to false, the loop will stop being executed. Assuming the condition evaluates to true, the loop body will then be executed. After the loop body is executed, the modification will be executed. This is usually where we increment the loop counter variable.
 The for loop is not really special, everything you can do with a for loop could be done also with a while loop. In fact, you can rewrite a for loop as a while loop, like this:
while (condition)
 //loop body

modification; }
 A while loop could also be rewritten as a for loop by omitting the initializer and modification. The real benefit of the for loop is that it is much more concise and easy to read. Also, typically a for loop is used for an even more specific type of loop, a loop that is meant to be executed a specific number of times. For this type of loop, the initializer usually sets an integer variable to 0, and then adds one to the variable in the modification. That way, the variable (loop counter) always contains the number of the current loop iteration. In order to break the loop after a specific number or iterations, the condition is set to check if that variable is currently greater than the desired number of loops. For instance:
int i; for (i=0; i<10; i=i+1)
 In this example, the loop body (printf()) will be executed exactly 10 times. The output of this program would be a list of all numbers from 0 to 9.
 In our case, what we want to do is count the number of times the user has guessed, and then tell the user that they have lost when the limit is exceeded.

Putting it all together
 Now that we have learned a bunch of new things, let's put it all together and try to utilize these concepts in the program we wrote last time. You can try to do it on your own if you think you can. You can also take a look at the example below if you need further illustration.

Completed Program Source
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int getGuess()
 int guess;
 printf("Enter your guess:\n"); //Ask the user for a guess
 scanf("%d", &guess); //Read the user's input

 return guess; //Return the user's guess to the caller

int main(int argc,char **argv)
 int guess;
 int random;

 srand(time(NULL)); //Seed the RNG
 random = rand() % 100; //Generate Random
 int i;  for (i=0;i<10;i++) //Loop 10 times
  guess=getGuess(); //Ask the user to guess
  if (guess > random)
   printf("Too high!\n"); //Guess was too high
  if (guess < random)
   printf("Too low!\n"); //Guess was too low
  if (guess == random)
   //The user guessed correctly
   printf("Good job! It only took you %d tries!\n",i+1);
   return 0; //Exit the program

 return 0;

 Now let's do a quick quiz to see how much you remember.
Answer the following questions:

1. What is a line starting with "//"?
2. What is a "for loop" used for?
3. Why do we call srand()?
4. Where is the prototype for time()?
5. What does "time(NULL)" return?
6. If a function is defined as "int func(char a)", what is the int used for?
7. In the previous question, what is "char" used for?
8. In question 6, what is the name of the function I defined?
9. What would a function prototype look for for the function defined in question 6?
10. At the end of the example program, if the user fails to guess the correct number, what will the variable "i" equal?

That's all for now, hopefully there will be more in the near future!