HEAD
Unit XYZ: asdfasdf
This week's stuff:
You can read through / play the audio for each slide at your own pace, or
Unit 11: Classes and Inheritance
This week's stuff:
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.
When we create any large programs we generally have the following goals:
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 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.
#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.
#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).
#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" );
)
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
|
Setter/Mutator functions
|
// 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!
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.
// 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.
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:
Variable names and the function return type don't factor into uniqueness.
class HotDog
{
public:
void SetName( string new_name );
string GetName() const;
private:
string name;
};
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;
}
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.
// 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 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: (repository)
Archived lecture videos