UMBC CMSC202, Computer Science II, Spring 1998,
Sections 0101, 0102, 0103, 0104 and Honors
Thursday May 7, 1998
Assigned Reading:
- A Book on C: 6.16 - 6.18, 13.1-13.11
- Programming Abstractions in C: 11.1-11.2
Handouts (available on-line):
Topics Covered:
We finish up hashing then discuss function overloading and functions as
parameters.
- First, some remarks about the declaration of ListItem as a student record.
One of the ListItem constructors has the following prototype:
ListItem(int, const char* = NULL, const char * = NULL) ;
The intention here is to construct a ListItem with the student's
social security number (ssn), name and major as parameters. (See
implementation.)
The = NULL after the second and third parameters indicates that
if the second or third parameters are missing, then the default value
NULL should be used. Thus, this constructor can be invoked in
3 ways:
ListItem(123456789, "John Smith", "CMSC") ;
ListItem(123456789, "John Smith") ;
ListItem(123456789) ;
This feature can be quite convenient and saves us from having to declare
and implement 3 different constructors.
- Another noteworthy item in the declaration of ListItem:
void copyto(ListItem&) const ;
The designation const here tells the compiler that the member
function copyto() does not change the data members of its host.
This is important because in the following ListNode constructor,
ListNode::ListNode(const ListItem& x) {
x.copyto(item) ; // item passed by reference
next = NULL ;
}
we want x to be a constant reference parameter. (Creating a
ListNode with a copy of the ListItem x doesn't change the value
of x.) However, the compiler will not let us invoke the
member function copyto() using x unless it knows
that copyto() won't change the value in x. Thus, the
const designation in the declaration of copyto() is
necessary.
- The implementation of the
HashTable class is relatively simple. The most complicated function is
the constructor. Here we first look for a prime number greater than or
equal to the parameter size. Recall that our hash function
simply takes a student's ssn divide it by the size of the table and use
the remainder as the index for the table. Choosing a prime number for
the table size tends to reduce the number of collisions. A little number
theory (Bertrand's Lemma) tells us that there is always a prime number
between size and 2 * size. One point of interest: the
HashTable class does not have a default constructor. Each time you
create a HashTable you must specify the size of the table.
- Otherwise, the HashTable member functions are straightforward.
In the Insert function for example,
void HashTable::Insert(const ListItem& x) {
int index ;
index = hash(x.ssn) ; // Hash by ssn
// Create list if no list at this index
if (Table[index] == NULL) {
Table[index] = new List ;
CrashOnNull(Table[index], "could not create new list") ;
}
count++ ;
Table[index]->Append(x) ;
}
we simply compute the hash table index, make a new list if one does not
already exist, and append the item to the list. The HashTable
constructor does not create a new list at each entry to save space.
- We test the HashTable class with two main programs. The first main program is a trivial test of
the HashTable member functions. (See sample
run.) The second program inserts
random ssn's into the hash table to test the number of collisions. The
sample run shows that the average number
of collisions is fairly predictable. With a good hash function, we can
control the average number of collisions by adjusting the size of the
table.
- Next topic: overloading functions. C++ allows you to have several
functions using the same name within the same scope, as long as the
functions can be distinguished by the number and type of their
parameters (also known as the signature of the functions). We explore
overloading functions with several programs.
- In the first program,
we have 4 functions called foo(). However, each function
has a distinct signature, so the compiler has no trouble figuring
out which one should be used by which function call.
(See sample run.)
- The second program shows
that the type of the return value of a function is not considered
part of its signature. Here we have two functions called
foo(). Both functions take two integer parameters, so
they have the same signature. One function returns an int and
the has return type void. However, in C/C++ the return value
does not have to be used, so it is impossible for the compiler to
determine which function should be used. The
sample run shows the error message
given by the compiler in such a situation.
- The third program shows
that the compiler is smart enough to determine which function should be
used even when a type conversion is required. In this program, the two
functions called foo() take either two integers or a float
and an integer. When foo() is called with an integer and a
float, the compiler calls the first function because that only involves
one type conversion (calling the second function would require two type
conversions). Similarly, when foo() is called with two floats,
there is a unique best choice of which foo() to use, so the
compiler is not confused. (See sample
run.)
- On the other hand, in the fourth
program, we have two functions named foo() which either
takes two integers or two floats. When foo() is called with an
integer and a float, the compiler cannot determine which function to use
because calling either function involves one type conversion. The
sample run shows the error message
that the compiler gives in this situation.
- Next topic: function pointers. C and C++ allows you to have
variables and parameters that hold the address of a function. For
example, if we have two functions called add3() and
add5() with the prototypes:
int add3(int n) ;
int add5(int n) ;
Since these functions have the same signature and have the same
return type, their addresses can be stored in a variable called
func declared in the following way:
int (*func)(int) ;
We can then store the address of add3() in func by saying:
func = &add3 ;
We can then call add3() using func with the simple
statement
result = func(2) ;
Since add3() adds 3 to its parameter and returns that value,
the value of result would be 5. We can similarly, have
parameters that take the address of a function. For an example, see program and sample run.
- One use of a function parameter is to call the qsort()
library function, which can sort any kind of array. (See man page for qsort.)
The only trick is that we have to provide a function to qsort()
that compares two entries of the array. Our first program to use qsort()
uses it to sort an array of int. The compare() function
has prototype:
typedef int data ;
static int compare(const data *, const data *) ;
When we use the SGI CC compiler to compile this program it complains
that a function with type int (*)(const data *, const data *)
is not compatible with the type of the parameter in the declaration of
qsort() which is int (*)(const void *, const void *)
even though void * pointers are compatible with pointers of any
type and the documentation for qsort() says so explicitly. So, we
use the g++ compiler and everything is fine. (See sample run.)
- We can easily convert this program into one that sorts doubles
instead of ints. All we have to do is change the type definition of
data. In the main program, we also have to change the call to
lrand48() into a call to drand48() because we want to
sort random doubles rather than random ints. Our second program does exactly this. The sample run shows that there is very
little difference between the two programs.
Last Modified:
22 Jul 2024 11:27:44 EDT
by
Richard Chang
Back up
to Spring 1998 CMSC 202 Section Homepage