Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

08 Pointers

Download as ppt, pdf, or txt
Download as ppt, pdf, or txt
You are on page 1of 79

Read (§8.4 & §8.

5)
8. Part I: Run-Time Arrays — Intro. to
Pointers
For declarations like
double doubleVar;
char charVar = 'A';
int intVar = 1234;
the compiler constructs the
object being declared, which
means that it:
1. Allocates memory needed 1234
for that A
type of object
2.
3. Associates the memory
Initializes that object's
name with
that memory

1
Addresses
& is the address operator in C++
&variable is the address (actual memory location) of variable

Example: For the previous scenario:


Values of &intVar, &charVar, and &doubleVal
  
0x1220, 0x1224, and 0x1225
To make addresses more useful, C++ provides pointer
variables.
Definition: A pointer variable (or simply pointer) is a variable
whose
value is a memory address.
Type * pointerVariable
Declarations:
declares a variable named pointerVariable
that can store the address of an object of type Type.
2
#include <iostream>
using namespace std;
int main()
{
int i = 11, j = 22;
double d = 3.3, e = 4.4;
// pointer variables that:
int * iptr, * jptr; // store addresses of ints
double * dptr, * eptr; // store addresses of doubles

iptr = &i; // value of iptr is address of i


jptr = &j; // value of jptr is address of j
dptr = &d; // value of dptr is address of d
eptr = &e; // value of eptr is address of e

cout << "&i = " << iptr << endl


<< "&j = " << jptr << endl
<< "&d = " << dptr << endl Output produced:
<< "&e = " << eptr << endl; &i = 0x7fffb7f4
} &j = 0x7fffb7f0
&d = 0x7fffb7e8
&e = 0x7fffb7e0 3
Dereferencing
* is the dereferencing (or indirection)
operator
It can be used to access the value stored in a
location.
*pointervariable
For an expression of the form
the value produced is not the address stored in
pointerVariable but is instead
the value stored in memory at that address.
3.3

*dptr
Value of dptr: 0x7fffb7e8
Value of *dptr: 3.3
We say dptr points to that memory
4
location (whose address is 0x7fffb7e8)
Suppose we replace the preceding output
statements by:
cout << "i = " << *iptr << endl
<< "j = " << *jptr << endl
<< "d = " << *dptr << endl
(*iptr)++;
<< "e = " << *eptr << endl;
cout << "i = " << i << endl;

Output produced:
i = 11
j = 22
d = 3.3
i
e = 12
4.4

5
A Note About Reference Parameters:
Recall the C++ function to exchange the values of two int
variables:
void Swap(int & a, int & b)
{ int temp = a; a = b; b = temp; }
The values of two int variables x and y can be exchanged with
the call:
Swap(x,y);
The first C++ compilers were just preprocessors that read a C+
+ program, produced functionally equivalent C code, and ran it
through the C compiler. But C has no reference parameters.
How were they handled?
Translate the function to
void Swap(int * a, int * b)
{ int temp = *a; *a = *b; *b = temp; }
 A reference parameter is a variable containing
and
the the preceding call to: Swap(&x, &y);
6
address of its argument (i.e., it's a pointer
This indicates how the call-by-reference parameter mechanism
Anonymous variables
Definitions:
A variable is a memory location.
A named variable has a name associated with its
memory location, so that this memory location can
be accessed conveniently.
An anonymous variable has no name associated
with its memory location; if the address of that
memory location
is stored in a pointer variable, then the variable
can be accessed indirectly using the pointer.

7
Named variables are created using a normal variable
declaration.
For example, the declaration
int j = 22;
i. constructed an integer (4-byte) variable at memory
address 0x7fffb7f0 and initialized those 4
bytes to the value 22; and
ii. associated the name j with that address, so that all
subsequent uses of j refer to address 0x7fffb7f0 ;
the
statement
cout << j << endl;
will display the 4-byte value (22) at address 8
0x7fffb7f0 .
new operator
Anonymous variables are usually created using the
new operator, whose form is:

new Type

When executed, this expression:


i. allocates a block of memory big enough for
object of type Type,
and
ii. returns the starting address of that block of
memory.
Type * ptr; ptr ?
ptr = new Type; ptr Memory
for Type
value 9
Example:
#include <iostream>
using namespace std;
int main()
{
double * dptr,
* eptr;
dptr = new double;
eptr = new double;
cout << "Enter two numbers: ";
cin >> *dptr >> *eptr;
cout << *dptr << " + " << *eptr
<< " = " << *dptr + *eptr << endl;
}
Sample run:
Enter two numbers: 2.2 3.3
2.2 + 3.3 = 5.5 10
The program uses the new operator to allocate two
anonymous variables whose addresses are stored in
pointer variables dptr and eptr:
anonymous
dptr variables
eptr
Note 1: We could have performed these allocations as
initializations in the declarations of dptr and eptr:
double * dptr = new double,
* eptr = new double;

The input values are then stored in these indirectly


via the pointers dptr and eptr:
dptr 2.2 *dptr

eptr 3.3 *eptr 11


Note 2: new must be used each time a memory allocation is
needed.
double *dptr, *eptr;
dptr = eptr = new double;

eptr = new double allocates memory for a double and


assigns its address to eptr.

dptr = eptr simply assigns this same address to dptr; it


does not allocate new memory.

*dptr = 3.14159;
*eptr = 0.1423;
cout << *dptr << " " << *eptr << endl;
dptr "Alias problem"
What’s
0.1423 output?
0.1423
eptr
12
Note 3: What if we changed this to:
double *dptr, *eptr;
dptr = new double;
*dptr = 3.14159;
*eptr = 0.1423;

The first assignment is okay and stores 3.14159 in the memory


location pointed to by eptr.

However, the second results in the most dreaded of all errors:


An attempt to dereference an undefined pointer
variable (i.e., one to which no memory address has
been assigned) produces
a segmentation fault.
Note 4: It is an error to attempt to allocate the wrong type of
memory block to a pointer variable; for example,
double dptr = new int; // error
produces a compiler error. 13
Run-time arrays
Recall that arrays as we know them have their capacities fixed
at compile time. For example, array a declared by
double a[50];
has exactly 50 elements.
This is adequate if a fixed-capacity array can be used to store all
of the data sets being processed. However, this is often not true
because the sizes of data sets vary. In this case, we must
either:
— Make array's capacity large enough to handle biggest data
set — an obvious waste of memory for smaller data sets.
or
— Change capacity in array's declaration in source code and
recompile
(yuck!).
It would be nice if the user could specify the capacity of the
array at
14
run time and an array of that capacity would then be allocated
Allocating an Array During Run-Time:
The operator new can be used in an expression of the form

new Type[n]

where n is an integer expression, to allocate an array with


n elements, each of type Type; it returns the base address
of that array.
n-1

This allocation occurs when this expression is executed, that


is, at
run-time, not at compile-time. This means that the user
can input a capacity, and the program can allocate an array
with 15
exactly that many elements!
The address returned by new must be assigned it to a pointer of
type Type.
So a declaration of a run-time-allocated array is simply a pointer
declaration: Type * arrayPtr;

int numItems;
double dub[20]; // an ordinary compile-time array
double *dubPtr; // a pointer to a (run-time) array
cout << "How many numbers to process? "; cin >> numItems;
dubPtr = new double[numItems];

Recall: The value of an ordinary array like dub is the base


address of the array. So, in a subscript expression like
dub[i] ( same as operator[](dub, i))
the subscript operator actually takes two operands: the base
address of the array and an integer index. Since pointer
variable dubPtr also is the base address of an array, it can be
used in the same manner as an array name:
dubPtr[i] ( same as operator[](dubPtr, i))
16
Example: for (int i = 0; i < numItems; i++)
Memory Allo-/Deallo-cation
new receives its memory allocation from pool
Run-Time
of available memory called the heap or free Stack
store. It is usually located between a program
and its run-time stack.

The run-time stack grows each time a function is Heap


called, so it is possible for it to overun the heap Program
(if main() calls a function that calls a function statements
that calls a function ... e.g., due to infinite or too
deeply nested recursion) It is also possible for
the heap to overun the run-time stack due to too
many (or too large) new operations.

17
When the heap has been exhausted, new returns the value 0 (called the
null address or null pointer).
Common to picture a null pointer variable using the electrical engineering
ground symbol:

dptr

Attempting to dereference a null (or uninitialized or void) pointer variable


produces a segmentation fault.

So check whether a pointer is null before attempting to dereference it.


double *dptr = new double;…
if (dptr == 0) // check if pointer has null value
{
cerr << "\n*** No more memory!\n";
exit(1);
}
or use an assertion: (Remember to #include <cassert>)
assert(dptr != 0); 18
delete operator
Run-time stack grows each time a function is called, and shrinks
again when that function terminates.
Need an analogous method to reclaim memory allocated by new, i.e.
shrink the heap when an anonymous variable is no longer needed.
Otherwise a memory leak (memory allocated but not usable) results.
For this, C++ provides the delete operation:

delete pointerVariable;

which deallocates the block of memory


whose address is stored in pointerVariable

19
For run-time arrays, to return to the heap memory allocated to array pointed to
by arrayPtr. use the delete operator in the form

delete[] arrayPtr;
Important to do this because memory leaks involving arrays can result in
considerable loss of memory.
For example:
for(;;)
{
int n;
cout << "Size of array (0 to stop): ";
cin >> n;
if (n == 0) break;
double * arrayPtr = new double[n];
// process arrayPtr
. . .
}
Each new allocation of memory to arrayPtr maroons old memory block
20
new and delete for Class
Objects
If Type is a class, new Type will
i. Allocate memory
ii. Call Type's constructor to create a value
to use to initialize this memory block.
new can also be used in the form
new Type(initVal)
to call an explicit-value constructor.
int * ptr1 = new int; // integer allocated
*ptr1 = 103; // anonymous integer initialized
int * ptr2; // no integer allocated
ptr2 = new int(104); // anonymous integer allocated
// and initialized
If Type is a class, delete Type will
i. Call Type's destructor to destroy the object
in this memory block.
ii. Deallocate the memory 21
Run-time Allo-/Deallo-cation in a
Class
Classes that use run-time allocated storage require some new
function members and modifications of others:
1.Destructors: To "tear down" the storage structure and
deallocate its memory.
2.Copy constructors: To make copies of objects
(e.g., value parameters)
3. Assignment: To assign one storage structure to another.

We will illustrate these using our Stack class.

22
Data Members
We will use a run-time allocated array so that the user can specify the capacity
of the stack during run time. We simply change the declaration of the
myArray member to a pointer and STACK_CAPACITY to a variable; to avoid
confusion, we will use different names for the data members.

//***** RTStack.h *****


. . .
template <typename StackElement>
class Stack
{
/***** Function Members *****/
public:
. . .
/***** Data Members*****/
private:
int myCapacity_, // capacity of stack
myTop_; // top of stack
StackElement * myArrayPtr; // run-time array to store
elements
}; 23
Class Constructors
Want to permit declarations such as
Stack<int> s1, s2(n);
to construct s1 as a stack with some default capacity,
to construct s2 as a stack with capacity n.
To permit both forms, we use a constructor with a default argument.
/* --- Class constructor ---
Precondition: A stack has been defined.
Receive: Integer numElements > 0; (default = 128)
Postcondition: stack constructed with capacity
numElements
*/

Stack(int numElements = 128);

This constructor must really construct something


(and not just initialize data members):

24
template <typename StackElement>
Stack<StackElement>::Stack(int numElements)
{
assert (numElements > 0); // check precondition
myCapacity_ = numElements; // set stack capacity
// allocate array of this capacity
myArrayPtr = new StackElement[myCapacity_];

if (myArrayPtr == 0) // memory available?


{
cerr << "Inadequate memory to allocate stack \n";
exit(1);
} // or assert(myArrayPtr != 0);
myTop_ = -1;
}

Now a program can use:


cin >> num;
Stack<double> s1, s2(num);
s1 will be constructed as a stack with capacity 128 and
25
s2 will be constructed as a stack with capacity num.
Other stack operations: empty, push, top, pop, output
The prototype and definition of empty() as well as the prototypes of push(),
top(), pop(), and operator<<() are the same as before (except for some name
changes). See pages 428-31.
Their definitions require accessing the elements of the array data member. As we have
noted, the subscript operator [] can be used in the same manner for run-time allocated
arrays as for ordinary arrays, and thus (except for name changes), the definitions of
these functions are the same as before; for example:
//*** Definition of push()
template <typename StackElement>
void Stack<StackElement>::push(const StackElement & value)
{
if (myTop_ < myCapacity_ - 1)
{
++myTop_;
myArrayPtr[myTop_] = value;
} // or simply, myArrayPtr[++myTop_] = value;
else
cerr << "*** Stack is full -- can't add new value ***\n";
} 26
Class Destructor
For any class object obj we have used up to now, when obj is declared, the
class constructor is called to initialize obj. When the lifetime of obj is over,
its storage is reclaimed automatically because the location of the memory
allocated is determined at compile-time.
For objects created during run-time, however, a new problem arises. To
illustrate, consider a declaration
Stack<double> st(num);
The compiler knows the data members st
myCapacity_
myCapacity_, myTop_, and myArrayPtr myTop_
of st so it can allocate memory for them:
myArrayPtr

Array to store stack elements is created by the constructor; so


memory for it isn't allocated until run-time:
st
myCapacity_
myTop_ num-1
0 1 2 3 4
myArrayPtr 27
When the lifetime of st ends, the memory allocated to myCapacity_,
myTop_, and myArrayPtr is automatically reclaimed, but not for the run-time
allocated array:
0 1 2 3 4 . . . num-1
Marooned!

We must add a destructor member function to the class to avoid this memory
leak.
 Destructor's role: Deallocate memory allocated at run-time
(the opposite of the constructor's role).
 At any point in a program where an object goes out of scope,
the compiler inserts a call to this destructor.
That is:

When an object's lifetime is over,


its destructor is called first.
28
Form of destructor:
 Name is the class name preceded by a tilde (~).

 It has no arguments or return type.

For our Stack class, we use the delete operation to deallocate


the run-time array.

/* --- Class destructor ---


Precondition: Lifetime of Stack containing this
function should end.
Postcondition: The run-time array in the Stack containing
this function has been deallocated.
--------------------------------------------------------*/

~Stack();
// Following class declaration
// Definition of destructor
template <typename StackElement>
inline Stack<StackElement>::~Stack()
{
delete[] myArrayPtr; 29
}
Suppose st is
st
myCapacity_ 5
myTop_ 2
0 1 2 3 4
myArrayPtr a b c

When st's lifetime is over, st.~Stack() will be


called first, which produces
st
myCapacity_ 5
myTop_ 2
myArrayPtr

Memory allocated to st — myCapacity_, myTop_, and


myaArrayptr — will then be reclaimed in the usual
manner. 30
Class Copy Constructor

Is needed whenever a copy of a class object must be built,


which occurs:
 When a class object is passed as a value parameter

 When a function returns a class object

 If temporary storage of a class object is needed

 In initializations

If a class has no copy constructor, the compiler uses a default


copy
constructor that does a byte-by-byte copy of the object.

This has been adequate for classes without dynamically


allocated data, but is not adequate for classes containing
pointers to run-time allocated arrays (or other structures) . It
gives rise to the aliasing problem.
31
Example: A byte-by-byte copying of st to produce a copy
stCopy gives st
myCapacity_ 5
myTop_ 2 0 1 2 3 4
myArrayPtr a b c

stCopy myCapacity_ 5
myTop_ 2
myArrayPtr

Not correct — copies of myCapacity_, myTop_, and myArrayPtr


were made, but not a copy of the run-time allocated array.
Modifying stCopy will modify st also! Need to create a distinct
copy of st, in which the array in stCopy has exactly the same
elements as
st the array in st:
myCapacity_ 5
myTop_ 2 0 1 2 3 4
myArrayPtr a b c

stCopy myCapacity_ 5
myTop_ 2 0 1 2 3 4 32
myArrayPtr a b c
Form of copy constructor:
 It is a constructor so it must be a function member, its name is
the class name, and it has no return type.
 It needs a single parameter (source of copy) whose type is
the class; this must be a reference parameter and should be
const since it does not change this parameter or pass
information
back through
(Otherwise it. be a value parameter, and since a value
it would
parameter is a copy of its argument, a call to the copy instructor
will try and copy its argument, which calls the copy constructor,
which will try and copy its argument, which calls the copy
constructor . . . )
/* --- Copy Constructor ---
* Precondition: A copy of a stack is needed
* Receive: The stack to be copied (as a const
* reference parameter)
* Postcondition: A copy of original has been constructed.
************************************************************/

Stack(const Stack<StackElement> & original);


33
Definition of copy constructor:
template <typename StackElement>
Stack<StackElement>::Stack(const Stack<StackElement> &
original)
{
myCapacity_ = original.myCapacity_; // copy myCapacity_
member
myTop_ = original.myTop_ ; // copy myTop_ member

myArrayPtr = new StackElement[myCapacity_];


// allocate array in copy

if (myArrayPtr == 0) // check if memory available


{
cerr << "*Inadequate memory to allocate stack ***\n";
exit(1);
}

// copy array member


for (int pos = 0; pos < myCapacity_; pos++)
myArrayPtr[pos] = original.myArrayPtr[pos];
} 34
Assignment
Another operation that requires special attention for classes
containing pointers to run-time arrays (or other structures). Like the
copy constructor, the default assignment operation does byte-by-
byte copying. With it, the assignment statement
s2Copy = s2;
will yield aliases as before; the myArrayPtr data members of both
s2 and s2Copy will both point to the same anonymous array.

Need to overload the assignment operator (operator=) so that it


creates a distinct copy of the stack being assigned.

operator= must be a member function. So an assignment


stLeft = stRight;
will be translated by the compiler as
stLeft.operator=(stRight);

35
Prototype:

/* --- Assignment Operator ---


* Receive: Stack stRight (the right side of the assignment
* operator
* Postcondition: The Stack will be a copy of stRight
* Return: A reference to the current Stack
************************************************************/
Stack<StackElement> & operator=
(const Stack<StackElement> & original);

The return type is a reference to a Stack since


operator=() must return the object on the left side of the
assignment and not a copy of it (to make chaining
possible).

36
Definition of operator=:
It is quite similar to that for the copy constructor, but there are
some differences:
1. Object on the left side of the assignment may already have a
value. Must
destroy old value — deallocate the old so no memory leak

and allocate a new one.
2. Assignment must be concerned with self-assignments: st =
st;.
Can't destroy the old value in this case.
Every
3. member function
operator=() of a the
must return class has access
Stack to a this
containing (hidden)
function.
pointer constant
For #3 we use the following
this property of classes:
whose value is the address of the object containing this
function.
The expression
*this 37
refers to the object itself.
template <typename StackElement>
Stack<StackElement> & Stack<StackElement>::operator=
(const Stack<StackElement> & original)
{
if ( this != &original ) // check that not st = st
{
delete[] myArrayPtr; // destroy previous array

myArrayPtr = new StackElement[myCapacity_];


if (myArrayPtr == 0) // check if memory available
Same
as copy {
con- cerr << "*** Inadequate memory ***\n";
structor exit(1);
}
myCapacity_ = original.myCapacity_;// copy myCapacity_
myTop_ = original.myTop_ ; // copy myTop_ member
// copy array member
for (int pos = 0; pos < myCapacity_; pos++)
myArrayPtr[pos] = original.myArrayPtr[pos];
}
return *this; // return reference to this object
38
}
//***** Test Driver *********** Sample Runs:
#include <iostream>
using namespace std; Enter stack capacity: 5
#include "RTStack.h" 5
Print (Stack<int> st) 4
{ 3
cout << st; 2
} 1
---------------------------
int main() Enter stack capacity: 3
{ *** Stack is full -- can't
int Size; add new value ***
cout << "Enter stack capacity: "; *** Stack is full -- can't
cin >> Size; add new value ***
3
Stack<int> S(Size); 2
for (int i = 1; i <= 5; i++) 1
S.push(i) ---------------------------
Enter stack capacity: 0
Stack<int> T = S; StackRT.cc:12: failed
cout << T << endl; assertion `NumElements > 0'
} Abort 39
See Figure 8.7 on p. 440
Test driver with statements in the
constructor, copy constructor, and destructor
to trace when they are called.

40
Read (§8.1 & §8.2)

Part II: Linked Lists


As an abstract data type,
a list is a finite sequence (possibly empty) of elements
with basic operations that vary from one application to
another.

Basic operations commonly include:

Construction: Allocate and initialize a list object


Traverse: Go through the list or a part of it, accessing
(usually empty)
and
Empty: Check if list is empty
processing the elements in the order they
are
stored
Insert: Add an item to the list at any point 41

Delete: Remove an item from the list at any point


Array/Vector-Based Implementation of a List:
Data Members: Store the list items in consecutive array
or vector locations:
a 1, a 2, a3 , ... an

 a[0] a[1] a[2] ... a[n-1] a[n] ... a[CAPACITY-1]
For an array, add a mySize member to store the length
(n) of the list.
Basic Operations
Construction: Set mySize to 0;
if run-time array, allocate memory for it

For vector: let its constructor do the work


Empty: mySize == 0
For vector: Use its empty() operation 42
Traverse:
for (int i = 0; or i = 0;
i < size; i++) while (i < size)
{ Process(a[i]); } { Process(a[i]); i++; }

Insert: Insert 6 after 5 in 3, 5, 8, 9, 10, 12, 13, 15


3, 5, 6, 8, 9, 10, 12, 13, 15
Have to shift array elements to make room.

Delete: Delete 5 from 3, 5, 6. 8, 9, 10, 12, 13, 15

3, 6, 8, 9, 10, 12, 13, 15


Have to shift array elements to close the gap.

This implementation of lists is inefficient for dynamic lists


(those that change frequently due to insertions and deletions),
so we look for an alternative implementation. 43
Linked Lists (§8.2)
Minimal requirements:
1. Locate the first element
2. Given the location of any list element, find its successor
3. Determine if at the end of the list
For the array/vector-based implementation:
1. First element is at location 0
2. Successor of item at location i is at location i + 1
3. End is at location size – 1
A gross inefficiency is caused by #2 — insertions and deletions
requires shifting elements.
Fix: Remove requirement that list elements be stored in
consecutive locations.
But then need a "link" that connects each element to its
successor
 linked lists 44
A linked list is an ordered collection of elements called nodes
each of which has two parts: data
first 9 17
(1) Data part: Stores an element of the list
(2) Next part: Stores a link (pointer) to the next list element.next
If there is no next element, a special null value is
used.
Additionally, an external link must be maintained that points to
the location
of the
This node
value storing
will be nullthe first
if the listlist element.
is empty.

Example: A linked list storing 9, 17, 22, 26, 34:

data
first 9 17 22 26 34

next

45
Basic Operations:
Construction: first = null_value first
Empty: first == null_value?

Traverse:
ptr = first;
while (ptr != null_value)
{
Process data part of node pointed to by ptr
ptr = next part of node pointed to by ptr;
}

See pp. 391-2

46
first 9 17 22 26 34
ptr = first;
while (ptr != null_value)
{ ptr
Process data part of
node pointed to by ptr; first 9 17 22 26 34
ptr = next part of node
pointed to by ptr;
} ptr
.
.
.
first 9 17 22 26 34

ptr

first 9 17 22 26 34

ptr

47
Insert: To insert 20 after 17 in the preceding linked list
 need address of item before point of insertion
 predptr points to the node containing 17
(1) Get a new node pointed to by newptr and store 20 in it
predptr

first 9 17 22 29 34

20
newptr

(2) Set the next pointer of this new node equal to the next
pointer in its predecessor, thus making it point to its
successor. predptr

first 9 17 22 29 34

20
newptr
48
(3) Reset the next pointer of its predecessor to point to this new
node. predptr

first 9 17 22 29 34

20
newptr

Note that this also works at the end of the list.


Example: Insert a node containing 55 at the end of the
list.
(1) as before
(2) as before — sets next link to nullpredptr
pointer
(3) as before
first 9 17 20 22 29 34

55
newptr 49
Inserting at the beginning of the list requires a modification of
steps 2 and 3:
Example: Insert a node containing 5 at the beginning of the list.
(1) as before
(2) set next link to first node in the list
(3) set first to point to new node.
predptr
first 9 17 20 22 29 34 55

5
newptr

 Note: In all cases, no shifting of list elements is required !


An O(1) operation!

50
Delete: Delete node containing 22 from following list. Suppose
ptr points to the node to be deleted and predptr points to its
predecessor (the node containing 20)
predptr ptr

first 5 9 17 20 22 29 34

(1) Do a bypass operation: Set the next pointer in the


predecessor to
point to the successor of the nodeptr
predptr to be deleted

first 5 9 17 20 22 29 34

(2) Deallocate the node being deleted.


free store
predptr ptr

first 5 9 17 20 22 29 34

51
Same process works at the end of the list.
Example: Delete the node at the end of the
list.
(1) as before — sets next link to null pointer
predptr ptr free store
(2) as before
first 5 9 17 22 29 34

Deleting at the beginning of the list requires a modification of


step 1:
Example: Delete 5 from the previous list
(1) reset first
(2) predptr
as before ptr free store

first 5 9 17 22 29

Note again that in all cases, no shifting of list elements is required ! 52


An O(1) operation!
Advantanges of linked lists:
• Access any item as long as external link to first item
maintained
• Insert new item without shifting
• Delete existing item without shifting
• Can expand/contract as necessary
Disadvantages
• Overhead of links: used only internally, pure overhead
• If dynamic, must provide destructor, copy constructor
• No longer have direct access to each element of the
list
• O(1) access becomes O(n) access since we must go
through first element, and then second, and then
third, etc.

53
List-processing algorithms that require fast access to each
element cannot (usually) be done as efficiently with linked lists.

Examples: Appending a value at the end of the list:


— Array-based method:
a[size++] = value;
or for a vector:
v.push_back(value);
— For a linked list:
Get a new node; set data part = value and next part =
null_value
If list is empty
Set first to point to new node.
Else
Traverse list to find last node
Set next part of last node to point to new node.
Other examples:
Many sorting algorithms need direct access 54
Binary search needs direct access
Implementing Linked Lists
Can be done in various ways. For example, we could use
arrays/vectors (Read §8.3)

For nodes:

typedef int DataType; // type of list elements


typedef int Pointer; // pointers are array indices
struct NodeType
{
DataType data;
Pointer next;
};

55
For free store:
const int NULL_VALUE = -1;

const int numberOfNodes = 2048;


NodeType node[numberOfNodes];
Pointer free; // points to a free node
// Initialize free store
// Each node points to the next one
for (int i = 0; i < numberOfNodes - 1; i++)
node[i].next = i + 1;
node[numberOfNodes - 1].next = NULL_VALUE;
free = 0;
free node data next
0 0 1
1 2
2 3
3 4
. . . .
.
numNodes-1
.
numNodes-1 -1 56
// Maintain free store as a stack
// New operation
Pointer New()
{ Pointer p = free;
if (free != NULL_VALUE)
free = node[free].next;
else
cerr << "***Free store empty***\n";
return p;
}
// Delete operation
void Delete(Pointer p)
{
node[p].next = free;
free = p;
}

57
For the linked list operations:
Use node[p].data to access the data part of node pointed to by
p
Use node[p].next to access the next part of node pointed to by
p

Example: Traversal
Pointer p = first;
while (p != NULL_VALUE)
{
Process(node[p].data);
p = node[p].next;
}

58
Using C++ Pointers and Classes (§8.6)

To Implement Nodes
class Node
{
public:
DataType data;
Node * next;
};

Note: The definition of a Node is a recursive (or self-


referential)
definition because it uses the name Node in its
definition:
the next member is defined as a pointer to a Node .

59
How do we declare pointers, assign them, access contents of
nodes, etc.?
Declarations:
Node * ptr; or typedef Node * NodePointer;
NodePointer ptr;

Allocate and Deallocate:


ptr = new Node; delete ptr;

To access the data and next part of node:


(*ptr).data and (*ptr).next
or better, use the -> operator:
ptr->data and ptr->next

60
Why make data members public in class Node?
This class declaration will be placed inside another class
declaration for LinkedList. The data members data and next
of struct Node will be public inside the class and thus will
accessible to the member and friend functions of the class, but
they will be private outside the class.
#ifndef LINKEDLIST
#define LINKEDLIST
template <typename DataType>;
class LinkedList
{
private: So why not just make
class Node Node a struct? We could,
{ but it is common
public:
DataType data; practice to use struct for
Node * next; C-style structs that
}; contain no functions
typedef Node * NodePointer; (and we will want to add
. . . a few to our Node class.)
}; 61
#endif
Data Members for LinkedList:
Linked lists like
first 9 17 22 26 34

are characterized by:


(1) There is a pointer to the first node in the list.
(2) Each node contains a pointer to the next node in the list.
(3) The last node contains a null pointer.
We will call the kind of linked lists we've just considered simple
linked lists to distinguish them from other variations we will
consider shortly — circular, doubly-linked, lists with head
nodes, etc..
For simple linked lists, only one data member is needed: a
pointer to the first node. But, for convenience, another data
member is usually added that keeps a count of the elements of
the list: 62
L
first 9 17 22 26 34

mySize 5

Otherwise we would have to traverse the list and count


the elements each time we need to know the list's
length.
(See p. 446)
1. Set count to 0.
2. Make ptr point at the first node.
3. While ptr is not null:
a. Increment count .
b. Make ptr point at the next node.
4. Return count .

63
Function Members for LinkedList:

Constructor: Make first a null pointer and set mySize to 0.


Insert, Delete
Destructor: Why is one needed? For the same reason as for run-
time arrays.
If we don't provide one, the default destructor used by the
free
compiler for a linked list store
like that above will result in:

L
first 9 17 22 26 34
mySize 5
marooned!

64
Copy constructor: Why is one needed? For the same reason
as for
run-time arrays.
If we don't provide one, the default copy constructor (which
just does a byte-by-byte copy) used by the compiler for a linked
list like L will produce:
L
first 9 17 22 26 34

mySize 5

copyOfL
first
mySize 5

65
Linked List Variants (§9.1)

In some applications, it is convenient to keep


access to both the first node and the last node in
the list.
L
first 9 17 22 26 34

last
mySize 5

This would work nicely for a linked-list


implementation of a queue (but we'll see an
alternative later.)
66
Sometimes a head node is used so that
every node has a predecessor, which
eliminates special cases for inserting and
deleting.
first ? 9 17 22 26 34

Constructor: first = new Node; first ?


Traversals start at first->next
Inserts & deletes: No special case (first node)
The data part of the head node might be used
to store some information about the list, e.g.,
the number of values in the list, or the name of
a set to which all the values in the list
belong . . . (e.g., §9.2: Linked polynomials)
67
Not very
Sometimes a trailer node is also used so common
every
that node has a successor.

first ? 9 17 22 26 34 ?

(Two or more lists can share the same trailer


node.)

68
In other applications a circular linked list is used;
instead of the last node containing a null pointer, it
contains a pointer to the first node in the list.
For such lists,one can use a single pointer to the
last node in the list, because then one has direct
access to it and "almost-direct" access to the first
node.

last 9 17 22 26 34

This is a good implementation for a linked


queue or for any problem in which "circular"
processing is required — e.g., the Josephus
problem. 69
All of these lists, however, are uni-directional; we
can
only move
In many from one node
applications, to its successor.
bidirectional movement is
necessary. In this case, each node has two
pointers — one to its successor (null if there is
none) and one to its predecessor (null if there is
none.) Such a list is commonly called a doubly-
linked (or symmetrically-linked) list.
L prev
last
9 17 22 26 34
first
mySize 5 next

e.g., §9.4 BigInt


70
And of course, we could modify this doubly-linked
list so that both lists are circular forming a doubly-
linked ring.
L
last
9 17 22 26 34
first
mySize 5

Add a head node and we have the


implementation used in STL's list class.
Other variations: §9.5
Multiply-ordered lists
Lists of lists (LISP) 71
The STL list<T> Class Template
From
list<T> is a sequential container that is STL's
doc.
optimized for
insertion and erasure
It is implemented as aat arbitrary
circular points in
doubly-linked list
the
with sequence.
head node. prev
L
last
data 9 17 22 26 34
first
mySize next
5

Its node structure is: struct list_node


{
pointer next,
prev;
T data;
72
}
On the surface, list<T> looks quite simple. But it's allo/deallo-
cation scheme is more complex than simply using new and delete
operations.
To reduce the inefficiency of using the heap manager for large
numbers of allo/deallo-cations, it does it's own memory
management.
Basically, for each list of a certain type T:

When a node is needed:


1. If there is a node on the free list, allocate it.
(This is maintained as a linked stack in exactly the way we
described earlier.)
2. If the free list is empty:
a. Call the heap manager to allocate a block of memory
(called a
buffer) — typical size: 4K bytes.
b. Carve it up into pieces of size required for a node of a
list<T>.
When a node is deallocated:
Push it onto the free list. 73
When all lists of this type T have been destroyed:
Comparing list<T> with other containers:

Property Array vector<T> deque<T> list<T>


Direct/random access ([]) X
Sequential access 
Insert/delete at front 
Insert/delete at end 
Insert/delete in middle 
Overhead lowest low low/medium high

Note that list<T> does not support direct


access and thus does not have the subscript
operator [].
74
list<T> iterators:
list<T>'s iterator is "weaker" than that for vector<T>.
vector<T>: random access iterators
list<T>: bidirectional iterators

They have the following operations in common:


++ Move iterator to next element (like ptr = ptr-> next)
-- Move iterator to preceding element (like ptr = ptr->
prev)
* dereferencing operator (like ptr-> data)
= assignment (for same type iterators)
it1 = it2 makes it1 positioned at same element as
it2
== and != (for same type iterators)
checks whether iterators are positioned at the same
75
element
Example: Construct a list containing first 4 even
integers;
then output the list.
list<int> l;
for (int i = 1; i <= 4; i++)
l.push_back(2*i);
for (list<int>::iterator it = l.begin();
it != l.end(); it++)
cout << *it << " ";
cout << endl;
But bidirectional iterators don't have:
+, -, +=, -=, []
This means that algorithms that require iterator
"jumping"
(iterator  n) or direct access cannot be used with
list<T>s.
Example: sort()
Solution: list<T> has it's own sort() operation. 76
Function Member Description

Constructors
list<T> l; Construct l as an empty list<T>
list<T> l(n); Construct l as a list<T> to contain n elements (set to
default value)
list<T> l(n, initVal); Construct l as a list<T> to contain n copies ofinitVal
Construct l as a list<T> to contain copies of elements in
list<T> l(fPtr, lPtr); memory locations fptr up to lptr (pointers of type T * )

Copy constructor
Destructor Destroy contents, erasing all items.
~list()

l.empty() Return true if and only if l contains no values


l.size() Return the number of values l contains

l.push_back(value); Append value at l's end


l.push_front(value); Insert value in front of l's first element
l.insert(pos, value) Insert value into l at iterator position pos and return an
iterator pointing to the new element's position
l.insert(pos, n, value);
Insert n copies of value into l at iterator position pos
l.insert(pos, fPtr, lPtr);
Insert copies of all the elements in the range [fPtr, lPtr)
at iterator position pos
77
l.pop_back(); Erase l's last element
l.pop_front(); Erase l's first element
l.erase(pos); Erase the value in l at iterator position pos
l.erase(pos1, pos2); Erase the values in l from iterator positions pos1 to pos2
l.remove(value); Erase all elements in l that match value, using == to
compare items.
l.unique() Replace all repeating sequences of a single element by a single
occurrence of that element.

l.front() Return a reference to l's first element


l.back() Return a reference to l's last element

l.begin() Return an iterator positioned to l's first value


l.end() Return an iterator positioned 1 element past l's last value

l.rbegin() Return a reverse iterator positioned to l's last value


l.rend() Return a reverse iterator positioned 1 element before l's first
value
l.sort(); Sort l's elements (using <)
l.reverse(); Reverse the order of l's elements

78
l1.merge(l2);
Remove all the elements in l2 and merge them into l1; that
is, move the elements of l2 into l1 and place them so that
the final list of elements is sorted using <; (Assumes both l2
and l1 were sorted using <)
l1.splice(pos, l2); Remove all the elements in l2 and insert them into l1 at
iterator position pos
l1.splice(to, l2, from); Remove the element in l2 at iterator position from and insert
it into l1 at iterator position to
l1.splice(pos, l2,
first, last); Remove all the elements in l2 at iterator positions
[first, last)and insert them into l1 at iterator position
l1.swap(l2); pos
Swap the contents of l1 with l2

l1 = l2 Assign to l1 a copy of l2
l1 == l2 Return true if and only if l1 contains the same items as l2,
n the same order
l1 < l2 Return true if and only if l1 is lexicographically less than l2

79

You might also like