Syllabus info

What class is this? CSci 133 (123 is a prereq.)

Who am I? Andrew Clifton, “Andy” or “Mr. Clifton” or “Prof. Clifton” are all fine.

What is this class about? If 123 gave you the tools to get started, 133 is about the tools themselves: how do they work, how were they built, how can we build new ones ourselves? So some of the things that you just used in 123, we’re actually going to build from scratch ourselves, to help understand how they work.

Grading

This course uses specifications grading a point-less (ha!) grading system where your final grade is based on proving to me that you’ve mastered the material in the course. Fortunately for you, this is fairly simple: this class is divided into 8 “modules”. Each module has an assignment and a section on the midterms. To pass an assignment, you must pass its assignment and section. (Note that later midterms include all the sections from previous ones, so if you fail a section on a midterm, you can just try again on the next one. There is no penalty for failing a section as long as you later pass it.) There are three midterms and the final, but the final is basically just the fourth midterm, so each test effectively adds the two most recent modules.

Depending on how things turn out, I might apply an adjustment at the end of the semester to keep things fair (e.g., drop the requirement for an A to 7/8 if no one gets all 8) but you shouldn’t count on that.

I’ll be posting your grades to Canvas, but it will basically just be 0/1 for each module. 1 indicating that you’ve passed it at some point.

Assignments are graded pass/fail, but you can resubmit failed assignments as many times as you like. Because each assignment includes automated tests telling you whether or not it passes, there’s really no point in submitting an assignment that you know will fail.

I probably shouldn’t point this out, but note that if you’re only aiming for a C, you could technically stop coming to class after you’ve completed your 5 modules and your grade won’t suffer at all.

The Personal Software Process

The PSP is a software development “exercise” (think pushups) to help you get some sick gainz in your software development skills. Essentially, it requires you to make estimates, before you begin coding, as to how many lines of code, how much time, how many bugs, etc. you think you will produce. Then, while you are programming you keep detailed records for all those categories. When you’re done, you compare your estimates with your real-world performance.

I wouldn’t suggest doing the PSP on the first assignment, as you’re likely to mess something up and have to resubmit. The upside is that the PSP forces you to pay attention to details, so you’re more likely to catch your own mistakes.

Note that the PSP is something you have to do at the same time as the assignment. You can’t do the assignment and then fill it out afterwards; you’ll get bad data because you’ll have forgotten about all the little bugs you fixed, and how much time you spent fixing them.

Logging into the server

What you need:

On Mac/Linux:

ssh username@fccsci.fullcoll.edu -p 5150

Replace username with your username. It will prompt you for your password.

On PuTTY, fill in the host and port and then click Connect. It will prompt you for your username and password. You can also “save” your connection details from the connect screen so you don’t have to type them in every time.

Moving around

Editing and Compiling

There are four editors on the server: Micro, Nano, Emacs, and VIM. All have their advantages and disadvantages:

Compiling using GCC:

g++ -c source1.cpp
g++ -c source2.cpp
...
g++ -o program source1.o source2.o ...

or for short

g++ -o program source1.cpp source2.cpp ...

You can list multiple source/object files in the second form.

But I’ve also built a shortcut for you:

compile source1.cpp source2.cpp ...

This will compile-and-link all the listed source files, creating an executable named source1. (I.e., named after the first source file.) Even better, compile handles figuring out the right order in which to list source files! (Remember that if you use G++ you have to list source files in a particular order, based one which files use which other files.)

If you want to compile-and-link-and-run you can do

compile source1.cpp source2.cpp ... && ./source1

The program will only run if the compile was successful.

Using compile also has the advantage that it will colorize the error messages printed by G++.

You don’t have to do your development on the server; if you want to work in Visual Studio or whatever on your own machine, that’s fine. However, the server is where I will collect your submissions, and compile and test your code, so you must make sure that your source code is on the server before the due date, and that it compiles and runs on the server. I don’t accept email submissions, and “it works on my machine” isn’t good enough. (If you write correct standard C++ it will work everywhere; a program that works with one compiler and not with another is a program that has something wrong with it.)

You should also make sure that whatever compiler/IDE you use understands C++11. You might have to change an option to enable C++11 mode, otherwise you’ll get error messages when you try to write code using the ranged-for loops or nullptr.

To move files to/from the server you can use FileZilla (on Windows/Linux/Mac) or WinSCP (on Windows), or SCP from the command-line (Linux/Mac). You still need all the above information. To use SCP from the command-line you can do

scp -P 5150 path/localfile.cpp username@fccsci.fullcoll.edu:path/remotefile.cpp

to copy the local (on your computer) file path/localfile.cpp into the remote (on the server) file path/remotefile.cpp. Just as on the server, you can use ~ in either the local or remote path to refer to either your local or remote home directory.

To use FileZilla or WinSCP, you need the above information (host, port, username, password); for FileZilla set the protocol to “SFTP”. Once you connect, you should be able to drag-and-drop files between your computer and the server.

I’ve heard that you can install FUSE for Mac OS and MacFusion which will give you the ability to permanently “mount” the server like a folder in Finder, but you’re on your own if you want to do that. (On Linux it’s possible to use sshfs to mount the server as a directory.)

Finally, you can run Emacs on your personal computer and use TRAMP to both save/load files on the server and even to do your compilation on the server.

Review Example 1: The bag data structure

Although most of you just had 123 last semester, I thought it would be good to review C++, just to make sure we’re all on the same page, especially since not all of you had 123 from me.

We’re going to begin our review by building a basic data structure called a “bag”. The idea of a bag is its something that supports the following operations:

An example of using a bag might look something like this:

bag b(10); // Capacity (max. size) = 10

b.insert(1);
b.insert(2);
b.insert(3);

cout << b.size() << endl;  // Prints 3
cout << b.empty() << endl; // Prints 0 (false)
cout << b.full() << endl;  // Prints 0 (false)

// This should print 1,2,3, but not necessarily in that order.
for(int i = 0; i < b.size(); ++i)
  cout << b.at(i) << endl;

b.remove(2);

cout << b.size() << endl; // Prints 2

// This should print 1,3, but not necessarily in that order.
for(int i = 0; i < b.size(); ++i)
  cout << b.at(i) << endl;

Because we can construct multiple bags, and because bags are required to be independent, we will pretty must have to implement bags as a class. The space used by the bag will have to be stored as a data member inside the class, so that each instance of the class gets its own space for its own elements.

class bag {
  public:

  private:

};

We could use a std::vector to store the contents of each bag, but I’m going to go a bit lower level and use a pointer to dynamically-allocated (i.e., heap) memory. We will have to manage this memory using new and delete ourselves. Since bags store ints, each bag will have an int* member, pointing to the space it is using:

class bag {
  public:

  private:
    int* data;
};

(If you remember 123, this means we will need to implement a destructor, copy constructor, and copy-assignment operator in order for bag to function correctly.)

To consider how to create this storage (in the constructor), we have to think about how we will use it:

Those are the only two operations that change the size of a bag (clear can be thought of as just while(!b.empty()) b.remove(0);). There are two basic ways we can implement these on top of a dynamically-allocated array:

The second approach is probably a better fit. (It’s also the method that the first assignment requires you to use.)

How do we insert a new element (assuming the bag is not full)? Because insert is not required to place the element in any particular position, the easiest place to put it is at the end:

void insert(int e) 
{
  if(!full()) {
    data[sz] = e;
    ++sz;
  }
}

This could be shortened to just data[sz++] = e; if you’re feeling clever.

How do we remove an element? If the element is at the end, it’s easy to “remove”: just decrement size and its effectively gone (it’s still there in memory, of course, but it will be overwritten by a later insert). What if the element is not at the end? Again, remove is not required to preserve the order of the other elements, so we can just swap the to-be-removed element with the last element, reducing the case of remove(i) to remove(size()-1):

void remove(int i)
{
  if(i >= 0 && i < size()) {
    std::swap(data[i], data[size() - 1]);
    --sz;
  }
}

Those are the most complex methods. The rest of the class looks like:

class bag {
  public:
    bag(int c)
    {
      cap = c;
      sz = 0;
      data = new int[cap];
    }

    int size()   { return sz; }
    bool empty() { return size() == 0; }
    bool full()  { return size() == cap; }

    int at(int i) 
    { 
      if(i >= 0 && i < size())
        return data[i];
      else
        throw std::out_of_range("at(i) out of range!");
    }

    void insert(int e)
    {
      if(!full)
        data[size++] = e;
    }

    void remove(int i)
    {
      if(!empty()) {
        std::swap(data[i], data[size()-1]);
        --sz;
      }
    }

    void clear()
    {
      sz = 0;
    }

  private:
    int cap, sz;
    int* data;
};

This implementation will work, however it has two problems:

To fix this, we must add the “big three”: destructor, copy constructor, and overloaded assignment operator:

// Destructor
~bag()
{
  delete[] data;
}

// Copy constructor: 
//   bag b2 = b1;
bag(const bag& other)
{
  sz = other.sz;
  cap = other.cap;
  data = new int[cap];

  // Copy bag contents:
  for(int i = 0; i < sz; ++i)
    data[i] = other.data[i];
}

// Overloaded assignment operator: 
//   b2 = b1;
bag& operator= (const bag& rhs)
{
  // "Copy and swap";
  bag copy = rhs; 

  // Swap *this with copy
  std::swap(sz, copy.sz);
  std::swap(cap, copy.cap);
  std::swap(data, copy.data);

  return *this;
}

The easiest way to write the overloaded assignment operator is to combine the steps of the destructor and copy constructor:

However, this has two problems: one, it involves writing the same code twice, and two, if anything goes wrong, notice that the bag is left in a messed-up state. If something happens between delete and new, we have a bag whose data points to already-deleted memory. The “copy-and-swap” idiom reverses these steps, and reuses the code we’ve already written in the copy constructor and destructor: it first makes a copy, and then swaps the copy with *this. (When the copy goes out of scope, it will be destroyed, but it contains our original data.)

The disadvantage to the copy-and-swap idiom is that it requires twice as much memory, because both the copy and the original must exist together, for a short period of time. This is the only way to get true safety, unfortunately, so that if anything goes wrong, we still have a valid bag.

C++11 coolness

With C++11 there are a couple of additional constructors/overloads we can add, bringing the big-three up to the big-five. Unlike the copy constructor and overloaded assignment operator, these are not required for correct operation: your code will work just fine without them. These make your programs faster in certain circumstances.

To see why these are needed, let’s add a method to bag remove_duplicates which returns a new bag, the same as the existing one except that there’s only one copy of each unique element:

bag remove_duplicates()
{
  bag b(cap);

  for(int i = 0; i < size(); ++i) {

    bool found = false;
    for(int j = 0; j < b.size(); ++j)
      if(at(i) == b.at(j)) {
        found = true;
        break;
      }

    if(!found)
      b.insert(at(i));
  }

  return b;
}

Consider what happens if we do something like this:

bag b1(10);
// Insert stuff into b1

bag b2 = b1.remove_duplicates();

In the last line, the following steps are performed:

It seems a little wasteful to create a bag, copy from it, and then immediately destroy it. Why not just steal from the temporary, and then tell the temporary somehow that it doesn’t need to bother delete-ing data in its destructor. This way, the new bag built by remove_duplicates is effectively constructed in-place, inside of b2, instead of being built separately and then copied.

This process is referred to as a move, and it can only occur in particular circumstances: We can only move from a temporary object, and object that will soon be destroyed. This is because moves are implemented as “stealing resources”; if we stole resources from a long-lived object, it would cause all kinds of problems. Stealing from a temporary is fine, though, because they were going to die soon anyway.

The move constructor looks like this:

bag(bag&& other)
{
  // Steal from other
  data = other.data;
  sz = other.sz;
  cap = other.cap;

  // Make sure other doesn't destroy *our* data
  other.data = nullptr;
}

bag&& is a special kind of reference called an rvalue-reference. “Rvalue” is the technical name for “temporary object”. bag&& can only bind to temporary objects, never to “real” bags. (Also note that unlike the normal copy constructor, we do not pass other as const, because we intend to modify other, by stealing from it.) At the end of the move-constructor, we set other.data = nullptr, so that when other is destroyed, it won’t do anything (delete[] nullptr always does nothing).

The move assignment operator is similar, except that we destroy our own data first:

bag& operator= (bag&& rhs)
{
  delete[] data;

  data = rhs.data;
  sz = other.sz;
  cap = other.cap;

  other.data = nullptr;

  return *this;
}

Additional operations

There are a few additional operations we can add to our bag to make it more useful:

find looks like this:

int find(int x)
{
  for(int i = 0; i < size(); ++i)
    if(at(i) == x)
      return i;

  return -1; // Not found
}

while count looks like this:

int count(int x)
{
  int c = 0;
  for(int i = 0; i < size(); ++i)
    if(at(i) == x)
      ++c;

  return c;
}

(You might notice that I use the size() method instead of sz and the at method instead of accessing the array directly. This is to help “future proof” my class. If I later decide I want to change how the class is implemented, I only have to update size, at insert and remove; find and count don’t need to change.)

Given these two, we have a choice as to how to write exists:

bool exists(int x) { return find(x) != -1; }
bool exists(int x) { return count(x) > 0;  }

Which is better? This brings us to the question of algorithmic efficiency. Both find and count contain a loop up to size(), so they could both potentially have to scan through the entire array. However, there is a crucial difference: find, because it is only looking for the first copy, and not all copies, can exit the loop early. It returns as soon as it finds its target, which means that, on average, find will be faster than count. Hence, we should write exists in terms of find.

Const-correctness

C++ allows us to declare variables as const, indicating that we are not allowed to modify them:

const int x = 12;
++x; // ERROR!

A const bag is not particularly useful, because we can’t insert anything into it, but still, for completeness, we will make the bag class const-correct. To do so, we look at each method and ask whether or not it modifies the data members of the bag. If it does not, we label it as const, like this:

bool exists(int x) const
{
  return find(x) != -1;
}

Note that a const method cannot call a non-const method, so this forces find to be const also. In the end, only insert and remove need to be non-const.

We can create a const reference to a non-const object, so we can do things like this:

bag b;
b.insert(1);
b.insert(2);
b.insert(3);

const bag& br = b;
cout << br.at(0) << endl; // Fine
br.insert(12);            // ERROR

Template classes

Currently, our bag only stores ints. We can make it more flexible, so that it can store any kind of value, by making it into a template class:

template<typename T>
class bag {
  ...
};

Within the class definition, wherever we used int for the type of the elements of the elements of the bag, we replace it with T.

template<typename T>
class bag {
  public:
    bag(int c) ...

    void insert(T x) ...

  private:
    T* data;
    int sz, cap;
};

Note that we do not replace every int: some ints represent the size or capacity of the bag, or indexes within the bag. Only those that represent values within the bag become Ts.

After making this change, we can build a bag of strings via

bag<string> bs; 
bs.insert("Hello");

Review Example 2: Text processing

Suppose we want to read a line of input from the user, split it into an array of “words”, and then filter out certain “noise” words.

We need to do several things:

Representing strings of text: some of you may have come from a class where you used char* as strings. Here we’re going to use the built-in string class which comes with “batteries included” and doesn’t require you to worry about allocation or length or anything:

#include <string>
using std::string;
...
string line;
getline(cin, line); // Read a line of text from cin into `line`

Now we need to split the line into words, separated by spaces.

How are we going to store our list of words? We could use an array, but then we’d have to decide on a maximum size, and keep track of how many words we had actually read in. All of our current commands have relatively few words, so this isn’t too bad, but we’d prefer something simpler. We’re going to use the built-in vector class.

#include <vector>
using std::vector;

A vector is like an array: you can access elements at a specific numeric index via

v.at(n)
v[n]

The first is “safe” in that if \(n \lt 0\) or \(n \gt \) the size of the vector it will throw an exception. The second is unsafe (it doesn’t do any “bounds-checking”) but it’s just as fast as a plain array access.

The nice thing about vectors is that we can add new elements onto the end, without having to worry about how big the vector is; the vector will resize itself when necessary:

vector<string> words;
words.push_back("word"); // Add another word

Vectors should normally be preferred to “raw” arrays in almost every situation (the exception is when you are implementing low-level data structures; e.g., when we sit down to write our own vector class, you’ll have to use arrays to build it). Vectors are just as fast as arrays, and safer and more flexible to boot.

To accomplish the splitting, we have two methods we can use:

  1. We can write a loop to do it ourselves. We’ll have to keep track of whether we are “inside” a word, and if so, collect characters into a string. When we transition from non-word to word, we have to start a new word, and when we go from word to non-word, we have to add the word to the list of words.

    Note that with a string, we don’t need to pick a “maximum size”; we can add a new character to the end by just doing

     s.append(c);
    

    and the string will be expanded to hold it.

    To use this method, we need to figure out all the things that can “happen” while reading the next character: it can be a word character (i.e., not a space) or a space character. There’s a third option, however, that is easy to overlook: the next character might not exist, if we’ve reached the end of the string. So we have two “states” and three possible transitions for each state, which leads us to a maximum of six cases we need to consider:

    In word?\next charSpaceWordEnd-of-string
    WORDFinish wordContinue wordFinish word, end
    SPACEIgnoreStart wordEnd

    “Finish word” means to add the word that is being constructed to the vector of words, clear the current word, and set the curren state to SPACE. “Start word” means to add the current character to the word (which should be initially empty, because we cleared it in “Finish”), and change the state to WORD. “Continue word” means to add the character to the end of the current word. “End” means we’re done; return the vector of words. “Ignore” means we basically don’t do anything with the current character, just continue to the next one.

    We can also draw a graphical representation of this as a state machine diagram (sometimes called a statechart diagram). We’ll see an example of this later.

    The one last thing we have to think about is what state to start in. If we look at the states, if we start in WORD and the first character is a space, then we will try to “finish” a word that hasn’t been started yet! On the other hand, if we start in SPACE and see a space, it’s ignored; if we see a non-space, we’ll start a new word. Both of those are fine, so our “start state” should be SPACE.

  2. We can let the standard library do the work for us, and use a stringstream. This is basically an input stream, like cin, except that it gets its input from an existing string, rather than from the user. This is useful to us because if we do

     string w;
     cin >> w;
    

    then w will contain the next word (it will skip over any leading spaces, and stop reading when it encounters a space at the end).

Using a vector to hold the words, and a stringstream to get them we have something like this:

// same comment
vector<string> split_words(string input) {
    vector<string> words; 
    string current_word;  

    std::stringstream in(input);

    while(in >> current_word) 
        words.push_back(current_word);

    return words;
}

(Note that a stringstream can also be used for output, like cout, so that anything you write to it gets “printed” into the string. That’s not useful for what we’re doing here, but you might find it useful in other problems.)

One thing you should notice is that I have no problems with you using the standard library. It’s there to make your life easier, so why not use it? (The exception is, of course, if I ask to you to rewrite something provided by the library; then I expect you to actually write it yourself.) When possible, you should prefer high-level methods (which is what the library provides) to lower-level ones (like doing everything yourself).

Coding standards

Some things to note about the coding standard that I use:

Other C++ features

Templates

I won’t expect you to write templates, but I may occasionally use them in class or ask questions involving them on tests, so you should at least know what a template is and how they work.

Suppose we want to write a function that checks whether a vector contains a particular element. If we know that we have a vector of ints we could write this:

bool has_element(vector<int>& data, int target) {
    for(int e : data)
        if(e == target)
            return true;

    return false;
}

Thanks to function overloading we could write versions (with the same name) for vector<string>, etc., but every single version would be identical, except for the element type. What we really want is a way for C++ to generate the various versions for different types, as they are needed. This is what templates give us: a way to leave out the types and have C++ fill them in later, when we use the function:

template<typename T>
bool has_element(vector<T>& data, T target) {
    for(T e : data)
        if(e == target)
            return true;

    return false;
}

The leading template<typename T> says that this is not a normal function definition but a template definition. A template definition doesn’t actually define the function, the function itself is defined only when we use the template function, providing an actual type for T:

vector<string> names;
if(has_element(names, "John"))
    ...

Here, I’ve let C++ figure out that T should be string. I could also tell it explicitly:

has_element<string>(names, "John")

Either way, the compiler basically makes a copy of the template definition, replacing T with string, and the proceeds from there.

We can do the same thing with a class:

template<typename T>
class myvector {
  public:
    ...
    T at(int index);

  private:
    T* data;
};

Once again, when we tell C++ what type to use (via myvector<int>) it will make a copy, replacing T with int and use that.

All you really need to remember is that, inside a template definition, T (or whatever type variable(s) are declared in the template header) can be treated just like any other type (int, string, etc.), with the exception that we don’t know anything about it yet.

To solve the problem above (finding the smallest element in either a checked_array or a array_view), we can write a function like this:

template<typename T>
int smallest(T array) {
  int s = array.at(0);

  for(int i = 0; i < array.size(); ++i)
    if(array.at(i) < s)
      s = array.at(i);

  return s;
}

Note that this function will work for any type T which has methods .at and .size. In fact, it will even work for the vector<int> type!

Inheritance

The other way to make the two classes compatible is inheritance. We can make both classes inherit from a common abstract base class, which specifies what methods they have. The abstract base class looks like this:

class array_base {
  public:
    virtual int& at(int i) = 0;
    virtual int size() = 0;
};

This definition says that any class which inherits from array_base must supply methods .at and .size in order to be complete. We can then make both our classes inherit from this base class:

class checked_array : public array_base { ... };
class array_view    : public array_base { ... };

To make use of this base class, we write a function which takes an array_base, either by reference or as a pointer:

int smallest(array_base& array) {
  int s = array.at(0);

  for(int i = 0; i < array.size(); ++i)
    if(array.at(i) < s)
      s = array.at(i);

  return s;  
}

This version of the function will only work with subclasses of array_base. It won’t work with unrelated classes like vector<int>.

The advantage to using templates is that the resulting executable code is customized to the class used: it’s as fast as if you’d written the function specifically to work with array_view or checked_array or vector<int>. The disadvantage is that it has to generate a completely different version of the function for each type you use it on, so the size of your program can get bigger.

The advantage to inheritance is that there’s only one version of the function, so your program is smaller. The disadvantage is that it has to “figure out” what type of array it’s dealing with while the program is running, which slows it down a bit.

Implementing string splitting using strings and vectors

The string class

#include <string>
using std::string;

string s = "Andy";

If you declare a string variable without initializing it, it defaults to the empty string:

string e; // e is the empty string

You can initialize a string object from a char* literal. The basic string operations are:

Operation Description
s.at(i), s[i] Access individual characters by index (char)
s.front(), s.back() Access first/last characters
s.length(), s.size() Length of string
s.empty() True if length is 0
s.substr(i) Extract substring starting at i, to the end
s.substr(i,l) Extract substring, starting at i, of length l
s1 + s2 Concatenate two strings to form a new one
s1 < s2, etc. Compare strings alphabetically
s.c_str() Get a char* representation of a string
s.find(c), s.find(s2) Get position of the first occurrence of char c or string s2 in s
s.rfind(c), s.rfind(s2) Get position of last occurrence


s.push_back(c); Add a character c to the end of the string
s.pop_back(); Delete the last character of the string
s.clear(); Delete all characters from string (len. = 0)
s1 += s2;, s1.append(s2); Append s2 to the end of s1, modifying s1
s1.insert(s2,i); Insert s2 into s1, before position i
s.erase(i,l); Erase characters starting at i, of length l
s1.replace(s2,i,l); Replace l characters of s1, starting at i, with s2
s1 = s2; Copy all of s2 into s1

find and rfind will return the special value string::npos if the thing you are searching for cannot be found at all.

Note that strings do not have a nul character at the end! This means that the length of a string is strictly the number of characters in it, and the last character (s.at(s.length() - 1)) is not necessarily nul. (In fact, strings can have nul characters anywhere inside them!)

To read a single word from cin, you can use

string w;
cin >> w; 

This reads a word because the normal behavior of >> is to stop at the first space character it sees.

To read an entire line, use

string l;
getline(cin, l);

You can, of course, print strings normally:

string name = "Andy";
cout << "Hello, " << name << endl;

For our purpose, we’re going to read in an entire line (using getline) and then process it one character at a time. We can do the latter using a loop:

for(unsigned i = 0; i < s.length(); ++i)
  // Use s[i] ...

Or we can use the fancy ranged-for-loop:

for(char c : s)
  // Use c ...

Vectors

You can think of a vector as a version of a string that stores any kind of element, not just characters. The operations supported by vectors are somewhat more limited than those supported by strings, just because vectors cannot make any assumptions about what kind of data they are holding.

vector<int>    vi;                                     // empty vector of ints
vector<char>   vc1(10);                                // Vector of 10 chars
vector<char>   vc2(10, 'x');                           // Vector of 10 'x' chars
vector<string> names = {"Bruce", "Richard", "Alfred"}; // vector of three strings

Vectors manage their own storage, so you don’t need to delete them.

To access the elements of a vector by index, use either .at(i) or [i]:

The shortcut methods v.front() and v.back() provide easy access to the first and last elements. These methods are “checked”: if the vector is empty, they will throw an exception.

Other vector operations:

Operation Description
v.size() Size of the vector (number of elements)
v.empty() True if size is 0
v.clear() Reset size to 0, deleting all elements
v1 == v2 True if v1 and v2 are identical
v.push_back(e); Add a new element to the end of the vector
v.pop_back(); Erase the last element of the vector
v1 = v2; Copy all elements from v2 into v1

Note that unlike arrays, vectors support copying, via assignment. This means that you can pass vectors as parameters to functions, and return from functions as well. This makes vectors much easier to work with than arrays, especially since you don’t need to worry about dynamically allocating them. The fact that vectors can grow, via push_back makes them even more useful; e.g., if you want to read some number of ints in from the user, and you don’t know how many, you can just do

vector<int> data;
int x;
while(cin >> x)
  data.push_back(x);

It’s possible to insert/erase elements in the middle of a vector, but doing so requires the use of an iterator. Here’s an example to get you started:

vector<int> vs = {1, 2, 3, 4};
vs.insert(vs.begin() + 2, 100); // vs = { 1, 2, 100, 3, 4 }
vs.erase(vs.begin() + 1);       // vs = { 1, 100, 3, 4 }

You can also insert the contents of one vector in the middle of another.

Splitting strings

Our table for states/inputs looks like this:

Letter Space
WORD Add to cur. word
Stay in WORD
Finish word
Switch to SPACE
SPACE Start new word
Switch to WORD
Ignore
Stay in SPACE

Implementing this as a function, it will take a string as its input, and return a vector<string> as its output:

vector<string> split(string input) {
  string word;            // Current word
  vector<string> output;  // List of words

  const int SPACE = 0;
  const int WORD = 1;
  int state = SPACE;

  for(char c : input) {

  }

  return output;
}

All of our work needs to be done inside the for loop. The current character is c, and the current state is state. Since we have four table entries, we’ll have an if-else for each of the four possibilities:

  for(char c : input) {
    if(state == WORD && c != ' ') {

    }
    else if(state == WORD && c == ' ') {

    }
    else if(state == SPACE && c != ' ') {

    }
    else { // state == SPACE && c == ' '

    }
  }

There’s one last thing we have to think about, and that’s what happens at the end of the input string:

Trace through this on the input string "get the rock". Run example.

References and pointers

A reference is just another name for an existing variable or location:

int  x = 1; // x is a variable, OK
int& y = x; // y is another name for x

Both x and y refer to the same thing. It’s impossible to distinguish them, because they are just different names for the same object. Any changes to x will be reflected in y, and vice versa.

Because a reference is an “alias” for something, you must initialize a reference variable with another variable/location (or another reference):

int& z = 1; // ERROR
int& q;     // ERROR

(The C++ name for things that you can get a reference to is “lvalue”, as in, values that can be on the left side of an assignment. Temporary objects are rvalues.)

You can, however, get a reference to an element of an array or vector (or string):

int         arr[] = {1, 2, 3};
vector<int> vec   = {5, 6, 7};

int& y = arr[1]; // OK, another name for arr[1]
int& z = vec[2]; // Also OK

With vectors, this can be a little dangerous, because the size of a vector can change, and thus there’s the possibility that the thing the reference is referring to might disappear. But as long as you are careful, everything is OK.

Functions can take references as parameters, in which case the formal parameter becomes “another name” for whatever argument is used when the function is called. Similarly, functions can return references (as long as the thing referred to still exists after the function exits!)

References can cause some weird behavior: Look at this function; can you see any way in which it might print out a 2, instead of a 1?

void f(int& a, int& b) {
  a = 1;
  b = 2;
  cout << a << endl;
}

References have some limitations:

Pointers

Pointers remove these limitations, but add some complexity in doing so. A pointer is like a reference that can change what it refers to. However, the simplicity of references comes from the fact that they cannot change; hence, there are no operations on the reference. Anything you do to a reference variable is transparently done to the the on the other end of the reference. Because pointers can change, this means that we now need two different syntaxes:

In other words, while references have no “identity”, the don’t exist on their own and are just aliases for other things, pointers are objects in their own right, and thus we need some way of manipulating them, as opposed to the thing they point to. All of the complexity of pointers springs from this duality: we always have to be clear what we are doing, are we manipulating the pointer or the object it points to?

Semantically, pointers work by storing addresses. Every (lvalue) object in our program exists somewhere in the computer’s memory; every location in the computer’s memory has an address, a number. A pointer stores an address of another thing. “Dereferencing” the pointer means going to the address it contains.

Pointer syntax:

Type Description
T* “Pointer to T”
T& “Reference to T”
Expression Description
*p Get object pointed to by p (look at the addr. in p)
&v Get pointer to object v (get the addr. of v)

The connection between the two is

Thus, for expressions, * and & are kind of like opposites. & adds a layer of “pointer to-ness” while * removes a layer.

Although pointers are often introduced with dynamic memory, you don’t actually have to do that. Just like with a reference, you can get a pointer to any lvalue:

int  x = 1;
int* y = &x; // y points to x

x++;         // Increments x
(*y)++;      // Also increments x

(Note that ++ has higher precedence than *, so the parentheses are necessary!)

As with references, you can get a pointer to an element of an array, vector, or string:

int         arr[] = {1, 2, 3};
vector<int> vec   = {2, 3, 4};
string      str   = "Hello";

int*  ap = &arr[1]; 
int*  vp = &vec.front(); // Same as vec.at(0)
char* cp = &str[3];

Once again, with strings and vectors, because elements can be removed, you have to be careful to make sure that the pointers still point to something that exists! A pointer that points to a non-existent object is called dangling and using it results in undefined behavior!

If we do ap == vp will this be true or false? What are we asking? We are asking if ap and vp point to the same thing. We are not asking anything about the values pointed to by ap and vp. Similarly, if I do, ap = vp; what happens? Do any of the values in the array or vector change? No: only the pointer ap changes what it is pointing to. The most important thing about pointers is to get yourself clear on when we are talking about the pointers themselves, and when we are talking about the things pointed to. If you have some pointer variables, and there aren’t any *s in the expression, then you are talking about the pointers. If there are *s then you are probably talking about the objects pointed to.

Pointers to array/vector elements

If we have a pointer into an array, vector, or string, we can do some interesting things with it:

vector<int> vec = {1, 2, 3, 1};
int* p1 = &vec[0];
int* p2 = &vec[3];

Consider the following:

 p1 == p2     // True or false?
*p1 == *p2    // True or false?
 p1 <  p2     // True or false?

p1 + 3        // What does this mean?
*(p1 + 3)     // What about this?
p1[3]         // This?
p1+3 == p2    // True or false?
p1++;         // What happens if we do this?

Pointers into an array can have arithmetic done on them (adding/subtracting integers) and it will cause them to move around within the array. Adding 1 moves the pointer one element forward within the array. Comparisons between pointers effectively compare the the indexes within the array that they point to. Note that none of the above operations change the elements in the array!

Pointers to structure and class instances

There are a few extra things we can do with pointers if we have a struct or a class. Here’s an example struct:

struct thing {
  int a;
  char* b;
  string s;
};
char c = '?';
thing t1 = {1, &c, "Hello"};
thing* tp = &t1;

How do I refer to each of the members of t2?

(*tp).a     // == 1
(*tp).b     // == pointer to c
(*tp).c     // == "Hello"

This is a bit cumbersome to type out, so there’s a shortcut:

tp->a
tp->b
tp->c

What if I want to access the actual char pointed to by tp->b?

*((*tp).b)   // == '?'
*(tp->b)     // == '?'

We can also get a pointer to a member of the structure:

int* ip = &(t1.a); 
        = &(tp->a); // Same thing

Finally, nothing stops us from having multiple pointers to the same structure:

thing* tp2 = tp; 

tp == tp2    // True
tp->a = 2;   // Also changes tp2->a

All of this applies equally to classes, and to methods. If we have a class

class thing {
  public:
    void run() {
      cout << "WHATUP" << endl;
    }
};

and then we have a pointer to an instance of that class

thing* t = ... ;

Then we can use either (*t).run() or t->run() to dereference t and call the run method:

(*t).run();
t->run();    // Same thing

In this semester, we’ll use a lot of pointers, but they will usually be pointers to structs or classes, so we’ll use -> a lot. We’ll almost never use &, because we generally won’t need to get pointers to existing objects. We’ll get our pointers by creating objects dynamically, via new. (Remember that new T returns a T*, a pointer to a T.

The null pointer

nullptr is the value you should use for the null pointer (the pointer that points to nothing at all), not NULL. The null pointer is unique: no other, non-null pointer is == to it, and it is == only to other null pointers. Thus, you can use nullptr to signal that a pointer doesn’t point to anything at all. This gives pointers a bit of power that normal “values” don’t have. E.g., if we have an int variable, there is no way to say that it contains “nothing”; it always contains some int value. But if we have an int pointer then there are two possibilities:

Dynamic memory allocation

I’ve intentionally separated pointers from dynamic memory because often students assume that whenever you have pointers to must also have dynamic allocation somewhere, but this is not the case. You can get a pointer to anything that “hangs around”. Dynamically allocated objects are particularly convenient for this, because they hang around until we delete them, but its not required to use them. Dynamic allocation is useful when we don’t how what or how many objects we will need (or how long they need to live), until our program is running.

Stack vs. heap. Activation records. Heap allocation. Lifetime of objects.

new finds space on the heap, big enough for an object of the given type, and then gives you a pointer to that space. (The array-new finds enough space for multiple objects of the same type.) Because the object is allocated on the heap, it will remain “alive” (taking up space) until we delete it.

A common mistake (coming from the assumption that pointers have to be used with dynamic allocation) is to write something like this:

int x = 12;
...

// Now we want a pointer to x
int* p = new int();
p = &x;
// carry on with p

What’s wrong with this? We create a new (dynamic) int, but then immediately discard the pointer to it, replacing it with a pointer to x. This means that we have no way to refer to the heap memory we just reserved, and hence no way to delete it. That memory is lost to our program, until our program exists; we have created a memory leak. The correct way to do this is

int* p = &x;
// carry on with p

If the thing you want to get a pointer to already exists, then just initialize your pointer to that; there’s no need to allocate any space. Only use new when you want to reserve some additional space on the heap.

Similarly, just because we are done with p does not mean we need to delete it. In this case, because the thing pointed to by p was not created via new, it should not be destroyed via delete.

An object can only be delete-d once; we saw last time that trying to delete something more than once crashes your program with a “double-free” error. This means that if we allocate something, we need to decide what part of our program “owns” it, and that part is responsible for delete-ing it. A pointer that points to an object that it will eventually delete is called an “owning” pointer. It “owns” the object on the other end, and somewhere, that object will be deleted via that pointer. There should only ever be one owning pointer to an object; as more than one would mean that the object would be deleted more than once. There can, of course, be any number of non-owning pointers to an object, because those won’t delete it when they are done.

Assignment 1

The first assignment is a kind of C++/CSci 123 review, but also hopefully gets you thinking about the kind of issues that will be important to us. The assignment is to implement an ordered array. A lot of the operations will be similar to those on the bag structure we implemented earlier, but a few are different.

Because an ordered array has a size, a capacity, and its elements, you will probably need three data members: size, capacity, and a dynamic array of elements (you could also use a vector).

The description of how the ordered array works says that the special value -2147483648 cannot be stored in the array. E.g., if you try to insert it, nothing happens, exists(-2147483648) always returns false, etc. This seems arbitrary, but it’s done so that if you want, you can use -2147483648 as a “special” value internally. E.g., if you store the contents of the ordered array in a (dynamic) array, you could implement remove(e) by finding the location of e and then simply putting a -2147483648 there, to mark it as “deleted”. This method will require you to implement every other method to ignore -2147483648 entries. If you use this method, you don’t need to store the size: the size of the array is just the number of entries that are != -2147483648.

Alternatively, you could implement it the way we implemented the bag, by using the front portion of the array, starting at index 0, for the “used” elements, and the second half for unused elements. Using this method, you must store the size of the array, and remove must shift array elements around (we cannot simply swap to the end, because that would un-sort the array!).

The assignment asks you to think about how much time the various operations will take, relative to the size of the array. E.g., as the array grows, which operations will get slower? Which will take the same amount of time? We’ll investigate these ideas further in the next few lectures.