Command-Line Arguments
So far, the programs we have considered have not worked with user input. More interesting programs, however, incorporate behavior that responds to user input. We will see two mechanisms for passing input to a program: command-line arguments and standard input.
Command-line arguments are arguments that are passed to a program when it is invoked from a shell or terminal. As an example, consider the following command:
$ g++ -Wall -O1 -std=c++17 -pedantic test.cpp –o test
Here, g++
is the program we are invoking, and the arguments tell
g++
what to do. For instance, the -Wall
argument tells the
g++
compiler to warn about any potential issues in the code,
-O1
tells the compiler to use optimization level 1, and so on.
Command-line arguments are passed to the program through arguments to
main()
. The main()
function may have zero parameters, in which
case the command-line arguments are discarded. It can also have two
parameters [1], so the signature has the following form:
int main(int argc, char *argv[]);
The first argument is the number of command-line arguments passed to
the program, and it is conventionally named argc
. The second,
conventionally named argv
, contains each command-line argument as
a C-style string. An array parameter is
actually a pointer parameter, so the following signature is
equivalent:
int main(int argc, char **argv);
Thus, the second parameter is a pointer to the first element of an array, each element of which is a pointer to the start of a C-style string, as shown in Figure 32.
The command-line arguments also include the name of the program as the first argument – this is often used in printing out error messages from the program.
We recommend converting command-line arguments to std::string
before working with them, as it is less error-prone than working with
C-style strings directly.
As an example, the following program takes an arbitrary number of integral arguments and computes their sum:
#include <iostream>
#include <string> // for string type and stoi() function
using namespace std;
int main(int argc, char *argv[]) {
int sum = 0;
for (int i = 1; i < argc; ++i) {
string arg = argv[i];
sum += stoi(argv[i]);
}
cout << "sum is " << sum << endl;
}
The first argument is skipped, since it is the program name. Each
remaining argument is converted to an int
by the stoi()
function, which takes a string as the argument and returns the integer
that it represents. For example, stoi("123")
returns the number
123 as an int
.
The following is an example of running the program:
$ ./sum.exe 2 4 6 8 10
sum is 30
Input and Output (I/O)
User input can also be obtained through standard input, which
receives data that a user types into the console. In C++, the cin
stream reads data from standard input. Data is extracted into an
object using the extraction operator >>
, and the extraction
interprets the raw character data according to the target data type.
For example, the following code extracts to string
, which extracts
individual words that are separated by whitespace:
string word;
while (cin >> word) {
cout << "word = '" << word << "'" << endl;
}
The extraction operation evaluates to the cin
stream, which has a
truth value – if the extraction succeeds, it is true, but if the
extraction fails, the truth value is false. Thus, the loop above will
continue as long as extraction succeeds.
The following is an example of the program:
$ ./words.exe
hello world!
word = 'hello'
word = 'world!'
goodbye
word = 'goodbye'
The program only receives input after the user presses the enter key.
The first line the user entered contains two words, each of which gets
printed out. Then the program waits for more input. Another word is
entered, so the program reads and prints it out. Finally, the user in
this example inputs an end-of-file character – on Unix-based
systems, the sequence Ctrl-d enters an end of file, while Ctrl-z does
so on Windows systems. The end-of-file marker denotes the end of a
stream, so extracting from cin
fails at that point, ending the
loop above.
The program above prints output to standard output, represented by
the cout
stream. The insertion operator <<
inserts the text
representation of a value into an output stream.
I/O Redirection
Shells allow input redirection, which passes the data from a file
to standard input rather than reading from the keyboard. For instance,
if the file words.in
contains the data:
hello world!
goodbye
Then using the <
symbol before the filename redirects the file to
standard input at the command line:
$ ./words.exe < words.in
word = 'hello'
word = 'world!'
word = 'goodbye'
A file has an implicit end of file at the end of its data, and the program terminates upon reaching the end of the file.
We can also do output redirection, where the shell writes the
contents of standard output to a file. The symbol for output
redirection is >
:
$ ./words.exe > result.out
hello world!
goodbye
$ cat result.out
word = 'hello'
word = 'world!'
word = 'goodbye'
Here, we redirect the output to the file result.out
. We then enter
input from the keyboard, ending with the Ctrl-d sequence. When the
program ends, we use the cat
command to display the contents of
result.out
.
Input and output redirection can also be used together:
$ ./words.exe < words.in > result.out
$ cat result.out
word = 'hello'
word = 'world!'
word = 'goodbye'
Example: Adding Integers
Using standard input, we can write a program that adds up integers
entered by a user. The program will terminate either upon reaching an
end of file or if the user types in the word done
:
#include <iostream>
#include <string> // for stoi()
using namespace std;
int main() {
int sum = 0;
cout << "Enter some numbers to sum." << endl
string word;
while (cin >> word && word != "done") {
sum += stoi(word);
}
cout << "sum is " << sum << endl;
}
The code extracts to a string so that it can be compared to the string
"done"
. (The latter is a C-style string, but C++ strings can be
compared with C-style strings using the built-in comparison
operators.)
The following is an example of running the program:
$ ./sum
Enter some numbers to sum.
2
4
6
done
sum is 12
An alternate version of the program extracts directly to an int
.
However, it can only be terminated by an end of file or other failed
extraction:
#include <iostream>
using namespace std;
int main() {
int sum = 0;
cout << "Enter some numbers to sum." << endl
int number;
while (cin >> number) {
sum += number;
}
cout << "sum is " << sum << endl;
}
File I/O
A program can also read and write files directly using file streams.
It must include the <fstream>
header, and it can then use an
ifstream
to read from a file and an ofstream
to write to a
file. The former supports the same interface as cin
, while the
latter has the same interface as cout
.
An ifstream
object can be created from a file name:
string filename = "words.in";
ifstream fin(filename);
Alternatively, the ifstream
object can be created without a file
name, and then its open()
function can be given the name of the
file to open:
string filename = "words.in";
ifstream fin;
fin.open(filename);
In general, a program should check if the file was successfully opened,
regardless of the mechanism used to create the ifstream
:
if (!fin.is_open()) {
cout << "open failed" << endl;
return 1;
}
Once we’ve determined the file is open, we can read from it like
cin
. The following program reads individual words from the file
words.in
and prints them:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
string filename = "words.in";
ifstream fin;
fin.open(filename);
if (!fin.is_open()) {
cout << "open failed" << endl;
return 1;
}
string word;
while (fin >> word) {
cout << "word = '" << word << "'" << endl;
}
fin.close(); // optional
}
The program closes the file before exiting. Doing so explicitly is
optional – it will happen automatically at the end of the
ifstream
object’s lifetime (e.g. when it goes out of scope if it
is a local variable).
Best practice is to extract from an input stream, whether it is
cin
or an ifstream
, in the test of a loop or conditional. That
way, the test will evaluate to false if the extraction fails. The
following examples all print the last word twice because they do not
check for failure between extracting and printing a word:
while (!fin.fail()) {
fin >> word;
cout << word;
}
while (fin.good()) {
fin >> word;
cout << word;
}
while (!fin.eof()) {
fin >> word;
cout << word;
}
while (fin) {
fin >> word;
cout << word;
}
The following is printed when using any of the loops above:
$ ./main.exe
hello
world!
goodbye
goodbye
Multiple extractions can be placed in the test of a loop by chaining them. The test evaluates to true when all extractions succeed. For example, the following reads two words at a time:
string word1, word2;
while (fin >> word1 >> word2) {
cout << "word1 = '" << word1 << "'" << endl;
cout << "word2 = '" << word2 << "'" << endl;
}
For words.in
, only the first two words are printed, since the test
will fail in the second iteration when it tries to read a fourth word:
$ ./main.exe
word1 = 'hello'
word2 = 'world!'
An entire line can be read using the getline()
function, which
takes in an input stream and a target string (by reference) and
returns whether or not reading the line succeeded. If so, the target
string will contain the full line read:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
string filename = "hello.txt";
ifstream fin;
fin.open(filename);
if (!fin.is_open()) {
cout << "open failed" << endl;
return 1;
}
string line;
while (getline(fin, line)) {
cout << "line = '" << line << "'" << endl;
}
}
For words.in
, this will result in:
$ ./main.exe
line = 'hello world!'
line = 'goodbye'
An ofstream
works similarly to an ifstream
, except that it is
used for printing output to a file. The following program prints data
to the file output.txt
:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
const int SIZE = 4;
int data[SIZE] = { 1, 2, 3, 4 };
string filename = "output.txt";
ofstream fout;
fout.open(filename);
if (!fout.is_open()) {
cout << "open failed" << endl;
return 1;
}
for (int i = 0; i < 4; ++i) {
fout << "data[" << i << "] = " << data[i] << endl;
}
fout.close(); // optional
}
The following shows the resulting data in output.txt
:
$ cat output.txt
data[0] = 1
data[1] = 2
data[2] = 3
data[3] = 4
More on Streams
Previously, we learned about the standard input and output streams, as well as file streams. We examine the relationship between streams more closely now, as well as how to write unit tests using string streams.
A stream is an abstraction over a source of input, from which we can read data, or a sink of output, to which we can write data. Streams support the abstraction of character-based input and output over many underlying resources, including the console, files, the network, strings, and so on.
In C++, input streams generally derive from istream
[2]. We will
see what this means specifically when we look at inheritance and polymorphism in
the future. For our purposes right now, this means that we can pass
different kinds of input-stream objects to a function that takes in a
reference to an istream
. Similarly, output streams generally
derive from ostream
, and we can pass different kinds of
output-stream objects to a function that takes in a reference to an
ostream
.
The istream
type is actually an alias for
basic_istream<char>
, which is an input stream that supports
input using the char
type. The same goes for ostream
and basic_ostream<char>
.
To write data into an output stream, we use the insertion operator
<<
. The actual data written out depends on both the value itself
as well as its type. For instance, if we use a string
as the
right-hand-side operand, the insertion operation will write the
characters from the string into the stream:
int i = 123;
cout << i; // writes the characters 123
double d = 12.3;
cout << d; // writes the characters 12.3
char c = 'c';
cout << c; // writes the character c
string s = "Hello";
cout << s; // writes the characters Hello
Expressions that apply an operator generally evaluate to a value. In the case of stream insertion, the result is the actual stream object itself. This allows us to chain insertion operations:
cout << i << d << endl;
// equivalent to ((cout << i) << d) << endl;
// ^^^^^^^^^
// evaluates back to the cout object
To read data from an input stream, we use the extraction operator
>>
, with an object on the right-hand side. The characters are
interpreted according to the type of the object. For built-in types,
whitespace is generally skipped when extracting.
char c;
cin >> c; // reads a single character; does not skip whitespace
string s;
cout >> s; // reads in one "word", delimited by whitespace
int i;
cin >> i; // attempts to parse the next characters as an integer value
double d;
cin >> d; // attempts to parse the next characters as a floating-point value
As with the insertion operator, an expression that applies the extraction operator evaluates back to the stream itself, allowing extraction operations to be chained:
cin >> c >> s >> i >> d;
String Streams
When writing unit tests, we often want the tests to be standalone
without requiring access to external data. For tests that work with
streams, we can use string streams rather than standard input/output
or file streams. To use a string stream, we #include <sstream>
. We
can then use an istringstream
as an input stream, and an
ostringstream
as an output stream.
The following is an example of using an istringstream
to represent
input data for testing a function that takes in an input stream:
TEST(test_image_basic) {
// A hardcoded PPM image
string input = "P3\n2 2\n255\n255 0 0 0 255 0 \n";
input += "0 0 255 255 255 255 \n";
// Use istringstream for simulated input
istringstream ss_input(input);
Image *img = new Image;
Image_init(img, ss_input);
ASSERT_EQUAL(Image_width(img), 2);
Pixel red = { 255, 0, 0 };
ASSERT_TRUE(Pixel_equal(Image_get_pixel(img, 0, 0), red));
delete img;
}
We start with a string
that contains the actual input data and
then construct an istringstream
from that. We can then pass that
istringstream
object to a function that has a parameter of type
istream &
. When that function extracts data, the result will be
the data from the string we used to construct the istringstream
.
We can similarly use an ostringstream
to test a function that
takes an output stream:
TEST(test_matrix_basic) {
Matrix *mat = new Matrix;
Matrix_init(mat, 3, 3);
Matrix_fill(mat, 0);
Matrix_fill_border(mat, 1);
// Hardcoded correct output
string output_correct = "3 3\n1 1 1 \n1 0 1 \n1 1 1 \n";
// Capture output in ostringstream
ostringstream ss_output;
Matrix_print(mat, ss_output);
ASSERT_EQUAL(ss_output.str(), output_correct);
delete mat;
}
We default construct an ostringstream
object and pass it to a
function with a parameter of type ostream &
. The ostringstream
will internally capture the data that the function inserts into it. We
can then call .str()
on the ostringstream
to obtain a
string
that contains that data, which we can then compare to
another string
that contains the expected output.