http://www.oocities.org/SiliconValley/Peaks/8778/TAU_advprog.html
2 Reminding C
2.1 Basic C Programming
2.1.1 C Program Structure
A C program basically has the following form:
- Preprocessor Commands
- Type definitions
- Function prototypes - declare function types and variables passed
to function.
- Variables
- Functions
We must have a main() function.
A function has the form:
type function_name( parameters)
{
local variables declaration
C statements
}
If the type definition is omitted C assumes that function returns an
integer type. NOTE: This can be a source of problems in a program.
So the classic example of the simplest C program:
main()
{
printf( "Hello world!\n");
exit( 0);
}
NOTE:
- C requires a semicolon at the end of every statement.
- printf is a standard C function - called from main.
- \n signifies newline. Formatted output - more later.
- exit() is also a standard function that causes the program to
terminate.
- Strictly speaking it is not needed here as it is the last line of
main() and the program will terminate anyway.
2.1.2 Variables
C has the following simple data types:
C type
|
Size (bytes) - platform dependent
|
Lower bound
|
Upper bound
|
char
|
1
|
-128
|
127
|
unsigned char
|
1
|
0
|
255
|
short int
|
2
|
-32768
|
32767
|
unsigned short int
|
2
|
0
|
65535
|
(long) int
|
4
|
-231
|
231 - 1
|
float
|
2
|
-3.2 * 1098
|
3.2 * 1098
|
double
|
4
|
-1.7 * 10908
|
1.7 * 10908
|
On most 32-bits systems all ints are long ints unless
specified as short int explicitly.
NOTE: There is NO Boolean type in C - you should use char, int or
(better) unsigned char.
Unsigned can be used with all char and int types.
To declare a variable in C, do:
var_type list variables;
e.g.:
int i, j, k;
float x, y, z;
2.1.2.1 Defining Global Variables
Global variables are defined outside of any function in the following
way:
short number, sum;
int big_number;
char letter;
main()
{
}
It is also possible to pre-initialise global variables using the =
operator for assignment.
For example:
float sum = 0.0;
int big_sum = 0;
C also allows multiple assignment statements using =, for example:
a = b = c = 3;
...which is the same as, but sometimes more efficient than:
a = 3;
b = 3;
c = 3;
This kind of assignment is only possible if all the variable types
in the statement are the same.
You can define your own types use typedef. This has greater
relevance for creation more complex data structures.
As an example of a simple use let us consider how we may define two
new types real and letter. These new types can then be used in the
same way as the pre-defined C types:
typedef real float;
typedef letter char;
func()
{
real a;
letter c;
}
2.1.2.2 Printing Out and Inputting Variables
C uses formatted output. The printf function has a special formatting
character (%) - a character following this defines a certain format
for a variable:
%c - character
%d - integer
%f - float
e.g.:
printf( "%c%d%f", ch, i, x);
NOTE: Format statement enclosed in "...", variables follow after. Make
sure order of format and variable types match up.
scanf() is the function for inputting values to a data structure: Its
format is similar to printf, e.g.
scanf( "%c%d%f", &ch, &i, &x);
NOTE: & before variables. Please remember to include it. It is to
do with pointers.
2.1.3 Arithmetic Operations
As well as the standard arithmetic operators (+, -, *, /) found in most
languages, C provides some more operators.
Assignment is = e.g.
i = 5;
ch = 'y';
Increment ++, Decrement -- which are usually more efficient than their
long hand equivalents, for example:
x ++;
is faster than
x = x + 1
The ++ and -- operators can be either in post-fixed or pre-fixed.
With pre-fixed the value is computed before the expression is
evaluated whereas with post-fixed the value is computed after the
expression is evaluated.
In the example below, ++z is pre-fixed and the w-- is post-fixed:
int x,y,z;
main()
{
x = (( ++z ) * ( w--)) % 100;
}
This would be equivalent to:
int x,y,z;
main()
{
z++;
x = (z * w) % 100;
w--;
}
The % (modulus) operator only works with integers.
Division / is for both integer and float division. So be careful.
The answer to: x = 3/2 is 1 even if x is declared a float!!
RULE: If both arguments of / are integer then do integer division.
So make sure you do this. The correct (for division) answer to the
above is x = 3.0/2 or x = 3/2.0 or (better) x = 3.0/2.0.
There is also a convenient shorthand way to express computations in C.
It is very common to have expressions like: i = i + 3 or
x = x * (y + 2). This can be written in C in a shorthand form like
this:
i += 3 and x *= y + 2.
NOTE: that x *= y + 2 means x = x * (y + 2) and NOT x = x * y + 2.
2.1.4 Comparison Operators
To test for equality is: ==
A warning: Beware of using "=" instead of "==", such as writing
accidentally.
if ( i = j ) ...
This is a perfectly LEGAL C statement (syntactically speaking) which
copies the value in "j" into "i", and delivers this value, which will
then be interpreted as TRUE if j is non-zero. This is called
assignment by value - a key feature of C.
Not equals is: !=
Other operators < (less than), > (grater than), <= (less than
or equals), >= (greater than or equals) are as usual.
2.1.5 Logical Operators
Logical operators are usually used with conditional statements. The
two basic logical operators are( there is also unary NOT -
! operator):
&& for logical AND,
|| for logical OR.
Beware & and | have a different meaning for bitwise AND and OR.
2.1.6 Order of Precedence
It is necessary to be careful of the meaning of such expressions as
a + b * c. We may want the effect as either
(a + b) * c
or
a + (b * c)
All operators have a priority, and high priority operators are
evaluated before lower priority ones.
Operators of the same priority are evaluated from left to right, so
that
a - b - c
is evaluated as
(a - b ) - c
as you would expect.
From high priority to low priority the order for all C operators is:
() [] -> .
|
! ~ -(unary) * & sizeof castings ++ --
|
* / %
|
+ -
|
< <= >= >
|
== !=
|
&
|
^
|
|
|
&&
|
||
|
?:
|
= += -= *= /=
|
,(comma)
|
Thus
a < 10 && 2 * b < c
is interpreted as
( a < 10 ) && ( ( 2 * b ) < c )
2.2 Conditionals
There are various methods that C can control the flow of logic in a
program. Apart from slight syntactic variation they are similar to
other languages.
As we have seen following logical operations exist in C:
==, !=, ||, &&
One other operator is the unitary - it takes only one argument -
not ! .
These operators are used in conjunction with the following
statements:
- The if statement
- The ?: operator
- The switch statement
2.2.1 The if statement
The if statement has the same function as other languages. It has
three basic forms:
if( condition)
statements
or:
if( condition)
statements1
else
statements2
or (combination of the previous):
if( condition1)
statements1
else if( condition2)
statements2
else
statements3
For example:
int x, y, z;
main()
{
x = ...
if( x > 0)
y = x;
else
z = x;
}
2.2.2 The ?: operator
The ? (ternary condition) operator is a more efficient form for
expressing simple if statements. It has the following form:
condition ? expression1 : expression2
It simply states:
if condition then expression1 else expression2
For example to assign the maximum of a and b to z:
z = ( a > b) ? a: b;
which is the same as:
if( a > b)
z = a;
else
z = b;
2.2.3 The switch statement
The C switch allows multiple choice of a selection of items at one
level of a conditional where it is a far neater way of writing
multiple if statements:
switch( expression)
{
case item1:
statements1
break;
case item1:
statements1
break;
...
default:
statements1
break;
}
In each case the value of item must be a constant, variables are
not allowed.
The break is needed if you want to terminate the switch after
execution of one choice. Otherwise the next case would get evaluated.
Note: This is source for common mistakes (the forgotten break). The
default case is optional and catches any other cases.
For example:
switch( letter)
{
case ' ':
case '\t':
case '\n':
num_of_spaces++;
break;
case 'A':
case 'E':
case 'I':
case 'O':
case 'U':
num_of_vowels++;
/* note: no break */
default:
num_of_letters++;
break;
}
If the value of letter is ' '(space), '\t'(tab) or '\n'(new line character) then num_of_spaces is incremented.
In the above example if the value of letter is 'A', 'E', 'I', 'O' or
'U' then num_of_vowels is incremented.
And for both these vowel values and all other non-space symbols number
of num_of_letters is incremented.
2.3 Looping and Iteration
2.3.1 The for statement
The C for statement has the following form:
for( expression1; expression2; expression3)
statements
expression1 is the initializer; expression2 is the terminate test;
expression3 is the modifier (which may be more than just simple
increment). NOTE: C basically treats the for statements as the while
type loops. For example:
main()
{
int x;
for( x = 3; x > 0; x--)
printf( "x=%d\n", x);
}
...prints:
x=3
x=2
x=1
2.3.2 The while statement
The while statement is similar to those used in other languages
although more can be done with the expression statement - a standard
feature of C. The while has the form:
while( expression)
statements
For example (the same program as in the example for the for
statement):
main()
{
int x;
x = 3;
while( x > 0)
{
printf( "x=%d\n", x);
x -- ;
}
}
Because the while loop can accept expressions, not just
conditions, the following is legal and furthermore it performs
complete operations within the while expression:
while( (ch = getchar()) != 'q')
putchar( ch);
This example uses C standard library functions getchar() - reads a
character from the keyboard - and putchar() - writes a given char to
screen. The while loop will proceed to read from the keyboard and echo
characters to the screen until a 'q' character is read. NOTE: This
type of operation is used a lot in C and not just with character
reading!
2.3.3 The do-while statement
C's do-while statement has the form:
do
statement
while( expression);
For example (again the same program):
main()
{
int x = 3;
do {
printf( "x=%d\n", x);
x -- ;
}
while( x > 0)
}
The main difference between while(){} and
do{}while() is: do-while always executes statement at
least once and only then it checks the condition.
2.3.4 break and continue
C provides two commands to control how we loop:
- break - exit form loop or switch.
- continue - skip 1 iteration of loop.
Consider the following example where we read in integer values and
process them according to the
following conditions. If the value we have read is negative or zero,
we wish immediately abandon the loop. If the value read is great than
100, we wish to ignore it and continue to the next value in the
data.
while( scanf( "%d", &val) == 1)
{
if( val <= 0)
break; /* abandon the loop */
if( val > 100)
continue; /* skip the val */
/* some actions with the val */
}
2.4 Arrays
2.4.1 Single and Multi-dimensional Arrays
Let us first look at how we define arrays in C:
int list_of_numbers[50];
BEWARE: In C Array subscripts start at 0 and end one less than the
array size. For example, in the above case valid subscripts range
from 0 to 49. This is a BIG difference between C and other languages
and does require a bit of practice to get in the right frame of
mind.
Elements can be accessed in the following ways:-
num3 = list_of_numbers[2];
list_of_numbers[5] = 100;
Multi-dimensional arrays can be defined as follows:
int table_of_numbers[50][50];
for two dimensions. (For further dimensions simply add more [].)
Elements can be accessed in the following ways:
a_number = table_of_numbers[2][3];
table_of_numbers[15][5] = 100;
2.4.2 Strings
In C Strings are defined as arrays of characters. For example, the
following defines a string of 50 characters:
char name[50];
C has no string handling facilities built in and so the following are
all illegal:
char name[50];
name = "john";
However, there is a special library of string handling routines, e.g.
the previous "assignment" may be done so:
char name[50];
strcpy( name, "john");
To print a string we use printf with a special %s control character:
printf("%s", name);
NOTE: We just need to give the name of the string.
In order to allow variable length strings the 0 character is used
to indicate the end of a string.
So we if we have a string char name[50]; and we store the
"DAVE" in it its contents will look like:
Byte no:
|
0
|
1
|
2
|
3
|
4
|
...
|
50
|
Value
|
'D'
|
'A'
|
'V'
|
'E'
|
0
|
|
|
2.6 Functions
C provides functions which are again similar most languages. One
difference is that C regards main() as function. Also unlike some
languages, such as Pascal, C does not have procedures - it uses
functions to service both requirements.
Let us remind ourselves of the form of a function.
return_type function_name( parameters)
{
local_variables
statements
}
Let us look at an example to find the average of two integers:
float average( float a, float b)
{
float av;
av = ( a + b) /2.0;
return( av);
}
We would call the function as follows:
main()
{
float x = 5, y = 10, z;
z = average( x, y);
printf( "average = %f\n", z);
}
Note: The return statement passes the result back to the main
program.
2.6.1 void functions
If you do not want to return a value you must use the return type void
and miss out the return statement:
void print_first_10()
{
int i;
for( i = 1; i <= 10; i ++)
printf( "%d\n", i);
}
main()
{
print_first_10();
}
NOTE: We must have () even for no parameters unlike some languages.
2.6.2 Functions and Arrays
Single dimensional arrays can be passed to functions as follows:
float find_max( float list[], int size)
{
int n;
float max = list[0];
for( n = 1; n < size; n++)
if( max < list[n])
max = list[n];
return( max);
}
Here the declaration float list[]; tells C that list is an
array of float. Note we do not specify the dimension of the array
when it is a parameter of a function.
Multi-dimensional arrays can be passed to functions as follows:
void print_table( int x_size, int y_size, float table[][5])
{
int x, y;
for( x = 0; x < x_size; x ++)
{
for( y = 0; y < y_size; y ++)
printf( "\t%d", table[x][y]);
printf( "\n");
}
}
Here float table[][5]; tells C that table is an array of
dimension N*5 of float. Note we must specify the second (and
subsequent) dimension of the array BUT not the first dimension.
2.6.3 Function Prototyping
Before you use a function C must have knowledge about the type it
returns and the parameter types the function expects.
The ANSI standard of C introduced a new (better) way of doing this
than previous versions of C. (Note: all new versions of C now adhere
to the ANSI standard.)
The importance of prototyping is twofold.
- It makes for more structured and therefore easier to read code.
- It allows the C compiler to check the syntax of function calls.
How this is done depends on the scope of the function. Basically if a
functions has been defined before it is used (called) then you are OK
to merely use the function.
If NOT then you must declare the function. The declaration simply
states the type the function returns and the type of parameters used
by the function.
It is usual (and therefore good) practice to prototype all functions
at the start of the program, although this is not strictly
necessary.
To declare a function prototype simply state the type the function
returns, the function name and in brackets list the type of parameters
in the order they appear in the function definition. E.g.
int strlen(char []);
This states that a function called strlen returns an integer value
and accepts a single string as a parameter.
2.7 Further Data Types
2.7.1 Structures
Structures in C allow to define new data type consisting of several
other simple data types or structures. For example:
struct gun
{
char name[50];
int magazine_size;
float calibre;
};
struct gun arnies;
defines a new structure gun and makes arnies an instance of it.
NOTE: that gun is a tag for the structure that serves as shorthand
for future declarations. We now only need to say struct gun and the
body of the structure is implied as we do to make the arnies variable.
The tag is optional.
Variables can also be declared between the } and ; of a struct
declaration, i.e.:
struct gun
{
char name[50];
int magazine_size;
float calibre;
} arnies;
struct's can be pre-initialised at declaration:
struct gun arnies = { "Kalashnikov", 25, 7.62};
which gives arnies a 7.62mm Kalashnikov with 25 rounds of
ammunition.
To access a member (or field) of a struct, C provides the . operator.
For example, to give arnie more rounds of ammunition:
arnies.magazineSize=100;
2.7.2 Defining New Data Types
typedef can also be used with structures. The following creates a
new type agun which is of type struct gun and can be initialised as
usual:
typedef struct gun
{
char name[50];
int magazine_size;
float calibre;
} agun;
agun arnies = { "Kalashnikov", 25, 7.62};
Here gun still acts as a tag to the struct and is optional. Indeed
since we have defined a new data type it is not really of much use.
agun is the new data type. arnies is a variable of
type agun which is a structure. C also allows arrays of
structures:
agun arniesguns[1000];
This gives arniesguns a 1000 guns. This may be used in the following
way:
arniesguns[51].calibre = 5.45;
gives Arnie's gun number 50 a calibre of, and:
itscalibre = arniesguns[0].calibre;
assigns the calibre of Arnie's first gun to itscalibre.
2.7.3 Unions
A union is a variable which may hold (at different times) objects of
different sizes and types. C uses the union statement to create
unions, for example:
union number
{
short shortnumber;
long longnumber;
float floatnumber;
} anumber;
defines a union called number and an instance of it called anumber.
number is a union tag and acts in the same way as a tag for a
structure.
Members can be accessed in the following way:
printf( "%d\n", anumber.longnumber);
This clearly displays the value of longnumber.
When the C compiler is allocating memory for unions it will always
reserve enough room for the largest member (in the above example
this is 8 bytes for the double).
2.7.4 Coercion or Type-Casting
C is one of the few languages to allow coercion, that is forcing one
variable of one type to be another type.
C allows this using the cast operator (). So:
int int_val;
float float_val = 9.87;
int_val = (int)float_val;
assigns 9 (the fractional part is thrown away) to int_val. And:
int int_val = 10;
float float_val;
float_val = (float)int_val;
assigns 10.0 to float_val.
When cast is necessary? E.g. to get really float result of division:
int int_val1 = 10;
int int_val2 = 4;
float float_val;
float_val = (float)int_val1 / (float)int_val2;
This ensures floating point division.
2.7.5 Enumerated Types
Enumerated types contain a list of constants that can be addressed
in integer values. We can declare types and variables as follows:
enum days {mon, tues, ..., sun}week;
enum days week1, week2;
NOTE: As with arrays first enumerated name has index value 0. So mon
has value 0, tues 1, and so on.
The week1 and week2 are variables.
We can define other values:
enum spec_chars { new_line = '\n', tab = '\t', backspace = '\b'};
We can also override the 0 start value:
enum months {jan= 1, feb, mar, ......, dec};
Here it is implied that feb = 2 etc.
2.7.6 Static Variables
A static variable is local to particular function. However, it is
only initialized once (on the first call to function).
Also the value of the variable on leaving the function remains
intact. On the next call to the function the static variable has
the same value as on the last leaving.
To define a static variable simply prefix the variable declaration
with the static keyword. For example:
void stat_test()
{
int local_var = 0;
static int static_var = 0;
printf( "local = %d, static = %d\n", local_var, static_var);
local_var ++ ;
static_var ++ ;
}
main()
{
int n;
for( n = 0; n < 3; n++)
stat_test();
}
Output is:
local = 0, static = 0
local = 0, static = 1
local = 0, static = 2
Clearly the auto_var variable is created each time. The static_var
is created once and remembers its value.
2.8 Common Mistakes in C
2.8.1 Assignment (=) versus Comparison (==)
C uses assignment by value so
if (a = b) ...
is legal. Note: b is assigned to a, the expression a = b results with
the new value of a, so the if is equivalent to:
a = b;
if( b) ...
I.e. it is true if b != 0, false if b == 0.
2.8.2 Missing () of a function
It is common to forget to put () at the end of a function The () must
be placed EVEN if there are no parameters passed to the function.
2.8.3 Array indices
An array of n elements has a
0..n-1 index range. (NOT
1-n range!!!)
2.8.4 C is Case Sensitive
The upper/lower case chars are regarded as being different in C.
The variables my_var and My_var are completely different variables!
2.8.5 Semicolons end every statement
This is easy to forget but the compiler will pick this up (usually
this is just a syntax error).
2.9 Pointers
Pointers are a fundamental part of C. If you cannot use pointers
properly then you have basically lost all the power and flexibility
that C allows. The secret to C is in its use of pointers.
C uses pointers a lot. Why?
- It is the only way to express some computations.
- It produces compact and efficient code.
- It provides a very powerful tool.
C uses pointers explicitly with:
- Arrays,
- Structures,
- Functions.
2.9.1 What is a Pointer?
A pointer is a variable containing the address in memory of another
variable. We can have a pointer to any variable type.
The unary operator & gives the "address of a variable".
The indirection (or dereference) operator * gives the "contents of
an object pointed to by a pointer".
To declare a pointer to a variable do:
int *pointer;
NOTE: We must associate a pointer to a particular type: You can't
assign the address of a short int to a long int, for instance
(although it is possible to do with casting).
Consider the effect of the following code:
int x = 1, y = 2;
int* ip;
ip = &x;
y = *ip;
*ip = 3;
Assume for the sake of this discussion that variable x resides at
memory location 100, y at 200 and ip at 1000. Note: a pointer is
a variable and thus its values need to be stored somewhere.
Now the assignments x = 1 and y = 2 obviously load these values into
the variables. ip is declared to be a pointer to an integer and is
assigned to the address of x (&x). So ip gets loaded with the
value 200.
Next y gets assigned to the contents of ip. In this example ip
currently points to memory location 200 - the location of x. So y
gets assigned to the values of x - which is 1.
Finally we can assign a value to the contents of a pointer (ip).
IMPORTANT: When a pointer is declared it does not point anywhere. You
must set it to point somewhere before you use it.
So the source:
int* ip;
*ip = 100;
will generate an error (usually program crash). The correct use might
be:
int* ip;
int x;
ip = &x; /* assign to ip some REAL memory address */
*ip = 100;
We can do integer arithmetic on a pointer (so called pointer
arithmetic):
int *ip;
...
ip += 10;
++ip;
The reason we associate a pointer to a data type is so that it knows
how many bytes the data is stored in. When we increment a pointer
we increase the pointer by one "block" memory.
So for a character pointer ++ch_ptr adds 1 byte (sizeof( char) == 1)
to the address. For an integer or float ++ip or ++flp adds 4 bytes
(sizeof( int) == 4 and sizeof( float) == 4) to the address. So the
code:
float* fp;
float arr[100];
fp = arr; /* fp points to the beginning of arr, i.e. to arr[0] */
fp += 2; /* fp points to arr[2] */
In the statement fp += 2; the pointer fp moves 2 float posisitons,
i.e. 8 bytes ( 2 * sizeof( float) == 2 * 4).
2.9.2 Pointer and Functions
Let us now examine the close relationship between pointers and C's
other major parts. We will start with functions.
When C passes arguments to functions it passes them by value.
There are many cases when we may want to alter a passed argument
in the function and receive the new value back once to function has
finished. C uses pointers explicitly to do this. (Other languages
mask the fact that pointers also underpin the implementation of
this.)
The best way to study this is to look at an example where we must
be able to receive changed parameters.
Let us try and write a function to swap variables around?
The usual function call:
swap( a, b); /* WON'T WORK */
Pointers provide the solution: Pass the address of the variables to
the functions and access address of function.
Thus our function call in our program would look like this:
swap( &a, &b);
The code to swap is fairly straightforward:
void swap( int* px, int* py)
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}
2.9.3 Pointers and Arrays
Pointers and arrays are very closely linked in C.
(Hint: think of array elements arranged in consecutive memory
locations.)
Consider the following:
int a[10], x;
int* pa;
pa = &a[0]; /* pa points to address of a[0] */
x = *pa; /* the same: x = a[0] */
To get somewhere in the array using a pointer we could do:
pa + i <------> a[i]
WARNING: There is no bound checking of arrays and pointers so you
can easily go beyond array memory and overwrite other things.
C however is much more subtle in its link between arrays and
pointers.
For example we can just type
pa = a;
instead of
pa = &a[0];
and
a[i]
can be written
as *(a + i)
i.e. &a[i] <-------> a + i, i.e. name of an array is also
a pointer to the beginning of the array in the memory.
We also express pointer addressing like this:
pa[i] <-------> *(pa + i)
However pointers and arrays are different:
- A pointer is a variable. We can do pa = a and pa++.
- An array is NOT a variable. a = pa and a++ ARE ILLEGAL.
This stuff is very important. Make sure you understand it.
We can now understand how arrays are passed to functions. When an
array is passed to a function what is actually passed is its initial
elements location in memory.
So:
strlen(s) <-------> strlen(&s[0])
This is why we declare the function:
int strlen( char s[]);
An equivalent declaration is:
int strlen(char *s);
since char s[] <-------> char *s.
strlen() is a standard library function that returns the length of
a string. Let's look at how we may write a function:
int strlen( char* s)
{
int i;
for( i = 0; s[i] != '\0'; i++)
;
return( i);
}
Now lets write a function to copy a string to another string.
(strcpy() is a standard library function that does this.)
void strcpy( char* d, char* s)
{
whle( ( *d++ = *s++) != '\0')
; /* NOTE: Uses of empty statements */
}
This uses pointers and assignment by value.
2.9.4 Arrays of Pointers
We can have arrays of pointers since pointers are variables. Let's
take an example: sort lines of text of different length. (NOTE: Text
can't be moved or compared in a single operation.)
Arrays of pointers are a data representation that will cope
efficiently and conveniently with variable length text lines.
How can we do this?:
- Store lines end-to-end in one big char array. '\0' will delimit
lines.
- Store pointers in a different array where each pointer points
to 1st char of each new line.
- Compare two lines using strcmp() standard library function.
- If 2 lines are out of order - swap pointer in pointer array
(not text).
This eliminates:
- complicated storage management.
- high overheads of moving lines.
2.9.5 Multidimensional arrays and pointers
In C: a 2D array is really a 1D array, each of whose elements is
itself an array. Hence:
a[n][m] notation.
Array elements are stored row by row.
When we pass a 2D array to a function we must specify the number
of columns - the number of rows is irrelevant.
The reason for this is pointers again. C needs to know how many
columns in order that it can jump from row to row in memory.
Considering a[5][35] to be passed in a function:
We can do:
f(int a[][35]) {.....}
Now lets look at the (subtle) difference between pointers and arrays.
Strings are a common application of this. Consider:
char* ptr_name[10];
char arr_names[10][20];
We can legally do ptr_name[3][4] and arr_name[3][4] in C. However:
- arr_name is a true 200 element 2D char array; access elements
via row + col + base_address in memory.
- ptr_name has 10 pointer elements.
The disadvantages of the latter are:
1. there are extra memory units used for pointers;
2. the programmer must worry to supply the necessary memory for
the strings themselves (usually via dynamic memory allocation - will
be discussed later).
The advantage is that each pointer can point to arrays be of
different length.
2.9.6 Static Initialization of Pointer Arrays
Initialization of arrays of pointers is an ideal application for
an internal static array.
void some_func()
{
static char* months[] =
{
"jan",
"feb",
...
};
}
2.9.7 Pointers and Structures
These are fairly straight forward and are easily defined. Consider
the following:
struct COORD
{
float x, y, z;
};
struct COORD pt;
struct COORD* pt_ptr;
pt_ptr = &pt;
The -> operator lets us access a member of the structure pointed
to by a pointer, i.e.:
pt_ptr->x = 1.0;
pt_ptr->y = pt_ptr->y - 3.0;
(Recall the examples using . operator allowing access to a member
of the structure when there is no use of pointers.)
Let's take a very important example: a trivial linked list of
integers. It means that we want to store many integers somewhere
but we don't know how many integers there will be.
Let's do it:
typedef struct element_struct
{
int val;
struct element_struct* next;
} NODE;
Now we can link several elements. In a C program this may look:
NODE n1, n2, n3;
n1.next = &n2;
n2.next = &n3;
...
(We don't want to discuss how to create new elements and how to
manage such a list, e.g. what is the sign of the end - all this
will be discussed later.)
Note an important thing: we can only declare next as a pointer
to NODE. We cannot have a element of the variable type as this
would set up a recursive definition which is NOT ALLOWED. We are
allowed to set a pointer reference since 4 bytes are set aside for
any pointer.
The above code links a node n1 to n2, etc. We will look at this
matter further later.
2.9.8 Common Pointer Pitfalls
Here we will highlight two most common mistakes made with pointers.
2.9.8.1 Not assigning a pointer to memory address before using it
int* x;
*x = 100;
The error here is that we assign some value (100) to not initialized
memory. We need a physical memory location, e.g.:
int y;
int* x;
x = &y;
*x = 100;
This error may be hard to spot. NO COMPILER ERROR. Also x could
some random address so we can corrupt another memory field.
2.9.8.2 Illegal indirection
int y;
int* x;
x = &y;
x = 100;
The error here is that we assign some value (100) to the pointer
itself not in the memory it points to. So instead to set y to 100
(as we wanted) we assigned 100 to the pointer (and due to such a
value is usually an invalid to address a memory we will get run-time
error next time we try to obtain value from the *x.)
2.10 Dynamic Memory Allocation
Dynamic allocation is a pretty unique feature to C (amongst high
level languages). It enables us to create data types and structures
of any size and length to suit our programs need within the
program.
We will look at two common applications of this:
- dynamic arrays
- dynamic data structure, e.g. linked lists
2.10.1 Malloc
The function malloc is most commonly used to attempt to "grab" a
portion of memory. It is defined by:
void* malloc( unsigned int number_of_bytes);
That is to say it returns a pointer to a character that is the start
in memory of the reserved portion of size number_of_bytes. If memory
cannot be allocated a NULL pointer is returned (a good C program
must check this).
So:
char* cp;
cp = (char*) malloc( 100); /* to get 100 bytes and assign the */
/* start address to cp */
If you want a pointer to another data type you must use coercion. Also
it is usual to use the standard built-in function sizeof() to specify
the number of bytes:
int* ip;
ip = (int*)malloc( 100 * sizeof( int));
The (int *) means coercion to an integer pointer. Coercion to the
correct pointer type is very important to ensure pointer arithmetic
is performed correctly.
It is good practice to use sizeof() even if you know the actual
size you want - it makes for platform independent (portable) code.
sizeof() can be used to find the size of any data type, variable
or structure. Simply supply one of these as an argument to the
function.
So:
int i;
struct COORD { float x, y, z; };
typedef struct COORD PT;
PT pt;
PT* pt_ptr;
All the following:
sizeof( int)
sizeof( i)
sizeof( struct COORD)
sizeof( PT)
sizeof( pt)
sizeof( pt_ptr)
is acceptable and completely legal.
Like above we can treat the reserved (allocated) memory like an
array. i.e. we can do things like:
int* ip;
ip = (int*)malloc( 100 * sizeof( int));
for( i = 0; i < 100; i++)
scanf( "%d", ip[i]);
When we finish to use the allocated memory we should "return" it
using another standard function:
void free( void* ptr);
2.10.2 Linked Lists
Let us now return to out linked list example:
typedef struct element_struct
{
int val;
struct element_struct* next;
} NODE;
Let's define that the sign of the last element of the list is NULL
in its "next" field. So we can write, for example, a procedure to
print the list (the function printf() used here will be discussed
in more details later):
void print_list( NODE* list)
{
NODE* node;
for( node = list; node != NULL; node = node->next)
printf("%d\n", node->val);
}
We can now try to grow the list dynamically:
new_node = (NODE*) malloc( sizeof( NODE));
This will allocate memory for a new element. Well, now we can store
as many elements as we need, e.g.:
int main()
{
NODE* first;
NODE* node;
NODE* new_node;
first = NULL;
while( something)
{
new_node = (NODE*) malloc( sizeof( NODE));
new_node->val = some_value;
new_node->next = NULL;
/* if our list is empty - that's all */
if( first == NULL)
{
first = new_node;
continue;
}
/* let's find the last element of the list */
for( node = first; node->next != NULL; node = node->next)
;
node->next = new_node;
}
/* Now we can use the newly built list, e.g. to print it
using the procedure written above */
print_list( first);
}
Do not forget to free the memory not needed anymore with the free()
function.
2.11 Input and Output (I/O)
Now we will look at many forms of I/O. We have briefly mentioned
some forms before.
Firstly your programs will need to include the standard I/O header
file so do:
#include <stdio.h>
2.11.1 Streams
Streams are a portable way of reading and writing data. They provide
a flexible and efficient means of I/O.
A stream may be a file or a physical or virtual device (e.g. printer,
monitor or window) which is manipulated with a handler of the stream
(usually implemented as a pointer).
The most important and frequently used streams in C are represented
by structure FILE (which is defined in stdio.h). We simply need to
refer to the FILE structure in C programs when performing I/O with
streams. Usually FILE is used to deal with real files, keyboard
input and terminal output (that may be redirected but these are
details of a specific operating system, e.g. MS Windows).
To use a stream we need:
- to declare a variable or pointer of this type in our programs
(we do not need to know any more specifics about this definition),
- to open a stream before doing any I/O,
- then access it,
- and then close it.
2.11.1.1 Predefined Streams
Most operating systems define 3 predefined streams (in stdio.h):
- stdin (standard input stream)
- stdout (standard output stream)
- stderr (standard output stream for error/warning/etc. messages)
They all use text as the method of I/O. These streams are automatically
open in any program.
2.11.1.2 Redirection
By default the standard input stream accepts input from the keyboard
while the standard output and standard error streams prints to the
screen (terminal, window, etc.). The most of operating systems allow
to override the predefined I/O defaults.
This is not part of C but operating system dependent. We will do redirection from the command line.
> - redirect stdout to a file.
So if we have a program, out, that usually prints to the screen then
out > file1
will send the output to a file, file1.
< - redirect stdin from a file to a program.
So if we are expecting input from the keyboard for a program, in we
can read similar input from a file
in < file2
| - pipe: puts stdout from one program to stdin of another
prog1 | prog2
e.g. to sent output (usually to console) of a program to another
program performing text formatting:
prog | fmt
2.11.2 Basic I/O
There are a couple of function that provide basic I/O facilities.
Probably the most common are: getchar() and putchar(). They are
defined and used as follows:
int getchar(void) - reads a char from stdin.
int putchar(int ch) - writes a char to stdout, returns character
written.
E.g.:
int ch;
ch = getchar();
putchar( ch);
Related Functions:
int getc(FILE *stream)
int putc(int ch,FILE *stream)
2.11.3 Formatted I/O
We have seen examples of how C uses formatted I/O already. Let's
look at this in more detail.
2.11.3.1 printf
The function is defined as follows:
int printf(char *format, arg list ...) - prints to stdout the list
of arguments according specified format string. Returns number of
characters printed.
The format string has 2 types of object:
- ordinary characters - these are copied to output.
- conversion specifications - denoted by % and listed in the table:
Format spec (%)
|
Type
|
Result
|
c
|
char
|
single character
|
i, d
|
int
|
decimal number
|
o
|
int
|
octal number
|
x, X
|
int
|
hexadecimal number
|
u
|
int
|
unsigned int
|
s
|
char*
|
print string terminated by '\0'
|
f
|
double, float
|
format: m.ddd...
|
e,E
|
double, float
|
scientific format: -5.46e003
|
g, G
|
double, float
|
the most compact of f or e formats
|
%
|
-
|
print % character
|
Between % and format char we can put:
- (minus sign) - left justify.
integer number - field width.
m.d - m = field width, d = precision of number of digits after
decimal point or number of chars from a string.
So:
printf( "%-6.3f", 15.9826)
The output on the screen is:
15.983
and:
printf( "my part is 15.5%%\n");
...outputs:
my part is 15.5%
2.11.3.2 scanf
This function is defined as follows:
int scanf(char *format, args....) - reads from stdin and puts input
in address of variables specified in args list. Returns number of
chars read.
Format control string similar to printf().
Note: The ADDRESS of variable or a pointer to one is required by
scanf.
scanf("%d'', &i);
We can just give the name of an array or string to scanf since this
corresponds to the start address of the array/string.
char line[80];
scanf( "%s", line);
2.11.4 Files
Files are the most common form of a stream.
The first thing we must do is open a file. The function fopen() does
this:
FILE *fopen(char *name, char *mode)
fopen() returns a pointer to a FILE. The name string is the name of
the file on disc that we wish to access. The mode string controls
our type of access. If a file cannot be accessed for any reason a
NULL pointer is returned.
Modes include:
"r" - read
"w" - write
"a" - append
To open a file we must have a pointer that points to a FILE
structure. So to open a file, called myfile.dat for reading we
would do:
FILE* fp;
fp = fopen( "myfile.dat", "r");
It is good practice to check if file has been opened OK:
FILE* fp;
if( ( fp = fopen( "myfile.dat", "r")) == NULL)
{
printf( "Can't open file myfile.dat\n");
return( some_err_code);
}
... read the file ...
2.11.4.1 Reading and writing FILES
The functions fprintf and fscanf a commonly used to access files.
int fprintf( FILE* stream, char* format, args...);
int fscanf( FILE* stream, char* format, args...);
These are similar to printf() and scanf() except that data is read
from the stream that must have been opened with fopen().
The stream pointer is automatically incremented with ALL file
read/write functions. We do not have to worry about doing this.
FILE* fp;
char line[80];
if( ( fp = fopen( "myfile.dat", "r")) == NULL)
{
printf( "Can't open file myfile.dat\n");
return( some_err_code);
}
fscanf( fp, "%s", line);
Other functions for files:
int getc(FILE *stream);
int fgetc(FILE *stream);
int putc(char ch, FILE *s);
int fputc(char ch, FILE *s);
These are like getchar, putchar.
NOTE: getc is defined as preprocessor MACRO in stdio.h. fgetc is a
C library function. Both achieve the same result.
fflush(FILE *stream) - flushes a stream.
fclose(FILE *stream) - closes a stream.
Of course, we can access predefined streams with fprintf etc.
fprintf(stderr,´´Cannot Compute!!n'');
fscanf(stdin,´´%s'',string);
2.11.5 sprintf and sscanf
These are like fprintf and fscanf except they read/write to a
string.
int sprintf(char *string, char *format, args..);
int sscanf(char *string, char *format, args..);
For example:
float Pi=3.141.59;
char line_for_output[80];
sprintf( line_for_output, "PI=%f", Pi);
2.11.6 Command line input
We can type arguments after the program name when we run the
program. C lets read arguments from the command line which can then
be used in our programs.
We have seen this with the some editor for example:
winword MyDoc.doc
winword is the program, MyDoc.doc the argument.
In order to be able to use such arguments in our code we must define
them as follows:
main(int argc, char **argv)
So our main function now has its own arguments. Here:
- argc is the number of arguments typed - including the program name.
- argv is an array of strings holding each command line argument
- including the program name in the first array element.
A simple program example:
main( int argc, char* argv[])
{
int i;
for( i = 0; i < argc; i++ )
printf( "%d = %s\n", i, argv[i]);
}
Assume it is compiled to run it as argv. So if we type:
argv abc def xyz
The output would be:
0 = argv
1 = abc
2 = def
3 = xyz
2.12 Low Level Operators
We will not discuss such things detailed as we don't need them for
the purposes of this course. Anyway it is necessary to know their
existence, so we will pass them briefly. (More details may be found
in on-line help and reference manuals.)
2.12.1 Bitwise Operators
The bitwise operators of C a summarized in the following table:
&
|
AND
|
|
|
OR
|
^
|
XOR (exclusive OR)
|
~ (unary)
|
One's Complement (0->1; 1->0)
|
<<
|
Left shift
|
>>
|
Right shift
|
NOTE: DO NOT confuse & with &&: & is bitwise AND,
&& is logical AND. Similarly for | and ||.
2.12.2 Bit Fields
Bit Fields allow the packing of data in a structure. This is
especially useful when memory or data storage is at a premium (not
the case of our course's programming). Typical examples:
- Packing several objects into a machine word. e.g. 1 bit flags
can be compacted - Symbol tables in compilers.
- Reading external file formats - non-standard file formats
could be read in. E.g. 9 bit integers.
C lets us do this in a structure definition by putting :bit length
after the variable. e.g.:
struct packed_struct
{
unsigned int flag1:1;
unsigned int flag2:1;
unsigned int flag3:1;
...
};
NOTE:
- Bit fields are always converted to integer type for computation.
- The unsigned definition is important - ensures that no bits are
used as a flag.
2.13 The C Preprocessor
The preprocessor more or less provides its own language which can be
a very powerful tool to the programmer. All preprocessor directives
or commands begin with a #.
Use of the preprocessor is advantageous since it makes:
- programs easier to develop,
- easier to read,
- easier to modify,
- C code more transportable between different machine architectures.
2.13.1 #define
Use this to define constants or any macro substitution. Use as
follows:
#define macro replacement
For example:
#define PI 3.14159
We can also define small "functions'' using #define. For example
max. of two variables:
#define max(A,B) ((A)>(B)?(A):(B))
(Recall that ? is the ternary operator in C.)
Note: that this does not define a proper function max(). All it means
that wherever we place max(x,y) the text gets replaced by the
appropriate definition. (x and y are any variable names - not
necessarily x and y.)
So if we have:
int x = 8;
int y;
y = max(x, 10);
This is preprocessed to:
int x = 8;
int y;
y = ((x)>10?x:10);
BE CAREFUL: if in our C code we typed something like:
x = max( q + r, s + t);
after preprocessing, if we were able to look at the code it would
appear like this:
x = ( ( q + r) > ( s + t) ? ( q + r) : ( s + t));
So we pay by calculations performed twice. Moreover if we write:
x = max( a++, b++);
then we get:
x = ( (a++) > (b++) ? (a++) : (b++));
So we change value of either a or b (whichever is greater) twice!
Other examples of #define could be:
#define percents( x, y) ((x)*100./(y))
2.13.2 #undef
This commands undefined a macro. (A macro must be undefined before
being redefined to a different value.)
2.13.3 #include
This directive includes a file into code.
It has two possible forms:
#include <file>
or
#include "file"
<file> tells the compiler to look first in the directories
specified by user and then where system include files are held
(this place is compiler dependent).
"file" looks for a file first in the current directory and then
in the directories specified by user.
NOTE: different compilers treat "the current directory" differently,
so it is not recommended to use the latter notation.
Included files usually contain C prototypes and declarations from
header files and not (algorithmic) C code.
2.13.4 #if - Conditional inclusion
#if evaluates a constant integer expression. You always need a
#endif to delimit end of statement.
We can have else etc. as well by using #else. Another common use of
#if is with:
#ifdef - if defined
#ifndef - if not defined
These are useful for checking if macros are set - perhaps from
different program modules and header files.
Usually this is used to write portable code (i.e. program that may
be compiled by different compilers on different platforms) - using
macros predefined by compilers.
For example, let's assume that you want to handle as big integer
number as possible. MS C++ has a special built-in type __int64
allowing to store very big numbers (up to about 4e18). So you can
write:
#ifdef _MSC_VER
typedef __int64 BIG_INT;
#else
typedef long BIG_INT;
#endif
BIG_INT my_big_integer;
(Note that sizeof( BIG_INT) will tell you the real size of the
type if you need it.)