Chapter 10 Notes¶
Please read chapter 10.1 of the textbook. You do not need to read sections 10.2 and onwards (at least for this course — classes will be a topic in later courses).
Structures¶
in C/C++, a struct
is used to define a collection of (often different)
values
for example
struct Person {
string name;
int age;
}; // note the semi-colon: it's an error to leave it out!
we use it to create variables of type Person
Person p{"Bill", 61};
cout << p.name << "\n" // Bill
<< p.age << "\n"; // 61
p.name = "William";
p.age++;
cout << p.name << "\n" // William
<< p.age << "\n"; // 62
we use the dot-notation to access the members of p
Structures¶
it’s usually best to use the {}-notation to provide initial values to a structure as we have been doing
however, the {}-notation is new in C++11, and doesn’t work in C or older versions of C++
in that case you can do something like this
Person p; // p.name and p.age have unknown values
p.name = "Bill";
p.age = 61;
cout << p.name << "\n" // Bill
<< p.age << "\n"; // 61
in C++11, it is almost always best to use the {}-notation to ensure your structures have sensible initial values
unknown initial values can be the source of subtle errors!
Passing a Structure to a Function¶
you can pass structures to functions
void print1(Person p) { // p is passed by value;
cout << p.name << "\n" // i.e. a copy of p is made
<< p.age << "\n";
}
void print2(Person& p) { // p is passed by reference;
cout << p.name << "\n" // i.e. no copy of p is made
<< p.age << "\n";
}
void print3(const Person& p) { // p is passed by constant
cout << p.name << "\n" // reference; i.e. no copy is made
<< p.age << "\n"; // and the compiler ensures p is not
} // modified
for print
, the best way to pass p
is by constant reference here to
avoid making a copy of the data in p
this avoids making a copy and tells any other programmer that use print
that they don’t need to worry about it modifying the Person
struct passed
to it
Examples of Structures: dates¶
times and dates are a surprisingly complex topic
here lets look at a simple representation of a date as a struct of integers
struct Date {
int day; // starts at 1
int month; // starts at 1
int year;
}; // don't forget the semi-colon!
a Date
struct is essentially a triple of 3 integers
(8, 11, 2014) is November 8th, 2014
(16, 7, 1969) is July 16th, 1969
not all triples of numbers are valid dates
(31, 4, 2011) is an invalid date: there is no April 31st
(29, 2, 1900) is an invalid date: there was no February 29th in 1900
recognizing all valid dates is tricky!
Examples of Structures: dates¶
the following programs shows some simple code for processing dates
it includes an incomplete date validity checker (it doesn’t handle leap years)
it prints dates in only two different formats (there are many different date formats!)
// dates.cpp
#include "cmpt_error.h"
#include <iostream>
#include <vector>
using namespace std;
struct Date {
int day;
int month;
int year;
}; // don't forget the semi-colon!
const vector<string> months = {
"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"
};
// Returns true if d holds a valid date, and false otherwise.
// Numbering starts at 1 for both days and months.
// Does *not* handle leap years!
bool is_valid(const Date& d) {
// first check that basic ranges are satisfied
if (d.day < 1 || d.day > 31) return false;
if (d.month < 1 || d.month > 12) return false;
if (d.year < 0) return false;
// different months have different numbers of days
// 28 days: feb (leap years are not handled!)
// 30 days: apr, jun, sep, nov
// 31 days: all other months
switch (d.month) {
case 2:
return d.day <= 28;
case 4: case 6: case 9: case 11:
return d.day <= 30;
default:
return true;
}
}
string get_month(const Date& d) {
if (!is_valid(d)) {
cmpt::error("invalid date");
}
return months[d.month - 1];
}
string get_day_ordinal(const Date& d) {
if (!is_valid(d)) {
cmpt::error("invalid date");
}
if (d.day == 11 || d.day == 12 || d.day == 13) {
return "th";
}
int last_digit = d.day % 10;
switch (last_digit) {
case 1: return "st";
case 2: return "nd";
case 3: return "rd";
default: return "th";
}
}
void print(const Date& d) {
cout << get_month(d) << " "
<< d.day << get_day_ordinal(d) << ", "
<< d.year;
}
void println(const Date& d) {
print(d);
cout << "\n";
}
void print_raw(const Date& d) {
cout << "("
<< d.day << ", "
<< d.month << ", "
<< d.year << ")";
}
void println_raw(const Date& d) {
print_raw(d);
cout << "\n";
}
int main() {
for(int i = 1; i < 31; ++i) {
Date d{i, 11, 2014};
print(d);
cout << " ";
println_raw(d);
}
}
Structures within Structures¶
you can put structures within structures when it makes sense
struct Point {
double x;
double y;
}; // don't forget the semi-colon!
struct Circle {
Point center;
double radius;
}; // don't forget the semi-colon!
struct Rectangle {
Point corner;
double width;
double height;
}; // don't forget the semi-colon!
both circles and rectangles have constraints on the values they can take on
so lets write some checking functions to test that they are valid
bool valid_circle(const Circle& c) {
return c.radius > 0.0;
}
bool valid_rectangle(const Rectangle& r) {
return r.width > 0.0 && r.height > 0.0;
}
next, lets write functions to calculate perimeters and areas
// Pre-condition:
// c.radius > 0
// Post-condition:
// returns the area of c
double perimeter(const Circle& c) {
if (!valid_circle(c)) cmpt::error("invalid circle");
return 2.0 * 3.1415926 * c.radius;
}
// Pre-condition:
// c.radius > 0
// Post-condition:
// returns the area of c
double area(const Circle& c) {
if (!valid_circle(c)) cmpt::error("invalid circle");
return 3.1415926 * c.radius * c.radius;
}
// Pre-condition:
// c.radius > 0
// Post-condition:
// returns the area of c
double perimeter(const Rectangle& r) {
if (!valid_rectangle(r)) cmpt::error("invalid rectangle");
return 2.0 * (r.width + r.height);
}
// Pre-condition:
// c.radius > 0
// Post-condition:
// returns the area of c
double area(const Rectangle& r) {
if (!valid_rectangle(r)) cmpt::error("invalid rectangle");
return r.width * r.height;
}
these functions have been written pretty carefully
notice that the documentation (pre-condition and post-condition) and error- checking is longer than the function itself!
the if-statement at the start of each function does decrease the performance of the functions
so it’s possible in some applications this might cause the functions to run too slowly
so you could remove the error checks, and run the risk of not catching errors
or, you could re-think how this code is designed and make it object-oriented
we won’t do any object-oriented programming in this course, but it is worth mentioning that, in this example, an object-oriented design could improve the performance and keep the safety
Sorting with Structs¶
the standard C++ sort
is efficient and flexible
if you want to sort, say, a vector of structs, then you need to write a comparison function
for example
struct Date {
int day; // 1 <= day <= 31
int month; // 1 <= month <= 12
int year; // 0 <= year
};
// Pre-condition:
// a and b are valid dates
// Post-condition:
// returns true if a comes before b
// Note:
// This is not necessarily the fastest way
// to compare two dates. The intention is for the
// code to be clear an explicit, so that it can
// be seen to be correct more easily.
bool less_than(const Date& a, const Date& b) {
if (a.year < b.year) return true;
if (a.year > b.year) return false;
// at this point we know a.year == b.year
if (a.month < b.month) return true;
if (a.month > b.month) return false;
// at this point we know a.month == b.month
if (a.day < b.day) return true;
if (a.day > b.day) return false;
// at this point we know a.day == b.day
return false;
}
int main() {
vector<Date> dates = {
Date{1, 2, 3},
Date{25, 12, 2014},
Date{25, 12, 2016},
Date{2, 9, 2014}
};
sort(dates.begin(), dates.end(), less_than);
for(Date d : dates) {
println(d);
}
}
the println
function used here is defined above
if you want the dates sorted from most recent to earliest, use reverse
sort(dates.begin(), dates.end(), less_than);
reverse(dates.begin(), dates.end());
Structs and Pointers¶
struct Assignment {
string name; // e.g "Assignment 1"
string short_name; // e.g. "a1"
int out_of; // > 0
};
we could use Assignment
with pointers like this
Assignment* a1 = new Assignment; // a1 points to an Assignment
(*a1).name = "Assignment 1";
(*a1).short_name = "a1";
(*a1).out_of = 10;
cout << (*a1).name << "\n"
<< (*a1).short_name << "\n"
<< (*a1).out_of << "\n";
delete a1;
here we create an Assignment
object dynamically
a1
has the type “pointer to Assignment
“
*a1
has the type Assignment
so to access the members of *a
, we use dot-notation, e.g. (*a).name
the expression *a1.name
is incorrect because in C++ the .
operator has
higher precedence than the *
operator, and so C++ evaluates a1.name
first
but since a1
is a pointer, it does not have a member called name
so *a1.name
causes an error, and we must use brackets to force *a1
to
be evaluated first
Structs and Pointers¶
pointers to structs are very common in C/C++ programs
the notation (*a1).name
is awkward and hard to read
so C/C++ provide a special ->
operator for this case
a1->name
is the same as (*a1).name
Assignment* a1 = new Assignment; // a1 points to an Assignment
a1->name = "Assignment 1";
a1->short_name = "a1";
a1->out_of = 10;
cout << a1->name << "\n"
<< a1->short_name << "\n"
<< a1->out_of << "\n";
delete a1;
once you get used to it, the ->
is quite readable