Functions & Arrays Lecture Notes1


In C, some variables hold values and some hold memory addresses. Both of these are binary numbers. Every variable has its own memory address. The ones you are most familiar with
hold values: int, double, float.

Pointers, arrays, and function names hold memory addresses, not ordinary numerical values. The relationship between pointers and arrays is discussed in the ABC text and below.

In a function call, we pass down only numbers: either numerical values or one of the special numbers used for memory addresses (like a post-office-box number).

Functions

hip C lingo - use parentheses after the name to show it is the name of a function: fname()

Function Declarations and Function Definitions MUST be consistent, don't rely on the compiler to find mistakes!!

Functions can be classified by what they take as inputs, and what they return as outputs. Below we give examples of

Scalar Functions       Scalar Functionals       Scalar Functions of Vector Arguments
Vector Functions       Vector Functionals

Scalar Functions

    double dV_spherical(double R, double theta)
    {
            return pow(R,2)*sin(theta);
    }                                     /* would need #include<math.h>, gcc -lm */

Scalar Functionals

Functionals take function names as arguments: they operate on functions, not just values. A simple example is the differentiation operator:

double deriv(double f(double), double x, double RTOL, double ATOL);

call deriv() this way to evaluate the derivative of g at y to the specified tolerances:

dg_dy = deriv(g,y,RTOL,ATOL);

Convergence is usually defined by RTOL and ATOL, typically we iterate until

|Ans(N) - Ans(N-1)| < RTOL * Ans(N) + ATOL

Although the derivative is defined by

f'(x) = lim h->0 (f(x+h)-f(x))/h

for a numerical derivative h must be nonzero: you cannot divide by zero and in fact you do not want h to be too small or your accuracy will be killed by roundoff error, e.g. if x>>h, the computer thinks (x+h) and x are exactly the same, and the program will say f'(x) =0

The same problem comes up in integration, where the normal thing to do is something like

h=(b-a)/N;
for (i=0,i<N,i+=1)
{
    sum += f(x);
    x += h;
}
return h*sum;

and to achieve high accuracy you want to use a big value of N. But this always fails if N is too large (i.e. if h is too small).

You already have written some scalar functionals: for example, root-finding programs take a function f() as input and return a number X that satisfies f(X)=0 to some tolerance.

Integration routines typically take a function as input and return a number (the value of the integral).

Scalar Functions of Vector Arguments

How to represent vectors? Easiest way is to use arrays , see ABC 6.1
/* Unless you do something special, in C the INDEX STARTS AT ZERO!!! */
A 1-d array is V[i], a 2-d array is M[i][j], etc.
By convention, i is the index of the row, and j is the index of the column, i.e. M[0][j] is the first row of the matrix.

To compute the dot product a.b:

#define NDIM 3
double dot(double v1[], double v2[], int N);
int main(void)
{
        double a[NDIM], b[NDIM], a_dot_b;
        int N;
        N=Ndim;
                ...
        a_dot_b = dot(a,b,N);
                ...
        return 0;
}

double dot (double v1[], double v2[], int N)
{
        double dot_product=0.0;
        int i;
        for (i=0; i<N, ++i)
        {
           dot_product+=v1[i]*v2[i];
        }
        return dot_product;
}

How about functions that RETURN a vector?  These are called

VECTOR FUNCTIONS

A function can only return a single value, but the elements of arrays in the argument list
can also be changed. Note that ordinary variables in the argument list cannot be changed
by a function (unless you pass their memory location down).

A common example is a program that computes the cross-product between two 3-vectors:

in main: declare

double a[3],b[3],c[3];

and fill in the values in a[0],a[1],a[2] and b[0],b[1],b[2], then invoke cross():

cross(a,b,c);

cross() is defined this way:

int cross(double v1[], double v2[], double v3[])
{ad``
 ...
 for(i=0,i<3,i++)
    {
        v3[i]=...
    }
 return 0;
}

cross() actually fills in the values of c[0],c[1]c[2] in main(). This works because array names are actually memory addresses:

int i=3;
double x=4.0;
double a[3];

if you ask the computer what i is, it will say i is 3.
    it will say x is 4.0
    it will say a is 0x43219 A MEMORY ADDRESS!

To get the numbers stored in a, you need indices: a[0], a[1], ...

Of course the computer knows the memory location of i and x as well, but
they are called &i and &x. If we pass those memory addresses down, a function can
stuff numbers in them too.
 

VECTOR FUNCTIONALS

A good example of a function that takes a function name as input and returns a vector is the numerical gradient:

int grad(double f(double v[]), double x[], double gradf[], double ATOL, double RTOL, int Ndim); /*returns gradf[], the gradient of f at x. */

first define a function which computes the derivative along one dimension, indexed by Nspecial:

double derivN(double f(double v[]), double x[], int Ndim, int Nspecial, double RTOL, double ATOL);

int grad(double f(double v[]), double x[], double gradf[], double ATOL, double RTOL, int Ndim)
/* gradf[] is the output, the rest are inputs. */
{
  ...
  for(i=0;i<Ndim;++i)
  {
        gradf[i]=derivN(f,x,N,i,RTOL,ATOL);
  }
  ...
  return 0;
}

You might actually call grad() from main() like this:

grad(phi,V0,grad_phi,ATOL,RTOL,3);

where phi() was declared and defined as a scalar function of a 3-vector, and
we previously declared

double V0[3],grad_phi[3],ATOL,RTOL;

Note that if you want, you can overwrite the elements of a vector and return the answer in the same memory slot, e.g.

int miles_to_km(double distances[], int N)
{
        int i;
        for(i=0;i<N;i+=1)
        {
                distances[i]=distances[i]*1.63;
        }
        return N;
}
 
 

EQUIVALENCE OF v[i] and *(v+i)

The variable v holds the memory address where the array v[] begins. Since it is a memory address, we can pass it down to functions

int f(double V[], int N);

OR

int f(double *V, int N);

In fact, C interprets V[i] as *(V+i), i.e. move over i memory slots from memory address V. And if you want to, you can actually use expressions like

*(V+7) instead of the simpler V[7].

If we read in a string of data

float data[100]

and later find out that the first two data points are screwed up, we might
want to define

float *good_data;
good_data=data +2;

and then

good_data[j] is the jth good data point (i.e. data[j+2]).

Say you knew that only the 42nd, 56th, and 98th data points were good?
then you might define a pointer array that points to the good data values:

float *good_data_pointer[3];
good_data_pointer[0]=data+41;
good_data_pointer[1]=data+55;
good_data_pointer[2]=data+98;

and then

*good_data_pointer[j] is the jth good data point.