CIS 330 Practice Final Exam Solutions


  1. Find the significant errors in the following code:
    class Array {
    public:
        Array() : len(0) { data = NULL; }
        Array(int size) : len(size) { data = new int[len]; }
        Array(const Array & a) : len(a.len) {
            data = new int[len];
            for (int i = 0; i < len; ++i) data[i] = a.data[i];
        }
    
        int length() const { return len; }
    
        // Array element access - lvalue and rvalue
        virtual int & operator [] (int i) {
            static int bucket;
            return (i >= 0 && i < len) ? data[i] : bucket;
        }
        virtual int operator [] (int i) const {
            return (i >= 0 && i < len) ? data[i] : 0;
        }
    
        // Assignment (sizes need not match)
        Array & operator = (const Array & a) {
            delete [] data; data = new int[len = a.len];
            for (int i = 0; i < len; ++i) data[i] = a.data[i];
            return *this;
        }
    
        // Utility for printing
        virtual void print(ostream & o) {
            o << "[";
            for (int i = 0; i < length(); ++i)
                o << (i == 0 ? "" : " ") << data[i];
            o << "]";
        }
    
    private:
        int *data;   // The values in the array
        int len;     // The size of the array
    };
    
    inline
    ostream & operator << (ostream & o, const Array & a) {
        a.print(o);
        return o;
    }
    
    Errors:
    • There is no destructor, so the memory allocated in the constructor will not be recovered.
    • There is no check against self assignment in the assignment operator, so the data could be deleted before the same data is copied.
    • The print method is not constant yet is called for a constant object in the ostream operator.
    • The constructor with no arguments sets a length of zero and a NULL data pointer, but none of the other code checks for this case before accessing data. This could cause problems if the assumption is that the object always has allocated memory. This is not necessarily an error, but is an error prone way of doing things.

  2. Given the declarations:
    int * (*x)(int *);   int i;
    
    1. What is the type of (*x)(&i) ?
      pointer to int
    2. What is the type of *x ?
      function returning pointer to int (and taking a pointer to int)

  3. What is the order in which constructors are called in a class inheritance hierarchy? What is the order in which the destructors are called?

    Constructors are called begining with the earliest ancestor class' constructor, down the inheritance hierarchy to the last derived class' constructor. Destructors are called in the reverse order, beginning with the last derived class' destructor.

  4. Why should a class that is going to be used as a base class define its destructor as virtual? What if the class does not need to define any code for a destructor?

    If the base class destructor is not virtual, and we have a pointer to a base that is actually the address of a derived object, only the destructor for the base will be called, leaving the work of the derived destructor undone.

  5. Describe the access control implemented by the keyword friend.

    Friends of class A are classes or functions that enjoy the same access privileges as member functions of class A. That is, they can access the private data and methods of class A. The specification of friendship is part of the interface definition of class A, so is a privilege extended by class A itself.

  6. If you do not define a copy constructor for a class that contains data fields of another class type, what happens when copy construction is needed? How about assignment? Same questions, but for one class deriving from another.

    If copy constructors or assignment is not defined, then the default action is memberwise copy construction or assignment of the data fields. This means that the copy constructors or assignment operators for the types of those fields are used. This behavior is applied recursively.
    The same behavior occurs for inheritance hierarchies - the copy constructor and/or assignment of the base class are used. Even if they are defined in the derived object, these operations are still used for the base part of the class.

  7. Write a makefile that compiles a C++ program named hello from the source files hello1.c and hello2.c. Each source file includes a common header file named hello.h.
    
    CC = CC
    
    all: hello
    
    hello: hello1.o hello2.o
    	$(CC) -o hello hello1.o hello2.o
    
    hello1.o hello2.o: hello.h
    
    hello1.o: hello1.c
    	$(CC) -c hello1.c
    
    hello2.o: hello2.c
    	$(CC) -c hello2.c
    

  8. Suppose we have this implementation of an Array of integers:
    class Array {
    public:
        Array(int size) : len(size) { data = new int[len]; }
        Array(const Array & a) : len(a.len) {
            data = new int[len];
            for (int i = 0; i < len; ++i) data[i] = a.data[i];
        }
        virtual ~Array() { delete [] data; }
    
        int length() const { return len; }
    
        // Array element access
        virtual int & operator [] (int i) {
            if (i < 0 || i >= len) throw Error("index out of bounds");
            return data[i];
        }
    
        // Assignment (sizes need not match)
        Array & operator = (const Array & a) {
            if (this == &a) return *this;
            delete [] data; data = new int[len = a.len];
            for (int i = 0; i < len; ++i) data[i] = a.data[i];
            return *this;
        }
    
        // Utility for printing
        virtual void print(ostream & o) const {
            o << "[";
            for (int i = 0; i < length(); ++i)
                o << (i == 0 ? "" : " ") << data[i];
            o << "]";
        }
    
        class Error {
        public:
            const char * const emessage;
            Error(const char * m = "") : emessage(m) { }
        };
    
    private:
        int *data;   // The values in the array
        int len;     // The size of the array
    };
    
    inline
    ostream & operator << (ostream & o, const Array & a) {
        a.print(o);
        return o;
    }
    
    And suppose you have the growable array derived from Array:
    class GrowableArray : public Array {
    public:
        GrowableArray(int size) : Array(size) { fill(); }
    
    private:
        // Grow the array and copy values
        void grow() {
            . . .
        }
    };
    
    You can assume that the method grow increases the array size by some amount. Show the code you would add to the class so that an out of range exception would not occur, but the array would grow to be the necessary size.

    Add the following implementation of int & operator [] (int) to GrowableArray:

    int & GrowableArray::operator [] (int i) {
        while(true) {
            try {
                return Array::operator[] (i);
            }
            catch (...) {
                if (i < 0) throw Error("negative index");
                grow();
                continue;
            }
        }
    }