Copyright © 2016, 2022
Robert W. Hasker

Note 8: C++ with Class

Ch. 10 of textbook

Classes in C++

Concrete Types

Arrays and Classes

Inheritance, virtual methods

  • See sample Time class for basic inheritance
  • override: declare that you are overriding a method, like @override in Java
    • That is, ensures the new method does redefine some method in the base class
  • virtual: allows redefining method in subclass
    • These are often called virtual functions
    • It supports inheritance polymorphism
      • Example: a graphics image with squares, circles, etc: appling a draw operation to each would require making the draw method be virtual (in C++)
    • subclass methods must have the same argument types, return type
    • we say the subclass methods override the base class ones
  • Java: all methods are virtual
  • C++: can control which are virtual - desired because virtual methods do have overhead
  • what is the practical reason for inheritance?
        Time *submissions[20] = { new Time(10, 22),
                                  new Time(13, 45),
                                  new MilTime(13, 18),
                                  new MilTime(23, 0),
                                  new MilTime(5, 10),
                                  new Time(1, 15)
                                };
    
        ...
    
        for(int i = 0; submissions[i] != nullptr; ++i) {
          submissions[i]->print(cout);
          cout << endl;
        }
    
  • ": public" in the class header: means derived from
    • If use ": private", then base class public methods are private in subclass
    • This is never a good idea - why use inheritance if not going to follow the interface defined by the base class?
      • If pass the object to to a function that takes the base class as an argument, that function can still access those base class operations that we tried to hide!
  • Suppose we have public class Firefly extends Insect (in Java)
    • [In what order are the constructors called?]
    • These rules are the same in C++
    • [How are arguments to Insect's constructor specified?]
    • C++ version:
          class Insect {
          public:
            Insect(std::string species);
          };
      
          class Firefly : public Insect {
          public:
            Firefly(std::string species) : Insect(species) {
              // more code
            }
          };
      

Pointers and Classes

  • Issue: if we create an instance of a class with new, it stays on the heap until the program exits
  • Fix: use delete to "undo" allocating the space:
            Task *todo = new Task("eat", 0.75);
            // code to process the item
            delete todo;
    
    • Returns the space allocated for the task to the heap - available for the next new operation
    • This does not change todo:
              delete todo;
              cout << todo->description();  // dangling pointer reference!
                                                     // (random result)
      
    • Good practice, set todo to point to nothing:
              delete todo;
              todo = nullptr;
      
      so later references to todo will cause run-time errors.
  • Basic principle: a delete for every new
  • Suppose we have:
            TaskList *items = new TaskList();
            items->add(new Task("sleep", 8.0));
            items->add(new Task("eat breakfast", 0.25));
            items->add(new Task("learn something", 6.0));
            ...
            delete items;
    
    • Issue: all of the tasks on the list are still allocated
    • Fix: add a destructor to TaskList:
              class TaskList {
              ...
              public:
                 ~TaskList() {
                    for(int i = 0; i < count; ++i)
                      delete todo[i];
                 }
      
    • Now deleting the TaskList also deletes any tasks currently on the list
    • Warning: the following code is very dangerous:
              TaskList *items = new TaskList();
              Task *x = new Task("do something", 1.0);
              items->add(x);
              delete items;
              delete x;               // breaks the heap!
      
  • But Consider
        class Student {
          string name;
          string address;
          double total_grade_points, total_credits;
          Schedule *schedule;
        public:
          Student() : schedule{new Schedule},
                      total_grade_points{0.0},
                      total_credits{0.0},
                      name(""),
                      address("") 
          { }
          virtual ~Student() { delete schedule; }
          double gpa() const { return total_grade_points / total_credits; }
          virtual void add(Course *new_course);
        };
    
        class SpecialStudent : public Student {
          Certificate *cert;
        public:
          ~SpecialStudent() { delete cert; }
          void add(Course *new_course); ...
        };
    
    
    • When allocate a Student object, also allocate a Schedule object
    • delete student; - works, but doesn't reclaim the space for the schedule
      • Every new must have a corresponding delete
      • (Or the object must be able to exist until the end of the application)
    • Solution: add a "destructor":
          ~Student() { delete schedule; }
      
    • Note: add is virtual; can be redefined in subclasses
    • If add is virtual, then ~Student must be:
          virtual ~Student() { delete schedule; }
      
    • Destructor is called when exit scope as well:
          void doSomethingNice() {
            Student someStudent;
            ...
            // destructor called here
          }
      

Abstract classes

    class AbstractContainer {
    public:
      virtual double& operator[](int index) = 0;
      virtual int size() = 0;
      virtual ~AbstractContainer() { }
    };  
  • The = 0 syntax means the method is abstract or pure virtual
    • These methods must be redefined - cannot create an object of type AbstractContainer
    • All methods must be defined or get linker errors
      • Linker will say something like "unresolved symbol"
      • a class is abstract if it has at least one pure virtual method
    • C++ has no special keyword for "all pure virtual"
  • Returning double& in operator[] means we can assign as well as use the value
    • Consider the code
         void swapFirstLast(AbstractContainer &nums) {
           int last_index = nums.size() - 1;
           assert(last_index > 0); // last_index can be 0!
           double save = nums[0];
           nums[0] = nums[last_index];
           nums[last_index] = save;
         }
      
      
         ... declare xs to be some class derived from AbstractContainer
         ... and read data into the container
         // first shall be last, last shall be first:
         swapFirstLast(xs);
      
      • nums passed by reference
      • code generated by this: equivalent to returning a pointer and then dereferencing that pointer on each access:
            double* p = &nums[0];
            *p = nums[last_index];
        

Container

  • Have already seen containers in previous examples
  • from ch. 4, arbitrary-size vector with destructor: see here
    • Note return by reference: double&
    • useful as both an l-value and an r-value
    • r-value: value on the right-hand-side of an assignment - the contents
    • l-value: value on left-hand-side of assigment - the address
  • Usage:
        {
          int n; 
          cin >> n;
          FixedVector xs(n);
          ...
          {
            FixedVector ys(n * 2);
            ...
          } // ys destroyed here
          ...
        } // xs destroyed here
    
    • In general: destructor called at end of lifetime (usually, when leave scope)
    • if a class has a virtual function, the destructor must be virtual
      • Compiler will give warning if it is not virtual.
      • Deleting the object invokes the destructor of the derived class, that destructor will cause the base class destructor to execute
    • Constructors cannot be virtual - it makes no sense
    • What would happen if you made the constructor =0?
    • How would you make a singleton object in C++?
  • Resource Acquisition Is Initialization (RAII):
    • no naked new operations associated with stack-allocated objects
    • move resource management into well-behaved abstractions
    • If you move new, delete follows
    • Applies to objects without identity; if have single instance, use pointers
  • Initialization
    • Add constructor FixedVector(std::initializer_list<double>)
      • initializer_list is an STL type known to the compiler
      • This allows FixedVector v1 = {1, 2, 3, 4, 5};
      • We won't cover the details on this.

Using Abstract Classes

  • Suppose we have a really large vector: megavector.h
  • Initialize container to a sequence:
        void initializeUpSequence(AbstractContainer *container,
                                  double start, double step) {
          double value = start;
          for(int ix = 0; ix < container->size(); ++ix) {
            (*container)[ix] = value;
            value += step;
          }
        }
    
        ...
        AbstractContainer *some_numbers = new MegaVector();
        initializeUpSequence(some_numbers, 1.0, 0.1);
        double sum = 0.0;
        for(int i = 0; i < some_numbers->size(); ++i)
          sum += (*some_numbers)[i];
    
  • Note: initializeUpSequence cannot be
        void initializeUpSequence(AbstractContainer container,
        double start, double step);
    
    (with no *)
    • Get a copy of the container without the parts that make it a sequence
    • This creates an instance of an AbstractContainer, something C++ does not allow
  • But can use references:
        void initializeUpSequence(AbstractContainer &container,
        double start, double step);
    
    however, the pointer version makes it more obvious that the object is on the heap

Pass-by-reference and Containers

  • Consider what happens when we pass a container by value:
        double first_and_last(MegaVector nums) {
          double sum = nums[0] + nums[nums.size() - 1];
          return sum;
        }
    
  • What is the big-O of
        MegaVector xs;
        ...
        double s = first_and_last(xs);
    
  • Better:
        double first_and_last(MegaVector &nums) {
          double sum = nums[0] + nums[nums.size() - 1];
          return sum;
        }
    
    • Issue: no protection against accidental changes:
          double sum = (nums[0] = 0.0) + nums[nums.size() - 1];
      
  • Better:
        double first_and_last(const MegaVector &nums) {
          double sum = nums[0] + nums[nums.size() - 1];
          return sum;
        }
    
  • Other examples of using const&
    • Overloading < on students:
          bool operator<(const Student &a, const Student &b) {
            return a.name() < b.name();
          }
      
      • Avoid copying whole Student object just to compare names

Review

  • Concrete types: built-in behavior
    • const methods, default constructor
    • overloading operators; inside vs. outside class
  • containers
    • return by reference
    • l-value vs. r-value
    • destructor
    • RAII: Resource Acquisition Is Initialization
  • override keyword
  • inheritance, virtual methods
  • abstract classes
  • pure virtual methods