Monday, January 27, 2003

The fgets() function (K&R § 7.7)

The fgets() function can be used to read input a full line at a time. This function takes three arguments: a pointer to the first character of an allocated buffer (the buffer could either be an array or it could have been allocated dynamically), a maximum buffer size (say, maxline), and a FILE * (stdin, representing standard input, is an appropriate value for this parameter).

fgets() will read the next line (including the newline) from the specified file and store them in the specified buffer. However, it will never read/store more than maxline-1 characters. (This makes it possible to safely read lines from a file without fear of overflowing a buffer.) Because of this, fgets() will only read/store the first maxline-1 characters on a line. If there are any remaining characters on the line (including the newline), they will be read on subsequent input requests. If a line has been fully read, then the last character stored in the buffer should be a \n (unless, of course, there was no newline in the input).

Note that fgets() will automatically put a nul byte in the buffer after the last character it reads. fgets() returns NULL in the event of error or end-of-file.

The following program will read input from standard input and echo each line (enclosed in double quotes) to the screen:


#include	<stdio.h>
#include	<string.h>

#define BUFFER_LEN	(80+1+1)	/* 80 regular characters
					 * +1 for the newline
					 * +1 for nul byte
					 */

int
main()
{
	char	buffer[BUFFER_LEN];

	while (fgets(buffer, sizeof(buffer), stdin) != NULL)  {
		/* Get rid of the newline */
		if (buffer[strlen(buffer)-1] == '\n')
			buffer[strlen(buffer)-1] = '\0';
		printf("Read string: \"%s\"\n", buffer);
	}
	return 0;
}


Passing pointers to functions (K&R § 5.2)

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;
}


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 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.

Function Parameters: 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 interchangeable.

Passing Multidimensional Arrays

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 border() function from Assignment #1 as an example, the way to represent a pointer to an array would be:

int border(char (*ar)[MAX_COL], int nrow, int ncol)

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 border(char *ar[MAX_COL], int nrow, int ncol)

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).

Dynamic memory allocation

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]);
}


The 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 character outside the array bounds. A much safer way of doing string input is with fgets(), which we will study later.

The 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 file.

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.

The 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. 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.

Things to remember about malloc() and free()

There are three important things to remember when dealing with dynamically allocated:

  1. You should always make sure that your request for memory was successful. (An unsuccessful call to malloc() returns NULL).
  2. As with arrays you should never access memory beyond what you allocated.
  3. You should always free memory that you have allocated when you no longer need it. Otherwise, your program will leak memory.

The 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)
...

Allocating memory inside a function

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]);
}


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: Tue Jan 28 22:41:40 2003