Advanced Types - Simple Programming - Practical C Programming, 3rd Edition (2011)

Practical C Programming, 3rd Edition (2011)

Part II. Simple Programming

Chapter 12. Advanced Types

Total grandeur of a total edifice, Chosen by an inquisitor of structures.

—Wallace Stevens

C provides the programmer with a rich set of data types. Through the use of structures, unions, and enumerated types, the programmer can extend the language with new types.

Structures

Suppose we are writing an inventory program for a warehouse. The warehouse is filled with bins that contain various parts. All the parts in a bin are identical, so we don’t have to worry about mixed bins.

For each bin, we need to know:

§ The name of the part it holds (string 30 characters long)

§ The quantity on hand (integer)

§ The price (integer cents)

In previous chapters, we have used arrays for storing a group of similar data types. However, in this example, we have a mixed bag: two integers and a string.

Instead of an array, we will use a new data type called a structure. In an array, all the elements are of the same type and are numbered. In a structure, each element or field is named and has its own data type.

The general form of a structure definition is:

structstructure-name {

field-type field-name; /* comment */

field-type field-name; /* comment */

. . . .

} variable-name;

For example, we want to define a bin to hold printer cables. The structure definition is:

struct bin {

char name[30]; /* name of the part */

int quantity; /* how many are in the bin */

int cost; /* The cost of a single part (in cents) */

} printer_cable_bin; /* where we put the print cables */

This definition actually tells C two things. The first is what a struct bin looks like. This statement defines a new data type that can be used in declaring other variables. The variable printer_cable_bin is also declared by this statement. Because the structure of a bin has been defined, we can use it to declare additional variables:

struct bin terminal_cable_box; /* Place to put terminal cables */

The structure-name part of the definition may be omitted:

struct {

char name[30]; /* name of the part */

int quantity; /* how many are in the bin */

int cost; /* The cost of a single part (in cents) */

} printer_cable_bin; /* where we put the print cables */

The variable printer_cable_bin has been defined, but no data type has been created. In other words, printer_cable_bin is the only variable of this structure you want in the program. The data type for this variable is an anonymous structure.

The variable-name may also be omitted. The following example would define a structure type, but no variables:

struct bin {

char name[30]; /* name of the part */

int quantity; /* how many are in the bin */

int cost; /* The cost of a single part (in cents) */

};

You can now use the new data type (struct bin) to define variables such as printer_cable_bin.

In an extreme case, both the variable-name and the structure-name may be omitted. This syntax creates a section of correct, but totally useless code.

We have defined the variable printer_cable_bin containing three named fields: name, quantity, and cost. To access them, we use the syntax:

variable.field

For example, if we just found out that the price of the cables went up to $12.95, we would do the following:

printer_cable_bin.cost = 1295; /* $12.95 is the new price */

To compute the value of everything in the bin, we can use the following:

total_cost = printer_cable_bin.cost * printer_cable_bin.quantity;

Structures may be initialized at declaration time by putting the list of elements in curly braces ({ }):

/*

* Printer cables

*/

struct bin {

char name[30]; /* name of the part */

int quantity; /* how many are in the bin */

int cost; /* The cost of a single part (in cents) */

} printer_cable_bin = {

"Printer Cables", /* Name of the item in the bin */

0, /* Start with empty box */

1295 /* cost -- $12.95 */

};

Unions

A structure is used to define a data type with several fields. Each field takes up a separate storage location. For example, the structure:

struct rectangle {

int width;

int height;

};

appears in memory. A union is similar to a structure; however, it defines a single location that can be given many different field names:

union value {

long int i_value; /* integer version of value */

float f_value; /* floating version of value */

};

The fields i_value and f_value share the same space.

You might think of a structure as a large box divided up into several different compartments, each with its own name. A union is a box, not divided at all, with several different labels placed on the single compartment inside.

Figure 12-1 illustrates a structure with two fields. Each field is assigned a different section of the structure. A union contains only one compartment, which is assigned different names.

Layout of structure and union

Figure 12-1. Layout of structure and union

In a structure, the fields do not interact. Changing one field does not change any others. In a union, all fields occupy the same space, so only one may be active at a time. In other words, if you put something in i_value, assigning something to f_value wipes out the old value of i_value.

Example 12-1 shows how a union can be used.

Example 12-1. Using a Union

/*

* Define a variable to hold an integer or

* a real number (but not both)

*/

union value {

long int i_value; /* The real number */

float f_value; /* The floating-point number */

} data;

int i; /* Random integer */

float f; /* Random floating-point number */

main()

{

data.f_value = 5.0;

data.i_value = 3; /* data.f_value overwritten */

i = data.i_value; /* legal */

f = data.f_value; /* not legal, will generate unexpected results */

data.f_value = 5.5; /* put something in f_value/clobber i_value */

i = data.i_value; /* not legal, will generate unexpected results */

return(0);

}

Unions are frequently used in the area of communications. For example, suppose we have a remote tape and we want to send it four messages: open, close, read, and write. The data contained in these four messages is vastly different depending on the message.

The open message needs to contain the name of the tape; the write message needs to contain the data to write; the read message needs to contain the maximum number of characters to read; and the close message needs no additional information.

#define DATA_MAX 1024 /* Maximum amount of data for a read and write */

struct open_msg {

char name[30]; /* Name of the tape */

};

struct read_msg {

int length; /* Max data to tranfer in the read */

};

struct write_msg {

int length; /* Number of bytes to write */

char data[DATA_MAX]; /* Data to write */

};

struct close_msg {

};

const int OPEN_CODE=0; /* Code indicating an open message */

const int READ_CODE=1; /* Code indicating a read message */

const int WRITE_CODE=2; /* Code indicating a write message */

const int CLOSE_CODE=3; /* Code indicating a close message */

struct msg {

int msg; /* Message type */

union {

struct open_msg open_data;

struct read_msg read_data;

struct write_msg write_data;

struct close_msg close_data

} msg_data;

};

typedef

C allows the programmer to define her own variable types through the typedef statement. This statement provides a way for the program to extend C’s basic types. The general form of the typedef statement is:

typedeftype-declaration;

where type-declaration is the same as a variable declaration except that a type name is used instead of a variable-name. For example:

typedef int count;

defines a new type count that is the same as an integer.

So the declaration:

count flag;

is the same as:

int flag;

At first glance, this statement is not much different from:

#define count int

count flag;

However, typedefs can be used to define more complex objects that are beyond the scope of a simple #define statement. For example:

typedef int group[10];

A new type named group now exists, denoting an array of ten integers:

main()

{

typedef int group[10]; /* Create a new type "group" */

group totals; /* Use the new type for a variable */

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

totals[i] = 0;

return (0);

}

One frequent use of typedef’s is in the definition of a new structure. For example:

struct complex_struct {

double real;

double imag;

};

typedef struct complex_struct complex;

complex voltag1 = {3.5, 1.2};

enum Type

The enumerated data type is designed for variables that contain only a limited set of values. These values are referenced by name (tag). The compiler assigns each tag an integer value internally. Consider an application, for example, in which we want a variable to hold the days of the week. We could use the const declaration to create values for the week_days, as follows:

typedef int week_day; /* define the type for week_days */

const int SUNDAY = 0;

const int MONDAY = 1;

const int TUESDAY = 2;

const int WEDNESDAY = 3;

const int THURSDAY = 4;

const int FRIDAY = 5;

const int SATURDAY = 6;

/* now to use it */

week_day today = TUESDAY;

This method is cumbersome. A better method is to use the enum type:

enum week_day {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,

FRIDAY, SATURDAY};

/* now use it */

enum week_day today = TUESDAY;

The general form of an enum statement is:

enumenum-name { tag-1, tag-2, . . .}variable-name

Like structures, the enum-name or the variable-name may be omitted. The tags may be any valid C identifier; however, they are usually all uppercase.

C implements the enum type as compatible with integer. So in C, it is perfectly legal to say:

today = 5; /* 5 is not a week_day */

although some compilers will issue a warning when they see this line. In C++, enum is a separate type and is not compatible with integer.

Casting

Sometimes you must convert one type of variable to another type. This is accomplished through the cast or typecast operation. The general form of a cast is:

(type)expression

This operation tells C to compute the value of the expression, and then convert it to the specified type. This operation is particularly useful when you work with integers and floating-point numbers:

int won, lost; /* # games won/lost so far */

float ratio; /* win/lose ratio */

won = 5;

lost = 3;

ratio = won / lost; /* ratio will get 1.0 (a wrong value) */

/* The following will compute the correct ratio */

ratio = ((float) won) / ((float) lost);

Another common use of this operation is for converting pointers from one type to another.

Bit Fields or Packed Structures

Packed structures allow us to declare structures in a way that takes up a minimum amount of storage. For example, the following structure takes up six bytes (on a 16-bit machine).

struct item {

unsigned int list; /* true if item is in the list */

unsigned int seen; /* true if this item has been seen */

unsigned int number; /* item number */

};

The storage layout for this structure can be seen in Figure 12-2. Each structure uses six bytes of storage (two bytes for each integer).

Unpacked structure

Figure 12-2. Unpacked structure

However, the fields list and seen can only have two values, and 1, so only one bit is needed to represent them. We never plan on having more than 16383 items (0x3fff or 14 bits). We can redefine this structure using bit fields, so that it takes only two bytes, by following each field with a colon and the number of bits to be used for that field:

struct item {

unsigned int list:1; /* true if item is in the list */

unsigned int seen:1; /* true if this item has been seen */

unsigned int number:14; /* item number */

};

In this example, we tell the compiler to use one bit for list, one bit for seen, and 14 bits for number. Using this method, we can pack our data into only two bytes, as seen in Figure 12-3.

Packed structure

Figure 12-3. Packed structure

Packed structures should be used with care. The code to extract data from bit fields is relatively large and slow. Unless storage is a problem, packed structures should not be used.

In Chapter 10, we needed to store character data and five status flags for 8,000 characters. In this case, using a different byte for each flag would eat up a lot of storage (five bytes for each incoming character). We used bitwise operations to pack the five flags into a single byte. Alternatively, a packed structure could have accomplished the same thing:

struct char_and_status {

char character; /* Character from device */

int error:1; /* True if any error is set */

int framing_error:1;/* Framing error occurred */

int parity_error:1; /* Character had the wrong parity */

int carrier_lost:1; /* The carrier signal went down */

int channel_down:1; /* Power was lost on the channel */

};

Using packed structures for flags is clearer and less error prone than using bitwise operators. However, bitwise operators give the programmer additional flexibility. You should use the one that is clearest and easiest for you to use.

Arrays of Structures

Structures and arrays can be combined. For example, suppose we want to record the time a runner completes each lap of a four-lap race. We define a structure to store the time:

struct time {

int hour; /* hour (24 hour clock ) */

int minute; /* 0-59 */

int second; /* 0-59 */

};

const int MAX_LAPS = 4; /* we will have only 4 laps */

/* the time of day for each lap*/

struct time lap[MAX_LAPS];

We can use this structure as follows:

/*

* Runner just passed the timing point

*/

lap[count].hour = hour;

lap[count].minute = minute;

lap[count].second = second;

++count;

This array can also be initialized at run time.

Initialization of an array of structures is similar to the initialization of multi-dimensional arrays:

struct time start_stop[2] = {

{10, 0, 0},

{12, 0, 0}

};

Suppose we want to write a program to handle a mailing list. Mailing labels are 5 lines high and 60 characters wide. We need a structure to store names and addresses. The mailing list will be sorted by name for most printouts, and sorted in zip code order for actual mailings. Our mailing list structure looks like this:

struct mailing {

char name[60]; /* Last name, first name */

char address1[60];/* Two lines of street address */

char address2[60];

char city[40];

char state[2]; /* Two character abbreviation */

long int zip; /* Numeric zip code */

};

We can now declare an array to hold our mailing list:

/* Our mailing list */

struct mailing list[MAX_ENTRIES];

The state field is two elements because it is designed to hold two characters. The field is not a string because we have not allocated enough space for the end of string character ('\0').

Summary

Structures and unions are some of the more powerful features of the C language. No longer are you limited to C’s built-in data types—you can create your own. As we will see in later chapters, structures can be combined with pointers to create very complex and powerful data structures.

Programming Exercises

Exercise 12-1: Design a structure to hold the data for a mailing list. Write a function to print out the data.

Exercise 12-2: Design a structure to store time and date. Write a function to find the difference between two times in minutes.

Exercise 12-3: Design an airline reservation data structure that contains the following data:

§ Flight number

§ Originating airport code (three characters)

§ Destination airport code (three characters)

§ Starting time

§ Arrival time

Exercise 12-4: Write a program that lists all the planes that leave from two airports specified by the user.