Bca & B. Sc.-It Sem-1 C-Programming
Bca & B. Sc.-It Sem-1 C-Programming
Course Dossier
Semester: V
Subject Name: C-PROGRAMMING
Subject Code:
Swarrnim School of Computing & IT
BCA (Honours) Programme
BCA Semester I
Course Outcomes(COs)
1. Analyze a given problem and develop an algorithm to solve the problem.
2. Design, develop and test programs written in 'C'.
3. Write, compile and debug programs in C language.
4. Use different data types in a computer program.
5. Design programs involving decision structures, loops and functions.
Syllabus:
No of
Modul Contents Weightage
Sessions
e
1 Introduction: History, Facilities, Concepts, 15 30%
Uses; Basic Program Structure, Header
Files, Comments; A Simple C program,
Identifiers, Basic Data Types and Sizes,
Constants, Variables, Arithmetic,
Relational and Logical Operators,
Increment and Decrement Operators,
Conditional Operator, Bit-wise Operators,
Assignment Operators, Expressions, Type
Conversions, Conditional Expressions,
Precedence and Order of Evaluation.
Evaluation
1 Assignments / Quizzes / Class Participation / Role 30% (Internal Assessment)
Play/ Project etc.
2 Internal Examination 20% (Internal Assessment)
3 External Examination (University Exam) 50% (External Assessment)
Reference Books:
Sr. Author/s Name of Publisher Edition
No. the Book
1 Peter Vander Linden, Outline of theory TMH Latest
Schaum's and problems of
programming
with C
2 Peter Vander Linden Expert C PHI Latest
programming
3 Balagurusamy E. Computing TMH Latest
Fundamentals
and C
Programming
WEB RESOURCES:
1. Khan Academy (www.khanacademy.org)
2. Computer Hope (www.computerhope.com)
3. TechTerms (www.techterms.com)
4. HowStuffWorks (www.howstuffworks.com)
5. W3Schools (www.w3schools.com)
6. Computer Science for Fun (www.cs4fn.org)
7. Neso Academy (www.nesoacademy.org)
8. Studytonight (www.studytonight.com)
9. Computer Science Unplugged (csunplugged.org)
10. Exploring Computer Science (www.exploringcs.org)
Practical List:- (Credit- 01)
Sr. Practicals No of Sessions
No.
1 Write a program to print “HELLO”. 1
2 Write a program to display multiplication 1
table.
3 Write a program to print +1/2+1/3+1/4+……… 1
+1/N series.
4 Write a program to find sum of all integers 1
greater than 100 & less than 200 and are
divisible by 5.
5 Write a program to convert days into months 1
and days.
Write a program to print following patterns. 2
6
* 12345
* * 2345
* * * 345
* * * * 45
5
AAAAA 1
BBBB 01
CCC 101
DD 0101
E
Notice that there is no space between the square bracket [ and the name of the array. This
statement would cause space for three integers type variables to be created in memory
next to each other as in the diagram below.
------------------------------------
int triplet: | | | |
------------------------------------
The number in the square brackets of the declaration is referred to as the `index' (plural:
indicies) or `subscript' of the array and it must be an integer number between 0 and (in this
case) 2. The three integers are called elements of the array and they are referred to in a
program by writing:
triplet[0]
triplet[1]
triplet[2]
Note that the indicies start at zero and run up to one less than the number which is placed in
the declaration (which is called the dimension of the array.) The reason for this will become
clear later. Also notice that every element in an array is of the same type as every other. It is
not (at this stage) possible to have arrays which contain many different data types. When
arrays are declared inside a function, storage is allocated for them, but that storage space is
not initialized: that is, the memory space contains garbage (random values). It is usually
necessary, therefore, to initialize the array before the program truly begins, to prepare it for
use. This usually means that all the elements in the array will be set to zero.
Why use arrays?
Arrays are most useful when they have a large number of elements: that is, in cases where it
would be completely impractical to have a different name for every storage space in the
memory. It is then highly beneficial to move over to arrays for storing information for two
reasons:
The storage spaces in arrays have indicies. These numbers can often be related to
variables in a problem and so there is a logical connection to be made between an
array an a program.
In C, arrays can be initialized very easily indeed. It is far easier to initialize an array
than it is to initialize twenty or so variables.
The first of these reasons is probably the most important one, as far as C is concerned, since
information can be stored in other ways with equally simple initialization facilities in C. One
example of the use of an array might be in taking a census of the types of car passing on a
road. By defining macros for the names of the different cars, they could easily be linked to
the elements in an array.
Type Array Element
car 0
auto 1
bil 2
The array could then be used to store the number of cars of a given type which had driven
past. e.g.
/***********************************************/
/* */
/* Census */
/* */
/***********************************************/
#include <stdio.h>
#define NOTFINISHED 1
#define CAR 0
#define AUTO 1
#define BIL 2
/************************************************/
main ()
{ int type[3];
int index;
while (NOTFINISHED)
{
printf ("Enter type number 0,1, or 2");
scanf ("%d", &index);
skipgarb();
C would happily try to write the character * at the location which would have corresponded
to the seventh element, had it been declared that way. Unfortunately this would probably
be memory taken up by some other variable or perhaps even by the operating system. The
result would be either:
The value in the incorrect memory location would be corrupted with unpredictable
consequences.
The value would corrupt the memory and crash the program completely! On Unix
systems this leads to a memory segmentation fault.
The second of these tends to be the result on operating systems with proper memory
protection. Writing over the bounds of an array is a common source of error. Remember
that the array limits run from zero to the size of the array minus one.
Arrays and for loops
Arrays have a natural partner in programs: the for loop. The for loop provides a simple way
of counting through the numbers of an index in a controlled way. Consider a one
dimensional array called array. A for loop can be used to initialize the array, so that all its
elements contain zero:
#define SIZE 10;
main ()
{ int i, array[SIZE];
It could equally well be used to fill the array with different values. Consider:
#define SIZE 10;
main ()
{ int i, array[size];
This fills each successive space with the number of its index:
index 0 1 2 3 4 5 6 7 8 9
---------------------------------------
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
contents ---------------------------------------
The for loop can be used to work on an array sequentially at any time during a program, not
only when it is being initialized. The example listing below shows an example of how this
might work for a one dimensional array, called an Eratosthenes sieve. This sieve is an array
which is used for weeding out prime numbers, that is: numbers which cannot be divided by
any number except 1 without leaving a remainder or a fraction. It works by filling an array
with numbers from 0 to some maximum value in the same way that was shown above and
then by going through the numbers in turn and deleting (setting equal to zero) every
multiple of every number from the array. This eliminates all the numbers which could be
divided by something exactly and leaves only the prime numbers at the end. Try to follow
the listing below.
Example Listing
/******************************************************/
/* */
/* Prime Number Sieve */
/* */
/******************************************************/
#include <stdio.h>
main ()
{ short sieve[SIZE];
FillSeive(sieve);
SortPrimes(sieve);
PrintPrimes(sieve);
}
/*********************************************************/
/* Level 1 */
/*********************************************************/
short sieve[SIZE];
{ short i;
/**********************************************************/
short sieve[SIZE];
{ short i;
/***********************************************************/
short sieve[SIZE];
{ short i;
/***********************************************************/
/* Level 2 */
/***********************************************************/
short i,sieve[SIZE];
{ short j, mult = 2;
/* end */
Every element in this grid needs two indicies to pin-point it. The elements are accessed by
giving the coordinates of the element in the grid. For instance to set the element 2,3 to the
value 12, one would write:
array[2][3] = 12;
The usual terminology for the two indicies is that the first gives the row number in the grid
and that the second gives the column number in the grid. (Rows go along, columns hold up
the ceiling.) An array cannot be stored in the memory as a grid: computer memory is a one
dimensional thing. Arrays are therefore stored in rows. The following array:
------------
|1|2|3|
------------
|4|5|6|
------------
|7|8|9|
------------
would be stored:
------------------------------------
|1|2|3|4|5|6|7|8|9|
------------------------------------
* ROW # 1 * ROW # 2 * ROW #3 *
Another way of saying that arrays are stored row-wise is to say that the second index varies
fastest, because a two-dimensional array is always thought of as...
array[row][column]
so for every row stored, there will be lots of columns inside that row. That means the
column index goes from 0..SIZE inside every row, so it is changing faster as the line of
storage is followed.
A three dimensional array, like a cube or a cuboid, could also be defined in the same kind of
way:
double cube[SIZE][SIZE][SIZE];
Three dimensional arrays are stored according to the same pattern as two dimensional
arrays. They are kept in computer memory as a linear sequence of variable stores and the
last index is always the one which varies fastest.
Arrays and Nested Loops
Arrays of more than one dimension are usually handled by nested for loops. A two
dimensional array might be initialized in the following way:
main ()
{ int i,j;
float array[SIZE1][SIZE2];
{ int i,j,k;
float array[SIZE1][SIZE2][SIZE3];
An example program helps to show how this happens in practice. The example below
demonstrates the so-called "Game of Life". The aim is to mimic something like cell
reproduction by applying some rigid rules to a pattern of dots . and stars *. A dot is a place
where there is no life (as we know it!) and a star is a place in which there is a living thing. The
rules will be clear from the listing. Things to notice are the way the program traverses the
arrays and the way in which it checks that it is not overstepping the boundaries of the
arrays.
Example Listing
/*********************************************************/
/* */
/* Game of Life */
/* */
/*********************************************************/
#include <stdio.h>
#define SIZE 20
#define MAXNUM 15
#define INBOUNDS (a>=0)&&(a<SIZE)&&(b>=0)&&(b<SIZE)
#define NORESPONSE 1
/*********************************************************/
/* Level 0 */
/*********************************************************/
main ()
{ int count[SIZE][SIZE];
char array[SIZE][SIZE];
int generation = 0;
/**********************************************************/
/* Level 1 */
/**********************************************************/
char array[SIZE][SIZE];
{ int i,j;
char ch;
/********************************************************/
char array[SIZE][SIZE];
int count[SIZE][SIZE];
{ int i,j;
/*******************************************************/
BuildNextGeneration (array,count)
char array[SIZE][SIZE];
int count[SIZE][SIZE];
{ int i,j;
/*******************************************************/
char array[SIZE][SIZE];
int g;
{ int i,j;
/*******************************************************/
/* Level 2 */
/*******************************************************/
numalive (array,i,j)
{ int a,b,census;
census = 0;
return (census);
}
/********************************************************/
/* Toolkit input */
/********************************************************/
quit()
{ char ch;
while (NORESPONSE)
{
scanf ("%c",&ch);
if (ch != '\n') skipgarb();
switch (ch)
{
case 'q' : case 'Q' : return (1);
default : return (0);
}
}
}
/********************************************************/
skipgarb ()
{
while (getchar() != '\n')
{
}
}
Output of Game of Life
Game of Life
^^^^^^^^^^^^^^^^^^^^
(user types in: (It doesn't matter if the input
.................... spills over the SIZE guide,
.................... because "skipgarb()" discards it.)
.....................
.....................
.....................
..........***........
...........*.........
......................
.....................
.....................
.....................
*********************
.....................
......................
....................
.....................
......................
......................
......................
...................... )
Generation 1
....................
....................
....................
....................
...........*........
..........***.......
..........***.......
....................
....................
....................
.******************.
.******************.
.******************.
....................
....................
....................
....................
....................
....................
....................
Generation 2
....................
....................
....................
....................
..........***.......
....................
..........*.*.......
...........*........
....................
..****************..
.*................*.
*..................*
.*................*.
..****************..
....................
....................
....................
....................
....................
....................
Q for quit. RETURN to continue.
Generation 3
....................
....................
....................
...........*........
...........*........
..........*.*.......
...........*........
...........*........
...*******...****...
..****************..
.******************.
**................**
.******************.
..****************..
...**************...
....................
....................
....................
....................
....................
Generation 4
....................
....................
....................
....................
..........***.......
..........*.*.......
..........***.......
....*****.*.*.**....
..*..............*..
.*................*.
*..................*
*..................*
*..................*
.*................*.
..*..............*..
....************....
....................
....................
....................
....................
Initializing Arrays
Arrays can be initialized in two ways. The first way is by assigning every element to some
value with a statement like:
array[2] = 42;
array[3] = 12;
or perhaps with the aid of one or more for loops. Because it is tedious, to say the least, not
to mention uneconomical, to initialize the values of each element to as different value, C
provides another method, which employs a single assignment operator = and curly braces
{ }. This method only works for static variables and external variables.
Recall that arrays are stored row-wise or with the last index varying fastest. A 3 by 3 array
could be initialized in the following way:
static int array[3][3] =
{
{10,23,42},
{1,654,0},
{40652,22,0}
};
The internal braces are unnecessary, but help to distinguish the rows from the columns. The
same thing could be written:
int array[3][3] =
{
10,23,42,
1,654,0
40652,22,0
};
Take care to include the semicolon at the end of the curly brace which closes the
assignment.
Note that, if there are not enough elements in the curly braces to account for every single
element in an array, the remaining elements will be filled out with zeros. Static variables are
always guaranteed to be initialized to zero anyway, whereas auto or local variables are
guaranteed to be garbage: this is because static storage is created by the compiler in the
body of a program, whereas auto or local storage is created at run time.
then numbers is a pointer to the first floating point number in the array; numbers is a
pointer in its own right. (In this case it is type `pointer to float'.) So the first element of the
array could be accessed by writing:
numbers[0] = 22.3;
or by writing
*numbers = 22.3;
For character arrays, which are dealt with in some depth in chapter 20, this gives an
alternative way of getting at the elements in the array.
char arrayname[5];
char *ptr;
The code above sets the array arrayname to zero. This method of getting at array data is not
recommended by this author except in very simple computer environments. If a program is
running on a normal microcomputer, then there should be few problems with this
alternative method of handling arrays. On the hand, if the microcomputer is multi-tasking,
or the program is running on a larger system which has a limited manager, then memory
ceases to be something which can be thought of as a sequence of boxes standing next to
one another. A multi-tasking system shares memory with other programs and it takes what
it can find, where it can find it. The upshot of this is that it is not possible to guarantee that
arrays will be stored in one simple string of memory locations: it might be scattered around
in different places. So
ptr = arrayname + 5;
might not be a pointer to the fifth character in a character array. This could be found instead
using the & operator. A pointer to the fifth element can be reliably found with:
ptr = &(arrayname[5]);
Be warned!
Arrays as Parameters
What happens if we want to pass an array as a parameter? Does the program copy the
entire array into local storage? The answer is no because it would be a waste of time and
memory. Arrays can be passed as parameters, but only as variable ones. This is a simple
matter, because the name of the array is a pointer to the array. The Game of Life program
above does this. Notice from that program how the declarations for the parameters are
made.
main ()
{
char array[23];
function (array);
.....
}
function (arrayformal)
char arrayformal[23];
{
}
Any function which writes to the array, passed as a parameter, will affect the original copy.
Array parameters are always variable parameters
Questions
1. Given any array, how would you find a pointer to the start of it?
2. How do you pass an array as a parameter? When the parameter is received by a
function does C allocate space for a local variable and copy the whole array to the
new location?
3. Write a statement which declares an array of type double which measures 4 by 5.
What numbers can be written in the indicies of the array?
UNIT-2
Pointer
a pointer is a variable that points to or references a memory location in which data is stored.
In the computer, each memory cell has an address that can be used to access that location
so a pointer variable points to a memory location we can access and change the contents of
this memory location via the pointer.
Pointer declaration:
A pointer is a variable that contains the memory location of another variable in which data is
stored. Using pointer, you start by specifying the type of data stored in the location. The
asterisk helps to tell the compiler that you are creating a pointer variable. Finally you have to
give the name of the variable. The syntax is as shown below.
type * variable name
Address operator:
Once we declare a pointer variable then we must point it to something we can do this by
assigning to the pointer the address of the variable you want to point as in the following
example:
ptr=#
The above code tells that the address where num is stores into the variable ptr. The variable
ptr has the value 21260,if num is stored in memory 21260 address then
First of all, what is declared is a pointer, so space for a `pointer to int' is allocated by the
program and to start off with that space will contain garbage (random numbers), because
no statement like
a = &someint;
has yet been encountered which would give it a value. It will then attempt to fill the
contents of some variable, pointed to by a, with the value 2. This is doomed to faliure. a only
contains garbage so the 2 could be stored anywhere. There may not even be a variable at
the place in the memory which a points to. Nothing has been said about that yet. This kind
of initialization cannot possibly work and will most likely crash the program or corrupt some
other data.
Example Listing
/**********************************************/
/* */
/* Swapping Pointers */
/* */
/**********************************************/
#include <stdio.h>
main ()
{ int *a,*b,*c; /* Declr ptrs */
int A,B; /* Declare storage */
c = a; /* swap pointers */
a = b;
b = c;
}
Types, Casts and Pointers
It is tempting but incorrect to think that a pointer to an integer is the same kind of object as
a pointer to a floating point object or any other type for that matter. This is not necessarily
the case. Compilers distinguish between pointers to different kinds of objects. There are
occasions however when it is actually necessary to convert one kind of pointer into another.
This might happen with a type of variable called "unions" or even functions which allocate
storage for special uses. These objects are met later on in this book. When this situation
comes about, the cast operator has to be used to make sure that pointers have compatible
types when they are assigned to one another. The cast operator for variables, See The Cast
Operator, is written in front of a variable to force it to be a particular type:
(type) variable
i = (int *) ch;
This copies the value of the pointer ch to the pointer i. The cast operator makes sure that
the pointers are in step and not talking at cross purposes. The reason that pointers have to
be `cast' into shape is a bit subtle and depends upon particular computers. In practice it may
not actually do anything, but it is a necessary part of the syntax of C.
The following program illustrate the pointer expression and pointer arithmetic :
In a function declaration, the pointer are very much used . Sometimes, only with a pointer a
complex function can be easily represented and success. In a function definition, the usage
of the pointers may be classified into two groups.
1. Call by reference
2. Call by value.
Call by value:
We have seen that there will be a link established between the formal and actual
parameters when a function is invoked. As soon as temporary storage is created where the
value of actual parameters is stored. The formal parameters picks up its value from storage
area the mechanism of data transfer between formal and actual parameters allows the
actual parameters mechanism of data transfer is referred as call by value. The
corresponding formal parameter always represents a local variable in the called function.
The current value of the corresponding actual parameter becomes the initial value of formal
parameter. In the body of the actual parameter, the value of formal parameter may be
changed. In the body of the subprogram, the value of formal parameter may be changed by
assignment or input statements. This will not change the value of the actual parameters.
Call by Reference:
The address should be pointers, when we pass address to a function the parameters
receiving. By using pointers, the process of calling a function to pass the address of the
variable is known as call by reference. The function which is called by reference can change
the value of the variable used in the call.
/* example of call by reference*?
/* Include< stdio.h >
void main()
{
int x,y;
x=20;
y=30;
printf(”\n Value of a and b before function call =%d %d”,a,b);
fncn(&x,&y); printf(”\n Value of a and b after function call =%d %d”,a,b);
}
fncn(p,q)
int p,q;
{
*p=*p+*p;
*q=*q+*q;
}
Pointer to arrays:
an array is actually very much similar like pointer. We can declare as int *a is an address,
because a[0] the arrays first element as a[0] and *a is also an address the form of
declaration is also equivalent. The difference is pointer can appear on the left of the
assignment operator and it is a is a variable that is lvalue. The array name cannot appear as
the left side of assignment operator and is constant.
/* A program to display the contents of array using pointer*/
main()
{
int a[100];
int i,j,n;
printf(”\nEnter the elements of the array\n”);
scanf(%d,&n);
printf(”Enter the array elements”);
for(I=0;I< n;I++)
scanf(%d,&a[I]);
printf(”Array element are”);
for(ptr=a,ptr< (a+n);ptr++)
printf(”Value of a[%d]=%d stored at address %u”,j+=,*ptr,ptr);
}
We know the name of an array stands for address of its zeros element the same concept
applies for names of arrays of structures. Suppose item is an array variable of the struct
type. Consider the following declaration:
struct products
{
char name[30];
int manufac;
float net;
item[2],*ptr;
UNIT-3
Strings
A string may contain any character, including special control characters, such as \n, \r, \7
etc...
"Beep! \7 Newline \n..."
#define SIZE 10
char *global_string1;
char global_string2[SIZE];
main ()
{ char *auto_string;
char arraystr[SIZE];
static char *stat_strng;
static char statarraystr[SIZE];
main ()
{ char string[LENGTH];
string[0] = 'T';
string[1] = 'e';
string[2] = 'd';
string[3] = 'i';
string[4] = 'o';
string[5] = 'u';
string[6] = 's';
string[7] = '!';
string[8] = '\0';
This method of handling strings is perfectly acceptable, if there is time to waste, but it is so
laborious that C provides a special initialization service for strings, which bypasses the need
to assign every single character with a new assignment!. There are six ways of assigning
constant strings to arrays. (A constant string is one which is actually typed into the program,
not one which in typed in by the user.) They are written into a short compilable program
below. The explanation follows.
/**********************************************************/
/* */
/* String Initialization */
/* */
/**********************************************************/
main ()
char arraystr[20];
printf ("%s %s", global_string1, global_string2);
printf ("%s %s %s", auto_string, stat_strng, statarraystr);
}
/* end */
The details of what goes on with strings can be difficult to get to grips with. It is a good idea
to get revise pointers and arrays before reading the explanations below. Notice the
diagrams too: they are probably more helpful than words.
The first of these assignments is a global, static variable. More correctly, it is a pointer to a
global, static array. Static variables are assigned storage space in the body of a program
when the compiler creates the executable code. This means that they are saved on disk
along with the program code, so they can be initialized at compile time. That is the reason
for the rule which says that only static arrays can be initialized with a constant expression in
a declaration. The first statement allocates space for a pointer to an array. Notice that,
because the string which is to be assigned to it, is typed into the program, the compiler can
also allocate space for that in the executable file too. In fact the compiler stores the string,
adds a zero byte to the end of it and assigns a pointer to its first character to the variable
called global_string1.
The second statement works almost identically, with the exception that, this time the
compiler sees the declaration of a static array, which is to be initialized. Notice that there is
no size declaration in the square brackets. This is quite legal in fact: the compiler counts the
number of characters in the initialization string and allocates just the right amount of space,
filling the string into that space, along with its end marker as it goes. Remember also that
the name of the array is a pointer to the first character, so, in fact, the two methods are
identical.
The third expression is the same kind of thing, only this time, the declaration is inside the
function main() so the type is not static but auto. The difference between this and the other
two declarations is that this pointer variable is created every time the function main() is
called. It is new each time and the same thing holds for any other function which it might
have been defined in: when the function is called, the pointer is created and when it ends, it
is destroyed. The string which initializes it is stored in the executable file of the program
(because it is typed into the text). The compiler returns a value which is a pointer to the
string's first character and uses that as a value to initialize the pointer with. This is a slightly
round about way of defining the string constant. The normal thing to do would be to
declare the string pointer as being static, but this is just a matter of style. In fact this is what
is done in the fourth example.
The fifth example is again identical, in practice to other static types, but is written as an
`open' array with an unspecified size.
The sixth example is forbidden! The reason for this might seem rather trivial, but it is made
in the interests of efficiency. The array declared is of type auto: this means that the whole
array is created when the function is called and destroyed afterwards. auto-arrays cannot be
initialized with a string because they would have to be re-initialized every time the array
were created: that is, each time the function were called. The final example could be used to
overcome this, if the programmer were inclined to do so. Here an auto array of characters is
declared (with a size this time, because there is nothing for the compiler to count the size
of). There is no single assignment which will fill this array with a string though: the
programmer would have to do it character by character so that the inefficiency is made as
plain as possible!
Arrays of Strings
In the previous chapter we progressed from one dimensional arrays to two dimensional
arrays, or arrays of arrays! The same thing works well for strings which are declared static.
Programs can take advantage of C's easy assignment facilities to let the compiler count the
size of the string arrays and define arrays of messages. For example here is a program which
prints out a menu for an application program:
/*********************************************************/
/* */
/* MENU : program which prints out a menu */
/* */
/*********************************************************/
main ()
{ int str_number;
/*********************************************************/
int n;
{
static char *t[] =
{
" -------------------------------------- \n",
"| ++ MENU ++ |\n",
"| ~~~~~~~~~~~~ |\n",
" | (1) Edit Defaults |\n",
" | (2) Print Charge Sheet |\n",
" | (3) Print Log Sheet |\n",
" | (4) Bill Calculator |\n",
" | (q) Quit |\n",
"| |\n",
"| |\n",
" | Please Enter Choice |\n",
"| |\n",
" -------------------------------------- \n"
};
return (t[n]);
}
Notice the way in which the static declaration works. It is initialized once at compile time, so
there is effectively only one statement in this function and that is the return statement. This
function retains the pointer information from call to call. The Morse coder program could be
rewritten more economically using static strings, See Example.
Example Listing
/************************************************/
/* */
/* static string array */
/* */
/************************************************/
#include <stdio.h>
#define CODE 0
/*************************************************/
main ()
{ short digit;
scanf ("%h",&digit);
/************************************************/
short digit;
{
static char *code[] =
{
"dummy", /* index starts at 0 */
"-----",
".----",
"..---",
"...--",
"....-",
".....",
"-....",
"--...",
"---..",
"----.",
};
printf ("%s\n",code[digit]);
}
Strings from the user
All the strings mentioned so far have been typed into a program by the programmer and
stored in a program file, so it has not been necessary to worry about where they were
stored. Often though we would like to fetch a string from the user and store it somewhere
in the memory for later use. It might even be necessary to get a whole bunch of strings and
store them all. But how will the program know in advance how much array space to allocate
to these strings? The answer is that it won't, but that it doesn't matter at all!
One way of getting a simple, single string from the user is to define an array and to read the
characters one by one. An example of this was the Game of Life program the the previous
chapter:
Define the array to be a certain size
Check that the user does not type in too many characters.
Use the string in that array.
Another way is to define a static string with an initializer as in the following example. The
function filename() asks the user to type in a filename, for loading or saving by and return it
to a calling function.
char *filename()
do
{
printf ("Enter filename :");
scanf ("%24s",filenm);
skipgarb();
}
while (strlen(filenm) == 0);
return (filenm);
}
The string is made static and given an initializing expression and this forces the compiler to
make some space for the string. It makes exactly 24 characters plus a zero byte in the
program file, which can be used by an application. Notice that the conversion string in scanf
prevents the characters from spilling over the bounds of the string. The function strlen() is a
standard library function which is described below; it returns the length of a string.
skipgarb() is the function which was introduced in chapter 15.
Neither of the methods above is any good if a program is going to be fetching a lot of
strings from a user. It just isn't practical to define lots of static strings and expect the user to
type into the right size boxes! The next step in string handling is therefore to allocate
memory for strings personally: in other words to be able to say how much storage is needed
for a string while a program is running. C has special memory allocation functions which can
do this, not only for strings but for any kind of object. Suppose then that a program is going
to get ten strings from the user. Here is one way in which it could be done:
1. Define one large, static string (or array) for getting one string at a time. Call this a
string buffer, or waiting place.
2. Define an array of ten pointers to characters, so that the strings can be recalled
easily.
3. Find out how long the string in the string buffer is.
4. Allocate memory for the string.
5. Copy the string from the buffer to the new storage and place a pointer to it in the
array of pointers for reference.
6. Release the memory when it is finished with.
The function which allocates memory in C is called malloc() and it works like this:
malloc() should be declared as returning the type pointer to character, with the
statement:
char *malloc();
malloc() takes one argument which should be an unsigned integer value telling the
function how many bytes of storage to allocate. It returns a pointer to the first
memory location in that storage:
char *ptr;
unsigned int size;
ptr = malloc(size);
The pointer returned has the value NULL if there was no memory left to allocate. This
should always be checked.
The fact that malloc() always returns a pointer to a character does not stop it from being
used for other types of data too. The cast operator can force malloc() to give a pointer to
any data type. This method is used for building data structures in C with "struct" types.
malloc() has a complementary function which does precisely the opposite: de-allocates
memory. This function is called free(). free() returns an integer code, so it does not have to
be declared as being any special type.
free() takes one argument: a pointer to a block of memory which has previously been
allocated by malloc().
int returncode;
returncode = free (ptr);
The pointer should be declared:
char *ptr;
The return code is zero if the release was successful.
An example of how strings can be created using malloc() and free() is given below. First of
all, some explanation of Standard Library Functions is useful to simplify the program.
Handling strings
The C Standard Library commonly provides a number of very useful functions which handle
strings. Here is a short list of some common ones which are immediately relevant (more are
listed in the following chapter). Chances are, a good compiler will support a lot more than
those listed below, but, again, it really depends upon the compiler.
strlen()
This function returns a type int value, which gives the length or number of characters in a
string, not including the NULL byte end marker. An example is:
int len;
char *string;
len = strlen (string);
strcpy()
This function copies a string from one place to another. Use this function in preference to
custom routines: it is set up to handle any peculiarities in the way data are stored. An
example is
char *to,*from;
to = strcpy (to,from);
Where to is a pointer to the place to which the string is to be copied and from is the place
where the string is to be copied from.
strcmp()
This function compares two strings and returns a value which indicates how they compared.
An example:
int value;
char *s1,*s2;
value = strcmp(s1,s2);
The value returned is 0 if the two strings were identical. If the strings were not the same,
this function indicates the (ASCII) alphabetical order of the two. s1 > s2, alphabetically, then
the value is > 0. If s1 < s2 then the value is < 0. Note that numbers come before letters in the
ASCII code sequence and also that upper case comes before lower case.
strstr()
Tests whether a substring is present in a larger string
int n;
char *s1,*s2;
if (n = strstr(s1,s2))
{
printf("s2 is a substring of s1, starting at %d",n);
}
strncpy()
This function is like strcpy, but limits the copy to no more than n characters.
strncmp()
This function is like strcmp, but limits the comparison to no more than n characters.
More string functions are described in the next section along with a host of Standard Library
Functions.
Example Listing
This program aims to get ten strings from the user. The strings may not contain any spaces
or white space characters. It works as follows:
The user is prompted for a string which he/she types into a buffer. The length of the string is
tested with strlen() and a block of memory is allocated for it using malloc(). (Notice that this
block of memory is one byte longer than the value returned by strlen(), because strlen()
does not count the end of string marker \0.) malloc() returns a pointer to the space
allocated, which is then stored in the array called array. Finally the strings is copied from the
buffer to the new storage with the library function strcpy(). This process is repeated for
each of the 10 strings. Notice that the program exits through a low level function called
QuitSafely(). The reason for doing this is to exit from the program neatly, while at the same
time remembering to perform all a programmer's duties, such as de-allocating the memory
which is no longer needed. QuitSafely() uses the function exit() which should be provided as
a standard library function. exit() allows a program to end at any point.
/******************************************************/
/* */
/* String storage allocation */
/* */
/******************************************************/
#include <stdio.h>
#define NOOFSTR 10
#define BUFSIZE 255
#define CODE 0
/******************************************************/
/* Level 0 */
/******************************************************/
main ()
array[i] = malloc(strlen(buffer)+1);
if (array[i] == NULL)
{
printf ("Can't allocate memory\n");
QuitSafely (array);
}
strcpy (array[i],buffer);
}
QuitSafely(array);
}
/******************************************************/
/* Snakes & Ladders! */
/******************************************************/
char *array[NOOFSTR];
{ int i, len;
exit (CODE);
}
/* end */
String Input/Output
Because strings are recognized to be special objects in C, some special library functions for
reading and writing are provided for them. These make it easier to deal with strings, without
the need for special user-routines. There are four of these functions:
gets()
puts()
sprintf()
sscanf()
gets()
This function fetches a string from the standard input file stdin and places it into some
buffer which the programmer must provide.
#define SIZE 255
strptr = gets(buffer);
If the routine is successful in getting a string, it returns the value buffer to the string pointer
strptr. Otherwise it returns NULL (==0). The advantage of gets() over scanf("%s"..) is that it
will read spaces in strings, whereas scanf() usually will not. gets() quits reading when it finds
a newline character: that is, when the user presses RETURN.
NOTE: there are valid concerns about using this function. Often it is implemented as a macro
with poor bounds checking and can be exploited to produce memory corruption by system
attackers. In order to write more secure code, use fgets() instead.
puts()
puts() sends a string to the output file stdout, until it finds a NULL end of string marker. The
NULL byte is not written to stdout, instead a newline character is written.
char *string;
int returncode;
returncode = puts(string);
puts() returns an integer value, whose value is only guaranteed if there is an error.
returncode == EOF if an end of file was encountered or there was an error.
sprintf()
This is an interesting function which works in almost the same way as printf(), the exception
being that it prints to a string! In other words it treats a string as though it were an output
file. This is useful for creating formatted strings in the memory. On most systems it works in
the following way:
int n;
char *sp;
sscanf()
This function is the complement of sprintf(). It reads its input from a string, as though it
were an input file.
int n;
char *sp;
sp is a pointer to the string which is to be read from. The string must be NULL terminated (it
must have a zero-byte end marker '\0'). sscanf() returns an integer value which holds the
number of items successfully matched or EOF if an end of file marker was read or an error
occurred. The conversion specifiers are identical to those for scanf().
Example Listing
/************************************************/
/* */
/* Formatted strings */
/* */
/************************************************/
#include <stdio.h>
#define SIZE 20
#define CODE 0
/************************************************/
main ()
{ static char *s1 = "string 2.3 55x";
static char *s2 = "....................";
char ch, *string[SIZE];
int i,n;
float x;
if (n > SIZE)
{
printf ("Error: string overflowed!\n");
exit (CODE);
}
puts (s2);
}
Questions
1. What are the two main ways of declaring strings in a program?
2. How would you declare a static array of strings?
3. Write a program which gets a number between 0 and 9 and prints out a different
message for each number. Use a pre-initialized array to store the strings.
UNIT-4
Structures and Unions
Grouping data. Tidying up programs.
Tidy programs are a blessing to programmers. Tidy data are just as important. As programs
become increasingly complex, their data also grow in complexity and single, independent
variables or arrays are no longer enough. What one then needs is a data structure. This is
where a new type of variable comes in: it is called a struct type, or in other languages, a
record. struct types or structures are usually lumped together with another type of variable
called a union. In fact their purposes are quite different.
page.
This shows a program as a kind of society of sealed function capsules which work together
like a beehive of activity upon a honeycomb of program data. This imaginative idea is not a
bad picture of a computer program, but it is not complete either. A program has to
manipulate data: it has to look at them, move them around and copy them from place to
place. All of these things would be very difficult if data were scattered about liberally, with
no particular structure. For this reason C has the facility, within it, to make sealed capsules -
not of program code - but of program data, so that all of these actions very simply by
grouping variables together in convenient packages for handling. These capsules are called
structures.
struct
A structure is a package of one or usually more variables which are grouped under a single
name. Structures are not like arrays: a structure can hold any mixture of different types of
data: it can even hold arrays of different types. A structure can be as simple or as complex as
the programmer desires.
The word struct is a reserved word in C and it represents a new data type, called an
aggregate type. It is not any single type: the purpose of structures is to offer a tool for
making whatever shape or form of variable package that a programmer wishes. Any
particular structure type is given a name, called a structure-name and the variables (called
members) within a structure type are also given names. Finally, every variable which is
declared to be a particular structure type has a name of its own too. This plethora of names
is not really as complicated as it sounds.
Declarations
A structure is declared by making a blank template for a variable package. This is most easily
seen with the help of an example. The following statement is actually a declaration, so it
belongs with other declarations, either at the head of a program or at the start of a block.
struct PersonalData
{
char name[namesize];
char address[addresssize];
int YearOfBirth;
int MonthOfBirth;
int DayOfBirth;
};
This purpose of this statement is to create a model or template to define what a variable of
type struct PersonalData will look like. It says: define a type of variable which collectively holds
a string called name, a string called address and three integers called YearOfBirth, MonthOfBirth
and DayOfBirth. Any variable which is declared to be of type struct PersonalData will be
collectively made up of parts like these. The list of variable components which make up the
structure are called the members of the structure: the names of the members are not the
names of variables, but are a way of naming the parts which make up a structure variable.
(Note: a variable which has been declared to be of type struct something is usually called just
a structure rather than a structure variable. The distinction is maintained here in places
where confusion might arise.) The names of members are held separate from the names of
other identifiers in C, so it is quite possible to have variable names and struct member
names which are the same. Older compilers did not support this luxury.
At this stage, no storage has been given over to a variable, nor has any variable been
declared: only a type has been defined. Having defined this type of structure, however, the
programmer can declare variables to be of this type. For example:
struct PersonalData x;
declares a variable called x to be of type struct PersonalData. x is certainly not a very good
name for any variable which holds a person's personal data, but it contrasts well with all the
other names which are abound and so it serves its purpose for now.
Before moving on to consider how structures can be used, it is worth pausing to show the
different ways in which structures can be declared. The method shown above is probably
the most common one, however there are two equivalent methods of doing the same thing.
A variable can be declared immediately after the template definition.
struct PersonalData
{
char name[namesize];
char address[addresssize];
int YearOfBirth;
int MonthOfBirth;
int DayOfBirth;
}
x; /* variable identifier follows type */
Alternatively, typedef can be used to cut down a bit on typing in the long term. This type
definition is made once at the head of the program and then subsequent declarations are
made by using the new name:
typedef struct
{
char name[namesize];
char address[addresssize];
int YearOfBirth;
int MonthOfBirth;
int DayOfBirth;
}
PersonalData;
then declare:
PersonalData x;
Scope
Both structure types and structure variables obey the rules of scope: that is to say, a
structure type declaration can be local or global, depending upon where the declaration is
made. Similarly if a structure type variable is declared locally it is only valid inside the block
parentheses in which it was defined.
main ()
{ struct ONE
{
int a;
float b;
};
struct ONE x;
function ()
Using Structures
How does a program use the variables which are locked inside structures? The whole point
about structures is that they can be used to group data into sensible packages which can
then be treated as single objects. Early C compilers, some of which still exist today, placed
very severe restrictions upon what a program could do with structures. Essentially, the
members of a structure could be assigned values and pointers to individual structures could
be found. Although this sounds highly restrictive, it did account for the most frequent uses
of structures. Modern compilers allow more flexible use of structures: programs can assign
one structure variable to another structure variable (provided the structures match in type);
structure variables can be passed, whole, as parameters to functions and functions can
return structure values. This makes structures extremely powerful data objects to have in a
program. A structure is assigned to another structure by the following statements.
x = y;
The whole bundle of members is copied in one statement! Structures are passed as
parameters in the usual way:
function (x,y);
function (x,y)
{
}
{
}
Notice that the return type of such a function must also be declared in the function which
calls that it, in the usual way. The reader will begin to see that structure names account for a
good deal of typing! The typedef statement is a very good way of reducing this burden.
The members of a structure are accessed with the . dot character. This is a structure
member operator. Consider the structure variable x, which has the type struct PersonalData.
The members of x could be assigned by the following program:
main ()
{ struct PersonalData x;
where FillArray() is a hypothetical function which copies the string in the first parameter to
the array in the second parameter. The dot between the variable and the names which
follow implies that the statements in this brief program are talking about the members in
the structure variable x, rather than the whole collective bundle. Members of actual
structure variables are always accessed with this dot operator. The general form of a
member reference is:
This applies to any type of structure variable, including those accessed by pointers.
Whenever a program needs to access the members of a structure, this dot operator can be
used. C provides a special member operator for pointers, however, because they are used
so often in connection with structures. This new operator is described below.
Arrays of Structures
Just as arrays of any basic type of variable are allowed, so are arrays of a given type of
structure. Although a structure contains many different types, the compiler never gets to
know this information because it is hidden away inside a sealed structure capsule, so it can
believe that all the elements in the array have the same type, even though that type is itself
made up of lots of different types. An array would be declared in the usual way:
int i;
The members of the arrays would then be accessed by statements like the following
examples:
array[i] = x;
array[i] = array[j];
array[i].YearOfBirth = 1987;
i = array[2].MonthOfBirth;
Example
This listing uses a structure type which is slightly different to PersonalData in that string
pointers are used instead of arrays. This allows more convenient handling of real-life strings.
/*********************************************************/
/* */
/* Structures Demo */
/* */
/*********************************************************/
#include <stdio.h>
#define NAMESIZE 30
#define ADDRSIZE 80
#define NOOFPERSONS 20
#define NEWLINE() putchar('\n');
/*********************************************************/
typedef struct
{
char *Name;
char *Address;
int YearOfBirth;
int MonthOfBirth;
int DayOfBirth;
}
PersonDat;
/*********************************************************/
{ PersonDat record[NOOFPERSONS];
PersonDat PersonalDetails();
int person;
DisplayRecords (record);
}
/*********************************************************/
{ PersonDat dat;
char strbuff[ADDRSIZE], *malloc();
return (dat);
}
/**********************************************************/
DisplayRecords (rec)
PersonDat rec[NOOFPERSONS];
{ int pers;
/**********************************************************/
/* Toolkit */
/**********************************************************/
int a,b;
{ int p, i = a - 1;
/**********************************************************/
{
while (getchar() != '\n')
{
}
}
/* end */
Structures of Structures
Structures are said to nest. This means that structure templates can contain other structures
as members. Consider two structure types:
struct first_structure
{
int value;
float number;
};
and
struct second_structure
{
int tag;
struct first_structure fs;
}
x;
These two structures are of different types, yet the first of the two is included in the second!
An instance of the second structure would be initialized by the following assignments. The
structure variable name is x:
x.tag = 10;
x.fs.value = 20;
x.fs.number = 30.0;
Notice the way in which the member operator . can be used over and over again. Notice also
that no parentheses are necessary, because the reference which is calculated by this
operator is worked out from left to right. This nesting can, in principle, go on many times,
though some compilers might place restrictions upon this nesting level. Statements such as:
variable.tag1.tag2.tag3.tag4 = something;
are probably okay (though they do not reflect good programming). Structures should nest
safely a few times.
A word of caution is in order here. There is a problem with the above scheme that has not
yet been addressed. It is this: what happens if a structure contains an instance of itself? For
example:
struct Regression
{
int i;
struct Regression tag;
}
There is simply no way that this kind of statement can make sense, unless the compiler's
target computer has an infinite supply of memory! References to variables of this type
would go on for ever and an infinite amount of memory would be needed for every variable.
For this one reason, it is forbidden for a structure to contain an instance of itself. What is not
forbidden, however, is for a structure to contain an instance of a pointer to its own type
(because a pointer is not the same type as a structure: it is merely a variable which holds the
address of a structure). Pointers to structures are quite invaluable, in fact, for building data
structures such as linked lists and trees. These extremely valuable devices are described
below.
Pointers to Structures
ptr is then, formally, a pointer to a structure of type Name only. ptr can be assigned to any
other pointer of similar type and it can be used to access the members of a structure. It is in
the second of these actions that a new structure operator is revealed. According to the rules
which have described so far, a structure member could be accessed by pointers with the
following statements:
(*ptr).YearOfBirth = 20;
This says let the member YearOfBirth of the structure pointed to by ptr, have the value 20.
Notice that *ptr, by itself, means the contents of the address which is held in ptr and notice
that the parentheses around this statement avoid any confusion about the precedence of
these operators. There is a better way to write the above statement, however, using a new
operator: ->. This is an arrow made out of a minus sign and a greater than symbol and it is
used simply as follows:
ptr->YearOfBirth = 20;
This statement is identical in every way to the first version, but since this kind of access is
required so frequently, when dealing with structures, C provides this special operator to
make the operation clearer. In the statements above, it is assumed that ptr has been
assigned to the address of some pre-assigned structure: for example, by means of a
statement such as:
ptr = &x;
Example
/*********************************************************/
/* */
/* Structures Demo #2 */
/* */
/*********************************************************/
#include <stdio.h>
#define NAMESIZE 30
#define ADDRSIZE 80
#define NOOFPERSONS 20
#define NEWLINE() putchar('\n');
/*********************************************************/
typedef struct
{
char *Name;
char *Address;
int YearOfBirth;
int MonthOfBirth;
int DayOfBirth;
}
PersonDat;
/*********************************************************/
{ PersonDat record[NOOFPERSONS];
int person;
DisplayRecords (record);
}
/*********************************************************/
PersonDat *dat;
/**********************************************************/
DisplayRecords (rec)
PersonDat rec[NOOFPERSONS];
{ int pers;
/**********************************************************/
/* Toolkit */
/**********************************************************/
/* As before */
In the chapter on arrays it was shown how static and external type arrays could be initialized
with values at compile time. Static and external structures can also be pre-assigned by the
compiler so that programs can set up options and starting conditions in a convenient way. A
static variable of type PersonDat (as in the example programs) could be declared and
initialized in the same statement:
#define NAMESIZE 20
#define ADDRESSSIZE 22
struct PersonDat
{
char *name;
char *address;
int YearOfBirth;
int MonthOfBirth;
int DayOfBirth;
};
main ()
/* rest of program */
The items in the curly braces are matched to the members of the structure variable and any
items which are not initialized by items in the list are filled out with zeros.
Probably the single most frequent use of struct type variables is in the building of dynamical
data structures. Dynamical data are data which are created explicitly by a program using a
scheme of memory allocation and pointers. Normal program data, which are reserved space
by the compiler, are, in fact, static data structures because they do not change during the
course of a program: an integer is always an integer and an array is always an array: their
sizes cannot change while the program is running. A dynamical structure is built using the
memory allocation function:
malloc()
and pointers. The idea is to create the memory space for a new structure as and when it is
needed and to use a pointer to access the members of that structure, using the -> operator.
malloc() was described in connection with strings: it allocates a fixed number of bytes of
memory and returns a pointer to that data. For instance, to allocate ten bytes, one would
write something like this:
ptr = malloc(10);
ptr is then a pointer to the start of that block of 10 bytes. When a program wants to create
the space for a structure, it has a template for that structure, which was used to define it,
but it does not generally know, in advance, how many bytes long a structure is. In fact, it is
seldom possible to know this information, since a structure may occupy more memory than
the sum of its parts. How then does a program know how must space to allocate? The C
compiler comes to the rescue here, by providing a compile time operator called
sizeof ()
which calculates the size of an object while a program is compiling. For example:
sizeof(int)
Works out the number of bytes occupied by the type int.
sizeof(char)
Works out the number of bytes occupied by a single character. This equals 1, in fact.
sizeof(struct PersonalData) works out the number of bytes needed to store a single structure
variable. Obviously this tool is very useful for working with malloc(). The memory allocation
statement becomes something like:
There is a problem with this statement though: malloc() is declared as a function which
returns a type `pointer to character' whereas, here, the programmer is interested in pointers
of type "pointer to struct Something". malloc() has to be forced to produce a pointer of the
correct type then and this is done by using the cast operator to mould it into shape. The cast
operator casts pointers with a general form:
(type *) value
Consider the following example of C source code which allocates space for a structure type
called SomeStruct and creates a correctly aligned pointer to it, called ptr.
This rather laboured statement provides both the memory and the location of that memory
in a legal and type-sensical way. The next section of this book discusses what we can do
with dynamically allocated structures.
Unions
A union is like a structure in which all the `members' are stored at the same address. Clearly
they cannot all be there at the same time. Only one member can be stored in such an object
at any one time, or it would be overwritten by another. Unions behave like specially sized
storage containers which can hold many different types of data. A union can hold any one of
its members but only at different times. The compiler arranges that a union type variable is
big enough to handle the job.
The real purpose of unions is to prevent memory fragmentation by arranging for a standard
size for data in the memory. By having a standard data size we can guarantee that any hole
left when dynamically allocated memory is freed will always be reusable by another instance
of the same type of union. This is a natural strategy in system programming where many
instances of different kinds of variables with a related purpose and stored dynamically.
Declaration
A union is declared in the same way as a structure. It has a list of members, which are used
to mould the type of object concerned.
union IntOrFloat
{
int ordinal;
float continuous;
};
At different times the program is to treat x,y and z as being either integers or float types.
When the variables are referred to as
x.ordinal = 1;
the program sees x as being an integer type. At other times (when x is referred to as
x.continuous) it takes on another aspect: its alter ego, the float type. Notice that x by itself
does not have a value: only its members have values, x is just a box for the different
members to share.
Using unions
Unions are coded with the same constructions as structures. The dot . operator selects the
different members for variable and the arrow -> selects different values for pointers. The
form of such statements is:
union_variable.member;
union_pointer->member;
Unions are seldom very useful objects to have in programs, since a program has no
automatic way of knowing what type of member is currently stored in the union type. One
way to overcome this is to keep a variable which signals the type currently held in the
variable. This is done very easily with the aid of enumerated data. Consider the following
kind of union:
union WhichType
{
int ordinal;
float continuous;
char letter;
};
enum Types
{
INT,
FLOAT,
CHAR
};
union WhichType x;
enum Types x_status;
switch (x_status)
{
case INT : x.ordinal = 12;
break;
case FLOAT : x.continuous = 12.23;
break;
case CHAR : x.letter = '*';
}
struct Union_Handler
{
union WhichType x;
enum Types x_status;
}
var;
var.x.ordinal = 2;
ptr->x.ordinal = 2;
var.x_status = CHAR;
and so on...
Questions
UNIT-V
Preprocessor Commands
is a command which tells the preprocessor to treat the file stdio.h as if it were the actually
part of the program text, in other words to include it as part of the program to be compiled.
Macros are words which can be defined to stand in place of something complicated: they
are a way of reducing the amount of typing in a program and a way of making long ungainly
pieces of code into short words. For example, the simplest use of macros is to give constant
values meaningful names: e.g.
#define TELEPHNUM 720663
This allows us to use the word TELEPHNUM in the program to mean the number 720663. In
this particular case, the word is clearly not any shorter than the number it will replace, but it
is more meaningful and would make a program read more naturally than if the raw number
were used. For instance, a program which deals with several different fixed numbers like a
telephone number, a postcode and a street number could write:
printf("%d %d %d",TELEPHNUM,postcode,streetnum);
instead of
printf("%d %d %d",720663,345,14);
Using the macros instead makes the actions much clearer and allows the programmer to
forget about what the numbers actually are. It also means that a program is easy to alter
because to change a telephone number, or whatever, it is only necessary to change the
definition, not to retype the number in every single instance.
The important feature of macros is that they are not merely numerical constants which are
referenced at compile time, but are strings which are physically replaced before compilation
by the preprocessor! This means that almost anything can be defined:
#define SUM 1 + 2 + 3 + 4
would allow a commonly used string to be called by the identifier "string" instead of typing
it out afresh each time. The idea of a define statement then is:
#define macroname definition on rest of line
Macros cannot define more than a single line to be substituted into a program but they can
be used anywhere, except inside strings. (Anything enclosed in string quotes is assumed to
be complete and untouchable by the compiler.) Some macros are defined already in the file
stdio.h such as:
EOF
The end of file character (= -1 for instance)
NULL
The null character (zero) = 0
Macro Functions
A more advanced use of macros is also permitted by the preprocessor. This involves macros
which accept parameters and hand back values. This works by defining a macro with some
dummy parameter, say x. For example: a macro which is usually defined in one of the
standard libraries is abs() which means the absolute or unsigned value of a number. It is
defined below:
#define ABS(x) ((x) < 0) ? -(x) : (x)
The result of this is to give the positive (or unsigned) part of any number or variable. This
would be no problem for a function which could accept parameters, and it is, in fact, no
problem for macros. Macros can also be made to take parameters. Consider the ABS()
example. If a programmer were to write ABS(4) then the preprocessor would substitute 4
for x. If a program read ABS(i) then the preprocessor would substitute i for x and so on.
(There is no reason why macros can't take more than one parameter too. The programmer
just includes two dummy parameters with different names. See the example listing below.)
Notice that this definition uses a curious operator which belongs to C:
<test> ? <true result> : <false result>
This is like a compact way of writing an if..then..else statement, ideal for macros. But it is
also slightly different: it is an expression which returns a value, where as an if..then..else is a
statement with no value. Firstly the test is made. If the test is true then the first statement is
carried out, otherwise the second is carried out. As a memory aid, it could be read as:
if <test> then <true result> else <false result>
(Do not be confused by the above statement which is meant to show what a programmer
might think. It is not a valid C statement.) C can usually produce much more efficient code
for this construction than for a corresponding if-else statement.
When and when not to use macros with parameters
It is tempting to forget about the distinction between macros and functions, thinking that it
can be ignored. To some extent this is true for absolute beginners, but it is not a good idea
to hold on to. It should always be remembered that macros are substituted whole at every
place where they are used in a program: this is potentially a very large amount of repetition
of code. The advantage of a macro, however, is speed. No time is taken up in passing
control over to a new function, because control never leaves the home function when a
macro is used: it just makes the function a bit longer. There is a limitation with macros
though. Function calls cannot be used as their parameters, such as:
ABS(function())
has no meaning. Only variables or number constants will be substituted. Macros are also
severely restricted in complexity by the limitations of the preprocessor. It is simply not
viable to copy complicated sequences of code all over programs.
Choosing between functions and macros is a matter of personal judgement. No simple rules
can be given. In the end (as with all programming choices) it is experience which counts
towards the final ends. Functions are easier to debug than macros, since they allow us to
single step through the code. Errors in macros are very hard to find, and can be very
confusing.
Example Listing
/************************************************************/
/* */
/* MACRO DEMONSTRATION */
/* */
/************************************************************/
#include <stdio.h>
/************************************************************/
{
printf (STRING1);
printf (STRING2);
printf ("%d\n",EXPRESSION);
printf ("%d\n",EXPR2);
printf ("%d\n",ABS(-5));
printf ("Biggest of 1 2 and 3 is %d",BIGGEST(1,2,3));
}
There are a handful more preprocessor commands which can largely be ignored by the
beginner. They are commonly used in "include" files to make sure that things are not
defined twice.
NOTE : true has any non zero value in C. false is zero.
#undef
This undefines a macro, leaving the name free.
#if
This is followed by some expression on the same line. It allows conditional compilation. It is
an advanced feature which can be used to say: only compile the code between #if and
#endif if the value following #if is true, else leave out that code altogether. This is different
from not executing code--the code will not even be compiled.
#ifdef
This is followed by a macro name. If that macro is defined then this is true.
#ifndef
This is followed by a macro name. If that name is not defined then this is true.
#else
This is part of an #if, #ifdef, #ifndef preprocessor statement.
#endif
This marks the end of a preprocessor statement.
#line
Has the form:
#line constant filename
This is for debugging mainly. This statement causes the compiler to believe that the next
line is line number (constant) and is part of the file (filename).
#error
This is a part of the proposed ANSI standard. It is intended for debugging. It forces the
compiler to abort compilation.
Example
/***********************************************************/
/* To compile or not to compile */
/***********************************************************/
#define SOMEDEFINITION 6546
#define CHOICE 1 /* Choose this before compiling */
/***********************************************************/
#if (CHOICE == 1)
#else
#endif
/***********************************************************/
#ifdef SOMEDEFINITION
#else
#endif
/************************************************************/
main ()
{
printf (OPTIONSTRING);
printf (DITTO);
}
Questions
1. Define a macro called "birthday" which describes the day of the month upon which
your birthday falls.
2. Write an instruction to the preprocessor to include to maths library math.h.
3. A macro is always a number. True or false?
4. A macro is always a constant. True or false?
Bit Patterns
All computer data, of any type, are bit patterns. The only difference between a string and a
floating point variable is the way in which we choose to interpret the patterns of bits in a
computer's memory. For the most part, it is quite unnecessary to think of computer data as
bit patterns; systems programmers, on the other hand, frequently find that they need to
handle bits directly in order to make efficient use of memory when using flags. A flag is a
message which is either one thing or the other: in system terms, the flag is said to be `on' or
`off' or alternatively set or cleared. The usual place to find flags is in a status register of a CPU
(central processor unit) or in a pseudo-register (this is a status register for an imaginary
processor, which is held in memory). A status register is a group of bits (a byte perhaps) in
which each bit signifies something special. In an ordinary byte of data, bits are grouped
together and are interpreted to have a collective meaning; in a status register they are
thought of as being independent. Programmers are interested to know about the contents
of bits in these registers, perhaps to find out what happened in a program after some
special operation is carried out. Other uses for bit patterns are listed below here:
Messages sent between devices in a complex operating environment use bits for
efficiency.
Serially transmitted data.
Handling bit-planes in screen memory. (Raster ports and devices)
Performing fast arithmetic in simple cases.
Programmers who are interested in performing bit operations often work in hexadecimal
because every hexadecimal digit conveniently handles four bits in one go (16 is 2 to the
power 4).
bitpattern variable;
byte message;
The flags or bits in a register/message... have the values 1 or 0, depending upon whether
they are on or off (set or cleared). A program can test for this by using combinations of the
operators which C provides.
<<
Bit shift left (a specified number or bit positions)
>>
Bit shift right(a specified number of bit positions)
|
Bitwise Inclusive OR
^
Bitwise Exclusive OR
&
Bitwise AND
~
Bitwise one's complement
&=
AND assign (variable = variable & value)
|=
Exclusive OR assign (variable = variable | value)
^=
Inclusive OR assign (variable = variable ^ value)
>>=
Shift right assign (variable = variable >> value)
<<=
Shift left assign (variable = variable << value)
The meaning and the syntax of these operators is given below.
The Meaning of Bit Operators
Bitwise operations are not to be confused with logical operations (&&, ||...) A bit pattern is
made up of 0s and 1s and bitwise operators operate individually upon each bit in the
operand. Every 0 or 1 undergoes the operations individually. Bitwise operators (AND, OR)
can be used in place of logical operators (&&,||), but they are less efficient, because logical
operators are designed to reduce the number of comparisons made, in an expression, to the
optimum: as soon as the truth or falsity of an expression is known, a logical comparison
operator quits. A bitwise operator would continue operating to the last before the final
result were known.
Below is a brief summary of the operations which are performed by the above operators on
the bits of their operands.
Shift Operations
Imagine a bit pattern as being represented by the following group of boxes. Every box
represents a bit; the numbers inside represent their values. The values written over the top
are the common integer values which the whole group of bits would have, if they were
interpreted collectively as an integer.
128 64 32 16 8 4 2 1
-------------------------------
|0|0|0|0|0|0|0|1| =1
-------------------------------
Shift operators move whole bit patterns left or right by shunting them between boxes. The
syntax of this operation is:
value << number of positions
would have the value 2, because the bit pattern would have been moved one place the the
left:
128 64 32 16 8 4 2 1
-------------------------------
|0|0|0|0|0|0|1|0| =2
-------------------------------
Similarly:
1 << 4
has the value 16 because the original bit pattern is moved by four places:
128 64 32 16 8 4 2 1
-------------------------------
|0|0|0|1|0|0|0|0| = 16
-------------------------------
And:
6 << 2 == 12
128 64 32 16 8 4 2 1
-------------------------------
|0|0|0|0|0|1|1|0| =6
-------------------------------
-------------------------------
|0|0|0|0|1|1|0|0| = 12
-------------------------------
Notice that every shift left multiplies by 2 and that every shift right would divide by two,
integerwise. If a bit reaches the edge of the group of boxes then it falls out and is lost
forever. So:
1 >> 1 == 0
2 >> 1 == 1
2 >> 2 == 0
n >> n == 0
A common use of shifting is to scan through the bits of a bitpattern one by one in a loop:
this is done by using masks.
Truth Tables and Masking
The operations AND, OR (inclusive OR) and XOR/EOR (exclusive OR) perform comparisons
or "masking" operations between two bits. They are binary or dyadic operators. Another
operation called COMPLEMENT is a unary operator. The operations performed by these
bitwise operators are best summarized by truth tables. Truth tables indicate what the
results of all possible operations are between two single bits. The same operation is then
carried out for all the bits in the variables which are operated upon.
Complement ~
The complement of a number is the logical opposite of the number. C provides a "one's
complement" operator which simply changes all 1s into 0s and all 0s into 1s.
~1 has the value 0 (for each bit)
~0 has the value 1
AND &
This works between two values. e.g. (1 & 0)
value 1 & value 2 == result
0 0 0
0 1 0
1 0 0
1 1 1
0 0 0
0 1 1
1 0 1
1 1 1
0 0 0
0 1 1
1 0 1
1 1 0
The result is 1 if one OR the other (but not both) of the values is 1.
Bit patterns and logic operators are often used to make masks. A mask is as a thing which
fits over a bit pattern and modifies the result in order perhaps to single out particular bits,
usually to cover up part of a bit pattern. This is particularly pertinent for handling flags,
where a programmer wishes to know if one particular flag is set or not set and does not care
about the values of the others. This is done by deliberately inventing a value which only
allows the particular flag of interest to have a non-zero value and then ANDing that value
with the flag register. For example: in symbolic language:
MASK = 00000001
VALUE1 = 10011011
VALUE2 = 10011100
MESSAGE == 00000111
It should be emphasized that these expressions are only written in symbolic language: it is
not possible to use binary values in C. The programmer must convert to hexadecimal, octal
or denary first. (See the appendices for conversion tables).
Example
A simple example helps to show how logical masks and shift operations can be combined.
The first program gets a denary number from the user and converts it into binary. The
second program gets a value from the user in binary and converts it into hexadecimal.
/***************************************************/
/* */
/* Bit Manipulation #1 */
/* */
/***************************************************/
#include <stdio.h>
#define NUMBEROFBITS 8
/****************************************************/
main ()
{ short i,j,bit,;
short MASK = 0x80;
if (i > 128)
{
printf ("Too big\n");
return (0);
}
printf ("Binary value = ");
printf ("\n");
}
/* end */
Output
Enter any number less than 128: 56
Binary value = 00111000
Example
/***************************************************/
/* */
/* Bit Manipulation #2 */
/* */
/***************************************************/
#include <stdio.h>
#define NUMBEROFBITS 8
/****************************************************/
main ()
{ short j,hex = 0;
short MASK;
char binary[NUMBEROFBITS];
/* end */
Example
Enter any number less than 128: 56
Binary value = 00111000
Questions
1. What distinguishes a bit pattern from an ordinary variable? Can any variable be a bit
pattern?
2. What is the difference between an inclusive OR operation and an exclusive OR
operation?
3. If you saw the following function call in a program, could you guess what its
parameter was?
4. OpenWindow (BORDER | GADGETS | MOUSECONTROL | SIZING);
5. Find out what the denary (decimal) values of the following operations are:
1. 7 & 2
2. 1 & 1
3. 15 & 3
4. 15 & 7
5. 15 & 7 & 3
Try to explain the results. (Hint: draw out the numbers as binary patterns, using the
program listed.)
6. Find out what the denary (decimal) values of the following operations are:
1. 1 | 2
2. 1 | 2 | 3
7. Find out the values of:
1. 1 & (~1)
2. 23 & (~23)
3. 2012 & (~2012)
(Hint: write a short program to work them out. Use short type variables for all the
numbers).
UNIT-VI
FILE MANAGEMENT
What is a File?
Abstractly, a file is a collection of bytes stored on a secondary storage device, which is
generally a disk of some kind. The collection of bytes may be interpreted, for example, as
characters, words, lines, paragraphs and pages from a textual document; fields and records
belonging to a database; or pixels from a graphical image. The meaning attached to a
particular file is determined entirely by the data structures and operations used by a
program to process the file. It is conceivable (and it sometimes happens) that a graphics file
will be read and displayed by a program designed to process textual data. The result is that
no meaningful output occurs (probably) and this is to be expected. A file is simply a machine
decipherable storage media where programs and data are stored for machine usage.
Essentially there are two kinds of files that programmers deal with text files and binary files.
These two classes of files will be discussed in the following sections.
Binary files
A binary file is no different to a text file. It is a collection of bytes. In C Programming
Language a byte and a character are equivalent. Hence a binary file is also referred to as a
character stream, but there are two essential differences.
1. No special processing of the data occurs and each byte of data is transferred to or
from the disk unprocessed.
2. C Programming Language places no constructs on the file, and it may be read from,
or written to, in any manner chosen by the programmer.
Binary files can be either processed sequentially or, depending on the needs of the
application, they can be processed using random access techniques. In C Programming
Language, processing a file using random access techniques involves moving the current file
position to an appropriate place in the file before reading or writing data. This indicates a
second characteristic of binary files
– they a generally processed using read and write operations simultaneously.
For example, a database file will be created and processed as a binary file. A record update
operation will involve locating the appropriate record, reading the record into memory,
modifying it in some way, and finally writing the record back to disk at its appropriate
location in the file. These kinds of operations are common to many binary files, but are
rarely found in applications that process text files.
Creating a file and output some data
In order to create files we have to learn about File I/O i.e. how to write data into a file and
how to read data from a file. We will start this section with an example of writing data to a
file. We begin as before with the include statement for stdio.h, then define some variables
for use in the example including a rather strange looking new type.
/* Program to create a file and write some data the file */
#include <stdio.h>
#include <stdio.h>
main( )
{
FILE *fp;
char stuff[25];
int index;
fp = fopen("TENLINES.TXT","w"); /* open for writing */
strcpy(stuff,"This is an example line.");
for (index = 1; index <= 10; index++)
fprintf(fp,"%s Line number %d\n", stuff, index);
fclose(fp); /* close the file before ending program */
}
The type FILE is used for a file variable and is defined in the stdio.h file. It is used to define a
file pointer for use in file operations. Before we can write to a file, we must open it. What
this really means is that we must tell the system that we want to write to a file and what the
file name is. We do this with the fopen() function illustrated in the first line of the program.
The file pointer, fp in our case, points to the file and two arguments are required in the
parentheses, the file name first, followed by the file type.
The file name is any valid DOS file name, and can be expressed in upper or lower case
letters, or even mixed if you so desire. It is enclosed in double quotes. For this example we
have chosen the name TENLINES.TXT. This file should not exist on your disk at this time. If
you have a file with this name, you should change its name or move it because when we
execute this program, its contents will be erased. If you don’t have a file by this name,
that is good because we will create one and put some data into it. You are permitted to
include a directory with the file name. The directory must, of course, be a valid directory
otherwise an error will occur. Also, because of the way C handles literal strings, the directory
separation character ‘\’ must be written twice. For example, if the file is to be stored in
the \PROJECTS sub directory then the file name should be entered as “\\PROJECTS\\
TENLINES.TXT”. The second parameter is the file attribute and can be any of three letters, r,
w, or a, and must be lower case.
Reading (r)
When an r is used, the file is opened for reading, a w is used to indicate a file to be used for
writing, and an indicates that you desire to append additional data to the data already in an
existing file. Most C compilers have other file attributes available; check your Reference
Manual for details. Using the r indicates that the file is assumed to be a text file. Opening a
file for reading requires that the file already exist. If it does not exist, the file pointer will be
set to NULL and can be checked by the program.
Here is a small program that reads a file and display its contents on screen. /* Program to
display the contents of a file on screen */
#include <stdio.h>
void main()
{
FILE *fopen(), *fp;
int c;
fp = fopen("prog.c","r");
c = getc(fp) ;
while (c!= EOF)
{
putchar(c);
c = getc(fp);
}
fclose(fp);
}
Writing (w)
When a file is opened for writing, it will be created if it does not already exist and it will be
reset if it does, resulting in the deletion of any data already there. Using the w indicates that
the file is assumed to be a text file.
Here is the program to create a file and write some data into the file.
#include <stdio.h>
int main()
{
FILE *fp;
file = fopen("file.txt","w");
/*Create a file and add text*/
fprintf(fp,"%s","This is just an example :)"); /*writes data to the file*/
fclose(fp); /*done!*/
return 0;
}
Appending (a):
When a file is opened for appending, it will be created if it does not already exist and it will
be initially empty. If it does exist, the data input point will be positioned at the end of the
present data so that any new data will be added to any data that already exists in the file.
Using the a indicates that the file is assumed to be a text file.
Here is a program that will add text to a file which already exists and there is some text in
the file.
#include <stdio.h>
int main()
{
FILE *fp
file = fopen("file.txt","a");
fprintf(fp,"%s","This is just an example :)"); /*append some text*/
fclose(fp);
return 0;
}
Outputting to the file
The job of actually outputting to the file is nearly identical to the outputting we have already
done to the standard output device. The only real differences are the new function names
and the addition of the file pointer as one of the function arguments. In the example
program, fprintf replaces our familiar printf function name, and the file pointer defined
earlier is the first argument within the parentheses. The remainder of the statement looks
like, and in fact is identical to, the printf statement.
Closing a file
To close a file you simply use the function fclose with the file pointer in the parentheses.
Actually, in this simple program, it is not necessary to close the file because the system will
close all open files before returning to DOS, but it is good programming practice for you to
close all files in spite of the fact that they will be closed automatically, because that would
act as a reminder to you of what files are open at the end of each program.
You can open a file for writing, close it, and reopen it for reading, then close it, and open it
again for appending, etc. Each time you open it, you could use the same file pointer, or you
could use a different one. The file pointer is simply a tool that you use to point to a file and
you decide what file it will point to. Compile and run this program. When you run it, you will
not get any output to the monitor because it doesn’t generate any. After running it, look
at your directory for a file named TENLINES.TXT and type it; that is where your output will
be. Compare the output with that specified in the program; they should agree! Do not erase
the file named TENLINES.TXT yet; we will use it in
some of the other examples in this section.
Reading from a text file
Now for our first program that reads from a file. This program begins with the familiar
include, some data definitions, and the file opening statement which should require no
explanation except for the fact that an r is used here because we want to read it.
#include <stdio.h>
main( )
{
FILE *fp;
char c;
funny = fopen("TENLINES.TXT", "r");
if (fp == NULL)
printf("File doesn't exist\n");
else {
do {
c = getc(fp); /* get one character from the file
*/
putchar(c); /* display it on the monitor
*/
} while (c != EOF); /* repeat until EOF (end of file)
*/
}
fclose(fp);
}
In this program we check to see that the file exists, and if it does, we execute the main body
of the program. If it doesn’t, we print a message and quit. If the file does not exist, the
system will set the pointer equal to NULL which we can test. The main body of the program
is one do while loop in which a single character is read from the file and output to the
monitor until an EOF (end of file) is detected from the input file. The file is then closed and
the program is terminated. At this point, we have the potential for one of the most common
and most perplexing problems of programming in C. The variable returned from the getc
function is a character, so we can use a char variable for this purpose. There is a problem
that could develop here if we happened to use an unsigned char however, because C usually
returns a minus one for an EOF - which an unsigned char type variable is not
capable of containing. An unsigned char type variable can only have the values of zero to
255, so it will return a 255 for a minus one in C. This is a very frustrating problem to try to
find. The program can never find the EOF and will therefore never terminate the loop. This is
easy to prevent: always have a char or int type variable for use in returning an EOF. There is
another problem with this program but we will worry about it when we get to the next
program and solve it with the one following that.
After you compile and run this program and are satisfied with the results, it would be a good
exercise to change the name of TENLINES.TXT and run the program again to see that the
NULL test actually works as stated. Be sure to change the name back because we are still
not finished with TENLINES.TXT.
Files Generally
C provides two levels of file handling; these can be called high level and low level. High level
files are all treated as text files. In fact, the data which go into the files are exactly what
would be seen on the screen, character by character, except that they are stored in a file
instead. This is true whether a file is meant to store characters, integers, floating point
types. Any file, which is written to by high level file handling functions, ends up as a text file
which could be edited by a text editor.
High level text files are also read back as character files, in the same way that input is
acquired from the keyboard. This all means that high level file functions are identical in
concept to keyboard/screen input/output.
The alternative to these high level functions, is obviously low level functions. These are
more efficient, in principle, at filing data as they can store data in large lumps, in raw
memory format, without converting to text files first. Low level input/output functions have
the disadvantage that they are less `programmer friendly' than the high level ones, but they
are likely to work faster.
File Positions
When data are read from a file, the operating system keeps track of the current position of a
program within that file so that it only needs to make a standard library call to `read the next
part of the file' and the operating system obliges by reading some more and advancing its
position within the file, until it reaches the end. Each single character which is read causes
the position in a file to be advanced by one.
Although the operating system does a great deal of hand holding regarding file positions, a
program can control the way in which that position changes with functions such as ungetc()
if need be. In most cases it is not necessary and it should be avoided, since complex
movements within a file can cause complex movements of a disk drive mechanism which in
turn can lead to wear on disks and the occurrence of errors.
These are all generalized file handling versions of the standard input/output library. They
work with generalized files, as opposed to the specific files stdin and stdout which printf()
and scanf() use. The file versions differ only in that they need an extra piece of information:
the file pointer to a particular portal. This is passed as an extra parameter to the functions.
they process data in an identical way to their standard I/O counterparts. Other filing
functions will not look so familiar. For example:
fopen()
fclose()
getc()
ungetc();
putc()
fgetc()
fputc()
feof()
Before any work can be done with high level files, these functions need to be explained in
some detail.
Opening files
A file is opened by a call to the library function fopen(): this is available automatically when
the library file <stdio.h> is included. There are two stages to opening a file: firstly a file portal
must be found so that a program can access information from a file at all. Secondly the file
must be physically located on a disk or as a device or whatever. The fopen() function
performs both of these services and, if, in fact, the file it attempts to open does not exist,
that file is created anew. The syntax of the fopen() function is:
FILE *returnpointer;
returnpointer = fopen("filename","mode");
or
FILE returnpointer;
char *fname, *mode;
returnpointer = fopen(fname,mode);
The filename is a string which provides the name of the file to be opened. Filenames are
system dependent so the details of this must be sought from the local operating system
manual. The operation mode is also a string, chosen from one of the following:
r
Open file for reading
w
Open file for writing
a
Open file for appending
rw
Open file for reading and writing (some systems)
This mode string specifies the way in which the file will be used. Finally, returnpointer is a
pointer to a FILE structure which is the whole object of calling this function. If the file (which
was named) opened successfully when fopen() was called, returnpointer is a pointer to the
file portal. If the file could not be opened, this pointer is set to the value NULL. This should
be tested for, because it would not make sense to attempt to write to a file which could not
be opened or created, for whatever reason.
A read only file is opened, for example, with some program code such as:
FILE *fp;
{
printf ("File could not be opened\n");
error_handler();
}
A question which springs to mind is: what happens if the user has to type in the name of a
file while the program is running? The solution to this problem is quite simple. Recall the
function filename() which was written in chapter 20.
char *filename() /* return filename */
do
{
printf ("Enter filename :");
scanf ("%24s",filenm);
skipgarb();
}
while (strlen(filenm) == 0);
return (filenm);
}
This function makes file opening simple. The programmer would now write something like:
FILE *fp;
char *filename();
{
printf ("File could not be opened\n");
error_handler();
}
and then the user of the program would automatically be prompted for a filename. Once a
file has been opened, it can be read from or written to using the other library functions
(such as fprintf() and fscanf()) and then finally the file has to be closed again.
Closing a file
A file is closed by calling the function fclose(). fclose() has the syntax:
int returncode;
FILE *fp;
fp is a pointer to the file which is to be closed and returncode is an integer value which is 0 if
the file was closed successfully. fclose() prompts the file manager to finish off its dealings
with the named file and to close the portal which the operating system reserved for it.
When closing a file, a program needs to do something like the following:
if (fclose(fp) != 0)
{
printf ("File did not exist.\n");
error_handler();
}
fprintf()
This is the highest level function which writes to files. Its name is meant to signify "file-print-
formatted" and it is almost identical to its stdout counterpart printf(). The form of the
fprintf() statement is as follows:
fprintf (fp,"string",variables);
where fp is a file pointer, string is a control string which is to be formatted and the variables
are those which are to be substituted into the blank fields of the format string. For example,
assume that there is an open file, pointed to by fp:
int i = 12;
float x = 2.356;
char ch = 's';
The conversion specifiers are identical to those for printf(). In fact fprintf() is related to
printf() in a very simple way: the following two statements are identical.
printf ("Hello world %d", 1);
fscanf()
The analogue of scanf() is fscanf() and, as with fprintf(), this function differs from its
standard I/O counterpart only in one extra parameter: a file pointer. The form of an fscanf()
statement is:
FILE *fp;
int n;
n = fscanf (fp,"string",pointers);
where n is the number of items matched in the control string and fp is a pointer to the file
which is to be read from. For example, assuming that fp is a pointer to an open file:
int i = 10;
float x = -2.356;
char ch = 'x';
The remarks which were made about scanf() also apply to this function: fscanf() is a
`dangerous' function in that it can easily get out of step with the input data unless the input
is properly formatted.
skipfilegarb() ?
Do programs need a function such as skipgarb() to deal with instances of badly formatted
input data? A programmer can assume a bit more about files which are read into a program
from disk file than it can assume about the user's typed input. A disk file will presumably
have been produced by the same program which generated it, or will be in a format which
the program expects. Is a function like skipgarb() necessary then? The answer is: probably
not. This does not mean to say that a program does not need to check for "bad files", or
files which do not contain the data they are alleged to contain. On the other hand, a
programmer is at liberty to assume that any file which does not contain correctly formatted
data is just nonsense: he/she does not have to try to make sense of it with a function like
skipgarb(), the program could simply return an error message like "BAD FILE" or whatever
and recover in a sensible way. It would probably not make sense to use a function like
skipgarb() for files. For comparison alone, skipfilegarb() is written below.
skipfilegarb(fp)
FILE *fp;
{
while (getc(fp) != '\n')
{
}
}
/* open file */
ch = getc (fp);
ch = fgetc (fp);
These functions return a character from the specified file if they operated successfully,
otherwise they return EOF to indicate the end of a file or some other error. Apart from this,
these functions/macros are quite unremarkable.
ungetc()
ungetc() is a function which `un-gets' a character from a file. That is, it reverses the effect of
the last get operation. This is not like writing to a file, but it is like stepping back one position
within the file. The purpose of this function is to leave the input in the correct place for
other functions in a program when other functions go too far in a file. An example of this
would be a program which looks for a word in a text file and processes that word in some
way.
while (getc(fp) != ' ')
{
}
The program would skip over spaces until it found a character and then it would know that
this was the start of a word. However, having used getc() to read the first character of that
word, the position in the file would be the second character in the word! This means that, if
another function wanted to read that word from the beginning, the position in the file
would not be correct, because the first character would already have been read. The
solution is to use ungetc() to move the file position back a character:
int returncode;
returncode = ungetc(fp);
The returncode is the ascii code of the character sent, if the operation was successful,
otherwise it is EOF.
fgets() and fputs()
Just as gets() and puts() fetched and sent strings to standard input/output files stdin and
stdout, so fgets() and fputs() send strings to generalized files. The form of an fgets()
statement is as follows:
char *strbuff,*returnval;
int n;
FILE *fp;
strbuff is a pointer to an input buffer for the string; fp is a pointer to an open file. returnval is
a pointer to a string: if there was an error in fgets() this pointer is set to the value NULL,
otherwise it is set to the value of "strbuff". No more than (n-1) characters are read by fgets()
so the programmer has to be sure to set n equal to the size of the string buffer. (One byte is
reserved for the NULL terminator.) The form of an fputs() statement is as follows:
char *str;
int returnval;
FILE *fp;
returnval = fputs (str,fp);
Where str is the NULL terminated string which is to be sent to the file pointed to by fp.
returnval is set to EOF if there was an error in writing to the file.
feof()
This function returns a true or false result. It tests whether or not the end of a file has been
reached and if it has it returns `true' (which has any value except zero); otherwise the
function returns `false' (which has the value zero). The form of a statement using this
function is:
FILE *fp;
int outcome;
outcome = feof(fp);
Most often feof() will be used inside loops or conditional statements. For example: consider
a loop which reads characters from an open file, pointed to by fp. A call to feof() is required
in order to check for the end of the file.
while (!feof(fp))
{
ch = getc(fp);
}
Translated into pidgin English, this code reads: `while NOT end of file, ch equals get
character from file'. In better(?) English the loop continues to fetch characters as long as the
end of the file has not been reached. Notice the logical NOT operator ! which stands before
feof().
Filing Errors
The standard library provides an error function/macro which returns a true/false result
according to whether or not the last filing function call returned an error condition. This is
called ferror(). To check for an error in an open file, pointed to by fp:
FILE *fp;
if (ferror(fp))
{
error_handler();
}
This function/macro does not shed any light upon the cause of errors, only whether errors
have occurred at all. A detailed diagnosis of what went wrong is only generally possible by
means of a deeper level call to the disk operating system (DOS).
ftell()
fseek()
rewind()
fflush()
These functions provide facilities to read and write whole blocks of characters in one
operation as well as further facilities to locate and alter the current focus of attention within
a file. They offer, essentially, low level filing operations for files which have been opened for
high level use!
The parameters in parentheses provide information about where the data will be stored
once they have been read from a file. fp is a pointer to an open file; ptr is a pointer to the
start of a block of memory which is to store the data when it is read; size is the size of a
block of data in characters; n is the number of blocks of data to be read. Finally noread is a
return value which indicates the number of blocks which was actually read during the
operation. It is important to check that the number of blocks expected is the same as the
number received because something could have gone wrong with the reading process. (The
disk might be corrupted or the file might have been altered in some way.) fwrite() has an
identical call structure to fread():
FILE *fp;
int nowritten,n,size;
char *ptr;
This time the parameters in parentheses provide information about where the data, to be
written to a file, will be found. fp is a pointer to an open file; ptr is a pointer to the start of a
block of memory at which the data are stored; size is the size of a "block" of data in
characters; n is the number of blocks of data to be read; nowritten is a return value which
indicates the actual number of blocks which was written. Again, this should be checked.
A caution about these functions: each of these block transfer routines makes an important
assumption about the way in which data are stored in the computer system. It is assumed
that the data are stored contiguously in the memory, that is, side by side, in sequential
memory locations. In some systems this can be difficult to arrange (in multi-tasking systems
in particular) and almost impossible to guarantee. Memory which is allocated in C programs
by the function malloc() does not guarantee to find contiguous portions of memory on
successive calls. This should be noted carefully when developing programs which use these
calls.
pos = ftell(fp);
fp is an open file, which is in some state of being read or written to. pos is a long integer
value which describes the position in terms of the number of characters from the beginning
of the file. Aligning a file portal with a particular place in a file is more sophisticated than
simply taking note of the current position. The call to fseek() looks like this:
long int pos;
int mode,returncode;
FILE *fp;
The parameters have the following meanings. fp is a pointer to a file opened by fopen(). pos
is some way of describing the position required within a file. mode is an integer which
specifies the way in which pos is to be interpreted. Finally, returncode is an integer whose
value is 0 if the operation was successful and -1 if there was an error.
0
pos is an offset measured relative to the beginning of the file.
1
pos is an offset measured relative to the current position.
2
pos is an offset measured relative to the end of the file.
Some examples help to show how this works in practice:
long int pos = 50;
int mode = 0,returncode;
FILE *fp;
rewind(fp);
fseek(fp,0L,0); /* = rewind() */
fflush()
This is a macro/function which can be used on files which have been opened for writing or
appending. It flushes the output buffer which means that it forces the characters in the
output buffer to be written to the file. If used on files which are open for reading, it causes
the input buffer to be emptied (assuming that this is allowed at all). Example:
FILE *fp;
fflush(fp);
These low level routines work on the operating system's end of the file portals. They should
be regarded as being advanced features of the language because they are dangerous
routines for bug ridden programs. The data which they deal with is untranslated: that is, no
conversion from characters to floating point or integers or any type at all take place. Data
are treated as a raw stream of bytes. Low level functions should not be used on any file at
the same time as high level routines, since high level file handling functions often make calls
to the low level functions.
Working at the low level, programs can create, delete and rename files but they are
restricted to the reading and writing of untranslated data: there are no functions such as
fprintf() or fscanf() which make type conversions. As well as the functions listed above a
local operating system will doubtless provide special function calls which enable a
programmer to make the most of the facilities offered by the particular operating
environment. These will be documented, either in a compiler manual, or in an operating
system manual, depending upon the system concerned. (They might concern special
graphics facilities or windowing systems or provide ways of writing special system
dependent data to disk files, such as date/time stamps etc.)
File descriptors
At the low level, files are not handled using file pointers, but with integers known as file
handles or file descriptors. A file handle is essentially the number of a particular file portal in
an array. In other words, for all the different terminology, they describe the same thing. For
example:
int fd;
open()
open() is the low level file open function. The form of this function call is:
int fd, mode;
char *filename;
fd = open (filename,mode);
where filename is a string which holds the name of the file concerned, mode is a value which
specifies what the file is to be opened for and fd is either a number used to distinguish the
file from others, or -1 if an error occurred.
A program can give more information to this function than it can to fopen() in order to
define exactly what open() will do. The integer mode is a message or a pseudo register
which passes the necessary information to open(), by using the following flags:
O_RDONLY Read access only
O_WRONLY Write access only
O_RDWR Read/Write access
main()
{ char *filename();
int fd;
fd = open(filename(), O_RDONLY);
if (fd == FAILED)
{
printf ("File not found\n");
error_handler (failed);
}
}
This opens up a read-only file for low level handling, with error checking. Some systems
allow a more flexible way of opening files. The four appended modes are values which can
be bitwise ORed with one of the first three in order to get more mileage out of open(). The
bitwise OR operator is the vertical bar "|". For example, to emulate the fopen() function a
program could opt to create a file if it did not already exist:
fd = open (filename(), O_RDONLY | O_CREAT);
open() sets the file position to zero if the file is opened successfully.
close()
close() releases a file portal for use by other files and brings a file completely up to date with
regard to any changes that have been made to it. Like all other filing functions, it returns the
value 0 if it performs successfully and the value -1 if it fails. e.g.
#define FAILED -1
if (close(fd) == FAILED)
{
printf ("ERROR!");
}
creat()
This function creates a new file and prepares it for access using the low level file handling
functions. If a file which already exists is created, its contents are discarded. The form of this
function call is:
int fd, pmode;
char *filename;
fd = creat(filename,pmode);
filename must be a valid filename; pmode is a flag which contains access-privilege mode bits
(system specific information about allowed access) and fd is a returned file handle. In the
absence of any information about pmode, this parameter can be set to zero. Note that, the
action of creating a file opens it too. Thus after a call to creat, you should close the file
descriptor.
read()
This function gets a block of information from a file. The data are loaded directly into
memory, as a sequence of bytes. The user must provide a place for them (either by making
an array or by using malloc() to reserve space). read() keeps track of file positions
automatically, so it actually reads the next block of bytes from the current file position. The
following example reads n bytes from a file:
int returnvalue, fd, n;
char *buffer;
The return value should be checked. Its values are defined as follows:
0
End of file
-1
Error occurred
n
the number of bytes actually read. (If all went well this should be equal to n.)
write()
This function is the opposite of read(). It writes a block of n bytes from a contiguous portion
of memory to a file which was opened by open(). The form of this function is:
int returnvalue, fd, n;
char *buffer;
lseek()
Low level file handing functions have their equivalent of fseek() for finding a specific
position within a file. This is almost identical to fseek() except that it uses the file handle
rather than a file pointer as a parameter and has a different return value. The constants
should be declared long int, or simply long.
#define FAILED -1L
pos gives the new file position if successful, and -1 (long) if an attempt was made to read
past the end of the file. The values which mode can take are:
0
Offset measured relative to the beginning of the file.
1
Offset measured relative to the current position.
2
Offset measured relative to the end of the file.
int returnvalue;
char *filename;
filename is a string containing the name of the file concerned. This function can fail if a file
concerned is protected or if it is not found or if it is a device. (It is impossible to delete the
printer!)
rename()
This function renames a file. The programmer specifies two filenames: the old filename and
a new file name. As usual, it returns the value -1 if the action fails. An example illustrates the
form of the rename() call:
#define FAILED -1
char *old,*new;
if (rename(old,new) == FAILED)
{
printf ("Can't rename %s as %s\n",old,new);
}
rename() can fail because a file is protected or because it is in use, or because one of the
filenames given was not valid.
Example
This example strings together some low level filing actions so as to illustrate their use in a
real program. The idea is to present a kind of file or "project" menu for creating, deleting,
renaming files. A rather feeble text editor allows the user to enter 255 characters of text
which can be saved.
/***************************************************************/
/* */
/* LOW LEVEL FILE HANDLING */
/* */
/***************************************************************/
#include <stdio.h>
#include <ctype.h>
#include <fcntl.h> /* defines O_RDONLY etc.. */
#define CODE 0
#define SIZE 255
#define FNMSIZE 30 /* Max size of filenames */
#define TRUE 1
#define FALSE 0
#define FAILED -1
int fd;
/***************************************************************/
/* Level 0 */
/***************************************************************/
main ()
{ char *data,getkey(),*malloc();
while (TRUE)
{
menu();
switch (getkey())
{
case 'l' : LoadFile(data);
break;
case 's' : SaveFile(data);
break;
case 'e' : Edit(data);
break;
case 'd' : DeleteFile();
break;
case 'r' : RenameFile();
break;
case 'q' : if (sure())
{
return (CODE);
}
break;
}
}
}
/*************************************************************/
/* Level 1 */
/*************************************************************/
menu ()
{
CLRSCRN();
printf (" ---------------------------------\n");
printf ("| MENU |\n");
printf ("| ~~~~~~ |\n");
printf ("| |\n");
printf ("| L) Load File |\n");
printf ("| S) Save File |\n");
printf ("| E) Edit File |\n");
printf ("| D) Delete File |\n");
printf ("| R) Rename File |\n");
printf ("| Q) Quit |\n");
printf ("| |\n");
printf ("| Select Option and RETURN |\n");
printf ("| |\n");
printf (" --------------------------------- \n");
NEWLINE();
}
/*************************************************************/
char *data;
{ char *filename(),getkey();
int error;
fd = open(filename(), O_RDONLY);
if (fd == FAILED)
{
printf ("File not found\n");
return (FAILED);
}
if (error == FAILED)
{
printf ("Error loading file\n");
wait();
}
else
{
if (error != SIZE)
{
printf ("File was corrupted\n");
wait();
}
}
close (fd,data,SIZE);
return (error);
}
/*************************************************************/
char *data;
{ char *filename(),getkey(),*fname;
int error,fd;
fd = open ((fname = filename()), O_WRONLY);
if (fd == FAILED)
{
printf ("File cannot be written to\n");
printf ("Try to create new file? Y/N\n");
if (yes())
{
if ((fd = CreateFile(fname)) == FAILED)
{
printf ("Cannot create file %s\n",fname);
return (FAILED);
}
}
else
{
return (FAILED);
}
}
close (fd,data,SIZE);
wait();
return (error);
}
/*************************************************************/
char *data;
{ char *ptr;
int ctr = 0;
skipgarb();
}
/*************************************************************/
{ char *filename(),getkey(),*fname;
fname = filename();
if (sure())
{
if (remove(fname) == FAILED)
{
printf ("Can't delete %s\n",fname);
}
}
else
{
printf ("File NOT deleted!\n");
}
wait();
}
/*************************************************************/
RenameFile()
{ char old[FNMSIZE],*new;
if (rename(old,new) == FAILED)
{
printf ("Can't rename %s as %s\n",old,new);
}
wait();
}
/*************************************************************/
/* Level 2 */
/*************************************************************/
CreateFile (fname)
char *fname;
{ int fd;
return (fd);
}
/*************************************************************/
/* Toolkit */
/*************************************************************/
do
{
printf ("Enter filename :");
scanf ("%24s",statfilenm);
skipgarb();
}
while (strlen(statfilenm) == 0);
return (statfilenm);
}
/**************************************************************/
{
printf ("Are you absolutely, unquestionably certain? Y/N\n");
return(yes());
}
/**************************************************************/
yes()
{ char getkey();
while (TRUE)
{
switch(getkey())
{
case 'y' : return (TRUE);
case 'n' : return (FALSE);
}
}
}
/**************************************************************/
wait()
{ char getkey();
/**************************************************************/
ch = getchar();
skipgarb();
return((char)tolower(ch));
}
/**************************************************************/
{
while (getchar() != '\n')
{
}
}
/* end */
Questions
1. What are the following?
1. File name
2. File pointer
3. File handle
2. What is the difference between high and low level filing?
3. Write a statement which opens a high level file for reading.
4. Write a statement which opens a low level file for writing.
5. Write a program which checks for illegal characters in text files. Valid characters are
ASCII codes 10,13,and 32..126. Anything else is illegal for programs.
6. What statement performs formatted writing to text files?
Internal Exams
BCA II
C Programming-S106T
Max. Marks: 30 Time: 90 min.
Section-A
4. What is union
Section-B
Or
Discuss about closing of a file.
Section-C
11. What do you understand by I/O operation on files? Discuss with examples.