v3.0.2 / chapter 4 of 5 / 01 jul 07 / greg goebel / public domain
* This chapter discusses some useful standard C libraries:
math library
standard utility library
the "sprintf()" function
string function library
character class test library
-- and the following minor topics:
command-line arguments
dynamic memory allocation
pointers to functions
PC memory model and other declarations
troubleshooting hints
* The math library requires the declaration:
#include <math.h>
The math functions consist of:
sin( x ) Sine of x.
cos( x ) Cosine of x.
tan( x ) Tangent of x.
asin( x ) Inverse sine of x.
acos( x ) Inverse cosine of x.
atan( x ) Inverse tangent of x.
sinh( x ) Hyperbolic sine of x.
cosh( x ) Hyperbolic cosine of x.
tanh( x ) Hyperbolic tangent of x.
exp( x ) Exponential function -- e^x.
log( x ) Natural log of x.
log10( x ) Base 10 log of x.
pow( x, y ) Power function -- x^y.
sqrt( x ) Square root of x.
ceil( x ) Smallest integer not less than x, returned as double.
floor( x ) Greatest integer not greater than x, returned as double.
fabs( x ) Absolute value of x.
All values are "doubles", and trig values are expressed in radians.
* The utility functions library features a grab-bag of functions. It
requires the declaration:
#include <stdlib.h>
Useful functions include:
atof( <string> ) Convert numeric string to double value.
atoi( <string> ) Convert numeric string to int value.
atol( <string> ) Convert numeric string to long value.
rand() Generates pseudorandom integer.
srand( <seed> ) Seed random-number generator -- "seed" is an "int".
exit( <status> ) Exits program -- "status" is an "int".
system( <string> ) Tells system to execute program given by the seed.
abs( n ) Absolute value of "int" argument.
labs( n ) Absolute value of long-int argument.
The functions "atof()", "atoi()", and "atol()" will return 0 if they can't
convert the string given them into a value.
The time and date library includes a wide variety of functions, some of them
obscure and nonstandard. This library requires the declaration:
#include <time.h>
The most essential function is "time()", which returns the number of seconds
since some long-ago date. It returns a value as "time_t" (a "long") as
defined in the header file.
The following function uses "time()" to implement a program delay with
resolution in seconds:
/* delay.c */
#include <stdio.h>
#include <time.h>
void sleep( time_t delay );
void main()
{
puts( "Delaying for 3 seconds." );
sleep( 3 );
puts( "Done!" );
}
void sleep( time_t delay )
{
time_t t0, t1;
time( &t0 );
do
{
time( &t1 );
}
while (( t1 - t0 ) < delay );
}
The "ctime()" function converts the time value returned by "time()" into a
time-and-date string. The following little program prints the current time
and date:
/* time.c */
#include <stdio.h>
#include <time.h>
void main()
{
time_t *t;
time( t );
puts( ctime( t ));
}
This program prints a string of the form:
Tue Dec 27 15:18:16 1994
BACK_TO_TOP
* The "sprintf" function creates strings with formatted data. Technically
speaking, this is part of the standard-I/O library, and requires the
declaration:
#include <stdio.h>
However, it is really a string function and needs to be discussed along with
the other string functions. The syntax of "sprintf()" is exactly the same as
it is for "printf()", with the notable exception that the first parameter is
a pointer to a string. For example:
/* csprntf.c */
#include <stdio.h>
void main()
{
char b[100];
int i = 42;
float f = 1.1234f;
sprintf( b, "Formatted data: %d / %f", i, f );
puts( b );
}
-- prints the string:
Formatted data: 42 / 1.1234
There is also an "sscanf()" function that similarly mirrors "scanf()"
functionality.
* The string-function library requires the declaration:
#include <string.h>
The most important string functions are as follows:
strlen() Get length of a string.
strcpy() Copy one string to another.
strcat() Link together (concatenate) two strings.
strcmp() Compare two strings.
strchr() Find character in string.
strstr() Find string in string.
strlwr() Convert string to lowercase.
strupr() Convert string to uppercase.
* The "strlen()" function gives the length of a string, not including the
NULL character at the end:
/* strlen.c */
#include <stdio.h>
#include <string.h>
void main()
{
char *t = "XXX";
printf( "Length of <%s> is %d.\n", t, strlen( t ));
}
This prints:
Length of <XXX> is 3.
* The "strcpy" function copies one string from another. For example:
/* strcpy.c */
#include <stdio.h>
#include <string.h>
void main()
{
char s1[100],
s2[100];
strcpy( s1, "string 2" );
strcpy( s2, "string 1" );
puts( "Original strings: " );
puts( "" );
puts( s1 );
puts( s2 );
puts( "" );
strcpy( s2, s1 );
puts( "New strings: " );
puts( "" );
puts( s1 );
puts( s2 );
}
This will print:
Original strings:
string 1
string 2
New strings:
string 1
string 1
Please be aware of two features of this program:
These comments are applicable to most of the other string functions.
There is a variant form of "strcpy" named "strncpy" that will copy "n"
characters of the source string to the destination string, presuming there
are that many characters available in the source string. For example, if the
following change is made in the example program:
strncpy( s2, s1, 5 );
-- then the results change to:
New strings:
string 1
string
Notice that the parameter "n" is declared "size_t", which is defined in
"string.h".
* The "strcat()" function joins two strings:
/* strcat.c */
#include <stdio.h>
#include <string.h>
void main()
{
char s1[50],
s2[50];
strcpy( s1, "Tweedledee " );
strcpy( s2, "Tweedledum" );
strcat( s1, s2 );
puts( s1 );
}
This prints:
Tweedledee Tweedledum
There is a variant version of "strcat()" named "strncat()" that will append
"n" characters of the source string to the destination string. If the
example above used "strncat()" with a length of 7:
strncat( s1, s2, 7 );
-- the result would be:
Tweedledee Tweedle
Again, the length parameter is of type "size_t".
* The "strcmp()" function compares two strings:
/* strcmp.c */
#include <stdio.h>
#include <string.h>
#define ANSWER "blue"
void main()
{
char t[100];
puts( "What is the secret color?" );
gets( t );
while ( strcmp( t, ANSWER ) != 0 )
{
puts( "Wrong, try again." );
gets( t );
}
puts( "Right!" );
}
The "strcmp()" function returns a 0 for a successful comparison, and nonzero
otherwise. The comparison is case-sensitive, so answering "BLUE" or "Blue"
won't work.
There are three alternate forms for "strcmp()":
* The "strchr" function finds the first occurrence of a character in a
string. It returns a pointer to the character if it finds it, and null if
not. For example:
/* strchr.c */
#include <stdio.h>
#include <string.h>
void main()
{
char *t = "MEAS:VOLT:DC?";
char *p;
p = t;
puts( p );
while(( p = strchr( p, ':' )) != NULL )
{
puts( ++p );
}
}
This prints:
MEAS:VOLT:DC?
VOLT:DC?
DC?
The character is defined as a character constant, which C regards as an
"int". Notice how the example program increments the pointer before using it
("++p") so that it doesn't point to the ":" but to the character following
it.
The "strrchr()" function is almost the same as "strchr()", except that it searches for the last occurrence of the character in the string.
* The "strstr()" function is similar to "strchr()" except that it searches
for a string, instead of a character. It also returns a pointer:
char *s = "Black White Brown Blue Green";
...
puts( strstr( s, "Blue" ) );
* The "strlwr()" and "strupr()" functions simply perform lowercase or
uppercase conversion on the source string. For example:
/* casecvt.c */
#include <stdio.h>
#include <string.h>
void main()
{
char *t = "Die Barney die!";
puts( strlwr( t ) );
puts( strupr( t ) );
}
-- prints:
die barney die!
DIE BARNEY DIE!
These two functions only implemented in some compilers and are not part of
ANSI C.
* These functions perform various tests on characters. They require the
declaration:
#include <ctype.h>
The character is represented as an "int" and the functions return an "int".
They return 0 if the test is false and non-0 if the test is true:
isalnum( c ) Character is alpha or digit.
isalpha( c ) Character is alpha.
iscntrl( c ) Character is control character.
isdigit( c ) Character is decimal digit.
isgraph( c ) Character is printing character (except space).
islower( c ) Character is lower-case.
isprint( c ) Character is printing character (including space).
ispunct( c ) Character is printing character but not space/alpha-digit.
isspace( c ) Character is space, FF, LF, CR, HT, VT.
isupper( c ) Character is upper-case.
isxdigit( c ) Character is hex digit.
The library also contains two conversion functions that also accept and
return an "int":
tolower( c ) Convert to lower case.
toupper( c ) Convert to upper case.
BACK_TO_TOP
* C allows a program to obtain the command line arguments provided when the executable is called, using two optional parameters of "main()" named "argc (argument count)" and "argv (argument vector)".
The "argc" variable gives the count of the number of command-line parameters provided to the program. This count includes the name of the program itself, so it will always have a value of at least one. The "argv" variable is a pointer to an array of strings, with each element containing one of the command-line arguments.
The following example program demonstrates:
/* cmdline.c */
#include <stdio.h>
void main( int argc, char *argv[] )
{
int ctr;
for( ctr=0; ctr < argc; ctr++ )
{
puts( argv[ctr] );
}
}
If this program is run from the command line as follows:
stooges moe larry curley
-- the output is:
stooges
moe
larry
curley
In practice, the command line will probably take a number of arguments, some
of which will indicate options or switches, designated by a leading "-" or
"/". Some of the switches may be specified separately or together, and some
may accept an associated parameter. Other arguments will be text strings,
giving numbers, file names, or other data.
The following example program demonstrates parsing the command-line arguments
for an arbitrary program. It assumes that the legal option characters are
"A", "B", "C", and "S", in either upper- or lower-case. The "S" option must
be followed by some string representing a parameter.
/* cparse.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
main( int argc, char *argv[] )
{
int m, n, /* Loop counters. */
l, /* String length. */
x, /* Exit code. */
ch; /* Character buffer. */
char s[256]; /* String buffer. */
for( n = 1; n < argc; n++ ) /* Scan through args. */
{
switch( (int)argv[n][0] ) /* Check for option character. */
{
case '-':
case '/': x = 0; /* Bail out if 1. */
l = strlen( argv[n] );
for( m = 1; m < l; ++m ) /* Scan through options. */
{
ch = (int)argv[n][m];
switch( ch )
{
case 'a': /* Legal options. */
case 'A':
case 'b':
case 'B':
case 'C':
case 'd':
case 'D': printf( "Option code = %c\n", ch );
break;
case 's': /* String parameter. */
case 'S': if( m + 1 >= l )
{
puts( "Illegal syntax -- no string!" );
exit( 1 );
}
else
{
strcpy( s, &argv[n][m+1] );
printf( "String = %s\n", s );
}
x = 1;
break;
default: printf( "Illegal option code = %c\n", ch );
x = 1; /* Not legal option. */
exit( 1 );
break;
}
if( x == 1 )
{
break;
}
}
break;
default: printf( "Text = %s\n", argv[n] ); /* Not option -- text. */
break;
}
}
puts( "DONE!" );
}
For a more practical example, here's a simple program, based on an example
from the previous chapter, that attempts to read the names of an input and
output file from the command line. If no files are present, it uses standard
input and standard output instead. If one file is present, it is assumed to
be the input file and opens up standard output. This is a useful template
for simple file-processing programs.
/* cpfile.c */
#include <stdio.h>
#include <stdlib.h>
#define MAX 256
void main( unsigned int argc, unsigned char *argv[] )
{
FILE *src, *dst;
char b[MAX];
/* Try to open source and destination files. */
switch (argc)
{
case 1: /* No parameters, use stdin-stdout. */
src = stdin;
dst = stdout;
break;
case 2: /* One parameter -- use input file & stdout. */
if ( ( src = fopen( argv[1], "r" )) == NULL )
{
puts( "Can't open input file.\n" );
exit( 0 );
}
dst = stdout;
break;
case 3: /* Two parameters -- use input and output files. */
if ( ( src = fopen( argv[1], "r" )) == NULL )
{
puts( "Can't open input file.\n" );
exit( 0 );
}
if ( ( dst = fopen( argv[2], "w" )) == NULL )
{
puts( "Can't open output file.\n" );
exit( 0 );
}
break;
default: /* Too many parameters. */
puts( "Wrong parameters.\n" );
exit( 0 );
}
/* Copy one file to the next. */
while( ( fgets( b, MAX, src ) ) != NULL )
{
fputs( b, dst );
}
/* All done, close up shop. */
fclose( src );
fclose( dst );
}
BACK_TO_TOP
* This document has explained how to declare pointers to variables, arrays, and structures in C. It is alos possible to define pointers to functions. This feature allows functions to be passed as arguments to other functions. This is useful for, say, building a function that determines solutions to a range of math functions.
The syntax for declaring pointers to functions is obscure, and so let's start
with an idiot example: declaring a pointer to the standard library function
"printf()":
/* ptrprt.c */
#include <stdio.h>
void main()
{
int (*func_ptr) (); /* Declare the pointer. */
func_ptr = printf; /* Assign it a function. */
(*func_ptr) ( "Printf is here!\n" ); /* Execute the function. */
}
The function pointer has to be declared as the same type ("int" in this case)
as the function it represents.
Next, let's pass function pointers to another function. This function will
assume the functions passed to it are math functions that accept double and
return double values:
/* ptrroot.c */
#include <stdio.h>
#include <math.h>
void testfunc ( char *name, double (*func_ptr) () );
void main()
{
testfunc( "square root", sqrt );
}
void testfunc ( char *name, double (*func_ptr) () )
{
double x, xinc;
int c;
printf( "Testing function %s:\n\n", name );
for( c=0; c < 20; ++c )
{
printf( "%d: %f\n", c,(*func_ptr)( (double)c ));
}
}
It is obvious that not all functions can be passed to "testfunc()". The
function being passed must agree with the expected number and type of
parameters, as well as with the value returned.
* For simple programs, it is OK to just declare an array of a given size:
char buffer[1024]
In more sophisticated programs, this leads to trouble. There may be no way
of knowing how big an array needs to be for the specific task the program is
performing, and so allocating an array in a fixed size will either result in
wasted memory or in not having enough to do the job.
The answer to this problem is to have the program allocate the memory at
runtime, and that's what the "malloc()" library function does. For example,
let's use "malloc()" to allocate an array of "char":
/* malloc.c */
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h> /* For "exit" function. */
void main()
{
char *p; /* Pointer to array. */
unsigned count; /* Size of array. */
puts( "Size of array?" );
scanf( "%d", count ); /* Get size in bytes. */
p = (char *)malloc( (size_t)count ); /* Allocate array. */
if( p == NULL ) /* Check for failure. */
{
puts( "Can't allocate memory!" );
exit( 0 );
}
puts( "Allocated array!" );
free( p ); /* Release memory. */
}
The header file "malloc.h" must be included, and a pointer to the memory
block to be allocated must be declared. The "malloc()" function sets the
pointer to the allocated memory block with:
p = (char *)malloc( (size_t)count );
The count is in bytes and it is "cast" to the type of "size_t", which is
defined in "malloc.h". The pointer returned by "malloc()" is "cast" to type
"char *", that is, a pointer to type "char". By default, in ANSI C,
"malloc()" returns a pointer of type "void", which allows the pointer to be
cast to any other type.
If the "malloc()" fails because it can't allocate the memory, it returns the value null (as defined in "stdio.h").
It is simple to allocate other data types by changing the "cast" operations:
int *buf;
...
buf = (int *)malloc( (size_t)sizeof( int ) * count );
The "sizeof()" function is used to determine the number of bytes in the "int"
data type.
When the programs finished using the memory block, it get rids of it using
the "free" function:
free( p );
C also contains two other memory-allocation functions closely related to
"malloc()": the "calloc()" function, which performs the same function as
"malloc()" but allows the block allocated to be specified in terms of number
of elements:
void *calloc( size_t <number_elements>, size_t <sizeof_element_type> );
-- and the "realloc()" function, which reallocates the size of an array
that's already been allocated:
void *realloc( void *<block_pointer>, size_t <size_in_bytes> );
BACK_TO_TOP
* There are a number of common programming pitfalls in C that even trap experienced programmers:
1: Confusing "=" (assignment operator) with "==" (equality operator). For
example:
if ( x = 1 )
{
}
-- is bogus, and so is:
for ( x == 1; ...
2: Confusing precedence of operations in expressions. When in doubt, use
parentheses to enforce precedence.
3: Confusing structure-member operators. If "struct_val" is a structure and
"struct_ptr" is a pointer to a structure, then:
struct_val->myname
-- is wrong and so is:
struct_ptr.myname
4: Using incorrect formatting codes for "printf()" and "scanf()". Using a
"%f" to print an "int", for example, can lead to bizarre output.
5: Remember that the actual base index of an array is 0, and the final index
is 1 less than the declared size. For example:
int data[20];
...
for ( x = 1; x <= 20; ++x )
{
printf( "%d\n", data[x] );
}
-- will give invalid results when "x" is 20. Since C does not do bounds
checking, this one might be hard to catch.
6: Muddling syntax for multidimensional arrays. If:
data[10][10]
-- is a two-dimensional array, then:
data[2][7]
-- will select an element in that array. However:
data[ 2, 7 ]
-- will give invalid results but not be flagged as an error by C.
7: Confusing strings and character constants. The following is a string:
"Y"
-- as opposed to the character constant:
'Y'
This can cause troubles in comparisons of strings against character
constants.
8: Forgetting that strings end in a null character ('\0'). This means that a string will always be one character bigger than the text it stores. It can also cause trouble if a string is being created on a character-by-character basis, and the program doesn't tack the null character onto the end of it.
9: Failing to allocate enough memory for a string -- or, if pointers are declared, to allocate any memory for it at all.
10: Declaring a string with a fixed size and then assigning it to a string
literal:
char a[256] = "This doesn't work!";
11: Failing to check return values from library functions. Most library
functions return an error code; while it may not be desireable to check every
invocation of "printf()", be careful not to ignore error codes in critical
operations.
Of course, forgetting to store the value returned by a function when that's the only way to get the value out of it is a bonehead move, but people do things like that every now and then.
12: Having duplicate library-function names. The compiler will not always catch such bugs.
13: Forgetting to specify header files for library functions.
14: Specifying variables as parameters to functions when pointers are
supposed to be specified, and the reverse. If the function returns a value
through a parameter, that means it must be specified as a pointer:
myfunc( &myvar );
The following will not do the job:
myfunc( myvar );
Remember that a function may require a pointer as a parameter even if it
doesn't return a value, though as a rule this is not a good programming
practice.
15: Getting mixed up when using nested "if" and "else" statements. The best way to avoid problems with this is to always use brackets. Avoiding complicated "if" constructs is also a good idea; use "switch" if there's any choice in the matter. Using "switch" is also useful even for simple "if" statements, since it makes it easier to expand the construct if that is necessary.
16: Forgetting semicolons -- though the compiler usually catches this -- or
adding one where it isn't supposed to be -- which it usually doesn't. For
example:
for( x = 1; x < 10; ++x );
{
printf( "%d\n", x )
}
-- never prints anything.
17: Forgetting "break" statements in "switch" constructs. As commented earlier, doing so will simply cause execution to flow from one clause of the "switch" to the next.
18: Careless mixing and misuse of signed and unsigned values, or of different data types. This can lead to some insanely subtle bugs. One particular problem to watch out for is declaring single character variables as "unsigned char". Many I/O functions will expect values of "unsigned int" and fail to properly flag EOF. It is recommended to cast function arguments to the proper type even if it appears that type conversion will take care of it on its own.
19: Confusion of variable names. It is recommended that such identifiers be unique in the first 6 characters to ensure portability of code.
20: In general, excessively tricky and clever code. Programs are nasty beasts and even if it works, it will have to be modified and even ported to different languages. Maintain a clean structure and do the simple straightforward thing, unless it imposes an unacceptable penalty.