Computational Physics
Structures organise your data
Comments and questions to John Rowe.
Show me your flow charts and
conceal your tables and I
shall continue to be mystified, show me your tables and I
won't usually need your flow charts; they'll be obvious.
Brooks
[ Flow charts were once popular methods of showing the organisation and
structure of your code and functions. Tables are used in other
languages to show the organisation and structure of your data; they
are the equivalent of structures in C. ]
Structures
do for variables what functions do for code
We have already discussed the fact that nearly every program is so
large that it is completely impossible to hold in all in your head at
the same time.
Functions organise code into self contained
units, enabling you to think at a higher level without having to keep
track of the details.
Structures do exactly the same for
variables.
Structures reduce
the number of arguments to functions
Imagine a function to calculate and print out the height and velocity
of a projectile thrown into the air. Its height and velocity are known
at time t=0 and we need print out its position and velocity at time
t + dt. Its
prototype would look something like:
void height_velocity(float y, float vy, float mass,
float drag_coeff, float ywind,
float viscosity, float dt);
This is a very simple problem but the function has seven
arguments. Worse, they are all floats so it would be very easy to get
two in the wrong order and the compiler would not notice. There are
over five thousand different legal ways of ordering seven arguments of
the same type but only one of them is the right one!
C structures give us a way of rolling up several
different variables into a single package, just as a carrier bag
allows us to carry several items together at the supermarket, thus
making it easier to pass them to functions.
Structures
organise and package variables into groups
Representation is the essence of programming.
Brooks
If we take a look at the arguments to the function above we see they
split into three groups: y, vy, mass and drag_coeff are properties of
the projectile, ywind and viscosity are properties of the air and time
is a physical quantity in its own right. This suggests we want two
structures, one for the projectile and one for the air, and
to leave time as it is.
The following code declares what are in effect two
new types of variables:
struct projectile {
float y;
float vy;
float mass;
float drag_coeff;
}; /* Notice the semi-colon! */
struct airprop {
float ywind;
float viscosity;
};
This code does not actually create any structures,
it just tells the compiler what we mean by 'struct projectile' and
'struct airprop'.
Creating structures
We can create actual instances of these structures
- including arrays of structures - just as we would floats or ints:
void myfun(void) {
struct airprop air;
struct projectile myproj, projarray[20];
myproj.mass = 1.0;
myproj.y = 1.5;
myproj.vy = 11.6;
projarray[0].mass = 1.3;
/* Whatever else here */
}
The function height_velocity is now declared as:
void height_velocity(struct projectile proj,
struct airprop air, float dt);
Not only do we now only have three variables rather than seven, all
three are of a different type so if we were to call the function
with two of its arguments in the wrong order the compiler would notice
and tell us.
The function would then contain lines like:
proj.y += proj.vy * dt;
Notice:
- The first code fragment defined the meaning of 'struct
projectile', the second fragment used this information to actually
create some structures.
- The individual members of the structure are accessed as
proj.mass, etc. You can treat each member as an ordinary variable in
exactly the same way that you can treat each element of an array as an
ordinary variable.
- We were careful to divide the data into different structures
according to what object the data was a property of.
- Just as a function prototype must precede the first reference to
that function in the file, the declaration of what the structure means
must precede the first use of one of those structures.
- Again, just as in the function prototype, if we were to use a
structure in two different files we would not manually type the
declaration into each file; we would create a header file and
include it in every file where it was needed.
- If we pass a pointer to a function then when we change
proj.mass inside the function we are changing the
original object;
It so happens that all of the members of the above were of
the same type (float), but structures can
contain all types including arrays and other structures.
Also, we can use a little piece of syntactic sugar to ease our mental model:
typedef struct aardvark {
float mass;
float age;
float inside_leg[4];
int social_security_number;
struct ant diet; /* Struct ant must already have been declared */
} Aardvark;
// 'Aardvark' is now a synonym for 'struct aardvark':
main() {
Aardvark pinky;
struct aardvark perky;
}
Structures allow extensibility
Our simple example only has one dimension, y. But if our program is
successful somebody is bound to ask us to extend it to three
dimensions in which case
y,
vy and
ywind
will be replaced by arrays. If we used the "separate variables"
approach we would have to go through each of our functions changing
the number (and type) of arguments.
With the "structure" approach, we just add some more members to the
structure definition, change the part of the code responsible for
calculating the acceleration and recompile.
Did somebody mention spin?
Real objects spin in the air and have texture. Even if we assume a
spherical shape, that's several more variables. And somebody is sure
to want to throw non-spherical objects. In this case our
height_velocity function is going to split into two parts:
- An up-market numerical algorithm for moving the projectile.
- An function for calculating the force on the object (and
hence its acceleration), taking into account shape, spin, etc.
But if we use structures, the function definition remains unchanged.
Structures help make our functions more modular
The spin/texture/shape argument above shows that, if we chose to go
that way, our acceleration function could become very complicated
but
our function call remains unchanged. Thus,the rest of our code doesn't need to know about it.
A simple example
This rather simple example shows a function whose job is to calculate
and print out the area of an ellipse. (In practice this function is so
simple it probably isn't worth making a separate function.)
#include <stdio.h>
typedef struct ellipse {
float centre[2];
float axes[2];
float orientation;
} Ellipse;
void print_area(Ellipse el);
int main() {
Ellipse ellie;
int i;
printf("Centre? (x,y)\n");
scanf("%f %f", &ellie.centre[0], &ellie.centre[1]);
for(i = 0; i < 2; ++i) {
do {
printf("Length of axis number %d ( > 0 )?\n", i + 1);
scanf("%f", &ellie.axes[i]);
} while (ellie.axes[i] <= 0);
}
printf("Orientation to the vertical?\n");
scanf("%f", &ellie.orientation);
print_area(ellie);
return 0;
}
void print_area(Ellipse el) {
#define PI 3.14159265358979
float area = PI * el.axes[0] * el.axes[1];
printf("The area is %f\n", area);
}
Executive summary
- If you have a whole load of variables referring to the same
object stick them all into a structure.
- Organise the data into structures according to the object they
refer to, along the same principles as using functions to organise
your code and according to the same criteria: how to best
minimise the amount you have to keep in your head at any one
time.
- A structure contains everything you need to know about that
object. When you add new properties to an object you can easily
extend your structure definition and the parts of the code that don't
deal with that aspect of it don't need to know about it.
There is, of course, an obvious weakness with what we have learnt so
far: we know how to pass the values of a structure into a function but
we have not yet learnt how to change them within the function and to
pass them back to the calling function. We shall see how to do this
next week.
Reference
Brooks, F. R. Jr, The Mythical Man-Month
(1975).