Classes¶
Introduction¶
classes let programmers define their own data types
a class is like a struct
on steroids
a class lets you create objects, and objects are, essentially, a collection of related values and functions
a class is like a factory for objects
or you could think of a class as being like a recipe for a cake, and the object as being the cake itself
challenge: Explain the difference between a class and an object.
A Book Class¶
here is a simple class for representing a book:
class Book {
public:
string title;
string author;
int pubDate;
};
the Book
class lists everything necessary to create a Book
object
A Book Class¶
you use it like a struct
void print(const Book& b) {
cout << " Title: " << b.title << "\n"
<< " Author: " << b.author << "\n"
<< "Pub. Date: " << b.pubDate << "\n";
}
// ...
Book a; // make a new book object
a.title = "The Name of the Rose";
a.author = "Umberto Eco";
a.pubDate = 1980;
print(a);
Book b = a;
print(b);
// C++11-style initialization
Book c{"The Name of the Rose", "Umberto Eco", 1980};
print(c);
// d points to a newly created Book object on the free store
Book* d = new Book{"The Name of the Rose", "Umberto Eco", 1980};
print(*d);
delete d;
A Book Constructor¶
before C++11, initializing the values of a class was tedious:
Book a; // make a new book object
a.title = "The Name of the Rose";
a.author = "Umberto Eco";
a.pubDate = 1980;
now it’s much easier to do, e.g.:
Book c{"The Name of the Rose", "Umberto Eco", 1980};
C++ gives you even more control over how an object is initialized using constructors
class Book {
public:
string title;
string author;
int pubDate;
// constructor
Book(const string& t, const string& a, int pd)
: title(t), author(a), pubDate(pd) // initialization list
{
// empty body!
}
};
// ...
Book b{"The Name of the Rose", "Umberto Eco", 1980};
print(b);
the calling syntax is the same as the C++11-initialization, but, as we will see, it has the added advantage that we can execute any code we like during the initialization
for example, a common use of a constructor is to validate the data you pass it:
Book(const string& t, const string& a, int pd)
: title(t), author(a), pubDate(pd) // initialization list
{
if (pd < 1900 || pd > 2016) cmpt::error("pubDate out of range");
}
A Book Constructor¶
a constructor has no return type (not even void
)
they don’t return a value the way a regular function does
the name of a constructor is the same as the name of the class
constructor’s are usually in the public
part of the class
a class can have multiple constructors as long as the inputs are different
Initialization Lists¶
initialization lists force the variables of an object to be initialized before any code in the constructor runs on them
the idea is to avoid manipulating non-initialized data (which would almost certainly be an error)
you could instead do the initialization in the constructor body
always use an initialization list when you can
Special Constructors¶
a default constructor takes no parameters
Book() // default constructor
: title("no title"), author("no author"), pubDate(0)
{ }
a default constructor doesn’t make intuitive sense for a Book
, because
real books always have a title, author, and publication date
but it may be necessary in order to use it with container classes (such as
vector
)
if you don’t write any constructors for a class, the compiler automatically synthesizes a default constructor for you
see the textbook for more details
Special Constructors¶
a copy constructor makes a copy of another object
Book(const Book& b)
: title(b.title), author(b.author), pubDate(b.pubDate)
{ }
in this particular case, the copy constructor uses an initialization list to
make copies of each piece of Book
data
so its body is empty
but in general, a copy constructor can do whatever it needs to do in its body to make the copy
Public and Private Data¶
a class can have both a private
part and a public
part
(there is also protected
, but we will skip that in this course)
the private
parts of an object can only be accessed by code in the object
itself, or in code declared to be the object’s friend
the public
parts of an object can be accessed by any code
Public and Private Data¶
by making data private, we can control access to it and so make sure it stays consistent
class Book {
private:
string title;
string author;
int pubDate;
public:
Book(const string& t, const string& a, int pd)
: title(t), author(a), pubDate(pd) // initialization list
{
// empty body!
}
};
now the print
function doesn’t compile because it is not allowed to
access the private variable’s of a Book
object
Getters¶
a getter is a function in a class that returns the value of a variable in the class
class Book {
private:
string title;
string author;
int pubDate;
public:
Book(const string& t, const string& a, int pd)
: title(t), author(a), pubDate(pd) // initialization list
{
// empty body!
}
// getters
string get_title() const {
return title;
}
string get_author() const {
return author;
}
int get_pubDate() const {
return pubDate;
}
};
Getters¶
we’ve added three getters to Book
consider get_title()
string get_title() const {
return title;
}
it return the value of title
it is const
, which means it does not change the value of any Book
variables
note the position of const
!
Getters¶
now we can rewrite print
void print(const Book& b) {
cout << " Title: " << b.get_title() << "\n"
<< " Author: " << b.get_author() << "\n"
<< "Pub. Date: " << b.get_pubDate() << "\n";
}
it uses the getters instead of the variables themselves
Immutability¶
the way we’ve defined Book
objects makes them immutable
that means you cannot change them after they’re created
immutable objects tend to be very good things
they are easy to copy (just share pointers/references)
they are easy to use (never have to worry about assigning wrong values)
they are easy to use in concurrent programs where more than one process can access them at a time (although we won’t discuss that in this course)
Immutability¶
rule of thumb: always try to make your objects immutable
it’s not always possible, of course
Setters¶
a setter is a function in a class that assigns a value to a variable
class Book {
private:
string title;
string author;
int pubDate;
public:
Book(const string& t, const string& a, int pd)
: title(t), author(a), pubDate(pd) // initialization list
{
// empty body!
}
// getters
// ...
// setters
void set_title(const string& t) {
if (t.empty()) cmpt::error("title can't be empty");
title = t;
}
void set_author(const string& a) {
if (a.empty()) cmpt::error("author can't be empty");
author = a;
}
void set_pubDate(int pd) {
if (pd < 1900 || pd > 2015) cmpt::error("pubDate out of range");
pubDate = pd;
}
};
Setters¶
often a good idea to put checking code in setters to make sure that the assigned value makes sense
void set_pubDate(int pd) {
if (pd < 1900 || pd > 2015) cmpt::error("pubDate out of range");
pubDate = pd;
}
Methods¶
a function declared in a class is called a method
both setters and getters are examples of methods
we will sometimes say “function in a class” to mean the same thing
consider print
void print(const Book& b) {
cout << " Title: " << b.get_title() << "\n"
<< " Author: " << b.get_author() << "\n"
<< "Pub. Date: " << b.get_pubDate() << "\n";
}
this is a function because it is not declared inside Book
Methods¶
however we could put it inside book like this
class Book {
// ...
public:
// ...
void print() const {
cout << " Title: " << get_title() << "\n"
<< " Author: " << get_author() << "\n"
<< "Pub. Date: " << get_pubDate() << "\n";
}
};
we’ve renamed it to print
since print_book
is redundant
it is a const
method because it does not change the values of any
variables
instead of b.get_title()
, we just write get_title()
, etc.
Methods¶
the calling syntax of a method is different than a function’s
print(b); // print is a function
b.print(); // print is a method in Book
when we discuss inheritance and polymorphism, we’ll see that the print
method might not be known at compile-time (!)
Destructors¶
a destructor is a special method in an object that is automatically called when the object is deleted
destructors cannot be called manually!
class Book {
private:
// ...
public:
// ...
// destructor
~Book() {
cout << title << " destroyed!\n";
}
// ...
}
}; // class Book
consider this program:
int main() {
Book b{"To Mock a Mockingbird", "Raymond Smullyan", 1984};
b.print();
}
after b.print()
is called, object b
is destroyed because b
has
gone out of scope
when b
is destroyed, its destructor is called, and so a message is printed
practically, speaking, Book
doesn’t need a destructor, except possibly for
debugging
however, we will soon see examples of classes where destructors are incredibly useful
Using Constructors and Destructors for Tracing¶
the Trace
class is a simple way of tracing C++ functions
by declaring a Trace
object at the start of a function, you get a message
when the function begins and (thanks to the destructor) when it ends
this can be quite useful when debugging programs
Using Constructors and Destructors for Tracing¶
an extra feature of Trace
is the static
variable indent
indent
keeps track of how many spaces each message should be printed to
the screen
indent
is a static
variable, which means there is a single copy of
indent
shared by all Trace
objects
static variables are owned by the class, not the object
#include <iostream>
using namespace std;
class Trace {
private:
string message;
static int indent;
public:
Trace(const string& msg)
: message(msg)
{
cout << string(indent, ' ') << message << " ...\n";
indent += 3;
}
~Trace() {
indent -= 3;
cout << string(indent, ' ') << " ... " << message << "\n";
}
};
// static variables are initialized outside their class
int Trace::indent = 0;
int recursive_sum(int n) {
// note the use of the standard to_string function
// to convert an int to a string
Trace trace("recursive_sum(" + to_string(n) + ")");
if (n <= 1) {
return n;
} else {
return n + recursive_sum(n - 1);
}
}
int main() {
Trace trace("main()");
cout << recursive_sum(10) << "\n";
}
The this Pointer¶
C++ adds a special variable called this
to every object
this
points to the object itself
for example:
class Book {
private:
string title;
string author;
int pubDate;
public:
string get_title() const {
return this->title;
}
// setters
void set_title(const string& t) {
if (t.empty()) cmpt::error("title can't be empty");
this->title = t;
}
// ...
};
this
is a variable of type Book*
using the standard ->
notation, we can access any variable/function inside
of Book
why would you ever use this
?
one reason is that if you overload operator=
for an object, you need to
check for self-assignment using this
class Book {
private:
string title;
string author;
int pubDate;
public:
// ...
Book& operator=(const Book& other) {
if (&other != this) {
title = other.title;
author = other.author;
pubDate = other.pubDate;
}
return *this;
}
};
operator=
is tricky — it must check for self-assignment
i.e. statements like b = b
when self-assignment occurs, nothing needs be copied
the only way to check for self assignment is to use this
also, operator=
is required to return the object itself, hence it returns
*this
a second reason you might want to use this is for source code clarity, e.g.:
class Book {
// ...
public:
// ...
Book& operator=(const Book& other) {
if (&other != this) {
this.title = other.title;
this.author = other.author;
this.pubDate = other.pubDate;
}
return *this;
}
};