int_vec: A Header and an Implementation

The purpose of this note is to discuss the process of splitting a class, such as int_vec, into a .h file and .cpp. This usually what is done with in large programs. It allows the code for int_vec to be compiled once, but #included as many times as needed.

Step 1: Move all the method bodies of int_vec to after int_vec.
The int_vec class below shows the result of doing this. What remains in the class itself are just the (private) variables, and the headers for all the methods. The method bodies are declared after the int_vec class. All the method names start with int_vec:: because they belong to int_vec.
Step 2: Put the int_vec class in a .h header file, and put the method

implementations in a .cpp file. We’ll call these files int_vec.h and int_vec.cpp. They have this basic structure:

// int_vec.h

class int_vec {
    // ...
};

void clear(int_vec& v);
// ...

//////////////////////////////////////////////////////////////

// int_vec.h

#ifndef INT_VEC_H
#define INT_VEC_H

#include "int_vec.h"
#include "cmpt_error.h"
#include <iostream>
// ...

// ... all functions and methods of int_vec go here ...

#endif

Now we can compile int_vec.cpp separately, which is a big time-saver for large programs.

Notice that int_vec.h begins with these two lines:

#ifndef INT_VEC_H
#define INT_VEC_H

And ends with this line:

#endif

In C++, commands that start with # are pre-processor directives, i.e. commands to the pre-processor that scans the code before the main C++ compiler sees it.

The directive #ifndef INT_VEC_H tests if INT_VEC_H has been defined before (by a #define statement). If so, then everything after #ifndef INT_VEC_H is skipped and not seen by the compiler. But if INT_VEC_H is not defined, then it is immediately defined in the next line, and the rest of the file is compiled as usual. The #endif at the end of the file matches the #ifndef at the top.

The reason for doing this is to prevent multiple inclusion of int_vec.h. In large programs, it is common to unintentionally include a file like int_vec.h twice or more in the same file, which will cause the compile to complain that int_vec is being defined multiple times (a class must be defined exactly once in C++). The #ifndef acts as a guard to make sure that at most one instance of int_vec.h gets compiled.

Step 3: Compile int_vec.cpp to create a .o object file that can later

be linked into other files to create an executable program.

The basic command option to compile a file in g++ is -c, and is used like this:

$ g++ -c int_vec3.cpp

After running this command successfully, the object file int_vec.o should be in the folder.

We are using g++ with various options turned on, so the actual g++ command is longer:

g++ -std=c++11 -Wall -Wextra -Werror -Wfatal-errors
    -Wno-sign-compare -Wnon-virtual-dtor -g -c int_vec.cpp

Don’t miss the -c near the end!

Step 4: As a test, create a .cpp file with a main function and some

code that uses int_vec. Lets call this int_vec_test.cpp:

// int_vec_test.cpp

#include "int_vec.h"
#include <iostream>

using namespace std;

int main() {
    int_vec a;
    for(int i = 0; i < 10; ++i) {
        a.append(i);
    }

    cout << "a = " << a << "\n";

    clear(a);

    cout << "a = " << a << "\n";
}

We compile this into a .o object file using g++ and the -c option:

$ g++ -c int_vec_test.cpp

Now int_vec_test.o should be in the folder.

As before, the actual g++ command has more options and would look like this:

$ g++ -std=c++11 -Wall -Wextra -Werror -Wfatal-errors
   -Wno-sign-compare -Wnon-virtual-dtor -g -c int_vec3_test.cpp
Step 4: Link the .o object files together using g++’s -o option to

make an executable file:

g++ -o int_vec3_test int_vec3.o int_vec3_test.o

If all goes well, this creates an executable file named int_vec_test in the current directory that you can run like this:

$ ./int_vec3_test

Separate compilation and the linking the way we’ve done it here has lots of little details you need to get right, and so in practice, these steps are usually written down in a makefile so that all you need to do is type make to create the int_vec_test executable. However, in this course we won’t go any further into the details of using make.

int_vec.h

    // int_vec.h

#ifndef INT_VEC_H
#define INT_VEC_H

    #include <iostream>
    #include "cmpt_error.h"
    #include <cassert>

    using namespace std;

    class int_vec {
    private:
        int capacity; // length of underlying array
        int* arr;     // pointer to the underlying array
        int size;     // # of elements in this int_vec from user's perspective

        void resize(int new_cap);

    public:
        int_vec();
        int_vec(int sz, int fill_value);
        int_vec(const int_vec& other);

        ~int_vec();

        int get_size() const;
        int get_capacity() const;

        int get(int i) const;
        void set(int i, int x);


        int& operator[](int i);
        int operator[](int i) const;

        void print() const;
        void println() const;

        void append(int x);

        int_vec& operator=(const int_vec& other);

        friend void clear(int_vec& v);
    }; // class int_vec

    void clear(int_vec& v);

    bool operator==(const int_vec& a, const int_vec& b);
    bool operator!=(const int_vec& a, const int_vec& b);

    ostream& operator<<(ostream& out, const int_vec& arr);

    #endif

int_vec.cpp

// int_vec.cpp

#include "int_vec.h"

void int_vec::resize(int new_cap) {
    if (new_cap < capacity) return;
    capacity = new_cap;

    int* new_arr = new int[capacity];  // create new, bigger array

    for(int i = 0; i < size; ++i) {    // copy elements of arr
        new_arr[i] = arr[i];           // into new_arr
    }

    delete[] arr;                     // delete old arr

    arr = new_arr;                    // assign new_arr
}

int_vec::int_vec()
: capacity(10), arr(new int[capacity]), size(0)
{ }

int_vec::int_vec(int sz, int fill_value)
: capacity(10), size(sz)
{
    if (size < 0) cmpt::error("can't construct int_vec of negative size");
    if (size > 0) capacity += size;
    arr = new int[capacity];
    for(int i = 0; i < size; ++i) {
        arr[i] = fill_value;
    }
}

int_vec::int_vec(const int_vec& other)
: capacity(other.capacity), arr(new int[capacity]), size(other.size)
{
    cout << "int_vec copy constructor called ...\n";
    for(int i = 0; i < size; ++i) {
        arr[i] = other.arr[i];
    }
}

int_vec::~int_vec() {
    cout << "... ~int_vec called\n";
    delete[] arr;
}

int int_vec::get_size() const {
    return size;
}

int int_vec::get_capacity() const {
    return capacity;
}

int int_vec::get(int i) const {
    if (i < 0 || i > size) cmpt::error("get: index out of bounds");
    return arr[i];
}

void int_vec::set(int i, int x) {
    if (i < 0 || i > size) cmpt::error("get: index out of bounds");
    arr[i] = x;
}

int& int_vec::operator[](int i) {
    cout << "(modifying operator[] called)\n";
    return arr[i];
}

int int_vec::operator[](int i) const {
    cout << "(const operator[] called)\n";
    return arr[i];
}

void int_vec::print() const {
    if (size == 0) {
        cout << "{}";
    } else {
        cout << "{";
        cout << arr[0];
        for (int i = 1; i < size; ++i) {  // i starts at 1 (not 0)
            cout << ", " << arr[i];
        }
        cout << "}";
    }
}

void int_vec::println() const {
    print();
    cout << "\n";
}

void int_vec::append(int x) {
   if (size >= capacity) {
        resize(2 * capacity);   // double the capacity
   }
   assert(size < capacity);
   arr[size] = x;
   size++;
}

int_vec& int_vec::operator=(const int_vec& other) {
    // self-assignment is a special case: don't need to do anything
    if (this == &other) {
        return *this;
    } else {
        // re-size this int_vecs underlying array if necessary
        if (capacity < other.size) {
            resize(other.size + 10);     // a little bit of extra capacity
        }                                // to speed up append

        size = other.size;

        for(int i = 0; i < size; ++i) {  // copy other's values
            arr[i] = other.arr[i];       // into this array
        }

        return *this;
    }
}

////////////////////////////////////////////////////////////////////////////////

void clear(int_vec& v) {
    v.size = 0;
}

bool operator==(const int_vec& a, const int_vec& b) {
    if (a.get_size() != b.get_size()) {
        return false;
    } else {
        for(int i = 0; i < a.get_size(); ++i) {
            if (a.get(i) != b.get(i)) {
                return false;
            }
        }
        return true;
    }
}

bool operator!=(const int_vec& a, const int_vec& b) {
    return !(a == b);
}

ostream& operator<<(ostream& os, const int_vec& arr) {
    if (arr.get_size() == 0) {
        os << "{}";
    } else {
        os << "{";
        os << arr.get(0);
        for (int i = 1; i < arr.get_size(); ++i) {  // i starts at 1 (not 0)
            os << ", " << arr.get(i);
        }
        os << "}";
    }
    return os;
}

int_vec_test.cpp

// int_vec_test.cpp

#include "int_vec.h"
#include <iostream>
#include "cmpt_error.h"

void test() {
    int_vec a;
    for(int i = 0; i < 10; ++i) {
        a.append(i);
    }

    cout << "a = " << a << "\n";

    clear(a);

    cout << "a = " << a << "\n";
}

int main() {
    test();
}