Arrays or "numbered variables"
Comments and questions to John Rowe.
Warning: do not scroll past the heading
"What is an array?" until you have answered the
warm-up questions.
As scientists we often deal with vectors of length N with
coefficients V1, V2, Vk,
etc.
Clearly it would be a nightmare to have to declare N
separate variable with names like V1, V2,
V7. Instead we need to be able to declare
a "super-variable" to represent the whole vector and to be
able to use expressions such as Vk to
access the element we want. C's arrays
allow us to represent vectors and matrices directly.
Locations as mathematical expressions
If we want to have a single "super-variable" to
represent a whole vector of doubles
then each value of Vk
will need four or eight bytes to store it. It follows that the
address of Vk must be a mathematical
expression that depnds on k. That is,
we are moving from machine code
that looks like double@constant to machine code
that looks like
double@(expression) and that expression
must depend upon k.
Wider use
Although we tend to think of vectors and matrices
in their mathematical sense with operations such as
division, multiplication etc. there is also a more general
non-mathematical use of "Thing number 1", "Thing number 2",
which use the same underlying mechanism. Indeed modern programs can
easily use hundreds of mega-bytes or even gigabytes of memory
and only a tiny fraction of that is for named variables with
addresses that are constants that are known at compile time.
Most data is accessed via machine-code that looks like
typename@(expression),
just like the vectors described above.
This means that
discusing how vectors, and to a lesser extent matrices,
are implmented and passed to functions
represents a good introduction to a concept that is extremely
important in C programing and which will form the basis of
much of the next three lessons.
Warm-up discussion 1: Storing vectors
Suppose we wish to store the elements of a vector as doubles
the computer's memory and it turns out the the value of the
first element of the vector is stored at location 400.
- If the first element of the vector is stored at location 400 where would be the simplest and
most logical place to store the second element? (Remember
that doubles require eight bytes each)
- Where would be the simplest and most logical place to
store the third element?
- Provide a simple mathematical expression for the simplest
and most logical place to store the Kth element.
Warm-up discussion 2: numbering things
Since arrays are all about numbered variables, let's briefly
think about how we number things.
- What was the first year of the twenty-first century?
- If this is the twenty-first century, why do the years
start with "20"?
- Is "12 pm" twelve noon or twelve midnight? )
- What's your reason for the previous answer?
Although intuitively our reaction is usually to number things
starting from "1", as these warm-up discussion show, this
tends to lead to problems later on. Therefore all modern
numbering systems tend to start from zero. For example, the
twenty-four clock numbers its hours from 0-23 (not 1-24) and
its minutes and seconds from 0-59.
We shall return to this a little later in the lesson.
Hopefully in the above discussion you decided that if the
first element of an array of doubles is stored at address 400
then the logical place to store the second element was at
location 408!
An array is a series of
objects of the same type stored sequentially in the computer's
memory.
We can make arrays of anything we like, such
as arrays of ints, arrays of doubles,
arrays of double complexes etc. As we shall see
below we can even make arrays of arrays! (Otherwise known as
two-dimensional arrays.)
All arrays have both a type and a length.
Arrays are used where we have a set of things of
the same type and we need to access each element by number,
not by name.
Coordinates, vectors (and later, matrices)
When referring to the position of a point in space we use a
vector of three coordinates, e.g. (1.0, 1.0, 2.3).
More generally, we refer to the components on an n-dimensional
vector, V, as Vi where i is
an integer, and a matrix, M, is a two-dimensional
array of numbers. Mi j.
It is usually better to use an array for
coordinates than individual variables x, y
and z.
Arrays give us a convenient way of defining these and
referring to their individual components, or elements, as v[i],
m[i][j], etc.
If set up an array called, say myarray of length
twenty, we can then treat any of its elements, such as "myarray[4]",
"myarray[17]" just like a variable. We can also use
expressions such as "myarray[i]", etc., which is why
arrays are so useful.
Arrays can allow us to think at
a higher level, in terms of vectors and matrices rather than
their individual components.
Sequences of values
If we are looking at the population of a country over time we
may have an array of integers, one for each year. Or we may be
measuring or calculating a quantity that changes with time and
we will have an array of doubles to store the values at
different points in time. In each of these examples if we want
to do something for every member of the sequence we can do it
with a loop over the indices rather than just repeating the
same code many times over.
Arrays help us to avoid "copy-paste-and-edit
loops" in much the same way as functions do.
The obvious, although not the only, way to
create an array is simply to declare it in the same way we
would declare an ordinary variable. C uses square brackets [] to designate the number in an array, both
when declaring it and when accessing the individual elements.
Example: declaration and simple assignment
The following code sets up an array of three
doubles and then sets the value of its elements.
#include <stdio.h>
int main() {
double sides[3];
sides[0] = 5.2;
sides[1] = 3.1;
sides[2] = 4.6;
return 0;
}
Step through this code
The first statement ("double sides[3];")
reserves enough space to store three values, in this case doubles.
Arrays can have any type: int, long, etc.
We have given the array the name sides, which
suggests we are dealing with a triangle. The following lines
set the values of the three individual elements. The number
inside the [] is referred to as that element's index and can be any integer expression,
not just a constant.
- int myarray[N] (note
the typename "int") declares a brand new array of N ints.
- myarray[j] on its own (note no "int") refers
to one particular element of the array.
- Step through the above code
- To show how arrays are used and how they are stored in
memory.
- Step through the above code, seeing how:
- The array is declared.
- The individual elements are accessed.
- Reload the page and select either "Combined view" or
"Computer view". The main difference from what we have seen
in previous codes is that the addresses are expressions rather than constants.
- Check that way the compiler is storing the elements of
the array is the same as your answer in the preparation
section (bearing in mind that the first array element is
numbered zero.)
- Step through the code seeing how the address maths picks
out each element in turn.
The first element of the array has index zero, the last has
index N-1
As can be seen above, C numbers its arrays from zero. Although intuitively our
reaction is usually to number things starting from "1", as the
warm-up discussion shows, it tends to lead to problems later
on. Therefore all modern numbering systems tend to start from
zero. For example, the twenty-four clock numbers its hours
from 0-23 (not 1-24) and its minutes and seconds from 0-59.
If we have an array of size N and
the first element has index zero then the last element must
have index N-1, not N!.
The classic array error
Be sure to remember this one!
double sides[3];
sides[3] = 1.7; /* WRONG ! */
This is a particularly
nasty bug which C does not check for. Trying to
access the N+1th value of an array of size N
is like standing three steps away from a cliff-top and taking
four steps forward.
The elements of an array of size N are array[0]
to array[N-1] inclusive.
Using scanf()
We can use scanf() to read in sides[0]
and sides[1] just like a variable (see the complete example):
scanf("%lg %lg", &sides[0], &sides[1]);
Here &sides[0] does what we want it to: it's the
address of sides[0] .
Arrays can have their values initialised at
the point of declaration to make it more difficult to forget
to miss one out. This is done using { ... }
:
double sides[3] = { 5.2, 3.1, 4.6 };
We have already seen { ... } used to
group several statements together,
here they are used to group several initialisers
together.
Arrays can be inialised on declaration by putting
the values inside { }
int myarray[3] = { 1, 2, 3 };
Two convenient array initialisation short cuts
Automatic array sizing
If we miss out the array size the compiler infers the array
size from the number of initialisers:
double sides[] = { 5.2, 3.1, 4.6 };
The compiler can infer the array size from the
number of initialisers.
Missing initialisers are treated as zeros
Conversely, if we provide fewer initialisers than required
the rest of the array elements are initialised to zero:
int myarray[3] = { 1 };
Here we have shown an array of integers for variety. myarray[0]
is initialised to 1, the other two elements to zero (not
one!!). This does not apply to
uninitialised arrays, only to arrays with fewer
initialisers than values. To initialise an array to all zero
use:
int anotherarray[3] = { 0 };
Missing initialisers are treated as zeros but
this does not apply to uninitialised arrays.
The
for() loop:
for(Initialise
; Test
; Increment
)
- Initialise
- Loop:
- Test and if false quit the
loop.
- Execute the body.
- Increment.
In the example above the array indices were constants (0, 1
and 2). Whilst this is of course legal, the whole point of
using arrays is that we can use indices that are expressions.
A common example is looping over all the elements of an array
in turn. We can use the " for()" loop to loop over
an entire array, in this case to read it in:
#include <stdio.h>
int main() {
int numbers[10], k;
for (int i = 0; i < 10; ++i ) {
printf("Please enter value %d\n", i);
scanf("%d", &numbers[i]);
}
for (int i = 0; i < 10; ++i )
printf("Value %d: %d\n", i, numbers[i]);
return 0;
}
Step through this code
The for(Initialise; Test; Increment)
loop can be used to loop over the elements of an array.
Remember: we don't give ourselves the
opportunity to go wrong!
In this code the size of the array (10) is hard-coded in two
different places. This is sometimes referred to as a "magic
number": it's just there and we have no idea why. If we were
to change the size of the array to 12 we would have to change
the limits of our loops to 12 as well. Worse, there may be
other occurrences of the number 10 that we don't have to
change, as they have the value 10 for a different reason. And
if we see the number "20" we will need to ask if it should now
have the value "24".
The following # (preprocessor) line allows
us to give a name to a constant:
#define VALUES 10
- Any subsequent occurrence of the word VALUES
(outside of text strings) is replaced by the rest of the
line; in this case "10". Note there is no semi-colon!
- Like all "#" directives, it is line-based with
no semi-colon
- It's conventional to use ALL UPPER-CASE letters, and
digits for symbolic constants.
We shall expect you to
always use symbolic constants for array dimensions,
except for applications where it is logically impossible to
change the array size, for example when dealing with a
triangle.
NB: #define can be used for non-numerical constants as well.
Use #define to avoid "magic constants".
Example
#include <stdio.h>
#define NSTARS 10
void loopdemo2(void) {
double mass[NSTARS];
for (int star = 0; star < NSTARS; ++star ) {
printf("Please enter mass of star %d\n", star);
scanf("%lg", &mass[star]);
}
}
Step through this code
The original version of the C language
required the dimensions of an array to be constants, defined
when the program was written but this restriction has now
been relaxed. It is occasionally useful to be able to write:
#include <stdio.h>
void myfun(int n) {
int x[n];
for(int i = 0; i < n; ++i)
scanf("%d", &x[i]);
}
int main() {
myfun(2);
myfun(6);
return 0;
}
Step through this code
Arrays serve as a useful introduction to
accessing data via its memory address rather than by name.
This is (in)famously the most confusing part of C for
beginners which is why we are taking the chance to look at
this in a little more detail now.
We have already said that an array is a sequence of
objects stored one-after the other in the computers memory.
For example, if we have an array of four-byte integers called
iarray, let us suppose the compiler has chosen to
store its first element, iarray[0] at byte 640. Then
iarray[1] will be stored at byte 644, iarray[2]
will be stored at byte 648, etc:
-------------------------------------------------------------------------
Byte: | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | etc.
-------------------------------------------------------------------------
Stores: | <---- iarray[0] ----> | <---- iarray[1] ----> | <---- iarray[2] ----> | etc.
-------------------------------------------------------------------------
This method arrayname[index] is
specifically designed to provide a natural and easy method for
referring to array elements.
Hopefully our answers to the initial questions in this lesson
should have lead us to the conclusion that the "computer's
view" of the expression for element j of an array of doubles
starting at location 400 is float@(400+8*j),
or in the above example the computer's view of iarray[j]
is int@(640+4*j).
C being C, it provides two simple rules for first finding
the start of the array, and second going along it and finding
what is there.
Rule 1: In an expression C treats the name of an array as
meaning the address of its first element
Expressions have both a value and a type.
When (the of the name of) an array of Things is used
in an expression it is replaced by the address of its first
element, and its type is the address of a Thing (or
"pointer to a Thing") and is a constant.The type is
used both for traversing the array and for working out what to
do when it gets there.
So in the above example iarray is a constant and
in the example above, has the value "640" and type "address of
an int". We can even print out the value of iarray
using the "%p" format used for addresses.
The name of an array is a
shorthand for the address of its first element (in both value
and type).
myfunction(arrayname);
myfunction(&arrayname[0]);
Rule 2: If a is the address of a Thing
then a[k] means "the Thing whose address is
(a + k * the number of bytes needed to
store a Thing)"
If a is the address of a Thing
then a[k] means "the Thing whose address is
(a + k * the size of a Thing)".
This combination of these two rules is what makes the "ar[n]"
syntax work: if ar is an array of ints
then the construct "ar[n]" is just a synonym for "go
to location ar and move along by n times
the size of an int and see what's there".
One result of this is that unlike some more specialised
languages, C has no way of referring to an entire array (only
its type and the address of its first element) and does not
allow whole-array operations:
int myarray[N], yourarray[N];
...
yourarray = myarray; // Wrong!
for (i = 0; i < N; ++i) // Correct
yourarray[i] = myarray[i];
Note: this rule is
both more simple and more subtle than it may first appear. For
a variable x then the
value of x is found by going to the address of x
(in the computer's memory) and seeing what is stored there.
For an array ar the "value" of ar is just
the address of ar[0] which is a constant
and the content of the computer's memory is not looked at.
The array type is used:
- To know how many bytes to move along when traversing
the array.
- To know how to interpret the bytes stored there.
Notes
- No array bound
checking. The above notes have the disconcerting
property that the compiler can follow the rules without
needing to know the length of the array! In fact for a
one-dimensional array the declaration is the only place the
array size is used. The instructions generated by "ar[k]"
are entirely independent of the length of the array and it
is our responsibility not to go over the edge.
- Address offsets are always multiplied by the size of the
object it is an array of. If C did not do this then for an ar[6]
would be size bytes from ar[0], so for an
array of doubles this would land us into the middle of the
first element, which would be a disaster.
- The address[index] notation is specifically
designed for use with arrays as it provides a simple way to
say "go to this address, move along by this much and see
what's there" (eg double@(address + 8*i)).
We shall see in the next lesson an simpler
notation for the situation where we just want to say "go to
this address and see what's there", (eg double@address
rather than
double@(address + 0*i).
After an array has been declared the only thing
the compiler knows is the address and type of its first
element.
There is no array bound
checking, we are responsible for this ourselves.
Stepping over the edge of the cliff
If in our previous example we assume that ar had
dimension 10, then the compiler would have reserved forty
bytes (640 to 679 inclusive) to store it. This prompts the
question: "Exactly what happens if I try to assign a value to
the eleventh element, ar[10]?" The answer is that
the computer will try to write four bytes to location 680. In
other words, it doesn't do a check for us.
This will most likely result in one of two things:
- The variable whose address is 680 will have its value
mysteriously changed. This tends to lead to random and
confusing behaviour.
- This will be a piece of memory we are not allowed to
access and the program will die with a "segmentation fault"
or SIGSEGV as it is also called.
Exactly the same will happen if we pass the wrong address to
scanf(), or we try to access a file we have opened
with fopen() that we have not checked for being NULL.
If we go a bit over the end of
an array we will change the values of random variables, if we
go a lot over the end we are likely to crash the entire
program.
SIGSEGV or randomly-changing variables
are likely to be due to going over the end of an array, bad
arguments to scanf() or failing to check for failed
calls to fopen().
One of the main reasons for the rule "The name of the array
is equal to the address of the start of the array" is that
having declared an array in one function, we often want to
"pass that array to another function".
We mentioned above that the name of an array (say x)
is just a synonym for the address of its first element. This
has the some-what counterintuitive effect that any time we see the
name of the array x on its own it's exactly the same
as writing &x[0]:
double x[N];
myfunction(x);
is exactly equivalent to:
double x[N];
myfunction(&x[0]);
Example: normalising a vector
A common requirement in science and maths is
to normalise a vector, that is scale it so that its magnitude
is one. The following code normalises an array of N doubles:
double norm = 0;
for(int i = 0; i < N; ++i)
norm += a[i]*a[i];
if ( norm > 0) {
norm = sqrt(norm);
for(int i = 0; i < N; ++i)
a[i] /= norm;
}
(Note: the statement a[i] /= norm;
means "divide a[i] by norm".)
But suppose we have three vectors to
normalise, a, b and c? We could Copy-and-Paste the code and
then replace "a" by "b", and then again for "c". But this
would lose us a lot of marks(!). What we would really like to
do is to be able to write a function to normalise a vector and
then write something like:
normalise(a);
normalise(b);
normalise(c);
This is far from a theoretical example: vectors and matrices
are very important in science and there is a wealth of
libraries to do high-level operations such as inverting
matrices, solving systems of linear equations, etc. A language
that did not let us write functions to manipulate vectors and
matrices would be a serious problem for scientists. Notice
also that normalise() needs to work on the original
arrays a, b and c. It would be
no use passing it a copy of the array and normalising the
local copy.
In order to work on the original array the
function normalise() will need to know where it is
stored in memory. Fortunately rule 1 says:
The name of an array is a shorthand for the address of its first element (in both value
and type).
That is to say that in the three calls to normalise()
above we are passing the function the
address of the first element of the array, just
like we pass an address to scanf(). And of course,
if normalise() knows the type and location of the
first element it knows the location of the second, the third
and so on. (But it doesn't know where it ends unless we tell
it!)
Kernighan and Ritchie wrote the book
("K&R") that first defined the C language.
When an array name is passed
to a function, what is passed is the location of the beginning
of the array.
[Kernighan and Ritchie: The C Programming Language]
Writing the normalise() function
The body of the function will look exactly the same as above,
although for clarity we shall rename "a" to "p". It will look a
bit like this, although we are not yet sure what the parameters
will look like:
void normalise() {
double norm = 0;
for(int i = 0; i < N; ++i)
norm += p[i]*p[i];
if ( norm > 0) {
norm = sqrt(norm);
for(int i = 0; i < N; ++i)
p[i] /= norm;
}
}
Looking first at the body of the function we
see the familiar notation p[i] to access an array
element. The value of p must be the address of a[0] when the
function is called with a as an argument, the address of b[0]
when called with b as an argument and the address of c[0] when
called with c as an argument. So p must be a variable,
specifically p must a variable whose value is the
address of the first element of the array we wish to
normalise.If the C programming language did not
provide us with this ability then we would be unable to write
functions that acted on vectors and matrices declared inside
other functions.
For example, let's assume that the array a is
stored starting at location 200, then the comparison
between a[i] inside main() and p[i]
inside normalise() is as follows:
Expression
|
Type
|
Expands to
|
Use
|
a[i] |
a is an array (constant) |
double@(200+8*i) |
Can only process the array a
|
p[i] |
p is a variable |
double@(p+8*i) |
Can process any array provided we set
the value of p to the address of its first element
|
If the first time we set p to be the address of a[0],
the second to the address of b[0] and the third to
the address of c[0] then the one function can be
used to normalise all three arrays.
We saw above that the function call normalise(a)
passes the address of a[0] to the function. Since this
is precisely the value p needs to have it's clear that
p is the parameter to normalise(). The
function prototype must be something like:
void normalise( p);
The parameter type must say first that p is a memory address but also that it is the memory address of a double and not, for example, a float so that when the compiler encounters the code p[i] it knows to generate double@(p+8*i) and not float@(p+4*i).
Fortunately we don't need to worry too much about how to
declare p due to the following useful trick:
C makes it simple for us
C gives us a simple rule:
As a convenience to the
programmer, when a function expects to be passed the address
of the first element of an array the corresponding parameter
can just be declared with the same declaration as the passed
array with the left-hand dimension being ignored.
Not only is the left-hand dimension ignored, it can be
omitted altogether which is a good idea that it emphasises we
have not created a new array. For a one-dimensional array
there is only one dimension of course so in our case the
function prototype is just:
void normalise(double p[]);
This does not mean that p is a duplicate of the
original array, it means it is a variable ready to receive the
address of an array, storing a memory address with the
property that when the compiler sees p[k] it
accesses exactly the same memory
address as it would had output been an array of that
type. Of course, it's our job to ensure the function is passed
a legitimate memory address.
This has two consequences:
- The computer only has to pass a single address (typically
four or eight bytes) to the function not the entire array,
in this case a million elements.
- When a function modifies the array it
is modifying the original array.
Example: the full program.
The full program is:
#include <stdio.h>
#include <math.h>
#define N 3
void normalise(double p[]);
int main() {
double a[N] = {2.7, 5.3, 8.3};
double b[N] = {5.8, 2.9, 3.0};
double c[N] = {-9.6, 13.9, 6.41};
normalise(a);
normalise(b);
normalise(c);
return 0;
}
void normalise(double p[]) {
double norm = 0;
for(int i = 0; i < N; ++i)
norm += p[i]*p[i];
if ( norm > 0) {
norm = sqrt(norm);
for(int i = 0; i < N; ++i)
p[i] /= norm;
}
}
Step through this code
Example: using functions to set up and print an array
Just to give another example, here we declare an array inside of main() and pass it to two functions one on which gives the array some values, the other prints it to the screen.
#include <stdio.h>
#define VALUES 4
void setuparray(int input[VALUES]);
void printarray(int output[]);
int main() {
int num[VALUES];
setuparray(num);
printarray(num);
return 0;
}
void setuparray(int input[VALUES]) {
int i;
for (i = 0; i < VALUES; ++i)
input[i] = i;
}
void printarray(int output[]) {
int i;
for (i = 0; i < VALUES; ++i ) {
printf("%d\n", output[i]);
}
}
Step through this code
Realistically this gives us a slight problem: printarray()
has a loop and we need to know when it finishes so in practice
we are likely to write:
void printarray(int output[], int n);
here n may be less than or equal to the true size
of the array but should not be larger!
Being able to have a variable whose value is the
address of another variable (a pointer) offers us tremendous flexibility:
- We can write a function that can operate on an array declared inside another function.
- We can not only read the values of array elemnts we can change them.
These advantages are useful in several other areas and so we will investigate pointers a little more over the next few lessons.
Warm-up discussion 2: Storing matrices
Suppose we wish to store the elements of a two-dimensional
4x10 square matrix of floats, that is four rows of ten values,
and that the first row starts at
location 1000.
- If the first element of the first row of 10 floats is stored
at location 1000
where would be the simplest and most logical place
to store the first element of the second row? (Remember that
floats require four bytes each)
- Provide a simple mathematical expression for
the simplest and most logical place
to store the start Jth row.
- Provide a simple mathematical expression for
the simplest and most logical place
to store the Kth element of the Jth row.
Multi-dimensional arrays are simple and straightforward and
fit in well with our commonly-used ideas of matrices and "more
than one vector"
Example: simultaneous equations
When solving simultaneous equations it's common to have a set
of vectors Ej with Ejk
referring to the kth component of jth
vector. Furthermore it's also then common to think of E
as a matrix in its own right ("the matrix formed by the
vectors Ej"). So for a 3x3 set of equations
we might write:
E00 X0 + E01 X1 + E02
X2 = C0
E10 X0 + E11 X1 + E12
X2 = C1
E20 X0 + E21 X1 + E22
X2 = C2
C's ability to make an "array of anythings" makes this very
natural and easy. We just define an "array of arrays" and write:
double vectors[NVECS][VECLEN];
...
for(int v = 0; v < NVECS; ++v)
printarray(vectors[v]);
Almost anything we can have one of, we can
make an array of.
Matrices and collections of several vectors are one and the
same thing
It follows that if vectors[v] is the vth
vector then vectors[v][k] is the kth
element of vth vector. In other words an array of
(one-dimensional) arrays of doubles, can equally be
thought of as a two-dimensional array of doubles.
The following example declares and initialises a two
dimensional array.
In some situations it may be more natural to
use
NROWS,
NCOLS,
row and
col
instead of
NX,
NY,
x and
y.
Or if we had a two-dimensional array of planetary data for
different stellar systems we might use star and planet.
#include <stdio.h>
#define NX 3
#define NY 2
int main() {
int grid[NX][NY] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
int x, y;
for (x = 0; x < NX; ++x)
for (y = 0; y < NY; ++y)
printf("grid[%d][%d] is %d\n", x, y, grid[x][y]);
return 0;
}
Step through this code
(Note that C considers a 3x2 array to be three arrays each of
length two.)
We can think of a two-dimensional array either
as a matrix, or a collection of vectors, or both.
More dimensions
We can do this as many times as we like. For example is we
wanted to store the electric and magnetic fields as an array
of length 4 with one field for each point of a
three-dimensional grid we could define the four-dimensional
array:
double field[NX][NY][NZ][4];
and we could treat field[NX][NY][NZ] as being the
one-dimensional array (of size 4) representing the field at
that point.
An N+1 dimensional array is
just an array of N dimensional arrays (for N > 0).
How multidimensional arrays are stored in memory
The storage of multidimensional arrays follows immediately
from their definition as "an array of arrays". As an example,
we declare, say a six by two array such as:
int iar[6][2];
The computer will store it as six one-dimensional arrays of
length two, one after the other. We illustrate the start of
the array storage here:
-------------------------------------------------------------------------
Byte: | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 |
-------------------------------------------------------------------------
Stores: | <---- iar[0][0] ----> | <---- iar[0][1] ----> | <---- iar[1][0] ----> |
-------------------------------------------------------------------------
It follows that in the general case, if we were to declare a
multiple dimensional array:
For the int array iar[M][N];
starting at location 800 the element ar[j][k] will be stored in
location
800 + 4 * (N*j + k).
A multi-dimensional array is a single chunk of memory and the compiler calculates the
address of an array element using the address of the start of the array, the
size of each array element and the dimensions of the array
(excluding the left-hand dimension).
Note that we never need to know the left-hand dimension to
find the address of an element of the array, that just tells
us where to stop!
The left-hand dimension of an array is not needed
to calculate the address of an array element.
Passing multidimensional arrays to functions
In a later lecture we shall see an even more
flexible way of doing this using dynamic memory allocation.
When passing multi-dimensional array to functions we have to
tell the function the dimensions of the array (apart from the
leading dimension). This isn't a problem if we are using
symbolic constants, but if they are dimensioned at run-time we
have to pass the dimensions as additional arguments, before the array name in the argument
list:
The following is a very simple matrix-multiply function.
void matmul(int nx, int ny, int nz, double a[][ny],
double b[][nz], double c[][ny]) {
int x, y, z;
for (x = 0; x < nx; ++x) {
for (y = 0; y < ny; ++y) {
a[x][y] = 0.0;
for (z = 0; z < nz; ++z)
a[x][y] += b[x][z] * c[z][y];
}
}
}
As always when using arrays, if we were to make a mistake
and end up with the function thinking that the dimensions of
the array were different from those it was originally declared
with, we would be in serious trouble.
If pass an array to another function we also
have to tell that function the dimensions of the array, other
than the left-most one.
The text of each key point is a link to the place in the web page.