Skip to content
School of Physics
Home Our Teaching Resources C programming structures.html
Back to top

Computational Physics: Structures organise our data

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,

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 our head at the same time. Functions organise code into self contained units, enabling us to think at a higher level without having to keep track of the details. Structures do exactly the same for variables.

Example: a nuclear problem

Imagine a simple problem involving radioactive nuclides. If each nuclide has a name (stored as an array of characters) and a a half life (stored as a double) then to storethe data for several nuclides we will need a two-dimensional array of characters and a one-dimensional array of doubles.

This isn't very convenient, but C doesn't define a composite variable type consisting of a string and a double and even if it did, it's likely that a little later on we would want to add more data such as mass and atomic number.

It would be great if C allowed us to define our own variable types, with names such as "Nuclide" or "Isotope", and luckily it does. The following code defines a composite variable called a "Nuclide":

#define MAXLEN 64
typedef struct nuclide {
  char name[MAXLEN];
  double halflife;
} Nuclide;

How it works

The above combines two C feautures: structures, which actually do the work, and typedefs which give user-friendly names to things which already exist.

Structures

The following is the bare structure definition which defines a new variable type, "struct nuclide":
struct nuclide {
  char name[MAXLEN];
  double halflife;
};

We can now create variables of type "struct nuclide" just like ints, floats, etc.

typedef

Since "struct nuclide" is not the most user-friendly name for a new variable type, we can make things a little neater by using C's "typedef" mechanism to give our structure a neater name:

The structure name (nuclide) is optional but if it is present it's normal to give the structure and the typedef similar names. In our case the typedef name is the same as the structure name but is capitalised.

typedef struct nuclide {
  char name[MAXLEN];
  double halflife;
} Nuclide;

Now "Nuclide" and"struct nuclide" are synonyms; they mean the same thing.

Declaring actual nuclides

If we have the above code just once at the top of our file then Nuclide joins the list of variables we can now declare, just like ints, floats, etc. We can also have arrays of them:

#define MAXNUCLIDES  120
int main() {
  int i;
  double x;
  Nuclide my_nuclide;
  Nuclide many_nuclides[MAXNUCLIDES];
  ...
}

Notice how we have created both a single Nuclide and a whole array of them.

Before seeing how we use structures, let's look at another example and see another advantage of using them.

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 allow us to roll 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. Again, this code does not actually create any structures, it just tells the compiler what we mean by "Projectile" and 'Air'.

typedef struct projectile {
  float y;
  float vy;
  float mass;
  float drag_coeff;
} Projectile; 

typedef struct air {
  float ywind;
  float viscosity;
} Air;

We have gone from seven numbers to three things: projectile, air and time.

The function height_velocity is now declared as:
void height_velocity(Projectile proj, Air thisair, 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.

Using structures and their members

When we decalre a structure variable we can access it's components (or "members") by using a dot ('.') followed by the member name.

Projectile example

void myfun(void) {
  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 height_velocity function would then contain lines like:
proj.y += proj.vy * dt;

Nuclide example

Returning to our original nuclide example, here we read in the names and half-life of a single Nuclide:
#define MAXNUCLIDES  120
int main() {
  Nuclide my_nuclide;
  
  printf("Please enter the name and half life\n");
  scanf("%s %lf", my_nuclide.name, &my_nuclide.halflife);
  ...
}

and here an entire array of Nuclides:

#define MAXNUCLIDES  120
int main() {
  int i;
  Nuclide many_nuclides[MAXNUCLIDES];
  
  for(i = 0; i < MAXNUCLIDES; ++i) {
    printf("Please enter the name and half life of nuclide %d\n", i);
    scanf("%s %lf", many_nuclides[i].name, &many_nuclides[i].halflife);
  }
  ...
}

Another example: a game

If we were programming a two-player game we may well want a data structure for each player, for example:

typedef struct player {
  char name[MAXLEN];
  int score;
  char token;
} Player;

and we may (or may not!) decide to have a global array of two Players since the information will be used throughout the program:

Player players[2];

We can then refer to player[0], player[1], player[j], etc.

Recap

  • The structure definition defines the meaning of 'Projectile', 'Nuclide', etc. but does not create any. It occurs just once.

    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.

  • Afterwards we use this information to actually create some nuclides and projectiles.

  • The individual members of the structure are accessed as proj.mass, etc. We can treat each member as an ordinary variable in exactly the same way that we 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.

Structures let us think at a higher level

Almost without noticing it, we've made bit of a mental leap. We started by thinking about how we could reduce the number of arrays needed to represent our nuclides or reduce and organise the (floating point) arguments to a function. But we have quickly reached the stage where we have stopped talking about integers, floating-point numbers and strings and have started talking about nuclides, projectiles, and players.

This is the point about structures: we identify the types of "things" we are dealing with and typically we define a type of structure to represent that type.

Structures allow extensibility

We have already mentioned that our nuclide information may need to be extended to include its mass and atomic number.

Our simple projectile 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.

Later on we might need to deal with the fact that real projectiles 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.

Similarly, for our game program we may wish to extend our player structure to include the number of games they have won, drawn and lost and their high score.

It's extremely common for a program to start off quite simply but for more features and properties to be added later, so the question of extensibility is hugely important.

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 we 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 our code and according to the same criteria: how to best minimise the amount we have to keep in our head at any one time.

  • A structure contains everything we need to know about that object. When we add new properties to an object we can easily extend our 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. Functions receive copies of their arguments, if we change the copy inside the function the original is unchanged. So 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 those changes back to the calling function.

There's a less obvious weakness too. When nuclides decay they don't just vanish, they produce other nuclides. In other words, nuclides have relationships between them. How do we represent these relationships within a structure?

We shall see how to do both of these next week.

Reference

Brooks, F. R. Jr, The Mythical Man-Month (1975).

                                                                                                                                                                                                                                                                       

Validate   Link-check © Copyright & disclaimer Share
Back to top