Chapter 4 Notes¶
Please read chapter 4 of the textbook.
Top Down Design¶
designing big programs is a very challenging task
we won’t be designing big programs in this course
but it is useful to start thinking about systematic ways to create programs
one way to design a program is to use top-down design
top-down design starts with the general problem and then splits it into smaller sub-parts
each sub-part is refined into more and more detail, eventually reaching the level of program code
Top Down Design¶
suppose you want to design a program to read a file of numbers and print their sum and average
top-down design goes like this
- (first version) read in numbers, print statistics
- (more refined) open file, read in numbers, print statistics
- (more refined) open file, use a loop to read numbers from a file, print statistics
- (more refined) open file, use a loop to read numbers from a file, keep track of sum and count as numbers read in, print statistics
- (more refined) open file, initialize sum and count variables, use a loop to read numbers from the file, keep track of sum and count as numbers read in, print statistics
etc.
the basic idea is to replace each part of such a design with a function
Function Terminology¶
if possible, it’s often best to implement parts of your program using pre- defined functions
they are often faster and more accurate than functions you create yourself
cmath
has many useful basic math functions, e.g. see p. 187 of the
textbook for a list of some
confusingly, a few functions, like abs
and rand
, are found in
cstdlib
of course, we can’t always rely on pre-defined functions being the solution to all are problems
(what if your job is to create these pre-defined functions for a new compiler?)
Function Terminology¶
we need some terminology for talking about functions
consider the pre-defined sqrt
function from cmath
as an example
sqrt(x)
is a function call or function invocation, i.e. we are
calling, or invoking, sqrt
x
is the function’s argument, i.e. the input to the function
sqrt(x)
returns some value, i.e. it evaluates to the square root of
x
all pre-defined functions have documentation, e.g. http://www.cplusplus.com/reference/cmath/sqrt/
the documentation specifies things like the exact type of the arguments and the exact type of the return value
Random Number Generation¶
generating random numbers is useful and interesting
we collectively refer to the sub-programs that are used to get random numbers as a random number generator (sometimes abbreviated RNG)
one basic C++ random number generator uses two functions, srand
and
rand
srand
initializes the random number generator and should be called once at
the start of your program
rand
returns a new random number
Note
Since at least C++11, C++ has come with the standard library
<random>
, which contains a much better and more flexible collection of
random number generators. However, they are more difficult to use, and
since this is a beginning course, we will stick to rand
and srand
.
Random Number Generation¶
this program prints 10 random numbers
#include "cmpt_error.h"
#include <iostream>
#include <cstdlib>
using namespace std;
int main() {
srand(532); // set random number seed
for(int i = 0; i < 10; ++i) {
int r = rand();
cout << r << "\n";
}
cout << "RAND_MAX = " << RAND_MAX << "\n";
} // main
srand
takes a an unsigned int
as input, and does not return any value
it’s job is to initialize the random number generator
rand()
takes no input, but every time you call it returns a new random
number
rand()
always returns an int
from 0 to RAND_MAX
, where
RAND_MAX
is a pre-defined value that is the largest value that rand()
can return
the sequence of numbers rand()
returns on each call depend upon the value
given to srand
using the same seed, you can re-create the same sequence of calls to
rand()
(which can be quite useful for debugging)
Random Number Generation¶
if you run the program multiple times, you’ll always get the same sequence of numbers
that’s because the seed to srand
is always the same
to get different numbers on each run, you need to give srand
a different
seed each run
a common trick for doing this is to use the current time
#include "cmpt_error.h"
#include <iostream>
#include <cstdlib>
using namespace std;
int main() {
srand(time(NULL)); // set random number seed based on current time
for(int i = 0; i < 10; ++i) {
int r = rand();
cout << r << "\n";
}
cout << "RAND_MAX = " << RAND_MAX << "\n";
} // main
Random Number Generation¶
often you want a random number between two given numbers
for instance, to simulate rolling a (fair!) 6-sided die, choose a random number from 1 to 6
int roll = 1 + (rand() % 6);
rand() % 6
returns a value from 0 to 5 (inclusive)
so 1 + (rand() % 6)
returns a value from 1 to 6
the expression 4 + (rand() % 6)
returns a value from 4 to 9 (inclusive)
Random Number Generation¶
// guesser.cpp
#include "cmpt_error.h"
#include <iostream>
#include <cstdlib>
#include <time.h>
using namespace std;
int main() {
srand(time(NULL)); // set random number seed based on current time
// secret is a randomly chosen number from 1 to 10
int secret = 1 + rand() % 10;
cout << "I'm thinking of a number from 1 to 10. Can you guess it?\n";
int guess = 0;
while (guess != secret) {
cout << "What is your guess? ";
cin >> guess;
}
cout << "That's right, you got it!\n";
} // main
Type Casting¶
C++ often needs to convert between different types of values
sometimes, C++ automatically coverts types
int a = 4.9; // 4.9 automatically converted to the int 4
double b = 7; // 7 automatically converted to the double 7.0
C++ lets you explicitly convert between types of data using function-like expressions, and this process is known as type casting
there is old C-style casting and new C++-style casting
double x = static_cast<double>(5); // new-style C++ casting: prefer this style
double y = double(5); // older C++-style casting
double z = (double) 5; // C-style casting
all three statements do the same thing
in C++, the first one with static_cast
is probably the best, because it is
the clearest and most explicit
but you will often see the other 2 in practice
Type Casting¶
double a = 1 / 5;
// 1 / 5 is 0, which is converted to 0.0
double b = static_cast<double>(1 / 5);
// 1 / 5 is 0, and this is explicitly cast to 0.0
double c = 1.0 / 5;
// 5 is converted to 5.0, and 1.0 / 5.0 is 0.2
double d = 1 / 5.0;
// 1 is converted to 1.0, and 1.0 / 5.0 is 0.2
double e = 1.0 / 5.0;
// no conversion since 1.0 and 5.0 are both doubles
it’s important to note that static_cast<double>
is always safe
that means converting from an int
to a double
never loses any
information
Type Casting¶
casting a double
to an int
could lose information
int f = static_cast<int>(3.14); // f gets the value 3
int g = static_cast<int>(2.99); // g gets the value 2
Note
In addition to static_cast
, C++ has other kinds of casting
functions including dynamic_cast
(used with pointers and references),
reinterpret_cast
(converts pointer types), and const_cast
(for
manipulating const
values). However, in this course we will only be
using static_cast
.
Defining Functions¶
C++ lets you define your own functions
defining functions is familiar in mathematics
for example, \(f(x) = x^2\)
we say \(f\) is a function
we can use it to do calculations
like \(f(3) = 3^2 = 9\)
or \(f(1 + f(2)) = f(1 + 2^2) = f(5) = 5^2 = 25\)
Defining Functions¶
C++ lets you define many mathematical-type functions
and also many other kinds of functions that have no mathematical counterpart
for example, the main()
function we use in every program is not a
mathematical function
Defining Functions¶
here’s a C++ equivalent of \(f(x) = x^2\)
double square(double x) { // function header (or signature)
return x * x; // function body
}
the top line of the function is called the function header, or signature
the header of a function contains the information you need to know to call the function
the part between the {
and }
is called the function body
it contains the code that is run when the function is called
Function Headers¶
lets look at just the header of the function
double square(double x) {
// ...
}
the name of this function is square
like variables, functions should usually have clear, self-descriptive names
the double
before the name is the type of value that the function will
return when it is called
square
takes one input which it calls x
x
has type double
Function Bodies¶
now lets look at the body of the square
function
double square(double x) {
return x * x;
}
the variable x
is passed as a parameter to square
, and so we can use
it anywhere in the body
it’s as if we had defined x
at the very start of the function
inside a function, return
indicates the value the function will evaluate
to
when a return
statement is executed, the flow of control jumps out of the
function
so if we had put any statements after return
they would not be executed
in this case our function body has only a single statement, but in general bodies can have any number of statements
Defining Functions¶
// square.cpp
#include "cmpt_error.h"
#include <iostream>
using namespace std;
// returns the square of x
double square(double x) {
return x * x;
}
int main() {
cout << "Please enter a number: ";
double x = 0.0;
cin >> x;
cout << "square(" << x << ") = " << square(x)
<< "\n";
} // main
Another Function¶
here’s a function that calculates the distance between two 2-dimensional points
// returns the distance between the points
// (x1, y1) and (x2, y2)
double dist(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
return sqrt(dx * dx + dy * dy);
}
look at just the header
double dist(double x1, double y1, double x2, double y2)
from this we can tell that the function’s name is dist
that it returns a double
that it takes 4 inputs, all of type double
Another Function¶
notice also that the body consists of three different statements
// returns the distance between the points
// (x1, y1) and (x2, y2)
double dist(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
return sqrt(dx * dx + dy * dy);
}
we’ve also declared two local variables in this function
dx
and dy
are local variables
they only exist in the function
C++ automatically deletes them when the function ends
Functions Return Values¶
it’s important the you use the proper terminology when talking about a function
we say that a function returns a value
functions don’t print values (unless they happen to use cout
)
printing means causing something to appear on the screen
returning means ending a function and providing a return value
Where do functions get their inputs from?¶
// returns the distance between the points
// (x1, y1) and (x2, y2)
double dist(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
return sqrt(dx * dx + dy * dy);
}
where do the value of x1
, y1
, x2
, and y2
come from?
the could come from anywhere
maybe from cin
or a file
or from the return value of some other functions
or as literals written directly in source code of the function call
...
when you call dist
, that’s when you must provide the values of x1
,
y1
, x2
, and y2
Boolean functions¶
a function whose return type is bool
is called a boolean function
bool is_digit(char c) {
if (c >= '0' && c <= '9') {
return true;
} else {
return false;
}
}
recall that char
s are like small integers, with the integers
representing the character’s numeric ASCII/Unicode value
it turns out that the ASCII/Unicode values of the characters '0'
, '1'
,
..., '9'
are in sequential order
for(char c = '0'; c <= '9'; ++c) {
cout << "'" << c << "' = " << static_cast<int>(c) << "\n";
}
this prints
'0' = 48
'1' = 49
'2' = 50
'3' = 51
'4' = 52
'5' = 53
'6' = 54
'7' = 55
'8' = 56
'9' = 57
this means that, for instance, the ASCII/Unicode value of '5'
is 53
the expression c >= '0'
asks if the ASCII/Unicode value of c
is bigger
than, or equal to, the ASCII/Unicode value of '0'
some programmers even write the expression as c >= 48
, because 48 is the
code for '0'
(but that’s pretty confusing!)
similarly, the expression c <= '9'
asks if the ASCII/Unicode value of
c
is less than, or equal to, the ASCII/Unicode value of '9'
Boolean functions¶
the function body is more verbose than necessary
c >= '0' && c <= '9'
is a boolean expression because it evaluates to
either true
or false
so we could just return its value
bool is_digit(char c) {
return c >= '0' && c <= '9';
}
many programmers like this style, but it takes some getting used to
it’s short, simple, and fast (only 2 comparisons!)
void Functions¶
a function that doesn’t return a value uses the keyword void
in place of a
type for its return value
void print_title() {
cout << "BIOS Diagnostics Screen\n"
<< "-----------------------\n\n";
}
void
is not a type, e.g. variables and values cannot be void
sometimes, void
functions are called procedures to distinguish them
from functions that return a value
but we’ll just call them void
functions
Functions as Black Boxes¶
almost all well-designed programs use, and define, a lot of functions
a well-made function is sometimes referred to as a black box
you put input into the black box, and the black returns some output
because the box is black, you can’t see inside of it
but that’s okay, we can still use it
try to write functions that act like a black box
programmers should not need to know how a function is implemented in order to use it
for example, we don’t need to know exactly how C++ implements its sqrt
function in order to use it in our programs; we only need to know what it
does
Functions as Black Boxes¶
functions that work as black boxes often need some documentation explaining what they do
for example, the documentation for sqrt
tells us that it returns the
square root of a number
it says nothing about how it actually calculates the square root
for our functions, we will usually write documentation to make it clear to the programmer what the function is supposed to do
Local and Global Variables¶
a variable declared inside a function is called a local variable
local variables can only be used inside the function in which they are defined
global variables are defined outside of any function
they can be used by any function
here are some global variables
const double PST_RATE = 0.07;
const double GST_RATE = 0.05;
int main() {
cout << "What is the cost of your purchase? ";
double cost = 0.0;
cin >> cost;
double total_tax = (PST_RATE + GST_RATE) * cost;
double total_cost = cost + total_tax;
cout << "The tax is $" << total_tax << "\n";
cout << "The total cost is $" << total_cost << "\n";
} // main
the globals have been declared const
, which means the compile does not
allow them to be changed (i.e. they are read-only)
this can help stop you from making certain kinds of errors
traditionally, constant global C++ variables are written in ALL_CAPS so they can be recognized at a glance
Local and Global Variables¶
experience shows that global variables can lead to subtle bugs
since any code could modify a global variable, it can be hard to see how they change in a large program
the Java language doesn’t even have traditional global variables!
rule of thumb: if you must use global variables in C++, make them
const
it will make your programs easier to understand, and prevent a subtle class of errors
Function Parameters are Local Variables¶
the parameters in a function act like local variables
they come into existence when the function is called
and disappear when the function is returned
they can’t be accessed outside the function
Block Scope¶
the scope of variable where in a program a variable is accessible
C++ use block scope, i.e. variables are accessible inside the inner-most block in which they are defined
{
and }
usually mark the begin and end of a block (an exception is an
if-statement or loop that consists of a single — then the braces are
optional)
note that C++ global variables are not inside any block, and so they are accessible anywhere
Overloading Function Names¶
C++ lets you give two different functions the same name as long as they have different input parameters (so that C++ can tell them apart)
for example
// constraint x to be in the range 0.0 to 1.0
double clip(double x) {
if (x < 0.0) {
return 0.0;
} else if (x > 1.0) {
return 1.0;
} else {
return x;
}
}
// constraint x to be in the range lo to hi
double clip(double x, double lo, double hi) {
if (x < lo) {
return lo;
} else if (x > hi) {
return hi;
} else {
return x;
}
}
notice that if you’ve written the second general-purpose clip
function
the other one could be written like this
// constraint x to be in the range 0.0 to 1.0
double clip(double x) {
return clip(x, 0.0, 1.0);
}
another example
int twice(int n) {
return 2 * n;
}
string twice(string s) {
return s + s;
}
by checking the type of the input parameter, C++ can always tell which function to call
C++ Functions are Not Mathematical Functions¶
even relatively simple-looking C++ functions can behave in ways that break the rules of how mathematical functions behave
#include "cmpt_error.h"
#include <iostream>
using namespace std;
//////////////////////////////////////////////////////////////////////////////
//
// WARNING!
//
// This is an example of how functions in C/C++ can behave differently than
// functions in mathematics. The functions f and g are artificial examples of
// the weird things functions can end up doing. They are not examples of how
// you should actually write functions in real programs!
//
//////////////////////////////////////////////////////////////////////////////
int f_count = 0;
int f(int n) {
f_count++;
return n + f_count;
}
bool g_called_with_1 = false;
int g(int n) {
if (n == 1) {
g_called_with_1 = true;
}
if (g_called_with_1) {
return 1;
} else {
return n;
}
}
int main() {
// Shows that calling the same function more than once doesn't necessarily
// result in the same value being returned.
cout << f(1) << "\n"; // 2
cout << f(1) << "\n"; // 3
// Some mathematical simplifications cannot be used because they change
// the number of times a function is called.
cout << f(2) + f(2) << "\n"; // 15
f_count = 0;
cout << 2 * f(2) << "\n"; // 6
f_count = 0;
// The order in which you call functions can make a difference.
cout << g(1) + g(2) << "\n"; // 2
g_called_with_1 = false;
cout << g(2) + g(1) << "\n"; // 3
}
the problem here is that the behaviour of functions f
and g
depends in
part on the values of global variables
C++ Functions are Not Mathematical Functions¶
here’s another C++ function that doesn’t really have a mathematical counterpart
#include "cmpt_error.h"
#include <iostream>
using namespace std;
bool loop_forever() {
cout << "loop_forever called ...\n";
for(;;) {
// runs forever ...
}
cout << "... loop_forever done\n";
return true;
}
int main() {
int x = 5;
if (x > 0 || loop_forever()) {
cout << "loop_forever not called!\n";
}
} // main
C++ Functions are Not Mathematical Functions¶
a function that calls cmpt::error
does not return
instead, we say that the function has thrown an exception
#include "cmpt_error.h"
#include <iostream>
using namespace std;
int twice(int n) {
cout << "twice(" << n << ") called ...\n";
if (n < 0) error("n is negative");
cout << "... twice(" << n << ") done\n";
return 2 * n;
}
int main() {
int a = twice(1) + twice(-2) + twice(3);
cout << "a = " << a << "\n";
} // main
this shows that when an exception is thrown, a function does not return in the normal sense
instead, the function is immediately exited
Function Side Effects¶
when a function “communicates” with the rest of a program if it modifies some non-local value, or performs an action (such as printing to the screen) that has some noticeable effect
common side effects include: modifying a global variable, printing to the screen (or a file)
some side effects, such as printing, or obviously important and useful
but they can lead to unexpected behaviour
consider this code fragment
char a() {
cout << "a() called\n"; // side effect
return 'a';
}
char b() {
cout << "b() called\n"; // side effect
return 'b';
}
int main() {
cout << a() << "\n"
<< b() << "\n";
}
compiling this with g++ on my computer, the program prints the following
b() called
a() called
a
b
this shows that function b
is called before function a
just because a()
appears before b()
in the cout
statement does not
guarantee that a()
is called first
C++ does not guarantee in what order functions are evaluated when they are use
with cout
and <<
as in this example
some compilers might evaluate a()
, while others (such as g++) might
evaluate b++
first
the order of evaluation is not specified by C++, and so the compiler is free to evaluate them in whatever order is most convenient