Skip to content
Physics and Astronomy
Home Our Teaching Resources C programming PHY3134 revision.html
Back to top

Computational Physics - Revision

The computer stores the values of its variables in its memory

At the core of any computer are its processor unit (CPU) and its memory (RAM - for Random Access Memory). Memory is measured in bytes and you can think of it being arranged in a block with the first byte numbered one, the second two, etc..

Typically it takes four bytes (but may be as little as two bytes on some systems) to store an int, so when the computer encounters a line like:

int i, j;
it reserves two chunks of four bytes each to store the values of these two variables:
              -------------------------------------------------
Byte number:  | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 |
              -------------------------------------------------
Used for:     | <-------- i --------> | <-------- j --------> |
              -------------------------------------------------
In this example and all the examples below we have assumed four byte ints and we have arbitrarily assumed that the compiler has decided to put i and j next to each other with i beginning in memory location 400.

Notice how, as far as the computer is concerned, references to variable names such as i and j are just ways of referring to a particular part of the memory.

Steps for the simple Assignment.

eg j = i + 1;
  1. Read the values stored in the memory assigned to the variables on the right hand side of the statement into the central processor.
  2. Perform the calculation, keeping the result in the processor.
  3. Store this value in in the memory assigned to the item on the left hand side of the statement.

Short cuts

C programmers love to express things compactly. Some examples:

Long formShort form
y = 1;
x = y;
x = y = 1;
x = x + 1; ++x;
x = x - 1; --x;
x = x * y x *= y;
x = x + y x += y;

You can use the *= type notation for any of +, -, *, /, <<, >> or % .

Experienced C programmers use the above notations all the time, both because they express a simple concept ("x equals y equals one", "multiply x by y") very simply and because these expressions arise so frequently that you very soon get to recognise them.

You will never see a line like any of those in the left hand column in a 'real' C program and for this reason, we shall require you to use these shortcuts wherever they appear in a statement on their own.

When they appear in combination or as part of a more complicated statement you should feel free to use either form, depending on which you feel is clearer. For example, either of:


  y *= x;
  x = y;
or

  x = y *= x;
is fine. Personally I feel the two line version is clearer. Use shortcuts to make things simpler and clearer, not more confusing.

The % operator

If i is a positive integer and j a strictly positive integer i % j gives the remainder:
7 % 2 == 1
9 % 5 == 4
6 % 3 == 0
It's best not used with negative numbers.

Arrays

Arrays give you the ability to define a collection of variables all of the same type and to be able to refer to them all by the same name with a numerical subscript as in this rather silly example:

main()
{
  int i, iarray[8];

  iarray[0] = 1;
  iarray[3] = 7;
  i = 2;
  iarray[2*i+1] = iarray[3];
}
Notice a few things:
  • Array elements start from zero and go up to one less than the numbers of elements in the array.
  • You can use an array element anywhere you may use an ordinary variable.
  • The contents of the square brackets [] may be any integer expression provided its value lies within the legal range.
  • This example leaves the values of array elements 1,2,4,6 and 7 completely undefined.

When you specify an array of length, say, ten the machine reserves a contiguous block of memory large enough to store the values of ten variables - forty bytes in this case. iarray[0] then refers to the first four byte chunk, iarray[1] to the second four byte chunk, and so on:

              -------------------------------------------------
Byte number:  | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 |
              -------------------------------------------------
Used for:     | <---- iarray[0] ----> | <---- iarray[1] ----> |
              -------------------------------------------------
This is why you can use an array element anywhere where you use a variable - to the computer they are both just ways of specifying a chunk of memory and it doesn't care how you did it.

Multidimensional arrays

Arrays can have as many dimensions as you like as in this snippet:

  int a[3][2];  /* Two dimensional array of integers */

  a[2][1] = -10;
If the compiler decide to store the array starting at byte number forty in its memory the elements of the array would then stored in memory as:
  ------------------------------------------------------------------
  | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
  ------------------------------------------------------------------
  | <--- a[0][0] ---> | <--- a[0][1] ---> | <--- a[1][0] ---> | etc.
  ------------------------------------------------------------------
Notice the order it stores them in: just as the number 10 is followed by 11 not 21 so a[1][0] is followed by a[1][1] not a[2][0].

NB: statically-declared multidimensional arrays like these are used much less than dynamically allocated ones which we discuss in the next lecture.

Characters and strings

In C single characters are contained in single quotes ('a'), strings of characters in double quotes("abc"). The special value zero ('\0') means "end of string". ('\0' is very different from '0'!)

Strings are stored as an array of characters:


char string[4] = "abc";
Notice that string is an array of length four, not three. Its elements have the following values:

string[0]'a'
string[1] 'b'
string[2] 'c'
string[3] '\0'

If statements

An if statement consists of a logical test followed by a block of statements that are only executed if the test is true (ie non-zero). The simplest if statement looks like this:

  if (i == j) {
    /* Statements here only get executed if i is equal to j */
  }
(The notation == for "logical equals" is slightly confusing:
i = j means "make i equal to the current value of j", whereas
i == j is a test for "is i equal to j?".)

If the statement in parentheses after the 'if' is true the statements inside the curly brackets (called braces) are executed, if not they are ignored.

It is possible to specify alternatives if the first statement is not true:


  if (i == j) {
    /* statements here */
   }
  else if ( i < j ) {
    /* More statements here */
  }
  else if (i == 0 && j > 1 ) {
    /* More statements here */
  }
  else {
    /* Only executed if all the others fail */
  }
Note:
  • The braces are optional if the content of an if block consists of just a single statement.
  • The else if and else statements are optional
  • if .. else if statements specify alternatives - if more than one test is true only the first is evaluated - the rest are skipped.

A common mistake

  if (i = j) {
    /* statements here */
   }
This is perfectly legal, what does it do?

Relational operators

C provides the following relational operators:

!
not, e.g. ! (i == j) is equivalent to i != j)
<  <=  >  >=
less than, less than or equal to, etc
==  !=
equal to, not equal to
&&
and
||
or
eg:

if ( b*b > 4*a*c && a != 0 )
  return (-b + sqrt(b*b - 4*a*c))/(2*a);
In the above list operators on the top lines have higher precedence (bind more tightly) than the lower lines. Items on the same line have equal precedence. For example:


 i == j && j < 0 ||  i != j && j > 0
is equivalent to:

 (i == j && j < 0) || (i != j && j > 0)

C doesn't bother to have a separate data type for logical expressions, it just treats them as integers. The logical expressions above return the value one if they are true and zero if they are false. Conversely, C treats any integer expression with a value of zero as being false, any other value is treated as true. If the expression given is not an integer it is converted to one.

The switch statement

The switch statement chooses between one of several (integer) alternatives.

  switch (i) {

  case 'a':
    printf("a\n");
    break;

  case 'b': case 'c':
    printf("b or c\n");
    break;

  case 'd':
    printf("d\n");
    /* Fall through */

  case 'e':
    printf("d or e\n");
    break;

  default:
    printf("Something else\n");
  }
Something of a mis-feature is the fact that by default control falls through to the next case (as in the case 'd' above). Personally, if I want this to happen I comment it as above.

Loops

The while loop

A while loop looks like this:

  while (expression) {
    /* Commands in here */
  }
Here expression is any integer expression, eg i+j or k != m, just as in the if statement above. When the program runs, if the value of the expression is zero the entire contents of the braces {} are skipped, if the value of the expression is anything except zero they are executed. In the latter case the compiler then goes back, re-evaluates the test expression and tries again. Naturally, if the contents of the loop do nothing to change the value of the controlling expression the loop will continue for ever!

    Steps for the while loop

    1. Evaluate the test expression.
    2. If the test was true, execute the statement block, otherwise quit.
    3. Go back to 1.

The do .. while loop

The do .. while loop does the same as the while loop but in the opposite order in that the block of commands is executed before the test is made. Thus the contents are always run once even if the test is false.

  do {
    printf("Please enter an integer from 1 to %d\n", j);
    scanf("%d", &i);
  } while (i < 1 || i > j);
The do .. while loop is not used very much, the above is the most common case.

    Steps for the do .. while loop

    1. Execute the statement block.
    2. Evaluate the test expression.
    3. If the test was true, go back to 1, otherwise quit.

The for loop

The for loop consists of an initialisation, a test and an update (which is executed after the statement block) all in one statement. We will show this loop with a single statement in the loop, allowing us to omit the optional braces:

  for (i = 0; i < 10; ++i)
    iarray[i] = i*i;
This is a classic use of the for statement - doing something on each element of an array. Notice:
  • Just like the while loop, the test is done before the statement block is executed.
  • If the test is false to start off with then neither the statement block nor the update are executed.
  • The order within the for statement is the order in which they occur:  for (initialisation; test; update)

    Steps for the for loop

    1. Perform the initialisation.
    2. Evaluate the test expression.
    3. If the test was false quit.
    4. Otherwise, execute the statement block and
    5. perform the update operation and
    6. go back to 2.
Neither of these loops does anything that you couldn't do with a while loop, it's just that the situations they describe occur sufficiently frequently to make it worth having special statements for them.

Infinite loops

Either of these gives you a loop that never finishes:

while (1) {
  /* Infinite loop here */
}


for(;;) {
  /* Infinite loop here */
}
The first is more intuitive but the second is the more commonly used. It uses the fact, not mentioned before, that any of the three elements of the for statement may be omitted and that if the logical test is omitted it defaults to TRUE.

Functions

A typical function in C looks something like:

float fun1(int n, float x) {
  int i;
  float z, myarray[2*n];

  /* Main body of function here */
  
  return(z);
}
and can be called from another function as part of an expression as in this example:

   a = x +  fun1(m, b);
   y = x +  fun1(2, sin(theta) );
This particular function returns a value (a float) but others do not. In C even the main routine is a function, called main.

Variables inside functions disappear when the function returns

None of the variables defined within the function (in this case the variables i and z and the array myarray) existed before this function was called and when the function returns they will go away again and their values can never be recovered. If fun1 is called again later they will not start off with the values they had when they last left fun1.

Variables defined inside functions are called automatic variables because they are automatically created and destroyed as needed.

A classic bug

char *badfun(some args) {
   char string[12];

   /* Write something to the string */
   return string;
}

Functions receive copies of their arguments

In the above example, when you called fun1 the values of m and b in the calling routine are copied to new temporary variables n and x inside fun1. When fun1 exits n and x will be lost along with i and myarray. This is more obvious in the second example where it's hard to even imagine how you could change the values of 2 and sin(theta).


#include <stdio.h>

void addem(int i) {
  i = i + 2;
  printf("Inside addem i is now: %d\n", i);
}

int main() {
  int i;

  i = 317;
  printf("Before addem i is: %d\n", i);
  addem(i);
  printf("After addem i is: %d\n", i);
}

Automatic variables are very convenient

  • The calling function is easier to understand because there is no danger that fun1 can change either m or b.

  • The machine takes care of the memory bookkeeping for you.

    Computers only have a limited amount of memory and if every time fun1 was called it created a brand new set of variables and didn't destroy them when it returned then the amount of memory being used by the program would increase every time fun1 was called until the computer ran out of memory. By deleting them when the program leaves the function it can reuse the same memory time and time again.

  • fun1 is able to call itself and each invocation has its own values of all its variables, etc. This is known as recursion (as in this simple example on the Web).

  • The process of allocating and deallocating space for these variables is very quick.

Automatic variables have their limitations

  • There is no way fun1 can change the values any of the variables in its calling routine. Although you don't normally want to do that there is always the odd occasion when you do want to. You have already met this problem using scanf when the answer was to put '&' in front of the variable name and later we will discuss what this means.

  • Sometimes a routine does need to create a permanent new variable or array which can then be used by the rest of the program and automatic variables cannot do that.
We note in passing that many of limitations, and more importantly all of these advantages, do not apply to variables defined outside of functions. These are called external variables and we will discuss them a little later in the course.

Prototypes check function arguments

It's surprisingly easy to get the arguments to a function wrong when calling it. For example you might miss one out, or get them in the wrong order. A little later you may add a new feature to the function which causes it to require another argument but you might forget to add this argument to one of the places where it is called.

Fortunately, you can get the compiler to check that you have the right number of arguments and that they are in the right order:

  • If the function and all the places it is called are in the same file and the function appears before all the places it is called then the compiler will do this automatically.

  • If the function is in a different file from where it is called, create a file called, say, decls.h, containing the line:
    
    float fun1(int n, float x);
    
    This is called a prototype and it tells the compiler what type of arguments fun1 requires and what type of value it returns. If you prefer you can miss out the 'n' and the 'x' and just write float fun1(int, float); . Now in the file where fun1 is defined and in all the files where fun1 is called add the line:
    
    #include "decls.h"
    
    at the top. The compiler will behave as if the contents of decls.h had actually been written in the file at that point and will use the prototype to check the arguments. Naturally, you should do this for each of the functions you define.

  • If the function and all the places it is called from are both in the same file but you prefer to have the calling routine first you can either use the previous method or just add the prototype before the first function definition in the file.
If you fail to use one of these methods to ensure that the compiler checks your arguments for you you will lose marks.

Of course, this checking has one obvious limitation: if a function requires two arguments of the same type the compiler has no way of knowing if you have put them in the right order or not.

                                                                                                                                                                                                                                                                       

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