ICS 45C Spring 2022
Notes and Examples: Classes

Includes a code example with the moniker Classes


Background

In C++, a class is a blueprint for a new type of object. When we write a class, we specify everything needed so that our new type of object is able to do everything it needs to do. When we're done, we'll have a full-fledged data type, every bit as capable as built-in types like int or types in the C++ Standard library like std::string.

First of all, it's important to recognize that C++ is not unique in offering a feature like this. There are a number of programming languages that allow you to define classes, and while they all have similarities, C++ offers a decidedly more flexible set of features for defining its classes. The more complex feature set offers the upside of finer-grained control over both the memory and time efficiency of the objects of your classes, as well as being able to design your classes so that they can be used as expressively as the types built into the language. The cost of that flexibility, as is often the case in C++, is a dramatic increase in the level of complexity. There's simply more to know if you want to write classes well in C++; one of the reasons we've waited until now to discuss them is so that we know enough about how C++ works so that we can make the right kinds of decisions properly.

These notes, along with its associated code example, begin our exploration of how to design and write classes in C++, but there is plenty more ground to be covered if we want to gain a full understanding. We'll see more details in later examples.


Data types in C++

Some programming languages draw a distinction between the data types built into the language and those you create. A notable example is Java, which divides all of its types into two categories: primitive types and class types, with all primitive values being statically-allocated (and passed by value) and all objects (the instances of class types) being dynamically-allocated (and passed, effectively, by reference).

C++ does not draw this distinction. The built-in types like int and the user-defined types, like those you create by writing classes, are on an equal footing. Any value of any type is considered to be an object, whether the value is an instance of a class or not. The objectives, rights, and responsibilities are all the same for all types. A type exhibits (at least) the following characteristics:

When we write a class, we're defining a new data type, so these are the various knobs we can turn in our design. Gradually, we'll learn how to turn all of these knobs, but this example begins by introducing only some of them. Here, we will explore the implementation and use of a class called Song, which defines a new kind of object that might represent a song in a media player application. For simplicity, we'll say that a song has two properties that are both strings: an artist and a title. Within that simple framework, though, lie a number of interesting problems; we'll attempt to make the best decisions we can about both design and performance.


Writing a class in C++

Interface vs. implementation

When we write classes in C++, there are two things we're interested in specifying:

For code that only needs to use objects of a class, all that's necessary is that interface; as long as it's clear how the objects are laid out in memory and what can legally be done to them, it's possible to use them. This is consistent with an idea we've seen previously: All you need in order to use something is a declaration of it. In the case of a class, all you need in order to use it is something called a class declaration. Because of this, classes are quite often declared in header files, so that other source files can include those header files to obtain the necessary declaration.

On the other hand, the implementation is something you would write once, generally in its own separate source file. You'd write it in a source file for the same reason we've seen previously: There can be only one definition for things in C++.

Members

A class' interface consists, broadly, of a set of members. There are two kinds of members we'll use for now — though I should point out that there are others that we'll see later.

Public vs. private

Some members are declared public, which means they are intended to be used by code outside of the class they're declared in. Others are declared private, meaning that they are only accessible to code within the class (e.g., private member variables in a class can only by accessed by the member functions in that class). Note that the distinction between public and private members is a compile-time one; the compiler will disallow a program from compiling successfully and report an error if code outside of a class attempts to access one of that class' private members.

In general, you should tend to want to make details of a class' implementation private whenever possible — or leave them out of the interface altogether, using local variables (or other constructs we've yet to see, such as the unnamed namespace) instead. The fewer details you reveal to other parts of a program, the more of those details you can change without breaking the other parts; this is the essence of how you write very large programs so that they can still be maintained over time. It's not about security; it's about writing each piece so that it relies on as few details of the other pieces as possible, so that changes are as controlled and localized as possible.

Class declarations

A class declaration is how we specify the existence of a class. A class declaration is used to specify how objects of a class are used, and has two audiences:

So, in a class declaration, we specify all of the class' members: its member variables and member functions, both public and private. The declaration of a member function is like the function declarations we've seen before; it specifies the existence of the member function without giving it a body.

Using header and source files

As with the declarations of structs, we write the declarations of classes in header files whenever we intend those classes to be used in many source files. However, as with the definitions of functions, we'll write definitions of member functions in a single source file. (Later, we'll see that this distinction is more relaxed when writing classes than it is when you write functions — motivated largely by efficiency and optimization concerns — but we'll start by holding firm to that distinction.)

Quite often, we'll arrange things so that every class is a matched pair of files: a header file specifying the class' declaration and a source file specifying the definitions of its member functions. We'll use that arrangement in this example.


The code

There are a lot of syntactic details, both in terms of how you declare classes, define their members, create objects from them, and use those objects. Many of those details are described in the code example below. Be sure you read through both the code and the associated comments; we'll be relying on your understanding of these details as we go forward from here.

The official moniker for this code example is Classes, so your best bet is to do this:

Alternatively, you can click the link to the tarball below: