|
~ Introduction to Programming: C ~ Session 9 - Structures & Files |
In this session, we will be look at how to group sets of related information (e.g. a name, address and telephone number) together, and how to use arrays to store a list of such information – known as a database of information
We will also be looking at how to store this information to disk, so that we can keep a permanent record, and recall it again to make additions, alterations, and deletion to the data.
A structure is a way of grouping a number of variables together to form a new type of information. For example, we might store co-ordinates of a character position on the screen using two variables – e.g. column and row as follows:-
int col, row;
If we wanted to store several of these then we may do so in a few ways:-
int
previous_col, previous_row, current_col, current_row, next_col, next_row;
or
int col[3],row[3];
Really, the column and row are part of a single entity – a co-ordinate. So wouldn't it make more sense to group them together and give them a name?
To do this, we use the struct command:-
struct
coordinate {
int row,col;
};
The command starts struct followed by the name to give the structure. The variables to be grouped together are listed after the name between curly braces. Remember to terminate the whole thing with a semi-colon.
Note that a structure is not a variable itself. It is a type, in the same way as an integer and a character are types.
This means, we can have lots of variables that are co-ordinates – e.g.
struct
coordinate previous, current, next;
struct coordinate coords[3];
To access the co-ordinates, you use dot-notation as follows:-
previous.col
= 5; previous.row = 6;
current.col=previous.col; current.row= previous.row;
next = previous;
Note that you can assign one structure to another to copy the entire contents to another variable, or you can use each of the values in the structure individually.
Try the following program to test these statements out :-
#include
<stdio.h>
#include
<conio.h>
void
main() {
struct coordinate {
int row,col;
};
struct coordinate previous, current, next;
previous.col = 5; previous.row = 6;
current.col=previous.col; current.row=
previous.row;
next = previous;
printf("previous.col = %d, previous.row = %d\n",
previous.col, previous.row);
printf("current.col = %d, current.row = %d\n",
current.col, current.row);
printf("next.col = %d, next.row = %d\n",
next.col, next.row);
getch();
}
You should get the same results of 5 and 6 back for all of variables previous, current and next.
Note that there is a shorthand way of declaring the structure, and any variables using the structure, at the same time. Simply list the variable names after the closing curly brace but before the terminating semi-colon as follows: -
struct coordinate {
int row,col;
} previous, current, next;
The use of structures becomes even more useful when we want to store lists of data – e.g. a list of names, addresses, dates of birth and telephone numbers to make up an electronic address book, or a list of dates, start times, end times, and descriptions to make up an electronic diary.
Let's take the electronic address book as an example. Our structure might look something like this: -
struct address {
char name[31],address[301],telephone[31];
int birth_day,birth_month,birth_year;
};
This gives us a name of up to 30 characters (plus 1 for the terminating zero), an address of up to 300 characters, and a telephone number of up to 31 characters – although it is a number, it could contain spaces and parentheses, so we make it a character string. Finally, the day month and year of birth are given as individual integers.
Our variable that uses this structure might be: -
struct address address_book[100];
This would give us an address book of (potentially) 100 addresses.
As the program is only likely to contain one address book, we may as well combine the two statements together so that both the structure, and the variable using the structure, are declared at the same time: -
struct address {
char name[30],address[300],telephone[30];
int birth_day,birth_month,birth_year;
} address_book[100];
You may wonder what the point of having a name for the address book type (address) when it is only being used once. This would be a good point, and if you are declaring the variable at the same time as the structure, you can indeed choose to omit the type name as follows, and just use the variable name(s): -
struct {
char name[30],address[300],telephone[30];
int birth_day,birth_month,birth_year;
} address_book[100];
Define a structure for a diary program, calling the structure diary_struct and the diary variable itself diary, allowing up to 300 diary entries. Make the date up of three integers called day, month and year. Make the start and end times up of integers too, called start_hours, start_minutes, end_hours and end_minutes. The final variable to include is a description string of the event, called description which can be up to 200 characters in length (add 1 for the terminating zero-character).
Did you notice that our minutes and hours are duplicates for start and end? Define another structure before diary_struct called time_struct and use this within the diary_struct, creating two variables using the time_struct of names event_start and event_end.
The
solution is at the back of the session, if you wish to check your answers.
It also includes a further example on populating (putting data into) the
diary.
Files are named items of data that are typically held on a disk such as a floppy disk or a hard disk.
Files can contain any type of data. Typically, in order to help identify the type of data in the file, the name had a dot followed by up to three letters – for example, files ending with .doc are typically Microsoft Word documents, files ending with .txt are typically text files, files ending with .pas are typically Pascal programs, and files ending with .c or .cpp are typically C or C++ programs.
Unlike programs and data that are stored in your computer's memory, information stored in files stay the same even after you turn the computer's power supply off.
You can store data in a disk file using your C program in order to keep information between runs of your program, even if the computer has been switched off between times. This is known as persistent storage, whereas data and programs stored in your computer's memory in order to use them are lost when the power is switched off, and are held in what is known as volatile storage – typically Random Access Memory.
To 'open' a file (i.e. use it), we make use of the fopen statement, which is located in the stdio.h library header file.
For example, to create a new file (or overwrite an existing one) of the name address.dat, we would use the following: -
FILE
*addr_file;
addr_file
= fopen("address.dat","wt");
When creating a file, you need to declare a FILE pointer variable. This stores the address in memory that holds information about the file you are opening. You will need this to read from or write to the file, as well as close it again when you have finished with it.
On most computer systems, when you open a file, you lock the file, so that nobody else can get access to it while you write your data. This prevents people reading incorrect data, or writing data at the same time as you are trying to do so. In order to tell users that they can access the file again (including other programs on your computer), you must unlock the file by closing it. To do this, use the fclose statement as follows:-
fclose(addr_file);
If the computer was unable to open the file (e.g. because it was locked by another user), then the addr_file gets set to a special value called NULL. It is therefore good practice to check whether the file has been opened before doing any reading or writing – e.g.
addr_file
= fopen("address.dat","wt");
if
(addr_file == NULL) printf("Error!");
else
{ /* Processing */ fclose(addr_file);
}
When you open a file, the first parameter for the fopen statement is the name of the file. If it is held in a particular directory, you will need to include this in the name – e.g. if it was held in the c:\temp directory, then the name might be "c:\temp\address.dat".
The second parameter specifies what you are going to do with the data, and the type of data being written. Use the letter r to read from an existing file, w to write to a new file or overwrite an existing file, or a to append to (add to the end of) an existing file. You can also add the letter t to indicate that this is to be a text file (i.e. you could open it up using a text editor such as notepad and recognize the data as ASCII characters) – you might use this for log files (files that list all the important actions that have taken place in a program, so that you can later trace back what has happened if something unexpected happens). Alternatively, you could add the letter b to indicate binary data – i.e. data that may be more compact in storage, but may be unreadable in text editor programs. This is fine if you're not bothered about the data being unreadable outside of your program – e.g. data files.
There are a number of commands you can use to read from and write to the file. Most have oddities which mean that you should be careful in which circumstance to use them.
fscanf is used like scanf, except it reads from a file. Similarly, fprintf is used like printf, except it writes to a file.
Here is a small program demonstrating their use:-
#include
<stdio.h>
#include
<stdlib.h>
#include
<conio.h>
#include
<string.h>
void
main() {
FILE *out = fopen("c:\\temp\\test.txt","wt");
int age; char name[31];
if (out==NULL) exit(0);
printf("Enter your age: "); scanf("%d",&age);
fprintf(out,"Your age is: %d\n",age);
printf("Enter your name: "); scanf("%s",name);
fprintf(out,"Your name is: %s\n",name);
fclose(out);
age = 0; strcpy(name,"");
printf("Age is now %d and name is '%s'\n",age,name);
FILE *in = fopen("c:\\temp\\test.txt","rt");
if (in==NULL) exit(0);
fscanf(in,"Your age is: %d\n",&age);
fscanf(in,"Your name is: %s\n",name);
printf("Opened: age is %d and name is '%s'.\nPress a key. ",
age, name);
fclose(in);
getch();
}
Run the above program, and put your age in years and your first name only. Notice that the values are blanked before they are read to prove they are being read back from the file properly.
Run the program again, but this name include your first name, followed by a space, followed by your surname. Notice that the data you get back only shows your first name.
This is because scanf, sscanf and fscanf all treat spaces as ends of strings (as well as new lines). Thus, the string is read in until the first space after your first name is read. In order to get two words of information, you would need to use two variables as follows: -
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
void main() {
FILE
*out = fopen("c:\\temp\\test.txt","wt");
int
age; char name[31], name2[31];
if
(out==NULL) exit(0);
printf("Enter
your age: "); scanf("%d",&age);
fprintf(out,"Your
age is: %d\n",age);
printf("Enter
your name: "); scanf("%s%s",name,name2);
fprintf(out,"Your
name is: %s %s\n",name,name2);
fclose(out);
age
= 0; strcpy(name,""); strcpy(name2,"");
printf("Age
is now %d and name is '%s %s'\n",age,name,name2);
FILE
*in = fopen("c:\\temp\\test.txt","rt");
if
(in==NULL) exit(0);
fscanf(in,"Your
age is: %d\n",&age);
fscanf(in,"Your
name is: %s %s\n",name,name2);
printf("After
open, age is %d and name is '%s %s'.\nPress a key. ",
age, name, name2);
fclose(in);
getch();
}
This is obviously not ideal. What if we want to input either one or two words in different circumstances? Some people may want to put just their first name, some both firstname and surname. We need something that does not try to split a string using spaces.
fputs can be used to write a string out to a file with a terminating new-line (\n) character. For example:- fputs("String to be stored",outfile); where outfile is of type *FILE and was used to open the file.
fgets can be used to read a string from a file that was previously written using fputs. It reads up to a terminating new-line (\n) character. For example:- fgets(mystr,30,infile); where mystr is defined as char mystr[31]; and infile is defined as FILE *infile; - the 30 determines the maximum length of string that can be read in – 30 characters in this case.
Similarly, you can use puts("My string\n"); to put a string to the screen (console), and gets(mystr); to get a string (spaces and all) from the keyboard into a string.
Note that if you do use gets, you may like to call flushall() first to ensure that gets() doesn't use any buffered keyboard input from any previous input statements.
The following example simplifies the above program so that only one string is needed to store the name – you can enter either a one-word or two-word name: -
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
void main() {
FILE
*out = fopen("c:\\temp\\test.txt","wt");
int
age; char name[31];
if
(out==NULL) exit(0);
printf("Enter
your age: "); scanf("%d",&age);
fprintf(out,"Your
age is: %d\n",age);
printf("Enter
your name: "); flushall(); gets(name);
fputs(name,out);
fclose(out);
age
= 0; strcpy(name,"");
printf("Age
is now %d and name is '%s'\n",age,name);
FILE
*in = fopen("c:\\temp\\test.txt","rt");
if
(in==NULL) exit(0);
fscanf(in,"Your
age is: %d\n",&age);
fgets(name,30,in);
printf("After
open, age is %d and name is '%s'.\nPress a key. ",
age, name);
fclose(in);
getch();
}
Make the changes to the program as indicated in bold above (remember to remove the references to the name2 variable. Run the program, and try it for both your first name only, and then for first and surname. This should now operate correctly.
Change the program to prompt for a telephone number, write it away, clear it, and recall and display it from the file.
If you wish to store and recall a large chunk of data – for example an entire array, or a structure variable, or a combination of both – you may wish to use the fread and fwrite commands.
For example, later on is a complete listing of an address book program, In it are defined a structure called address that holds the name, address, telephone number and date of birth. There is also an array of 100 addresses called address_book. The number of addresses currently filled in is held in a variable called address_count.
These declarations are listed below:-
struct
address {
char name[30],address[300],telephone[30];
int birth_day,birth_month,birth_year;
} address_book[100];
int
address_count = 0;
A function is declared to save the data in the array to disk. This is listed below: -
void save_data() {
FILE
*address_file;
address_file
= fopen("c:\\temp\\address.dat","wb");
if
(address_file != NULL) {
fprintf(address_file,"Total Records: %d\n",address_count);
fwrite(address_book, sizeof(address), address_count, address_file);
fclose(address_file);
}
}
Thus, the total number of records is written to the file using an fprintf statement, and then the address_book array is written using the fwrite statement.
The sizeof(address) parameter returns the amount of physical space in memory that the address structure takes up, the address_count variable gives the number of address records in the array to write, and the address_file is the file to which the data should be written. Thus, the computer just takes the starting address of the address book, multiplies the size of the address by the number of records being written, and writes that number of bytes to the file.
Similarly, to read the data back again, the following function is defined: -
void load_data() {
FILE
*address_file;
address_file
= fopen("c:\\temp\\address.dat","rb");
if
(address_file != NULL) {
fscanf(address_file,"Total Records: %d\n",&address_count);
fread (address_book, sizeof(address), address_count, address_file);
fclose(address_file);
}
}
This works similarly, except the file is opened for reading "r". The number of records is read using an fscanf command, and from this, the size of the data is known.
Thus, the fread command reads from the file to the memory starting at the beginning of the address_book array, with each record being sizeof(address) in length, and then number of records read in being address_count taken from the fscanf value read in previously, and finally the address_file is the input file from which the data is being read.
Thus, the entire array is written out and read in one easy step.
You don't always know in advance how long a file is going to be. Take for example, the example where you are reading in a text file created by another program. You are going to read in the file, and write it out to another file as UPPER CASE text.
The problem is that it is difficult to predict how long a free-text file like this will be. What we can do is attempt to read data, and check to see if there is any more to read from a file using the feof() function. Just pass the file handle variable (type FILE *) to the function, and a value of true is returned...
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
#include <ctype.h>
void main() {
char text[81];
FILE *in =
fopen("c:\\temp\\input.txt","rt");
if (in==NULL) exit(0);
FILE *out = fopen("c:\\temp\\output.txt","wt");
if (out==NULL) { fclose(in); exit(0); }
while (!feof(in)) {
fgets(text,80,in);
for(int i=0;i<strlen(text);i++) text[i]=toupper(text[i]);
fputs(text,out);
}
fclose(out); fclose(in);
printf("Conversion done. Check c:\\temp\\output.txt\n");
getch();
}
Type out the listing above.
Create a new file by opening notepad (click on Windows Desktop Start menu, select Run... and type notepad in the text box and click on the OK button. Enter some text – mix upper and lower case and put in a few symbols such as exclamation marks for good measure – here's an example:-
Hello
there everybody
The
quick brown foxes jumped over the lazy dogs.
Aardvarks!!!
Save the file using the filename c:\temp\input.txt
Now run the program. When it has completed, press a key.
Now
open a new file from notepad – c:\temp\output.txt and compare it to
your original text. It should be the same, but all in capital letters. Magic!
HELLO
THERE EVERYBODY
THE
QUICK BROWN FOXES JUMPED OVER THE LAZY DOGS.
AARDVARKS!!!
Try altering the program so that it writes each line backwards. Hint: You will need two strings – one for the input, one for the output. Your loop will look something like this:-
for (int i=strlen(text)-1; i>=0;i--) ....
As a longer example, which is about the difficulty level you could expect in an exam question, try the following program, which reads a fixed data file called c:\temp\address.dat, and lets the user add to, change, delete from, and browse an address book. When the user quits, the data is saved back to the file, ready for the next time.
Using the fwrite and fread commands means that the entire array of records is written/read in a single line of code!
Also note, that the most efficient coding style has not always been adopted for the sake of clarity of style.
/* Address book program
Written by (c) 2002-4 Simon Huggins
for C Programming Course
Demonstrates use of structures, arrays and files
*/
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <ctype.h>
#include <string.h>
/* Prototypes */
void load_data();
void save_data();
void goto_record();
void enter_details(char edit);
void delete_record();
/* Constants */
int const max_recs = 100;
/* Note we need to use double-\\ as \ is an
escape character */
char const data_file_name[100] =
"c:\\temp\\address.dat";
/* Variables */
struct
address {
char name[30],address[300],telephone[30];
int birth_day,birth_month,birth_year;
} address_book[max_recs];
int
address_count = 0, current_record = 0;
/* Main Loop */
void main() {
char
reply;
load_data();
/*
loop until quit chosen */
do
{
clrscr();
printf("Address Book\n------------\n\n");
/* If any records, display current record */
if (address_count > 0) {
printf("Record %3d/%3d\n==============\n\n",
current_record+1, address_count);
printf(" Name:
%s\n", address_book[current_record].name);
printf(" Address:
%s\n", address_book[current_record].address);
printf(" Telephone: %s\n",
address_book[current_record].telephone);
printf("Birth Date: %02d/%02d/%d\n",
address_book[current_record].birth_day,
address_book[current_record].birth_month,
address_book[current_record].birth_year);
}
printf("\n\nEnter a Letter: ");
printf("[S]et, [B]ack, [F]orward, [A]dd, [E]dit, [D]elete, [Q]uit:
");
/* Using getchar means that flushing the buffer works better later
*/
reply = (char)(toupper(getchar())); printf("\n\n",reply);
/* Act on the chosen option */
switch(reply) {
case('S') : goto_record(); break;
case('B') : if (current_record > 0) current_record--; break;
case('F') : if (current_record < (address_count-1)) current_record++;
break;
case('E') : if (address_count == 0) break; /* Can't edit nothing! */
case('A') : enter_details(reply); break;
case('D') : if (address_count > 0) delete_record();
}
}
while (reply != 'Q');
save_data();
}
/* Opens a binary data file, reads number
of records, and then reads in
whole array in one go using fread */
void load_data() {
FILE
*address_file;
address_file
= fopen(data_file_name,"rb");
if
(address_file != NULL) {
fscanf(address_file,"Total Records: %d\n",&address_count);
fread (address_book, sizeof(address), address_count, address_file);
fclose(address_file);
}
}
/* Opens a binary data file, writes the
number of records, and then write
the whole array in one go using fwrite */
void save_data() {
FILE
*address_file;