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