CSCI 1300 Notes 5 Files and Character Strings

Purpose: Learn how your programs can read information from files rather than from the keyboard. Learn how to represent text using character strings.

Background

Files. In the programs you've written so far, if the user needs to supply information to the program when it's running, for example to specify a bus stop, he or she types it in when the program asks for it. That's OK for small amounts of information, but what should you do when there's a lot of information the user needs to supply? The answer is, put the information in a file and have your program read it from the file. A file is just a collection of information stored on disk.

Creating a file. You've been creating files each time you write a program: your C programs are stored as files. You can create a data file, that is, a file that contains information some program will read, in just the same way: use Notepad (or EMACS, or some other editor program) to type in the information you want in the file in just the same way you write a program. Just as you can make corrections in your programs whenever needed, so you can make any changes needed in your data file.

There is one thing you should do differently when you create a data file as opposed to a program: you should not use a name ending in ".c", as you do for programs, but some other ending (called an extension), say .DAT.

The contents of your data file can be typed in, or they can be copied and pasted from other source. Some of the files you'll need in these notes you will copy from this web page.

Making your program read information from a file. Once you have a file, your program can read from it in much the same way it reads from the keyboard. To read an int from the keyboard into an information holder called foo, you write this:

scanf("%d", &foo);

To read from a file, you'll use a similar statement:

fscanf(fp, "%d", &foo);

Notice that there's an extra ingredient here, fp. That's a pointer to information used to access the file, called a file pointer, and before you can do anything with a file you have to set up a file pointer. Here's how.

How to create a file pointer and connect it to your file. The declaration

FILE *fp;

declares fp to be a pointer to a thing of type FILE, which contains the information used to access a file. The type FILE is defined for you, and brought into your program when you put

#include <stdio.h>

in your program.

Now you have to connect up fp to the file you want to use. Let's say the file you want to connect it to is on your A: drive in a directory called MYFILES and is named SAMPLE.DAT. Then this statement will connect the stream foo to that file:

fp=fopen("A:\\MYFILES\\SAMPLE.DAT", "r");

(The double backslashes are in there because the compiler thinks a single backslash gives a special meaning to the following character, as in \n for newline. It treats a backslash following a backslash as just a backslash, which is what you want.)

The "r" in there stands for "read"; if you want to do something else to the file, like write to it, you put something else there (see the C reference site, http://www.acm.uiuc.edu/webmonkeys/book/c_guide/, for more on this if interested.)

"Open" is the term used to refer to connecting a program to a file. The process includes asking the operating system to find where the file is (and making sure it exists), and setting things up so that you start at the beginning of the file, and not somewhere in the middle, when you begin reading from it.

Testing whether fopen() worked. As you might guess, there is a good chance that fopen() will not work. You may have mistyped the file name, or you might have the wrong disk in the machine. It's a good idea to make your program check that everything is OK before proceeding. If fopen() does not work, it returns a special pointer value NULL, which is treated as false in a test. So you can write something like this:

if(!fp)

     ("Something went wrong!\n");

If fopen() did not work, it may not make sense to continue with your program. You can bail out from anywhere in your program by calling a special library function named exit(). You pass exit() an integer value; by convention values other than 0 mean something went wrong. So here would be a more complete response to fopen() not working:

if(!fp)

{

     printf("Something went wrong!\n");

     exit(1);

}

If you want to use exit() you have to put #include <stdlib.h> at the start of your program to declare it.

Reading from a file. Once you have fp connected to the right file you can use it to read using fscanf(), as shown before. So here's a little program that tries to read 10 integers from a file, and then prints them out. Note the two uses of GAPP.

#include <stdio.h> //for file and other i/o operations

#include <stdlib.h> //for exit()

int main()

{

     int numbers[10];

     int i;

     FILE *fp;

     fp=fopen("A:\\MYFILES\\SAMPLE.DAT","r");

     if(!fp)

     {

          printf("Something went wrong opening data file\n");

          exit(1);

     }

     for(i=0;i<10;i++)

          fscanf(fp,"%d",&numbers[i]);

     for(i=0;i<10;i++)

          printf("numbers[%d] is %d\n",i,numbers[i]);

     return 0;

}   

Running out of data and other problems. I said that example program "tries" to read 10 integers from the file, because whether it can or not depends on what is in the file. There might only be 7 integers in the file, or there could be plenty of integers, but there could be some invalid characters, that is, characters that can't be parts of integers, like letters or decimal points, mixed in. It's important to make your program check whether the data it is getting is valid, and whether there's enough of it.

To allow you to check these things, fscanf() returns an answer every time you use it. The answer it returns is an int, and its value is the number of data items it successfully read. In the example, each call to fscanf() should read just one item, so if all is well fscanf() will return 1 each time. If it returns 0 we'll now something went wrong. Here's a modification of the example program that includes a test for this:

#include <stdio.h> //for file and other i/o operations

#include <stdlib.h> //for exit()

int main()

{

     int numbers[10];

     int i;

     int fscanf_ans;

     FILE *fp;

     fp=fopen("A:\\MYFILES\\SAMPLE.DAT","r");

     if(!fp)

     {

          printf("Something went wrong opening data file\n");

          exit(1);

     }

     for(i=0;i<10;i++)

     {

          fscanf_ans=fscanf(fp,"%d",&numbers[i]);

          if (fscanf_ans != 1) //1 is normal

          {

              printf("something wrong in fscanf()\n");

              exit(2);

          }

     }

     for(i=0;i<10;i++)

          printf("numbers[%d] is %d\n",i,numbers[i]);

     return 0;

}

Constants. The use of 10 in this program is bad. We can see why it's there; when we declared the array numbers[] we only allowed room for 10 ints. The bad part is, if we changed our mind, and decided to allow room for 50 or 500 ints, not only would we have to change the declaration, we'd have to change our code in two other places as well. Do you see where? Making multiple changes like that is not only tedious but also error prone.

C allows us to define a constant with the value 10, and then use that constant anywhere in the program. If we need to change it, we only need to change the definition. Here's how:

#define MAX_NUMS 10

When the compiler sees this, it replaces any instances of MAX_NUMS it sees with 10. We can use any name we like, but there's a common convention that uses all caps to mark that something is defined using #define.

Here's how we take advantage of a constant to clean up our program:

#include <stdio.h> //for file and other i/o operations

#include <stdlib.h> //for exit()

#define MAX_NUMS 10

int main()

{

     int numbers[MAX_NUMS];

     int i;

     int fscanf_ans;

     FILE *fp;

     fp=fopen("A:\\MYFILES\\SAMPLE.DAT","r");

     if(!fp)

     {

          printf("Something went wrong opening data file\n");

          exit(1);

     }

     for(i=0;i<MAX_NUMS;i++)

     {

          fscanf_ans=fscanf(fp,"%d",&numbers[i]);

          if (fscanf_ans != 1) //1 is normal

          {

              printf("something wrong in fscanf()\n");

              exit(2);

          }

     }

     for(i=0;i<MAX_NUMS;i++)

          printf("numbers[%d] is %d\n",i,numbers[i]);

     return 0;

}

Notice in this version that if we wanted to change 10 to 20 we only have to make the change in one place. There's an old and important principle of programming that this illustrates:

eCODE ONE ASSUMPTION IN ONE PLACE

In this example, the "one assumption" is "we'll be processing 10 numbers". In the revised program, that assumption is written in one and only one place in the program.

Reading unknown amounts of data. This example program knows how many numbers it wants to read from the file: 10 is written into the program. But very often you don't know in advance how many things should be read from the file, because you want to read as many items as the user has put in the file... in fact, you'll need to do that in this exercise. To solve some of the problems you need to have your program check, each time it tries to read some data, whether it has come to the end of the file or not. You'll have the program keep reading data until it does come to the end, and then stop. How can you do this?

The key to doing this neatly is to write a function that does two things. First, it tries to read an item from the file, and if it can it does that. Then, it returns a value that signals whether it was able to do the read or not. Specifically, it will return 1 if the read worked and 0 if it did not. Let's call this function try_to_read(). Then our program can look like this:

while(try_to_read(...))

{

     //code to process what we read

}

The while loop will keep running as long as try_to_read(...) returns 1. As soon as it returns 0, the test in the while will fail, and the loop will stop.

Here is an example program fragment that reads ints from a file and prints them out, stopping when there are no more ints in the file(or when it encounters bad data in the file.)

int item;

while(try_to_read(fp,&item))

     printf("%d\n",item);

...

int try_to_read(FILE *fp, int *pitem)

{

     int fscanf_ans;

     fscanf_ans=fscanf(fp,"%d",pitem);

     if (fscanf_ans==1)

          return 1; //ok

     else return 0; //not ok

}

 

Stop here and see if you can complete this program (see Ex 1).

 

Reading structs from a file. Because the compiler has no way of knowing what your structs will be like before it sees your code, there are no predefined format codes like %d or %f for reading in your structs. So you have to break down reading a struct into reading the parts of the struct and putting them together. Now that you know about functions, you can bundle these operations up neatly in a function, so that it is as easy to read one of your structs as it is to read an int or a float. Here is an example, using a struct that represents a point:

 

struct point

{

      int x;

      int y;

};

int try_to_read_point(FILE *fp, struct point *ppt)

{

      int fscanf_ans;

      int anint;

      fscanf_ans=fscanf(fp,"%d",&anint); //try to read an int

      if(fscanf_ans!=1)

            return 0; //read didn't work

      (*ppt).x=anint; //put the int we read into the x part of what ppt points to

      fscanf_ans=fscanf(fp,"%d",&anint); //try to read another int

      if(fscanf_ans!=1)

            return 0; //read didn't work

      (*ppt).y=anint; //put the int we read into the y part of what ppt points to

      return 1; //if we get here both reads worked

}

Arrow notation for pointers to structs. As this example shows, it's common to have a pointer to a struct, ppt in this case, and then to want to refer to the members of the struct, as in (*ppt).x. This is so common that C has a shorthand for it:

ppt->x means the same thing as (*ppt).x

So we can rewrite the try_to_read_point()  function like this:

int try_to_read_point(FILE *fp, struct point *ppt)

{

      int fscanf_ans;

      int anint;

      fscanf_ans=fscanf(fp,"%d",&anint); //try to read an int

      if(fscanf_ans!=1)

            return 0; //read didn't work

      ppt->x=anint; //put the int we read into the x part of what ppt points to

      fscanf_ans=fscanf(fp,"%d",&anint); //try to read another int

      if(fscanf_ans!=1)

            return 0; //read didn't work

      ppt->y=anint; //put the int we read into the y part of what ppt points to

      return 1; //if we get here both reads worked

}

 

Reading directly into the parts of a struct. We don't really need to use an information holder like anint to read our ints into in this function. Instead, we can read directly into ppt->x and ppt->y like this:

 

int try_to_read_point(FILE *fp, struct point *ppt)

{

      int fscanf_ans;

      fscanf_ans=fscanf(fp,"%d",&(ppt->x)); //try to read an int into the x part of what ppt points to

      if(fscanf_ans!=1)

            return 0; //read didn't work

      fscanf_ans=fscanf(fp,"%d",&(ppt->y)); //try to read another int into the y part

      if(fscanf_ans!=1)

            return 0; //read didn't work

      return 1; //if we get here both reads worked

}

Character strings. Anytime you write something inside double quotes in your programs you're creating a character string. It's time to learn how to make a program manipulate these strings like other data, for example, by reading in a string, storing it somewhere, and printing it out or doing something else with it. For example, it would be nice in the example above to be able to prompt the user to type in a file name, rather than having to write the file name in when the program is prepared.

We've mentioned before, but seldom used, the fact that in addition to ints and floats, C has a data type char, for character. A char information holder takes up one byte, and can store one character, such as a letter, a digit, or a punctuation mark. A character string is just an array of characters, like this, say,

char s[100];

but it is managed in a special, clever way, that it's important to know about.

The thing about character strings is that you don't want to worry ahead of time about exactly how long they are. If someone is going to type in a file name, you don't want to tell them it has to be exactly so many characters long. Rather, you want to be able to deal with as many characters as they choose to type, up to some limit.

The C designers came up with a way to do this: they put a special marker character into every character string to mark its end.

 The idea is simple. If a string is empty, the marker character is sitting right in the first slot. It doesn't matter what's in the rest of the slots, because anybody processing the string stops when they see the marker. If the string contains the word eggplant, the first eight slots in the array contain the letters e, g, g, p, l, a, n, t, and then the next slot contains the marker character.

You can print out a character string using the format code %s in printf() like this:

char sample[100];

// some code here that puts something into sample

printf("%s",sample);

printf() knows about the marker character, and will only display the characters in sample until it gets to the marker.

You can read into a string like this:

scanf("%s", sample);

Note: Because sample is an array, its value is a pointer to itself. So you don't put &sample here.

The function scanf() will monitor what the user types. It will skip blanks or other "white space" (such as tabs or newlines) until the user types some non white space characters. It puts all of these into sample, until the user types some more white space. When it has read in what it is going to, it puts the marker character into sample just after what it has read.

Get the picture? Anybody who does anything with a string knows to ignore whatever may be in there past the marker character. Anybody who puts anything into a string knows they have to put the marker character in at the right spot.

Most of the time this goes on without your having to be aware of it. Besides printf() and scanf() there are a whole bunch of library functions that do all kinds of things with strings, and they all play the marker game. For example,

strcpy(s1,s2);

will copy string s2 into s1. This is handy, given that, since strings are just arrays of chars, you can't copy them just by doing an assignment. You can read about strcpy() and the rest of the String Library in at the C reference site http://www.acm.uiuc.edu/webmonkeys/book/c_guide/  . If you use any of them you have to put #include <string.h> in your program.

Reading the declarations of functions in the string library. The declaration of strcpy() is

char * strcpy(char *s, const char *ct);

You may wonder, where are the character strings? The answer is that the arguments s and ct are indeed arrays of characters. They are declared as pointers to char because the value of a char array is a pointer to a char. So an argument declared char * works just fine when you pass a char array. For some reason C-niks seem to prefer declaring array arguments as pointers rather than as (for example) char s[], which would be equivalent, and, (in my humble opinion) clearer.

Notice that strcpy() returns a pointer to a char. That's as close as you can come to returning an array, but it's not really that close. For example, you can't assign the result of strcpy() to an array you already have, as in

char foo[10];

foo=strcpy(s,t);

because the value of foo is a pointer to its own storage, and cannot be changed. Better to think of strcpy() as returning just a pointer, which is what is really happening anyway.

Finally, notice the "const" in the declaration. That signals that the second argument is not going to be changed by the function, so you can safely pass some precious string that you don't want tampered with into strcpy() as the second argument.

Comparing character strings. A library routine you will definitely want to use is strcmp(): it allows you to compare strings. If s and t are strings, and you want to know whether they are equal or not, you can't just write if(s==t) (can you see why not?) Instead, you have to write if(strcmp(s,t)==0). A tricky point here is that strcmp(s,t) returns 0 (which is interpreted as false) when the strings are equal: that's because it returns -1 when s is alphabetically earlier than t and +1 when s is alphabetically later than t. So 0 means s and t are equal.

A pesky point to remember about declaring strings. Even though the marker game is usually played for you, you do have to remember one thing. If you want a string to be able hold, say, 80 characters, you have to declare it with room for 81. That's because you have to leave room for the marker!

For example, if you declare

char label[8];

you won't be able to store "eggplant" in there!

Single characters vs character strings. What can you do with an information holder declared like this?

char c;

It's not a string, because it's not an array. It's just a char, and you can assign to it in the same way you can to an int or float. In fact, it works just like a very short integer, and that's because the way characters are represented inside the computer is by using small integers called character codes. You can specify a character code just by writing whatever character you want inside single quotes, like this:

c='g';

If you use double quotes you are specifying not a single character code, but a character string. For example,

"g"

is really a string containing g and then the marker character, so it is an array with 2 slots (the compiler sets all this up automatically.) So you can't write

c="g";

because you can't assign from an array.

You can print a single character, or read one, using the %c format tag with printf() or scanf().

What is the marker character? You may be wondering what character could be used as a marker character. After all, whatever it is obviously can't be used in a string. As it happens, when the ASCII character code system was created, the code 0 was not used, and that's what is used as the marker.

Note that the character code 0 is not the code for the character '0', which is some ordinary positive number (you can find out what it is by assigning '0' to an int variable and printing it out, or by finding a table of the ASCII codes.)

Sometimes you want to be able to refer to the character code 0, and obviously '0' isn't going to be right at all. The compiler recognizes the special notation'\0' as the character code 0, and lets you use it in places like this:

if (c=='\0')

Example: reading in a file name. Here's how to use a character string to read in a file name. Instead of

FILE *fp;

fp=fopen("A:\\WIDGETS.DAT","r");

in the earlier example, we'll have

FILE *fp;

char filename[100]; //allow for a longish name...

     //user won't be able to overflow this (unless

     //using a tiny font)because scanf() will stop at the newline

printf("Type in a file name: "); //prompt

scanf("%s",filename); //if this does overflow we are in trouble...

     //really should use %99s... see ref site on field width

fp=fopen(filename,"r");

A smarter program will use a loop here, continuing to prompt for a filename if the one typed in couldn't be opened successfully. See if you can see how to use do... while for this (see the C reference site)... it's a better fit than while because you don't want to test until you've tried opening, and the test in a while loop is always executed before the body.

Processing the individual characters in a string. Sometimes you want to work your way through a string, doing something with each character. You’d like to use a GAPP to do this, but what will you use for the length? There’s a useful function in the string library that works here, strlen() (strlen is short for “string length”). Here’s how to use it in a GAPP:

char s[100];

int i;

for(i=0;i<strlen(s);i++)

     if (s[i]!=’g’)

          printf(“%c”,s[i]);

This code will print all the characters in s except for any g’s. Note that strlen tells you how many actual characters there are in s, not including the zero marker, which is what you want here.

Exercises

I urge you to work with one or more classmates on these exercises!

Ex 5-1. Complete the fragment in the section on "Reading unknown amounts of data" and test it out. To do this you will need to create file with some ints in it, and show that your program will print them out, regardless of how many ints there are in the file. In your submission show a listing of your program, a listing of your test file, and a screen snapshot showing (a) the compilation (with no warnings) and (b) the results of a test run.

Ex5-2. What does the library function strcat() do? Submit a short test program that demonstrates it. (You'll need to read about strcat() on the reference site or in a book to do this.)

Ex 5-3. Write a program that asks the user to type in a word, and then prints out the word with all e's replaced by a's, and all a's replaced by e's. For example, if the user types the word eggplant the program should display aggplent. In your submission show a listing of your program, and a screen snapshot showing (a) the compilation (with no warnings) and (b) the results of a test run.

Ex 5-4. Modify your program for Ex 5-1 so that it prompts the user to type in the name of the file it should use. In your submission show a listing of your program, a listing of your test file, and a screen snapshot showing (a) the compilation (with no warnings) and (b) the results of a test run.

Ex5-5 Write a program that reads a file like this, where each 4 ints represents the coordinates of the ends of a line, and draws the lines.

10 10  100 100

100 100 100 200

200 200 10 10

Although it would be possible to do this without using structs or functions other than main(), for this exercise you must do the following:

(a) define a struct like this:

struct line

{

     int x1;

     int y1;

     int x2;

     int y2;

};

(b) write and use a function try_to_read_line() similar to the one in the example in the notes that reads in points

(c) write and use a function draw_line() that takes a struct line as argument and uses the library function line() to draw the line

Also, have your program prompt the user for the name of the file it should read. Be sure your program does not assume it knows ahead of time how many lines are specified in the file.

Create a file that defines some nice shape.

In your submission show a listing of your program, a listing of your test file, and a screen snapshot showing (a) the compilation (with no warnings) and (b) the results of a test run.

Be sure everyone on your team turns in their own submission, including time log.

If you have more time, see what you can find out about the following issues:

Opt1 What is UNICODE? What’s the purpose of it? How does it work?

Opt2 Investigate the issues in representing text in an alphabet like Arabic or Hebrew.

Opt3 How is Chinese text processed by computer? How do you type it?