Вы находитесь на странице: 1из 175

Module 1 : Linear Data

Structures and Applications

Supratim Biswas
Computer Science & Engineering Department
Indian Institute of Technology, Bombay
Linear Data Structures and Applications
Prerequisite : Familiarity with Programming in C / C++;
basics of algorithms; Material covered in Lecture 1 of this
module

Topics for Lecture 2 :


• Dynamic memory based implementation of of a linked
list : concepts of pointer, class and dynamic memory
allocation.
• Designing programs in C++ for representation and
manipulation of a linked list
• Designing the stack and queue data structures using
a singly linked list data structure.
• Comparison of array vs linked list implementation –
time and space complexity of various operations
Linked List Representation
We have seen an informal introduction to a linked
list and a feel for some of the operations performed
on it through pictures.

Implementation of a linked list in C++ is the first


topic of this lecture. The features of C++ that are
required are reviewed.

• Pointer data type and operations on pointers


• Reference data type and their use
• Class – heterogenuous collection of types
• Template functions and class templates
Data Type for addresses of objects
• C++ allows us to know the physical address where a
variable or object resides in memory (RAM) at
runtime.

• The unary prefix operator & does this job. For


example, we may write statements such as

int i = 10;

cout << “ i = “ << i << “ Address of i : “ << &i


<< “ size : “ << sizeof i << endl;
Pointer Data Type
The expression &i refers to the address of variable i
i n m e m o r y. T h e v a r i o u s a t t r i b u t e s f o r a
variable/object in C++ are its

1. Name
2. Data Type
3. Size in bytes
4. r-value, the read value of the variable/object
5. l - v a l u e , t h e a d d r e s s i n m e m o r y o f
variable/object
6. Scope (visibility)
Pointer Data Type
A data type is defined in terms of a set of values and a
set of operators.

• The value set associated with the pointer data


type is the set of memory addresses.

• The pointer data type enables o n e to trea t


addresses as first class citizens, like the other
data types, and permit users to create, save and
manipulate addresses of objects in their programs.

• The operations, supported on addresses along


with their attributes, in C++ are tabulated
below.
Pointer Data Type
Table : Operators on pointer data type in C++
Precedence Associativity Arity Operator Function of the
operator

16 L binary [] array index

15 R unary ++, −− increment, decrement

15 R unary *, & dereference,


address of
12 L binary +, − arithmetic operators

10 L binary <, <=, >, relational operators


>=
9 L binary ==, != equality, inequality

5 L binary && logical AND

4 L binary || logical OR

2 R binary =, +=, −= assignment operators


Declaration of Pointers
An address is relevant only when it is residence of
some variable / object.
• For instance, constants of any data type are not
assigned memory and hence do not have an
address.
• To define a pointer to a variable, the declaration
of the variable must exist in the first place. Since
a pointer is always associated with a variable /
object, pointer data type is often referred to as a
derived data type.
• For a variable x of any data type, &x yields the l-
value or address of x. Addresses may be stored,
processed and manipulated using the pointer data
type.
Declaration of Pointers
A pointer has an associated type. For a type, say T, the
type of pointer to T is defined to be the expression T *

T specifies the type of data object the pointer will


address.

Examples :

int *p1, *p2 ; // p1 and p2 both are pointers to


//integers
unsigned char * cp; // cp is a pointer to unsigned
//char
float *f // f is pointer to float
Working with Pointer Data Type

Consider the declaration of variables and pointers to them.

int i = 20; int * p = &i;


bool flag = true; int * q = & flag;
float x = 2.35; int * r = & x;

• The declarations in the first column declares and


initializes 3 scalars.
• The declarations in the second column
int * p = & i;
Working with Pointer Data Type
• The lhs of the declaration, read from right to left is,
p * int, or verbally as “p is a pointer (*) to data
t y p e i n t ” . We re a d * i n t h e c o n t ex t o f s u c h
declaration as pointer to.
• The rhs shows that p is to assigned the address of i,
that is the l-value of i.
• The changes in memory after the declarations are
processed are shown below:
Declaration Memory
int i = 20; The l-value is arbitrary and used only for the
sake of illustration.
l-value : 1000 size : 4
r-value : 20 Type : int
Name : i
Working with Pointer Data Type
Declaration Memory

bool flag = true; The l-value is arbitrary.


l-value : 2000 size : 1
r-value : true Type : bool
Name : flag

float x = 2.35; The l-value is arbitrary.


l-value : 4000 size : 4
r-value : 2.35 Type : float
Name : x

int * p = &i; l-value : 6000 size : 8 l-value : 6000 size :4


r-value : true r-value : 20
Name : p Name : i
Type : int* Type : int
Pointer Arithmetic
What are the legal operations on pointer data ?

• Addition / subtraction of pointer by integral values


are permitted, this is the only kind of pointer
arithmetic that is allowed.

• The result of the operation p = p + 1 ; depends on


the type of object p points to. If p is pointing to
integer, then the current value of p is incremented
by 4 (size of integer). However if p was pointing to
an object of size, say 60 bytes, then the new value of
p is 60 added to the earlier value of p.
Pointer Arithmetic
• Pointer arithmetic is severely restricted purposely to
avoid unneccesary errors.
• Even i the real world, arithmetic over addresses
does not find any use and the same is, by and large,
true here too.
• Addition (+) on pointers is restricted and expressions
of the form p + c are only allowed, where p is a
pointer and c is an integer value.
• Storage allocated to a pointer : large enough to hold
a memory address. With RAMs in Tera Byte range in
the market, 8 bytes suffice for the present and the
near future.
Pointer Arithmetic
• Initialization of a pointer variable can be done by
assigning it to
1. either a l-value of data object of same type, or

2. value 0 (NULL) – a pointer that is not


addressing a data object

Examples : int *ip = 0;


int *p = & i;

• Literals, constants or expressions do not


have l-value and therefore may not be an
operand of operator &.
Pointer Arithmetic

• For clarity of understanding, you may ask the


following question after p has been declared as
above, all the three are relevant for a pointer, but for
a variable, such as i, *i, is not valid.

– what is p ?
– what is *p ?
– what is &p?
Operations on Pointers
A pointer may be assigned to another pointer of
same type.

int i = 0;
int *ip = & i; // legal ?
int k = ip ; // legal ?
unsigned int *uip = ip ; // is it legal ?
int *ip2 = &ip ; // is it legal ?
int **ip3 = &ip ; // is it legal
int *ip4 = i ; // is it legal ?

Assigning a data object's r-value to a pointer is


always an error.
Type Check with Pointer Data Type

• C++ is strongly typed - in this context it means


that all initializations and assignments are type
checked at compile time.

• To illustrate this consider some of the declarations


given above and let us learn to perform a manual
type check.
int i = 0;
int *ip = & i;
Basics of Type Checking :
To perform type checking for an assignment, we
determine the types of its lhs and rhs separately
and then check whether these two are compatible.
How to deduce the type in a declaration ?

int * ip lhs expression


type variable

• The type of ip as shown above is " int * ";

which is also the expression to left of “ip”.


Basics of Type Checking :

• For the same lhs, if we wish to know the type of

“* ip”, we proceed similarly as above.

int * ip lhs expression


type variable

• The expression, *p, where p is a pointer, denotes


that the pointer p is to be dereferenced (unary
operator * is known as dereference operator) and
the result is the object reached.
Basics of Type Checking :
• When the type of p is int*, the type of *p is just
“int”. Observe that the type of an expression in a
declaration is again the string to the left of it.
Therefore in the declaration float *** f; the types
are as given below.

Expression f *f **f ***f


Type float *** float ** float * float

• Now consider the rhs, “&i”, of the declaration,


“int *ip = & i; “
Basics of Type Checking :
• The type of i is int, because of the declaration,
“int i = 0; “ and the same rules applied to “int i”.
The type of " &i " which is address of an integer is
treated same as pointer to integer or "int *". This
expression is therefore type compatible since the
types of both the lhs and rhs are the same.

int * ip - & i

lhs expression rhs expression


type = int * type = int *
Basics of Type Checking :
• Now consider a few more declarations for type-
checking
int i = 0; // D1
int *ip = & i; // D2
int *ip2 = & ip; // D3
int ** ip3 = & ip; // D4

int * ip - & i

lhs expression rhs expression


type = int * type = int *

Type checking of declarations D1 and D2 are done.


Basics of Type Checking :

We examine D3 next.

int * ip2 - & ip

lhs expression rhs expression


type = int * type = int **

• This declaration is not type compatible, since the


type of lhs is "int*" while that of rhs is "int**". D3
will therefore be flagged as syntax error.
Basics of Type Checking :

Now consider D4. The type checking

int * * ip2 - & ip

lhs expression rhs expression


type = int ** type = int **

results show that this declaration is fine as both


sides have type, “int **”
Basics of Type Checking :
Examples : Test your understanding of type checking and pointer
arithmetic on the following statements and state which have
compiler errors and why?
int i = 10;
int *ip = & i;

int j = *ip; // would assign *ip, i.e., 10 to j

int k = ip; // integer object assigned a pointer ? warning

*ip = *ip + 2 ; // legal ? same as i = i + 2

ip = ip + 2 ; // legal ? pointer ip is incremented by ???


Segmentation fault is a fairly common error with pointers.
• It indicates an attempt to refer to a memory address outside
the address space of a process, the expression, *p, where p is a
pointer may cause this fatal runtime error.
VOID Type

This type behaves syntactically as a fundamental


type.

• used only as part of a derived type

• there are no objects of type void

• functions which do not return a value; pointers to


objects of unknown type, e.g., void * p

• A pointer of any type can be assigned to a variable


of type void *
Illustration of Pointer Features
Consider the following program :
int main()
{ int i = 20;
bool flag = false;
char ch = '#';
string s = " CS 101";
cout << " i : " << i << " size : " << sizeof i << " address : "
<< &i << endl;
cout << " flag : " << flag << " size : " << sizeof flag << "
address : "
<< &flag << endl;
cout << " ch : " << ch << " size : " << sizeof ch << "
address : "
<< &ch << endl;
cout << " s : " << s << " size : " << sizeof s << " address : "
<< &s << endl;
}
Illustration of Pointer Features
Its output displayed on the screen is :

i : 20 size : 4 address : 0x7fff1a24bd58


flag : 0 size : 1 address : 0x7fff1a24bd5f
ch : # size : 1 address : #
s : CS 101 size : 8 address : 0x7fff1a24bd50
Illustration of Pointer Features
Remarks on the output
• The 5 attributes of the four variables are summarized
in the table below.

Attributes i flag ch s
Name i flag ch s
Type int bool char string
Size 4 bytes 1 byte 1 byte 8
r-value 20 0 # CS 101
l-value 0x7fff1a24 0x7fff1a24bd # 0x7fff1a24bd50
bd58 5f

• The entries of the first 4 rows are obvious, except for


size of string data type perhaps.
Illustration of Pointer Features
• Why is the size of string s, 8 bytes, and what is the

relationship of 8 to the r-value of s, “ CS 101” ? Note

that the length of s is 7.

• You may also change the length of s and observe that


size of s remains 8. The answer is that size of a string
variable is always 8 independent of its length. The
reason for this is explained later.

• The l-value for the variables, except for ch is an


expression of the form : 0x7fff1a24bd58. This is a
hexadecimal number (base 16).
Illustration of Pointer Features
• Hex numbers are written in C++ by prefixing the
number with the symbols 0x. The hexadecimal system
has 16 symbols {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e,
f}.
• The letters 0 through 9 have the same meaning as their
decimal (base 10) system counterpart. The letters a
through f denote the decimal numbers 10, 11, 12, 13,
14 and 15 respectively. The decimal equivalent of
0x7fff1a24bd58 is given below :

0x7fff1a24bd58 = 8*160+ 5*161 + 13*162 + 11*16 3 + 4*164 +

2*165 + 10*166 +1*167 + 15*168 + 15*169 + 15*1610+ 15*1611 + 7*1612


Illustration of Pointer Features

• The layout of this hex number in memory is

shown below in 8 bytes, numbered from byte

0 (least significant byte) to byte 7 (most

significant byte)

7 6 5 4 3 2 1 0

00 00 7f ff 1a 24 bd 58
Pointers and Hexadecimal Representation

• The hex representation is a compact representation,


two hex symbols, each in the range from [0, f]
naturally fit into a single byte.
– The exact bit pattern in the bytes 0 through 7 is
given below for which the hex notation was used
to display the number.
– The bit pattern is stored in a byte and the hex
number equivalent is shown below the box.
7 6 5 4 3 2 1 0
0000 0000 0000 0000 0111 1111 1111 1111 0001 1010 0010 0100 1011 1101 0101 1000

00 00 7f ff 1a 24 bd 58
Pointers and Hexadecimal Representation
The 5th rowof the table that provides the l-value
attribute of a variable needs some details for its
explanation.
l-value 0x7fff1a24bd58 0x7fff1a24bd5f # 0x7fff1a24bd50

Usually it denotes the runtime address of the variable


in memory.
• The addresses shown against l-values for variable i
and flag are indeed the address of their first byte.
• The value of &ch is not an address but the character
stored in ch – this is true whenever & is applied to a
char variable.
Pointers and Hexadecimal Representation
• The l-value of s is correctly displayed. However the string
is not placed in these addresses.
• The string literal which is of length 7 is stored in another
part of memory and the address of its start byte is the r-
value of s. However for string variables, display of its r-
value is interpreted by the compiler as accessing the
string stored there.
• T h i s i s t h e o n l y ex p l a n a t i o n t h a t s a t i s f i e s a l l t h e
observations, i) size of s is 8 bytes, ii) 8 bytes are used to
store addresses, iii) r-value of s must be a 8 byte entity, iv)
display of r-value of s shows the actual string literal
supposed to be stored there.
• The representation is part of my laptop specification. It
shows that 8 bytes are used for addresses or l-values of
variables / objects.
Pointers and Hexadecimal Representation
Please note the following in connection with addresses
or l-values.
1. The number of bytes required to hold an address
depends on the size of RAM in the system. To address
every memory byte in a RAM, we need to know the
largest address in RAM.
– For example, if 2 bytes of storage is allocated for
holding an address, the largest unsigned number
that can be stored in it is 65535 which is 64 KB.
– For RAMs of size > 64 KB, this will not work as parts
of the RAM can not be reached. In the early days of
computing, when KB of memory were considered
huge, 2 bytes for address were adequate.
Pointers and Hexadecimal Representation
2. When RAMs of sizes in MB became available and
affordable, the 2 byte allocation was extended to 4
bytes.
– This change affected those working programs who
had used addresses; they had to ported now to the
new environment (similar to Y2K problem) before
they could be used.

3. However, the change offered flexibility in writing new


programs.
– It was also a forward looking decision in the sense
that when Mega Byte RAMs appeared to be the
ultimate, provision for handling RAMs upto 4 GB
was done.
Pointers and Hexadecimal Representation
– The following table shows the largest number that
can be stored with n-bits, the number we are
interested here happens to be a memory address.

n 2n Largest Byte Unit


Number
10 1024 210-1 1 KB (Kilo byte)
20 1048576 220-1 1MB (Mega byte)
30 1073741824 230-1 1GB (Giga byte)
40 1099511627776 240-1 1 TB (Tera byte)
50 1125899906842624 250-1 1 PB (Peta byte)
60 1152921504606846976 260-1 1 EB (Exa byte)
Pointers and Hexadecimal Representation
4. We are on the brink of a need for another change as RAMs
of TB range are there in the market.
– The following table shows that 8 bytes is a good choice
for increasing the allocation from 4 bytes of the past.
No.of Max address of RAM Remarks
Bits

8 255 bytes or 0.25 KB 4 bytes were assigned to addresses, way early,


16 65535 bytes or 64 KB even when RAM sizes were in Kilo bytes. Since 4
bytes were adequate uotp 4 GB, this allocation
32 4294967295 bytes
or 4GB was adequate for the past 25-30 years.
48 281474976710656 Allocating 6 bytes or addresses has 2 problems –
or 256 TB i) soon outdated as TB RAMs are in,
ii) accessing memory in multiples of 4 bytes is
better in hardware
64 20-digit number Best alternative at the moment both from h/w and
or 16EB also in terms of longer shelf life of new programs.
Reference Type in C++
A reference type is a new data type in C++, this does
not have a counterpart in C.
• A reference to an object is an alternative name ( or
alias) for an object. Its syntax is given below :
type_name & alias = object_type_name ;

• Here type_name is the name of a type ( fundamental


or user defined), & is the symbol that indicates a
re f e re n c e ( s i m i l a r t o * u s e d f o r p o i n t e r t y p e
declaration), alias is the name of the reference object
being defined.

• The entity object_type_name occuring after the =


denotes the object that ref is going to reference.
Reference Type in C++
Note that & as used here not as an "address-of"
operator, though the implied meaning is somewhat
similar.

As indicated above, a reference object (such as ref


above) must be initialized.

Concrete examples are :


int i = 0;
int & refi = i; // refi is a reference object for
object i
int & refj ; // error - refj is not initialised

A reference object is an alias of the object it is


referencing. For example, i and refi are aliases of
each other.
Reference Type in C++

• Two objects are aliases if they refer to the same

memory space. An implication of aliasing is that

all operations applied to the reference act on the

object to which it refers.

int i = 0;

int & refi = i; // refi is a reference object for

object i

refi = refi + 1 ; // effect is same as i = i + 1


Reference Type in C++
A relevant question to ask is how does one create
an alias of another object ?

• Examine the following declarations :


int i = 0;
int j = i;
Are i and j aliases of each other ?

The answer is no, because we know that though the


r-values of both are incidentally identical at this
point in the program, they have distinct l-values.
Reference Type in C++
• Since they have distinct l-values ( or addresses ),
a change in the r-value of one is not reflected in
the other.
• Two objects are aliases when their r-values are
always identical ( at every point in the program )
and this is because they denote the same
memory space.

Note that in view of the following declarations


int i = 0;
int * pi = &i;
objects i and *pi are aliases of each other.
Reference Type in C++

• This is precisely the reason why the two

expressions i = i + 1; and *pi = *pi + 1; are

semantically equivalent (but not pi = pi + 1 ).

• It may also be instructive to figure out whether

aliases can be created without using the features

of pointers and references.


Reference Type in C++
How is a reference object actually realised ? The answer
is : "as a special kind of pointer".

A reference is a constant pointer to the object it


references.
• The constant qualifier explains why a reference must
be initialised at declaration time itself.
• A reference type once introduced must point to the
same object ( being a constant) for its entire lifetime.
• If the object to be referenced is not specified at
declaration time, it can not be specified later, and
hence is signalled as a compile time error.
Reference Type in C++
It is not possible to change the object to which a
reference object refers to. What about the following
code :

int i = 0;
int & refi = i;
int j = 10;
refi = j; // same as i = j, i.e., both i and refi
// have 10 as their r-value
refi = &j; // type error - lhs is int & and rhs is
// int* and they are distinct
*refi = j; // *refi is illegal !
....
Reference Type in C++
You may try other means but you just can not
change the binding of refi to i that got established
at the time of declaration of refi.

This situation is very different from that of a pointer


to an object, because we can change the pointer to
point to some other object of the same type as
shown below.
int i = 0;
int * pi = &i; // pi points to object i
int j = 10;
pi = &j; // pi now points to object j
Reference Type in C++
Viewed as a constant pointer, a reference
object is automatically dereferenced (by the
compiler) every time it is used. The l-value
of the reference is never made available to
the program.

Comparison with Pointer


It is important to understand the similarities and
differences between references and pointers so
that one may choose the right type while
designing a program.
Function Arguments as Reference Type

Functions that use arguments as a reference type


object enjoy the benefits of the pointer world but
without its inconveniences.

• Examine the code for swapping given below, which


now has three versions of functions using different
parameter passing mechanisms. Note the way the
parameters are used in function definitions and
function calls.
Function Arguments as Reference Type

void swap ( int i, int j )


{ int tmp;
tmp = i ; i = j; j = tmp;
}
void swap_ptr ( int * p, int * q )
{ int tmp;
tmp = *p; *p = *q; *q = tmp;
}
void swap_ref ( int& i, int& j )
{ int tmp;
tmp = i ; i = j; j = tmp;
}
Function Arguments as Reference Type

int main()
{ int x, y;
cout << " Give 2 integers : " ;
cin >> x >> y ; cout << " values before direct
swap " << endl;

cout << " x = " << x << " y = " << y << endl;
swap(x, y );
cout << " values after direct swap " << endl;
cout << " x = " << x << " y = " << y << endl;
Function Arguments as Reference Type

swap_ptr(&x, &y );
cout << " values after poiner swap " << endl;
cout << " x = " << x << " y = " << y << endl;
swap_ref(x, y );
cout << " values after reference swap " << endl;
cout << " x = " << x << " y = " << y << endl;
}

Run the program, and study the various parameter


passing mechanisms used here.
Parameter Passing Mechanisms
Quick review of parameter passing mechanism in C++

Reference type and objects


The parameter passing mechanisms employed by C++
are call by value and call by reference.

• Call by value : This is the default parameter


passing mechanism used in C++. All objects as well
as pointers to objects are passed by value.

– For example in the swap program discussed in the


class - any call to both the functions, swap() and
swap_ptr() are always passed parameters using
call by value.
Parameter Passing Mechanisms
• Call by reference : used when reference to an
object is passed between the formal (arguments in
the definition) and actual arguments (those in the
call).

Strings in C++ are stored as reference to the strings


they denote. The char* type also behaves like a
reference. The array name is a reference to its first
element.

The reference type is widely applied for passing


parameters between functions. A few situations are
discussed here.
Parameter Passing Mechanisms
Functions with 2 return values
Often we have to write a function that need to return 2
or more values to its caller. A function can always
return one value thorugh its return type.

• In case more values have to be communicated from


the callee to its caller, a scheme as given below may
be used.
• The set up is shown below as a caller function with
two variables x1 and x2 of type1 and type2
respectively.
• It calls a function f() by passing both x1 and x2 as
arguments. The snapshots when control of execution
(marked by red arrow) reaches interesting points in
the program are shown below.
Parameter Passing Mechanisms
Situation 1: Just before start of execution.

type3 f(type y1, type2&y2)


type1 x1 = v1
type2 x2 = v2
type3 x3 = v3

x3 - f(x1, x2)

y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms
Situation 2: After processing of declarations : variables
are initialised.
type3 f(type y1, type2&y2)

type1 x1 = v1 x1 v1

type2 x2 = v2 x2 v2

type3 x3 = v3 x3 v3

--------

x3 - f(x1, x2)
y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms
Situation 3: Just before call to f() : parameters are
passed, x1 to y1 by value and x2 to y2 by reference,
y2 has the lvalue of x2.

type3 f(type y1, type2&y2)

type1 x1 = v1 x1 v1

type2 x2 = v2 x2 v2 &x2 v1
y2 y1
type3 x3 = v3 x3 v3

--------

x3 - f(x1, x2)
y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms
Situation 3: Just before call to f() : parameters are
passed, x1 to y1 by value and x2 to y2 by reference,
y2 has the lvalue of x2.

type3 f(type y1, type2&y2)

type1 x1 = v1 x1 v1

type2 x2 = v2 x2 v2 &x2 v1
y2 y1
type3 x3 = v3 x3 v3

--------

x3 - f(x1, x2)
y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms
Situation 4: Just after call to f() : control is passed from
caller to the first statement in the body of callee f() and
execution continues from this point.

type3 f(type y1, type2&y2)

type1 x1 = v1 x1 v1

type2 x2 = v2 x2 v2 &x2 v1
y2 y1
type3 x3 = v3 x3 v3
-----------
--------

x3 - f(x1, x2)
y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms
Situation 5: In the body of f() just after execution of y2
= v4. The value of x2 in the caller changes because of
reference.
type3 f(type y1, type2&y2)

type1 x1 = v1 x1 v1

type2 x2 = v2 x2 v4 &x2 v1
y2 y1
type3 x3 = v3 x3 v3
-----------
--------

x3 - f(x1, x2)
y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms
Situation 6: Just after execution of return e1 in caller f().
The control is back to the caller and the rhs of call is
complete, the execution resumes to complete the
assignment to lhs.
type3 f(type y1, type2&y2)

type1 x1 = v1 x1 v1

type2 x2 = v2 x2 v4

type3 x3 = v3 x3 v3
-----------
--------

x3 - f(x1, x2)
y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms
Situation 7: Just after completion of the call statement
in caller. The return value is assigned to x3.

type3 f(type y1, type2&y2)

type1 x1 = v1 x1 v1

type2 x2 = v2 x2 v4
----------
type3 x3 = v3 x3 e1

--------

x3 - f(x1, x2)
----------
y2 = v4;
return e1
caller callee
Parameter Passing Mechanisms

The scheme used above may be employed in spirit for


returning any number of variables and arrays between
functions.

• Arrays are always passed by reference, so no extra

care is required for passing an array.

• However, it is a safe and good practice for a caller to

pass the size of the array as an additional parameter

by value.
A note on strings and array of strings
A string is given 8 bytes, because of which an array of
size 100 of strings, requires 800 bytes. What is there in
these 8 bytes ?

Let us construct a small program fragment to seek an


answer :

string str = " CS 101 : Computer Programming &


Utilization ";
cout << " sizeof str : " << sizeof str << " " << " r-
value of str = " << str << endl;
cout << " address of str (l-value) : " << &str << "
length of str : " << str.length() << endl;
A note on strings and array of strings

O n c o m p i l a t i o n a n d exe c u t i o n , t h e o u t p u t
displayed is :

sizeof str : 8
r- v a l u e o f s t r = CS 101 : Computer
Programming & Utilization

address of str (l-value) : 0x7fff8cb10250

length of str : 45
Strings and Reference Type
Attributes of a Variable : All declared objects
possess 5 attributes and in fact more, which shall
be introduced in due course. The five attributes
of the string variable str, as displayed in the
output, are

str : name attribute of string


size attribute of str : 8 bytes
string : type attribute of str
r-value of str = CS101 : Computer Programming &
Utilization
l-value : 0x7fff8cb10250
Strings and Reference Type
???? 0 str[0] to str[44] as an array 44
0x7fff8cb10250 b C n b

0x7fff8cb10250

The figure above depicts the inside view of memory


• First an array of strings cannot keep the strings
themselves as elements of the array without
compromising its direct access property (strings may be
of varying lengths and hence of different sizes which is
not permitted for arrays).
• The strings are therefore kept in a different part of
memory and the address of the first character of the
string is stored in the array.
Strings and Reference Type
• Since addresses require same size of memory, 8 bytes
here, an array of strings is essentially an array of
addresses to the first character of the individual strings.
This strategy honours the data structure requirement of
an array.
• However this view is not consistent with the output
displayed.
r-value of str address of str (l-value)
Analysis 0x7fff8cb10250 ????

Display CS 101 : Computer 0x7fff8cb10250


Programming & Utilization
Strings and Reference Type

• If the snapshot of memory drawn above is correct, the


r-value of str, the contents of str is the address of
me mo ry wh e re t h e l i t e ra l “ C S 101 : C o m p u t e r
Programming & Utilization “ has been stored.

• The value 0x7fff8cb10250, is a hexadecimal number.


Assume this to be address of the string in memory.

• The l-value of str is the memory location where the


address 0x7fff8cb10250 has been stored and assumed
to be unknown and represented by the symbols ????.
Strings and Reference Type

The display however shows the r-value of str to be the


string “ CS 101 : Computer Programming & Utilization “.

• This is not possible because this string has length 45


which cannot be fitted into 8 bytes, the sizeof str, also
displayed in the output.

• Further the l-value of str cannot be the address


0x7fff8cb10250, if this is assumed to be the address of
the first location of the string, “ CS 101 : Computer
Programming & Utilization “.

• The answer lies in the contents and interpretation of


the r-value and l-value of a string str. For a string, say s.
Strings and Reference Type

• The r-value of s is the address of the first character


where the string is stored; however when the r-
value of str is referred to, the system takes the
address, extracts the string stored in that address
and the string is made available.

• This process of using an address is called,


“dereferencing the address”. Therefore when the r-
value is an address, two possibilities exist, i) give
the address stored in it, or ii) dereference the
address and give the object stored there.
Strings and Reference Type

• When the l-value of a str is asked, the system again


has two possibilities, either i) give the address of
the str, or ii) dereference the r-value in str to get
the string stored there and then get the l-value of
the string and return this address.

The crux of the matter is that there are certain data


types in C++ which when accessed, are first
dereferenced and then the value, l-value or r-value as
appropriate, is obtained and returned to the context.
Template Functions
We know that C++ permits function names to be overloaded
provided their signatures are distinct. In real life
programming, functions often need to be rewritten with
minor changes; here is an example as evidence.

Problem 1 : A function that finds the maximum value of an


integer array is written.
int max ( int a[], int size) // a is an integer array passed by
reference
{ int res = a[0]; // size is the number of elements in a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept in res
return res;
}
Template Functions
The function may be called by writing a main_program
and tested with sample inputs.

int main(){
int a[] = { 2, 5, 3, 4, 1, 5};
int b[] = { 12, 15, 3, 4, 19, 5};
cout << " maximum is " << max(a, 6) << endl;
cout << " maximum is " << max(b, 6) << endl;
return 0;
}

The values displayed may be checked to be correct with


the max values in the arrays a[ ] and b[ ].
Template Functions
Problem 2 : Write a function that finds the maximum value in an
array of characters, in addition to the function in Problem 1.

Reuse : On the face of it, the computational effort in finding max in


an integer array is expected to be similar to finding max in a
character array.
Let us examine the integer max function for reuse.

int max ( int a[], int size) // a is an integer array passed by


reference
{ int res = a[0]; // size is the number of elements in a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept in res
return res;
}
Template Functions
1. For a character array, the return value is the max in the array
and hence must be a char type. The return type int should be
changed to char
2. The first argument of the function must be char array passed by
reference, that is int a[] should change to char a[]
3. The second argument int size represents the number of
elements in the array of the first argument. Since all arrays have
integer number of elements, this requires no change, as size of
an array is always an integer regardless of the type of its
elements.
4. int res = a[0]; The variable res is used to hold the maximum
value and should be of the same type as that of the array.
Change to char res = a[0];
5. 5. Arguing on similar lines as above, it can be seen that the rest
of the statements of the function max() does not need a change.
Template Functions
To summarize, only the three words marked in red above, need
change. Therefore copy, paste and do 3 edits to yield the
required function.

char max ( char a[], int size) // a is an integer array passed by


reference
{ char res = a[0]; // size is the number of elements
in a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept in res
return res;
}
Template Functions
A main_program to test both the functions

int main() {
int a[] = { 2, 5, 3, 4, 1, 5};
int b[] = { 12, 15, 3, 4, 19, 5};
char ch[] = {'1', '2', 'a', ';', '!'};
cout << " maximum is " << max(a,6) << endl;
cout << " maximum is " << max(b,6) << endl;
cout << " maximum is " << max(ch, 5) << endl;
return 0;
}

The values displayed may be checked for their correctness.


Template Functions
Problem 3 : We need several definitions of functions that can find
maximum over arrays of different data types. A main() that calls
such functions is given below, and our task is to supply the
appropriate function definitions.
int main(){
int a[] = { 2, 5, 3, 4, 1, 5};
int b[] = { 12, 15, 3, 4, 19, 5};
char ch[] = {'1', '2', 'a', ';', '!'};
float f[] = { 2.3 , 2.1, 2.7, 1.5, 0.3};
string str[] = { "sb", "as", "dmd", "ss"};
cout << " maximum is " << max(a,6) << endl;
cout << " maximum is " << max(ch, 5) << endl;
cout << " maximum is " << max(f,5) << endl;
cout << " maximum is " << max(str,4) << endl;
cout << " maximum is " << max(b,6) << endl;
return 0;
}
Template Functions
Solution 1 : Write the bodies of all the required function
definitions, using the reuse analysis described in Problem 2.
This results is creating 4 overloaded max functions with
minor differences between them.

int max ( int a[], int size) // a is an integer array passed by


reference
{ int res = a[0]; // size is the number of elements
in a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept in
res
return res;
}
Template Functions
char max ( char a[], int size) // a is an integer array passed by
reference
{ char res = a[0]; // size is the number of elements in
a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept in res
return res;
}

float max ( float a[], int size) // a is an integer array passed by


reference
{ float res = a[0]; // size is the number of elements in
a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept in res
return res;
}
Template Functions

string max ( string a[], int size) // a is an integer array


passed by reference
{ string res = a[0]; // size is the number of
elements in a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept
in res
return res;
}

The programmer has to be careful during the copy, paste


and edit actions, which are error prone, even though there
may not be much intellectual content in this task.
Template Functions
Solution 2: C++ provides a feature, called as template functions,
that addresses this problem squarely.
• A programmer, instead of writing different definitions of a
function, with largely a similar behaviour, may give a generic
definition of such a function once. Template function is such a
feature.
• The compiler saves the template definition, just as it does
when it finds a non-template function. We shall use the term
concrete function instead of a non-template function
henceforth in this course.
• When the compiler encounters a function call, it attempts to
resolve the call using the algorithm for oveloaded function, the
same is now augmented to handle calls by using the function
template, if necessary.
Template Functions

We write a template function now. Let us consider one of


the concrete functions discussed earlier.

float max ( float a[], int size) // a is an integer array


passed by reference
{ float res = a[0]; // size is the number of
elements in a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is
kept in res
return res;
}
Template Functions
The words marked in red are the parts of the function that
depend on the data type.

1. Let choose a variable, say T, to capture this dependence


and use it to replace the keyword, float.

T max ( T a[], int size) // a is an integer array passed by


reference
{ T res = a[0]; // size is the number of elements in
a[]
for ( int i = 0; i < size ; i++ )
if ( a[i] > res) res = a[i]; // larger of a[i] and res is kept in
res
return res;
}
Template Functions
2. Inform the compiler the role played by our varable T.
This is done by writing

template < class T>

The complete template definition of function max follows


along with a main() that uses it repeatedly:

template < class T>


T max ( T array[], int size)
{ T res = array[0];
for ( int i = 0; i < size ; i++ )
if ( array[i] > res) res = array[i];
return res;
}
Template Functions
int main()
{
int a[] = { 2, 5, 3, 4, 1, 5};
int b[] = { 12, 15, 3, 4, 19, 5};
char ch[] = {'1', '2', 'a', ';', '!'};
float f[] = { 2.3 , 2.1, 2.7, 1.5, 0.3};
string str[] = { "sb", "as", "dmd", "ss"};
cout << " maximum is " << max(a,6) << endl;
cout << " maximum is " << max(ch, 5) << endl;
cout << " maximum is " << max(f,5) << endl;
cout << " maximum is " << max(str,4) << endl;
cout << " maximum is " << max(b,6) << endl;
return 0;
}
Template Functions
It can be verified that the program does its intended job.
But how does it do so ? The compiler works in the
background and does the menial job that would have to
done by us otherwise.

The working of the compiler as it processes the main()


function is given in the following.

Recall that a compiler merely translates the input source


program to the machine language. What is shown below
are the actions performed by the compiler as it
continues with its processing to generate machine code.
Template Functions
Statement Compiler action Remarks
int a[] = Creates an integer The address of a[0]
{ 2, 5, 3, 4, 1, 5}; array of 6 elements, is stored in the
initializes with values variable named, a;
supplied in order from a is a reference
a[0] to a[5] type
... ... ...
string str[] = Similar actions as Similar as above
{ "sb", "as", "dmd", above
"ss"};
max(a, 6) Uses the template max() Resolves the call
to unify the call to with the
max(a, 6); creates a generated int max
concrete function int ( int [], int)
max(int array[], int size) {..}
{int res =
array[0];................... }
Template Functions
Statement Compiler action Remarks
max(ch, 5) creates a concrete function Resolves call to
char max(char array[], int char max (char[],
size) {char res = array[0]; } int) { }
max(f, 5) Creates the function calls float max
float max (float[], int) {..} (float[], int) { }
max(str, 4) Creates the function Calls string max
string max (string[], int) { ..} (string[], int){ ..}
max(b, 6) Finds the function Calls int
int max (int[], int) {..} max( int[], int) {..}

The non-obvious issue is that the number of function


definitions change at different points in a C++ program as
the compiler opens up templates to match function calls.
Keep the role played by the compiler in mind when you
define and use template functions.
Compile time Resolution of Function Calls
An outline of the algorithm employed by a compiler for C++ in
resolving a function call.

1. N o d e f n i t i o n s o f s u c h a f u n c t i o n i s f o u n d – r e p o r t
appropriate error message. Call is not resolved.
2. Use the signature of the call to find an exact match with a
signature of a concrete function definition from the set of
all definitions of this function available at the call-point.
• In case the signature of call matches exactly with one
definition, the call is resolved to this definition.
• Note that the call cannot exactly match two or more
concrete function definitions because the signatures of
two or more definitions of an overloaded function are
different.
• Also note that template definitons of this function are not
considered in this step.
Compile time Resolution of Function Calls
3. In the event that compiler fails to find an exact match in
step 2, the compiler examines the template definitions
available.
• Derives the value of Type variable T by unifying the two
signatures. If the unification is successful, the template
definition is instantiated to create a concrete function.
• The compiler resolves the call to this created concrete
function. Note that if the unification is not successful or
more than 1 template definition unifies successfully, the
compiler fails to resolve the call.
• The resolution of the call in this step may also be viewed
as an exact match with the definition of the concrete
function used.
• The only difference is that this concrete function did not
exist before the call, is fresh from the mint, generated by
the compiler using the template.
Compile time Resolution of Function Calls

4. When all efforts to find a definition that matches


exactly with the signature of the call fails, the compiler
attempts to find an approximate match with one of the
existing concrete functions.
• The template definitions are ignored in this step.
• Approximate match permits conversion of types for
the corresponding arguments, according to the rules
defined by the language designer. We skip the
details as they are beyond the scope of this course.
• If Approximate match leads to a unique definition,
the compiler resolves the call to this definition, but
unfortunately gives no indication back to the user.
Compile time Resolution of Function Calls

• If multiple definitions match approximately –


compiler reports an error, stating that the call is
ambiguous and displays all the definitions that
match approximately as a feedback to the user.

• No definitions match even approximately – reports


error and fails to resolve the call, stating that no
matching defintions are found. It may display the
definitions of the function that are present as
feedback to the user.
Linked Lists
In order to work with linked lists, a programming
language needs to provide the following support :
1. creating new type in the language - in order to
create an object comprising of pairs of a value
and an address.
The address part of the pair is used up to
connect the various elements of the list that
are scattered anywhere in memory. In C++, the
class construct permits us to create our own
types.
Linked Lists
2. A type for dealing with addresses of objects be
a part of the language.
The pointer type enables us to work with
addresses of objects. For declaring a pointer to
type t, we use "*t ". For dereferencing a pointer
p, "* p" is available.
For address of an object o, unary operator &
exists. To select a data member a or a function
member f() of an object o, using a pointer p of
the object o, we use p->a or p->f() respectively.
Linked Lists

Recall that the data and function members of


an object o, can also be accessed through
the notations : o.a or o.f() resppectively.

The syntax for accessing a field of an object


is different when the object is used or when
a pointer to the object is used. Use the
selector operator "•" for object and the
operator "->" for a pointer to the object.
Linked Lists

3. A construct using which program may ask for


chunks of memory space at run-time. This is the
support for dynamic memory from the language.

In C++, the construct new <type_name>


enables the programmer to ask for memory, of
the size of type_name, at run-time.

4. Basic steps for creating a linked list of integers -


read integers, one by one, supplied as input and
then display the list.
Linked Lists
We shall assume that we have no idea about the number
of integers that would be supplied, else an array could
have been used.

The declaration for creating objects of the kind of an


element of the linked list are met by :
class node {

public :

int val;

node * next;

};
Linked Lists
• This introduces a new type to the program, named
as " node". However this declaration does not create
an object and hence does not get any memory
allocated.

• Memory is allocated only to objects. The role of


"node" is same as that of "int" or "float" or any other
fundamental type supported by C++.

• The recursive specific a t ion of n ode * w it h ou t


completing the definition of node is permitted as a
special case. The data member declaration states
that space be alllocated for 2 members, 4 bytes for
int value (item) and 8 bytes for a pointer (node*).
Linked Lists
• Change the second data member declaration
to “node x;” and observe the response of the
compiler.

• The compiler determines that 12 b y tes


amount of memory are required for an object
of type node for any object of type node.

• The first requirement is to model the


situation at the start when the list is empty to
begin with.
node *list = 0;
Linked Lists
An object named, list, is created as result of above
statement. The r-value of list is expected to store the
address of the first element of the linked list ( of type
node* ).
We initialize list to 0 (not address 0), this denotes an
empty list to begin with. The value 0 when
interpreted as an address denotes null address.

• Attempt to dereference list, *list, is illegal and likely to


give the run-time error, known as segmentation fault.
• The design of a class for handling our singly linked lists
is the objective. Along the lines discussed in Lecture 1,
our linked list should have the following capabilities.
Linked List Class Design
The specifications of our design is outlined below.
• Create a linked list : constructor for the class that is
about to be defined. Intended usage is of the form :
List L; // to create an empty list L

• Bool isempty () : returns true or false depending on


whether is list is empty or not. A member function of
our class. Intended usage :
List L; // to create an empty list L
cout << L.isempty();

• int length() : returns the number of elements in a list. A


member function of our class. Intended usage :
List L; // to create an empty list L
cout << L.length();
Linked List Class Design
• insert (int x) : inserts the value x to the head of the list
and returns the new list. The value x becomes the first
member of the new list followed by the earlier
members. To be added as a member function of our
class.
List L; // to create an empty list L
int val;
while (cin >> val) L.insert(val);

• append (int x) : appends the value x to the end of the


list and returns the new list. The value x becomes the
last element of the changed list. Add as a member
function of our class.
List L; // to create an empty list L
int val;
while (cin >> val) L.append(val);
Linked List Class Design
• concat(list L) : concatenates the given list with the list
L passed as a argument and returns the cancatenated
list. Add as a member function.
List L1, L2; // to create an empty list L
int val;
while (cin >> val)
{L1.insert(val); L2.append(val);}
L1.concat(L2);

• operator=() : overloaded operator assignment (=)


among objects constructed from our list class.
List L; // to create an empty list L
int val;
while (cin >> val) L = L.insert(val);
Linked List Class Design
• operator<<() : overloaded operator<< to output our
list objects – designed to display all elements on the
linked list, from the first to the last element. Non-
member function of our class.
List L; // to create an empty list L
int val;
while (cin >> val) L = L.insert(val);
cout << “ List L : “ << L << endl;

• The specifications of our class list comprises of 7


m e m be r fu n c t ion s a n d 1 n on - m e m be r fu n c t i o n ,
operator<<; why non-member function ?

• Before implementing the specifications, let us examine


its use through a main() function.
Linked List Class Design
// main() function that uses the class design to checks it out.
int main()
{ int num;
list l,m ;
while ( cin >> num)
{ l = l.append(num);
m = m.insert(num);
}
cout << " length of list l = " << l.length() << endl;
cout << " display of list l follows \n" << l << endl;
cout << " length of list m = " << m.length() << endl;
cout << " display of list m follows \n" << m << endl;
list merge1;
merge1 = l + l;
cout << merge1.length() << endl;
cout << " display merged list l + m \n" << merge1 <<
endl;
}
Linked List Class Design
Member function for creating an empty list :
class list {
class node { public:
int val;
node * next;
};
public : node * head; // lone data member
list() { head = 0;} // constructor

bool isempty ()
{ if ( head == 0 ) return true;
else return false;
}
}
Linked List Class Design
• The only data member of an object instance of our
class is a place to save the address of the first element
of the list. The name, head, is used for this purpose.

• The elements of the list reside on another part of RAM,


k n o w n a s h e a p . H e a p i s f ro m w h e re m e m o r y i s
allocated and deallocated dynamically.
public : node * head; // lone data member
list() { head = 0;} // constructor member function

• The specification of isempty() member function is


obvious.
Linked List Class Design
• The member function length() traverses the list
starting from the first element and counts the number
till it reaches the end of the list.
int length ()

{ node * p = head;

int count = 0;

while ( p != 0 ) {count ++; p = p->next; };

return count;
}
Linked List Class Design
• Member function insert() is relatively easy as we insert the
new element at the head of the list. First get memory from
the heap, initialize with the element value.
node *p = new node;

if ( p == 0 )
cerr << " fatal error : no space on heap " << endl;
p->val = elm;

• Now add the element *p to the head of the list by a few


pointer manipulations
p->val = elm;

p->next = head; // connects new node to head of list


head = p;
Design of Linked List Class
The full specification of insert() :

list insert ( int elm )


{ node *p = new node;
if ( p == 0 )
cerr << " fatal error : no space on heap "
<< endl;
p->val = elm;
p->next = head; // connects new node
to head of list
head = p; // changes head of list
to new element
return *this; // return the new list
after insertion
}
Design of Linked List Class
• The append() member function is provided to insert the
new element at the end of a list. Since the end of the
list is not directly accessible in our design, it has to be
first located and then the new element is inserted.
list append (int elm )
{ // find the last node of list
node *p = new node;
p->val = elm; p->next = 0;
if ( head == 0 ) head = p; // add to empty list
else { node *curr = head; node *prev = head;
while ( curr != 0 ) { prev = curr; curr =
curr->next; };
prev->next = p;
};
return *this;
p->val = elm;
p->next = head; // connects new node to head of list
head = p;
}
Design of Linked List Class
The merging of 2 lists are implemented by concat() member function.
The list given as left operand is first copied element by element into a
new list created for the merged list. The last element of the first list is
connected to the head of the second list given as right operand).

list concat (list l)


{ list res;
int val;
node *curr = head; node *tail = res.head;
if ( curr == 0 ) return res = l;
else
{ while (curr != 0) {
res = res.append (curr->val);
if ( tail != 0 ) tail = tail->next; else tail = res.head;
curr = curr->next;
};
tail->next = l.head;
};
return res;
}
Design of Linked List Class

Q. What is the role played by the while loop ?

Q. What is the purpose for the pointer tail


used above?
Complete design of the linked list class
// 1. insert() and append() for adding elements
// 2. concat() for merging of two lists
// 3. display() member function for output

class list {
class node { public:
int val;
node * next;
};
public : node *head;
list() { head = 0;} // constructor

bool isempty () {if (head == 0) return true;


else return false; }
Complete design of the linked list class
int length ()
{ node * p = head; int count = 0;
while ( p != 0 ) {count ++; p = p->next; };
return count;
}

list insert ( int elm )


{ node *p = new node;
if ( p == 0 ) cerr << " fatal error : no space
on heap " << endl;
p->val = elm; p->next = head; head = p;
return *this;
}
Complete design of the linked list class

void operator= (node* p) {head = p; return;}

void display ( )

{ node * p = head;

for ( int i = 1 ; i <= this->length(); i++ )

{cout << " element no " << i << " is = "

<< p->val << endl; p = p->next; };

}
Complete design of the linked list class
list concat (list l)
{ list res; int val;
node *curr = head; node *tail = res.head;
if ( curr == 0 ) return res = l;
else
{ while (curr != 0)
{ res = res.append (curr->val);
if ( tail != 0 ) tail = tail->next;
else tail = res.head;
curr = curr->next;
};
tail->next = l.head;
};
return res;
}
Complete design of the linked list class
list append (int elm )
{ // find the last node of list
node *p = new node;
p->val = elm; p->next = 0;
if ( head == 0 ) head = p;
else { node *curr = head; node *prev = head;
while ( curr != 0 )
{ prev = curr; curr = curr->next; };
prev->next = p;
};
return *this;
}
};
Complete design of the linked list class
// A function to use the class design
int main()
{ int num;
list l,m ;
while ( cin >> num)
{ l = l.append(num); m = m.insert(num);}
cout << " length of list l = " << l.length() << endl;
cout << " display of list l follows " << endl;
l.display();
cout << " \nlength of list m = " << m.length() << endl;
cout << " display of list m follows " << endl; m.display();
list merge1;
merge1 = l.concat(m);
cout << " \nlength of list merge1 = " << merge1.length()
<< endl;
cout << " display merged list l and m \n" << endl;
merge1.display();
}
Complete design of the linked list class
For the Input : 7 31 41 13 53
The output is shown in 2 columns below :

length of list l = 5 element no 5 is = 7

display of list l follows length of list merge1 = 10


element no 1 is = 7
element no 2 is = 31 display merged list l and m
element no 3 is = 41 element no 1 is = 7
element no 4 is = 13 element no 2 is = 31
element no 5 is = 53 element no 3 is = 41
length of list m = 5 element no 4 is = 13
element no 5 is = 53
display of list m follows element no 6 is = 53
element no 1 is = 53 element no 7 is = 13
element no 2 is = 13 element no 8 is = 41
element no 3 is = 41 element no 9 is = 31
element no 4 is = 31 element no 10 is = 7
Linked List Class
1. While designing a linked list class, the following issues
were examined.
2. What should be data members of the class and what
should be public or what should be private ?
3. What should be the member functions of the class ?
4. What are the intended applications of the class ?

Let us examine the third issue first. The applications of


linked list are problems where the number of objects are
hard to predict statically.
• Sparse polynomial is a good example.
• Personal collections such as books, songs, video clips,
photographs, etc also increase and reduce in spurts and
are good examples of such situation.
Linked List Class
• The stack and queue data structures also meet this
requirement. The data structure stack employed by
compilers and OS are better implemented as linked lists.
• Non-linear data structures – such as sparse matrices,
trees, graphs and others, which are used in a wide
variety of applications use linked lists for their
realization. However these data structures may need
more than one link and doubly linked / multi-linked
lists are often deployed for their implementation.

• In summary there are large number of applications that


deal with real time data whose sizes are hard to predict
in advance and need the flexibility of a linked list
implementation.
Linked List Class : Improvements
Many improvements are possible to our first design of the
linked list. They include, among others :
• Overloading operator+ in lieu of insert(), such as
• Overloading operator+ in place of concat()
• Replacing display() member function by an overloaded
operator<<
The changes are mostly syntactic but it does make a
considerable difference to the ease of use of the class.

Earlier Design Proposed Design


operator+(int elm) { }
list insert(int elm) { }
list operator+(list l) { }
list concat (list l) { }
list operator&(int elm) { }
list insert(int elm) { } friend ostream & operator
void display( ) { }list << (ostream &x, list l)
Linked List Class : Improvements
The change from display() to operator<< is the only
nontrivial change :
friend ostream & operator<< (ostream &x, list l)
{ node * p = l.head;
if (l.length() == 0) x << " Empty list " << endl;
else
for ( int i = 1 ; i <= l.length(); i++ )
{ x << " element no " << i << " is = " << p->val << endl;
p = p->next; };
return x;
}
Q. Why the use of friend qualifier ? Is operator<<() a
member or non-member ? Why are the types of return
and first argument types and why?
Stack as a Linked List
A single linked list implementation is the obvious choice for
realizing a stack. Let us create a stack of integers as a linked
list.

Every element of a linked stack would have two elements –


the integer value and the address of the next element on
the stack. The collection of 2 values, int and a pointer is
represented by type node, which has 2 components – val of
type int and next of type node*.

val next
int node*
node
Stack as a Linked List
A linked stack would require a single pointer to the stack.
The stack elements, themselves, would be residing on the
heap. An initialized linked stack is visually shown below.
This operation may be denoted by the pseudo code :

stack T;
An empty stack has no elements and hence looks like :
top
null

After 2 push operations on the stack above, say integers 20


and -10, the stack configuration in the array representation
would appear as follows.
Stack as a Linked List
The pseudo code for the operations are :
T.push(20); T.push(-10);

top
heap
val next
20 val next
-10 null

After a pop operation on the stack, the stack configuration


changes to the following contents and the element on the
top of stack (tos) is returned back.
Stack as a Linked List
We use the pseudo code : int elm = T.pop();

top
heap
val next
20 null
elm
-10

The popped element is saved in variable elm, while the


variable top points to the tos element after the deletion.
Sample C++ class design for realizing a stack as a linked list
is outlined in the following. Note that other
implementations are also possible.
Stack as a Linked List
Singly Linked List Implementation of a stack in C++:

The specifications of a stack data structure are :


• create an empty stack
• push(), a new item on the stack; existing stack grows by
1 more element
• pop(), the top item from a stack; existing stack has one
element less
• peek(), the value of the top of stack (tos) item is returned;
stack remains unchanged
• isempty(), returns true if stack is empty, false otherwise
• isfull(), returns true if stack is full (heap exhausted), false
otherwise.
Stack as a Linked List
Another class named as, class stack_item, is used to create
objects of the type shown below.
info next
string stack_item*
stack_item

• The private member function, stack_item* makenode


(item_type item) is used to create an object of the type
stack_item on the heap.
• The member functions, push() and pop() of a linked list
are the usual insert / delete at the head of the list (or
stack).
• Push() uses makenode() to create a new node on the
heap while pop() returns back the node at tos to the
heap.
Stack as a Linked List
/* file stack_linked_new.h */
/* file stack_linked_new.h */

typedef string item_type;


class stack; // forward declaration
class stack_item { friend class stack;
private :
item_type info;

stack_item *next;
};
Stack as a Linked List
class stack {
stack_item * stack_top; // stack top pointer
stack_item* makenode (item_type item)
{ stack_item *p;
if ( (p = new stack_item) == NULL ) cerr
<< "run out of memory"<< endl;
else { p->info = item; p->next = NULL;
return p; }
}
public :
stack() { stack_top = 0;}
Stack as a Linked List

void push (item_type item ) // changes tos

{ stack_item *p;

p = makenode(item);
if ( p == NULL) cerr << "push not
possible : no node" << endl;

else { p-> next = stack_top; stack_top =


p; }

}
Stack as a Linked List

item_type pop() // changes tos


{ stack_item *p;
item_type item;
if ( stack_top == 0) cerr << " em p ty
stack" << endl;
else { p = stack_top; stack_top = p->next;
item = p->info;
delete p; return item;
}
}
Stack as a Linked List

item_type peek () // doesn't change top


{ stack_item *p;
item_type item;
if ( stack_top == 0) cerr << " empty
stack" << endl;
else { p = stack_top; item = p->info;
return item; }
}

bool isempty () { return stack_top == 0;}


Stack as a Linked List

bool isfull()
{ stack_item *p;
if ( (p = new stack_item) == NULL ){ cerr
<< "run out of memory" << endl;

return true; }
else return false;
}

}; // end of stack definition


Stack as a Linked List
• A main() to use the stack class defined above.
/* contents of a file, stack_linked_new.C */
#include "stack_linked_new.h"
int main() {
item_type item;
stack s; // creating an empty stack
while (cin >> item)
{ s.push(item);
cout << "Item inserted is " << s.peek()
<< endl;
if ( s.isfull() ) break;
};
Stack as a Linked List
• For the input :
Python Bash Java C++ C CUDA C#

• Output produced is :
Item inserted is Python
Item inserted is Bash
Item inserted is Java
Item inserted is C++
Item inserted is C
Item inserted is CUDA
Item inserted is C#
stack is not full
Stack as a Linked List

Item deleted is C#

Item deleted is CUDA

Item deleted is C

Item deleted is C++

Item deleted is Java

Item deleted is Bash

Item deleted is Python

stack is empty
Template class design for singly linked list
The single linked list realization for a stack of strings may be
extended to create a stack of objects of various types.
// file stack_template_linked.h
// declaration section
template <class T > class stack;
template <class T > class stack_item {
friend class stack <T> ;
private : T info;
stack_item<T> *next;
friend istream& operator>> ( istream & x,
stack_item<T> &s )
{ return x >> s.info; };
friend ostream& operator<< ( ostream & x,
stack_item<T> s )
{ return x << s.info; };
};
Template class design for singly linked list

template < class T > class stack


{
private :
stack_item<T> *tos; // stack realised as a linked
list
stack_item<T>* makenode (T item)
{ stack_item<T> *p;
if ( (p = new stack_item<T>) == NULL )
error ("run out of heap memory");
else { p->info = item; p->next = NULL; };
return p;
}
Template class design for singly linked list

void error ( string s) { cerr << s << endl ; }

public :
stack ( ) { tos = 0; }

void push ( T item ) // changes tos


{ stack_item<T> *p;
p = makenode(item);
p-> next = tos; // tos is the stack's top
tos = p; // is this enough ?
}
Template class design for singly linked list

T pop(void) // changes tos


{ stack_item<T> *p;
T item;
if ( isempty () )
cerr << "Stack underflow -- it is empty :
ignoring this pop \n";
else { p = tos; tos = p->next; item = p->info;
delete p;
return item;
}
}
Template class design for singly linked list

T peek(void) // doesn't change tos


{ stack_item<T> *p;
T item;
if ( isempty () )
cerr << "Stack underflow -- it is empty : ignoring
this peek \n";
else {
p = tos; item = p->info;
return item;
}
}
bool isempty () { return tos == 0;}
}; // end of stack class definition
Template class design for singly linked list

template < class T >


ostream& operator<< ( ostream & x, stack<T> &s )
{ stack<T> t1, t2;
T elm;
while ( !s.isempty() ) { elm = s.pop();
t1.push(elm); };
while ( !t1.isempty() ) { elm = t1.pop();
s.push(elm); t2.push(elm);};
while ( !t2.isempty()) x << t2.pop() << " ";
return x ;
}
Template class design for singly linked list

template <class T> int stacksize ( stack<T> s )


{ stack<T> tmp;
int count = 0;
while ( !s.isempty() )
{ tmp.push( s.pop() ); count ++; };
//stack s is now inverted in tmp; now restore the
original stack
while ( !tmp.isempty() { s.push( tmp.pop() ); };
return count;
}
Template class design for singly linked list
• Testing the design with a main() function that creates
stacks of at least two different types and operates on
both.
#include "stack_template_linked_new.h"

int main()
{
int i; float item;
stack<float> floatstack;
ifstream inf ("floatfile", ios::in);
ifstream inc ("charfile", ios::in);
cout << " creating a float stack " << endl;
while (inf >> item)
{ floatstack.push(item);
cout << "inserted element is : " <<
floatstack.peek () << endl;
};
Template class design for singly linked list

// now determine the size of mystack


cout << " size of floatstack is " <<
stacksize(floatstack) << endl;

// now show the contents of floatstack


cout << "stack elements are " << floatstack << endl;
while (! floatstack.isempty())
cout << "element deleted is : " << floatstack.pop()
<< endl;
Template class design for singly linked list

// another test for the template

char citem; stack<char> charstack;

cout << " creating a character stack " << endl;

while (inc >> citem)

{ charstack.push(citem);

cout << "inserted element is : " <<


charstack.peek () << endl;

};
Template class design for singly linked list

// now determine the size of mystack


cout << " size of charstack is " <<
stacksize(charstack) << endl;

// now show the contents of charstack


cout << "stack elements are " << charstack << endl;
while (! charstack.isempty())
cout << "element deleted is : " << charstack.pop()
<< endl;

}
Template class design for singly linked list
• Try testing with the input :
floatfile : 2.5 -3.1412 130.23 4.0 55
charfile : c @ :& ~ %$

• The output produced :


creating a float stack
inserted element is : 2.5
inserted element is : -3.1412
inserted element is : 130.23
inserted element is : 4
inserted element is : 55
size of floatstack is 5
stack elements are 55 4 130.23 -3.1412 2.5
Template class design for singly linked list

creating a character stack


inserted element is : c
inserted element is : @
inserted element is : :
inserted element is : &
inserted element is : ~
inserted element is : %
inserted element is : $
size of charstack is 7
Template class design for singly linked list

stack elements are $ % ~ & : @ c


element deleted is : $
element deleted is : %
element deleted is : ~
element deleted is : &
element deleted is : :
element deleted is : @
element deleted is : c
QUEUE – LINKED LIST IMPLEMENTATION
Recall that a queue organization is described as having 2
ends, (i) the front (or head) and (ii) the rear (or tail).

An item in the front of the queue may only be deleted. The


item at the front is the oldest item on the queue ( delete
operation)

An item may be inserted into the queue from the rear. The
item at the rear of a queue is the youngest item ( insert
operation)

Analogous to a stack, a queue may also be implemented in


different ways, for instance using arrays or linked lists.
QUEUE – LINKED LIST IMPLEMENTATION
We consider a linked list representation for a queue, we
may extend the scheme to work out a linked list
representation for the case of a stack.

A direct representation that uses a singly linked list with


two data members front and rear, can be designed easily
in a manner similar to that for a stack.

This involves writing member functions for a linked queue


class, isempty(), isfull(), insert(); del(); length(), display(),
etc..

However, it turns out that a queue admits a more efficient


representation.
QUEUE – LINKED LIST IMPLEMENTATION

Caution has to be exercised while traversing a circular list;


e.g., the simple function of finding the length() of such a
list may get into an infinite loop if not written properly.

A circular list is a potential candidate for being used to


realise a queue.

A circular list, usually, has only only pointer ( the beginning


of the list).

A queue, on the other hand, requires two pointers ( a front


and a rear ).
QUEUE – LINKED LIST IMPLEMENTATION

For using a circular list as a data structure, one of the


queue pointers has to be dropped. In principle, any one
of the two queue pointers may be dropped, but the
consequences are different.

Let us draw pictures to realize a queue using a circular


singly linked list. Let n is the number of elements in the
queue.
QUEUE AS A CIRCULAR LINKED LIST
The insert() operation on a circular queue with pointer to
its front takes O(n) time, because the rear has to be
located for insertion and this may need a complete
traversal of the data structure.
Consider a circular queue with pointer to its rear element.
Performing both the operations insert() and delete() on
such a queue can be shown pictorially.

Both operations take constant time ( require 2 or 3 pointer


accesses which is independent of the number of elements
in the queue).

The crucial observation which makes this possible is the


fact in a circular queue, the next node of the “rear” is the
"front".
Class Design for a Queue as a circular
linked list
The member functions and their definitions are given
below.
// file queue_circular_linked.h
// uses a circular list to implement a queue
#include <string>
typedef string item_type;
class queue {
class node { public :
item_type val;
node * next;
};
node * rear; // only data member
void error ( string s) { cout << s << endl; };
Class Design for a Queue as a circular
linked list
public :
queue() { rear = 0; } // constructor

void insert (item_type item)


{
node *p = new node;
if ( p == 0 ) error ( " no space on heap ");
else { p->val = item; p->next = p;
if ( rear == 0 ) rear = p;
else
{ p->next = rear->next; rear->next = p; rear
= p; };
};
}
Class Design for a Queue as a circular
linked list
item_type del ()
{
item_type elm ;
if ( isempty() ) error("queue underflow -it is
empty");
else
{ elm = (rear->next)->val;
// check for single element Q
if ( rear->next == (rear->next)->next) rear = 0;
else rear->next = (rear->next)->next;
};
return elm;
}
Class Design for a Queue as a circular
linked list
bool isempty () { return rear == 0; }
bool isfull () { return false; }

int length ( ) // support member function


{ int len = 0;
if ( rear == 0 ) return len;
else
{ node * p = rear->next;
len ++;
while ( p != rear){ len++; p = p->next;};
return len;
}
}
}; // end of circular list implementation of class queue
Class Design for a Queue as a circular
linked list
A main() function to use the class design for the queue
given above.
/ file queue_circular_use.C
#include <iostream>
#include "queue_circular_linked.h" // use this
//for linked list versio
#include <string>
int main()
{
item_type item;
queue q1;
cout << "Give the items to be queued : ";
Class Design for a Queue as a circular
linked list
while ( cin >> item )
{ if ( q1.isfull() ) { cout << " Q is full " << endl; break;};
q1.insert(item); cout << " \n inserted " << item <<
" in Q " << endl;
};

cout << " \n ******* testing length after insertion


************ \n \n";

cout << "length of q1 = " << q1.length() << endl;

cout << " \n ************ testing deletion


************ \n \n";
Class Design for a Queue as a circular
linked list
while (! q1.isempty())
{ display ("deleted element is : ", q1.del());

cout << " \n ******** testing length after deletion


******** \n \n";

cout << "length of q1 = " << q1.length() << endl;


};

// ********* end of file queue_use.C **************


Class Design for a Queue as a circular
linked list
Sample input and Output :

Input strings :
Gaurav Pankaj Apurv Piyush Pratay Shivam Ashish

Output produced :
Give the items to be queued :
inserted Gaurav in Q
inserted Pankaj in Q
inserted Apurv in Q
inserted Piyush in Q
inserted Pratay in Q
inserted Shivam in Q
inserted Ashish in Q
Class Design for a Queue as a circular
linked list
******* testing length after insertion ************
length of q1 = 7

************ testing deletion ************


deleted element is : Gaurav

******** testing length after deletion ********


length of q1 = 6
deleted element is : Pankaj

******** testing length after deletion ********


length of q1 = 5
deleted element is : Apurv
Class Design for a Queue as a circular
linked list

******** testing length after deletion ********


length of q1 = 4
deleted element is : Piyush

******** testing length after deletion ********


length of q1 = 3
deleted element is : Pratay

******** testing length after deletion ********


length of q1 = 2
deleted element is : Shivam
Class Design for a Queue as a circular
linked list

******** testing length after deletion ********


length of q1 = 1
deleted element is : Ashish

******** testing length after deletion ********


length of q1 = 0

A template class design for a circular queue can be


developed along the lines outlined for a linked stack and
is left as an exercise for the participants.
****** End of Lecture 2******

Вам также может понравиться