January 19 (Monday) January 23 (Friday)
As mentioned earlier, we can pass pointers to variables in order to simulate pass by reference. We already saw an example of this with arrays. Instead of passing the entire array, a pointer to the first element of the array is implicitly passed instead. Any changes to the array contents inside the function will affect the original array as well.
In order to explicitly pass pointers to variables of other types
(e.g. integers), we must use the address-of operator,
&
, as described earlier. For example, consider the
following two functions, each of which, on the surface appear to do the
same thing:
#include <stdio.h>
/* no_swap()'s parameters are of type "integer" */
void
no_swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
printf("At end of no_swap(): a = %d, b = %d\n", a, b);
}
/* swap()'s parameters are of type "pointer to integer" */
void
swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
printf("At end of swap(): *a = %d,*b = %d\n", *a, *b);
}
int
main()
{
int a = 1, b = 2;
printf("Originally: a = %d, b = %d\n", a, b);
/* Pass integers */
no_swap(a, b);
printf("After return from no_swap() a = %d, b = %d\n", a, b);
/* Pass pointers to integers */
swap(&a, &b);
printf("After return from swap() a = %d, b = %d\n", a, b);
return 0;
}
swap.c
There is a very important, but subtle difference between the two
functions, however. no_swap()
is passed copies of the
values of the variables a
and b
.
The function then exchanges these two copies while leaving the original
values of a
and b
in the main()
function unchanged. The fact that a
and b
are changed inside no_swap()
, but are unchanged outside is
demonstrated by the output generated by the calls to printf()
.
The function swap()
on the other hand, is passed copies of
the addresses of a
and b
. Because
swap()
now knows the addresses of the values of these
variables, it can make changes to the variables' values themselves by
dereferencing these addresses and have these changes reflected in the
in context of the calling function (main()
, in this case).
Note that swap()
is still receiving its parameters by
value. However, instead of passing it copies of the values
of a
and b
, it is being passed copies of
their respective addresses. By modifying the values at
these addresses, swap()
is able to exchange the values of
a
and b
and have those changes preserved when
the function ends.
char string[]
vs char *string
(K&R § 5.3)
As a convention, when passing pointers, it is common to always use the
*
symbol in the parameter declaration, even if what is
actually being passed is an array. For example, previously, we saw the
alternate_caps()
function declared as:
int alternate_caps(char string[]);
In practice, however, the []
notation is not frequently used
when we are passing one dimensional arrays. Instead, it is generally
agreed that because an array passed as a parameter degrades into a pointer
to the array's first element, it is more accurate to declare/define the
function as:
int alternate_caps(char *string);
Both declarations are essentially equivalent, but do not jump to the conclusion that arrays and pointers are the same thing -- they are different beasts entirely. The fact that arrays become pointers to their first elements does not mean that pointers and arrays can be used interchangeably.
A similar strategy can be used when passing arrays with more than one dimension. However, remember that the array degrades into a pointer to its first element. The first element of a two dimensional array is a one dimensional array (i.e. the first row of the array). So when we pass a two dimensional array as a parameter, what the function receives is a pointer to the first element of the array, which, in this case, is an array representing the first row. Therefore, the function gets a pointer to an array.
Using the addone()
function from a previous lecture as an
example, the way to represent a pointer to an array would be:
int addone(int (*ar)[MAX_COL], int nrows, int ncols)
This declares ar
to be a pointer to an array of size
MAX_COL
. Note that because of the associativity of the
[]
and *
operators, we could not write:
int addone(int *ar[MAX_COL], int nrows, int ncols)
because that would declare ar
to be an array of pointers,
which is something completely different than a pointer to an array
(K&R § 5.7, 5.9).
In all our examples, so far, we have had arrays of fixed size. C allows us to request dynamically allocated memory, the size of which may not be known until runtime. For example, consider the following program which dynamically allocates, uses, then deallocates an array of integers.
#include <stdio.h>
#include <stdlib.h>
void initialize(int *mem, int num_elements, int init);
void show(int *mem, int num_elements);
int
main()
{
int num, init;
int *mem;
printf("How big an array would you like? ");
scanf("%d", &num);
if ((mem = (int *) malloc(sizeof(int) * num)) == NULL)
{
fprintf(stderr, "Unable to allocate memory\n");
exit(1);
}
printf("What would you like the initial value to be? ");
scanf("%d", &init);
initialize(mem, num, init);
show(mem, num);
free(mem);
return 0;
}
void
initialize(int *mem, int num_elements, int init)
{
int i;
for (i = 0; i < num_elements; i++)
*(mem + i) = init++;
}
void
show(int *mem, int num_elements)
{
int i;
puts("The values in the array are: ");
for (i = 0; i < num_elements; i++)
printf("mem[%d] = %d\n", i, mem[i]);
}
malloc1.c
scanf()
function (K&R § 7.4)
The program first asks the user how large an array to allocate,
then it acquires user input using the scanf()
function.
The scanf()
function takes a format string argument
containing conversion specifiers (the format string is similar
to printf()
) and a list of variable addresses. The
scanf()
function will then read, from standard input,
the values types specified by the format string and store them in the
locations given by the rest of the argument list.
We can also use scanf
to input strings by using a
%s
conversion specifier and a corresponding character
array in the argument list. Note that because an array becomes
a pointer to the first element of the array, there should be
no &
preceding the array name:
char str[10]; scanf("%s", str);
Generally speaking, this method of inputting strings is very unsafe,
because a user could always enter more characters that have been
allocated by the array and scanf()
will write characters
outside the array bounds. A much safer way of doing string input is
with fgets()
, which we will study later.
fprintf()
function (K&R § 7.5)
When an error occurs in your program (for example, a call to
malloc()
fails), it is usually a good idea to print
diagnostic information describing the error to a different output
stream than your regular output. This way, diagnostic output (such as
warnings/error produced by your program) can be viewed separately from
your program's regular output.
This can be done by using the fprintf()
function.
fprintf()
operates the same as printf()
except
that it has an additional parameter, namely the file to which to write
the formatted string. By default, three "files" are accessible by C:
standard input (stdin
), standard output (stdout
)
and standard error (stderr
). By default, standard input
is your keyboard; standard error and standard output are both displayed
on your console. Using redirection, we can change this:
$ ./a.out < input.txt > output.dat 2> error.log
This invocation causes a.out
to read input from
the file named input.txt
, sends output generated
by printf()
, puts()
etc. to the
file output.dat
and sends output generated by
fprintf(stderr, ...)
to the file error.log
.
The 2
in front of the '>
' redirection symbol
represents the stderr
output stream.
Note that printf(...)
is identical to fprintf(stdout,
...)
. Also, in general fprintf()
can be used to
write data to files opened explicitly by your C
programs, as we will see later.
Also note that because the fprintf()
call in the example
is simply displaying a string literal (i.e. there is no
formatted output taking place), we could replace the fprintf()
call with a call to fputs()
:
fputs("Unable to allocate memory\n", stderr);
malloc()
and free()
functions
(K&R § 6.5, 7.8.5)
Once the program obtains the size of the array, the
malloc()
function is called. (This function is declared in
<stdlib.h>
, so we must #include
that file
before using the function.) The malloc()
function, takes,
as its argument, the number of bytes to allocate. In our example, we are
allocating an area of memory to store integers, therefore, each element of
the array will require sizeof(int)
bytes. In order to store
num
integers, we will require sizeof(int)*num
bytes altogether, hence explaining the argument to malloc()
.
The malloc()
function returns a pointer to the first
address of the region of memory that it allocated for us. As with
uninitialized arrays, the contents of the memory will be uninitialized.
If malloc()
was unable to satisfy our request for memory a
NULL
pointer will be returned. If this happens, the code
will display an error message and call the exit()
function
(K&R § 7.6). This function causes the program to terminate
with the value of the integer argument to the exit()
function begin returned to the shell. The exit()
function
lets us terminate a program immediately regardless of what function we
are currently in. Like malloc()
, the exit()
function is declared in <stdlib.h>
.
Because malloc()
is not aware of the type of data which
is being allocated, the pointer returned from malloc()
is
void
(i.e. malloc()
returns a void
*
), This basically means that malloc()
returns a
pointer to an unknown type. Before we can assign this void *
to mem
(which is defined as an int*
), we
must force the return value from malloc()
to be an int
*
. We do this by using a typecast: We prefix the the
call to malloc()
with the typecast: (int *)
(the parenthesis are required.)
Once we have the pointer returned by malloc()
stored in
mem
, we can use mem
as if it were an array,
as demonstrated by the initialize()
and show()
functions, which, respectively assign sequentially increasing numbers to
each subsequent array element (starting from a initial number specified
by the user) and output the values in the dynamically allocated array.
Note, that like an array, we can either use the *(mem + i)
notation for accessing elements of this array or the more conventional
mem[i]
notation. mem
is a pointer to the
first element of the array.
When we are finished with mem
, we must remember to deallocate
it by calling free()
. Note that if we do not call free
, then memory allocated will be retained by the program until the program
terminates, after which the memory will be reclaimed by the operating
system. In our particular example, free()
'ing the memory
isn't particularly important, since the program finishes immediately
after we do. However in programs that are to be run for extended periods
of time without stopping, forgetting to deallocate dynamically allocated
memory can lead to a memory leak which can slowly drain all the computer's
memory resources.
malloc()
and free()
There are three important things to remember when dealing with dynamically allocated:
malloc()
returns NULL
).
calloc()
function
There is another function, called calloc()
which operates
similarly to malloc()
except that it takes two parameters
instead of one. The first parameter represents the number of bytes
of each array element and the second number represents the total number
of elements to allocate. (calloc()
also has the added
feature that the allocated chuck of memory is set to zero.) The above
call to malloc()
could therefore be replaced with the following
(note that the typecast is still required):
if ((mem = (int *) calloc(sizeof(int), num)) == NULL)
...
The above code can be made more modular by introducing a
allocate()
function to do our memory allocation for
us instead of allocating it directly inside main()
:
#include <stdio.h>
#include <stdlib.h>
void allocate(int **mem, int num_elements);
void initialize(int *mem, int num_elements, int init);
void show(int *mem, int num_elements);
int
main()
{
int num, init;
int *mem;
printf("How big an array would you like? ");
scanf("%d", &num);
printf("What would you like the initial value to be? ");
scanf("%d", &init);
allocate(&mem, num);
initialize(mem, num, init);
show(mem, num);
free(mem);
return 0;
}
void
allocate(int **mem, int num_elements)
{
if ((*mem = (int *) malloc(sizeof(int) * num_elements)) == NULL)
{
fprintf(stderr, "Unable to allocate memory\n");
exit(1);
}
}
void
initialize(int *mem, int num_elements, int init)
{
int i;
for (i = 0; i < num_elements; i++)
*(mem + i) = init++;
}
void
show(int *mem, int num_elements)
{
int i;
puts("The values in the array are: ");
for (i = 0; i < num_elements; i++)
printf("mem[%d] = %d\n", i, mem[i]);
}
malloc2.c
Note the difference between the two programs:
A new function allocate()
has been added. Note that
this function takes a pointer to a pointer to int
as the
first argument (int **
). Because allocate()
modifies the mem
parameter and because we want this change to
be reflected in the calling function (i.e. main()
),
the main()
function must pass in the address
of mem
, hence giving rise to the double pointer. Note that
inside allocate()
we dereference mem
when
assigning to it. By doing so, we simultaneously update *mem
in allocate()
and mem
in main()
.
If we had defined allocate()
as follows:
void allocate(int *mem, int num_elements) { if ((mem = (int *) malloc(sizeof(int) * num_elements)) == NULL) { fprintf(stderr, "Unable to allocate memory\n"); exit(1); } }
and main()
had passed in mem
(and not
&mem
), then allocate()
's copy of
mem
would be assigned a pointer to the newly allocated
memory, but the mem
pointer in main()
would
still be uninitialized. When allocate()
is finished,
the memory that it dynamically acquired would still be allocated, but
there would no longer be any way to refer to it -- the memory will have
been leaked.
An alternative way of defining the allocate()
function is to
have it return a pointer to the allocated memory. For example:
int *allocate(int num_elements) { int *mem; if ((mem = (int *) malloc(sizeof(int) * num_elements)) == NULL) { fprintf(stderr, "Unable to allocate memory\n"); exit(1); } return mem; }
Now, when we call allocate()
, we must assign the pointer
returned from the function to mem
in the main()
function (of course, we must still remember to free()
it as well):
mem = allocate(num);
Last modified: January 22, 2004 00:51:58 NST (Thursday)