A "Feild" Guide to Programming with C++
Introductory programming notes by Henry FeildChapter 8: Structs and classes
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.
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.
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.
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.
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.
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: