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:
- Constructors/destructors
- Operator overloading
The files for this lab are found in CMSC12300 GitHub repository under cmsc12300/labs/lab4/. Please run git pull origin master in your local copy of the repository to fetch the files.
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, as we shall see a few examples of in this lab.
C++ adds many features to C, most notably classes, giving you the choice to program object-oriented, 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.
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 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, you learned that doing from numpy import * is frowned upon, which is similar to what we're doing here. However, in C++, this is an accepted style.
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:
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 povided you with a main.cpp that runs some tests, to see if the class works correctly.
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 your 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.
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 are different. The constructor is the entry point of any class instance, while the destructor is the exit point. For instance, if we allocate memory in the constructor, we should free it in the destructor. If you remember the node class that we worked with, we had the functions mkNode and freeNode. These are now replaced by a constructor and a destructor.
Exercises
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, 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 refer to 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 instad of 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.
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.
We have seen many similarities to Java so far, but overloading operators is something that is forbidden in Java, but allowed in C++.
Exercises
At this point you should pass at least the first four tests.
Note
Operators do not work on pointers of objects. If we have two pointers of 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.
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
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
Here are some exercises if you still have time:
Exercises
Implement another constructor for Polynomial that takes a single integer value and sets c to this value. This will allow your polyonimal class to do automatic type coercion to int.
For example, please confirm that it allows you to do this:
cout << Polynomial(4, 2, 1) + 5 << endl;
Or if you define the function:
Polynomial doublePolynomial(const Polynomial &p) { return p + p; }
You can then call doublePolynomial(3) and it will implicitly convert 3 to Polynomial(3).
Implement the * operator between a Polynomial and an int. You can implement this operator as a member function of Polynomial, but in that case you will only be able to do p1 * 2 and not 2 * p1. To make a symmetric operator between two different types, it has to be global, much like the friended << operator.