Journal Articles
Browse in : |
All
> Journals
> CVu
> 122
(18)
All > Topics > Programming (877) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Reading Integers
Author: Administrator
Date: 03 March 2000 13:15:35 +00:00 or Fri, 03 March 2000 13:15:35 +00:00
Summary:
Body:
I suspect that the third topic mentioned on most programming courses, irrespective of the language being taught, is reading in numbers. (The first, of course, is to write "Hello, world.", and the second is to write out a number.) And because of this, the topic is never re-visited later in the course when a greater knowledge of the language has been obtained and so the problems of reading numbers are better understood.
So in this piece I am going to look at certain aspects of reading numbers (or at least, the aspect that brought this topic to mind) and suggest that there is a better approach to fetching user responses than always calling scanf.
I suppose the first way one is told to read a number always looks like
#include <stdio.h> int i; scanf ("%i", &i);
and because the student has a lot to cope with even to get this to work, he may be told "Don't worry how it does it, but it does read a whole number from your terminal into 'i'."
But we've got a little further into the programming course and so know better. scanf reads characters from the file called stdin. This is connected, or opened, by default, such that it receives the characters typed by the user at the keyboard. So this function provides a way of getting numbers into one's program. It does this in the way directed by the first parameter, which is called the format and must contain zero, or more directives. Now a directive can be a conversion specifier that says how something is to be read. Other directives say how to discard characters so as to reach those we actually want.
The format used here is the string literal "%i". A character array would also do if correctly set up. The format "%i" contains a single conversion specifier instructing scanf to read an integer. And as the conversion specifiers are matched up with the subsequent arguments to scanf, the value of the integer read will be written into the integer i.
scanf returns an integer value which is either that of the macro EOF, which indicates that the end of file has been found, or the number of input items which were assigned. That is, it says how many numbers were successfully read in.
This return value is ignored here as the example was intended as a 'get you going' measure for a first lesson in programming. Now that I know better, I would check its value. In addition to EOF and the value 1, I would also be prepared for a return value of 0 which means nothing has been assigned, and something has gone wrong.
And finally, the standard header <stdio.h> is included to provide a prototype for scanf, which allows the compiler to do some limited checking that the function is being used as its author intended.
Let me continue with the format argument, "%i". What do you expect scanf to make of the following strings given this format?
10 0xa 012
In his haste to complete his notes, the tutor writing this example saw that the conversion specifier 'i' read an integer, but didn't appreciate that the subsequent mention of "the strtol function with a value of 0 for its 'base' argument" meant that the number base would be deduced from the first few characters of the number.
So all three of the examples are accepted in their entirety, and return the decimal value 10. I suspect that the hapless lecturer meant to use the specifier "%d" to force a decimal integer to be read. Then the values returned would be 10, 0 and 12 decimal, respectively. And in the second example, the characters "xa" would remain to be read by the next format specifier.
I would also like something like:
printf ("Please enter 'i': ");
to appear immediately before the use of scanf just to remind the user what is expected of him. Perhaps I am a little unfair here. After all, this is an example from a first lesson. Hopefully, the student will appreciate such niceties later.
Let me say a little more about the precise way scanf (and fscanf and sscanf) reads numbers. I shall continue using the "%i" format. scanf has the prototype
int scanf (char const *format,...)
provided in
#include <stdio.h>
scanf starts by reading from stdin, and discarding, all white space characters it sees. 'White space' characters are defined by what the isspace function does, but this generally means skipping the end of line marker from the previous line, then all spaces and tab characters until a 'visible character' is found.
Having found the first non-white space character, an integer is then built, so that character had better be a digit. Because a field width has not been specified, as many digits as possible are used to build that integer. As reading proceeds, the base of the number is sorted out. Reading stops when either reading a further digit would cause the number to become too big to be held in an int , or a non-digit is found (see editor's notes at end).
The number is returned in the variable pointed to by the next argument to scanf, and the number of items assigned is incremented. (Actually, it is set to one, as this is the first conversion specifier.)
The file stdin is left pointing to the character which is next to be read. In this example, this is probably the carriage return or line feed at the end of the line.
And here we start to see the first serious problem with scanf. If the user typed more digits than could reasonably be used in an int, the next call to scanf will build an integer from those remaining characters, rather than starting off by reading a new line. And the user will wonder why his program didn't wait for him to type in a number before continuing, or claim that he typed in one number and the machine stored something completely different.
This sort of problem can prove very baffling, especially if the input is being read direct from a disk file, rather than the terminal, so the user does not notice the machine not waiting for him to finish his line.
It is to avoid these problems that the more experienced programmer uses fgets to read a string from the terminal and then seeks methods other than sscanf to extract the number it contains. fgets has the prototype
#include <stdio.h> char *fgets (char *s, int n, FILE *stream);
one might be drawn to the somewhat similar function 'gets', which has the prototype
#include <stdio.h> char *gets (char *s);
But gets must never be used (unless you really understand what you are doing, and very rarely then), because although it reads directly from stdin, and does not leave the new line character, '\n', at the end of the line, it does not limit the number of characters read from the line. So typing too many characters into the array given to gets for the parameter 's' will result in something being overwritten and, at best, the program crashing immediately.
fgets avoids this by having an extra parameter, 'n', which places a limit on the number of characters read from the nominated file. Of course, if n is too small, you get the source line in several pieces, but this is usually avoided by making n (and the size of the character array, s) reasonably large, and ensuring that the returned string does indeed end with the new line character.
The line read in is then converted to an integer by using the function strtol This has the prototype
#include <stdlib.h> long int strtol (char const *nptr, char **endptr, int base);
A check can be made that all digits have been read from the line because strtol returns a pointer, endptr, to the character that caused it to terminate. If this is not the '\n' that fgets left at the end of the string it returned, then something has gone wrong.
So to read a number, we now have something like:
#include <stdio.h> #include <stdlib.h> #define LINELEN 150 char line[LINELEN]; char *next; int i; fputs ("Please enter lil: 11, stdout); if (fgets (line, LINELEN, stdin) == NULL) { perror ("'fgets' found an error"); exit (EXIT_FAILURE); } i = (int) strtol (line, &next, 10); if (*next 1= l\n') { fputs ( "'strtol' failed to convert all the line", stdout); fputs (line, stdout); exit (EXIT_FAILURE); }
Probably the only unfamiliar function in this example is perror, which prints the string given as its parameter together with a message identifying the most recent system error. In this case, it will explain why fgets returned NULL: perhaps it found end of file instead.
The next improvement I would make is, having read all the input I wanted from the user to write it all out in some readily comprehensible format. When (not if!) something goes wrong, this allows the user to see what the computer thought he said and compare it with what he claims he said (or actually said). Those who have had to sort out a colleague's incomprehensible program and get it to actually work will appreciate the subtleties of that sentence!
I might format that output so that, with only minimal editing, it could be fed directly back into the program to reproduce the original output.
In any event, the input to a program provides a useful document describing what that program was asked to do.
We can still improve on this. Why is that number being read in? What is going to be done to it? The answers to those two questions suggest that there are limits, or bounds, to legal values of that number which are probably significantly less than the limits of the type int. So why not check them? And if the entered number exceeds those bounds, print an error message and try again.
But don't do so endlessly. several (say five) consecutive failures suggests that the user is having difficulties with the program. Or in my case, I am reading the requested numbers from a command file (a text file written before the job starts and from which the program reads the input it would otherwise read from the terminal) and the list of answers I prepared when submitting the job has got out of step with my program's questions.
So stop the program to allow (or force) the user to re-think what he is doing before things go irretrievably wrong.
A final touch is to add a comment character. This is one which, when encountered, causes the remainder of the line to be ignored. As usual with comments, they do not have to be used (and I would not do so in this case when entering input directly from a terminal). But as most of my programs are run from command files, they provide a useful way of sorting out the inevitable mismatches between my dreams and reality.
We now have some relatively complex functionality that really should not be coded inline in your program, but appear as a function in its own right. Indeed, the previous example should probably be a function as well.
Arguments that we have made the task of reading a number far too complex, or are suffering from 'code bloat' and writing unnecessarily large programs are not valid. We are using the computer to protect ourselves from our own mistakes. We are separating out an idea into a function of its own and we can reuse the function in this, and our future programs, as we would presently use scanf.
My code for the function to do all this is in Listing 1. And I should perhaps say that this is in K&R C, rather than ISO C, for code and compiler legacy reasons. But you should have no difficulty converting this to the ISO standard.
Now we see that the fictional lecturer's opening 3-line example has expanded to 77 lines, which is the precise reason why he did not want to do this in the first place. But he could have given his class a library with this function in it, and told them about this, rather than scanf. As existing code, it would set an example of a desirable style, whilst, through the 'lower' and 'upper' limits,' forcing the students to think about what reasonable ranges for their numbers would be.
Given my remarks above, and the comments in the Listing, I think (hope) that what is going on is clear, but a few more remarks may help.
First, my get_int function does not print out the value it will return as I regard that as the user's prerogative.
Second, the comment character is implemented by using the standard library function strchr to search the line read from the terminal for the comment character, which is defined by the macro COMMENT. If found, it is replaced by the new line and end of string characters.
Range checking the number entered by the user requires a longer comment. It is reasonable to expect the user to want to return a value that is anywhere within the bounds of the legal values allowed for an int on the host system. But the range test must be made using only int arithmetic. For one thing, the compiler may support a subset of C that has only int. So we have to allow both lower and upper to be legitimate data values and equality of i to either of these must be permitted, otherwise, there is a legal positive, and a legal negative, integer which this function could never allow to be entered.
And if you do want to read in the largest possible positive (or negative) integer, the symbols to use for 'upper' (or 'lower') are INT_MAX (or INT_MIN) which are defined in the standard header <limits.h>.
Having written any function, we should ask how to test it, and my get_int is no exception. Listing 2 provides a short program that uses get_int to read a value and then fprintf to write it out. This provides a simple method of checking what get_int does.
Deliberately writing a program with an endless loop, and then requiring the user to force terminate the run with the Break key (or however else his operating system kills errant programs) is a little user unfriendly, but justified here because of the costs, and negligible benefits, of doing something more humane.
There are still a number of things that need doing to get_int. And these, I am going to leave to you. When complete, they will provide a library of functions to help your programs read in numbers safely.
What changes are needed to read in numbers of types short int and long int? And what about the unsigned variants of these, as well as unsigned int?
Then, how should the function be modified to read the floating point types, float, double and long double (together with any others your compiler allows)?
How should a default value be introduced, which is returned if the user enters a zero length line, or one containing only a comment? What if the user does not want a default value, but your function provides for one?
I spend a lot of time plotting graphs, so I have a variant of this function which reads two numbers (the x- and y-co-ordinates of the point to plot) from each line. And it treats there being only one number on a line as an error. How would you write this? Both integer and floating point versions are wanted.
Finally, and as a rather hard exercise which I don't know how to do: how should get_int be modified to work in a windows' style programming environment? I await your suggestions in a future issue of C Vu.
Editor's notes:
This article was provided in typed form, scanned and converted with OmniPage Pro 8.0. I think that I have manually corrected all conversion errors but source code in particular is notoriously difficult to scan correctly. Typos are most likely to be the result of this process.
Unfortunately I cannot easily contact the author so I cannot raise the issue of what I believe is an erroneous understanding of how strol works. I do not think that this function terminates when the number is too large to store in a long. My understanding is that it takes all input till the first non-digit, regardless.
There are other points that readers might wish to comment on. The following are a few ideas.
I have left the author's code in K&R C because even today, C programnmers need to be able to read legacy code written this way.
I think there is probably a need for an article (or more) on the differences between K&R and ISO C and how to convert legacy code to ISO C) aimed at those that have recently learnt C. I hope someone will take up the challenge.
I wonder if any reader would like to comment on why some programmers use single character strings where a char would seem more appropriate. In last issue's student code we had %s used to output single spaces (actually in that case there was no need for a format specifier). In Posul's code he over-writes his comment character by using strcpy. Can anyone tell me why he did not use a simple assignment such as:
*com_posn = '\n';
Notes:
More fields may be available via dynamicdata ..