classDiagram
Shape <|-- Circle
Shape <|-- Rectangle
class Shape {
xorigin : double
yorigin : double
getX() double
getY() double
}
class Circle {
radius : double
getRadius() double
}
class Rectangle {
width : double
height : double
getWidth() double
getHeight() double
}
C++ Classes
This exercise introduces you to the creation and use of C++ classes. You need to have done the Introduction to C++ exercise before attempting this.
Creating a Class
Create a file named
rect.hpp, containing the following:#pragma once class Rectangle { };Don’t forget the semi-colon at the end!
Then finish off this class definition so that it is able to represent rectangles. You should assume that a rectangle is represented by the x & y coordinates of its upper-left corner, its width and its height. Create private
doublefields for each of these.Your class should also include
A constructor that will create a
Rectangleobject, given values for x, y, width and heightA constructor that will create a
Rectangleobject, given values for width and height only (x & y defaulting to 0)Getter methods for each of the fields
A method called
perimeterthe returns the perimeter of the rectangleA method called
areathat returns the area of the rectangle
Include only prototypes for the methods, not full implementations. Don’t forget to use
constwhere necessary.TipLecture 3 worked through the implementation of a
Circleclass. This provides a good guide for what you need to do here!Now create a file named
rect.cpp, containing implementations of all the methods defined inRectangle. Use initializer lists when writing the constructors, and remember that one of them can delegate to the other.Check that the class compiles before proceeding further, using
g++ -c rect.cppIf all is well, this should create a file of object code named
rect.o. You’ll need this file for the next part of the exercise.
Using The Class
Now create a file named
testrect.cpp, containing a small program that tests theRectangleclass. Your program should create twoRectangleobjects with different positions and dimensions. It should then print out the perimeter and area of each rectangle.Compile your program with
g++ testrect.cpp rect.o -o testrectThen enter
./testrectto run it.
Managing Builds With CMake
Since CMake is used to manage the build process for Qt 6 applications, it is worth getting some practice with using it now.
Remove the executable file
testrectand object code filerect.ofrom the directory, then create a new file namedCMakeLists.txtin this directory. Give this file the following contents:cmake_minimum_required(VERSION 3.16) project(rectangle VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_executable(testrect testrect.cpp rect.cpp)Line 1 ensures that versions of CMake older than 3.16 cannot be used with this build file. Specifying a minimum version is important if you are using features of CMake that are available only from that version onwards.
Line 2 specifies the project name and version number, and declares that the project uses C++ (represented here as
CXX) rather than C.Lines 4–6 together specify that the compiler must, at minimum, offer strict support for the C++11 standard. If you are intending to use features introduced in C++17, for example, then you’ll need to change the ‘11’ to ‘17’.
Line 8 declares that executable
testrectwill be built from source filestestrect.cppandrect.cpp. Note that we’ve not included the dependency onrect.hpphere; CMake is smart enough to figure out this dependency for itself.1Create a
buildsubdirectory and run CMake in it to create the files needed to manage the build. This can be accomplished using the following chained sequence of commands:2mkdir build && cd build && cmake ..Don’t forget the
..on the end of this!You should now be in the
buildsubdirectory. Enterlsto list its contents. On Linux or macOS systems, you should see that CMake has created a makefile that can be used to build the application.The cross-platform command to execute the build is
cmake --build .However, on systems where a makefile has been generated, you can just enter
makeinstead. Try this now.Try making a small change to either
testrect.cpporrect.cpp. Save the file, then rebuild. You should see from the displayed output that only the changed file is recompiled to object code.However, if you make a change to
rect.hpp, both.cppfiles will be recompiled, because both of them include (and therefore depend on)rect.hpp.
The nice thing about the approach outlined above is that all of the build artifacts are in the build subdirectory, separate from the source code. This makes clean-up easy: all you need to do is remove build!
Improving The Class
Modify the object construction code so that the values supplied for width and height are validated, with any values that are less than or equal to 0.0 being rejected. If you’ve delegated appropriately, you should need to change only one of the two constructors.
The most appropriate way of doing this is to throw one of the standard exceptions supported by C++, namely
invalid_argument. Here’s an example of how to throw an exception:if (x > 100) { throw std::invalid_argument("x is too large"); }Note that you will need to have
#include <stdexcept>in your.cppfile for this to work. Note also that you can omit thestd::prefix if you haveusing namespace std;in your
.cppfile.Modify your
Rectangleclass so that everything except the constructor that does the validation is inlined into the class definition inrect.hpp. (See Lecture 3 if you’re not sure what inlining is.)Rebuild
testrectand check that its runtime behaviour is unchanged.Modify the test program so that it attempts to create an invalid rectangle. Rebuild and rerun so that you can see what happens when a C++ exception is thrown.
Add
try&catchblocks to the test program, to intercept the exception, display a more helpful message and terminate the program gracefully.
Inheritance & Polymorphism
In this part of the exercise, you will modify the Rectangle class so that it conforms to the inheritance hierarchy presented in Lecture 3:
Download the C++ & Qt lecture material from Minerva. Copy the the files
shape.hpp,circle2.hppandcircle2.cppfrom the03/srcdirectory to the directory in which you are currently working.Modify the
Rectangleclass so that it inherits fromShape, in the same way thatCircledoes.Check that the
testrectprogram still builds and runs as it did before—e.g., by runningmakein thebuildsubdirectory.Modify the three classes so that
Shapehas a pure virtualdraw()method (see Lecture 3), which is overridden inCircleandRectangle. The overridden method inCircleshould look something like this:void Circle::draw() const { cout << "Drawing Circle(" << "x=" << getX() << ",y=" << getY() << ",r=" << getRadius() << ")" << endl; }The
draw()method ofRectangleshould behave similarly.In a file named
shapedemo.cpp, write a program that- Creates a vector of pointers to
Shape - Populates that vector with a mixture of
CircleandRectangleobjects - Loops over the contents of that vector, invoking the
draw()method on the referenced objects
- Creates a vector of pointers to
Add the following to
CMakeLists.txt:add_executable(shapedemo shapedemo.cpp circle2.cpp rect.cpp)In the
builddirectory, rerunCMake, then build and runshapedemo:cmake .. make shapedemo ./shapedemoYou should see the program demonstrating polymorphic behaviour, whereby the correct version of
draw()(forCircleorRectangle) is always invoked, even though the drawing code is referencing all objects as if they were generic shapes.
Going Further
This last part of the exercise is optional.
There are some pre-existing methods in this class hierarchy that could be given a similar treatment—i.e., we could implement a pure virtual method in Shape that is overridden in subclasses.
Identify those methods and make the necessary changes to the code. Modify shapedemo.cpp so that these methods are invoked polymorphically.
Footnotes
Although including headers as explicit dependencies isn’t required, it shouldn’t cause problems if you do this, and some IDEs might find helpful for you to do so.↩︎
We could have separated these commands with a semicolon instead, but
&&makes the execution of the command to the right conditional on the success of the command to the left, which is safer. Of course, you can always just replace this command sequence with three separately-entered commands if you prefer…↩︎