C++

This lab is a quick introduction to C++ programming. During the course of the lab, you will implement a class to represent a polynomial. The lab will briefly introduce the following topics:

Resources

Files

The files for this lab are found in the same CMSC12300 GitHub repository that you used for a previous lab, under cmsc12300/labs/lab4/.

Introduction

As the name suggests, C++ is an extension of C, meaning it adds functionality on top of C. The language is in fact a superset of C, meaning that everything you already know about C, you can use inside your C++ program. However, in many cases there will be a more C++ way of doing things, and we shall see a few examples of this in this lab.

C++ adds many features to C, most notably classes, giving you the choice to program in an object-oriented manner, just like in Java. Another feature in C++ that you might recognize from Java is generic programming using templates, which in Java allowed you to use List<String> to mean a list of strings. Finally, C++'s Standard Template Library (STL) includes plenty of useful objects and containers, such as strings, dynamic arrays, hash maps, queues, etc.

Compiling

The familiar GCC compiler can compile C++ and offers a shorthand for doing so using the command g++ instead of gcc. In fact, that is all we need to change:

$ g++ source1.cpp source2.cpp
$ ./a.out

We use the extension cpp now to mark that we're using C++. For header files, many people still use a single h, but some people prefer to be more clear and use hpp.

Printing

Printing to standard output in C is done through printf, which is still possible under C++. However, a more C++ way is to use input/output streams from the STL:

#include <iostream>

int main() {
    std::cout << "Hello world" << std::endl;
    return 0;
}

The std:: accesses cout and endl (newline) inside the namespace std, which is where everything in the STL can be found. To avoid this verbosity, we can place everything in the STL in the global namespace:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello world" << endl;
    return 0;
}

In Python, the equivalent to this is from numpy import *. This is frowned upon in python. However, in C++, this is an accepted style.

Namespaces allow things with the same name to coexist by disambiguating which version you are referring to. They are a new feature in C++.

Classes

In this lab, you will be constructing a class to represent a polynomial. We will limit ourselves to second-order polynomials or lower to make it easier:

\begin{equation*} ax^2 + bx + c \end{equation*}

Take a look in poly.h, where you will find all members and prototypes declared already. You will not need to change this file in this lab.

It is considered bad style to use using namespace in a header file, since it pollutes the global namespace of any cpp file that includes it. This is why we write std::ostream in full at one point in the file.

The file poly.cpp does not implement all functions listed, which is what you will do as part of the lab. We have also provided you with a main.cpp that runs some tests, to see if the class works correctly.

Prototypes and implementation

Take a look at poly.cpp and you should get a feel for how prototypes of member functions are implemented by prefixing them with Polynomial::.

Exercises

  • Create empty functions in poly.cpp for all function prototypes found in poly.h, so that you can successfully compile and run g++ poly.cpp main.cpp. If the function requires a return value, return an arbitrary value for now. If the return type is Polynomial, you can temporarily return Polynomial().

    In the rest of the lab, you should compile and run your program often to confirm progress and avoid an excessive accumulation of errors.

Constructors and destructors

The function prototypes that are called Polynomial and ~Polynomial are the constructor and the destructor, respectively. You can have several distinct constructors, as long as their function signatures (parameter lists) are different. The constructor is the entry point of any class instance, while the destructor is the exit point. For example, if we allocate memory in the constructor, we should free it in the destructor. If you remember the node class that we worked with in 122, we had the functions mkNode and freeNode. These are now replaced by a constructor and a destructor.

Exercises

  • Increment and decrement numPolynomials by 1 in the constructors and destructor, respectively. This is a static variable that is shared between all instances, keeping track of how many polynomials have been created and are still in memory. Make sure to run the tests to see that this works.
  • Implement the copy constructor. Mimic the other constructors, but take the initial values from another Polynomial instance. Notice that we do not use the return keyword in constructors, even though we might be tempted to do so conceptually. The copy constructor test won't pass until you have implemented the == operator later.

References

In the file, some of the parameters are:

const Polynomial &p

The const denotes that the compiler won't allow us to change the object, but what does the & mean? It denotes a reference, and is similar in theory to a pointer but with more type safety, since it can't take the value NULL. It acts just like a stack object (i.e., an object declared as a local variable), so we do not need to dereference it or use the arrow operator (->) to access its members. Notice that this operator is not the same as the reference operator &, that finds the pointer of an object; the compiler knows which one you intend by context.

So, why are we passing variables as const Polynomial & instead of simply as Polynomial? This is a common practice in C++, since the former avoids the overhead of creating a copy of the object, which would happen in the latter case. Passing as a pointer, as we did in C, has the same memory benefits, but in that case we can't guarantee that we are given a valid non-null object.

In summary, passing objects as references instead of as pointers offers the guarantee that the object is valid, without requiring expensive copies to be made. Note that references can only replace pointers in some situations, such as passing parameters. We will still need to use pointers when managing dynamically allocated memory.

Instances

As with any basic data types in C, we can create class instances both on the stack:

Polynomial p(1, 2, 3);

or dynamically on the heap:

Polynomial *p = new Polynomial(1, 2, 3);

// ...

delete p;

In C++, instead of malloc and free, it is more common to use new and delete, which not only allocates and frees memory for us, but also ensures that the constructor and destructor are called.

Operator overloading

We have seen many similarities to Java so far, but overloading operators is something that is forbidden in Java, but allowed in C++.

Exercises

  • Implement the assignment operator =. It should set all the member variables to be equivalent to those of p, and then return a reference to itself (this part has already been done for you). The operator returns itself to allow expressions such as obj1 = obj2 = obj3;.
  • Implement the + operator. This does not return a reference, since it should create a new instance instead. This operator should not change the instance variables, in fact, since it's decorated with const the compiler won't allow it even if you try.
  • Implement the == operator, and require all coefficients to be pairwise equal for the objects to be equal.

At this point you should pass at least the first four tests.

Note

Operators do not work on pointers to objects. If we have two pointers to polynomials:

Polynomial *p1 = new Polynomial(1, 2, 3), *p2 = new Polynomial(1, 2, 3);

Then p1 == p2 will return false, since we're comparing the pointers and thus memory addresses. Only when we do *p1 == *p2 will we use the operator that we just overloaded and have it return true.

Member functions

Just like in Java, we use this to refer to the current instance inside a member function. We can omit this when accessing member variables, unless there is a name collision. Notice that this refers to a pointer of the object, and to access the object itself we must dereference it as *this.

Exercises

  • Implement evaluate, which returns the value of the polynomial, given a certain x.
  • Implement hasRealSolution, getRealSolution1 and getRealSolution2. These refer to setting the quadratic expression to 0 and trying to solve it. Let getRealSolution1() >= getRealSolution2().

Printing the object

As we saw above, printing in C++ is done by linking objects using the << operator, starting with the cout object. To make our class fit into this framework, we have to overload the operator that takes a cout object (which is an ostream object) on the left, and our object on the right. The syntax for this has already been included, as well as a basic implementation. Since this operator is not a member function of Polynomial, we have to declare it a friend of the class, in order to give it priveleges to access the private coefficients.

Exercises

  • Right now, it does not print the expressions in a very natural way, as can be seen when running main.cpp. Negative values should not be displayed as plus and then minus. Coefficients with value 0 can be omitted altogether and a 1 in front of x or x^2 is unnecessary. Please make improvements to the code to address these issues.

Templates

When you were creating linked lists and hash maps in C, you had to specify what type of object you wanted to store inside the container, or void * to make it general (but not as convenient to work with). In C++, we can use templates to create functions and classes of any data type, as demonstrated in template.cpp:

    #include <iostream>
    using namespace std;

    template <typename T>
    T unevenAdd(const T &a, const T &b) {
        return a + a + b;
    }

    int main() {
        float x = 10.5, y = 20;
        int i = 10, j = 20;
        cout << "unevenAdd(x, y) = " << unevenAdd(x, y) << endl;
        cout << "unevenAdd(i, j) = " << unevenAdd(i, j) << endl;
        return 0;
    }

What happens here is that the compiler will identify exactly which versions of ``unevenAdd`` will be required, and compile separate versions for each data type ``T`` that it comes across. In this example, it will compile as long as the class ``T`` overloads the ``+`` operator which is used in the function body. We can also explicitly tell the compiler what version of ``unevenAdd`` we will use by for instance calling ``unevenAdd<int>(10, 20)``.

This example may seem somewhat uncompelling; as you know, real numbers are a superset of integers. We might argue that you should implement this function once, over floats, and then use that implementation with ints as well. However, this specialized version avoids the need to convert back from a float to an int; maintains full precision for any possible int values (floats are not able to represent all integers with perfect precision); and avoids using the slower floating-point components of the processor. And, of course, it is intended to be a simple example.

It is even more common to template classes than it is to template functions, especially when creating containers such as lists and hash maps. For instance, the coefficients in our polynomial are stored with int. Instead, we could create Polynomial<T>, which would allow us to create polynomials with any type of coefficients, for instance floats or complex numbers.

Exercises

  • Modify template.cpp to include poly.h. Construct two polynomials and try using unevenAdd on them. Print the result.

    Don't forget to compile both template.cpp and poly.cpp into the file.

Bonus

Here are some exercises if you still have time:

Exercises