12. Object-Oriented Programming#
Object-oriented programming is a style of programming focused on objects. We create objects and tell them to do stuff by calling functions that belong to those objects. We typically refer to those object functions as methods. When two objects interact by calling methods and receiving return values, you may hear this called passing messages. The sending message is the method name and the associated arguments. The response message is the return value(s). With object-oriented programming, We reason about a program as a set of interacting objects rather than a set of actions(procedural programming).
Objects are instances of a class. A class describes a type of object we might create. Classes correspond to nouns - for example, a general category of something like a bank account or a stock. Classes then define the fields (state) that the object will have. They also define the behavior by defining methods within the class definition. This behavior often can be found in the verbs used to discuss the particular problem domain. Methods declarations are just function definitions within a class declaration. A class forms a blueprint from which we create objects.
As in Python, C++ classes provide similar abstraction capabilities to model something - i.e., the properties and behavior of something. However, C++ provides greater capacity for information hiding. Both languages support encapsulation, but with Python, programmers can peek in and directly manipulate the internal state of an object (one of Python’s design guidelines is to trust to programmer to not perform maliciously). C++ provides access control to properties and methods through the use of access specifiers - public
, private
, and protected
- to control who can see which properties and methods. Instead of an initialize method (__init__()
), C++ has constructors, which have the same name as the class. Constructors do not have a return type specified as implicitly they return an object of the class type. Also notice that the ‘self’ argument present in Python classes is not needed in C++ as the methods implicitly have access to a pointer called this
that points to the current object. The methods getY()
and setY()
demonstrate using this
. The type of this
in a member function of class X
is X*
(pointer to X
) [C++ Standard].
12.1. Defining a Class in C++#
The following file demonstrates creating a class to represent an x,y coordinate.
1//filename: simple_point.cpp
2//complile: g++ -std=c++17 -o simple_point simple_point.cpp
3//execute: ./simple_point
4#include <iostream>
5using namespace std;
6
7class Point {
8private:
9 double x;
10 double y;
11
12public:
13 Point(double initialX = 0.0, double initialY = 0.0) : x{initialX}, y{initialY} {}
14
15 double getX() const {
16 return x;
17 }
18
19 void setX(double val) {
20 x = val;
21 }
22
23 double getY() const{
24 return this->y;
25 }
26
27 void setY(double val) {
28 this->y = val;
29 }
30};
31
32
33int main(int argc, char *argv[]) {
34 Point p1;
35 Point p2(5,2);
36 Point p3(10);
37 p1.setY(10.0);
38
39 cout << "p1: " << p1.getX() << "," << p1.getY() << endl;
40 cout << "p2: " << p2.getX() << "," << p2.getY() << endl;
41 cout << "p3: " << p3.getX() << "," << p3.getY() << endl;
42}
Line 7 starts the declaration of the class with the keyword
class
followed by the name of the class. As with Python, we could have created an empty class withclass empty {};
, but as C++ is statically typed, this declaration is useless. Note that we have to end the class with a semi-colon in line 27.Line 8 contains an access specifier
private
. Until another access specifier appears, any methods or properties declared are considered “private” and only available for use within this class itself. This provides information hiding (external entities do not have access to our internal implementation), which is a recommended practice with object-oriented programming.Lines 9 and 10 declare two properties (attributes) of the class: x and y. These attributes are also referred to as data members.
Line 12 contains another access specifier,
public
. Methods and properties in this section are available for use outside of the class.Line 13 is a constructor to create a new instance (object) of the class
Point
. As with functions, we can provide provide default values. After the colon:
, is an initialization list that can set the object’s properties.Lines 15-17 and 23-25 are both accessor methods to allow other code to access the values for x and y. The
const
keyword explicitly states to both the compiler and other developers. Variables accessed within these methods (i.e., the object’s state) cannot be altered.Lines 19-21 and 27-29 are mutator methods used to allow other code to modify the object’s state.
Within the main function, we declare and instantiate three point objects. Notice that the constructor is automatically called in all three instances.
p1
uses both default values.p2
explicitly sets bothx
andy
.p3
setsx
, but leaves the default value fory
. The code then modifiesp1
’s state to altery
.
12.2. Improving the Point Class#
The following four files demonstrate a more robust version of the Point
class.
Rather than having all of the source code in a single file, we now have it spread across three separate files. While this may seem complicated, this becomes necessary as we create larger systems. The point class is now divided into two files: point.hpp
and point.cpp
. The point.hpp
contains the definition of the class. This header file will be included in whatever other source files (e.g., main.cpp
) that will use the object. The point.cpp contains the implementation of the class. If you do not include the header file a statement such as
#include "point.hpp"
then the compiler will generate error messages as it does not know about the class Point
. The main.cpp
file contains sample code using the Point class.
To make building executables easier (as well as scripting other tasks such as cleaning files), we use a widely-adopted build automation tool, make. When you execute “make” at the command-line, the tool looks for a configuration file called “Makefile”. This file defines targets and then a series of commands to “execute”/”complete” that target.
See the Makefile page in The Tools section for more information.
point.hpp
: Defines a header file that other classes can use via #include point.h at the top of the source file. Notice this defines our state and then the interfaces (behavior) that other classes and source files can use. We also use a preprocessor directive, #ifndef
to test if the POINT_H
macro has already been defined. If it has, then the code between #ifndef
and #endif
is skipped. This is useful when we have complicated includes to prevent code from appearing more than once.
1//filename: point.hpp
2//complile: g++ -std=c++17 -o point.o point.hpp
3//execute: not applicable - no main function
4#ifndef POINT_H
5#define POINT_H
6#include <iostream>
7
8class Point {
9private:
10 double x;
11 double y;
12
13public:
14 Point(double initialX=0.0, double initialY=0.0);
15 Point operator+(const Point& rhs) const;
16 double getX() const;
17 void setX(double val);
18 double getY() const;
19 void setY(double val);
20 void scale(double factor);
21 double distance(Point other) const;
22 void normalize();
23 Point operator+(Point other) const;
24 Point & operator+=(const Point &rhs);
25 Point operator*(double factor) const;
26 double operator*(Point other) const;
27};
28
29// Free-standing operator definitions, outside the formal Point class definition
30Point operator*(double factor, Point p);
31std::ostream& operator<<(std::ostream& out, Point p);
32#endif
point.cpp
: Implements the behavior for the Point
class.
In lines 44-60 - The class overloads the built-in operations for +
and *
. Notice also that we can either multiply by a factor or we can compute the dot product.
Lines 62-65 also overloads the *
, but in this situation, we provide capabilities when the double is specified first in the expression.
Lines 67-70 overload the <<
operator. Note the return of the stream reference which supports chaining these method calls in an expression.
1//filename: point.cpp
2//complile: g++ -std=c++17 -o point.o point.cpp
3//execute: not applicable - only builds an object file.
4#include "point.hpp"
5#include <cmath> // for sqrt definition
6#include <iostream> // for use of ostream
7using namespace std; // allows us to avoid qualified std::ostream syntax
8
9Point::Point(double initialX, double initialY) : x{initialX}, y{initialY} {}
10
11void Point::scale(double factor) {
12 x *= factor;
13 y *= factor;
14}
15
16double Point::getX() const {
17 return x;
18}
19
20void Point::setX(double val) {
21 x = val;
22}
23
24double Point::getY() const{
25 return y;
26}
27
28void Point::setY(double val) {
29 y = val;
30}
31
32double Point::distance(Point other) const {
33 double dx = x - other.x;
34 double dy = y - other.y;
35 return sqrt(dx * dx + dy *dy);
36}
37void Point::normalize() {
38 double mag = distance(Point());
39 if (mag > 0) {
40 scale(1 / mag);
41 }
42}
43
44Point Point::operator+(Point other) const {
45 return Point(x + other.x, y + other.y);
46}
47
48Point & Point::operator+=(const Point& rhs){
49 this->x += rhs.getX();
50 this->y += rhs.getY();
51 return *this;
52}
53
54Point Point::operator*(double factor) const {
55 return Point(x * factor, y * factor);
56}
57
58double Point::operator*(Point other) const {
59 return x * other.x + y * other.y;
60}
61
62// Free−standing operator definitions, outside the formal Point class scope. point p = factor * otherP
63Point operator*(double factor, Point p) {
64 return p * factor; // invoke existing form with Point as left operator
65}
66
67ostream &operator<<(ostream &out, Point p) {
68 out << "<" << p.getX() << "," << p.getY() << ">";
69 return out;
70}
main.cpp
: Provides example code for using the Point
class. Notice that in line 11, the uniform (brace) initialization syntax is used.
1//filename: main.cpp
2//complile: make
3//execute: ./point
4#include "point.hpp"
5#include <iostream>
6
7int main() {
8 std::cout << "Hello World!\n";
9
10 Point a; // Declares a, initalizes with the default constructor
11 Point b{5, 7}; // declares b, initilizes with a constructor
12 std::cout << "Point a: " << a << std::endl;
13 std::cout << "Point b: " << b << std::endl;
14
15 b.scale(2); // calls method
16 std::cout << "Point b(scaled): " << b << std::endl; //uses overload operator << on ostream
17 Point d = b * 2; // declares d, assigns it as the result of b * 2. * is defined in Point
18 std::cout << "Point d(b*2): " << d << std::endl;
19
20 Point e = 2 * b; // uses the freestanding method.
21 std::cout << "Point e(2*b): " << e << std::endl;
22
23 e += b;
24 std::cout << "Point e += b: " << e << std::endl;
25}
Makefile
: Compiles each of the object files (main.o and point.o) and then combines them into the executable. Also provides another target to remove the object files and any temporary files.
1//filename: Makefile
2//Note - builds to executable
3//execute: make
4CPPFLAGS=-std=c++17 -pedantic -Wall -Werror -ggdb3
5point: main.o point.o
6 g++ -o point main.o point.o
7main.o: main.cpp
8 g++ $(CPPFLAGS) -c main.cpp
9point.o: point.cpp point.hpp
10 g++ $(CPPFLAGS) -c point.cpp
11
12.PHONY: clean
13clean:
14 rm -f *.o *~ point
Now, in the terminal, execute “make” to build the executable. Then execute “./point” to run the code.
12.3. Constructors#
Constructors are used to initialize the data members (properties) of a class object. Whenever an object is created, a constructor will execute.
Constructors have the same name as the class name, but they do not have a return type - implicitly their return type is just that of the class itself. As with other functions, constructors can take zero or more parameters as well as specifying default values for parameters. Constructors may also be overloaded (i.e., we can have multiple constructors for a class) - they just must differ from each other by the parameter types and/or the number of parameters.
The point class could have the following constructors:
Point() { x = 0.0; y = 0.0; }
Point(double initialX, double initialY) : x{initialX}, y{initialY} {}
Point(Point &p) { x = p.x; y = p.y; }
The first constructor is called the default constructor as it takes no arguments. A constructor that provides default values for all of its parameters is also a default constructor. If a class does not explicitly define any constructors, the compiler implicitly defines a default constructor. This synthesized default constructor will initialize each data member (property) through calling a default constructor for that member unless it has been set to another value. Primitive types such as int and double are initialized to zero. Generally speaking, your classes should have a default constructor as many of the C++ standard libraries depend upon these being present.
In the second constructor, we utilize a constructor initializer list
that specifies the initial values for one or more of the object’s data members. The elements in this list are executed in order of how the members are defined for the class itself. To avoid confusion, you should follow the same order. Any data member not initialized in this list follows the same logic/process as it would have under the synthesized default constructor. Using the initialize list is the preferred way to establish initial attribute values. You can place more intricate initializations within the constructor body.
The third constructor is a copy constructor. This constructor is used to create a new constructor from an existing object. The C++ compiler will automatically create a copy constructor if one is not already written. By default, this synthesized constructed will individually assign each data member in order. Generally, this will not be a problem except when an object owns pointers or non-shareable references, such as to a file.
The copy constructor is called in the following situations: 1) an object of the class is returned by value from a method/function; 2) an object of the class is passed (to a method/function) by value as an argument; 3) an object is constructed based on another object of the same class; and 4) the compiler generates a temporary object.
Whenever you write a copy constructor, you should also write a destructor and assignment operator. “Rule of Three”. Note: With C++ 11, the has expanded to the “Rule of Five” and covered in Essential Class Operations.
When an object contains pointers or references as data members, it will generally be necessary to write a copy constructor. Generally, you will want to perform a deep-copy of the pointed to/referenced object. By default, the synthesized copy constructor only performs a shallow-copy. The semantics of the shallow vs deep copy are the same as those presented in the Python discussion.
Note: It does not make sense (and is not allowed) for a constructor to be declared as const
. The constructor’s purpose is to initialize (change) the object. An object cannot become immutable until after the construction (initialization) is complete.
From a best-practice perspective, an object should be ready for use once the constructor is complete.
12.4. Destructor#
The destructor performs whatever work is necessary to free resources used by an object (e.g., release dynamically allocated memory, close files) and then destroy data members themselves.
class Point {
public:
~Point() { //destructor
//perform cleanup actions
}
};
The ~ClassName defines the destructor, which takes no arguments and has no return value. As a destructor takes no arguments, it cannot be overloaded.
The destructor is called automatically whenever an object of its type is destroyed:
Variable goes out of scope.
Data members of an object when that object is destroyed.
Elements in a container (to be presented when we cover containers / STL) is destroyed.
When dynamically allocated object are destroyed through either the
delete
ordelete[]
operators.When temporary objects are no longer needed.
If pointers exist within an object, that memory is not automatically reclaimed (freed).
The C++ compiler will create a synthesized destructor for any class that does not define one.
12.5. Copy-Assignment Operator#
The copy-assignment operator controls how objects of the class are assigned
Point p1(5,2), p2;
p2 = p1; //uses the Point copy-assignment operator
As with the copy constructor and destructor, the compile creates a synthesized copy-assignment operator for a class if it is not already defined. The synthesized constructor assigns each data member of the right-hand side object to the object’s data members on the left-hand side. The following code explicitly defines the copy-assignment operator for Point:
Point& operator=(const Point& other) {
x = other.x;
y = other.y;
return *this;
}
In the last line, we want to return the Point itself, so we need to dereference this
to refer to the actual object.
As with the copy constructor and destructor, the copy-assignment operator needs to be written when dynamically allocated objects are present or with other allocated resources (e.g, file, network connection, etc.). Commonly, this is referred to as the “Rule of Three”.
12.6. Accessors vs Mutators#
Accessor methods are those methods that cannot alter the state of an object. Mutator methods are those methods that may alter an object’s state. In C++, we explicitly designate accessor methods by placing the keyword const
at the end of the function signature (i.e., after the parameters), but before the method’s body.
When the keyword const
is placed before the type of the parameter, the function/method guarantees (and enforced by the compiler) that the object will not be modified.
12.7. Static#
As with Python, C++ supports the concept that classes may require members (both properties and methods) that belong to the class itself rather than instances of the class (objects). Static members are declared by placing the keyword static before that member’s declaration. Static members can be public, private, or protected as well as refer to any possible variable.
1//filename: account.cpp
2//complile: g++ -std=c++17 -o account.o account.cpp
3//execute: not applicable - only an object file is produced.
4class Account {
5public:
6 Account(double initialAmount): amount{initialAmount} {};
7 void calculate() { amount += amount * interestRate;}
8 static double getRate() { return interestRate;}
9 static void setRate(double newRate) { interestRate = newRate;}
10private:
11 double amount;
12 static double interestRate;
13};
12.8. Sample LLM Prompts#
Should I always use access methods in c++ or is it acceptable to directly access an object’s variables(state)?
Why is it important to separate the class declaration from the class definition in c++?
Explain C++’s rule of three as if I student in an introductory example. Use a finance-related code sample.
12.9. Review Questions#
How do constructors and functions differ in C++?
How is a defualt constructor identified? Does C++ always have a default constructor? When is the default constructor called?
What actions should be performed within a destructor?
Acknowledgements: The Point class has been adopted from “A Transition Guide from Python 2.x to C++” by Michael Goldwasser and David Letscher.