This is part 1 of a fast paced C++ tutorial for programmers familiar with high-level languages like Python and Perl.
Hello, World!
Let’s start with the traditional Hello, World! program, so that we can test our C++ compiler suite. The file tut01_01.cpp contains:
// tut01_01.cpp -- a simple C++ hello, world program.
#include <iostream>
int
main(int argc, char *argv[])
{
std::cout << "Hello, World!" << std::endl;
}
With GCC, we can compile and run it manually like this:
% c++ -Wall -o tut01_01 tut01_01.cpp % ./tut01_01 Hello, World!
Exercise: Try to compile and run tut01_01.cpp on your platform. Unless you’re using a Unix-like operating system, you may have to install a C++ compiler, and figure out how to call it. You can’t proceed with this tutorial unless you’re able to compile C++ programs on your own.
Interactive Hello, World!
Now, let’s get some user input. Copy tut01_01.cpp into tut01_02.cpp and change it like this:
// tut01_02.cpp -- a simple C++ hello, world program, with user input
#include <string>
#include <iostream>
int
main(int argc, char *argv[])
{
std::string theName;
std::cout << "Hi! What's your name? ";
std::cin >> theName;
std::cout << "Welcome, " << theName << "!"
<< std::endl;
}
Again, compile and run:
% c++ -Wall -o tut01_02 tut01_02.cpp % ./tut01_02 Hi! What's your name? John Welcome, John!
But what if John entered his whole name?
% ./tut01_02 Hi! What's your name? John Doe Welcome, John!
As we can see, std::cin >> theName truncated the input, stopping at the first whitespace. Or, to be more precise, the input operator, applied to a std::string reads characters up to the next whitespace. This is counter-intuitive and the first gotcha. For Perl or Python programmers, reading a whole line is a lot more natural. To do this, we’ll use the std::getline() function:
// tut01_03.cpp -- a simple C++ hello, world program, with user input
#include <string>
#include <iostream>
int
main(int argc, char *argv[])
{
std::string theName;
std::cout << "Hi! What's your name? ";
std::getline(std::cin, theName); // read a whole line
std::cout << "Welcome, " << theName << "!"
<< std::endl;
}
Let’s test it:
% c++ -Wall -o tut01_03 tut01_03.cpp % ./tut01_03 Hi! What's your name? John Doe Welcome, John Doe!
Getting more data from the user
Now we care how old the user is (or pretends he/she is):
// tut01_04.cpp -- a simple C++ hello, world program, with user input
#include <string>
#include <iostream>
const unsigned short ADULT_AGE = 18;
int
main(int argc, char *argv[])
{
std::string theName;
unsigned short theAge;
std::cout << "Hi! What's your name? ";
std::getline(std::cin, theName); // read a whole line
std::cout << "How old are you? ";
std::cin >> theAge;
std::cout << "Hey, " << theName << ", welcome to ";
if (theAge < ADULT_AGE)
std::cout << "Disneyland!\n";
else
std::cout << "XXX-land!\n";
}
Trying it out:
% c++ -Wall -o tut01_04 tut01_04.cpp % ./tut01_04 Hi! What's your name? John Doe How old are you? 31 Hey, John Doe, welcome to XXX-land! % ./tut01_04 Hi! What's your name? Mickey TooYoung How old are you? 3 Hey, Mickey TooYoung, welcome to Disneyland!
An object-oriented version
Encapsulating theName and theAge into a single object would be neat. The file tut01_05.cpp starts with a definition of our new class TheUser:
// tut01_05.cpp -- a simple C++ hello, world program, class oriented.
#include <string>
#include <iostream>
const unsigned short ADULT_AGE = 18;
class TheUser
{
public:
std::string theName_;
unsigned short theAge_;
};
To use that class, we append the following main() function to the end of the source file tut01_05.cpp:
int
main(int argc, char *argv[])
{
TheUser theUser;
std::cout << "Hi! What's your name? ";
std::getline(std::cin, theUser.theName_);
std::cout << "How old are you? ";
std::cin >> theUser.theAge_;
std::cout << "Hey, " << theUser.theName_ << ", welcome to ";
if (theUser.theAge_ < ADULT_AGE)
std::cout << "Disneyland!\n";
else
std::cout << "XXX-land!\n";
}
We instantiate an object theUser of type TheUser, and start using its (public) members theName_ and theAge_.
Compiling and running tut01_05.cpp yields:
% c++ -Wall -o tut01_05 tut01_05.cpp % ./tut01_05 Hi! What's your name? John Doe How old are you? 32 Hey, John Doe, welcome to XXX-land!
Instead of using a class, and made the members public, we could also have used a struct in this particular case, as in:
struct TheUser
{
std::string theName_;
unsigned short theAge_;
};
But, the members shouldn’t be publicly accessible from the outside, if we want to uphold data encapsulation principles. We can hide the members, making them private. Then we should relax the hiding by making them indirectly accessible through accessor functions (which could perform additional checks, but won’t in our simple example):
// tut01_06.cpp -- a simple C++ hello, world program, class oriented
#include <string>
#include <iostream>
typedef unsigned short age_t;
const age_t ADULT_AGE = 18;
class TheUser
{
public:
TheUser(std::string theName = "N/A", age_t theAge = 0) :
theName_(theName), theAge_(theAge) { }
const std::string getName() const { return theName_; }
const age_t getAge() const { return theAge_; }
void setName(const std::string &newName) { theName_ = newName; }
void setAge(const age_t newAge) { theAge_ = newAge; }
private:
std::string theName_;
age_t theAge_;
};
Note that we’ve introduced a custom type age_t.
We can use this slightly modified class in the following main(). The code is similar to tut01_05.cpp, with the exception that we now access the member functions indrectly through the accessors getName() and getAge().
int
main(int argc, char *argv[])
{
std::string someName;
age_t someAge;
std::cout << "Hi! What's your name? ";
std::getline(std::cin, someName);
std::cout << "How old are you? ";
std::cin >> someAge;
TheUser theUser(someName, someAge);
std::cout << "Hey, " << theUser.getName() << ", welcome to ";
if (theUser.getAge() < ADULT_AGE)
std::cout << "Disneyland!\n";
else
std::cout << "XXX-land!\n";
}
Compiling and interacting with tut01_06 is the same as with tut01_05.
Input and output operators
It would be nice if we could input and output TheUser instances as if they were POD (plain old data) data types (like int, short etc…). That’s easy if we override the operators like this:
// tut01_07.cpp -- a simple C++ hello, world program, iostream operators
#include <string>
#include <iostream>
#include <istream>
#include <ostream>
#include <iomanip>
typedef unsigned short age_t;
const age_t ADULT_AGE = 18;
class TheUser
{
public:
TheUser(std::string theName = "N/A", age_t theAge = 0) :
theName_(theName), theAge_(theAge) { }
friend std::istream& operator>> (std::istream &in, TheUser &user);
friend std::ostream& operator<< (std::ostream &out, const TheUser &user);
private:
std::string theName_;
age_t theAge_;
};
std::istream& operator>> (std::istream &in, TheUser &user)
{
in >> user.theAge_ >> std::ws;
std::getline(in, user.theName_);
return in;
}
std::ostream& operator<< (std::ostream &out, const TheUser &user)
{
return out << user.theAge_ << " " << user.theName_ << std::endl;
}
Operator operator>> operates on a std::istream (defined in <istream>), which it should return after use. Operator operator<< operates on a std::ostream (defined in <ostream>), which it also should return after use. The second argument to both operators is the instance that to be input into, or output from. The best way to get used to those operators is to remember their signature by heart.
Note that the manipulator std::ws (defined in <iomanip>) swallows whitespaces.
Both operators are declare friend of TheUser, so that they can access TheUser‘s private (and protected) data members TheName_ and TheAge_.
With those operators in place, we can now write an object with std::cout << theUser, and read an object with std::cin >> theUser, as if theUser was a POD type like int or age_t:
int
main(int argc, char *argv[])
{
TheUser theUser;
std::cout << "Uninitialized: " << theUser << std::endl;
std::cout << "Please enter age [whitespace(s)] name: ";
std::cin >> theUser;
std::cout << "The user is: " << theUser;
}
See how readable the code has become?
Compling and interacting with this program:
% c++ -Wall -o tut01_07 tut01_07.cpp % ./tut01_07 Uninitialized: 0, N/A Please enter age [whitespace(s)] name: 32 John Doe The user is: 32 John Doe
File I/O
What’s a program worth if it can’t persist its data somewhere. We’d like to save and load instances of TheUser to and from files, respectively, one instance per line. We start where we’ve left off in tut01_07.cpp. With the addition of the headers <fstream> and <cstdlib> and the member function bump_age(), everything remains the same:
// tut01_08.cpp -- a simple C++ hello, world program, file streams
#include <string>
#include <iostream>
#include <istream>
#include <ostream>
#include <iomanip>
#include <fstream>
#include <cstdlib>
typedef unsigned short age_t;
const age_t ADULT_AGE = 18;
class TheUser
{
public:
TheUser(std::string theName = "N/A", age_t theAge = 0) :
theName_(theName), theAge_(theAge) { }
void bump_age(void) { ++theAge_; }
friend std::istream& operator>> (std::istream &in, TheUser &user);
friend std::ostream& operator<< (std::ostream &out, const TheUser &user);
private:
std::string theName_;
age_t theAge_;
};
std::istream& operator>> (std::istream &in, TheUser &user)
{
in >> user.theAge_ >> std::ws;
std::getline(in, user.theName_);
return in;
}
std::ostream& operator<< (std::ostream &out, const TheUser &user)
{
return out << user.theAge_ << " " << user.theName_ << std::endl;
}
Please note that the input and output operators still operate on std::istream and std::ostream. As such, they are file stream agnostic. But that will come in handy now. In the function main(), we make use of std::ifstream and std::ofstream (defined in <fstream>):
int
main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "Usage: " << argv[0]
<< " infile.dat outfile.dat"
<< std::endl;
return EXIT_FAILURE;
}
std::ifstream ifs(argv[1]);
std::ofstream ofs(argv[2]);
TheUser aUser;
while (ifs >> aUser) {
aUser.bump_age();
ofs << aUser;
}
ifs.close();
ofs.close();
return EXIT_SUCCESS;
}
Assuming we started with the file ifile.dat:
32 John Doe 8 Mickey TooYoung
compiling, then running the program tut01_08.cpp yields:
% c++ -Wall -o tut01_08 tut01_08.cpp % ./tut01_08 ifile.dat ofile.dat % cat ofile.dat 33 John Doe 9 Mickey TooYoung
As you can see, a std::ifstream is a std::istream, and a std::ofstream is a std::ostream, so the input and output operators could be used unchanged.
From a practical point of view, what’s important here is that the output stream must be readable 1:1 back into an input stream. Should we decide to add additional fields (members) to TheUser, those fields should be placed in such a way as to preserve the property of reading back what we’ve written out. If necessary, some fields will need to be enclosed in some well-defined delimiters, as in:
32 "John Doe" "15. Yellow Drive"
Obviously, the delimiter must be escaped if necessary:
32 "John \"the weasel\" Doe" "15. Yellow Drive"
Writing this out is easy. Modify the output operator accordingly. Reading it back in is a lot more complicated. This is where other techniques can be leveraged, like boost::serialization, XML parsers etc… (in another tutorial).
Containers
Creating a couple of TheUser instances is easy. Storing them in an appropriate data structure is easy too.
First, let’s modularize the data. The header tut01_09.h defines the class TheUser:
// tut01_09.h -- the class TheUser
#ifndef TUT01_09_H_INCLUDED
#define TUT01_09_H_INCLUDED
#include <string>
#include <istream>
#include <ostream>
typedef unsigned short age_t;
const age_t ADULT_AGE = 18;
class TheUser
{
public:
TheUser(std::string theName = "N/A", age_t theAge = 0) :
theName_(theName), theAge_(theAge) { }
void bump_age(void) { ++theAge_; }
friend std::istream& operator>> (std::istream &in, TheUser &user);
friend std::ostream& operator<< (std::ostream &out, const TheUser &user);
private:
std::string theName_;
age_t theAge_;
};
#endif // TUT01_09_H_INCLUDED
The implementation of the input and output operators (and later possibly additional functions) goes into tut01_09.cpp:
// tut01_09.cpp -- the class TheUser
#include "tut01_09.h"
#include <iomanip>
std::istream& operator>> (std::istream &in, TheUser &user)
{
in >> user.theAge_ >> std::ws;
std::getline(in, user.theName_);
return in;
}
std::ostream& operator<< (std::ostream &out, const TheUser &user)
{
return out << user.theAge_ << " " << user.theName_ << std::endl;
}
tut01_09.cpp could be compiled separately into an object file tut01_09.o, but we won’t do that in this example.
Finally, main() goes into the driver program tut01_09a.cpp:
// tut01_09a.cpp -- driver for tut01_09.cpp
#include "tut01_09.h"
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <vector>
typedef std::vector<TheUser> theuser_vec_t;
int
main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "Usage: " << argv[0]
<< " infile.dat outfile.dat"
<< std::endl;
return EXIT_FAILURE;
}
theuser_vec_t aVec;
TheUser aUser;
// Read TheUser instances into aVec
std::ifstream ifs(argv[1]);
while (ifs >> aUser) {
aUser.bump_age();
aVec.push_back(aUser);
}
ifs.close();
// Write TheUser instances into file
std::ofstream ofs(argv[2]);
if (!aVec.empty()) {
typedef theuser_vec_t::const_iterator vec_iter_t;
for (vec_iter_t i = aVec.begin(); i != aVec.end(); ++i)
ofs << *i;
}
ofs.close();
return EXIT_SUCCESS;
}
We used a std::vector container from the STL (Standard Template Library), instantiated to the data type TheUser. We read all instances of TheUser from a file (see previous example) into that container. Then we write every object of this container back out into another file, after incrementing the age of each TheUser instance.
We compile this program like this:
% c++ -Wall -o tut01_09a tut01_09.cpp tut01_09a.cpp % ./tut01_09a Usage: ./tut01_09a infile.dat outfile.dat
Starting again with ifile.dat, we get the same output file ofile.dat as shown above:
% cat ifile.dat 32 John Doe 8 Mickey TooYoung % ./tut01_09a ifile.dat ofile.dat % cat ofile.dat 33 John Doe 9 Mickey TooYoung
Exercise: Notice how the two whitespace between 8 and Mickey TooYoung in ifile.dat collapsed into a single whitespace between 9 and Mickey TooYoung in ofile.dat? Explain.