Programming with Functions
Modularity and Abstraction
Our programs are getting longer. On one hand complicated problems require complicated programs, but on the other hand, long programs are difficult to understand, and after reliability, understandability is the most important design goal. More time and money is spent in the maintenance phase of a program's life cycle than in its development phase. Maintenance means testing, debugging, and modifying. These are difficult and risky operations if the person performing them—often not the developer --doesn't understand the program.
We need some way to manage the complexity of large programs. Comments, documentation, layout, and choosing suggestive names help, but not nearly enough. We need something more powerful. The two most powerful principles in engineering for controlling complexity are:
The Modularity Principle
Programs (or any systems) should be constructed out of coherent, loosely coupled modules.
The Abstraction Principle
The implementation of a module should be independent of its interface.
A module is any kind of programming building block: functions, files, namespaces, data structures, types, classes, objects, etc. Loosely coupled means dependencies between modules are minimized. Coherent means the function or purpose of a module is well defined. A coherent, loosely coupled module is easy to replace and easy to reuse.
There are two ways to look at a module. The client (i.e., user) sees a module in terms of its function or purpose, while the implementer sees a module in terms of its internal structure, in terms of how rather than what function it performs. We call the client's view the module's interface and the implementers view the module's implementation. The abstraction principle says a client shouldn't need to know about a module's implementation in order to use it, and the implementer should be able to change the module's implementation without breaking the client's code (assuming the interface doesn't change).
For example, we are clients of the functions, classes, and objects in the C++ standard library. We use cin, cout, sin(), sqrt(), <<, etc. but we don't particularly care how they are implemented. We become frustrated when, for example in the case of having to flush cin's buffer, we are forced to understand implementation details. (Many of the functions and classes in the C++ standard library-- not to mention the language itself --are poorly designed.)
Taken together, the modularity and abstraction principles say that the overall design or architecture of a program should be understandable without needing to understand the implementation of the component modules, and the design of a component module should be understandable without needing to understand the entire program.
Procedural Programming
C++ provides many types of modules or abstraction mechanisms. For now we will focus on functions. Functions are sometimes called procedures, so building programs out of functions—i.e. using functions as the primary building block --is called procedural programming. The terms structured programming, functional decomposition, and imperative programming are also used.
A function definition consists of a function header followed by a block statement. The header tells us the function's return type, name, and parameter list. For example:
// = |x – y| = linear distance between x & y
double dist(double x, double y)
{
return fabs(x – y);
}
defines a function named dist. It expects two parameters of type double. (Be sure to include <cmath> to declare fabs.) Inside the program's block these parameters are called x and y. The function returns a double representing the distance between x and y on the number line. (Note the helpful comment explaining the interpretation of the return value.)
The function may be called many times. Each time it is called, two expressions called operands must be supplied. Executing the operands produces values called arguments. These arguments are substituted for the parameters x and y in the function's block. For example:
// client code:
cout << dist(6.5, 6.56) << '\n';
cout << dist(-6.5, 6.56) << '\n';
cout << dist(1e-10, 1e-11) << '\n';
prints:
0.06
13.06
9e-011
We called dist three times. (Don't confuse calling and defining a function). In the first call we supplied two arguments: 6.5 and 6.56. These arguments replace the parameters in the function's block, producing the statement:
{
return fabs(6.5 – 6.56);
}
The statement is executed. fabs(6.5 – 6.56) produces the value 0.06, which is returned to the caller, i.e. the function that called dist(). Every function except main() has a caller. In our example the caller was the insertion operator: <<. Essentially, fabs(6.5 – 6.56) is an expression that produces the value 0.06.
Important terms: calling a function, defining a function, caller, parameter, operand, argument, return value.
Top Down Design
The basic idea behind procedural programming is top down design: when main() gets too long—say more than half a page –we break it up into calls to loosely coupled, coherent supporting functions. We apply the same idea to the supporting functions. We repeat this process until there are no more supporting functions to be defined.
Example: Computing Volumes of Capped Pipes
Assume a plumbing supply manufacturer assembles pipes capped at both ends:

The assemblies have different lengths (l) and diameters (d). The manufacturer would like a program that perpetually prompts users for l and d, then computes and displays the pipe's volume.
We might begin with the interpreter pattern we have been using:
int main()
{
bool more = true;
double len = 0, diam = 0, vol = 0;
char response;
while(more)
{
cout << "Enter pipe's diameter in inches: ";
cin >> diam;
cout << "Enter pipe's length in inches: ";
cin >> len;
if (diam <= 0 || len <= 0)
cerr << "dimensions must be positive, try again\n";
else
{
cout << "length = " << len << " inches\n";
cout << "diameter = " << diam << " inches\n";
vol = pipeVolume(len, diam);
cout << "Volume = " << vol << " cubic inches\n";
cout << "Press any key to continue or q to quit: ";
cin.sync();
response = cin.get();
more = (response != 'q') && (response != 'Q');
}
}
return 0;
}
Notice that we computed the volume of the pipe by calling a function named pipeVolume(). Is this a standard C++ library function? It might be; it's worth a check, but alas, it is not. No worries, we'll define our own:
double pipeVolume(double l, double d)
{
return ???;
}
Note that we can choose any names we want for our parameters, as long as there are two and they are of type double (because that's what the caller expects). Following the modularity principle, we look for some way to break pipeVolume() into calls to loosely coupled, coherent supporting functions. We use the physical geometry of the pipe as our guide. The pipe is assembled out of a cylinder of radius = d/2 and length = l – d and two hemispherical caps, which, when glued together, form a sphere of radius = d/2. The volume of our pipe is simply the sum of the cylinder volume and the sphere volume:
double pipeVolume(double l, double d)
{
return cylinderVolume(l - d, d/2) + sphereVolume(d/2);
}
Perhaps cylinderVolume() and sphereVolume() are standard C++ library functions. Again, it's worth checking, again we're disappointed, and again we must define our own:
double cylinderVolume(double len, double rad)
{
return ???;
}
double sphereVolume(double rad)
{
return ???;
}
The volume of a cylinder is the area of its circular base times its length:
double cylinderVolume(double len, double rad)
{
return len * circleArea(rad);
}
Is circleArea() in the C++ library? No, but the area of a circle is simply pi times the radius squared:
double circleArea(double rad)
{
return pi * square(rad);
}
Surely square() is in the standard library. Nope:
double square(double x) { return x * x; }
Pi must also be defined, but we will also need pi to compute the sphere's volume. If we define pi as a local variable of circleArea(), i.e. inside circleArea()'s block, then it won't be visible inside sphereVolume(). Even if we make pi a local variable of main() it won't be visible inside sphereVolume(). The solution is to make pi a global variable. A global variable is defined outside of all function blocks, including main()'s. the scope of a global variable is the entire program:
const double pi = acos(-1);
Of course sphereVolume() isn't in the C++ library, so we must define it. According to my geometry book the volume of a sphere is:
V = 4/3 p r3
It seems wasteful to recalculate the constant 4/3 p each time sphereVolume() is called. By defining it as a global variable it will be calculated once at the beginning of the program:
const double fourThirdsPi = pi*4.0/3.0; // warning: 4/3 = 1
Here is our implementation of sphereVolume():
double sphereVolume(double rad)
{
return fourThirdsPi * cube(rad);
}
What about cube()? It too must be defined:
double cube(double x) { return x * square(x); }
Now we can build (i.e. compile and link) and test our program:
Enter pipe's diameter in inches: 4
Enter pipe's length in inches: 12
length = 12 inches
diameter = 4 inches
Volume = 134.041 cubic inches
Press any key to continue or q to quit:
Enter pipe's diameter in inches: 2
Enter pipe's length in inches: 25
length = 25 inches
diameter = 2 inches
Volume = 76.4454 cubic inches
Press any key to continue or q to quit:
Diagrams are often useful for understanding the structure of a program. In procedural programming dependency graphs are often used. The nodes of a dependency graph represent functions and an arrow connecting nodes indicates that one function calls another:

The calling function depends on or is supported by the called function. A change to the called function may impact the calling function. Here's a dependency graph for our application:

Note that we can eliminate the dependency of cube() on square() by redefining cube() as follows:
double cube(double x) { return x * x * x; }
This actually makes cube() more reusable because we only need to add cube() to a new project. We don't need to remember that square must also be added.
Here's an alternative implementation of pipeVolume(). It is correct, but would you want to maintain it?
double pipeVolume(double u, double k) { return u *
helper1(k) –
helper2(k)
+ helper3(k);} // Wally was right!
The three "helper" functions are not cohesive, hence not reusable:
double helper1(double x) { return 3.1416 * d * d / 4; }
double helper2(double x) { return d * helper1(d); }
double helper3(double x) { return helper2(d) * 2 / 3; }
The Stack Model for Function Calls
I have over simplified the function call mechanism used by C++. I said when a function is called:
1. Operands (which are expressions) are executed (i.e., evaluated) to produce arguments (which are values).
2. Arguments are substituted for parameters in the function's block.
3. The block is executed, which produces a return value.
For example, assume we want to call dist(6 + 7, 6 * 7). In step 1 the operands 6 + 7 and 6 * 7 are executed (i.e., evaluated) to produce the arguments 13 and 42. (Note: the order of evaluation is not specified.) In step 2 the arguments replace the parameters x and y in the block to form the new block:
{
return fabs(13 – 42);
}
Finally, the block is executed, producing the value: 29.
this is called the substitution model for function calling. Unfortunately, because of local variables, the substitution model is too simple. Instead, we must use the stack model. Instead, of substituting arguments for parameters in step2, a frame is created containing variables corresponding to the parameters and local variables. These variables are initialized by the arguments:

The frame is pushed on the program's stack. The block is now executed relative to this frame. When the function terminates, the frame is popped off the stack.
Inline Functions
Constructing and destroying frames can be inefficient. It almost seems wasteful for short functions. C++ solves this problem with inline functions. Calls to an inline function use the substitution model instead of the stack model. To do this, just place the inline specifier in front of the function definition:
inline double square(double x) { return x * x; }
inline double cube(double x) { return x * x * x; }
Warning: only declare a function to be inline if its block is simple. Functions containing loops in their blocks should not be declared inline. Also, an inline function must be defined in every file where it is called.
Parameter Mechanisms
The stack model has two potential downsides. First, if the arguments are large data structures, for example, long arrays, then copying them into the parameters may be inefficient. Second, any modifications a function makes to its parameters will be lost when the frame is destroyed (i.e., popped off the stack).
The second disadvantage can also be an advantage. If, for example, the argument is a global variable that must be shared with other functions, then inadvertently modifying a parameter unintentionally modifies the global. However, there are some cases where this is exactly what we want. For example, suppose we want to create a function that swaps the values of any pair of integer variables. We might try this:
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
} // this fails
The void Type
A quick observation is needed here. The goal of swap() is to swap the values stored in two variables. This is the way it might be called:
int main()
{
int a = 42, b = 35;
// etc.
cout << "a = " << a << '\n';
cout << "b = " << b << '\n';
swap(a, b);
cout << "a = " << a << '\n';
cout << "b = " << b << '\n';
// etc.
return 0;
}
Notice that no return value is expected, only the side effect of swapping a and b. Because swap() isn't expected to return a value, it does not contain a return statement. Instead, the function terminates when control reaches the end of its block. But all C++ function headers must declare a return type. C++ provides a void type which is used in two situations. It is used as the return type for functions that don't return values, such as swap() and it is used as the base type for generic pointers:
void *p; // p is a generic pointer
Pointer Parameters
Unfortunately, swap(a, b) fails to swap a and b. A frame is created with x = 42, temp = 42, and y = 35. x and y are swapped:

then the frame is destroyed, leaving a and b unchanged.
The traditional solution to this problem is to use pointers. Instead of passing the values of a and b to swap, their addresses are passed:
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
Of course the client must pass addresses, not integers, to swap():
int main()
{
int a = 42, b = 35;
// etc.
cout << "a = " << a << '\n';
cout << "b = " << b << '\n';
swap(&a, &b);
cout << "a = " << a << '\n';
cout << "b = " << b << '\n';
// etc.
return 0;
}
Here's what the frames look like:

Reference Parameters
I have two objections to the implementation of swap(). First, all the pointer dereferencing is confusing and scary. I suppose I could get used to it, but what about my poor client. He too must know enough about pointers to remember that it's &a and &b, not a and b that must be passed to swap().
C++ offers another solution. A reference is basically a constant pointer, only C++ automatically dereferences them and automatically converts variables assigned to them into addresses. For example, assume x is an integer variable:
int x = 42;
An integer reference is declared like this:
int& y = x;
This is translated to:
int const *y = &x;
The assignment:
x = x + y;
is translated to:
*x = *x + y;
With references we can re implement swap() as follows:
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
The client doesn't need to compute addresses:
int main()
{
int a = 42, b = 35;
// etc.
cout << "a = " << a << '\n';
cout << "b = " << b << '\n';
swap(a, b);
cout << "a = " << a << '\n';
cout << "b = " << b << '\n';
// etc.
return 0;
}
Constant Reference Parameters
Passing references is more efficient than passing values because only addresses are copied, but passing references is risky because programmers aren't protected from inadvertently modifying the client's variables. We can have the best of both worlds by declaring parameters to be constant. For example, assume members of a user defined type are large:
struct Shape { ... };
A function that expects a shape as an input can declare a constant reference parameter:
double surfaceArea(const shape& s) { ... }
Any attempt to modify s in the block of the function generates a compiler error.
Reference Return Values
Functions that return references can be called from the left side of an assignment operator. For example:
char& getChar(char[] str, int pos)
{
if (pos < 0 || strlen(str) <= pos)
{
cerr << "Position out of range\n"
exit(1);
}
return str[pos];
}
When getChar() is not called on the left of an assignment operator, C++ performs automatic dereferencing, as usual:
cout << getChar("Cat", 0); // prints 'C'
Calling getChar() on the left of an assignment operator suppresses the dereferencing operation:
getChar("Cat", 0) = 'B'; // turns Cat into Bat
Default Arguments
C++ allows default arguments to be specified when a function is defined. For example:
void init(int nums[], int size = 3, int fill = 0)
{
for(int i = 0; i < size; i++)
nums[i] = fill;
}
Initializes an array, nums, of length size with a fill value. Here are some sample calls:
int vals[5] = {100, 200, 300, 400, 500};
init(vals, 5, 20); // vals = {20, 20, 20, 20, 20}
init(vals, 2); // vals = {0, 0, 20, 20, 20}
init vals(); // vals = {0, 0, 0, 20, 20}
Note that in the second call, the fill argument was assumed to be 0. In the third call the fill was assumed to be 0 and the size was assumed to be 3.
Also note that arrays are not passed by value, so we are not initializing a private copy of vals.
Overloading
A great feature of C++ is that function names can be overloaded or shared. The compiler will determine which function to call by matching the argument types with the parameter types.
For example, assume three geometric types are declared:
struct Triangle { ... };
struct Rectangle { ... };
struct Circle { ... };
We would like to write functions that compute the areas of these shapes. In C++ we can give all of them the same name:
double area(Triangle x) { ... }
double area(Circle x) { ... }
double area(Rectangle x) { ... }
These three functions are called variants of area. When the compiler sees a call to area():
Circle c;
Triangle t;
Rectangle r;
cout << area(t); // calls triangle variant of area
cout << area(r); // calls rectangle variant of area
cout << area(c); // calls circle variant of area
The C++ compiler uses the types of the arguments: t, r, and c, to decide which variant to call.
Compare this with C. The three area functions must have distinct names:
double triangle_area(Triangle x) { ... }
double circle_area(Circle x) { ... }
double rectangle_area(Rectangle x) { ... }
The poor user must look up the name each time he wants to compute the area of a triangle. Was it triangle_area, area_triangle, triangleArea, etc.?
Function Templates
A powerful way to define a family of functions instead of a single function is a template. For example, the swap function only swaps integer variables. We'll need to define another swap function to swap doubles or strings or employee records. We can define all of these swap functions in one definition by declaring the type of the parameters to also be a parameter!
template <typename Data>
(The "class" keyword can be used instead of "typename".) When the C++ compiler sees a call to swap, it determines the type of the arguments and automatically generates definitions of the needed swap variants. For example, when C++ compiles:
int main()
{
int a = 42, b = 35;
swap(a, b); // ok
string u = "hello", v = "goodbye";
swap(u, v); // ok
return 0;
}
It automatically generates the definitions:
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
and
void swap(string& x, string& y)
{
string temp = x;
x = y;
y = temp;
}
Because of overloading, the functions can share the swap name.
Example: Vector Arithmetic
Let's use some of these tools in the context of an example: vector arithmetic. We will use the vectors in the standard library:
#include <vector>
#include <string>
#include <iostream>
using namespace std;
typedef vector<double> Vector;
We can hide the fact that vectors are C++ vectors by providing an inline dimension function that calls the size() function encapsulated by vectors:
inline int dim(const Vector& v) { return v.size(); }
Next, we define with a simple utility for displaying vectors. Note the use of default arguments and constant reference parameters:
ostream& write(const Vector& vec, ostream& os = cout)
{
os << "( ";
for(int i = 0; i < dim(vec); i++)
os << vec[i] << ' ';
os << ")\n";
return os; // for nesting calls
}
The ostream parameter was passed by reference because the argument is modified internally. Characters were inserted into it.
The vector addition function also uses constant reference parameters. One problem is that we don't want to add vectors of different sizes. We can easily check if two vectors have different sizes, but what should we return if they don't?
One solution is to stop the program if the sizes are different. This is fine when we are debugging the program. If we're not, then the solution is to throw an exception that can be handled in a part of the program that can deal with the problem. We use a #define directive to create a debug mode flag (we could have used a constant declaration, too):
#define DEBUG_MODE true
Our error handling function prints an error message to cerr and calls the library function exit(1). Calling exit(1) is the same as calling return 1 in main(). It immediately terminates the program returning an exit code of 1 (meaning unsuccessful completion) to the operating system. If debug mode is false, an object called an exception is created by calling runtime_error(). The exception encapsulates the error message. The exception object is then thrown. A thrown exception can be caught and handled in another part of the program able to deal with the problem. Uncaught exceptions cause the program to terminate. More on this later. Here's the code:
void error(const string& gripe)
{
if (DEBUG_MODE)
{
cerr << "Error: " << gripe << '\n';
exit(1);
}
else
throw runtime_error(gripe);
}
We are now ready for add(). We don't use an else clause because then we would have to specify a bogus return value when the vector dimensions were different, even though error() will cause the program to terminate before the value is returned:
Vector add(const Vector& vec1, const Vector& vec2)
{
int n = dim(vec1);
if (n != dim(vec2))
error("invalid input");
Vector sum(n);
for(int i = 0; i < n; i++)
sum[i] = vec1[i] + vec2[i];
return sum;
}
Note that unlike C, C++ allows structures, i.e. non simple values, can be returned by functions. Here is a simple test harness. Note that C++ vectors allow us to specify their initial sizes when they are declared:
int main()
{
const int size = 3;
Vector a(size), b(size), c(size);
for(int i = 0; i < size; i++)
{
a[i] = 2 * i + 1; // consecutive odds
b[i] = 3 * i; // consecutive multiples of 3
}
c = add(a, b);
write(a);
write(b);
write(c);
return 0;
}
Here's the output produced:
( 1 3 5 )
( 0 3 6 )
( 1 6 11 )
Other vector arithmetic functions can be defined similarly.
Improving the Interpreter
Recall the interpreter defined earlier that prompted users for the diameter and length of a capped pipe, then computed and printed the pipe's volume. The interpreter is such a common design pattern that it might be wise to consider improvements to the interpreter we can make using some of our new found technology.
Many of our early interpreters will ask the user if he wants to continue, then use the answer to set more, the Boolean LCV for the interpreter's main loop. We can define a general question asking function that returns true if the answer to the question is true, and false otherwise. The function can even handle bogus responses to the question by re prompting the user:
bool getResponse(const string& question)
{
string response;
bool yes, no, invalid;
do
{
cout << question + " (y/n) -> ";
cin >> response;
yes = response == "y" || response == "Y";
no = response == "n" || response == "N";
invalid = !(yes || no);
if (invalid)
cout << "Please type y for yes, n for no.\n";
}
while (invalid);
return yes;
}
Our early interpreters frequently prompt the user to input some data. The type of data varies, so we can use a template to postpone deciding what type of data to read.
Another problem we can solve is what to do if the user enters the wrong type of data. The problem is more difficult than the problem of the user merely entering data that is out of range. Presumably that part is application-specific and can be dealt with by the interpreter.
Recall that the extraction operator, >>, reads a string, then translates it into a binary value determined by the type of its right argument. If the translation can't be done, for example if the user types "three" when an integer is expected, then the input stream enters a fail state, it turns into 0. We can fix a failed stream by calling its clear() function and flushing the buffer using the stream's sync() function. Notice that var is passed by reference because this is where we will store the value read:
template <typename Data>
void getData(Data& var, const string& prompt)
{
Data response;
cout << prompt + " -> ";
while (!(cin >> response))
{
cerr << "Invalid entry, ";
cerr << "please try again or type <Ctrl>c to quit\n";
cin.clear(); // cin.state = good
cin.sync(); // flush buffer
cout << prompt + " -> ";
}
cout << "You entered " << response << '\n'; // echo
var = response;
}
We are now ready to rewrite main().
int main()
{
bool more = true;
double len = 0, diam = 0, vol = 0;
while(more)
{
getData(diam, "Enter pipe's diameter in inches");
getData(len, "Enter pipe's length in inches");
if (diam <= 0 || len <= 0)
cerr << "dimensions must be positive, try again\n";
else
{
vol = pipeVolume(len, diam);
cout << "Volume = " << vol << " cubic inches\n";
more = getResponse("Continue?");
}
}
return 0;
}
Here's a sample output from the program:
Enter pipe's diameter in inches -> 4
You entered 4
Enter pipe's length in inches -> 20
You entered 20
Volume = 234.572 cubic inches
Continue? (y/n) -> y
Enter pipe's diameter in inches -> three
Invalid entry, please try again or type <Ctrl>c to quit
Enter pipe's diameter in inches -> 3
You entered 3
Enter pipe's length in inches -> 15
You entered 15
Volume = 98.9602 cubic inches
Continue? (y/n) -> w
Please type y for yes, n for no.
Continue? (y/n) -> n
String Streams
Output String Streams
The insertion operators (<<) for streams are handy because they translate binary data into sequences of characters. Too bad there are no translators for strings. But like files and the monitor, strings can be sinks for a class of streams called output string streams (ostringstream). An output string stream is a special type of output stream (ostream), so any operations that can be performed on an output stream can also be performed on an output string stream. Thus, any type that defines a variant of the insertion operator can use it to change itself into a string:
template <typename Value>
string toString(const Value& val)
{
ostringstream os;
os << val; // hope << is defined for Value
return os.str();
}
Here is a patch of code that uses this template:
string s1 = "the answer is";
string s2 = toString(6 * 7);
string s3 = s1 + " = " + s2; // string concatonation
cout << s3 << '\n';
Input String Streams
Strings can also be character sources for input string streams. An input string stream (istingstream) is a special kind of input stream (istream), so all operations that can be performed on an input stream can also be performed on an input string stream. Here's the mate of the toString() function. This one translates strings into values. We must pass the value as a reference parameter so C++ can infer Value from calls to the function:
template <typename Value>
void fromString(Value& val, const string& str)
{
istringstream is(str);
is >> val; // hope >> is defined for Value
}
Here's a sample call::
int x;
fromString(x, "42"); // x = 42
The string stream declarations are in a header file called <sstream>, so you'll need the following include directive:
#include <sstream>
Another Interpreter
We can use string streams to build another, more traditional type of interpreter. The interpreter itself doesn't do much, it only executes simple infix arithmetic expressions, nested expressions aren't allowed, but the same pattern can be re used to build very powerful interpreters. Here's a sample run:
command -> 3 + 4
result = 7
command -> 3*4
result = 12
command -> 3/0
result = Error, can't divide by 0
command -> 3/2
result = 1.5
command -> 3 $ 2
result = Error, unrecognized operator: $
command -> 3 - 4
result = -1
command -> quit
bye
Notice that invalid inputs don't crash the interpreter. Also notice that instead of prompting the user each time with the annoying "Continue (y/n)?" prompt, the user simply types "quit" to end the program.
Taking another step toward reuse, we move the interpreter's control loop out of main():
void controlLoop(const string& prompt = "command")
{
bool more = true;
string result, cmmd;
while(more)
{
cout << prompt + " -> ";
getLine(cin, cmmd);
if (cmmd == "quit")
{
cout << "bye\n";
more = false;
}
else
{
result = execute(cmmd);
cout << "result = " << result << '\n';
}
}
}
The control loop prompts the user for a command, then reads the command. At the moment, there are only two possibilities: either the command is the quit command, in which case the loop exits, or not, in which case the command is executed by a separately defined execute() function, and the result is displayed. To keep the execute() function general, we only assume that it expects a string as an input and returns a string as an output:
string execute(string cmmd); // execute's prototype
To make the control loop more functional, we could add other possible "meta" commands such as help, undo, redo, and "\r".
The control loop doesn't assume the command is a single token (i.e., contains no white space). Instead of using the extraction operator, >>, which extracts up to the first white space character, the getLine() function is called. This function reads the entire line up to the first newline character. The C++ standard library has a function like this called getline(), but the VC++ version doesn't work, so I had to define my own:
istream& getLine(istream& in, string& str)
{
str = "";
char c;
do
{
c = in.get(); // extract next char (even white space!)
if (c != '\n') str += c;
}
while (in && c != '\n');
return in;
}
The execute function converts the command line (cmmd) into an input string stream (is), then uses the extraction operators (>>), to extract individual tokens. Next, else-if statements are used to determine the operator (+, -, /. *) and compute the result. Of course the result returned must be a string, to the toString() function is called to perform the conversion.
string execute(string cmmd)
{
string result = "";
char op = ' '; // the operator
double arg1 = 0, arg2 = 0; // first & second arguments
string msg1 = "Error, can't divide by 0";
string msg2 = "Error, unrecognized operator: ";
istringstream is(cmmd);
is >> arg1;
is >> op;
is >> arg2;
if (op == '+')
result = toString(arg1 + arg2);
else if (op == '*')
result = toString(arg1 * arg2);
else if (op == '-')
result = toString(arg1 - arg2);
else if (op == '/')
if (arg2 != 0)
result = toString(arg1/arg2);
else
result = msg1;
else
result = msg2 + op;
return result;
}
The execute() function checks for invalid operators and division by 0, but it doesn't check that there are enough tokens or that the arguments are really doubles. How could these types of errors be caught? What should be done?
Here are the include directives this program needs. Note that the string stream declarations are contained in <sstream>:
#include <string>
#include <sstream>
#include <iostream>
using namespace std;
The main() function is dead simple:
int main()
{
controlLoop();
return 0;
}
Problem: Blackjack
Write a program that plays Blackjack with the user. After each game the program displays the number of draws, user wins, and computer wins, then asks the user if the user wants another game.
A game of Blackjack consists of dealing a hand of two random cards to the user , then printing the value of the hand. The value is the sum of the card values, where:
ACE = 1 or 11
TWO = 2
etc.
NINE = 9
TEN = JACK = QUEEN = KING = 10
The user is repeatedly asked if he wants another card until he "holds" or until the value of the hand exceeds 21, in which case the game is over and the user looses.
If the user holds, then the computer deals two random cards to itself and displays the sum. The computer continues to deal itself a card until the sum of the cards is greater than or equal to 17. If the sum is greater than 21 or less than the sum of the user's hand, then the computer looses, if the sums are the same, then the game ends in a draw. Otherwise, the computer wins.
Use top down design. Start by defining a short main()function, then move on to define coherent, loosely coupled supporting functions.
Representing cards and decks provides an opportunity to introduce some new C++ features:
Enumerations
An enumeration is a user-defined type consisting of a finite number of names. For example:
enum Suit {CLUB, DIAMOND, HEART, SPADE};
enum Value
{
ACE, TWO, THREE, FOUR, FIVE,
SIX, SEVEN, EIGHT, NINE,
TEN, JACK, QUEEN, KING
};
The names in an enumeration can be used like integer constants. The definition of Suit is equivalent to:
enum Suit {CLUB = 0, DIAMOND = 1, HEART = 2, SPADE = 3};
Of course programmers can initialize the names in an enumeration in different ways.
Type Conversion
Sometimes values in one type can be converted to equivalent values in another type. Sometimes C++ will automatically perform these conversions where needed, other times programmers must explicitly request the conversion using a casting operator.
C++ will automatically perform conversions between its built-in types:
int x = 63;
double y = 42.5;
x = y; // x = 42
y = x; // y = 63.0
But some conversions involving user-defined types must be explicit. For example, C++ will automatically convert members of Suit to integers, but the reverse conversion must be explicit:
Suit u = 3; // error
int y = THREE; // ok
Suit x = Suit(3); // ok, x == THREE
Suit z = (Suit)4; // ok, but deprecated syntax
Suit w = Suit (25); // undefined, 25 out of range
Structures
A structure (i.e., a struct) is a convenient way for grouping variables (later we will learn structures can do a lot more):
struct Card
{
Value value;
Suit suit;
bool dealt; // = true if played
};
Declaring a structure:
Card c1 = {ACE, HEART, false}, c2 = {KING, SPADE, false}, c3;
declares the variables it encapsulates:
c1.value (= ACE)
c1.suit (= HEART)
c1.dealt (= false)
C++ allows assignment between structures:
c3 = c1;
c1.suit = SPADE;
cout << c3.suit; // prints 3 (= HEART)
Structures can be passed as parameters and returned as return values.
Here's a feeble way for printing cards. Write a better one later:
ostream& write(const Card& card, ostream& os = cout)
{
os << "[Suit = " << card.suit << ", ";
os << "Value = " << card.value << "]\n";
return os;
}
Decks as Arrays
A deck is simply any array of 52 cards:
typedef Card Deck[52];
Here's a tricky way to initialize a deck. Note the conversion operators used to change integers into suits and values:
void init(Deck d)
{
for(int i = 0; i < 4; i++)
for(int j = 0; j < 13; j++)
{
int n = 13 * i + j; // 0 <= n < 52
d[n].suit = Suit(i);
d[n].value = Value(j);
d[n].dealt = false;
}
}
Shuffling a deck is just a matter of clearing the dealt flags:
void shuffle(Deck d)
{
for(int i = 0; i < 52; i++)
d[i].dealt = false;
}
Here's a little utility for determining the number of undealt cards in a deck:
int undealtCards(Deck d)
{
int result = 0;
for(int i = 0; i < 52; i++)
if (!d[i]) result++;
return result;
}
Random Numbers
To pick a card, generate a random number between 0 and 51. If the card at that position is dealt, then increment the number modulo 52 until an undealt card is found:
Card pickCard(Deck d)
{
int i = rand() % 52;
int j = i;
while (d[j].dealt)
{
j = (j + 1) % 52;
if (i == j) error("No more cards");
}
d[j].dealt = true;
return d[j];
}
You'll need to seed the random number generator at the beginning of main:
#include <time.h> // not standard
// etc.
int main()
{
srand(time(0));
// etc.
return 0;
}
Recursive Functions
Earlier, we used a strange looking, unexplained algorithm to compute the number of ways to choose m items from n items. How would we compute this without such an algorithm dropping into our laps? When faced with a complex problem that can be divided into sub problems that are smaller versions of the original problem, we can use recursion.
Select one item from the n candidates to choose from; call it x. There are:
choose(n – 1, m) // x is out
ways to choose m items from n items assuming we aren't allowed to pick x, and there are
choose(n – 1, m – 1) // x is in
ways to choose m items from n items assuming x is already selected. Because x either will or will not be a selected item, there are
choose(n - 1, m) + choose(n - 1, m - 1)
ways to choose m items from n items, altogether. This leads to the following recursive function:
int choose(int n, int m)
{
if (m == 0 || n == 0)
return 1;
else if (n < m)
return 0;
else if (m == 1 || m == (n - 1))
return n;
else
return choose(n - 1, m) + choose(n - 1, m - 1);
}