We are already provided with a few types that do simple, everyday jobs. We have int to describe integers, double to describe decimal numbers, bool to describe true and false, and std::string to describe text. Those types are perfect for describing simple, everyday ideas.
However, the world is full of much more particular varieties.
A game program needs to describe a player. A shop program needs a way to describe a sale. A library program needs a way to describe a book. Those ideas all fit uncomfortably into a single string or a single int.
Normally, the special thing has a few related bits of data. For instance, a player may need a name, a score, and health data. It would be convenient to have three variable declarations, like so.
#include <string>
int main()
{
std::string player_name{ "Mira" };
int player_score{ 120 };
int player_health{ 100 };
return 0;
}
The three variables are right beside each other, and the long names all start with the same word. A human can guess they refer to the same thing. The compiler, who isn't going to guess anything, sees three unrelated, unconnected pieces of data.
The type system, which is already oblivious as to whether they constitute a whole, doesn't say they do.
If some later writer wants a player to be passed in, he can't pass them on all at once; he must instead pass a bunch of number of types individually without the compiler catching him and unintentionally ending up handing me my wife's score instead of my health.
A class solves that problem by allowing us to create a new type whose one job is to contain just the set of data we intend to group together. Creating a type like this starts with the reserved word class followed by the name of the new type we wish to create. By convention, the type name is given in Pascal case with each component capitalized as in Player, Book, or GameCharacter.
The name is then followed by a pair of braces that encapsulate the class body.
Following this pair of braces is another, again required, semicolon. That semicolon at the end is quite easy to forget; and if it is omitted, the compiler does not give an error about it immediately, but instead from the following line which can often lead newcomers astray.
Between the braces we specify what data the new type will hold. Each of those data is a data member, a data item if you like, specified just as if you were declaring a normal variable: a type, a variable name, and, almost always, a pair of empty braces that value initialise the member. For example, here's a basic Player class that collects the three pieces of data we considered above:
#include <string>
// Define the members that belong to this type
class Player
{
public:
std::string name{};
int score{};
int health{};
};
The keyword public: appears at the beginning of the body. For now, this is required syntax and merely makes the players public to the rest of the program; the rules for access will be explained in a later lesson. After this label are the three data members.
name is a std::string to store the player's name, score is an int to store the current score, and health is an int to store the remaining health.
Each data member is followed by {} so that a new Player object will initialize to a known state: blank, zero, zero. Otherwise, the data members would be left uninitialised, and reading an uninitialised int is undefined behaviour.
The same behaviour is valid for any other specialised concept. For instance, a library system could define a book as having a title, an author, number of pages, and a Boolean indicating if it is currently loaned or not.
#include <string>
class Book
{
// Members below this label are public
public:
std::string title{};
std::string author{};
int page_count{};
bool available{};
};
Once again, there's a data member declared with its type, and a value initializer pair of braces, and then a semicolon. This class definition isn't creating any real books, it's only describing to the compiler what sorts of data all eventual book objects will hold. We already do the same thing with built-in types: knowing what an int is doesn't create any integer variables, it only describes what sort of value can be put in them.
Since a class should be an implementation of a well-defined concept, then the data members should all be directly related to that concept. A class describing a book should store data relating to a book; a class representing a player should store information about a player. Combining unrelated data, such as recording the score of a player, the weather, and the number of disks in a disk drive inside the same class creates a data type that no longer serves a single well-defined concept and soon becomes confusing.
Its data members should have obvious names, for example title, page_count, score, and width, are all very clear.
The standard C++ rules for variable names still apply: no special characters, no starting with numbers, not a keyword. When spelling out names in full is inconvenient then use underscores between words: for instance, page_count or maximum_score.
A class is not just data. It can also have functions that manipulate this data. A function written within the class definition is known as a member function.
Data members specify what a type can hold; member functions specify what values of that type can do.
For instance, a program to count might just need one integer and a function to increment it.
class Counter
{
public:
int value{};
void increase()
{
// Increment the value before the expression uses it
++value;
}
};
The body of increase is written inside the class definition. Since it is a part of the class, it is allowed to access the data member value without any additional notation. The rectangle class can also store its width and height and provide a member function to compute the area.
class Rectangle
{
public:
double width{};
double height{};
double area()
{
return width * height;
}
};
Within area, the variables width and height refer to the data members of the specific rectangle for which the function will be called later. We will not worry about the specifics of how to call member functions until a later lesson; for now, the important thing to notice is that the class can hold both the data and the functions that operate on the data together.
In the little programs we write in class, we usually define the class above main. That order ensures that the compiler has seen the new type in time for a command to use it.
#include <string>
class Player
{
public:
std::string name{};
int score{};
void reset_score()
{
score = 0;
}
};
int main()
{
return 0;
}
The program declares a full class, and nothing else. As a declaration, it compiles fine, since a class definition is legal, it is just a new data type. We haven't declared any objects of that type yet, which is just what we need when we are still working out how to describe a type.
When reading through a class definition, look at the name at the top. Once you have the name, then you know what concept this type is supposed to be modeling. Next, you should look at the data members enclosed in the brackets.
Think about the data members; they should be pieces of data that make perfect sense to be associated with the concept.
Then if there are member functions, think about what they do, and see if the data members used are what you thought they should be. Lastly, identify the three pieces of punctuation; start with class, have a pair of matching brackets which contain the members, then end with a semicolon. These three things are a no-brainer; but everything else involved is a design decision involving what data members this new type should have.
Exercise: Defining a ClassStarter code, hints and solutionOpenClose
Practice: Defining a Class
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Create Players Ada with score 10 and Ben with score 7. Reset only Ada's score, then print both names and scores to show their independent state.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <string>
class Player
{
public:
std::string name{};
int score{};
void reset_score()
{
score = 0;
}
};
int main()
{
return 0;
}Hints
Player is defined, but main does not create one yet. Start with Player first{}; then set its public name and score members through first.
Create a second Player and call reset_score() on only one object. Each object owns its own score, so resetting one must not reset the other.
Worked solution
Two Players are initialized with separate names and scores. reset_score is called only on first, so Ada has 0 afterward and Ben still has 7. The second object does not share the first object's member storage.
#include <iostream>
#include <string>
class Player
{
public:
std::string name{};
int score{};
void reset_score()
{
score = 0;
}
};
int main()
{
Player first{"Ada", 10};
Player second{"Ben", 7};
first.reset_score();
std::cout << first.name << ' ' << first.score << '\n';
std::cout << second.name << ' ' << second.score << '\n';
return 0;
}
Example output
Ada 0
Ben 7
0 comments
No comments yet. Be the first.