Chapter 5 Notes¶
Please read chapter 5 of the textbook.
void Functions¶
recall that a void
function is a function that doesn’t return a value
void greet(string name) {
cout << "Hi " << name << "!\n";
}
void
is not a data type, e.g. you cannot declare a variable to be
void
instead, it indicates that a function has no return value
if necessary, you can use return;
(with no value before the ;
) inside
a void
function to immediately jump out of it
void greet(string name) {
cout << "Hi " << name << "!\n";
return; // optional
}
Functions with No Inputs¶
you can also have functions that take no inputs
such functions may, or may not, also be void
string url_prefix() {
return "http://";
}
void ping() {
cout << "ping!\n";
}
Passing Parameters by Value¶
in what follows we’ll use this function as an example
double square(double x) {
return x * x;
}
consider this code fragment
double a = 2.0;
cout << square(a) << "\n"; // 4.0
cout << a << "\n"; // 2.0
when square(a)
is called, a copy of the value in a
is assigned to
x
in the function
this is way of passing parameters to functions is know as pass by value
it is the default way that all parameters are passed in C++ (and C)
Passing Parameters by Value¶
the value passed to square
is a copy of the original value
thus, a variable that is passed by value cannot be modified by the function it is passed to
so we can’t do this:
void set_zero(double x) {
x = 0.0; // sets the local x to 0
}
// ...
double a = 5.0;
set_zero(a); // runs, but a doesn't change
there’s no change to a
because the actual value a
refers to is not
passed to set_zero
a copy of the value is passed and assigned to x
x
does get set to 0, but this does not affect the value of a
so set_zero
does nothing!
Passing Parameters by Value¶
another issue with pass by value is that making a copy takes time and memory
bool starts_with_dot(string s) {
if (s.empty()) {
return false;
} else {
return s[0] == '.';
}
}
s
is passed by value, which means a copy of the passed in string is made
and assigned to s
if the passed-in string is big, this wastes time and memory
if (starts_with_dot("some big huge multi-megabyte string ...")) {
// ...
}
Passing Parameters by Reference¶
C++ provides a solution to these problems
it lets you pass parameters by reference if you like
passing by reference gives the function the actual value being passed
no copy is made when a parameter is passed by reference
void set_zero(double& x) { // note the &; x is passed by reference
x = 0.0;
}
int main() {
double a = 5.6;
cout << "a = " << a << "\n"; // 5.6
set_zero(a);
cout << "a = " << a << "\n"; // 0.0
}
in C++, a &
after the type name in the parameter will cause that parameter
to be passed by value
Passing Parameters by Reference¶
this function is quite useful
// exchanges the values of a and b
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
sorting vectors/arrays of data is often done by repeatedly calling this
swap
function
take a moment to trace through this carefully, step by step; compare it to a
version where the &
characters are removed
Passing Parameters by Reference¶
another use of pass-by-reference parameter is to get back multiple values from a function
a function can only return a single value
but with pass-by-reference it can essentially “send back” any number of values
void get_point(double& x, double& y, double& z) {
cout << "What is x? ";
cin >> x;
cout << "What is y? ";
cin >> y;
cout << "What is z? ";
cin >> z;
}
void print_point(double x, double y, double z) {
cout << "(" << x << ", " << y << ", " << z << ")\n";
}
int main() {
double a = 0.0;
double b = 0.0;
double c = 0.0;
get_point(a, b, c);
print_point(a, b, c);
}
notice that print_point
uses pass-by-value, and not pass-by-reference
that’s because print_point
does not (and should not) change x
, y
,
or z
passing-by-reference would not have much (if any) benefit here, and it opens
the door for errors related to accidentally changing x
, y
, or z
also, if we used pass-by-reference we would not be able to write statements like this
print_point(1, 2, 3); // compile-time error: can't pass ints by reference
// only variables can be passed by reference
you should only use pass-by-reference when you have a clear, concrete reason for needing it
Passing Parameters by Reference¶
when thinking about how C++ functions work, it’s useful to consider the same function but with parameters passed in different ways
void set_zero1(int n) { // n passed by value
n = 0;
}
void set_zero2(int& n) { // n passed by reference
n = 0;
}
// ...
int a = 4;
int b = 5;
set_zero1(a); // a not changed
set_zero2(b); // b set to 0
Passing Parameters by Constant Reference¶
passing by constant reference lets you pass parameters by reference
but it does not let you modify those parameters (they are constant)
void say_hello(const string& name) { // note the use of const
cout << "Hello, " << name << "!\n";
}
the compiler ensures that the body of the function doesn’t modify name
; it
will report an error if name
is modified inside of say_hello
no copy of name
is made, so it is efficient
Specifying Functions with Pre and Post Conditions¶
functions are one of the key building blocks of C/C++ programs
we want functions to be efficient, readable, and easy to use
so it is often useful to very carefully specify what a function does
here we will see how we can use pre-conditions and post-conditions to clearly explain what a function does
Specifying Functions with Pre and Post Conditions¶
// Pre-condition:
// none
// Post-condition:
// The values of a and b are exchanged.
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
the pre-condition says what must be true just before the function is called
the post-condition guarantees what will be true after the function finishes (assuming the pre-condition was true when it was called)
for swap
, there are no special pre-conditions on either a
or b
,
i.e. you can pass in any int
s at all
so there is no pre-condition beyond the basic requirement that two int
_s
be passed to it
the post-condition explains how the variables are changed after the function is called
Specifying Functions with Pre and Post Conditions¶
// Pre-condition:
// radius > 0
// Post-condition:
// Returns the area of a circle with the given radius.
double circle_area(double radius) {
return 3.14 * radius * radius;
}
the circle_area
function has a pre-condition: radius
must be greater
than 0
it never makes sense to have a circle with a radius of 0 or less
it’s often useful to check the pre-condition inside the function
// Pre-condition:
// radius > 0
// Post-condition:
// Returns the area of a circle with the given radius.
double circle_area(double radius) {
if (radius <= 0) {
cmpt::error("radius must be positive");
}
return 3.14 * radius * radius;
}
now this function will crash if you give it an invalid radius
that might seem harsh, but while developing a program we almost always want the program to stop as soon as it knows there is an error
otherwise the program could continue computing nonsense values that are harder to debug because their cause can be so far away from where they appear as errors
in a correctly working program, function pre-conditions should never be violated
Specifying Functions with Pre and Post Conditions¶
recall that we saw earlier in the course how to calculate the square root of
number without using the standard C++ sqrt
function
here is that code inside a function
// Pre-condition:
// a >= 0, and precision > 0
// Post-condition:
// Returns a value x such that x * x is approximately equal to s,
// the true square root of a. It is guaranteed that
// abs(x - s) <= precision.
// Example:
// my_sqrt(5, 0.001) will return the square root of 5 accurate
// to two decimal places
double my_sqrt(double a, double precision = 0.0001) {
if (a < 0) {
cmpt::error("can't take square root of a negative number");
}
if (precision <= 0) {
cmpt::error("precision must be positive");
}
double prev_x = a;
double x = a / 2; // initial estimate of square root
while (abs(x - prev_x) > precision) {
prev_x = x;
x = (x + a / x) / 2.0;
}
return x;
}
the pre-condition makes explicit the valid and invalid values of the
parameters we can pass to my_sqrt
in this case it’s also relatively easy to check the pre-conditions inside the function
A Few Small Functions¶
// Pre-condition:
// none
// Post-condition:
// returns the smaller of x and y
double min(double x, double y) {
if (x < y) {
return x;
} else {
return y;
}
}
// Pre-condition:
// none
// Post-condition:
// returns the smaller of x, y, and z
double min(double x, double y, double z) {
return min(x, min(y, z));
}
in this case we have two functions named min
but they have different parameters, so C++ will always know which one is being called
note how the second function uses the previous min
function to calculate
its answer
this is good: it is clear and efficient
compare it to this version
// Pre-condition:
// none
// Post-condition:
// returns the smaller of x, y, and z
double min(double x, double y, double z) {
if ((x <= y) && (x <= z)) {
return x;
} else if ((y <= x) && (y <= z)) {
return y;
} else {
return z;
}
}
this version also works, but the logic is more complicated and harder to read
Testing¶
testing is an important topic in computer program
if you don’t test your programs, how do you know they work?
usually, it’s impossible to test all possible inputs to a function since there are too many
for example, consider this function
double calc(double x, int n)
assuming that double
is 64 bits, and int
is 32 bits, thats 96 bits of
input to this function
there are, exactly, \(2^{96}\) different patterns for those 96 bits
this is exactly 79228162514264337593543950336 different possible inputs to
calc
, i.e. about \(7.9 \times 10^{29}\) different input patterns
suppose we could test 1 trillion calls to calc
per second
then testing all \(2^{96}\) different inputs would take over 2.5 billion years to run
Testing¶
so instead of testing all possibilities, we can usually only test a few cases
it’s important to try to choose good tests cases
for example, very common inputs are often good test cases: make sure your function works well for the most common cases
boundary values are also often good tests cases, e.g. values around some natural boundary point in the input
for example, for int
_s, -1, 0, 1, MIN_INT
, MIN_INT
+ 1,
MAX_INT
, MAX_INT
- 1
that can still be quite a few test cases, e.g.
double calc(int a, int b, int c)
using just the extreme values above, this would result in \(7 \cdot 7 \cdot 7 = 7^3 = 343\) test cases
most programmers would probably not test a function with so many cases
unless, perhaps, the function was very important
Testing¶
my_abs(x)
should return the absolute value of x
double my_abs(double x) {
if (x < 0) {
return -x;
} else {
return x;
}
}
this implementation is pretty simple, and it’s hard to see how it could be wrong
there should be at least three test cases: a negative value, 0, and a positive value
e.g. my_abs(-14)
should equal 11, my_abs(289)
should equal 289, and
my_abs(0)
should equal 0
this tests only a tiny fraction of possible inputs
but those tests, combined with the implementation that looks correct, should give us confidence in its correctness
Test Drivers¶
another useful technique is to test a function using a driver program
for example, lets write a driver for this function we wrote earlier
// min_driver.cpp
#include "cmpt_error.h"
#include <iostream>
#include <string>
using namespace std;
// Pre:
// Post: returns the smaller of x, y, and z
double min(double x, double y, double z) {
if ((x <= y) && (x <= z)) {
return x;
} else if ((y <= x) && (y <= z)) {
return y;
} else {
return z;
}
}
int main() {
for (;;) { // infinite loop
cout << "Enter 3 doubles: ";
double x = 0.0;
double y = 0.0;
double z = 0.0;
cin >> x >> y >> z;
cout << "min(" << x << ", " << y << ", " << z << ") = "
<< min(x, y, z)
<< "\n";
}
}
recall that for(;;)
is an infinite loop
ctrl-C will end the program
the idea is to type in values to check that their output is correct
Function Stubs¶
as we mentioned earlier, top-down design is often a good way to design a program
the idea is to start with a general, high-level description of the program
and then progressively refine that description to be more and more low-level
one way to do this to use function stubs
a function stub is a function with the proper header but with a body that does nothing useful (yet)
here’s a very general example of function stubs
void print_intro() {
cout << "Intro will be printed here\n";
}
void get_input() {
cout << "get input here\n";
}
void process_input() {
cout << "process input here\n";
}
void display_output() {
cout << "display output here\n";
}
int main() {
print_intro();
get_input();
process_input();
display_output();
}
notice that the main
function reads almost like English
the logical structure is quite clear
all the details are hidden inside the functions
it also lets us run the program without having implementations for all the functions
Debugging¶
recognizing, finding, and fixing errors in programs is a major part of programming
here we offer some general advice for debugging
- use
cout
statements to print the values of variables; for more complicated programs, you may also use a debugger, such asgdb
, or a debugger in your IDE - keep an open mind: sometimes errors are due to trivial typing mistakes, or fundamental misunderstandings, or something you think can’t possible be the case is the case
- don’t be quick to assume there’s an error with the compiler; compiler’s definitely do have errors, but they are so rare in the sorts of programs we are writing that you can, practically speaking, assume they never occur
- narrow down the region where an error could occur (checking pre-conditions and post-conditions can help with this)