Review of last time
Classes — encapsulating data and behavior.
class name {
public:
... members ...
... methods ...
};
A class for dogs:
class dog {
public:
string name;
string breed;
bool tired;
void eat() {
cout << "Gulp!" << endl;
}
void speak() {
cout << name << " says, Woof!" << endl;
}
void walk() {
tired = true;
}
void nap() {
if(tired) {
tired = false;
cout << "Zzz..." << endl;
}
else
speak();
}
};
Usage:
dog fido{"Fido", "Corgi", false};
fido.speak(); // Prints "Fido says, Woof!"
fido.rest(); // Prints "Fido says, Woof!";
fido.walk();
fido.rest(); // Prints "Zzz..."
A longer example:
Here’s a class with a members and some methods:
class post_office {
public:
vector<string> mailboxes;
string check(int m) {
return mailboxes.at(m);
}
bool has_message(int m) {
return !mailboxes.at(m).empty();
}
void leave_message(int m, string message) {
mailboxes.at(m) = message;
}
void add_mailbox() {
mailboxes.push_back("No new mail"); // Should I do this?
}
int count_mailboxes() {
return mailboxes.size();
}
};
We can create multiple post_office
s:
post_office school;
post_office work;
post_office home;
And then each one can have a different collection of mailboxes:
school.add_mailbox();
school.add_mailbox();
school.add_mailbox();
work.add_mailbox();
home.add_mailbox();
home.add_mailbox();
Aside: What am I doing? repeating myself! I should make a version that can add more than one new mailbox at a time!
// Overloaded version
void post_office::add_mailbox(int n = 1) {
mailboxes.resize(mailboxes.size() + n, "");
}
So now we can just do
school.add_mailbox(3);
work.add_mailbox();
home.add_mailbox(2);
In the first batch, the current instance will be school
, so it will school
whose mailboxes are modified. Similarly, in the next section, it’s work
,
and after that it will be home
.
And then we can store some messages:
school.leave_message(0, "Hey, what's up?");
school.leave_message(1, "Why are you late?");
school.leave_message(2, "Your order is ready.");
work.leave_message(0, "Reports are due");
home.leave_message(1, "Feed the cat.");
cout << school.check(1) << endl; // Prints "Why are you late?"
So now if I do
school.count_mailboxes()
what am I going to get? (3).
Declaring and defining methods
Although I’ve written all the method declarations inside the class definition so far, we actually have some flexibility in this regard.
First, note that the usual rule for functions (they must be defined or declared before they can be used) is somewhat relaxed within class definitions. In particular, any method in a class definition can refer to any other method, regardless of the order of definition. (And similarly, any method can refer to any data member, even ones defined after it.)
This is mostly done to simplify writing classes. It’s common to put the methods of a class at the top of the class definition, roughly in order of importance; similarly, data members often appear towards the bottom of a class definition. This would not be possible if methods/members were held to the string “must be defined before use” rule that applies to functions and variables outside classes.
This means that we can structure a class like this:
class city { public: void pickup_trash() { trash_collected++; } private: int trash_collected = 0; };
As the above shows, we can give starting values for the data members. Every
city
that is created will start out having atrash_collected
equal to 0. The rules for uninitialized data members are the same as for variables:Simple types like
int
,float
andchar
start out with an undefined value. It’s just whatever happened to be in memory at that time. Giving an initial value (as I did above, fortrash_collected
) is always a good idea, just to make sure we know what’s in them.Class types like
string
andvector
are “default initialized” which just means they start out empty (empty string, empty vector). This means you don’t need to initialize them unless you want them to have some specific value to start with.We don’t have to define every method within the class itself. We can put just the method declaration inside the class definition, and then put the method definition outside it, elsewhere in the file (or even in a different file, later on…):
class whatever { public: void f(); // Declaration of f void g(); // Declaration of g }; void whatever::g(); { f(); cout << "Goodbye!"; } void whatever::f() { cout << "Hello!"; }
The
whatever::f
syntax is how we tell C++ that thef
we are defining is not a normal function, but is in fact part of the classwhatever
. C++ will look inside the class, see the matching declaration off
, and then link the definition to the declaration. Note that the method must be declared within the class; you cannot usewhatever::h()
to magically add a new method towhatever
.Like functions, classes must be declared or defined before they are used. This means that our
dog
andowner
classes must be defined like thisclass dog { public: ... }; class owner { public: vector<dog> dogs; };
because
owner
refers todog
. There is something corresponding to a “declaration” for classes, so that you can declare a class without giving it’s full definition, but it’s of much more limited use than a function declaration.A class declaration looks like this:
class dog;
and basically the only information it gives to C++ is that “
dog
is a class, which I’ll define later”. The problem is that this isn’t enough information for C++ to do anything really interesting withdog
s. You can’t create adog
yet, because C++ doesn’t know what should go into it. You can’t create avector<dog>
becausevector
need to know how bigdog
s are, and it doesn’t know that yet. A class likedog
that has a declaration but no definition yet is called an incomplete class. Pretty much the only thing you can do with an incomplete class is create a pointer to it:dog* puppy = new dog{...}; // OK dog woofers{...}; // NOT OK, dog is incomplete
So really, in order to do anything interesting with a class, you have to give its defintion, which means that you may have to put your classes definitions in a particular order, if they depend on each other.
Access control: private vs. public
Notice in the previous example that we never refer to .mailboxes
outside of
the methods of post_office
. This is actually a good thing! It means that we’ve
written our methods so as to properly encapsulate access to the data members.
In fact, if we could somehow “hide” mailboxes
so that only the methods of
post_office
were allowed to use it, that would be just fine. It would even
be a little safer, as it would mean that outside users couldn’t get in and
mess with the vector of mailboxes in weird ways.
For example, right now, any code outside of the post_office
class itself can
do
school.mailboxes.clear(); // PRANK! delete all mailboxes
and there’s nothing we can do about it.
We can fix this, however; we leave the methods as public but we’re going
to make mailboxes
private:
class post_office {
public:
string check(int m) {
return mailboxes.at(m);
}
bool has_message(int m) {
return !mailboxes.at(m).empty();
}
void leave_message(int m, string message) {
mailboxes.at(m) = message;
}
void add_mailbox() {
mailboxes.push_back("No mail yet!");
}
int count_mailboxes() {
return mailboxes.size();
}
private:
vector<string> mailboxes;
}
Anything after the public:
has public access, you can use it from outside
the post_office
class. But the public section ends at private:
; anything
after private:
has private access and can only be used from within
the post_office
class’s methods. This means that if we try to take this new
version and do
post_office school;
// ...
school.mailboxes.clear(); // PRANK! delete all mailboxes
we will get a compile-time error; access to private member mailboxes
is
forbidden.
What do we mean by “outside” the class? Simple: functions (including main) or other code that is not part of the class definition. (Note that if you put a method declaration inside the class, and the definition of it outside, the definition still counts as being “inside” the class.) For example:
class thing {
public:
int x = 0;
void f() {
cout << x; // OK, x is public
}
void g() {
cout << y; // OK, y is private but g is inside thing
}
void h(); // Declaration only
private:
int y = 1;
};
void thing::h() {
cout << x << y; // OK, h() is still inside thing
}
int main() {
thing whatever;
cout << whatever.x; // OK, x is public
whatever.f(); // OK, f is public
whatever.g(); // OK, g is public
cout << whatever.y; // ERROR, y is private
return 0;
}
Note, also, that data members and methods can be private. If a class has
some methods that shouldn’t really be used by “outsiders”, only by the class
itself, you can make them private
to enforce that.
Access specifiers like public
and private
let you separate the parts of
a class that other people are supposed to use, from the parts that are part
of the “guts” of the class, and should not be messed with by outsiders. From
the perspective of someone outside the class, using it, you know that you
don’t need to worry about anything that is private
. All you need to concern
yourself with are the things that are public
, because those are the only
things you can use. Again, it’s another kind of abstraction, another way of
hiding details that you don’t want to think about right now.
It’s common for a class to hide most or even all of its data members behind
private
, only exposing controlled access to them through public
methods.
The main reason for this is that a public
data member cannot really be
controlled: you cannot limit what values an outside user might assign into it,
at any time. If you hide it behind some methods (e.g., a method for getting its
value, and a method for setting its value) then you have complete control over
what the outside world sees, and what happens when they try to change it.
When data members are hidden, it’s common to provide “access methods” for the members that control how they can be read and written. For example,
class employee {
public:
string get_name() {
return name;
}
void set_name(string new_name) {
if(!new_name.empty())
name = new_name;
}
int get_age() {
return age;
}
private:
string name;
int age;
// etc.
};
Note that doing things this way gives the class an extra measure of control
over how, and if, its data members can be accessed. E.g., above, we provide
a method to change an employee
‘s name, but only if the new name is not
the empty string, and we do not provide any way to change an employee’s age.
private
members cannot be accessed by functions outside the class, but
sometimes we really want to write a function, outside a class, that accesses
its private members. There are two ways to go about this:
Add “accessor” methods for the private members as described above, and use those in your function. But note that these methods must be
public
, which means that anyone can use them; that might not be what you want.Declare the function to be a friend of the class.
A friend is a function, outside of a class, which has access to the class’s
private members and methods. The class itself declares who its “friends” are,
so you can’t just write a function and then make it a friend to gain access to
things you shouldn’t be accessing. To declare a function a friend, copy the
declaration of it into the class definition and put the keyword friend
in
front of it. For example:
void update_age(employee& e, int new_age);
class employee {
public:
//
friend void update_age(employee& e, int new_age);
private:
string name;
int age;
};
void update_age(employee& e, int new_age) {
e.age = new_age; // OK, update_age is a friend of employee
}
Note that if you don’t say either of public
or private
, then everything
inside a class defaults to being private:
class hidden {
int x, y, z; // These are all private
};
...
hidden my_secret;
my_secret.x = 1; // ERROR, x is private
Polynomials, continued
Now that we have methods, we can clean up our polynomial
class, moving some of the operations inside it, and making coeffs
private.
class polynomial {
public:
void normalize();
int degree();
void shift(int powers);
float evaluate(float x);
void print();
polynomial multiply(float s) {
polynomial output = ???
for(int i = 0; i < coeffs.size(); i++)
output.coeffs.at(i) = coeffs.at(i) * s;
return output;
}
polynomial add(polynomial b) {
int larger = max(degree(), b.degree());
polynomial output = ???
output.expand(larger); // Make room for coefficients
for(int i = 0; i < larger; i++) {
float x = i < coeffs.size() ? coeffs.at(i) : 0;
float y = i < b.coeffs.size() ? b.coeffs.at(i) : 0;
output.coeffs.at(i) = x + y;
}
return output;
}
void set(int i, float c) {
if(degree() < i)
coeffs.resize(i + 1);
coeffs.at(i) = c;
}
private:
vector<float> coeffs;
};
I’ve only writtten out some of the methods in full, and left most of them as
declarations.
I’ve also left out both the create
function and put ???s in the lines where it was
called. We’ll deal with the proper way to create objects in the next section.
Some general observations here:
Every function that used to take a single
polynomial
parameter now doesn’t need to take one: it can just use the current instance directly. Similarly, functions likeadd
that used to take two, now just take one.When a function used to take a parameter
polynomial p
, we just remove the parameter, and then remove every copy ofp
in the body of the function. Effectively,p
is being replaced by the current instance. In fact, if you’re converting non-class code to class-based, a good hint that a function ought to become a method is if it takes a single instance as an argument.Notice how
degree()
refers tonormalize()
? As with members, methods can call each other on the current instance, implicitly.multiply
can access the coefficients ofoutput
directly, even though they are private. This is becausemultiply
is part ofpolynomial
, even though it is not part of the same instance asoutput
.Just like we can overload functions, provided they have different numbers of parameters or different parameter types, so we can overload methods. Here, we’ve overloaded the
add
method, to provide a version that adds a scalar value to a polynomial (just adds it to the \(x^0\) term).
Because add
and multiply
take two polynomials, and don’t “target” one or
the other, it might make more sense to write them as normal functions, outside
the class, and then just make them friend
s of the class.
this
– The current instance
In add(float)
I used *this
. I mentioned that when we call a method on
an instance, it magically knows what the current instance is. this
is a
pointer to the current instance. That is, whenever we refer to a member
of method of the current instance, we are implicitly doing it through
this
. E.g.
class dog {
public:
string name;
void speak() {
cout << "Woof! says " << name << endl;
}
// is exactly equivalent to
void speak() {
cout << "Woof! says " << (*this).name << endl;
}
}
If we need to actually get the current instance, the whole thing, we can
get it through *this
, which is what we do in add
in order to make a copy
of it.
Constructors: Encapsulating object creation
How can we add create
to our polynomial
class? Can we add it as a method?
Let’s look at the prototype for create:
polynomial create(int degree);
Remember that functions which had one polynomial
parameter turned into methods
that had none. Functions that had two became methods with one, and so forth.
What do we do with a function that already took no polynomial parameters?
Another way to look at it is like this: methods can only be called if you have
an existing instance to call them on. create
is all about creating instances.
So how will we call it on an instance, something.create()
, without creating
an instance something
first? It’s
a chicken-and-egg problem. The answer is that create
turns into something
different within polynomial
: it becomes a constructor.
A constructor is a special part of a class that describes how instances of the class are created. It is responsible for setting up an instance, before it is used. Again, it’s all part of making the class responsible for its own objects: the class should handle setting up objects, and not rely on anything else to ensure that they are properly setup for it.
Inside a class definition, a constructor looks kind of like a method, except
It has no return type.
Its name must be the same as the name of the class.
It shouldn’t use any class methods (usually), because the current instance isn’t fully created yet. (You can use methods, so long as you’re sure that they are “safe” to use on an instance that isn’t quite finished being built yet.)
A class can have more than one constructor, provided they take different numbers/types of arguments (i.e., constructors can be overloaded just like functions).
Constructors are usually public, but sometimes its useful to have a private constructor. (A class with nothing but private constructors is one that you can never create instances of!)
For polynomial
, the constructor looks like this
class polynomial {
public:
polynomial(int degree) {
coeffs.resize(degree + 1, 0.0);
}
...
}
To use a constructor, we use the same syntax as to create a class instance by giving values for its members, only now we give values for the constructor’s parameters:
polynomial p{4}; // Creates a polynomial variable of degree 4
polynomial p2 = polynomial{4}; // Same
Now we can finish the add
and multiply
methods:
...
polynomial multiply(float s) {
polynomial output{degree()};
for(int i = 0; i < coeffs.size(); i++)
output.coeffs.at(i) = coeffs.at(i) * s;
return output;
}
polynomial add(polynomial b) {
int larger = max(degree(), b.degree());
polynomial output{larger};
for(int i = 0; i < larger; i++) {
float x = i < coeffs.size() ? coeffs.at(i) : 0;
float y = i < b.coeffs.size() ? b.coeffs.at(i) : 0;
output.coeffs.at(i) = x + y;
}
return output;
}
For multiply
, the output polynomial has the same degree as the current
instance (we are not adding or removing any terms). For add
, the resulting
polynomial may be as big as the larger of the current instance and the
parameter b
.
Note that just as with methods, we can overload the constructor to provide different versions that take different arguments. In particular, a constructor that takes no arguments is called the “default constructor” and it’s usually a good idea to provide one, if you provide any constructor at all:
polynomial() { }
The default constructor is called if you create an “unitialized” polynomial
:
polynomial p; // Calls default constructor
(This should tell you that both string
and vector
have default constructors,
and their default constructors set up the empty string and vector, respectively.)
One big caveat to note about providing constructors: Suppose we have a class
class point3d {
public:
float x,y,z;
};
Right now, we can create a point with
point3d p1{12,-5,3};
by just giving the values of x, y and z in the curly braces. However, if we add a constructor:
class point3d {
public:
point3d() {
// Does nothing
}
float x,y,z;
};
then we lose the ability to construct point3d
s by just listing the values for
their members:
point3d p2{-3,1,0}; // Error! no constructor takes 3 arguments
The idea is that the curly-braces-to-member-values is just a default provided by
C++ if you don’t do anything; if you write a constructor, C++ assumes that you
want to handle constructing the object yourself, and thus removes the default.
Normally, if you’re going to write a constructor, you’re going to write all
the constructors you might need. So for point3d
we’d probably want to add
class point3d {
public:
point3d() {
// Leave values at 0
}
point3d(float f) {
x = y = z = f;
}
point3d(float a, float b, float c) {
x = a; y = b; z = c;
}
float x = 0, y = 0, z = 0;
};
Now we can do
point3d p1{1,2,3};
just like before, only now it’s using our constructor, rather than the built-in one we had before.
Constructors for conversions
Let’s give ourselves a constructor that takes a float
and builds a constant
polynomial (i.e., a polynomial of degree 0):
polynomial(float c) {
coeffs.resize(1,c);
}
Now we have an easy way to construct polynomials that represent a single
constant value: polynomial{1.2}
is the polynomial
Suppose we do this
polynomial p1;
p1.expand(3);
p1.set(0,1);
p1.set(1,2);
p1.set(2,3);
// Now p1 = 1 + 2x + 3x^2
polynomial p2 = p1.add(12.0); // Huh?
Not only will this work, but afterwards
I.e., 12.0 was treated like a constant polynomial. What’s going on? The answer is implicit conversion. Think about what happens when we do something like this:
void f(float);
...
f(1);
1 is an int
, but it is implicitly converted to a float
because that’s
what f
expects. polynomial::add
expects another polynomial
, so C++ tries
to find a way to implicitly convert a float
into a polynomial
. And lo and
behold, it finds our constructor:
polynomial(float c) { ... }
This means that we can also do things like this:
polynomial q = 12.0;
and that will work just fine.
Another way of thinking of this constructor as, “here is how you transform a
float
into a polynomial
”, which is exactly how C++ interprets it, using it
to convert the float
12.0 into a polynomial
which is then add
-ed to p1
to get the result stored in p2
.
In other words, a constructor that takes a single parameter of some type T also defines an implicit conversion from values of type T to the class’s type.
Sometimes this isn’t what we want. Think about the other constructor: if we
had written p1.add(12)
then it would have taken the int 12, passed it to
the degree constructor, and constructed a 0 polynomial of degree 12, not what
we were expecting. For constructors like this, that should never be used
to perform conversions, we can mark them as explicit
:
explicit polynomial(int degree) { ... }
Now C++ will never try to convert an int
to a polynomial
by using this
constructor.
An exercise: polynomial multiplication
How do we multiply two arbitrary polynomials together? We’ll, it’s kind of like FOIL on steroids:
We form all possible pairs of products from both sides, then match them up by the resulting powers of \(x\), and add up all the terms that have the same exponent.
(Draw grid representation)
We’re going to create a polynomial with a degree as big as could be needed: this is the sum of the two input polynomial’s degrees.
polynomial multiply(polynomial b) {
polynomial output;
output.expand(degree() + b.degreee());
// ...
}
Then, we’re going to use a nested for
-loop over all possible pairings. For
each pair, we’ll compute the exponent and coefficient, and add it to that
entry in the final polynomial. At the end, we’ll normalize to clean up any
zeroes at the end.
polynomial multiply(polynomial b) {
polynomial output;
output.expand(degree() + b.degreee());
for(int i = 0; i <= degree(); i++)
for(int j = 0; j <= b.degree(); j++) {
float c1 = coeffs.at(i);
float c2 = b.coeffs.at(j);
output.coeffs.at(i + j) += c1 * c2;
}
output.normalize();
return output;
}