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;
    }

};