C from the Top

Part 10 - Structures n Unions

As you can by now appreciate, C is not only rather easy to understand, but is also quite jolly to program under. This is not by mistake either!

Onto a practical problem. A while back, I asked you to design and write a program which would store names, addresses, phone numbers and the like and quite probably, you would have had as global variables something akin to

char name[100], addr1[100], addr2[100] etc.....

and for the input routine, you'd have a pile of either gets(var[no]); or scanf("%s",&var[no]); commands. While this gets the job done, it does make for problems in both debugging and extra lines of code. This can all be done away with with the use of the struct command.

Remember, there is nothing to say that name[0] is related to addr[0]. The struct defines a new compound data type which holds all these pieces of information together.

Format

struct name {list} name2,*ptr

where name is a tag name (the type name for the structure) and name2 is known as the variable list. This is the part you use when accessing the structure. list is a list of variables associated with this structure.

name2 and *ptr are not obilgatory.

Unlike the BASIC counterpart, DIM, a structure can have any of the recognised variable types inside of it.

For instance, in the names and addresses program you had written, a suitable structure may look like this

struct entry
{
 char name[60];
 char address1[60];
 char address2[60];
 char postcode[10];
 int telephone;
};

This defines a structure type called entry, which will contain four strings and an int. Note that it only defines the structure. We haven't declared any variables to be of this type.

To do this, we do the following :

struct entry person;

This declares the variable person to be an "entry" struct.

As a struct is a grouping of several bits of data, we need to be able to access them separately. For example

strcpy (person.name,"Emergency services");
person.telephone=999;

The name of the variable is followed by a full stop and the name of the part of the struct we wish to access. We do exactly the same when wanting to output the data from the struct again :

printf("Phone no. : %d\n",person.telephone);

We have seen how to declare the format of a struct and how to create a variable of that type.

As we've already seen in C, we can combine the both by listing the variables after the struct declaration :

struct entry
{
 char name[60];
 char address1[60];
 char address2[60];
 char postcode[10];
 int telephone;
} person, people[50], *person_ptr;

This declares a variable person of type struct entry, an array called people, each element being a struct, and finally a pointer to this type of struct, *person_ptr.

This can be fixed by the use of a pointer. The pointer should be positioned after the variable type list name like so :

} details[100],*pointer;

At the start of the input routine, you should assign the pointer to equal the address of details. Using the pointer to write to a particular part of the structure requires a slightly different syntax. Whereas before you would have

gets (var[no].part);

you would now have

gets (pointer->part);

the important part is the ->. pointer is equal to the start address of the variable list name. What is happening here is that you are accessing the area assigned to var[no].part in memory.

While this may not seem to be that important, think along these lines. In your average database, you may have 3000 actual records, containing 8 fields (called a to h). Each field works out to have an average of 60 characters in it. This would work out to mean that the structure itself would have a size of 5.5Mb (3000 x 8 x 60 x 4 (the number of bytes set aside for a char or int)). Could you imagine attempting to write code which would be asking for you to input record 2999, field e? The nightmare would begin!

The good news though is that pointer arithmetic on structs is no problem. Take the following bit of code :

struct entry
{
 char name[60];
 char address[60];
 int telephone;
};

int main(void)
{
 struct entry person,people[50], *person_ptr;
 strcpy(people[40].name,"Emergency services");
 people[40].telephone=999;
 person_ptr=&people[0];
 printf("%s %d\n",person_ptr[40].name, person_ptr[40].telephone);
}

person_ptr has been set to be pointing at the base of the people array. person_ptr can then be passed to a function. The printf line shows how the data can be accessed from within the function. All you do is put the number of the array item you want in square brackets.

Structures are not restricted to one variable list, like for loops, they can also be embedded. It is done like this

struct listone
{
 char number1[10];
 int something;
}

struct listtwo
{
 char xyz[10];
 char abc[10];
 struct listone list;
} somename, *pointer;

This sort of embedding would be useful for the likes of a library system, or in a more practical way, for part of a wimp JPEG routine (I'll be getting onto the wimp in the not to distant future).

Writing to this embedded structure is the same as writing to any other part of the structure

pointer -> somename.list.number1

list is the name given in the somename structure for the first (listone) structure.

As a summary then :

Structures can also be tested using if statements :

if (strcmp(pointer-> abc,"Fred"))
{
 do something<
}

or if not used with a pointer, the line becomes

if (strcmp(somename.abc,"Fred"))
{
 do something
}

Unions

A union is a single piece of memory shared by two or more variables. These variables can be of different types, but only one variable from it can be used at any one time. To ensure that the stack is not corrupted, the size of the union will be determined by its largest member.

Format

union tag {type members} var-name;

The type members can be any of the recognised variable types, and you can have as many of them as you wish!

For example,

union name
{
 int number;
 char fred[2];
 double number2;
} listname;

This would produce (in memory) a block like this :

Accessing the union is the same as for a structure (e.g. above would be listname.number2=123.456;)

Okay, so how would we really use them. One good example is when you are creating an icon in a window (alright, that's for the WIMP which is coming, but for now it will show how it is used). This piece of code is really for those fortunate to be using a RISC OS based machine. Other machines may do something similar.

union alt
{
 char text[12];
 struct
 {
  int *pointer_text;
  int *pointer_validation;
  int text_len;
 } indirected;
};

typedef struct
{
 int window_handle;
 int min_x_bound_box;
 int min_y_bound_box;
 int max_x_bound_box;
 int max_y_bound_box;
 int icon_flags;
 union alt redirect;
} icon;

The union alt is saying (effectively) that either we have 12 characters of text or 3 ints held within the struct. Both have an overall memory allocation size of 12 bytes.

This is then used within the typedef struct to say that the seventh element (the union statement) is either text or another structure.

A final type of structure is the bitfield structure. This is where you are able to access by name one or more bits from a byte or word (a word being 4 bits).

Format

type name : size in bits;

type has to be either int or unsigned. You can make it signed, but this may not always work. The colon separates the name from the size in bits for this particular name.

As you are also only dealing with bits, then this should also require far less memory than a normal structure. For instance, this fragment could be used for storing CD information :

struct cd_info
{
 unsigned length : 1
 unsigned cd_type : 1
 unsigned price : 4
 unsigned status : 1
} cd [large_number];

(an explanation of the numbers : all numbers are in binary, therefore 4 will give 4 bits, meaning that any value from 0 to 15 can be stored.)

The downpoint to the use of a bit field structure is that in the above snippet, you cannot say that the CD is £12.99, it has to 13 pounds, and also that you need to use a system of codes. For instance, length would need 0 for 74 minutes, and 1 for 80.

The bit field also does not need to add up to 8 as the compiler will try and fit it into the smallest block of memory possible.

That said, bit fields can be quite useful as you can still use other types of members within the structure (the structure is still a structure after all!). If I took the above snippet and added

char title [30];
char artist [40];

the amount of memory required for the structure would have grown, but not as much as if I had defined the other variables within the structure as doubles, ints and the such.

Accessing the bitfield structure is the same as accessing any other type of structure.

The final part of this tutorial is saving and loading a structure.

Saving and loading the contents of a structure is the same as saving and loading anything else. It can be saved as a text or binary file (though binary would be best here).

There is one slight difference. Even though you may have said that you want a structure to have (say) 3000 elements, only 250 may have been used. There seems little point in outputting the other 2750 to disc. (Of course, the same would equally apply to a normal array - it's not a problem specific to structs!)

To get around this, the following can be used to save

if (fwrite(&current, sizeof current,1,fileptr)!=1)

where current is an int in which we have stored the index of the highest element of the array which is actually in use.

if (fwrite(pointer, sizeof struct_name,1,filename)!=1)

this writes the structure to disc (structure_name is the name of the struct your saving, e.g. entry).

or

if (fread(counter,sizeof counter,1,filename)!=1)

this reads in the number of the counter at the time of saving. It means that the next entry can be made without overwriting the last.

if (fread(entry, sizeof entry,1,filename)!=1)

should read the structure back in.

A practical example to sum this up...

struct name details[100],*ptr;
int top; /*stores number of entries use*/

: (- code comes here)

pointer=&details[0];
top=50;

: (more code)

if(fwrite(&top,sizeof top,1,fileptr)!=1)
{
 printf("Write error\n");
}

if(fwrite(pointer,sizeof (struct name),top,fileptr)!=top)
{
 printf("Write error\n");
}

and for reading

if (fread(&top,sizeof top,1,fileptr)!=1)
{
 printf("Error reading the counter\n");
}

if (fread(pointer,sizeof (struct name),1,fileptr)!=1)
{
 printf("Error reading in\n");
}

Next time, I'll be covering bitwise operations, typedef and enum. Until then a task.

Taking your original names and addresses program, convert is so that it uses structures and pointers to store the information. Compile the program - you should find that it is smaller than the original program (this may not be the case always, it depends on the size of your original code).


email me with your comments

Return