Important takeaways

  • structs are containers for groups of related variables called data members
  • classes are similar to structs, but contain both data members and methods
  • struct and class names should start with an upper case letter
  • if you've named your struct or class Student, create an instance by declaring a new variable of type Student: Student student;
  • a class constructor can be used to populate the data members of a class instance; create an instance of a class with a constructor by doing, e.g.: Student student("Pat", "3932", 90);
  • to access data members from a struct or class, use the dot notation: student.name
  • to invoke an instance method, also use dot notation, do: student.print()
  • classes have different access levels, such as private and public, and changes how data members and methods are accessed from outside of the class

Contents

Introduction

C++ has two constructs that allow programmers to create custom types beyond int, float, and the like. These constructs also allow us to store data and define behavior. In this chapter, we'll learn about these two constructs: structs and classes.

Structs

What is a struct?

It is common that we write programs that have to track things like people, inventory, accounts, etc. For instance, say we are creating a program that keeps track of student grades in a class. For each student, there are several pieces of information we would like to track: their name, student id, and overall grade. If we are tracking a single student, that's simple—we just store each piece of information in a variable, for a total of three variables. Here's what it might look like:

Now what if we want to track a class of 10 students? We need to declare three variables per student—that's 30 variables! Here's what it looks like for the first few students...

Note that we've prepended studentN for N from 1 to 10. This is clearly tedious and error prone. What would be great is if we could have one variable per student, and have that variable somehow encapsulate all of the pieces of information we want.

In C++, one way we can do this is using structs. A struct is a C++ construct that allows us to bundle a set of variables (we call these fields or data members) and give it a name. That name is used exactly like a variable type (e.g., int or string). So, we can then declare a new variable of this type and access all of the fields.

(Back to top)

Defining a struct

The first thing we need to do in using a struct is to define one. We do this below our header, and above main. The format is: the keyword struct followed by the name of the struct (start with a capital letter to make it obvious that it's a type and not a variable), followed by a pair of curly braces and a semi colon. We then declare (but do not initialize) any variables we need inside of the curly braces. Here's what a struct definition might look like to hold a single student's data. We're calling it Student:

That's all there is to defining a struct.

(Back to top)

Creating an instance of a struct

Now that we've defined a struct, we need to create an instance of it. We can use the name of a struct in the exact same way we use a type. Building on the example from above, we can create a variable that is of type Student. Here's what it looks like in code for the first student:

This creates a new instance of the struct, so student1 in this example now has it's own copy of each of the fields of the Student struct. We still need to set each of the fields, which we'll see how to do next.

(Back to top)

Accessing the fields of a struct instance

Once an instance of a struct has been created, we can access fields by using the notation: variable.fieldName. That is, the variable name, followed by a dot ("."), followed by the field name. To access the name field for the student1 instance, we write: student1.name. We can use that expression just like a variable: we can assign values to the field, print the field, or use it in expressions. Here's how we would initialize the fields for the first student:

Note that we don't need to specify the type of each of the fields for our instance. Why not? Because we already did that in our struct definition. Here's what it looks like when we create instances of Student for a few more of the students:

Now, it may seem like we've actually added additional code. In this example, we did. However, we only have one explicitly declared variable per student; the three variables for the student information are only defined once in our struct definition, so we don't need to redefine them each time we create an instance.

For this example, we can make things a little more efficient by storing our student instances in an array of students (allowing us to iterate over the instances if we need to), and by using a function to take care of creating the instances and assigning values to the fields. Here's what that might look like:

(Back to top)

Classes

What is a class?

Sometimes we want to do more than just encapsulate related data. Sometimes we want to include behavior, such as initializing or accessing the data within, manipulating it, or printing it out. With structs, we can do these sorts of things using functions, much like the initializeStudent() function from above. However, there is a construct very similar to structs called a class that allows us to embed these behaviors directly inside the definition of the class along side data. We'll learn all about how to use classes in this section.

(Back to top)

Basic class definition

A basic class definition looks much like a struct definition. Here's the class version of the Student struct from earlier:

Notice we use class instead of struct on the first line. Also, we've added the line public: just above the data members—this is called an access level, and we'll cover them in more detail below. This class can be initialized and used just like our struct. All of the code from the struct section above will work with this class definition.

(Back to top)

Instance methods

Besides data members, I mentioned that we can define behavior within a class. We do this by defining functions within the class, which we call instance methods (I will generally use the term "method" to talk about instance methods, and "function" to talk about regular functions). For example, we can add a method called print() to the Student struct, which will print out a student's information when invoked:

Notice that we use the data member name, id, and grade within the method. They are not declared in the method, though! Within an instance method, we have access to all of the instance's data members. Because name is not declared within print(), the compiler next checks the scope of the class, which includes all data members of the class. name is present at that level, so the compiler assumes that's the memory we meant when we used name. If name was not a data member, then we would get a compiler error. Something else to understand: the print() method is working with a specific instance of Student—it does not have access to other instances of Student. Invoking the method looks a lot like how we access a data member—we use the dot notation, followed by the method name and arguments within parentheses. Here's how we'd use the print method from above:

Every object has a special data member called this, which is a pointer to the instance itself (we will learn more about pointers in a later chapter). If for whatever reason you find that you have a locally defined variable with the same name as a data member that you need to access, you can access the data member via this. Here's our print() method using the this data member:

We use the arrow syntax (e.g., this->name) because this is a pointer and we have to dereference it. Again, we will learn more about that in a later chapter.

(Back to top)

Constructors

We're always looking for ways to code less. In the example above, we have to initialize each of the fields of our student individually. If we have 10 instances, we have to initialize each of them, resulting in 30 lines of code (yuck!). In our struct example, we saw that we can make a function to take care of creating and initializing an instance. With classes, we can define a special method called a constructor that will take care of this for us. As its name implies, it constructs an instance of an object for us. It is invoked when you create an instance. Like any other function or method, it can take parameters. Unlike other functions and methods, constructors must not have a return type specified, and should be named after the class. For our Student class, the constructor must be called Student. Here's what it looks like:

This constructor takes one parameter per data member. It then sets the data members to the corresponding values passed in. Note that we use the this keyword so we can specify the instance's data members on the left hand side of each assignment, and we leave it off on the right and side to specify the locally defined parameters. If we have this constructor, then we must initialize our Student instances like this:

Look at that, we saved three lines of code! Now, because our constructor says it takes three parameters, we must create instances by passing in all three parameters. So our old code won't work! But we can make it work if we add another constructor. That's right, we can have multiple constructors. The only catch is that they must each have a unique signature. Here's another version of Student that has two constructors, one that takes no parameters, and another that takes three:

We call a constructor with no parameters the default constructor. One other thing about constructors: there is an easier way to set data members. We can use something called a data member initializer list. This can only be used with constructor. It looks like this:

The syntax is, after the constructor signature but before the opening curly brace, put a colon followed by pairs of dataMember(parameter), each separated by a comma. So name(name) will set the data member this->name to the value of the parameter name. The parameter names can be anything you want, as long as they match the parameters in the signature of the constructor. That is, they do not have to match the data member name. Another valid version (though with poor variable names) of the above constructor is:

Literals can also be included in the data member initializer list parentheses. Here's an example where we take a student's name and id as parameters, but always set their grade to 100:

Here's our example from above redone to use data member initializer lists for the constructors:

(Back to top)

Access Levels

So far, any data members or methods we've stuck in a class definition have been accessible on the instance from outside the object. That is, in main(), we can do:

Sometimes it's handy to hide some data members and methods from the "outside" world (outside the class, that is, e.g., from main or another function). There are three levels of access we can use: public, private, and protected. We will only concentrate on the first two for now. By default, every data member and method in a class is considered private unless otherwise noted. In our examples above, we've been override this an making all data members and methods public by putting the public: keyword above the first data member.

To make a data member or method available from any other methods within an object, but not from outside, we can use the private: keyword. In this next example, I've created a new private data member called timesModified:

To motivate why private fields are useful, think about this. Suppose we want to use timesModified to keep track of the number of times our student's information has been modified. In order to track this, we need know when a student's information has been changed. If the name, id, and grade members are public, then any code with access to an instance of a Student can change any of those three fields, and we have no way of monitoring the changes.

One way can can work around this is to make those three fields private, then create getters and setters. These are methods that will get and set the value of the private data members. Inside of these methods, we can track the times modified. Here's an example:

So now, we cannot do student.name = "Patty Smithy";. Instead, we have do student.setName("Patty Smithy");. To get a field, we have to use one of the getters: cout << student.getName() << endl;. We can get the number of times the the student information has been modified by using student.getTimesModified(). Here's the example from earlier updated to use the new version of Student:

(Back to top)