Chapter 12. Advanced Types

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

Wallace Stevens

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

Structures

Suppose you are writing an inventory program for a warehouse. The warehouse is filled with bins, each containing a bunch of parts. All the parts in a bin are identical, so you don’t have to worry about mixed bins or partials.

For each bin you need to know:

  • The name of the part it holds (30-character string).

  • The quantity on hand (integer).

  • The price (integer cents).

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

Instead of an array, you 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 member, is named and has its own data type.

The general form of a structure definition is:

struct structure-name { 
    member-type 
               member-name;  // Comment 
               member-type 
               member-name;  // Comment 
    . . . . 
} variable-name;

For example, say you 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_box;    // 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. This statement also declares the variable printer_cable_box. Since the structure of a bin has been defined, you 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_box;    // Where we put the print cables

The variable printer_cable_box is still to be defined, but no data type is created. The data type for this variable is an anonymous structure.

The variable-name part also may be omitted. This 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)
};

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

Once the structure type has been defined you can use it to define variables:

struct bin printer_cable_box; // Define the box holding printer cables

C++ allows the struct to be omitted, so you can use the following declaration:

bin printer_cable_box; // Define the box holding printer cables

You have defined the variable printer_cable_box containing three named members: name, quantity, and cost. To access them you use the syntax:

               variable.member

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

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

To compute the value of everything in the bin, you can simply multiply the cost by the number of items using the following:

total_cost = printer_cable_box.cost * printer_cable_box.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)
};
struct bin printer_cable_box = { 
    "Printer Cables",   // Name of the item in the bin
    0,                  // Start with empty box
    1295                // Cost -- $12.95
};

The definition of the structure bin and the variable printer_cable_box can be combined in one step:

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_box = { 
    "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 members. Each member takes up a separate storage location. For example, the structure:

struct rectangle { 
    int width; 
    int height; 
};

appears in memory as shown in Figure 12-1.

A union is similar to a structure; however, it defines a single location that can be given many different member names:

union value { 
    long int i_value;   // Long integer version of value
    float f_value;      // Floating version of value
};
Structure and union layout
Figure 12-1. Structure and union layout

The members 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.

In a structure, the members do not interact. Changing one member does not change any others. In a union, all members 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.

The following shows how a union may be used:

/* 
 * 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

int 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);
}

Suppose you want to store the information about a shape. The shape can be any standard shape such as a circle, rectangle, or triangle. The information needed to draw a circle is different from the data needed to draw a rectangle, so you need to define different structures for each shape:

struct circle {
    int radius;           // Radius of the circle in pixels
};
struct rectangle {
    int height, width;   // Size of the rectangle in pixels
};
struct triangle {
    int base;            // Length of the triangle's base in pixels
    int height;          // Height of the triangle in pixels
};

Now you define a structure to hold the generic shape. The first member is a code that tells you what type of shape you have. The second is a union that holds the shape information:

const int SHAPE_CIRCLE    = 0;    // Shape is a circle
const int SHAPE_RECTANGLE = 1;    // Shape is a rectangle
const int SHAPE_TRIANGLE  = 2;    // Shape is a triangle

struct shape {
    int kind;               // What kind of shape is stored
    union shape_union {     // Union to hold shape information
        struct circle    circle_data;     // Data for a circle
        struct rectangle rectangle_data;  // Data for a rectangle
        struct triangle  triangle_data;   // Data for a triangle
    } data;
};

Graphically you can represent shape as a large box. Inside the box is the single integer kind and our union shape_union. The union is a box with three labels on it. The question is which one is the “real” label. You can’t tell from looking at the union, but that’s why you defined kind. It tells us which label to read. The layout of the shape structure is illustrated by Figure 12-2.

“shape” layout
Figure 12-2. “shape” layout

Now you can store a circle in the generic shape:

struct shape a_shape;
//...
a_shape.kind = SHAPE_CIRCLE;
a_shape.data.circle_data.radius = 50;  // Define the radius of the circle

In this example we are defining one basic data type (a shape) and adding in specific information for a bunch of different types of shapes. Although we are using a union to organize our data, this sort of data can be better organized using base and derived classes. (See Chapter 21.)

typedef

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

typedef type-declaration;

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

typedef int width; // Define a type that is the width of an object

defines a new type, width, that is the same as an integer. So the declaration:

width box_width;

is the same as:

int box_width;

At first glance, this is not much different from:

#define width int 

width box_width;

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

typedef int group[10];

This statement defines a new type, group, which denotes an array of 10 integers. For example:

int main(  ) 
{ 
    typedef int group[10];     // Create a new type "group"

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

    // Initialize each element of total
    for (i = 0; i < 10; ++i) 
        totals[i] = 0;

enum Type

The enumerated (enum) data type is designed for variables that can contain only a limited set of values. These values are referenced by name (tag[1] ). The compiler assigns each tag an integer value internally, such as the days of the week. You could use the directive const to create values for the days of the week (day_of_the_week) as follows:

typedef int day_of_the_week;   // Define the type for days of the week

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 */ 
day_of_the_week today = TUESDAY;

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

enum day_of_the_week {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,  
    FRIDAY, SATURDAY}; 

/* Now use it */ 
enum day_of_the_week today = TUESDAY;

The general form of an enum statement is:

enum enum-name {tag-1, tag-2, . . .} variable-name;

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

An additional advantage of using an enum type is that C++ will restrict the values that can be used to the ones listed in the enum declaration. Thus, the following will result in a compiler error:

today = 5;  // 5 is not a day_of_the_week

If we want to force the issue, we have to use a static_cast to transform 5 into a day:

today = static_cast<enum day_of_the_week>(5);

So far we’ve let C++ do the mapping from enum tags to integers. For example, our enum declaration:

enum day_of_the_week {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,  
    FRIDAY, SATURDAY};

results in SUNDAY being assigned 0, MONDAY gets 1, and so on. This works great if we don’t care about the mapping. But suppose we are interfacing to a device that returns a set of error codes. We would like to define an enum to hold them. The problem is that the device returns error numbers and we need to map them precisely to our enum tags. C++ lets you specify the mapping values in your enum declaration:

enum ERROR_RETURNS {
    MOTOR_FAILURE = 55,
    POSITION_ERROR = 58,
    OIL_FAILURE = 33
};

Bit Members or Packed Structures

So far all the structures you’ve been using have been unpacked. Packed structures allow you to declare structures in a way that takes up a minimum of storage. For example, the following structure takes up 6 bytes (using a 16-bit compiler):

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-3. Each structure uses 6 bytes of storage (2 bytes for each integer).

Unpacked structure
Figure 12-3. Unpacked structure

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

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, you tell the compiler to use 1 bit for list, 1 bit for seen and 14 bits for number. Using this method you can pack data into only 2 bytes, as seen in Figure 12-4.

Packed structure
Figure 12-4. Packed structure

You can add a bit field only to an int or enum variable. It doesn’t work on floating-point members, strings, or other complex types.

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

Also, the C++ standard does not define how packing must be implemented. The compiler is free to pack things together in any order it wants to. There is no guarantee that our structure will be stored in two 8-bit bytes. (Some cheap compilers treat packed structures the same an normal structures and leave everything unpacked. This is legal under the C++ standard.)

In Chapter 5, you 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). You 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
    unsigned int error:1;        // True if any error is set
    unsigned int framing_error:1;// A framing error occurred
    unsigned int parity_error:1; // Character had the wrong parity
    unsigned int carrier_lost:1; // The carrier signal went down
    unsigned 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 allow additional flexibility. You should use the approach that is clearest and easiest for you to use.

Question 12-1: Why does Example 12-1 fail?

Example 12-1. info/info.cpp
#include <iostream>

struct info {
    int valid:1;        // If 1, we are valid
    int data:31;        // The data
};

info a_info;

int main(  )
{
    a_info.valid = 1;
    if (a_info.valid == 1) 
        std::cout << "a_info is valid
";
    return (0);
}

The GNU compiler does try to give a hint as to what’s going on. Now if we could only understand what it is trying to say:

info.cpp: In function `int main(  )':
info.cpp:13: warning: comparison is always 0 due to width of bitfield

Arrays of Structures

Structures and arrays can be combined. Suppose you want to record the time a runner completes each lap of a four-lap race. You 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];

The statement:

struct time lap[MAX_LAPS];

defines lap as an array of four elements. Each element consists of a single time structure.

You can use this as follows:

/* 
 * Runner just past the timing point 
 */ 

assert((count >= 0) && (count <= sizeof(lap)/sizeof(lap[0])));
lap[count].hour = hour; 
lap[count].minute = minute; 
lap[count].second = second; 
++count;

This array can also be initialized when the variable is declared. Initialization of an array of structures is similar to the initialization of multidimensional arrays:

struct time start_stop[2] = { 
    {10, 0, 0}, 
    {12, 0, 0} 
};

Suppose you want to write a program to handle a mailing list. Mailing labels are 5 lines high and 60 characters wide. You need a structure to store names and addresses. The mailing list will be sorted by name for most printouts, and in Zip-code order for actual mailings. The 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];    // Name of the city 
    char state[2];    // Two-character abbreviation[2]
    long int zip;     // Numeric zip code
};

You can now declare an array to hold the mailing list:

/* Our mailing list */ 
struct mailing list[MAX_ENTRIES];

Programming Exercises

Exercise 12-1: Design a data structure to handle the data for a mailing list.

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 (3 characters)
Destination airport code (3 characters)
Departure time
Arrival time

Write a program that lists all planes leaving from two airports specified by the user.

Answers to Chapter Questions

Answer 12-1:The problem is that we have a single signed-integer-bit field. A three-bit-wide signed-integer field can take on the following values:

struct foo {
    int three_bits:3;
};

Bit pattern

Decimal value

100

-4

110

-3

101

-2

111

-1

000

0

001

1

010

2

011

3

A two-bit-wide signed-integer field can take on the following values:

struct foo {
    int two_bits:3;
};

Bit pattern

Decimal value

10

-2

11

-1

000

0

001

1

A one-bit-wide signed-integer field can take on the following values:

struct foo {
    int one_bit:1;
};

Bit pattern

Decimal value

1

-1

0

0

So the two values of this bit field are 0 and -1. That means that 1 can never be stored in this field.

A unsigned bit field of width 1 can hold the values 0 and 1. Using the declaration unsigned int valid:1 makes the program work correctly.



[1] Tags are also called “named constants” or “enumerators.”

[2] To store the state abbreviation as a C-style string, three characters are needed; two for the data and one for the end-of-string character. This is not a C-style string. Instead it is just two characters. So the dimension 2 is correct.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.109.234