1.1 How to Write a C++ Program
A function is an independent code sequence that performs some computation. It consists of four parts: the return type, the function name, the parameter list, and the function body. Let's briefly look at each part in turn.
The return type of the function usually represents the result of the computation. main() has an integer return type. The value returned by main() indicates whether our program is successful. By convention, main() returns 0 to indicate success. A nonzero return value indicates something went wrong.
The name of a function is chosen by the programmer and ideally should give some sense of what the function does. min() and sort(), for example, are pretty good function names.
The parameter list of a function is enclosed in parentheses and is placed after the name of the function. An empty parameter list, such as that of main(), indicates that the function accepts no parameters. The parameter list is typically a comma-separated list of types that the user can pass to the function when the function is executed. (We say that the user has called, or invoked, a function.)
A class is a user-defined data type. The class mechanism is a method of adding to the data types recognized by our program.
The class mechanism allows us to add layers of type abstraction to our programs. For example, we can define a Point3d class to represent location and orientation in space.Similarly, we can define a Camera class containing two Point3d class objects and a floating point value. We're still representing a camera by seven floating point values. The difference is that in our programming we are now directly manipulating the Camera class rather than seven floating point values.
The definition of a class is typically broken into two parts, each represented by a separate file: a header file that provides a declaration of the operations supported by the class, and a program text file that contains the implementation of those operations.
To use a class,we include its header file within our program. The header file makes the class known to the program.
we must define an object in which to store the information. We define an object by specifying the data type of the object and giving it a name. We've already seen one data type: int. That's hardly a useful way of storing someone's name, however! A more appropriate data type in this case is the standard library string class:
string user_name;
This defines
user_name as an object of the string class. The definition, oddly enough, is called a
declaration statement. This statement won't be accepted, however, unless we first make the string class known to the program. We do this by including the string class header file:
#include <string>
Rather than write successive output statements on separate lines,we can concatenate them into one compound output statement:
cout << '\n'
<< "Hello, "
<< user_name
<< " ... andgoodbye!\n";
using namespace std;
Both using and namespace are C++ keywords. std is the name of the standard library namespace. Everything provided within the standard library (such as the string class and the iostream class objects cout and cin) is encapsulated within the std namespace.
A namespace is a method of packaging library names so that they can be introduced within a user's program environment without also introducing name clashes. (A name clash occurs when there are two entities that have the same name in an application so that the program cannot distinguish between the two. When this happens, the program cannot run until the name clash is resolved.) Namespaces are a way of fencing in the visibility of names.
To use the string class and the iostream class objects cin and cout within our program, we must not only include the string and iostream header files but also make the names within the std namespace visible. The using directive using namespace std; is the simplest method of making names within a namespace visible.
1.2 Defining and Initializing a Data Object
Each object must be of a particular data type. The name of the object allows us to refer to it directly. The data type determines the range of values the object can hold and the amount of memory that must be allocated to hold those values.
An alternative initialization syntax, called a constructor syntax, is
int num_tries(0);
The use of the assignment operator (=) for initialization is inherited from the C language. It works well with the data objects of the built-in types and for class objects that can be initialized with a single value,such as the string class:
string sequence_name = "Fibonacci";
It does not work well with class objects that require multiple initial values, such as the standard library complex number class, which can take two initial values: one for its real component and one for its imaginary component. The alternative constructor initialization syntax was introduced to handle multiple value initialization:
#include<complex>
complex<double> purei(0, 7);
The strange bracket notation following complex indicates that the complex class is a
template class. We'll see a great deal more of template classes throughout the book. A template class allows us to define a class without having to specify the data type of one or all of its members.
The complex number class, for example, contains two member data objects. One member represents the real component of the number.The second member represents the imaginary component of the number. These members need to be floating point data types, but which ones? C++ generously supports three floating point size types: single precision, represented by the keyword float; double precision, represented by the keyword double; and extended precision, represented by the two keywords long double.
The template class mechanism allows the programmer to defer deciding on the data type to use for a template class. It allows the programmer to insert a placeholder that is later bound to an actual data type. In the preceding example, the user chose to bind the data type of the complex class members to double.
Note: we have to write three classes to cover all floating point data types if we don’t use template class.
C++ supports a built-in Boolean data type to represent true/false values. In our program, for example, we can define a Boolean object to control whether to display the next numeric sequence:
bool go_for_it = true;
A Boolean object is specified with the
bool keyword. It can hold one of two literal values, either
true or
false.
1.3 Writing Expressions
The division of two integer values yields a whole number. Any remainder is truncated(被删除的); there is no rounding(舍入). The remainder is accessed using the % operator:
5 / 3 evaluates to 1 while 5 % 3 evaluates to 2
5 / 4 evaluates to 1 while 5 % 4 evaluates to 1
5 / 5 evaluates to 1 while 5 % 5 evaluates to 0
The logical NOT operator (!) evaluates as true if the expression it is applied to is false. For example, rather than write
if (usr_more == false)
cout << "Your score forthis session is "
<< usr_score << "Bye!\n";
we can write
if (! usr_more) ...
An operator has a higher
precedence than an operator underit. Operators on the same line have equal precedence. In these cases, the orderof evaluation is left to right.
logical NOT
arithmetic (*, /, %)
arithmetic (+, -)
relational (<, >, <=, >=)
relational (==, !=)
logical AND
logical OR
assignment =
1.4 Writing Conditional and Loop Statements
Conditional Statements
if (usr_guess == next_elem)
{
// user guessed correctly
}
else
{ // user guessed incorrectly
if (num_tries == 1)
// ...
else
if (num_tries == 2)
// ...
else
if (num_tries == 3)
// ...
else // ...
cout << "Want to try again?(Y/N) ";
char usr_rsp;
cin >> usr_rsp;
if (usr_rsp == 'N' || usr_rsp == 'n')
go_for_it = false;
}
If the value of the condition being tested is an integral type, wecan replace the
if-else-if set ofclauses with a
switch statement:
// equivalent to if-else-if clauses above
switch (num_tries)
{
case 1:
cout << "Oops! Nice guessbut not quite it.\n";
break;
case 2:
cout << "Hmm. Sorry. Wrongagain.\n";
break;
case 3:
cout << "Ah, this isharder than it looks, isn't it?\n";
break;
default:
cout << "It must begetting pretty frustrating by now!\n";
break;
}
The
switch keyword is followed by an expression enclosed in parentheses (yes, the name of an object serves as an expression). The expression must evaluate to an integral value. A series of
case labels follows the
switch keyword, each specifying a constant expression. The result of the expression is compared against each
case label in turn. If there is a match, the statements following the
case label are executed. If there is no match and the
default label is present, the statements following the
default label are executed. If there is no match and no
default label, nothing happens.
After a case label is matched, all the case labels following the matched case label are also executed unless we explicitly break off execution.This is what the break statement does. Why, you're probably asking, is the switch statement designed this way? Here is an example of this fall-through behavior being just right:
switch (next_char)
{ case 'a': case 'A':
case'e': case 'E':
case'i': case 'I':
case'o': case 'O':
case'u': case 'U':
++vowel_cnt;
break;
//...
}
Loop Statements
A loop can be terminated within the body of its code sequence by the execution of a break statement. In the following code fragment, for example, the while loop executes until tries_cnt equals max_tries. If the user guesses the correct answer, however, the loop is terminated using the break statement:
int max_tries = 3;
int tries_cnt = 0;
while (tries_cnt < max_tries)
{
// read user guess
if (usr_guess == next_elem)
break; // terminate loop
tries_cnt++;
// more stuff
}
The
continue statement causes the current loop iteration to terminate: The remainder of the
while loop body is not evaluated. Rather, the loop begins again with a new evaluation of the condition expression.
1.5 How to Use Arrays and Vectors
Following are the first eight elements from six numericalsequences:
Fibonacci: 1, 1, 2, 3, 5, 8, 13, 21
Lucas: 1, 3, 4, 7, 11, 18, 29, 47
Pell: 1, 2, 5, 12, 29, 70, 169, 408
Triangular: 1, 3, 6, 10, 15, 21, 28, 36
Square: 1, 4, 9, 16, 25, 36, 49, 64
Pentagonal: 1, 5, 12, 22, 35, 51, 70, 92
Our program must display a pair of elements from a sequence and allow the user to guess the next element. If the user guesses right and wishes to continue, the program then displays a second pair of elements, then a third, and so on. How might we do that?
The solution we discuss in this section uses a container type that can hold a contiguous sequence of integer values that we can reference not by name but by position within the container.
In C++, we can define a container as either a built-in array or an object of the standard library vector class.
To define a built-in array, we must specify the type of element the array is to hold, give the array a name, and specify a dimension — that is, the number of elements the array can hold. The dimension must be a constant expression — that is, an expression that does not require run-time evaluation. For example, the following code declares pell_seq to be an array of 18 integer elements.
const int seq_size = 18;
int pell_seq[seq_size];
To define a
vector class object, we must first include the
vector header file.
The vector class is a template, so we indicate the type of its element in brackets following the name of the class. The dimension is placed in parentheses; it does not need to be a constant expression. The following code defines
pell_seq as a vector class object holding 18 elements of type
int. By default, each element is initialized to 0.
#include <vector>
vector<int> pell_seq(seq_size);
We access an element of either an array or a vector by specifying its position within the container.This element
indexing uses the subscript operator (
[]). One potential "gotcha" is that the first element begins at position 0 and not 1. The last element is indexed at 1 less than the size of the container. For
pell_seq, the correct indexes are 0 through 17, not 1 through 18. For example, to assign the first two elements of the Pell sequence, we write
pell_seq[0] = 1; // assign 1 to first element
pell_seq[1] = 2; // assign 2 to second element
Let's calculate the next ten elements of the Pell sequence. To
iterate over the elements of a vector or an array, we typically use a
for loop, the other primary C++ loop statement. For example,
for (int ix = 2; ix < seq_size; ++ix)
pell_seq[ix] = pell_seq[ix - 2] +2*pell_seq[ix - 1];
The
for loop consists of the following elements:
for (init-statement; condition; expression)
statement
Our container holds the second, third, and fourth elements of each of our six sequences. How can we fill the container with the appropriate values? A built-in array can specify an initialization list, providing a comma-separated list of values for all or a subset of its elements:
int elem_seq[seq_size] = {
1, 2, 3, // Fibonacci
3, 4, 7, // Lucas
2, 5, 12, // Pell
3, 6, 10, //Triangular
4, 9, 16, // Square
5, 12, 22 // Pentagonal
};
The vector class does not support an explicit initialization list. A somewhat tedious solution is to assign each element explicitly:
vector<int> elem_seq(seq_size);
elem_seq[0] =1;
elem_seq[1] =2;
// ...
elem_seq[17] =22;
One alternative is to initialize a built-in array and use that to initialize the vector:
int elem_vals[seq_size] = {
1, 2, 3, 3, 4, 7, 2, 5, 12,
3, 6, 10, 4, 9, 16, 5, 12, 22 };
// initialize elem_seq with values of elem_vals
vector<int> elem_seq(elem_vals, elem_vals+seq_size);
elem_seq is passed two values. These values are actually addresses. They mark the range of elements with which to initialize the vector.
1.6 Pointers Allow for Flexibility
In this section, we achieve transparency by accessing each vector indirectly by a pointer rather than by name. A pointer introduces a level of indirection to a program. Rather than manipulate an object directly, we manipulate a pointer that holds the address of an object. In our program, we define a pointer that can address a vector of integers. With each loop iteration, we modify the pointer to address a different vector. The actual code that manipulates the pointer does not change.
A pointer holds the address of an object of a particular type. To define a pointer of a particular type, we follow the type name with an asterisk:
int *pi; // pi is a pointer to an object of type int
To retrieve the address of the object rather than its value, we apply the address-of operator (
&):
&ival; // evaluates to the address of ival
To initialize
pi to
ival's address, we write the following:
int *pi = &ival;
To access the object addressed by a pointer, we must
dereference the pointer — that is, retrieve the object sitting at the address held by the pointer. To do that, we apply an asterisk to the pointer as follows:
// dereference pi to access the object it addresses
if (*pi != 1024) // read
*pi = 1024; // write
Note:* 仅在定义时代表定义pi为一个对象的指针,且赋给pi为地址;其他情况均为dereference the pointer,即获取指针所指对象的值;
pi; // evaluates to the address held by pi
we are, in effect, manipulating the pointer object. When we write
*pi; // evaluates to the value of the object addressed by pi
we are manipulating the object pi addresses.
A pointer that addresses no object has an address value of 0 (it is sometimes called a null pointer). Any pointer type can be initialized or assigned a value of 0.
// initialize each pointer to address no object
int *pi = 0;
double *pd = 0;
string *ps = 0;
To guard against dereferencing a null pointer, we test a pointer to see whether its address value is zero. For example,
if (pi && *pi != 1024)
*pi = 1024;
To test whether a pointer is null, we typically use the logical
NOT operator:
if (! pi) // true if pi is set to 0
Here are our six vector sequence objects:
vector<int> fibonacci, lucas, pell, triangular, square,pentagonal;
What does a pointer to a vector of integer objects look like?Well, in general, a pointer has this form:
type_of_object_pointed_to * name_of_pointer_object
Our pointer addresses the type
vector<int>. Let's name it
pv and initialize it to 0:
vector<int> *pv = 0;
pv can address each of the sequence vectors in turn. Of course, we can assign
pv the address of an explicit sequence:
pv = &fibonacci;
// ...
pv = &lucas;
But doing the assignment this way sacrifices code transparency. An alternative solution is to store the address of each sequence within a vector. This technique allows us to access them transparently through an index:
const int seq_cnt = 6;
// an array of seq_cnt pointers to
// objects of type vector<int>
vector<int> *seq_addrs[seq_cnt] = {
&fibonacci, &lucas, &pell,
&triangular, &square, &pentagonal
};
seq_addrs is a built-in array of elements of type
vector<int>*.
seq_addrs[0] holds the address of the
fibonacci vector,
seq_addrs[1] holds the address of the
lucas vector, and so on. We use this to access the individual vectors through an index rather than by name:
vector<int> *current_vec = 0;
// ...
for (int ix = 0; ix < seq_cnt; ++ix)
{
current_vec = seq_addrs[ix];
// all element display is implemented
// indirectly through current_vec
}
We handle a pointer to a class object slightly differently than we handle a pointer to an object of a built-in type. This is because a class object has an associated set of operations that we may wish to invoke. For example,to check whether the first element of the fibonacci vector is set to 1, we might write
if (! fibonacci.empty() && (fibonacci[1] == 1))
How would we achieve the same tests indirectly through
pv? The dot connecting
fibonacci and
empty() is called a
member selection operator. It is used to select class operations through an object of the class. To select a class operation through a pointer, we use the
arrow member selection operator(
->):
! pv->empty()
Because a pointer can address no object, before we invoke
empty() through
pv we must first check that
pv's address is nonzero:
pv && ! pv->empty()
Finally, to apply the subscript operator, we must dereference
pv. (The additional parentheses around the dereference of
pv are necessary because of the higher precedence of the subscript operator.)
if (pv && ! pv->empty() && ((*pv)[1] == 1))
1.7 Writing and Reading Files
To represent a literal file path under Windows, we need to escape the backslash:
"F:\\essential\\programs\\chapter1\\ch1_main.cpp";
To read and write to a file, we must include the
fstream header file:
#include <fstream>
To open a file for output,we define an ofstream (an output file stream) class object, passing it the name of the file to open:
// seq_data.txt is opened in output mode
ofstream outfile("seq_data.txt");
What happens when we declare
outfile? If it doesn't exist, the file is created and opened for output. If it does exist, it is opened for output and any existing data in the file is discarded. If we wish to add to rather than replace the data within an existing file, we must open the file in
append mode. We do this by providing an
ios_base::app second value to the ofstream object. (At this point in this book,it is better that you just use this and not inquire too deeply as to what the heck it actually is!)
// seq_data.txt is opened in append mode
// new data is added at the end of the file
ofstream outfile("seq_data.txt", ios_base::app);
A file may fail to open.Before we write to it, we must confirm that it has been opened successfully.The simplest way to check that is to test the truth value of the class object:
// if outfile evaluates as false,
// the file could not be opened
if (! outfile)
If the file could not be opened, the ofstream class object evaluates to false. In this example, we alert the user by writing a message to
cerr.
cerr represents
standard error.
cerr, like
cout, directs its output to the user's terminal. The difference is that
cerr's output is not buffered; it displays on the user's terminal immediately.
if (! outfile)
// open failed for some reason ...
cerr << "Oops! Unable tosave session data!\n";
else
// ok: outfile is open, let's writethe data
outfile << usr_name << ' '
<< num_tries << ' '
<< num_right << endl;
If the file is opened successfully, we direct output to it in the same way as we do for the ostream class objects
cout and
cerr. In this example, we write three values to
outfile, the second two separated by a space.
endl is a predefined
manipulator provided by the iostream library.
To open a file for input, we define an ifstream class object (aninput file stream), passing it the name of a file. If the file cannot be opened, the ifstream class object tests as false. Otherwise, the file is positioned at the beginning of the data stored in the file.
// infile opened in output mode
ifstream infile("seq_data.txt");
int num_tries = 0;
int num_cor = 0;
if (! infile)
{
// open failed for some reason ...
// we'll presume it is a new user ...
}
else
{
// ok: read each line of the input file
// see if user has played before ...
// format of each line:
// name num_tries num_correct
// nt: number of tries
// nc: number of correct guesses
string name;
int nt;
int nc;
while (infile >> name)
{
infile >> nt >> nc;
if (name == usr_name)
{
// match!
cout << "Welcome back, " << usr_name
<< "\nYour current score is " << nc
<< " out of " << nt << "\nGood Luck!\n";
num_tries = nt;
num_cor = nc;
}
}
}
Each iteration of the while loop reads the next line of the file until the end-of-file is reached. When we write
infile >> name
the return value of the input statement is the class object from which we are reading —
infile in this case. When the end-of-file is reached, the truth condition of the class object evaluates to false. This is why the conditional expression of the
while loop terminates when end-of-file is reached:
while (infile >> name)