CS 2124 — Object Oriented Programming

Introduction to C++

Topics

Caveat Emptor

The purpose of these notes is to get you started and help you focus on what you most need to know to write simple useful C++ programs.

This introduction covers the most basic elements of C++. I assume that you have already programmed in some other language so my goal is to show you the C++ way of writing the sorts of things you already know. I am not covering all the details of these features. We will cover more throughout the semester.

To get a solid understanding of the material, use these notes together with the sample code, the inclass code, your own notes from class and your reading from the recommended books. And of course, you will be writing lots of code. Hopefully more than just what we assign in recitation and as homework. Play around with the stuff. Try out different things.

Before we go on, note that much of the core syntax in C++ will look familiar to almost any programmer nowadays. Why? Because most languages base their syntax on the language C, and certainly C++ is no exception to that.

Simplest Program

Here is the simplest possible program:

int main() { return 0; }

It consists of a single function called main. Every program must have a function called main which has a return type of int. In the code above, main does not take any arguments, which is indicated by the open and close parentheses with nothing between them. The body of the function is enclosed in curly braces. C++ uses curly braces wherever it needs to group multiple lines of code together into a single unit. Other examples will be when defining loops and conditional expressions (i.e. if-then-else statements). The one line of code that is in the function says to return the value zero.

Statements in C++ all end with a semi-colon.

Actually I lied, just a little. This isn't the simplest program. C++ treats the function main specially. In main, but not in any other function, if we leave off the return statement, the compiler will automatically insert a return 0, so our simplest program can actually be written as:

int main() {  }

Hello World

The program above is about as uninteresting as you can get. Just a little better would be the familiar overly used example known as Hello World:

int main() {
   cout << "Hello world!";
}

We have added a line of output. The symbol cout is a variable that holds what is called an output stream. It usually represents the computer's screen. [Actually it represents what's know as "standard output", which defaults to the screen but can be redirected when you are running the program to go anywhere you like. We won't worry about that for now.] Yes, there will be input streams, too. Streams can also be defined that will be used for reading or writing files. For now, all we need is this one output stream, cout.

BTW, if you run it from your Visual Studio IDE, the display window may "disappear" before you get to see the actual output. The TA's will help you fix that in lab.

The pair of right-angle brackets (aka less-than signs) indicate that the expression to the right, "Hello world!" should be output to the stream on the left, cout. We often refer to << as the "output operator". [We could go into other uses of that operator, but not now.]

And the line of code ends with a semi-colon.

Oh sorry, I lied again. (I promise not to do that too often.) If you tried our hello world program, you found it didn't even compile. We need to tell the compiler a couple of things. The first is, what library to look in to get both the definition of that symbol cout and the code that is used for the ouput operator. To do that we change the program slightly, adding two lines:

#include <iostream>
using namespace std;

int main() {
   cout << "Hello world!";
}

The #include tells the compiler to read in the library known as iostream. The "using namespace" line saves us some typing later on. If we left it off, then we would again have to change the program slightly, adding a "qualifier" to the cout symbol:

#include <iostream>

int main() {
   std::cout << "Hello world!";
}

Being lazy, we usually like to avoid having to type those qualifiers, so the single using statement is often seen immediately after any includes. We will discuss the meaning of the using statement in more detail later on.

Oh, one last thing. Running the above program might look a little messy. If we want a "newline" character to get added at the end of our output, we can tack the newline character, '\n', on to the string.

#include <iostream>
using namespace std;

int main() {
   cout << "Hello world!\n";
}

Variables

To start making our program interesting (which probably won't happen in these notes), we need variables. C++ requires that all variables be defined before they are used and furthermore that every variable must have a type that sticks with the variable until it ceases to exist.

Suppose we want to modify the previous program to ask the "user" for a name to say hello to. We need to define a variable to hold the name and then give the user a chance to provide some input. Let's start small and just make a simple change to our program to use a variable to specify who we are saying hello to:

#include <iostream>
#include <string>
using namespace std;

int main() {
   string name = "world";
   cout << "Hello ";
   cout << name;
   cout << "!\n";
}

We have defined a variable called name, whose type is string. The type string is defined in another library and so we had to provide another include at the beginning of the program. We also provided a value for name. All that was left was to print out the three parts of our greeting: hello, the name and the exclamation mark followed by the newline character.

Programs would get pretty long if we had to use a separate line for each part of the output. C++ allows us to "chain" out operators. Just as you can write 1 + 2 + 3, chaining the plus operator, we can also chaing the output operator, writing cout << "Hello " << name << "!\n";. The resulting program is:

#include <iostream>
#include <string>
using namespace std;

int main() {
   string name = "world";
   cout << "Hello " << name << "!\n";
}

This didn't improve our program any, but we can go on from here to ask the user for their name and then greet them. We have alrady seen how to do ouput to standard ouput. Now we will need to introduce how to get input from standar input:

#include <iostream>
#include <string>
using namespace std;

int main() {
   string name;
   cout << "What is your name? ";
   cin >> name;
   cout << "Hello " << name << "!\n";
}

The symbol cin is also defined in the iostream library. It represents the standard input stream. Note the use of the >> operator as the "input operator", allowing us to read from standard input into the string variable name. We will discuss details of i/o later on, but reading a string from an input stream this way skips over any "whitespace", then grabs characters until it meets the next whitespace or until the end of the input stream. This provides a convenient way to read a single "word", in this case a sequence of characters delimited by whitespace.

Since we didn't have any suitable value for name until we got one from the user, I didn't provide any initial value here. When we don't initialize a string, it automatically gets initialized to the "empty string", meaning it is a string with zero characters. [Later on, we will learn exactly how that initialization works. But that has to wait till we start defining our own classes.]

C++ has a large number of types that we can use with our variables and it also allows us to define new types. We'll explore how to define types later on. In fact that idea is a major focus of this course. We should become familiar with the more common types that are built into the language (i.e., they don't require any includes).

There are others: short, long and float. Furthermore, some of them (the "integer types", char, short, int and long) can be modified with unsigned. But for most purposes, the four types above will suffice.

Conditions

C++ conditions follow the if / else if / else pattern. Examples:

// a simple if statement
if (answer == 42) {
   cout << "Now what was the question?";
}
// an if-else statement if (answer == 42) { cout << "Now what was the question?\n"; } else { cout << "The answer wasn't 42\n"; } // an if-else-if-else statement if (answer == 42) { cout << "Now what was the question?\n"; } else if (answer == 17) { cout << "That's pretty random\n"; } else { cout << "Wasn't anything we tested for.\n"; }

Clearly we can continue the pattern indefinitely.

Note that the condition must be in parentheses. The code to be executed for that condition should be in braces, aka curly brackets.

Logical operators

C++ uses

C++ also supports keywords for those operations, i.e "and", "or" and "not". Use of those keywords is discouraged (i.e. don't use them in this course). The default settings on some compilers will even flag them as errors. Why? Because most C++ programmers would be very surprised to see their use. They are present primarily to support non-English systems, where character encodings may not support the needed punctuation.

Looping

C++ has three types of looping, the for loop, while loop and do-while loop. Ok, if you're using C++11 (recommended), then there is also a neat looping style called the "ranged for", which we will introduce further down when we discuss vectors.

for loop

The for loop is most commonly used when you want to do something a particular number of times. E.g.:

#include <iostream>
using namespace std;

int main() {
   for (int i = 0; i < 5; ++i) {
      cout << "Hello world!\n";
   }
}

The for loop consists for three parts inside the parentheses and a "body" afterwards. The three parts are the 1) initialization, 2) test and 3) increment. The semi-colons are used to separate the tree pieces.

  1. In the initialization phase, I defined a counter called i. (This sort of loop counter is one of the few cases that I would use a one-letter variable name. Usually longer names make your code much easier to read.) By defining the variable here, it only exists within this for loop. This is a matter of something called scope, that we will discuss elsewhere. The scope, or visibility, of the counter defined this way lasts only from it's definition until the end of the loop. Anywhere else, use of a variable of that name is considered a different variable. Note, that it's a good thing to limit scope this way!
  2. The test is evaluated each time through the loop, inluding the first time. It determines when we should stop looping.
  3. What I called the "increment" part is whatever code we want to use after a pass through the loop and before we do the test to decide if we should begin another loop. In this example, I used ++i, which says to increase the value of variable i by one. (Later we will discuss the difference between ++i and i++. You can use either here.)

while loop

The purpose of the while loop is to continue looping over a block of code so long as some condition it true. The for loop that we wrote earlier could be written instead with a while loop:

#include <iostream>
using namespace std;

int main() {
   int i = 0;
   while (i < 5) {
      cout << "Hello world!\n";
      ++i;
   }
}

Certainly the for loop was a better solution in this example. In addition to being fewer lines, it also has the advantage of making it clear at a glance what the purpose is. Also, as mentioned before, it limits the scope of the counting variable to the loop.

We will see examples shortly in which a while loop is a better fit. Picking the most appropriate construct naturally makes code easier to read.

do-while loop

Probably the least often used looping construct, the do-while is used when you definitely want to do the body at least once and then stop when some condition is false. A loop would look something like:

do {
   // Some stuff we want to do
} while ( test() );

By the way, this shows an example of a comment. When you use a // then everything till the end of the line is ignored by the compiler.

So the do-while would first do whatever is inside the block, here just a comment, and then check the test. If the test is true we go through the loop again...

File I/O

First, I should make clear that we are going to assume that all of the files we will deal with are text files. The text can be a Shakespearian sonnet, the source code for a program, a set of passwords or even the digits of pi (well, not all of them). But whatever it is it will be text.

It will not be, for example, the binary executable for a program. Or a jpeg file containing an image. Or a wav file containing a sound. Sure, we can use C++ to read and write such files, but we have more important things to learn to do.

Simple File Input

To read a file, first we need to attach a "stream object" to it. A stream that can get input from a file has a type of ifstream. If we want to read a file called data.txt, we need to associate it with an ifstream variable. Here's a line of code to define a variable called ifs with the data.txt file and open it.

ifstream ifs("data.txt").

Note, I often use the name ifs to stand for an input file stream in examples, but in real code, I would use a name that better fit with the file that I was opening.

We should immediately check that the file opened successfully, otherwise we could end up trying to read from a file that wasn't there. In that case, it could take us a while to figure out why out code didn't work. To test if we failed to open correctly, the typical idiom would be something like:

if (!ifs) {
   cerr << "Could not open the file.\n";
   exit(1);
}

The test for !ifs returns true if there is something wrong with the stream. Since we just tried to open it, the problem must be that we couldn't. The most likely explanation is that the file isn't actually where we thought it was. Or maybe we misspelled the name. Whatever, if our program doesn't end up generating the correct output, it's awfully good to know that it was simply because we had a problem opening the file, so we won't bother looking for an error in our code that isn't there.

Next we will want to read from the file. How to do that will depend on what kind of information is in the file and how we want to use it. If it is a newspaper article, do we want to read it line by line or word by word? Or character by character? Or, what if it is a text file that contains numbers, i.e. sequences of characters '0' to '9', separated by spaces and newlines. Do we want to treat them as integers? Or maybe they are actually floating point numbers.

The most common way to read from a file is with a line like:

ifs >> x;

C++ would first skip over any whitespace, e.g. blanks, tabs or even newline characters. (We'll talk later about what to do if we don't want to skip the whitespace.)

C++ then decides how to interpret what it sees in file based on the type of the variable x. If it is of type int, then C++ will look to read an integer, such as 42. , the read until it ran out of digits to read and store the resulting integer into x.

Yes, we have to think about what happens if there weren't any digits to read at all. In general, if we try to read from a file and the attempt fails, the program will continue but we won't be able to do any reading from the file again. We will talk more about that later.

So that was what happened if x was of type int. What if it was of type string? Then after having skipped over the whitespace, C++ reads the following non-whitespace characters until it is stopped by another whitespace (or, of course, the end of the file).

Note that when we are reading into a string, we don't care if the characters we see are letter or digits or punctuation. So long as they aren't whitespace, we read them.

Looping Through a File

Suppose we have a file of integers and we want to read them in and then print them out. Clearly we need a loop. The usual loop looks like:

int anInt;
while (ifs >> anInt) {
   cout << anInt << endl;
}

For now, the best way to interpret that loop to say: "So long as we can successfully read an integer, then read it into the variable anInt and print it out on a separate line on standard output." Later on, we will discuss exactly what the value of the expression "ifs >> anInt" really is and how it works here.

Getting a Line

Often you will want to read an entire line at a time, complete with any whitespace. This is one case where we don't use the input operator. Instead we use a function getline, as in:

string line;
getline(cin, line);

The variable line will get everything from the current position to the end of the line. It won't have the end-of-line character included, even though that character will have been read from the stream. We can read the entire file line by line, printing the lines as we go with:

while (getline(ifs, line)) {
   cout << line << endl;
}

Close a File

In order to close a file, just call its close method:

ifs.close();

It is a good habit to close a file whenever you are done with it. Even if you are at the end of the program.

And a few more small things...

There plenty more we could say, but no sense getting lost in details. I'll just mention a couple of other things that can be useful, but that are not needed nearly as often as what we have discussed above.

If you have attempted to read something and couldn't, either because you came to the end of the file, or something of the type you wanted to read wasn't at that spot in the file, some flags get set inside the stream object. These flags will prevent you from reading anything else from the file. Say you tried to read an int but there wasn't any int there. Instead there was the word "walrus". Well, after the attempt to read into an int variable failed, you still might want to try reading into into a string. But first you will need to "clear" those flags. This can be done with

ifs.clear();

Another convenience at times is to be able to "jump" to a location in the file. For example, you may have already read through the file and want to begin again at the beginning for some odd reason. To do so, you can call:

// reset the file position to the beginning
ifs.seekg(0);

Remember that if you had already read all the way to the end of file, you will nead to call clear before you can do any more reading, or even before you can seek.

Vectors

Every language needs a flexible "container" that can hold any number of things, can grow as needed and provides quick access to any item given a number, its "index" or "subscript". In C++, this is done using the vector.

How do we use vectors? There's a lot we could cover here, but we just want the basics that make our lives easier. First, vectors are defined in a library so we have to include it:

#include <vector>

To define a variable as a vector, we have to say what kind of vector it is. Is it a vector of ints? A vector of Elephants? To define a variable called "vectorOfInts" to be a, well, vector of ints:

vector<int> vectorOfInts;

Note that the type of the variable is vector<int>.

Similarly, to define a variable called "vectorOfElephants" to be a vector of Elephants (assuming that someone has defined an Elephant type):

vector<Elephant> vectorOfElephants;

Later (when we cover templates) we will talk a lot more about the use of these "angle brackets" around the int and the Elephant.

To access an element of the vector, we usually use square brackets to specify which element. Inside the brackets goes the index. The first element is always numbered zero. Therefore, if our vector of ints had at least 17 items in it, and we want to print out the 17th element to standard output (usually the screen), then we write:

cout << vectorOfInts[16]; // The 17th element

But how many elements are there in the vector to begin with? None! At least there are none if we didn't say how many to start out with. (And we didn't say anything about the size when we defined vectorOfInts.) How do we add elements to a vector that doesn't have any? The easiest way to add elements to a vector is to add them to the end, i.e. after the last element in the vector. To do that then we use the method "push_back". Just pass the value that you want to put at the end of the vector and the vector will grow long enough to hold it. Here we will add two values to the end of our empty:

vectorOfInts.push_back(5);
vectorOfInts.push_back(8);

Now we have two items in the vector. We could print them out with:

cout << vectorOfInts[0] << ' ' << vectorOfInts[1];

Looping over a vector

What if there were more than just those two elements in our vector and we want to print them out? Obviously we should use a loop. Using the for loop discussed above, the code would look like:

for (size_t i = 0; i < vectorOfInts.size(); ++i) {
    cout << vectorOfInts[i] << endl;
}

In this code, we introduced two new features: vector's size method and the type, size_t. The method's purpose is obvious, it returns the count of items that the vector is holding.

What is the purpose to the type? Why not just use int? The official answer is that int might not be the right type for some compiler on some operating system on some computer. Maybe int won't be "big enough" to hold the size of a vector. To allow your code to be "portable", the type size_t was defined. It will always match up with whatever the size method returns. You may not be concerned with portabilty right now (though you should be!), however your compiler is. It will give you a warning when you compile your program if you use int instead of size_t, complaining of a "type mismatch". Avoid warnings, use size_t.

One last thing I should mention about size_t, for most compilers, size_t is considered unsigned, meaning it will never be negative. If you had a size_t variable that was set to zero and subtracted one from it, then the result, instead of being negative, would "wrap around" and be the largest possible positive value.

Ranged For

Back to looping. There is an easier way to loop over a vector! It is known as the ranged for. But as mentioned above, this is a new feature provided by the C++11 standard, so be sure to have a current version of your compiler. (The current versions of Visual Studio and g++ support the ranged for.)

for (int x : vectorOfInts) {
    cout << x << endl;
}

That's a lot easier! We didn't have to introduce an index variable or even discuss size_t or size. There's less to write, so you are more likely to type it correctly. And there's less to read, so anyone reading your code will have an easier time knowing what you are saying. Definitely a win-win.

I do have to make one technical detail clear. While our ranged for accomplishes the same thing as the traditional for loop I showed first, there is an important difference that goes beyond appearances. The ranged for is actually equivalent to the following variation on our loop:

for (size_t i = 0; i < vectorOfInts.size(); ++i) {
    int x = vectorOfInts[i];
    cout << x << endl;
}

So, what's the important difference? Notice that x is a copy of the element in the vector. Why does that matter? It doesn't in our example of printing the elements in a vector of ints. But what if we wanted to modify the contents of the vector? Below is a loop that looks like it modifies all of the elements in the vector to be 17. In fact, it doesn't change the contents of the vector at all.

for (int x : vectorOfInts) {
    x = 17;  // Does not modify the vector
}

If we want to change the contents, then we need to use a feature we introduced in the discussion of parameter passing, the "reference"

for (int& x : vectorOfInts) {
    x = 17;  //  Does modify the vector
}

See how we specified x's type to be a reference to an int, by using the "and-sign"? Now there won't be any copying. Instead x will be an alias for each element of the vector as it steps through the loop. (If you have a Java background, you should notice that this is different and more flexible that Java's "foreach" loop.)

Initializing a vector, specifying it's size

Suppose we want to have the vector start out with, e.g. 28 elements, all with the value 17, we could have defined our vector as:

vector<int> vectorOfInts(28, 17); // initialised to hold 28 seventeens.

Or, alternatively if we wanted 28 elelments but were willing to settle for the "default" value for the type, which for numbers is zero, we could have defined it as:

vector<int> vectorOfInts(28); // initialized to 28 zeros.

Note that if we had provided the size 28, as shown above in either example, then doing a subsequent push_back would, of course, add a 29th element.

However, it is more common that we will simply want to add to the end of an initially empty vector, hence you will most frequently see us definee our vectors without specifying a size:

vector<int> vectorOfInts; // initialized to a size of zero.

Defining a function that has a vector parameter

When writing a function that will take a vector of ints as an argument, the type is vector<int>. But we will almost certainly want to pass the vector by reference! Otherwise when we try to change the values stored in it, the changes will only happen to a copy! This is very different from passing arrays. Furthermore, if we are not going to change the constents of the vector then pass it by constant reference! Why? So that we don't waste time and space making a copy of the vector.

void displayVector(const vector<int>& aVector) {
   for (size_t i = 0; i < aVector.size(); ++i) {
      cout << aVector[i] << endl;
   }
}

Useful Vector Methods

For most of our uses of vectors, only the two methods that were described above are really needed.

What other methods are useful?

2D Vectors

There is nothing new in this section as far as C++ features, but I find that students are sometimes unsure how to handle a 2D world.

The following code examples are going to consider how to build up and initialize a two-dimensional world of ints in which all of the cells start out with the same value.

    int rows = 3;
    int cols = 4;
    vector<vector<int>> world;  // Our 2d world
    for (int r = 0; r < rows; ++r) {
        // The row that we will build up                        
        vector<int> aRow;  
        // Building up a row of 17's
        for (int c = 0; c < cols; ++c) { 
            aRow.push_back(17); 
        }
        world.push_back(aRow);
    }

That code is ok. But some students, especially if they are coming from a language like Python, really want a shorter way to do the same thing.

That code is perfectly fine, but in a moment we will consider other ways to write it. But first we should make sure we have the right results (test early and often!).

Of course there are the two usual approaches to looping over vectors, either by index or more directly by the elements in the collection. Let's do both!

First the good old-fashioned way, by index:

    for (size_t rowI = 0; rowI < world.size(); ++rowI) {
        for (size_t colI = 0; colI < world[0].size(); ++colI) {
            cout << world[rowI][colI] << ' ';
        }
        cout << endl;
    }
    

And now using the ranged for to directly access the elements. What does the world hold? Rows, represented as vectors.

    for (const vector& aRow : world) {
        for (int cell : aRow) {
            cout << cell << ' ';
        }
        cout << endl;
    }
    

Now back to modifications to the original code that created the 2D vector. Our first stelp will be to consider an alternative to that internal loop. We did discuss this before. Using one of the vector class's constructors we can simplify, or at least shorten, the above code to:

    int rows = 3;
    int cols = 4;
    vector<vector<int>> world;  // Our 2d world
    for (int r = 0; r < rows; ++r) {
        vector<int> aRow(cols, 17);  // Initialize a vector of 17's
        world.push_back(aRow);
    }

Many students will be happy to use this since it takes less typing. :-)
But there is nothing wrong with writing the loop! Please don't get too hung up on trying to remember library features.

If we are going to take this approach of using the constructor, we could push it a step further. The original outer loop is again just defining a vector whose length is known in which all of the elements will be the same. What will the length be? rows. And what will each element be? A vector. Put it all together and we have:

    int rows = 3;
    int cols = 4;
    vector<vector<int>> world(rows, vector<int>(cols, 17));  // Our 2d world

Please, if you find that version confusing, feel free never to use it! I simply make you aware of it as some students are anxious to know if C++ can be written with some of the apparent power that they enjoyed in Python. Yes, but sometimes the expressions will be a little more complicated.

So we are done. But I still want to go back to the original code. We have a nested loop, with the inner loop building up the same vector, over and over again. We could factor that out.

    int rows = 3;
    int cols = 4;
    vector<int> aRow;
    for (int c = 0; c < cols; ++c) {
        aRow.push_back(17);
    }
    vector<vector<int>> world;
    for (int r = 0; r < rows; ++r) {
        world.push_back(aRow);
    }

So, as I said at the beginning, we haven't introduced anything new here, but perhaps seeing it all together will have been helpful.

Strings

We have already used the type string. There are a few additional things that it is handy to know about them.

Like Vectors

The first is that they can be used as if they were a vector of characters. That means that the methods and operators that we discussed for vectors, all work for strings. So, for example, we could add a character to the end of a string with the push_back method to add a character to the end of a string.

string s = "abc";  // Note the double quotes around a string.
s.push_back('z');  // Note the single quotes around a character.

Note that we are pushing back a character. Character literals are denoted using single quotes, unlike string literals which use double quotes.

We can also access the characters in a string using the square-bracket operator. And if the string hasn't been marked as constant, we can change the value of a character inside it, the same way we would change any of the values inside a vector.

What then will the following display?

string a = "HAL";
for (size_t i = 0; i < a.size(); ++i) {
    a[i] = a[i] + 1;
}
cout << a << endl;

The answer is: IBM

Strings can be compared to each other using the == operator. (We didn't show that with vectors, but yes it works.)

+ and +=

Two string objects can be concatenated together using the + operator, to form a new string. Similarly, we can use += to add one string onto another string (i.e. to append one string to another).

string a = "The";
string b = "Cat";
cout << a + " " + b << endl;
or
string a = "The";
string b = "Cat";
a += " ";
a += b;
cout << a << endl;

Be aware that using the + operator, does not work if we are only using string literals. We have to have an actual string object. (Java programmers may be surprised at this requirement.) If we tried the above as

cout << "The" + " " + "Cat" << endl;  // Does NOT compile!
then the compiler rejects the code.

One neat thing is that we can also add single characters onto a string using + and +=. So, the earlier example of adding the character 'z' to our string could be written as:


string s = "abc";
s += 'z';  // Note the single quotes.

Structs

A common way to group two or more items, possibly of different types, into one "object". Example:

struct Cat {
   string color;
   string name;
   double weight;
};

Don't forget that semi-colon at the end! Leaving it out can make for some very confusing error messages.

Cat has three attributes: color, name and weight. Attributes are also known as fields or member variables. We will use the terms attribute, field and member variable interchangeably.

We can then create a cat, provide values for its various attributes and finally access those values to print them with the following code:

Cat myCat;
myCat.name = "Felix";
myCat.color = "grey";
myCat.weight = 3.14;
cout << myCat.name << '\t' << myCat.color << '\t' << myCat.weight << endl;

Observe the dot-notation used to indicate that we want to access, for example, the weight attribute of myCat.

Could we set some of the values inside the struct definition when we declare the member variables? Can we write the following?

// This struct definition does NOT compile, unless you are using C++11 or later.
struct Cat {
   string color = "black";  // Needs C++11
   string name = "Felix";   // Needs C++11
   double weight = 0;       // Needs C++11
};

In earlier versions of these notes, I had to state that the above did not work because even though it was legal according to the standard, not all implementations had caught up with this feature.

Assigning struct objects.

I can easily assign one cat to another. If we have:

Cat b;

Then we can write

b = myCat;

This is easier to write and has the same effect as:

b.color = myCat.color;
b.name = myCat.name;
b.weight = myCat.weight;

Which is easier to read? Which do you think you should write?

Testing if two objects are equal

Note that I cannot test if one cat is the same as another just by writing:

// COMPILATION  ERROR!!!    (We'll learn how to make it work halfway through the semester.)
if (b == myCat) {/* We want to do something if they are equal*/}

Instead we have to test if all the fields have the same value:

// This works correctly
if (b.color == myCat.color  &&  b.name = myCat.name  &&  b.weight == myCat.weight) {
    /* We want to do something if they are equal*/
}

We will learn later how to make the first way work, when we cover how to write our own function to implement the == operator for Cats. But that's a month or so into the course.

Equal or the Same?

Testing if two objects are "equal" often amounts to asking if the two are the exact same thing. This is done not by testing if the values of the member are the same, but instead by testing if the addresses of the objects are the same. We will see how to do that later on when we get to the discussion of pointers.

Printing an object

Another thing I cannot do, at least not without some additional machinery, is print out the contents of myCat with:

cout << myCat << endl;   // COMPILATION ERROR!!!

The output operator does not know how to print a Cat. As with implementing the == operator, we will cover how to do this in the lesson on operator overloading. For now we will write functions, e.g. printCat that will be passed in a Cat.

Filling a vector of structs

Above we defined a Cat struct. How would we fill a vector of cats from a file holding the cat information? Below is a function that is passed a stream to read from and a vector to fill with Cats. We assume that the file has one cat per line (though that isn't actually required here) and each cat's information is shown as name, color and weight.

void fillCatVector (ifstream& ifs, vector<Cat>& vc) {
   Cat c;
   while (ifs >> c.name >> c.color >> c.weight) {
      vc.push_back(c);
   }
}

Functions

A simple example

One of the most important parts of any computer language is to be able to define functions. We have already defined one function, main, but we won't want to put all of our code there. As always, breaking up a problem into parts makes it easier to read and provides us ways to reuse code.

A typical example of a function is one that computes the factorial.

// compute n!
int factorial(int n) {
   int result = 1;
   for (int i = 2; i <= n; ++i) {
      result *= i;
   }
   return result;
}

The function factorial has an int parameter called n and it returns a value that is also an int. It also has a local variable called result. The variable result is only visible inside the function and only from the point where it is defined. If there is another variable called result in some other function, that result variable will not conflict with this one.

To call the function we pass it an int argument.

// answer will have the value 120
int answer = factorial(5);

Here is a complete program that queries the user for a number and displays the factorial of that number:

#include <iostream>
using namespace std;

// compute n!
int factorial(int n) {
   int result = 1;
   for (int i = 2; i <= n; ++i) {
      result *= i;
   }
   return result;
}

int main() {
   cout << "N? ";
   int value;
   cin >> value;
   int answer = factorial(value);
   cout << value << "! is " << answer << endl;
}

Using Prototypes

Note that we wrote the definition for the factorial function before the definition for main. When the compiler is compiling main and sees the call to factorial, it needs to know that there really is a function called factorial and it is ok ot pass it an int.

Sometimes it is inconvenient, or impossible, to provide the defintion of a function before the function is called. The compiler will still be happy if you just promise that you will define the function. To do that, we provide a prototype for the function. The prototype shows the function's return type, its name and its parameter types. A prototype for our factorial function would be:

int factorial(int n);

Note the semi-colon at the end of the line. It is necessary.

Many people feel that the first function that you see defined in a C++ program should be main. Using the prototype for factorial, we could rewrite the program we showed that asks for a number and displays the number's factorial as:

#include <iostream>
using namespace std;

// compute n!
int factorial(int n);

int main() {
   cout << "N? ";
   int value;
   cin >> value;
   int answer = factorial(value);
   cout << value << "! is " << answer << endl;
}

int factorial(int n) {
   int result = 1;
   for (int i = 2; i <= n; ++i) {
      result *= i;
   }
   return result;
}

Void Return Type

Not all functions return a value. Suppose we were printing out factorials a lot. It might be convenient to write a function to do that. Naturally, it would use our factorial function for the actual computation.

// display n!
void displayFactorial(int n) {
   cout << factorial(n) << endl;
}

In this example, the function has nothing that it needs to return, so the return type is void. If we wanted, we could write the same thing as

// display n!
void displayFactorial(int n) {
   cout << factorial(n) << endl;
   return;
}

Here, we have added an extra line that just says to return. Again, there is nothing to return, so the return statement does not show a value to return. Whenever you have a void return type, your return statement will not have any value to return.

Parameter Passing

pass-by-value

Suppose we want a function that will return a value one more than what gets passed as the argument.

int addOne(int n) {
   int result = n + 1;
   return result;
}

Here we used a local variable called result to hold the result of the computation. The value in the parameter never changes. Similarly, the value that gets passed to the function never changes. Let's consider what happens if we call the function.

int main() {
   int num = 5;
   int answer = addOne(num);
   cout << "num: " << num << "; answer: " << answer << endl;
}

If you try it (remember to provide the necessary include and using statements), you should get the output:

num: 5; answer: 6

What would happen if instead of creating a local variable, we just used the parameter itself to hold the result?

int addOne(int n) {
   n = n + 1;
   return n;
}

What gets printed out if we call this new version of the function? The same thing! What's the point? The point is that number we passed into addOne is copied into the parameter. Nothing we do to the paremeter has any effect on what got passed in. This is called pass-by-value.

pass-by-reference

What if we do want the value to get changed? What if want the function addOne to actually add one to what it gets passed? If we want the output our program about to change to:

num: 6; answer: 6

then we need to change the way our function is written. Here is the modified version:

int addOneByReference(int& n) {
   n = n + 1;
   return n;
}

What changed? Just one little thing. There is an ampersand, also called an and-sign, right after the type of the parameter. Now the value that gets passed in does not get copied. The parameter name now acts as a kind of "alias" for the argument that was passed in. Any changes made to the parameter also effect the argument that was passed. This is known as pass-by-reference.

One caveat regarding pass-by-reference. Using our new function, addOneByReference, we cannot pass a "literal", such as 17:

addOneByReference(17);  // Won't compile!!  17 cannot be passed to a reference parameter.

Pass-by-reference requires the argument to be a thing that has an actual address somewhere in memory, something whose value we can modify. Similarly, it won't work if we are trying to pass the result of some computation:

int x = 3, y = 4;
addOneByReference(x+y);  // Won't compile!!  x+y cannot be passed to a reference parameter.

In this case, the result of x+y is "temporary", it doesn't exist past the expression it is in, and so C++ won't allow you to make a reference to it.

These problems go away if we use a constant reference, as described below.

pass-by-constant-reference

Let's consider one last variation on the theme of passing arguments. Suppose we write a trivial function:

void displayString(string aString) {  // BAD code!!!
   cout << aString << endl;
}

The comment above says that this is a bad idea. Why? What happend when we call the function? Whatever we are passing in gets copied into the variable aString. That takes time and space. If we pass in a large string, or call the function very often, we might be surprised how much this can impact the performance of our program. What can we do? We could try pass-by-reference as we did before.

void displayString(string& aString) {  // an improvement in performance, but still not good.
   cout << aString << endl;
}

This has a couple of problems. The main problem is that if we had a bug in this function that happened to change what is in aString, then the change would also effect whatever was passed in. And that change could effect other parts of the program. We want to keep problems localized whenever possible, and preferably keep them from ever occuring. The right solution is:

void displayString(const string& aString) {  // The right way to do it!
   cout << aString << endl;
}

This is called pass-by-constant-reference. It avoids the copy and prevents any changes from being made to aString.

Passing Streams

One group of parameter types that need to be treated specialy is the stream classes. Whether you are talking about the standard streams (i.e. cin, cout and cerr) or you are talking about a file stream, the all must be passed by reference.

Why? The point is we never want to make a copy of a stream. We don't want to have two different stream "objects" referring to a single real underlying stream. A stream has a number of attributes that describe the stream's current status. For example, is it still ok to read from? What is the next position in the stream that we will read from or write to? Every time you use a stream, those attributes can change. But if we have to different stream objects referring to the same underlying stream then they can end up disagreeing with each other. And that's not good.

To prevent such confusion, we always pass streams by reference. That way even when we have passed a stream to a function, the parameter name in the function is really referring to the same stream object as the calling function was using.

Oh, and you will find that you don't have any choice in this area. If you try to pass a stream by value, or try to make a copy of a stream in any other way, your program will not compile. The folks who wrote the stream classes made sure of that. Later on, when we discuss copy control, we will see how they did it.

What about making your stream a const parameter? Not likely to be a good idea. After all, think about what a stream object holds. Among other things, it has flags as to whether it is still readable or if we have hit the end of the file. And it has some sort of position marker that keeps track of where the next read will come from or where the next write will go to. All these things need to be free to be modified when we use the stream, so passing a stream by constant reference is not good.

Good Programming

It's a little scary to start a section on "good programming", especially in an introductory guide to C++. There's a lot to say under this heading, but I want to keep it short and to the point. First off, you should probably check my C++ Coding Guidelines for simple things like naming conventions and the like.

There are a few other things that you should pay careful attention to as you start your C++ programming career.

Globals

Global variables are variables that are defined outside of any function or type definition. Every variable that we have defined was either local to a function or was a member of a struct. My "advice" on using globals at this stage?

Don't.

Overuse of globals is generally viewed as a sign of sloppy code. What's the problem with them? Any piece of code, anywhere in the program can access and even modify their contents. It makes debugging much, much harder. And one of the advantages of using a language like C++ is to make debugging easier.

For this course, you will never use global variables. This is to get you thinking the right way. Later on, you will make up your own mind as to when a global is an improvement to your program. Hopefully by then you will recognize the danger of globals and use them sparingly.

Global constants, on the other hand, are ok, in fact we encourage you to use them! Here is an example:

const int THE_ANSWER = 42;

Scope

Define your variables using the smallest scope possible. This is in a sense an extension of what I said about globals. You want to define a variable so that ideally it can only be used where it makes sense. We saw an example of this earlier when I talked about the for loop and how you can limit the scope of the loop's index to just the for loop itself. That's a good thing.

In general, define a variable as close to the place where it is first used as possible. In some languages, like earlier versions of C, you have to define all local variables at the beginning of a function. That's not true in C++. In fact, it is considered poor programming style in C++ to define them all at the beginning of the function. (Old time C programmers will tend to disagree with me on this.)

So, if a variable is only going to be used inside a block of code (i.e. within a pair of curly-braces) such as in an if statement or a loop, then put the definition for that variable inside that block.

Other Stuff

There is so much one can say about C++ that it is hard to know when to stop. The material covered above is the crucial stuff for you to know in order to get started. Below, I will add things that I want to mention because they might make your life easier, but that you can, at the same time, easily get by without for the moment.

Overloading Functions

C++ allows you to write two different functions that have the same name. This is known as function overloading. The requirement is that the functions have different parameter lists. They can differ either in the number of parameters or in their types.

So, if we wanted to write a couple of [fairly useless] functions, one to display an int and another to display a string, we could either give them different names, as in:

void displayInt(int n) { cout << n << endl; }
void displayString(const string& s) { cout << s << endl; }
or we could just pick one function name for both:
void display(int n) { cout << n << endl; }
void display(const string& s) { cout << s << endl; }

Many languages support this feature nowadays. But some widely used languages, such as C, do not.

Note that in the programming literature there are those who argue against overloading functions, claiming that it can make the code more difficult to read. You should make sure that the overloaded functions mean the same thing to avoid confusion.

Home


Maintained by John Sterling (john.sterling@nyu.edu).