CS 311 Fall 2007
Midterm Exam Review Problems, Part 1
This is the first of three sets of review problems for the midterm exam.
For all three sets, see
the class web page,
or the following links:
Midterm Exam Information
The midterm exam will be given in class on Wednesday, October 24.
It will be worth 75 points.
The exam will cover all the material of the class from the beginning
through Monday, October 22.
Referring to books, notes, electronic devices, or other people
during the exam will not be permitted.
Problems
Review problems are given below.
Answers are in the
Answers section of this document.
Do not turn these in.
- When we define a (non-templated) class, we generally
use multiple files.
Indicate what each of these files is called and what it is for.
- Your instructor has been known to remark that, since we have entered the age of “generic programming”,
the fact that certain data structures and algorithms may be complex and difficult to implement is no longer a good reason to avoid them.
What is he talking about?
- What is “abstraction” and why do we use it?
- What is an “invariant”?
- Briefly explain the basics of “Design by Contract”.
(Hint: pre & post.)
- Write reasonable preconditions and postconditions for the following function:
double myFunc(int * p)
{
p[0] = 5;
p[2] = int(std::sqrt(p[1]));
return 6.2;
}
- What is a “class invariant”?
- How can the use of well chosen class invariants make coding easier?
- In terms of class invariants, what is a constructor for?
- Write reasonable class invariants for the following class,
which maintains an array whose size is stored in the data member n_.
Hint: The destructor is already written.
Write your invariants so that the destructor is correctly written.
class MyClass {
public:
// Ctors, dctor
// Default ctor & ctor from int
// Pre:
// theSize > 0
// Post:
// n_ == theSize
// May throw std::bad_alloc
explicit MyClass(int theSize = 10):
p_(new int[theSize]),
n_(theSize)
{}
// Dctor
// Pre: None.
// Post: None.
// Does not throw
~MyClass()
{ delete [] p_; }
private:
// No copy ctor, copy assn
MyClass(const MyClass & other);
MyClass & operator=(const MyClass & rhs);
public:
// General public functions
[Various functions go here]
private:
// Data members
int * p_;
int n_;
};
- List the three ways of passing a parameter (or return value)
in C++.
For each, indicate:
- Whether the method makes a copy of the object passed.
- Whether the method allows passing of const objects.
- Whether the method supports polymorphism & virtual dispatch.
- Whether the method allows implicit type conversions to be performed.
- Suppose you are writing a numeric class MyNum,
which should act something like the built-in type double.
You have already written operator*=.
Write binary operator* for this class.
You may assume that MyNum has a copy constructor
(but you should only use it when necessary).
Include pre- and postconditions.
- List the four primary member functions that C++ compilers
will silently write.
For each, indicate when the compiler writes it for you.
(Don’t worry about the address-of operator.)
- For each of the following operators, indicate whether we should
declare it as a member or a global function and how the parameter(s) should be passed
(by value, etc.). Briefly justify your answers.
- operator+=.
- Binary operator+.
- operator<< (for stream output).
- What are the “Big Three”?
- What is the “Law of the Big Three”?
- In what circumstances do we usually need to write the Big Three?
- If we do not write the Big Three, then who does?
- What does it mean to “own” a resource?
- What does “RAII” stand for, and what is it?
- Give two examples of resources what it means to release each one.
- How can ownership of a resource be shared?
- Suppose you are writing a class Widget
that has two data members, a bool called valid_
and an int * called p_.
If valid_ is true, then p_ points to memory,
allocated with new [], which the object owns.
If valid_ is false, then the value of p_ is unknown.
Write the destructor for class Widget.
Include preconditions and postconditions.
You may assume that the information above is already in the class invariants.
- In C++ programming we typically access containers, and ranges of data items,
through what kind of object?
- What operations are typically defined on the objects from part a?
- In “modern” C++ programming,
how do we usually pass a list of values to a function?
- What is an “iterator”?
- List some things we use iterators for.
- Suppose we have a variables v1 and v2,
declared as follows:
const std::vector<int> v1(SIZE);
std::vector<int> v2(SIZE);
- Write an iterator-based loop that prints the items in v1,
each on a separate line.
- Using one of the Standard Template Library algorithms,
write a single function call that
copies the data in v1 to v2.
Note that they are already the same size.
- What do we mean by an “error condition” (often just “error”)?
- List three ways of signalling an error.
Answers
- We define a class using:
- A header file.
Typical filename suffix: .h.
Contains the declaration of the class and associated global functions, with definitions of inline functions.
Intended to be included by source files that use the class.
- A source file.
Typical filename suffix: .cpp (or .c++, etc.).
Contains definitions of member functions and associated global functions.
Includes the header file.
Intended to be compiled separately.
- Generic programming means writing code for data structures and algorithms
so that it can be used with a variety of types.
(C++ templates and the Standard Template Library facilitate generic programming.)
In the bad old days, no one could write an implementation that would work in all situations,
so if you wanted a fancy data structure that was appropriate for your needs,
you had to write it yourself.
Now, with generic programming, high quality implementations of good data structures
and algorithms can be made available for use in many situations.
Thus, first, it becomes worth the trouble to implement complex data structures
and algorithms.
Second, good implementations are more likely to be available.
Therefore, complexity of implementation is no longer an excuse
for failing to use the best data structures and algorithms.
- Abstraction means separating the
purpose and interface of a module from its implementation.
Abstraction has a number of advantages,
most of which involve the fact that, when we use abstraction,
clients typically do not deal with the implementation at all.
Thus, the implementation can be changed (bug fixes, etc.) without breaking client code.
Furthermore, implementation decisions can be made without consulting everyone involved.
- An invariant is a condition that is always true at a particular point in a program.
- In Design by Contract,
functions have “preconditions” and “postconditions”.
A precondition is a statement that must be true when the function is called.
A postcondition is a statement that will be true when the function returns
(assuming the preconditions were true when it was called).
Thus, the operation of a function is described as a contract:
the function says to its caller,
“I will fulfill my postconditions if you fulfill my preconditions.”
- Preconditions
- p must point to an array of at least 3 int’s.
- p[1] >= 0.
Postconditions
- p[0] == 5.
- p[1] is unchanged.
- p[2] == int(sqrt(p[1])).
- Return value == 6.2.
- A class invariant is an invariant that holds for an object of the class outside of public member functions.
That is, it is a precondition of all public member functions, except constructors,
and it is a postcondition of all public member functions except the destructor.
Generally class invariants are statements about a class’s data members
that tell what it means for an object to be in a valid (usable) state.
- The existence of class invariants means that public functions can make certain assumptions
(usually about class data) when they are called.
And it is always easier to code when you know something about the environment your code will be executed in.
- The job of a constructor is to make sure that all class invariants hold for the constructed object.
- Class invariants:
- n_ > 0.
- p_ points to memory sufficient to hold n_ int’s, allocated with new [].
- *this owns the memory pointed to by p_.
- The three methods are passing by value, by reference, and by reference-to-const.
Passing by value:
- Makes a copy.
- Allows passing of const objects.
- Does not support polymorphism.
- Allows implicit type conversions.
Passing by reference:
- Does not make a copy.
- Does not allow passing of const objects.
- Supports polymorphism.
- Does not allow implicit type conversions.
Passing by reference-ro-const:
- Does not make a copy.
- Allows passing of const objects.
- Supports polymorphism.
- Allows implicit type conversions.
- operator* for class MyNum should be global:
// operator* for MyNum
// Multiplies two MyNum's
// Pre: None.
// Post: Return value is the product of a, b
MyNum operator*(const MyNum & a, const MyNum & b)
{
return MyNum(a) *= b;
}
Or, if you do not like squishing everything into one line,
you could do it this way.
This might be a little slower:
MyNum operator*(const MyNum & a, const MyNum & b)
{
MyNum result = a;
result *= b;
return result;
}
- Silently written member functions:
- Default constructor. Silently written when no constructor is declared.
- Copy constructor. Silently written when it is not declared.
- Copy assignment. Silently written when it is not declared.
- Destructor. Silently written when it is not declared.
- We generally want operator+= to be a member function.
First, we have no reason to make it a global.
Second, this operator is tightly tied to its first argument (which it generally modifies), and provides basic
functionality (operator+ is usually written to use operator+=).
Its parameter (the second argument) should generally be passed by reference-to-const.
This ensures that it can be called with a const object, avoids making an unnecessary copy,
properly handles derived types and virtual function calls,
and allows for implicit type conversions, where defined.
This operator should generally be something like this:
Foo & Foo::operator+=(const Foo & other);
- We generally want binary operator+ to be a global function,
to allow for implicit type conversions on both arguments.
Its parameters should generally be passed by reference-to-const,
for the same reasons as in part a.
This operator should generally be something like this:
Foo operator+(const Foo & a,
const Foo & b);
- We must declare the stream output operator as a global function;
it cannot be a member, because it would have to be a member of std::ostream,
and we are not allowed to create new members of this class.
Its first parameter (the stream) should be passed by reference, since we modify it.
Its second parameter (the value to be output) should be passed by reference-to-const
for the same reasons as in part a.
This operator should generally be something like this:
std::ostream & operator<<(std::ostream & theStream,
const Foo & printMe);
- The Big Three are the copy constructor, copy assignment, and destructor
member functions in a C++ class.
- The Law of the Big Three says that if you write one of the Big Three,
then you probably need to write them all.
- We generally need to write the Big Three when our class owns a resource.
The most common such resource is dynamically allocated memory/objects.
- The Big Three are silently written by the compiler, if we do not declare them.
- To own a resource means to be responsible for it,
in particular, for releasing it.
- RAII stands for “Resource Acquisition is Initialization”.
It refers (somewhat misleadingly) to the idea that a resource should be owned
by an object, and therefore released by the object’s destructor.
- Dynamically allocated memory. deleteing a pointer to it.
- An open file. Closing it.
Other answers are possible.
- The usual way to share resource ownership is by reference counting.
A reference count keeps track of the number of owners of a resource.
It is incremented when ownership is granted to a new object, and decremented
when an object relinquishes ownership.
When the reference count reaches zero, the resource is released.
// Dctor
// Pre: None.
// Post: None.
~Widget()
{
if (valid_)
delete [] p_;
}
The destructor has preconditions, of course,
but they are all class invariants, and so do not need to be given here.
The only postcondition is that the memory was deallocated, and this
is implied by ownership of the memory, which should be a class invariant.
Thus, postconditions do not need to be given here either
(although you may want to give them, just to be helpful).
- In C++ programming, we typically access containers using iterators.
- Typically, we define at least the following operations on iterators:
- Assignment.
- Equality/inequality test.
- Increment.
- Dereference.
- We pass a list of values by passing two iterators:
one pointing to the first item in the list,
and another pointing just past the last item in the list.
- An iterator is an object that acts like a pointer.
In particular, it is a pointer-like object that is used to access items in some data set,
and which does not own what it points to.
- We use iterators to iterate over the items in a container (thus the name).
We also use them to specify ranges of data.
- We use the usual iterator-based loop.
We must use a const_iterator, since v1 is const.
for (std::vector<int>::const_iterator citer = v1.begin();
citer != v1.end();
++citer)
{
std::cout << *citer << std::endl; // Must #include <iostream>
}
We could also declare the iterator outside the loop.
std::vector<int>::const_iterator citer;
for (citer = v.begin();
citer != v.end();
++citer)
{
std::cout << *citer << std::endl;
}
- We can copy using std::copy.
std::copy(v1.begin(), v1.end(), v2.begin()); // Must #include <algorithm>
- An error condition is a condition that occurs at runtime;
it is something that cannot be handled by the normal flow of execution.
Alternate Answer: An error condition exists when an operation fails to complete successfully.
- We can signal an error in three ways:
- Using a return code.
- Setting a flag that can be checked later.
- Throwing an exception.