15. Week 8 Tuesday: Structs and Object-Oriented Programming
≪ 14.1. Some Midterm Practise Solutions | Table of Contents | 16. Week 8 Thursday: Practise with Structs ≫A few weeks ago, we introduced functions in C++ as ways to “teach” C++ some of the more complex verbs that can appear within pseudocode, such as determining if a number is prime or computing the divisor sum of an integer.
There’s still something that gets in the way of cleanly converting pseudocode into C++ code, and that’s the presence of complex nouns. Consider, for instance, the following scenario:
Problem 1. Tea House
You are running a modest tea house, where people can order tea with the following attributes:
- The type of tea — green, oolong, or black tea.
- The size — 8oz, 12oz, or 16oz.
- Sugar or no sugar.
- Iced or hot.
All teas cost $3.00/$3.50/$4.00, depending on the size alone. However, sugar costs an additional 25 cents.
Write a C++ program that prompts a user for an order, which may consist of any number of drinks. Then, print out a receipt detailing each drink in the order, along with a total cost. For instance, the receipt may look like:
$3.50 Hot green tea (12oz)
$4.25 Iced black tea (sweetened, 16oz)
$3.00 Hot oolong tea (8oz)
-----
$10.75 Total
This is a bit more complex than the programs we have been writing in discussion thus far. Doing this without structs would be quite cumbersome: one can imagine that one needs to keep track of the four different characteristics for every drink in the customer’s order, hence we would need at least four different vectors to keep track of each of these user inputs!
Nevertheless, if we continue to think about pseudocode as “natural language”, the structure of our program should be fairly straightforward:
- Create an empty list of drinks that represents the user’s order.
- Ask the user for a drink order (type of tea, size, sugar, and iced/hot). Add this drink to their order.
- Ask the user if they’d like to order more. If yes, go back to step 2; otherwise, continue to step 4.
- Initialise a double called
total
to0
. - For each drink in the user’s order, perform the following:
- Compute the price of the drink and add the price to
total
. - Print out the price and the drink itself to the screen.
- Compute the price of the drink and add the price to
- Print out the total cost of the user’s order.
When we write our pseudocode, and when we think about this problem as humans, the data of a single drink is treated as a single noun or variable rather than four separate variables (for the tea type, size, sugar, and iced/hot). We need a single list of drinks rather than four lists for each attribute; we compute the price of a drink rather than the price of four different attributes; etc.
This is the role of structs (and later, classes) in C++ — they allow us to define new complex data types (representing certain nouns in pseudocode) comprised of several simpler pieces of information.
Remark 2. Object-Oriented Programming
This is our first foray into a design paradigm called object-oriented programming (OOP), where programs are constructed as systems of “objects” that can interact with each other and the user. Within pseudocode, the structure of the program is built around the nouns, and the verbs arise as interactions of these nouns with each other and with the “outside world”. This is not a universally applicable design paradigm, but it’s a very intuitive one that can often simplify and modularise complex problems with many moving parts.
Defining the struct
To define a struct, we need to know
- what kind of data it’s associated with (what “is” the struct?) and
- what kinds of actions can be performed on it (what can the struct “do”?).
In this case, a single drink has four pieces of data:
- a string representing the type of tea (green, oolong, or black),
- an integer representing the size of the drink in ounces,
- a boolean representing whether or not there’s sugar, and
- a boolean representing whether or not the drink is hot.
In addition to this, we need to be able to compute the price of a single drink and be able to print out a nicely formatted version of the drink to the screen!
Our first step is to just define the struct, which entails writing down the member variables (i.e., the data that comprise each object) and also function signatures for the different “actions” it should support. I’ll put this in its own header file rather than above a main function, as this might take up a bit of space.
drink.hpp
1#ifndef DRINK_H
2#define DRINK_H
3
4#include <string> // needed for string variables
5
6using namespace std;
7
8struct drink {
9 // member variables - what "is" a drink?
10 string tea_type;
11 int size;
12 bool sugar, hot;
13
14 // member functions - what can a drink "do"?
15 // prints a nicely formatted version of the
16 // drink's name.
17 void print_name();
18
19 // computes the price of the drink
20 double price();
21
22 // constructor - what information do we need to
23 // create a new drink?
24 drink(string _type, int _size, bool _sugar, bool _hot);
25};
26
27#endif // header guard
Note that we have not implemented any of the member functions yet; they are only function signatures! In addition, the two member functions do not take any arguments. Since they are within a drink
struct, they will be performed by a drink
object, in much the same way that the push_back
function is always attached to an existing string or vector.
In addition to this core data, we threw in a “constructor” at the end. This is a special function that C++ runs when we want to create a new variable of type drink
. While some variables types like vector
s and string
s can be declared without any initial value (i.e., empty vector
s and string
s), it doesn’t really make sense to just have a “blank” drink. Every drink needs to have the four constituent variables filled out in order to make sense, otherwise we can’t create the drink. Making a constructor is how we provide this semantic information to C++: we’re saying in order to make a drink, I need to know some kind of information first, and using that information I can then populate the necessary data fields.
Some things to note — first, we still need to have our #include
directives and the using namespace std;
within our header file, just like the other programs we’ve been writing! Second, we put a semicolon after the closing curly braces. This is required for structs (and later, for classes), unlike almost every othe use of curly braces!
Filling out the main
function
Before creating the implementation file drink.cpp
, where we will eventually provide the inner workings of the constructor and the two member variables, let’s revisit our pseudocode and try to create the main function.
The file main.cpp
will start the way it almost always does, but at the top of the file, we will also need to include the drink.hpp
file. By default, C++ does not understand what a drink
is; the “blueprint” for this data type is only provided in the header file.
1#include <iostream>
2#include <string>
3#include <vector>
4
5#include "drink.hpp"
6
7using namespace std;
8
9int main() {
10 // ...
Let’s fill out the inside of the main function. Recall that step 1 was to create an empty list of drink
variables.
1vector<drink> order;
For the second and third step, we’ll need to set up a while loop. As long as the user wants to add more drinks to their order, we’ll do just that. Let’s address the while loop later, though, and first demonstrate how to get the user to input a single drink.
1cout << "Enter tea type [green/oolong/black]: ";
2string tea_type;
3cin >> tea_type;
4
5cout << "Enter drink size [8/12/16]: ";
6int size;
7cin >> size;
8
9cout << "Add sugar? [y/n]: ";
10char sugar;
11cin >> sugar;
12
13cout << "Hot or iced? [h/i]: ";
14char temperature;
15cin >> temperature;
It doesn’t really matter how you prompt this information. Now let’s put all this together into a single drink
variable and add it to the order. We’ll call the constructor that we have yet to write when we declare that variable:
Note that we had to convert the user’s input, which was a single character, into a boolean in order to correctly use our constructor. An alternative is to directly set the member variables of the struct, but that would take many more lines of code and is not the best practise (more on this later).
Let’s now wrap this in an input loop. After asking for a new drink, we should ask if the user wants to continue ordering more drinks or not. I think this was one of your homework assignments, but try doing this yourself if you haven’t already.
1char response = 'y';
2while(response == 'y') {
3 // prompt the user for an order.
4 cout << "Enter tea type [green/oolong/black]: ";
5 string tea_type;
6 cin >> tea_type;
7
8 cout << "Enter drink size [8/12/16]: ";
9 int size;
10 cin >> size;
11
12 cout << "Add sugar? [y/n]: ";
13 char sugar;
14 cin >> sugar;
15
16 cout << "Hot or iced? [h/i]: ";
17 char temperature;
18 cin >> temperature;
19
20 // add the new drink to the order
21 drink new_drink(tea_type, size, sugar == 'y', temperature == 'h');
22 order.push_back(new_drink);
23
24 // ask if they want more
25 cout << "Order another? [y/n]: ";
26 cin >> response;
27}
Wheee that was a lot of code. But this is mostly stuff we’ve done before — a lot of asking for inputs and printing stuff out. The main meat of this code happens when we declare the new_drink
variable, which is secretly just a bundle of four smaller variables!
Let’s move on to the remaining steps, steps 4-6
. Here, we’ll be able to pretty much convert the pseudocode into real code by reading it off.
1// 4. initialise the total cost of the order to 0
2double total = 0;
3
4// 5. for each index i in the order, repeat:
5// * get the drink at index i
6// * compute the price of said drink
7// * add the price to the total
8// * print out the cost, followed by the drink name.
9for(int i = 0; i < order.size(); i++) {
10 drink curr_drink = order.at(i);
11 double cost = curr_drink.price();
12 price += cost;
13 cout << "$" << cost << " ";
14 curr_drink.print_name();
15}
16
17// 6. print out the total cost.
18cout << "$" << total << " total" << endl;
What? It feels like we’ve done no real coding. curr_drink.price()
is the price of the current drink because that’s how we set it up. What is there to say? This is the magic of structs and modularising code — if we define the right functions and the right structs in just the right way, our code can begin to resemble a the broken English a Chinese toddler might say.
Here’s the entire main.cpp
file, put in one place:
main.cpp
1#include <iostream>
2#include <string>
3#include <vector>
4
5#include "drink.hpp"
6
7using namespace std;
8
9int main() {
10 // create an empty list of drinks
11 vector<drink> order;
12
13 char response = 'y';
14 while(response == 'y') {
15 // prompt the user for an order.
16 cout << "Enter tea type [green/oolong/black]: ";
17 string tea_type;
18 cin >> tea_type;
19
20 cout << "Enter drink size [8/12/16]: ";
21 int size;
22 cin >> size;
23
24 cout << "Add sugar? [y/n]: ";
25 char sugar;
26 cin >> sugar;
27
28 cout << "Hot or iced? [h/i]: ";
29 char temperature;
30 cin >> temperature;
31
32 // add the new drink to the order
33 drink new_drink(tea_type, size, sugar == 'y', temperature == 'h');
34 order.push_back(new_drink);
35
36 // ask if they want more
37 cout << "Order another? [y/n]: ";
38 cin >> response;
39 }
40
41 // 4. initialise the total cost of the order to 0
42 double total = 0;
43
44 // 5. for each index i in the order, repeat:
45 // * get the drink at index i
46 // * compute the price of said drink
47 // * add the price to the total
48 // * print out the cost, followed by the drink name.
49 for(int i = 0; i < order.size(); i++) {
50 drink curr_drink = order.at(i);
51 double cost = curr_drink.price();
52 price += cost;
53 cout << "$" << cost << " ";
54 curr_drink.print_name();
55 }
56
57 // 6. print out the total cost.
58 cout << "$" << total << " total" << endl;
59
60 return 0;
61}
Implementing the struct
There’s still one last missing step — we need to actually implement the functions and constructor in the drink
struct. This will happen in the drink.cpp
file. We’ll kick things off by making a skeleton that includes the function signatures of each member function and constructor in the struct:
1#include <iostream>
2#include <string>
3
4#include "drink.hpp"
5
6using namespace std;
7
8drink::drink(string _type, int _size, bool _sugar, bool _hot) {
9
10}
11
12void drink::print_name() {
13
14}
15
16double drink::price() {
17
18}
Note that we’ve prepended the characters drink::
to each of these function shells. Without them, C++ will assume that we’re just creating isolated functions called drink
, print_name
, and price
that don’t belong to any struct at all. The drink::
makes clear to C++ that these functions all belong to the drink
struct, and this will make the member variables all accessible to these functions.
Let’s start with the constructor. We’ll use something called an initialiser list to do this.
drink::drink(string _type, int _size, bool _sugar, bool _hot) :
tea_type(_type), size(_size), sugar(_sugar), hot(_hot) {
// empty body
}
The syntax of the initialiser list can maybe be inferred from the above. In between the closing parentheses )
and the opening curly brace {
, we add a colon :
followed by a comma-separated list of statements of the form [member variable name]( [value] )
. This will tell C++ to set whatever member variable to the value within the parentheses whenever the constructor is called — for instance, we are setting tea_type
to the value of _type
.
The body of our constructor is blank, as all we need to do is set up some variable values. Often, the body of the constructor is used to do some additional processing, but perhaps we’ll see an example of this another time.
Next up is the print_name
function, which just…prints out a nicely formatted version of the drink it’s attached to.
1void drink::print_name() {
2 if(hot) { cout << "Hot "; }
3 else { cout << "Iced "; }
4
5 cout << tea_type << " tea (";
6
7 if(sugar) {
8 cout << "sweetened, ";
9 }
10
11 cout << size << "oz)" << endl;
12}
And finally, we need to be able to compute the price, which depends solely on the size of the drink and the presence of sugar.
double drink::price() {
double p = 0;
if(sugar) { p += 0.25; }
if(size == 8) { p += 3; }
else if(size == 12) { p += 3.5; }
else { p += 4; }
return p;
}
Here are the full contents of the drink.cpp
file:
drink.cpp
1#include <iostream>
2#include <string>
3
4#include "drink.hpp"
5
6using namespace std;
7
8drink::drink(string _type, int _size, bool _sugar, bool _hot) :
9 tea_type(_type), size(_size), sugar(_sugar), hot(_hot) {
10 // empty body
11}
12
13void drink::print_name() {
14 if(hot) { cout << "Hot "; }
15 else { cout << "Iced "; }
16
17 cout << tea_type << " tea (";
18
19 if(sugar) {
20 cout << "sweetened, ";
21 }
22
23 cout << size << "oz)" << endl;
24}
25
26double drink::price() {
27 double p = 0;
28 if(sugar) { p += 0.25; }
29
30 if(size == 8) { p += 3; }
31 else if(size == 12) { p += 3.5; }
32 else { p += 4; }
33
34 return p;
35}