Unit 11: Classes and Inheritance

  1. Structs vs. Classes
  2. Design goals
  3. UML diagrams
  4. Access levels
  5. Declaring a class
  6. Defining class functions (methods)
  7. Creating an object variable
  8. Types of functions
    1. Getter and Setter functions
    2. Constructor functions
    3. Destructor functions
    4. Overloaded functions
  9. Additional design considerations
    1. const and this
  10. Inheritance and Composition
    1. Class inheritance (Is-A)
    2. Class Composition (Has-A)

  11. Example code and additional resources

This week's stuff:

Structs vs. Classes

Classes and structs in C++ are very similar, though how we use each is different, according to C++ best practices.

Both classes and structs can contain member variables and member functions (aka methods), but we tend to use structs for very simple structures and we use classes for more complex ones.

Utilizing classes is at the core of Object Oriented Programming and designing software that is easier to manage over time.

Design goals

When we create any large programs we generally have the following goals:

  • Abstraction: Hiding complexity in the design, creating an "interface" for the user, generalizing functionality to a more "digestible" form.
  • Encapsulation: Also goes along with creating an "interface", though in this regard we are encapsulating related pieces together.
  • Loose coupling: Independence between our objects (classes).
  • High cohesion: Our objects (classes) should have specific responsibilities; don't just throw in the whole kitchen sink.

UML diagrams

MovieLibrary
- movie_list : vector<Movie>
+ AddMovie( new_movie : Movie ) : bool
+ UpdateMovie( index : int, updated_movie : Movie ) : bool
+ RemoveMovie( index : int ) : bool
+ DisplayAllMovies() : void
+ SaveData( path : string ) : bool
+ LoadData( path : string ) : bool

UML (Unified Modeling Language) is often used to diagram the structure of a class, as well as relationships between classes. You will see UML diagrams to describe already existing classes, or an idea of the structure of a class you'll need to write.

The FIRST SECTION of the UML diagram is the class name.

The SECOND SECTION of the UML diagram is the variables.

The THIRD SECTION of the UML diagram is the functions.

Since UML is language-agnostic, we write the type second, so "name : string" would translate to C++ as string name;

The - sign specifies that the member is private.
The + sign specifies that the member is public.

Access levels

Access keywords can be used with classes and with structs. The access level of a member variable or function specifies where that member variable can be accessed directly.

Access level UML symbol Class' functions? Class' childrens' functions? Outside functions?
Public +
Protected #
Private -

To adhere to good design principals, we generally make member variables private, then provide an "interface" in between those variables and the "outside" by creating public functions.

By default, struct members are public and class members are private.

Declaring a class

#ifndef _HOTDOG
#define _HOTDOG

#include <string>
using namespace std;

class HotDog
{
  public:
  void SetName( string new_name );
  string GetName() const;
  
  void SetPrice( float new_price );
  float GetPrice() const;
  
  void SetJpegUrl( string new_url );
  string GetJpegUrl() const;
  
  private:
  string name;
  float price;
  string jpeg_url;
};

#endif

Class declarations look like struct declarations, but we tend to be more explicit about the accessibility level of its member variables and functions.

Once again our class declarations go in header (.h) files. We declare our class and within its code block { }; we can declare its member variables and member functions.

Here, we include accessor markers like public:, and everything beneath it is public, until the next accessor private: begins.

Defining class functions (methods)

#include "HotDog.h"

void HotDog::SetName( string new_name )
{
  this->name = new_name;
}

string HotDog::GetName() const
{
  return this->name;
}

void HotDog::SetPrice( float new_price )
{
  this->price = new_price;
}

float HotDog::GetPrice() const
{
  return this->price;
}

void HotDog::SetJpegUrl( string new_url )
{
  this->jpeg_url = new_url;
}

string HotDog::GetJpegUrl() const
{
  return this->jpeg_url;
}

Within the class' .cpp file we define the member functions (aka methods). When we define functions that belong to a class, we need to prefix the function name with CLASSNAME::.

Functions that belong to a class have access to its private member variables. In this example, each private member variable is prefixed with this->, though that part is optional. I just did this to explicitly show what variables are members, and which are not (i.e., the function parameters).

Creating an object variable

#include "HotDog.h"

int main()
{
  // Declaring object variable
  HotDog my_dog;
  
  // Using the set functions
  my_dog.SetName( "Chili Cheese Dog" );
  my_dog.SetPrice( 2.99 );
  my_dog.SetJpegUrl( "blah.com/ccdog.jpg" );
  
  // Using the get functions
  cout << "HOT DOG: " << my_dog.GetName() << endl;
  cout << "PRICE:  $" << my_dog.GetPrice() << endl;
  cout << "IMAGE:   " << my_dog.GetJpegUrl() << endl;

  return 0;
}

Once we have a class created we can now declare variables whose data-type is that class. A variable whose data-type comes from a class (as opposed to being an int, float, etc.) is called an object.

To call a member function of the class, we take our object variable name (my_dog), followed by the dot operator, and then the name of the function and any arguments it needs (SetName( "Plain Dog" );)

Getter and Setter functions

void HotDog::SetName( string new_name )
{
  this->name = new_name;
}

string HotDog::GetName() const
{
  return this->name;
}

Since we keep our member variables private in order to protect them from unwanted access by "outsiders" (other programmers ;) , we have to create functions that work as an interface to those variables. This is where "Getter" (or "Accessor") and "Setter" (or "Mutator") functions come in.

Getter/Accessor functions

TYPE CLASS::GetVARIABLE() const
{
  return this->VARIABLE;
}
  • Return type matches the private member variable.
  • Function is marked as "const" (no changes allowed).
  • No input parameters needed.

Setter/Mutator functions

void CLASS::SetVARIABLE( TYPE new_var ) const
{
  this->VARIABLE = new_var;
}
  • Return type is void, we're not retrieving data.
  • Input parameter is required - same data type as the private member variable. This parameter stores the new value.
  • Within the function we overwrite the private member variable's data with the parameter's data.

Constructor functions

HotDog.cpp:
// Default constructor
HotDog::HotDog()
{
  this->name = "Plain Dog";
  this->price = 0.99;
}

// Parameterized constructor
HotDog::HotDog( string name, float price )
{
  this->name = name;
  this->price = price;
}

// Copy constructor
HotDog::HotDog( const HotDog& other )
{
  this->name = other.name;
  this->price = other.price;
  this->jpeg_url = other.jpeg_url;
}

Constructors are special types of functions that get called automatically when a new object variable of that class is created. Constructor functions must be named after the class!

  • Default constructor: These have NO PARAMETERS and are used to initialize member variables to default values.
  • Parameterized constructor: These have parameters that are used to give new values to the object immediately during declaration.
  • Copy constructor: This has a parameter of a different object of the same type. The "other" object's variable values will be copied over to "this" object's variables.
HotDog my_dog; // Default constructor
HotDog your_dog( "Chili Cheese Dog", 2.99 ); // Parameterized constructor
HotDog their_dog( my_dog ); // Copy constructor

If you don't add arguments to your object declaration it will call the default constructor. You can use ( ) and pass in the appropriate parameters to get the parameterized constructor to be called. And, if you pass in another object of the same time within ( ), the copy constructor will be called.

Destructor function

HotDog.cpp:
// Default constructor
HotDog::~HotDog()
{
  delete [] arrPtr;
}

Destructors are also special types of functions. They need to be named after the class, but with a tilde (~). Destructors are automatically called when an object of that type is destroyed. An object is destroyed when the function it lives in exits, or for pointers, if we use the delete keyword.

Destructors are handy when you need to clean things up automatically - usually, with open files or with pointers. They may not be very useful to us at this level.

Overloaded functions

HotDog.cpp:
void HotDog::Setup( string new_name )
{
  this->name = new_name;
}

void HotDog::Setup( string new_name, float new_price )
{
  this->name = new_name;
  this->price = new_price;
}

void HotDog::Setup( string new_name, float new_price, string new_url )
{
  this->name = new_name;
  this->price = new_price;
  this->jpeg_url = new_url;
}

Inside or outside functions we can declare multiple functions with the same name. As long as their parameter list is unique, this is completely valid. This is known as function overloading.

A unique parameter list means that either:

  • Different amounts of parameters, or
  • Parameters are different data types

Variable names and the function return type don't factor into uniqueness.

const and this

class HotDog
{
  public:
  void SetName( string new_name );
  string GetName() const;
  
  private:
  string name;
};

Const

When a function is marked with const after its parameter list, this means that none of the member variables can be changed within this function. This protects the data in the member variables.


void HotDog::SetName( string new_name )
{
  this->name = new_name;
}

// or 

void HotDog::SetName( string new_name )
{
  name = new_name;
}

This

The this-> keyword explicitly references the class object we're working with, and we can use it to specify that we want a member variable from this class. It isn't required, but sometimes it can be handy to explicitly state that it's a class' member variable to differentiate it from local variables and parameter variables.

Class inheritance (Is-A)

// Parent class (aka base class)
class Person
{
  public: 
  void Setup( string new_name );
  void Display() const;
  
  protected:
  string full_name;
};

// Child class (aka subclass)
class Student : public Person
{
  public:
  void Setup( string new_name, float new_gpa );
  void Display() const;
  
  protected:
  float gpa;
};

We can also build a family tree of classes by utilizing inheritance. Inheritance allows one function to mark another as its parent, and it will inherit all the public and protected members (variables and functions) from that parent.

We utilize Inheritance in order to reduce duplicate code - perhaps in a college system, there are various types of people on campus: Students, Teachers, Staff, Visitors. Whatever each of these have in common we would take and put into the parent class, such as maybe a name and a phone number. Students might need GPA information, Teachers might need to be assigned a Department, and so on, so we create specializations of People by using inheritance.

Class Composition (Has-A)

class Button
{
    public:
    void Draw();
    void SetText( const string& text );
    void SetDimensions( int width, int height );

    private:
    Image m_icon;   // Button HAS-A Image icon
    Label m_label;  // Button HAS-A text Label
};

Another way to create a relationship between classes is to use composition. This is difference from inheritance because instead of inheriting member variables and functions, we instead store another class within this class. This also allows us access to the other class, but it is "packaged" as a variable. This can especially be helpful when items need to interact but it wouldn't really be appropriate to say Item 1 "IS-A" Item 2.

Example code and additional resources

Example code: (repository)


Archived lecture videos