Classes
Classes
We've seen that structures in C++ allow us to hold different data types as
fields and access them through the dot notation. For example,
struct Student {
int hours;
float gpa;
};
Student s;
s.hours = 26;
s.gpa = 3.8;
class Student {
// Line 1
public:
// Line 2
Student();
// Line 3
void setCreditHours(int courseNumber, int hrs);
// Line 4
void setQualityPoints(int courseNumber, float qpts);
// Line 5
float getGpa();
// Line 6
// Line 7
private:
// Line 8
int creditHours[5];
// Line 9
float qualityPoints[5];
// Line 10
};
// Line 11
OK. There's a lot going on here, so let's take things line by line.
1. Notice that the class definition looks just like a struct definition, except
the class keyword is used instead of struct. This is not a
coincidence, as we will see later.
2. public is an access specifier (along with private and protected).
It is always followed by a colon. Anything after the colon and before the
next access specifier has the stated access. Public access means that
any instance of the class can access the field by dot notation. For clarity,
structures in C++ ALWAYS have public access. So if something is
public, you can do things with those fields the same as if they were in a
structure. Private access means that the field cannot be accessed by an
instance of it. Protected access only comes into play when we use
inheritance, so we'll ignore it for now.
3. This is a special kind of function called a class constructor. Notice that it
is a function (because of the parentheses), but it does not have a return
type. The job of a constructor is to initialize any class variables (in this
example, the variables are on Lines 9 and 10) and allocate memory
(which we'll see later on). Just remember that the job of the constructor
is to initialize variables and allocate memory.
4. This is a function that takes the course number (an index into the array
on Line 9) and the number of credit hours for that course. The function
(once implemented) should set creditHours[courseNumber] to be
hrs.
5. This function is similar to the one above, except that it sets the quality
points array.
6. This is the most interesting of the functions, because it is going to
operate on the two arrays to calculate the GPA and return it.
7. Nothing to see here.
8. Once again, private access means that nothing outside of the class
itself can see the members. That means that we can't use dot notation
to access anything private on an instance. For example, if we had an
instance of Student called s, it would be illegal for us to say
s.creditHours[0]. We CAN, however, operate on those private
variables in the class implementation, which we'll see soon.
9. This is the array that will hold the credit hours.
10. This is the array that will hold the quality points.
11.Notice the required semicolon at the end of a class definition (just like a
struct).
Now, just to be clear, what we have done so far is created a class definition.
We have not yet provided the class implementation. Before we do, let's look at
how the class might be used by client code.
So we create an instance (or object) of our Student class (which we call s),
and now we can use dot access to call the public functions we made. The
data inside the class (the two arrays) is hidden from us. But the functions that
also belong to the class (setCreditHours, setQualityPoints, and
getGpa) can all see those arrays and operate on them without passing them
around.
Class Implementation
The following is the class implementation for the Student class we've been
working on:
Student::Student() {
// This is the constructor. Its job is to initialize
variables and allocate memory.
// The variables here are the two arrays, and they're
initialized to hold all 0s.
for (int i = 0; i < 5; i++) {
creditHours[i] = 0;
qualityPoints[i] = 0;
}
}
float Student::getGpa() {
// This calculates the GPA based on the credit hours and
quality points that have been set.
float weightedPoints = 0;
int totalHours = 0;
for (int i = 0; i < 5; i++) {
weightedPoints += creditHours[i] * qualityPoints[i];
totalHours += creditHours[i];
}
// The GPA is weightedPoints / totalHours.
// But we can't divide by 0, so we have to check for
that.
if (totalHours == 0) {
return 0;
} else {
return weightedPoints / totalHours;
}
}
Since these functions belong to the class, they have the class name as a part
of their name. We can think of it sort of as a "surname", just like the way you
belong to your parents and take their name with you. So each function's name
(in the implementation) is preceded by the class name and two colons (called
the scope resolution operator). We've seen that same operator before when
we were talking about namespaces (like std::cout). It's exactly the same
thing here.
Also, it's clear in this code that the class functions can see and interact with all
of the private variables in the class (almost as if they were global variables;
they aren't, but they are at class-level scope, which is higher than local
scope).
For instance, for our Student class we would create the files "Student.h" and
"Student.cpp". (Recall that we discussed header files when we talked about
function prototypes and forward definitions. That's EXACTLY the same thing
here.)
Student.h
class Student {
// Line 1
public:
// Line 2
Student();
// Line 3
void setCreditHours(int courseNumber, int hrs);
// Line 4
void setQualityPoints(int courseNumber, float qpts);
// Line 5
float getGpa();
// Line 6
// Line 7
private:
// Line 8
int creditHours[5];
// Line 9
float qualityPoints[5];
// Line 10
};
// Line 11
Student::Student() {
for (int i = 0; i < 5; i++) {
creditHours[i] = 0;
qualityPoints[i] = 0;
}
}
Student::getGpa() {
float gpa = 0;
for (int i = 0; i < 5; i++) {
gpa += creditHours[i] * qualityPoints[i];
}
return gpa;
}
Now, our client code can be in any file we like. For instance, maybe we'll
create a file called "main.cpp".
main.cpp
#include <iostream>
#include "Student.h"
using std::cout;
using std::endl;
int main() {
Student s; // This calls the
constructor automatically.
s.setCreditHours(0, 3);
s.setQualityPoints(0, 3); // B
s.setCreditHours(1, 3);
s.setQualityPoints(1, 4); // A
s.setCreditHours(2, 4);
s.setQualityPoints(2, 3.3); // B+
s.setCreditHours(3, 2);
s.setQualityPoints(3, 2.3); // C+
s.setCreditHours(4, 3);
s.setQualityPoints(4, 3.7); // A-
float gpa = s.getGpa();
cout << "The GPA is " << gpa << endl;
return 0;
}
Notice that the "Student.h" file must be included in the client code, as well.
Implementing a Class
Challenge
Easy
● The point should have (internally) two floats representing the x and y
coordinates (but those should be private).
● There should be a constructor that takes no parameters and sets the x
and y coordinates to 0.
● There should be a void function called setX that takes a single float
parameter (called newX) and sets the x coordinate to the parameter.
● There should be a void function called setY that takes a single float
parameter (called newY) and sets the y coordinate to the parameter.
● There should be a function called distanceFromOrigin that takes no
parameters and returns a float representing the distance of the point
from the origin. This should be accomplished using the Euclidean
distance formula (from point (x, y) to point (0, 0)). We'll need the sqrt
function for this (from cmath). Remember that the Euclidean distance
from the origin is calculated as sqrt(x2 + y2).
Note: For convenience in this question, we'll place the class definition and the
class implementation in the same file (so no need to do any #include).
Instantiating a Class
So, now we've seen how to define and implement a class, and we've also
seen how to use the class in client code. Let's be more clear about that last
part. Suppose we have the following class definition:
class Point {
public:
Point(); // initializes (0, 0)
Point(int x, int y); // initializes (x, y)
float distanceToOrigin();
private:
int x;
int y;
};
We'll assume that the implementation has also been done for us, and we'll
ignore that for now to focus on how to USE the class.
It's important to be clear that, whenever we create a class, we are creating a
new DATA TYPE. Just like int, float, bool, etc., the Point class is a data
type now. Remember that those other data types are called primitive data
types, because they came first ("prime" means "first") as they were built into
the language. Any class that we make is called a composite data type,
because it is "composed" of other data types (two int variables, in this case).
We can imagine the primitive data types to be analogous to the chemical
elements. All of the variety in the universe is somehow created by ~100
elements. That's because the elements can be combined in a multitude of
ways to produce new molecular compounds. Our classes are analogous to
those molecules.
In client code (some other function, like main), we have to create variables of
this new data type we've made. We can't just talk about Point or say
Point.distanceToOrigin()
int + int
No, just like with primitive types, we have to create variables and operate on
those variables. When the variable's type is some class (some composite
type), it is called an instance or an object of that type. So we need to create
an instance of Point (or we might also say we need to instantiate
[in-STAN-chee-ate] Point).
The variable p here is the instance or object. And we can instantiate as many
variables as we need, where each of them has its own instance data (the x
and y integers in this case).
Scope Resolution
Let's look at that Point class again, but this time we'll focus on the
implementation. In particular, let's look at the constructors. Here is the
definition again.
class Point {
public:
Point(); // initializes (0, 0)
Point(int x, int y); // initializes (x, y)
float distanceToOrigin();
private:
int x;
int y;
};
And we'll start by implementing the default constructor. It should initialize the
point to (0, 0).
Point::Point() {
x = 0;
y = 0;
}
Now, let's imagine that we're the compiler. We see some variables x and y,
but they aren't declared in the local scope (obviously). So we look to the next
higher scope, which is determined by that Point:: attached to the
constructor name. That means we should look at the class Point's instance
variables to see if there's an x and y there, and sure enough, we found it. So
those instance variables will both be set to 0.
If we had NOT found those variables in Point, then the compiler would have
continued to look further and further out, with the next (and last) stop being
global variables.
Point::Point(int x, int y) {
x = x;
y = y;
}
This seems like the right idea, but let's again pretend we're the compiler. We
see x and y, so we determine where they were created, and we immediately
notice that they're the parameters to the function. That means we're just
setting the local variables x and y to themselves. This will NOT affect the
class's instance variables, because they are being shadowed by the locals.
So how do we solve this problem? Well, one solution would be to name the
parameters something different than the instance variables (for instance,
xValue and yValue):
This would work as we intended. But we should be able to use any parameter
names that we want, whatever is most meaningful. And we can; we just have
to have a way to specifically refer to the instance variables x and y. The way
we do that is by adding this-> in front of the variables (which we normally
read as "this arrow":
Point::Point(int x, int y) {
this->x = x;
this->y = y;
}
Now, we're explicitly saying that THIS instance's x and y variables should be
set to the parameters. For now, that's enough of an explanation for that syntax
(hopefully). Later on, we'll understand it in much more detail.
Implementing Methods
The functions that live inside our classes are typically called either "member
functions" or (more commonly) methods. The constructors that we've looked
at already are special methods, but all of our methods work similarly when
they're implemented. Consider the distanceToOrigin method of the
Point class.
class Point {
public:
Point(); // initializes (0, 0)
Point(int x, int y); // initializes (x, y)
float distanceToOrigin();
private:
int x;
int y;
};
#include <cmath>
using std::sqrt;
float Point::distanceToOrigin() {
return sqrt(x * x + y * y);
}
Once again, we can refer to the instance variables within the method.
Challenge
Easy
class Student {
public:
private:
string name;
int hours;
float balance;
};
Arrays of Instances
We can also create arrays of instances of our class. For instance, we could
create an array of Point instances.
But where is the call to the constructor here? There isn't one. So what
constructor is used? The DEFAULT constructor (the one that takes no
parameters). If you don't have a default constructor for your class, then you
CANNOT create an array of them. So this array of points is full of (0, 0)
instances.
float total = 0;
for (int i = 0; i < 8; i++) {
total += arr[i].distanceFromOrigin();
}