Journal Articles

CVu Journal Vol 12, #4 - Jul 2000 + Student Code Critiques from CVu journal.
Browse in : All > Journals > CVu > 124 (22)
All > Journal Columns > Code Critique (70)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: Student Code Critique Competition

Author: Administrator

Date: 07 July 2000 13:15:38 +01:00 or Fri, 07 July 2000 13:15:38 +01:00

Summary: 

Body: 

Prizes for this competition are provided by Blackwells Bookshops in co-operation with Addison-Wesley. After I had finished the last issue, I became a little concerned that the problem code was rather tough for the average reader of C Vu. I need not have as it produced three excellent responses and I am awarding two prizes (I wish it could have been three). FG

You will find the code for this issue's competition at the end of this column. Why not make my job harder (though more rewarding) by sending in an entry this time.

Code from last issue

See last issue. If someone reminds me, I will arrange for a copy to go on our website.

The Entries

Entry 1 from Anon (No name embedded in the text file)

A quick scan of the code indicated a few practices which I consider suspect, and a closer inspection revealed errors. It seems pertinent therefore to split this critique into two parts.

Part 1 - First Impressions

From top to bottom, rather than in order of severity then.

Header Guards. As a general rule, do not start any names with underscore. Such ID's are reserved by the compiler.

Header names. On the assumption you are using a modern, reasonably C++ Standard compliant compiler, the new header names should be used:

<cassert>
<iostream>

This will also necessitate stating the std:: namespace in places where cout, endl and assert are used.

Protected data. I prefer to avoid this whenever possible. Having non-private data means at least derived classes can directly access the data, which breaks your encapsulation. In this case, however, it seems entirely redundant, since the class cannot be safely inherited from without a virtual destructor. Even in the presence of inheritance, you have accessor functions which could be made virtual, thus safely providing access.

Constructor with default arguments. I would prefer to see a "proper" default constructor, which uses the initialisation list to default the member variables, and a separate two-argument constructor with no default parameters. I will return to this point later.

Uses of const. In some places it is used, in other places (where it should be) it is not. Where it is used, it appears to be used correctly; it is its absence which should be checked. For instance, operator[] does not change the object in any way, and thus should be a const function.

Uses of assert. When compiled without debug information (i.e. production code) assert does nothing at all. The fact that this is not production code is not an excuse; it is endorsing bad practice. Some uses of assert are acceptable, such as when testing conditions internal to the class during development so that they can be removed, but all of the uses in the Array class are testing run-time conditions which could occur in the "real world." It is safer to use explicit tests (if...else) constructs. assert is commonly (always?) a macro which expands to an if statement anyway, so you are not introducing any code overhead.

From this list, only the issues with assert are potentially fatal. A constructor with any number of default arguments may lead to compile-time ambiguities, and in this case, may lead to unexpected run-time behaviour.

Part 2 - Closer Inspection

The first thing I looked into was the default template argument. The comment gave me a clue, and I closely followed the use of IType to confirm my suspicion. It is there solely to demonstrate default template arguments. Consider what would happen if a different type were used to specialise Array, e.g. a string:

Array<char, std::string> my_array;

It is not the presence of the two int data members lobound and hibound which make this fail, but their use in conjunction with the template type in the outOfRange() function. If the index type can be parameterised, then both lobound and hibound should be the same type, but this indicates an associative array. In my opinion, its existence is obscuring the code for no good reason.

I want to return to the issue of the multi-argument constructor. It should be understood that this constructor is actually four constructors in one function; it is a default constructor (no arguments), a single argument constructor, and a two argument constructor. The (very) important point that may be missed is that it is also a converting (single argument) constructor. The absence of the explicit keyword here means that a compiler is free to consider this constructor to convert silently from another type, in order to make a call to a function. This can lead to hard-to-spot runtime errors, demonstrated below:

class Bad {
public:
  Bad(const double a = 0): size_(a) { }
  double Size () const { return size_; }
private:
  double size_;
};

class Test {
public:
  void Print (const Array & index) 
          { cout << "Array" << endl; }
// should have implemented a 
// Print (int x),but forgot to
};

int main (){
  Test t;
  t.Print (10);  
// this involves converting to an double, 
// and then constructing a Bad object 
// from the result.
}

This will not only compile, but run, too. It may not, however, be what you expect, especially if class Bad is deeply nested in included files.

Entry 2 from Robert Lytton

My first impression when I saw the student code critique No.4 was that this is for the C++ gurus. However as a novice, at whom the exercise could well be aimed, my interpretation of the code may be of interest. Please note well that my observations may be incorrect, misguided or ignorant. Please do correct me so that I may learn.

As the code is part of a exercise set by an instructor I will break the critique down into four sections, what is trying to be taught, what is actually being taught, implementing the exercise and finally what skills and knowledge is being practised. I will work through the exercise in this order and what follows will act as a journal of my thoughts.

What is trying to be taught:

At present I am unsure. I am sure the instructor's pep talk and fuller instructions would have enlightened me but it is not clear from the code. I will come back to this question at the end...

What is actually being taught:

One of the best ways (but not the only way) to learn is by example. This code has presented me with an example template class. In the future, if I need to construct something similar, I may refer to this class. I may even reuse the code fragment before hacking it into its new shape.

The following is a list of the unspoken lessons taught by example. These I have judged as being good 4 or bad 8, but please do not assume my judgement to be correct.

// header: header.h

4 document the file

8 but no need to give a full description as the code is self documenting

#ifndef _MYARRAY 
#define _MYARRAY

4 use header guards to prevent multiple inclusions

4 use uppercase for preprocessor defines

8 it is standard practice to not use initial underscores in define names (initial underscores are reserved for libraries and compilers, you may use underscores else where)

#include <assert.h>

8 use the standard C library (this is depreciated, also the use of <cname> is preferred over <name.h>)

#include <iostream.h>

8 C++ library header files need the trailing .h extension (not so!)

4 use <...> instead of "..." for library header files

8 include all includes in the interface header (these should be in the implementation file to reduce the dependencies of the interface)

8 use <iostream> in header files (for interfaces that use the iostream, <iosfwd> should be used in the header files, it has enough details)

template <class BType, class IType = int>

4 make the template general

8 make the template as general as possible (I am unsure why there is an IType, it seems a little too general and unnecessary, why not just use an int? If Itype is used it will have knock on effect throughout the implementation and should not be mixed with int)

protected:

8 use protected for implementation members (these should be private to this class, in fact they could be removed from the interface by using a pimpl, pointer to an implementation class that hold them)

int lobound, hibound;

8 use implicit conversion (these variables are used to hold values of type IType and so should be declared as such, comparison and conversions with ITypes may/will cause problems!)

Array(int SZ = 16, IType lo = 0);

4 use sensible default values for when none are specified

8 using all uppercase letters for variables is fine (there are those who would only use all uppercase for preprocessor defines)

Array(const Array &x);

4 use const when passing a parameter as a reference

~Array();

4 explicit constructors paired with explicit destructor

IType lo() const; // return this->lobound

4 make member functions const if they do not alter the state of the object

8 comments that state the obvious are good (this is an implementation detail, either put the implementation inline here or hide it)

8 comments do not need to be accurate (comments if given must be kept up to date, this comment is wrong)

BType& operator[](IType i);

8 only need to define a non-const member function (you will not be able to read Array subscripts from a const object without casting)

Array<BType, IType>& operator= (const Array<Btype, IType> &x);

8 returned references need not be const (making the return type const would forces the declaration to be an rvlaue. Would this be safer here?)

8 need to restate template types (the <BType, IType> does not need to be restated for type Array. However you may state a new types for Array, that of the object to be copied, template<class BBType, classIIType> const Array& operator=( const Array<BBType, IIType>& x))

void grow(int);

8 no need to add parameter names (add them and make them helpful)

// implementation

8 it is OK to place implementation in headers (place it in its own .c. If this header is included multiple times the code will be compiled multiple times. Also note that as long as the interface in the header file stays constant, the implementation file may change with no need to compile other code. See earlier comments on #includes and pimpl classes)

template<class BType, class IType>

4 It is assumed that the implementation has been moved and therefore the template<class BType, class IType> will be required before each member definition

4 the default type, IType=int, should only be stated once, in the header

Array<BType, IType>::Array(int SZ, IType lo)

4 the default values, int SZ=16, Itype lo=0, should only be stated once, in the header

hibound(lo + SZ -1)

8 mixed type arithmetic of unknown types is fine (what if IType is a signed char? See comments on the use of IType above)

8 may use previously initialised values (it is OK here as lolound is declared before hibound but they could be swapped around, viz: int hibound, lobound; and then hibound would be initialised first)

assert(SZ > 0)

8 use assert to trap incorrect values (assert should be used as a debugging aid. This line will be removed in a release build when NDEBUG is defined and negative values will pass through the code unhindered. This comment stands for the other asserts too!)

arrayData = new BType[SZ];
assert(arrayData != 0);

8 always check the pointer is valid (and if it is not? Either new will throw an exception or return null in which case the caller must deal with it!)

(*this) = x; // use assignment

8 factor out similar code ( (*this) = x == (*this).operator=(x) == this->operator=(x) == operator=(x) but is this the way to do it? Is it safe to use this in a constructor?)

cout << "Array index " << i << endl;

4 use endl to force a '\n' and flush the stream

8 no need to state using namespace std (either state it or use std::cout and std::endl)

8 the called should report (this restricts the usefulness of the member function and reporting would be better handled, if desired, by the caller)

4 IType will handle the cout<< or we will get a compile time

errorassert (hibound-lobound 
      == x.hibound-x.lobound);

8 restrict usage (see comment above about assert, also the code could be made to handle different sized arrays)

arrayData[i] = const_cast<
    Array<BType, IType>&>(x).arrayData[i];

8 use const_cast to force read access of const objects (we should not need to use a const_cast, unless Btype does not have an operator[] const member, see comments above about operator[])

8 use interface to access implementation (in this case it seems excessive, as this is a private implementation we could directly access the data and use copy())

8 don't worry about roll back (this is not exception neutral)

There may be other subtle lessons I am being taught through this code that I am unaware of. As a novice I would welcome corrective comments. As an aside you may be interested to know that I initially learnt C from H.Schilt and may still have lessons that need unlearning.

Implementing the exercise:

These implementations try to follow the style of the code used already!

// idea 1
template <class BType, class IType>
void Array<BType, IType>::grow(int newS)
{
  assert(newS >= hibound-lobound);
// implicit conversion. 
// No checking done if NDEBUG is defined
  BType* newarrayData = new BType [newS];
  assert( newarrayData != 0); 
// no checking done if NDEBUG is defined
// similar code as operator=, hopefully
// hibound - lobound is less than INT_MAX
  for(int i = 0; i<= hibound - lobound; ++i)
    newarrayData[i] = (*this).arrayData[i];
  ~Array();        // delete old data
  arrayData=newarrayData; // insert new data
  hibound = lobound - newS + 1;// new length
  cout << "done" << endl;  
    // I just couldn't resist!
}

// idea 2
template <class BType, class IType>
void Array<BType, IType>::grow(int newS)
{
  assert(newS >= hibound-lobound);
  // implicit cast, don't add any further 
  // run time checking
  Array tempArray(newS,lobound);  
  // object of new length
  int swaphibound = tempArray.hibound; 
// keep record for later
  tempArray.hibound = hibound;  
// force to same size
  tempArray = (*this);    
// and copy data across using operator=
  BType *swaparrayData = arrayData;  
// swap the blocks over
  arrayData = tempArray.arrayData;
  tempArray.arrayData = swaparrayData;
  hibound = swaphibound;    
// and fix the extended size the old data will
// be destroyed when tempArray goes out of scope
}

N.B. In true student style, I have not tried to compile the two submissions. I think this is justifiable, the exercise would not have compiled either without alterations. (I did say the implementations would follow the style of the code!)

What skills/knowledge is being practised:

As I knew the code was poor I found myself carefully checking the syntax against the C++ Programming Language, which I found of great value. However in the context of the exercise I found myself practising the poor techniques that had been presented. The exercise could have been an opportunity to practise and become more familiar with a classic design pattern, something that I need to do. As it is I have only become familiar and worked with some very dubious methodologies.

What is trying to be taught - take2:

After working through the whole exercise I still have no answers, except, bad habits. I will now go and purify my mind by reading what C++ Programming Language has to say about templates. I really must buy Design Patterns.

Entry 3 from Oliver Wigley

In an attempt to assess the student code for the May competition, I have commented on the design and limitations of the class itself, and on any other general issues arising from the code. As an array class (especially as one called Array - suggesting an all-encompassing approach) it should ideally be written to cover a multitude of applications by implementing a simple and intuitive public interface that simulates regular arrays.

The example uses a #define directive to prevent multiple parsing of the header file at compile time:

#ifndef _MYARRAY
#define _MYARRAY

Whilst code examples frequently use identifiers and function names like myfunc(), yourfunc(), _MYCONSTANT, it is generally understood that this is not what is done in practice, since it could very quickly lead to name clashes, especially - as in this example - in the absence of the use of namspaces. After all, the only reason this #define directive exists is to test the definition of a universally unique identifier, so some effort should be made to ensure that is unique.

The template class illustrates how a default type can be used in the declaration:

template <class BType, class IType = int> 
class Array{...}

class BType is the type that is to be stored in the internal array, and class IType is the type used to access elements in the array through operator[]. There are no instances where the latter type should behave as anything other than an integral type. Subsequently, as long as appropriate conversions are available for the type you use here, the compiler will happily generate the necessary code. For example, this generic class template lets me do the following:

Array<int,char> obj(10,'k');
for(char n = obj.lo(); n<=obj.hi();++n)
  obj[n]='m';

The IType value (here, a char) passed into the constructor is used to initialise the lobound data member (an int). Its integral equivalent is returned by lo(), and is used for operator[] indexing. Access to the array becomes 'k' - based rather than 0-based.

Whilst initially this would seem to offer some kind of flexibility to the class, assumptions about the type have been made. With IType being a char, for example, I doubt if the code will behave as expected if the call to cout in the outofRange() function is ever reached:

bool Array<BType, IType>::outofrange(IType i){
  if(i<lobound || i>hibound){
    cout << "Array index " << i 
         << "is out of range" << endl;  
    return true;
  }
else return false;
}

Why anyone should be able (or even want) to do this, I could not say. It is a dubious feature of the class as it stands, and smacks of a 'doing something just because you can' approach.

The class uses a hibound / lobound paradigm for array access, which is not the expected behaviour in an array, and introduces unnecessary complexity. It also reduces encapsulation by exposing implementation details. I would much rather the IType parameter were disposed with altogether, and implement array access to resemble access of normal array types (i.e.: 0-based array access with operator[] and integral types). The IType value and lobound / hibound treatment in the class together suggest poor abstraction, resulting in an array class that looks like it is already specialised for a specific purpose, and has then simply been template-ised. Apart from anything else, it makes iterations through the list unintuitive:

const unsigned int array_size = 12;
const int lowerbounds = -4;
Array<int,int> array_obj(array_size,lowerbounds);
for(int p = 0; p< array_size; p++)
  array_obj[p] = get_input();  //error
for(p = array_obj.lo(); 
     p<=array_obj.hi(); p++)
  array_obj[p] = get_input();  //like this

As far as the classes' public interface is concerned, it should allow access just like any other array. To do this it would need an int getsize() - type function, and access would look something like this:

for( int p = 0; p < array_obj.getsize(); p++ )
  process_data( array_obj[p] );

The default constructor declaration (Array::Array(int SZ = 16, Itype lo = 0)) could be smartened in several ways. The use of the magic number 16 has no obvious foundation, and is certainly not a very generic implementation. A descriptive constant would clarify what the reasoning was here, even if it was called MyRandomDefaultValue. Also, the use of a signed value for SZ is pointless - signed ints are too frequently the default choice where unsigned ones will avoid problems and will improve readability too. A look at the constructor shows that some effort has been made to deal with this predicament (assert(SZ>0)), though the handling of this could have been more helpful. An abnormal program termination brings this to light in debug mode, and is compiled to nothing in a production compilation. Although I would have limited this argument to an unsigned int, and would also have allowed the construction of 0-length arrays anyway, exception throwing might have been a more useful way to deal with values less than 1, giving the possibility of recovery from an incorrect initialisation.

The naming convention used for the arguments (SZ and lo) is inconsistent and ambiguous. An all upper case identifier suggests a pre-processor macro or constant: size would be better for the SZ argument. Both parameters should have const qualifiers, too.

With this constructor an array with any number of elements is constructed, memory is allocated, and the default constructor for those objects is called. An array created like this is probably not going to be much use to anyone until each member has been initialised with something specific - and there is currently no way for the client code to test whether or not an array item is anything but a default object. Of course this may be exactly what you want, and is fine if everyone using the class remembers to do this, but as a generic template class it is rather suspicious (but that is the way STL containers work. FG). I would rather by default construct an empty array (getting rid of the SZ parameter altogether) whose contents are grown and initialised with specific objects that can be retrieved as something useful straight away. The limitation of not being able to increase or initialise the array with something other than a default construction in a single function call is compounded in the grow() function argument, which also forces default construction:

template <BType, IType > 
void Array::grow(int newS){/*...*/}

I would have preferred to have had an Add(const BType& item) - type function being supplied, which increases the size of the internal array by one, and initialised the object with the item passed in. Not allowing an array of 0-length to be created in the constructor is a limitation in the design, and makes the class inflexible.

The use of the new operator is inconsistent, and wrong unless we happen to be working with an old compiler. In the constructor we are given:

arrayData=new BType[SZ]; 
assert(arrayData!=0);

There is no such check in the copy constructor. In any case, the check for the null pointer being returned by new is wrong, as new is guaranteed to return something else. A std::bad_alloc object (or derivation of) is thrown (rather 'should' be thrown - my compiler throws something different) if the requested memory cannot be allocated. So if anything, try and catch blocks are needed to handle the failure. There is a compatibility issue here, since old compilers may not support this behaviour, in which case you could simulate the same effect by installing (and then uninstalling) a new-handler function (using set_new_handler()).

There is no operator[] provision for const objects. The given one should be overloaded with a const version, which would return elements by value. Methods lo() and hi() are const, so some consideration has been given to const-ness, but some functions have been overlooked. Another contender for const-ness would be OutOfRange(IType i) - which could then be called from operator[] (IType i) const (although this should really be operator[] (int i) const).

The assignment operator fails the first rule of assignment operators: there is no check for self-assignment (nor does it use the alternative idiom. FG). This takes no time to implement, and is a good thing to do. Conceptually there are two ways to approach this: either check that an object isn't being assigned to itself, or check that the two separate objects aren't to all intents and purposes the same (e.g.: string a ("hello"), b ("hello"); a = b;). I have never seen the latter type of check, and am still unconvinced about its benefits, but it may as well be mentioned since it is apparently used.

The assignment operator is also odd in that the objects are required to be the same length for assignment to be successful. This is a limitation for any array class, let alone a template class. To avoid the assertion in this function you would need to verify the sizes of the two arrays before attempting to call it. This highlights that there is also no convenient size_t length() - type member function. Presumably this is what you are expected to do with the code given:

if(a.hi()-a.lo()==b.hi()-b.lo()) {
      /*go ahead with copy*/ }
else {/*resize our object maybe*/} 

To avoid this limitation in the operator=, memory could have been allocated for the new array, initialised it accordingly, then deleted the old memory. Because there is no operator[] for const objects, const_cast is used on the source object. As it stands, the use is harmless enough. However, since we are striving for exemplar code, then we should avoid the use of const_cast. You could be forgiven for thinking if this is allowed:

arrayData[i]=const_cast<Array<BType,IType>&>
            (x).arrayData[i];

then this must also be allowed:

const_cast<Array<BType,IType>&>
    (x).arrayData[i]=arrayData[i];   //woops

The assignment operator is incomplete in that the lobound and hibound members are not re-initialised. This may be the required behaviour in the client code, but contradicts the usual expectations in of an assignment operator, and is definitely unsuitable in a generic class of any sort.

The problems the students have in finishing the grow() function are twofold: firstly, given the function name and signature - what should the function do, and secondly making the code tally with the behaviour of the rest of the class. The problem is not "What would a grow() function do in a template array class I would write?", but rather "What does the writer of this class expect the grow() function to do?". The implementation I would propose would aim to be consistent with the rest of the code, rather than try to impose a different approach. To not try and write what is expected would almost certainly break the source code that the instructor is going to compile it with.

The two possible meanings that could be read into the function prototype are:

void Array<BType, IType>::resetsize(unsigned int newsize)  /*reset the size to this value*/

or

void Array<BType, IType>::change(int growby) /*change size by this amount*/

Since the given function is

void Array<BType, IType>::grow(
                    int newS /*new size*/)

I imagine that what is expected is for the array to be reset to the new size passed in. Given the name of the function, and the rest of the code given, newS is probably anticipated to be larger than the current size of the array, so I would expect to see this early in the function:

assert( newS > hibound-lobound+1);

The current state of the array as it stands would be maintained, and default objects constructed for the extra elements:

BType* oldData = arrayData;
arrayData = new BType[newS];
for(int p=0; (p<newS) && 
          (p<(hibound-lobound+1)) ;p++)
    arrayData[p]=oldData[p];
delete [] oldData;
hibound = lobound + newS - 1 ;

Given the limitations of operator=, the lobound / hibound stuff, the magic numbers, and magic types (IType = int), I would say that this class is an already specialised array class, thinly veiled as a generic template class. As such is it a poor illustration of a truly generic array class.

Prize Winners

If Oliver and Robert like to contact me, we can discuss the issue of prizes

Congratulations to them both. Now, check their comments and make sure you agree. If you just take their word for it, you willbe neither helping yourself nor them. Remember that one of the main reasons that this column appears in this section is that it is supposed to be part of a seminar like dialgue between readers. In general, I do not comment on dubious statements in a published critique; that is your job.

Competition No 5

sponsored by Blackwells Bookshops & Addison Wesley

Rather simpler this time. I change the identifiers to English apart from one that means nothing to me. Any German readers want to tell me what it means? Your critique should focus on the code rather than just the students stated problem.

As always there will be a prize provided by Blackwell's Bookshops in collaboration with Addison-Wesley. If other booksellers and/or publishers would like to sponsor one of our columns, I would be very happy to discuss it with them.

Entries by August 14th.

class Book {
public:
  virtual void details();
  void details(char* , char *, float ); //Author, Title,Pages
  virtual void anzeigen();
  Book();     ~Book();
  Book(char*, char *, float);
  Book(const Book&);
  Book& operator=(const Book&);
  bool operator== (const Book& );
protected:
  struct book {
    char* title;
    char* author;
  float price; } b;
};

and the class:

class ChildBook: public Book {
public:
  int reading_age;
  virtual void details();
  void details(char* , char *, float ,int);
    //author, title, pages
  virtual void anzeigen();
  ChildBook();
  ChildBook(char*, char *, float, int);
  ChildBook(const ChildBook&);
  ~ChildBook();
  ChildBook& operator=(const ChildBook&);
  bool operator== (const ChildBook&);
};
Furthermore, there is the class
class BookList: public Book {
public:
  Book b;
  ChildBook k;
  list<Book> Book;
  list<ChildBook> kiBook;
  void details();
  void anzeigen();
  ~BookList();
  BookList();
  };
which also derives from Book. It should represent a kind of Library.
In the main()-function I make an instance of class BookList and assign different values to the components:
BookList bue;
//one component for Book
bue.Book.push_back(Book("Book","Title",20));
//one component for ChildBook
bue.kiBook.push_back(ChildBook("Author"
      ,"Title",35,5));   
If I walk trough the program using the debugger, I can see that the list-entry for Book is created rightly, whereas the entry for ChildBook is destroyed immediately after creation. Every time after creating, the destructor is launched - why? At the component Book it is all correctly (in spite of launching the destructor!). For the ChildBook-object it seems that the wrong entry is deleted. There are some constructors of the classes
Book::Book() {
  b.author = new char[10];
  strcpy(b.author,"Author");
  b.title = new char[10];
  strcpy(b.title,"Title");
  b.price = 0.00f;
}

Book::Book(char * t, char * a, float p) {
  b.author = new char[strlen(a)+1];
  b.title = new char[strlen(t)+1];
  strcpy(b.author,a);
  strcpy(b.title,t);
  b.price = p;
}

Book::Book(const Book& p) {
  b.title = new char[strlen(p.b.title)+1];
  strcpy(b.title,p.b.title) ;
  b.author = new char[strlen(p.b.author)+1];
  strcpy(b.author,p.b.author) ;
  b.price = p.b.price;
}
Book::~Book () {
  delete b.title;
  delete b.author;
}

ChildBook::ChildBook() {reading_age=3;}
ChildBook::~ChildBook() {  }
ChildBook::ChildBook(char * t, char * a,
       float p, int la) : Book(t,a,p){
  reading_age=la;
}

ChildBook::ChildBook(const ChildBook& k){   b=k.b;
  reading_age=k.reading_age;
}

BookList::BookList(){ }
BookList::~BookList(){ }

Notes: 

More fields may be available via dynamicdata ..