7 Object Orientated Programming in C++
7.1 Introduction
Combine data and functions as a unit. Components of a class are called members. In OOP parlance functions members are called methods
- methods: behaviour of the object
- data members: state state of the object
Concrete objects created from a class that exist during life-time are called instances.
Classes are like new data types, and instance objects are declared just like variables of fundamental types would.
Example
class Account
{
public:
double get_balance() const;
double withdraw(double amount);
private :
double balance = 100; //initialize with default 100
};
::get_balance(){return balance;}
Account::withdraw(doulbe amount){balance -= amount;}
Account
;
Account a1.withdraw(25); a1
The keyword const
in double get_balance() const
denotes that get_balance()
doesn’t modify the state of the object \(\Rightarrow\) accessor method.
- accessor methods: methods that do not modify the state of the object like
get_balance()
. They should be denoted by theconst
keyword as above. - mutator methods: methods that modify the state of the object like
withdraw()
.
7.2 Encapsulation
- public members of the class is the interface provided for the user of the class.
- private members of the class are used to implement the public interface.
This separation is called encapulsation and information hiding. This facilitates changing the implementation without changing the interface or affecting other programs that use this class.
Usually an improvement of an existing code comes through changing the underlying data structures. If internal representation if kept hidden from the user of the class, than this change will not effect the user.
It is good practice to list public
members before private
ones, since users reading the class are primarily interested in the interface as opposed to implementation details.
To adhere to the principle of information hiding data members should be always kept private
. Accessing or mutating the data members should be provided through a public interface of accessor and mutator member functions, and never directly.
7.3 Separate Compilation
Encapsulation and information hiding nicely leads to the concept of modularization and separate compilation. When interface definitions and implementations of a class are separated in distinct source files, only the files that are modified can be recompiled, other files need not to be.
Consider the situation we want to simulate a cash register machine, with a class that provides the following interface:
We provide the interface for the class ’CashRegister’in a header file:
cash_register.hh
#ifndef CASHR_H
#define CASHR_H
class CashRegister
{
public:
();
CashRegisterdouble get_total() const;
int get_count() const;
void clear();
void add_item(double amount);
void display() const;
private:
int item_count;
double price_total;
};
#endif // !CASHR_h
Note the
#ifdef CASHR_H
#define CASHR_H
//... contents of the header file
#endif //CASHR_H
construct. This is called a header guard. It is possible that in a project there are many files that use the CashRegister
class. When multiple such files are included in another file, the problem will arise that header definitions of CashRegister
are included multiple times. As multiple definitions are not legal, this would cause a compiler error. Header guard ensures this, and always should be used.
The implementation of this interface definition is provided separately in a .cpp
file:
cash_register.cc
#include "cash_register.hh"
#include <iostream>
::CashRegister()
CashRegister{
= 0;
item_count = 0;
price_total }
int CashRegister::get_count() const {return item_count;}
double CashRegister::get_total() const {return price_total;}
void CashRegister::display() const {
std::cout << "count: " << get_count() << std::endl
<< "sum: " << get_total() << std::endl;
}
void CashRegister::clear()
{
= 0;
item_count = 0;
price_total }
void CashRegister::add_item(double amount)
{
++;
item_count+= amount;
price_total }
Note that other additional headers needed for the implementation like `
Finally we create test program called test_cashregister.cc
with a main()
function, that will utilize and test the CashRegister
class:
test_cashregister.cc
#include "cash_register.hh"
#include <iostream>
void display_n(CashRegister cr)
{
.display();
crstd::cout << std::endl;
}
int main(int argc, char const *argv[])
{
;
CashRegister cr1(cr1);
display_n.add_item(15.4);
cr1(cr1);
display_n.clear();
cr1(cr1);
display_nreturn 0;
}
Note that test_cashregister.cc
only has the interface to CashRegister
via the header cash_register.hh
but not the actual implementation. This is well intented, since we want to provide the implementation to the compiler as follows:
g++ -o test_cashregister test_cashregister.cc cash_register.cc
How headers are interrelated and compiled together into an executable can be visualised as follows:
The important advantage here is that if the implementation of CashRegister
changes, this is reflected solely in cash_register.cc
. Thus during recompilation, this file alone needs to be recompiled and linked against test_cashregister.cc
. For large software projects and collaborative programming this modularization is essential.
7.4 Constructors & Destructors
Constructors
- Constructor method is called after an object is initialized/created in memory.
- It can defined manually by the programmer, otherwise a default constructor always exists. In case of manual definition it can have a list of arguments, just like any other method.
- When a constuctor is defined manually, the default constructor (one without any arguments must be redefined explicitly by the programmer) \(\Rightarrow\) overloading.
- For class
A
its constructor is calledA()
. (Same name as its class) - Has no return value, but doesn’t use keyword
void
Destructors
- The method called before the memory occupied by the object is freed.
- It can be defined by the programmer, otherwise default destructor is created.
- Destructor for class
A
is called~A()
- Destructors have no arguments, no return value, do not use
void
.
Example:
class Account
{
public :
(double amount);
Account(); //default constructor must be now explicitly defined
Account~Account();
... //rest of class
};
// ... rest of implementations
// initializes account with initial balance of amount
::Account(double amount) {balance = amount;}
Account
// overloaded constructors defininig default constructor, which
// initializes account to a default value of 100
::Account() {balance = 100;} Account
Then
;
Account a1std::cout << a1.get_balance() << std::endl;
.withdraw(100);
a1std::cout << a1.get_balance() << std::endl;
(500);
Account a2std::cout << a2.get_balance() << std::endl;
.withdraw(100);
a2std::cout << a2.get_balance() << std::endl;
prints out
100
0
500 400
7.5 Pointers / References to Objects
Pointers or references can provide shared access to objects. Assume that a bank account is shared by two people. With pointers:
//ap1 points to an account object on heap
*ap1 = new Account(300);
Account //ap2 points to the same object
*ap2 = ap1; Account
Visually this looks like:
We can access this objects methods via its pointer:
(*ap1).withdraw();
Equivalent, and a more common way:
->withdraw(10); ap1
Which can be understood as: “follow the pointer ap1 to the object it follows and access the method”.
The changes will be reflected of course via the pointer ap2
:
->get_balance()
ap1->get_balance()
ap2//both return the same value of 290
Same can be achieved with references:
(20);
Account a&b = a;
Account
//both return 20
.get_balance();
a.get_balance();
b
//withdraw 5 from a
.withdraw(5);
a
/*changes reflected in both,
both return 15: */
.get_balance()
a.get_balance() b
7.6 Composition / Aggregation
In real world objects are usually composed of other objects/components. A car has chasis, tires, engine etc. In oop this is called composition or aggregation.
The compisition is classified based on the arity of objects that are aggregated and the nature of the association.
The nature of association is divided into two classes:
- strong association/strict aggregation.
- weak association.
First we consider strong association/strict aggregation
Strict Aggregation
In strict aggregation an object is concretely made up of its component objects, and its integrity and lifetime depends on them. A car is strictly composed of four tires and can not exist without the tires.
Strict aggregation itself is categorized depending on the number of objects aggregated:
- Fixed number composition: A car has exactly four tires, or exactly one engine.
- Arbitrary number composition: An order consists of at least one and at most arbitary number of items.
In UML notation:
Fixed Number Aggregation
We first consider the car-engine situation, where a car is composed exactly of one engine. (We ignore all other components that might make up a car for the sake of brevity).
The interface is illustrated in the following UML diagram:
Note that we provide two overloaded constructors: one that receives data members that need to initialize engine component as parameters, and another that receives and engine object as parameter and copies those engines data attributes to the corresponding data attributes in the car object.
c++ implementation with Engine engine
data member:
- the interface:
car.hh
#ifndef CAR_H
#define CAR_H
#include <string>
class Engine
{
public:
(std::string manufacturer, int horse_power);
Engineint get_hp();
std::string get_brand();
void display();
private:
std::string manufacturer;
int horse_power;
};
class Car
{
public:
(std::string brand,
Carstd::string type,
std::string engine_manufacturer,
int horse_power);
(std::string brand,
Carstd::string type,
);
Engine enginestd::string get_brand();
std::string get_type();
int get_hp();
std::string get_engine();
void display();
private:
;
Engine enginestd::string brand;
std::string type;
};
#endif // !CAR_H
- The implementatin of the classes:
car.cc
#include "car.hh"
#include <iostream>
::Engine(std::string _brand, int _hp) :
Engine(_brand),
manufacturer(_hp) {}
horse_power
std::string Engine::get_brand() {return manufacturer;}
int Engine::get_hp() {return horse_power;}
void Engine::display()
{
std::cout << "manufacturer: " << get_brand() << std::endl
<< "horse power: " << get_hp() << std::endl;
}
::Car(std::string _brand,
Carstd::string _type,
std::string engine_manufacturer,
int hp) :
(_brand),
brand(_type),
type(engine_manufacturer,
engine) {}
hp
::Car(std::string _brand,
Carstd::string _type,
) :
Engine _engine(_brand),
brand(_type),
type(_engine) {};
engine
std::string Car::get_brand(){return brand;}
std::string Car::get_type() {return type;}
std::string Car::get_engine() {return engine.get_brand();}
int Car::get_hp() {return engine.get_hp();}
void Car::display()
{
std::cout << "brand: " << get_brand() << std::endl
<< "type: " << get_type() << std::endl
<< " engine: " << std::endl;
.display();
engine}
- we test these classes:
test_car.cc
#include "car.hh"
#include <iostream>
int main(int argc, char const *argv[])
{
("Audi", "sport", "Volkswagen", 220);
Car c1.display();
c1std::cout << std::endl;
("Toyota", 135);
Engine e("Honda", "family", e);
Car c2.display();
c2std::cout << std::endl;
return 0;
}
output:
brand: Audi
type: sport
engine:
manufacturer: Volkswagen
horse power: 220
brand: Honda
type: family
engine:
manufacturer: Toyota horse power: 135
- We used
Engine
display()
function in the implementation ofCar
‘sdisplay()
function. Thus utilizing ’code-reuse’ principle of OOP in the context of aggregation. - We created two car objects, once with each of the overloaded constructors. Second constructor used an existing engine object to copy the data members of that engine to the corresponding data members in the car object.
Arbitrary Number Aggregation
Now consider the second case where an order has at least one item, but can have arbitrary many items.
This situation is demonstrated with the following UML diagram:
Note the overloaded constructors for Order
.
The c++ implementation with std::vector<Item> items
data member:
- the interface:
order.hh
#ifndef ORDR_H
#define ORDR_H
#include <string>
#include <vector>
class Item
{
public:
(std::string id,
Itemstd::string description,
double price);
std::string get_id();
std::string get_description();
double get_price();
void display();
private:
std::string id;
std::string description;
double price;
};
class Order
{
public:
(std::string order_id,
Orderstd::string customer_name,
std::string item_id,
std::string item_desc,
double item_price);
(std::string id,
Orderstd::string customer_name,
);
Item item
std::string get_order_id();
std::string get_customer_name();
double get_total_price() const;
int get_item_count() const;
void add_item(Item item);
void display() const;
private:
std::string id;
std::string customer_name;
std::vector<Item> items;
};
#endif // !ORDR_H
- the implementation:
order.cc
#include "order.hh"
#include <iostream>
::Item(std::string _id,
Itemstd::string _desc,
double _price) :
(_id),
id(_desc),
description(_price) {}
price
std::string Item::get_id() {return id;}
std::string Item::get_description() {return description;}
double Item::get_price() {return price;}
void Item::display()
{
std::cout << "id: " << id << std::endl
<< "description: " << description << std::endl
<< "price: " << price << std::endl;
}
::Order(std::string order_id,
Orderstd::string _customer_name,
std::string item_id,
std::string item_desc,
double item_price) :
(order_id),
id(_customer_name)
customer_name{
.push_back({item_id, item_desc, item_price});
items}
::Order(std::string _id,
Orderstd::string _customer_name,
) :
Item item
(_id),
id(_customer_name)
customer_name{
.push_back(item);
items}
std::string Order::get_order_id() {return id;}
std::string Order::get_customer_name() {return customer_name;}
double Order::get_total_price() const
{
double sum = 0;
for (auto it : items) {
+= it.get_price();
sum }
return sum;
}
int Order::get_item_count() const
{
return items.size();
}
void Order::add_item(Item item)
{
.push_back(item);
items}
void Order::display() const
{
std::cout << "order id: " << id << std::endl
<< "customer name: " << customer_name << std::endl
<< " Items: " << std::endl << std::endl;
for (auto it : items) {
.display();
itstd::cout << std::endl;
}
std::cout << "item count: " << get_item_count() << std::endl;
std::cout << "total price: " << get_total_price() << std::endl;
}
- testing:
test_order.cc
#include "order.hh"
#include <iostream>
int main(int argc, char const *argv[])
{
(
Order o1"1",
"bob",
"123",
"water",
0.95
);
.display();
o1(
Item i1"2",
"bread",
1.25
);
.add_item(i1);
o1std::cout << std::endl;
.display();
o1
(
Item i2"3",
"cola",
2.5
);
(
Order o2"3",
"alice",
i2);
std::cout << std::endl;
.display();
o2
std::cout << std::endl;
.display();
i1}
Finally we compile the test with the command
g++ -o test_order test_order.cc order.cc
to obtain the output:
order id: 1
customer name: bob
Items:
id: 123
description: water
price: 0.95
item count: 1
total price: 0.95
order id: 1
customer name: bob
Items:
id: 123
description: water
price: 0.95
id: 2
description: bread
price: 1.25
item count: 2
total price: 2.2
order id: 3
customer name: alice
Items:
id: 3
description: cola
price: 2.5
item count: 1
total price: 2.5
id: 2
description: bread price: 1.25
We created two orders, demonstrating each one of the overloaded constructors, by passing an existing item object to the second order objects constructor as argument.
We demonstrated the display()
function both of the Order
and of Item
objects.
Note that as before, display()
function from Order
class utilizes code-reuse by using display()
from the Item
object it aggregates.
The central notion of this chapter is arbitrary number aggregation. An order may have an arbitrary number of items. In c++ it is very common to realize such a relationship with the std::vector<>
class. We defined an std::vector<Item> items
attribute as a data member of the Order
class. Then items
simply aggregates Item
objects during the lifetime of a given Order
object. Since a c++ vector is a dynamic array and therefore can grow arbitrarily, this suitable reflects our sitation.
The constraint that an order must have at least on item is realized by our constructors: there is no defualt constructor, the constructors that we defined initialize an Order
object with one Item
object. Thus when an ordes is created, it always has one item. Afterwards new items may be added with the add_item()
method
To implement arbitrary number aggregation use std::vector<>
as a data member. E.g. if A
aggregates arbitrary number of B
s use std::vector<B> collection_of_Bs
as a data member of A
Weak Association
…