Most modern computer programming languages provide mechanisms for grouping data together into logical units. Already in C, we have seen the use of 1-dimensional arrays to store and retrieve data of the same type using an index. This course segment introduces two additional mechanisms for collections: a struct to store data of different types, and 2-dimensional arrays to store tables. This session discusses the struct concept; the next session covers two-dimensional arrays, and the third session puts the ideas of a struct and 2-dimensional arrays together to address the processing of image data.
C groups variables together into a "structure", called a struct. Conceptually, structs allow a programmer to group related data together; pragmatically, the programmer needs to be able to work with the collection of data at some times and with individual pieces at other times. Using a struct, you can simplify parameters, organize data, and protect information.
In C, a structure definition has three basic parts:
the keyword struct,
an identifying name for the structure (this is optional, as we shall see shortly), and
braces { } containing the types and names of the desired data elements for the collection.
Although most modern programming languages allow the grouping of data, terminology and details differ.
C: a collection of data is called a struct. Functions are defined separately, often with structs as parameters or return types.
Pascal and other (pre-object-oriented) programming languages: a collection of data is called a record; as with C, functions are defined separately, and records can be parameters and/or return types.
C++, Java, and other object-oriented programming languages: data elements are packaged with functions to form new data types called classes.
For example, a program for keeping track of students might use the following collection of variables:
struct student { int number; double testGrades[2]; double grade; };
The struct is named student
while its members
or fields are number
, testGrades
, and
grade
. The name of a struct is also called a
tag.
Once a struct is defined, variables and arrays are declared following a reasonably familiar syntax:
struct student hannah; struct student csc161[30];
In this example,
hannah is a structure variable.
csc161 is an array of 30 student structs.
With these declarations, space is allocated on the run-time stack for the variable or array, but the fields are not initialized.
Alternatively, C allows a struct to be initialized when it is declared, by giving values for each field in order, just as with number variables and with arrays, .
For example, the following declaration initializes a struct student jackson, with student number 2718281828, with two grades (98.6 and 83.4) for the testGrades array, and a numeric value (91.0) for grade
struct student jackson = {2718281828, {98.6, 83.4}, 91};
Once a struct variable is declared, it can be used within a program from at least two perspectives:
The variable can be considered as a collection of data items.
Individual fields of the variable referenced using the syntax
variableName.memberName
For example, given the declaration struct student hannah from above,
The variable hannah includes four data elements (the student number, two test grades, and an overall grade).
Individual fields can be assigned and accessed individually:
hannah.number = 991234567; hannah.testGrades[0] = 10.; hannah.testGrades[1] = 11.; hannah.grade = (hannah.testGrades[0]/1.5 + hannah.testGrades[1] / 1.2) / 2.;
To illustrate the use of a struct as a collection of related data, consider how to package data related to the movement of a robot. Many MyroC motion commands require two basic values: a speed and a duration. Since these parts relate to a single motion, it is natural to define them as a package:
struct movement{ double speed; double time; };
With this definition, a program could initialize a robot's movement data and then utilize the pieces to command a robot to move forward. The full program is available as square-move-1.c.
rConnect ("/dev/rfcomm0"); /* Declare and initialize data for one robot movement */ struct movement action = {0.8, 2.0}; /* Command to move forward according to the speed and time stored in the struct */ rForward ( action.speed, action.time); /* beep after movement */ rBeep (1, 600); rDisconnect();
The variable action is a struct with a speed field and a time field.
Both fields are initialized when action is declared.
The fields are used separately when the rForward command is called.
After the robot movement is completed, the robot beeps in celebration.
Although Version 1 of this program illustrates some syntactic elements when using a struct, Version 1 takes little advantage of the capability to group data elements.
In Version 2, the struct construction allows the main procedure to highlight the program's logical overall structure. In this approach, the variable action represents an overall robot movement, and the main procedure outlines the high-level steps required for processing this movement.
int main() { rConnect ("/dev/rfcomm0"); /* Declare information for one robot struct movement */ struct movement action; /* Initialize, print, and execute a robot movement */ initialize (&action); // address allows this variable to change printMove (action); moveRobot (action); /* beep after movement */ rBeep (1, 600); rDisconnect(); return 0; } // main
The variable action collects relevant data for moving a robot into a coherent entity.
To initialize action, we provide an address (&action), just as we would provide an address for obtaining numbers from scanf or another procedure.
Throughout main, the action is referenced as a coherent and logical data set; low-level details (e.g., specific values for speed or duration) do not clutter the overall flow of the main procedure.
Turning to the procedures for printMove and moveRobot, a movement parameter provides data as a collection. Within each procedure, the data fields are used separately as needed by printf or by MyroC functions.
/* print values in struct movement struct */ void printMove (struct movement move) { printf ("robot action: time = %lf, speed = %lf\n", move.time, move.speed); } /* move robot */ void moveRobot (struct movement move) { rForward (move.speed, move.time); }
Given the action parameter,
To initialize the action variable in the main procedure, the address &action is passed to the initialize procedure. Within initialize, the parameter move is declared as a pointer to a struct: struct movement *.
/* set speed and action for a move by the Scribbler 2 robot */ /* must pass address of pointer to get values out of procedure */ void initialize (struct movement *move) { (*move).speed = 0.8; (*move).time = 2.0; }
Given the struct pointer, *move references the memory allocated for the action variable in main. That is, *move refers to the action struct.
Since *move is a struct, (*move).time and (*move).speed refer to the fields within the struct.
Summary:
struct movement * move is the address of a struct
*move references the struct itself
(*move).speed refers to the speed field within the struct
(*move).time refers to the time field within the struct
The full, version 2 program is available as square-move-2.c.
Although version 2 of our program works fine, the frequent repetition of the phrase struct movement can feel somewhat tedious.
As an alternative, we can define a new data type to describe our student information, using the instruction:
typedef struct { double speed; double time; } movement_t;and then declare our variables using this new type, with instructions like:
movement_t action; // a typedef allows a simple, clean declaration
Similarly, procedure headers from Version 2 can now be simplified somewhat.
void initialize (movement_t *move) void printMove (movement_t move) void moveRobot (movement_t move)\
You might find it helpful to think of the typedef
instruction as giving a "blueprint" for the creation of a
movement_t
struct variable, while the declarations cause
the "construction" of variables having type movement_t
by
setting aside memory.
The full, version 3 program, using the typedef construction, is available as square-move-3.c.
In the definition of movement_t, we have chosen not to give a name after the keyword struct. Although we could provide a name:
typedef struct movement { double speed; double time; } movement_t;
the purpose of the typedef is to define a new type movement_t, and there is no need to refer to this type of structure as struct movement when the simple label movement_t is simpler.
Although any almost name can be specified as a type using a typedef statement, a common approach uses a struct followed by an underscore and a t. For example, in the movement example,
With this convention movement_t is a new type for a movement.
As with int, float, and double types, C allows arrays of structs. And, as with all arrays, an array variable identifies the base address of a sequence of struct elements.
As an example, consider program square-move-4.c that defines and uses an array of eight movements. Using the same typedef for movement_t defined above, the heart of the main procedure follows:
rConnect ("/dev/rfcomm0"); int i; /* Declares an array of 8 movement structs */ movement_t actions[8]; /* Loop to initialize the values in the structs */ for (i = 0; i < 8; i++) { /* Sets a constant speed for all actions */ actions[i].speed = 1 - (.1 * i); /* Sets the time for each action to increase by half a second */ actions[i].time = (i / 2.0) + 0.5; } /* Loop to move the Scribbler according to the structs stored times and speed(s). */ for (i = 0; i < 8; i++) { /* Command to move forward according to the speed and time stored in the struct in the array position i */ rForward ( actions[i].speed, actions[i].time); /* Command to make a (roughly) 90 degree turn to the left */ rTurnLeft (1, 0.8); } rBeep (1, 600); rDisconnect();
movement_t actions [8] allocates space for 8 movements, identified as actions[0], actions[1], ..., actions[7].
actions is an array of movement_t elements, so actions [i] is a single movement_t structure for each i.
Since actions [i] is a struct,
Once again, Version 4 utilizes an array, but the program takes little advantage of a struct as a collection of data.
Version 5 uses functions to organize processing. For initialization and printing, square-move-5.c uses similar functions, with struct parameters, that we used in Version 3. For illustration in this discussion, the moveRobot function takes the entire array as parameter. In this version, the main has these elements:
int main() { rConnect ("/dev/rfcomm0"); int i; /* Declares an array of 8 movement structs */ movement_t actions[8]; /* Loop to initialize the values in the structs */ for (i = 0; i < 8; i++) { initialize (&actions[i], i); } /* Loop to print the Scribbler times and speed(s). */ for (i = 0; i < 8; i++) { printMove (actions[i]); } moveRobot (actions, 8); rBeep (1, 600); rDisconnect(); return 0; } // main
Throughout main, processing highlights the high-level steps for this program.
actions is an array of 8 movement_t structures.
In this example, the initialization procedure
void initialize (movement_t *move, int index)
requires both the address of a movement_t structure and the index i for the ith array element.
The printMove function is identical with Version 3, requiring only a movement_t structure.
As a contrast, moveRobot takes the entire array actions as a parameter. Also, since an array variable identifies only a base address, a second parameter moveRobot specifies the number of elements in the array.
In this program, a function performs each primary processing step, as described in Version 4.
/* set speed and action for a move by the Scribbler 2 robot */ /* must pass address of pointer to get values out of procedure */ void initialize (movement_t *move, int index) { (*move).speed = 1 - (.1 * index); (*move).time = (index / 2.0) + 0.5; } /* print values in movement struct */ void printMove (movement_t move) { printf ("robot action: time = %lf, speed = %lf\n", move.time, move.speed); } /* move robot */ void moveRobot (movement_t move [], int size) { int i; for (i = 0; i < size; i++) { rForward (move[i].speed, move[i].time); /* Command to make a (roughly) 90 degree turn to the left */ rTurnLeft (1, 0.8); } }
Procedure initialize works with an individual movement struct. By passing in the address of the movement_t structure, the function can access an array element within main as part of initialization.
The printMove given here is the same as described in Version 3.
The moveRobot function takes the entire movement_t move array as a parameter; the entire sequence of movements is considered a single entity passed into the function.
As a final example illustrating robot motion with structs and arrays, consider a program in which each function operates on the entire array of movements.
In this version square-move-6.c, the main program is particularly streamlined
int main() { rConnect ("/dev/rfcomm0"); int i; /* Declares an array of 8 movement structs */ movement_t actions[8]; initSpeedTime(actions); moveScribbler (actions); rBeep (1, 600); rDisconnect(); return 0; } // main
Conceptually, the actions array contains a full sequence of eight movements.
Function initSpeedTime initializes the actions array. Since, in practice, all array variables specify the base address of an array, the variable actions is passed directly to initiSpeedTime; no need for an address operator &, since actions already is a [base] address.
Similarly, actions is passed directly into the moveScribbler function.
Within the function initSpeedTime and moveScribbler, the actions array comes in as a parameter. Thus, inside the functions, processing proceeds as with any declared array of structs.
void initSpeedTime (movement_t actions []) { int i; /* Loop to initialize the values in the structs */ for (i = 0; i < 8; i++) { /* Sets a constant speed for all actions */ actions[i].speed = 1 - (.1 * i); /* Sets the time for each action to increase by half a second */ actions[i].time = (i / 2.0) + 0.5; } } void moveScribbler (movement_t actions []) { int i; /* Loop to move the Scribbler according to the structs stored times and speed(s). */ for (i = 0; i < 8; i++) { /* Command to move forward according to the speed and time stored in the struct in the array position i */ rForward ( actions[i].speed, actions[i].time); /* Command to make a (roughly) 90 degree turn to the left */ rTurnLeft (1, 0.8); } }
As described earlier in this reading:
movement_t actions represents an array of structures
One movement_t structure is given by actions[0], actions[1], ..., actions[7]
Given one array element structure, actions[i],
As a completely separate example of a struct, the following structure may be used to represent a time value in hours, minutes and seconds format (e.g., 12:34:56.123):
typedef struct { int hours; int mins; double secs; } timeinfo_t;
The timeinfo_t
identifier is the struct "tag".
A new type called timeinfo_t
is created. We did not call it
time
, because there is already a C library function
called time
.
Structure types may be used as return types or argument types in functions. A function that converts time values given in seconds (e.g., 12345.67) to time values given in hh:mm:ss.sss format might have the prototype:
timeinfo_t convertTime( double realTime )
and would look like:
timeinfo_t convertTime( double realTime ) { timeinfo_t result; . . . return result; }
created 11 April 2008 by Marge Coahran revised 3 August 2011 by Erik Opavsky full revision 14 November 2011 by Erik Opavsky minor editing 14 November 2011 by Henry Walker naming convention subsection added 17 August 2012 by Henry Walker minor editing 26 October 2013 by Henry Walker |
|
For more information, please contact Henry M. Walker at walker@cs.grinnell.edu. |