B. Stroustrup - The C++ Programming Language (794319), страница 58
Текст из файла (страница 58)
The call ip−>get(ch) reads a single character from the input stream∗ip into ch. By default, get() does not skip whitespace the way >> does. The test if (!ip−>get(ch))succeeds if no character can be read from cin; in this case, Kind::end is returned to terminate thecalculator session. The operator ! (not) is used because get() returns true in case of success.Section 10.2.3Low-Level Input251The standard-library function isspace() provides the standard test for whitespace (§36.2.1);returns a nonzero value if c is a whitespace character and zero otherwise.
The test isimplemented as a table lookup, so using isspace() is much faster than testing for the individualwhitespace characters. Similar functions test if a character is a digit (isdigit()), a letter (isalpha()), ora digit or letter (isalnum()).After whitespace has been skipped, the next character is used to determine what kind of lexicaltoken is coming.The problem caused by >> reading into a string until whitespace is encountered is solved byreading one character at a time until a character that is not a letter or a digit is found:isspace(c)default:// NAME, NAME=, or errorif (isalpha(ch)) {string_value = ch;while (ip−>get(ch) && isalnum(ch))string_value += ch; // append ch to end of string_valueip−>putback(ch);return ct={Kind::name};}Fortunately, these two improvements could both be implemented by modifying a single local section of code.
Constructing programs so that improvements can be implemented through local modifications only is an important design aim.You might worry that adding characters to the end of a string one by one would be inefficient. Itwould be for very long strings, but all modern string implementations provide the ‘‘small stringoptimization’’ (§19.3.3). That means that handling the kind of strings we are likely to use as namesin a calculator (or even in a compiler) doesn’t involve any inefficient operations.
In particular,using a short string doesn’t require any use of free store. The maximum number of characters for ashort string is implementation-dependent, but 14 would be a good guess.10.2.4 Error HandlingIt is always important to detect and report errors. However, for this program, a simple error handling strategy suffices.
The error() function simply counts the errors, writes out an error message,and returns:int no_of_errors;double error(const string& s){no_of_errors++;cerr << "error: " << s << '\n';return 1;}The stream cerr is an unbuffered output stream usually used to report errors (§38.1).The reason for returning a value is that errors typically occur in the middle of the evaluation ofan expression, so we should either abort that evaluation entirely or return a value that is unlikely tocause subsequent errors. The latter is adequate for this simple calculator. Had Token_stream::get()252ExpressionsChapter 10kept track of the line numbers, error() could have informed the user approximately where the erroroccurred.
This would be useful when the calculator is used noninteractively.A more stylized and general error-handling strategy would separate error detection from errorrecovery. This can be implemented using exceptions (see §2.4.3.1, Chapter 13), but what we havehere is quite suitable for a 180-line calculator.10.2.5 The DriverWith all the pieces of the program in place, we need only a driver to start things. I decided on twofunctions: main() to do setup and error reporting and calculate() to handle the actual calculation:Token_stream ts {cin};// use input from cinvoid calculate(){for (;;) {ts.get();if (ts.current().kind == Kind::end) break;if (ts.current().kind == Kind::print) continue;cout << expr(false) << '\n';}}int main(){table["pi"] = 3.1415926535897932385;table["e"] = 2.7182818284590452354;// inser t predefined namescalculate();return no_of_errors;}Conventionally, main() returns zero if the program terminates normally and nonzero otherwise(§2.2.1).
Returning the number of errors accomplishes this nicely. As it happens, the only initialization needed is to insert the predefined names into the symbol table.The primary task of the main loop (in calculate()) is to read expressions and write out theanswer. This is achieved by the line:cout << expr(false) << '\n';The argument false tells expr() that it does not need to call ts.get() to read a token on which to work.Testing for Kind::end ensures that the loop is correctly exited when ts.get() encounters an inputerror or an end-of-file. A break-statement exits its nearest enclosing switch-statement or loop(§9.5). Testing for Kind::print (that is, for '\n' and ';') relieves expr() of the responsibility for handling empty expressions.
A continue-statement is equivalent to going to the very end of a loop.Section 10.2.6Headers25310.2.6 HeadersThe calculator uses standard-library facilities. Therefore, appropriate headers must be #included tocomplete the program:#include<iostream> // I/O#include<string>// strings#include<map>// map#include<cctype> // isalpha(), etc.All of these headers provide facilities in the std namespace, so to use the names they provide wemust either use explicit qualification with std:: or bring the names into the global namespace byusing namespace std;To avoid confusing the discussion of expressions with modularity issues, I did the latter.
Chapter14 and Chapter 15 discuss ways of organizing this calculator into modules using namespaces andhow to organize it into source files.10.2.7 Command-Line ArgumentsAfter the program was written and tested, I found it a bother to first start the program, then type theexpressions, and finally quit. My most common use was to evaluate a single expression.
If thatexpression could be presented as a command-line argument, a few keystrokes could be avoided.A program starts by calling main() (§2.2.1, §15.4). When this is done, main() is given two arguments specifying the number of arguments, conventionally called argc, and an array of arguments,conventionally called argv. The arguments are C-style character strings (§2.2.5, §7.3), so the typeof argv is char∗[argc+1]. The name of the program (as it occurs on the command line) is passed asargv[0], so argc is always at least 1. The list of arguments is zero-terminated; that is, argv[argc]==0.For example, for the commanddc 150/1.1934the arguments have these values:argc:2argv:0"dc""150/1.1934"Because the conventions for calling main() are shared with C, C-style arrays and strings are used.The idea is to read from the command string in the same way that we read from the inputstream.
A stream that reads from a string is unsurprisingly called an istringstream (§38.2.2). So tocalculate expressions presented on the command line, we simply have to get our Token_stream toread from an appropriate istringstream:254ExpressionsChapter 10Token_stream ts {cin};int main(int argc, char∗ argv[]){switch (argc) {case 1:// read from standard inputbreak;case 2:// read from argument stringts.set_input(new istringstream{argv[1]});break;default:error("too many arguments");return 1;}table["pi"] = 3.1415926535897932385;table["e"] = 2.7182818284590452354;// inser t predefined namescalculate();return no_of_errors;}To use an istringstream, include <sstream>.It would be easy to modify main() to accept several command-line arguments, but this does notappear to be necessary, especially as several expressions can be passed as a single argument:dc "rate=1.1934;150/rate;19.75/rate;217/rate"I use quotes because ; is the command separator on my UNIX systems.
Other systems have different conventions for supplying arguments to a program on startup.Simple as they are, argc and argv are still a source of minor, yet annoying, bugs. To avoid thoseand especially to make it easier to pass around the program arguments, I tend to use a simple function to create a vector<string>:vector<string> arguments(int argc, char∗ argv[]){vector<string> res;for (int i = 0; i!=argc; ++i)res.push_back(argv[i]);return res;}More elaborate argument parsing functions are not uncommon.10.2.8 A Note on StyleTo programmers unacquainted with associative arrays, the use of the standard-library map as thesymbol table seems almost like cheating.
It is not. The standard library and other libraries aremeant to be used. Often, a library has received more care in its design and implementation than aSection 10.2.8A Note on Style255programmer could afford for a handcrafted piece of code to be used in just one program.Looking at the code for the calculator, especially at the first version, we can see that there isn’tmuch traditional C-style, low-level code presented.
Many of the traditional tricky details have beenreplaced by uses of standard-library classes such as ostream, string, and map (§4.3.1, §4.2, §4.4.3,§31.4, Chapter 36, Chapter 38).Note the relative scarcity of loops, arithmetic, and assignments. This is the way things ought tobe in code that doesn’t manipulate hardware directly or implement low-level abstractions.10.3 Operator SummaryThis section presents a summary of expressions and some examples.