Hazards of dynamic memory allocation.
Dangling pointers and multiple deallocations.
A dangling pointer is a pointer variable that still contains the address to the free store memory that has already been deallocated using delete
or delete[]
. Dereferencing a dangling pointer makes you read from, or even worse write to memory that might already be allocated to and used by other parts of the program, resulting in all kinds of unpredictable results.
Multiple deallocations which occur when you deallocate an already deallocated memory (and hence dangling) pointer for a second time is a recipe for disaster.
One basic strategy to guard yourself against dangling pointers is to always reset a pointer to nullptr
, after the memory it points to is released. However, in more complex programs, different parts of the code often collaborate by accessing the same memory - an object or an array of objects - all through distinct copies of the same pointer. In such cases, our simple strategy falls short. Which part of the code is going to call delete
/delete[]
? And when? How do you ensure that no other part of the code is still using the same dynamically allocated memory.
Allocation/Deallocation mismatch.
A dynamically allocated array, allocated using new[]
, is captured in a regular pointer cariable. But, so is a single allocated value that is allocated using new
.
double* single_df {new double {0.95}};
double* array_of_dfs {new double[3] {1.00, 0.95, 0.90}};
After this the compiler has no way to distinguish between the two, especially once such a pointer gets passed around different parts of the program. This means that the following two statements will compile without error.
delete[] single_df;
delete array_of_dfs;
Every new
must be paired with a single delete
; every new[]
must be paired with a single delete[]
.
Memory Leaks.
A memory leak occurs when you allocate memory using new
or new[]
and fail to release it. If you lose the address of free store memory you have allocated by overwriting the address in the pointer you were using to access it, for instance, you have a memory leak.
When it comes to scope, pointers are just like any other variable. The lifetime of a pointer extends from the point at which you define it in a block to the closing brace of the block. After that it no longer exists, the free store goes out of scope and it’s no longer possible to delete the memory.
It’s still relatively easy to see, where you’ve simply forgotten to use delete
to free memory when use of the memory ceases at a point close to where you allocated it, but you’d be surprised how often programmers make mistakes like this, especially if, for instance, return
statements creep in between allocation and deallocation of your variable. And naturally, memory leaks are even more difficult to spot in complex programs, where memory may be allocated in part of the the program and should be released in a completely separate part.
One basic strategy for avoiding memory leaks is to immediately add delete
operation at an appropriate place each time you use the new
operator. But this strategy by no means is fail-safe. Even C++ programmers are fallible creatures.
Fragmentation of the Free-Store.
Memory fragmentation can arise in programs that frequently dynamically allocate and release memory blocks. Each time, the new
operator is used, it allocates a contiguous block of bytes. If you create and destroy many memory blocks of different sizes, it’s possible to arrive at a situation in which the allocated memory is interspersed with small blocks of free memory, none of which is large enough to accomodate a new memory allocation request by your program. The aggregate of the free memory can be quite large, but if all the individual blocks are small (smaller than a current allocation request), the allocation request will fail.
Golden rule of dynamic memory allocation.
Never use the operators new
, new[]
, delete
and delete[]
directly in day-to-day coding. These operators have no place in modern C++ code. Always use either the std::vector<T>
container to replace dynamic arrays or a smart pointer to dynamically allocate individual objects and manage their lifetimes.
Raw pointers and Smart Pointers.
Pointer types int*
, double*
are referred to as raw pointers because variables of these types contain nothing more than an address. A raw pointer can store the address of an automatic variable or a memory-block allocated in the free-store.
A smart pointer is an object that mimics a raw pointer in that, it contains an address, and you can use it in the same way in many respects. Smart pointers are normally used only to store the address of memory allocated in the free store. A smart pointer does much more than a raw pointer, though. The most notable feature of a smart pointer, is that we don’t have to worry about using the delete
or delete[]
operator to free memory. It will be released automatically, when it is no longer needed. This means that dangling pointers and multiple deallocations, allocation/deallocation mismatches and memory leaks will no longer be possible.
A
std::unique_ptr<T>
object behaves as a pointer to typeT
and is unique in the sense that there can be only one singleunique_ptr<>
object containing the same address. In other words, there can never be two or moreunique_ptr<T>
objects pointing to the same memory address at the same time. Aunique_ptr<>
object is said to own the object it points to exclusively. The uniqueness is enforced by the fact, that a compiler will never allow you to copy aunique_ptr<>
.A
std::shared_ptr<T>
object also behaves as a pointer to typeT
, but in contrast withunique_ptr<T>
there can be any number ofshared_ptr<>
objects that allow shared ownership of an object in the free-store. At any given moment, the number ofshared_ptr<>
objects that contain a given address in time is known by the runtime. This is called reference counting. The reference count for ashared_ptr<>
containing a given free store address is incremented each time a newshared_ptr
object is creating containing that address, and its decremented when ashared_ptr
containing the address is destroyed or assigned to point to a different address. When there are noshared_ptr
objects containing a given address, the reference count will have dropped to zero, and the memory for the object at that address is released automatically. Allshared_ptr<>
objects that point to the same address have access to the the count of how many there are.A
weak_ptr<T>
is linked to ashared_ptr<T>
and contains the same address. Creating aweak_ptr<>
does not increment the reference count associated with the linkedshared_ptr<>
object, though, so aweak_ptr<>
does not prevent the object pointed to from being destroyed. Its memory will still be released when the lastshared_ptr<>
referencing it is destroyed or reassigned to point to a different address, even when associatedweak_ptr<>
objects still exist. If this happens, theweak_ptr<>
will nevertheless not contain a dangling pointer, atleast not one that you could inadvertently access. The reason is that you cannot access the address encapsulated by aweak_ptr<T>
directly. The compiler forces you to first create ashared_ptr<T>
object out of it that refers to the same address. If the memory address for theweak_ptr<>
is still valid, forcing you to create ashared_ptr<>
first ensures that the reference count is again incremented and that the pointer can be used safely again. If the memory is released already, however, this operation will result in ashared_ptr<T>
containing anullptr
.
One use for having weak_ptr<>
objects is to avoid so called reference cycles with shared_ptr<>
objects. Conceptually, a reference cycle is where a shared_ptr<Y>
inside the object x
points to some other object y
that contains a shared_ptr<X>
, which points back to x
. With this situation, neither x
nor y
can be destroyed. In practice, this may occur in many ways. weak_ptr
allows you to break such cycles. Another use of weak pointers is in the implementation of object caches.
In the below code snippet, the destructors ~A()
and ~B()
are not invoked even when the objects shrd_a
and shrd_b
go out of scope.
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A{
public:
<B> m_b;
shared_ptr() {cout << "\nA()";}
A~A() {cout << "\n~A()";}
};
class B{
public:
<A> m_a;
shared_ptr() {cout << "\nB()";}
B ~B() {cout << "\n~B()";}
};
int main()
{
{
<A> shrd_a {make_shared<A>()}; //A's ref count = 1
shared_ptr<B> shrd_b {make_shared<B>()}; //B's ref count = 1
shared_ptr
->m_b = shrd_b; //B's ref count = 2
shrd_a->m_a = shrd_a; //A's ref count = 2
shrd_b}
//shrd_a and shrd_b go out of scope and are destroyed
// A's ref count = 1
// B's ref count = 1
// ((Memory of A, B is deallocated only when ref count drops to 0))
return 0;
}
A()
B()
To solve it, the programmer needs to be aware of the ownership relationship among the objects, or needs to invent an ownership relationship, if no such ownership exists. The above C++ code can be changed so that A owns B:
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A{
public:
<B> m_b;
shared_ptr() {cout << "\nA()";}
A~A() {cout << "\n~A()";}
};
class B{
public:
<A> m_a;
weak_ptr() {cout << "\nB()";}
B ~B() {cout << "\n~B()";}
};
int main()
{
{
<A> shrd_a {make_shared<A>()}; //A's ref count = 1
shared_ptr<B> shrd_b {make_shared<B>()}; //B's ref count = 1
shared_ptr
->m_b = shrd_b; //B's ref count = 2
shrd_a->m_a = shrd_a; //A's ref count = 1
shrd_b}
//shrd_a and shrd_b go out of scope and are destroyed
// A's ref count = 0
// B's ref count = 1
// A is destroyed
// B's ref count = 0
// B is destroyed
//
return 0;
}
A()
B()
~A()
~B()
References.
A reference is a name that you can use as an alias for another variable. Unlike a pointer, you cannot declare a reference and not initialize it. Because a reference is an alias, the variable which it is an alias must be provided when the reference is initialized. Also, a reference cannot be modified to be an alias for something else.
#include <iostream>
#include <memory>
void swap(int& a, int& b)
{
int temp {a};
= b;
a = temp;
b }
int main()
{
int x {10};
int y {15};
std::cout << "\n Before swap:";
std::cout << "\n x = " << x ;
std::cout << "\n y = " << y;
(x,y);
swap
std::cout << "\n After swap:";
std::cout << "\n x = " << x ;
std::cout << "\n y = " << y;
return 0;
}
Before swap:
x = 10
y = 15
After swap:
x = 15
y = 10
Never return a pointer or reference to an automatic stack-allocated local variable from within a function. Automatic variables are destroyed and the stack is popped, once the control goes outside the scope in which they are declared.
Function Templates.
A function template itself is not a definition of a function; it is a blueprint or a recipe for definining an entire family of functions. A function template is a parametric function definition, where a particular function instance is created by one or more parameter values. The compiler uses a function template to generate a function definition when necessary. If it is never necessary, no code results from the template. A function definition that is generated from a template is an instance or instantiation of the template.
The parameters of a function template are usually data-types, where an instance can be generated for a parameter value of type int
, for example, and another with parameter valuer of type string
. But parameters are not necessarily types. They can be other things such as a dimension, for example.
template <class T>
(T a, T b)
T larger{
return a > b ? a : b;
}
The compiler creates instances of the template from any statement that uses the larger()
function. Here’s an example:
int main()
{
std::cout << "\nLarger of 1.50 and 2.50 is : " << larger(1.5,2.5);
return 0;
}
Larger of 1.50 and 2.50 is : 2.5
You just use the function in the normal way. You don’t need to specify a value for the template parameter T
. The compiler deduces the type that is to replace T
from the arguments in the larger
function call. This mechanism is referred to as template argument deduction. The arguments to larger()
are literals of type double
, so this call causes the compiler to search for an existing definition of larger()
with double
parameters. If it doesn’t find one, the compiler creates this version of larger()
from the template by susbstituting double
for T
in the template definition.
The resulting function accepts arguments of type double
and returs a double
value.
The compiler makes sure to generate each template instance only once. If a subsequent function call requires the same instance, then it calls the instance that exists.
Template type parameters.
The name of the template type parameter can be used anywhere in the template’s function signature, return type and body. It is a placeholder for a type and can thus be put in any context you would normally put a concrete type.
template <class T>
const T& larger(const T& a,const T& b)
{
return a > b ? a : b;
}
Function Template overloading.
Templated functions can be overloaded.
#include <iostream>
#include <vector>
template <typename T>
const T& largest(const T& a,const T& b)
{
return a > b ? a : b;
}
template <typename T>
const T largest(const std::vector<T>& data)
{
{};
T max for(auto v:data)
{
if (v >= max)
= v;
max }
return max;
}
int main()
{
std::cout << "\nLarger of 1.50 and 2.50 is : " << largest(1.5,2.5);
std::vector<int> data {
2, 5, 8, 4, 7, 3
};
std::cout << "\nLargest of [2,5,8,4,7,3] is : " << largest(data);
return 0;
}
Larger of 1.50 and 2.50 is : 2.5
Largest of [2,5,8,4,7,3] is : 8
Classes and Object Oriented Programming.
An interesting exercise to write a Matrix<T>
class.
// Matrix.h
#include <iostream>
#include <vector>
#include <initializer_list>
#include <stdexcept>
template <typename T = double>
class Matrix {
public:
//Default constructor
() : Matrix(3, 3) {}
Matrix
//Parameterized constructor with number of rows, cols as
// as arguments.
(std::size_t m, std::size_t n) : m_rows(m), m_cols(n)
Matrix{
m_data.resize(m_rows * m_cols, 0);
}
//Parameterized constructor with matrix elements provided
// in brace initializer lists.
(std::initializer_list<std::initializer_list<T>> m) {
Matrixint i{}, j{};
for (auto row : m)
{
for (auto el : row)
{
m_data.push_back(el);
if (i == 0)
++j;
}
++i;
}
m_rows = i;
m_cols = j;
}
//Copy constructor
(const Matrix& A) : m_rows{ A.m_rows }, m_cols{ A.m_cols }, m_data{ A.m_data } {}
Matrix
std::size_t rows() const
{
return m_rows;
}
std::size_t cols() const
{
return m_cols;
}
& at(int i, int j)
T{
return m_data[i * m_cols + j];
}
const T& at(int i, int j) const
{
return m_data[i * m_cols + j];
}
& operator()(int i, int j)
T{
if (i < 0)
throw std::invalid_argument("The row index must be non-negative!");
if (j < 0)
throw std::invalid_argument("The column index must be non-negative!");
if (i >= m_rows)
throw std::invalid_argument("The row index must be less than " + m_rows);
if (j >= m_cols)
throw std::invalid_argument("The col index must be less than " + m_cols);
return at(i, j);
}
const T operator()(int i, int j) const
{
if (i < 0)
throw std::invalid_argument("The row index must be non-negative!");
if (j < 0)
throw std::invalid_argument("The column index must be non-negative!");
if (i >= m_rows)
throw std::invalid_argument("The row index must be less than " + m_rows);
if (j >= m_cols)
throw std::invalid_argument("The col index must be less than " + m_cols);
return at(i, j);
}
const Matrix operator+(const Matrix& mat)
{
if (mat.rows() != rows())
throw std::runtime_error("In A + B, matrices A, B should have the same number of rows!");
if (mat.cols() != cols())
throw std::runtime_error("In A + B, matrices A, B should have the same number of cols!");
(rows(), cols());
Matrix result
for (int i{}; i < rows(); ++i)
{
for (int j{}; j < cols(); ++j)
{
(i, j) = at(i, j) + mat(i, j);
result}
}
return result;
}
const Matrix operator-(const Matrix& mat)
{
if (mat.rows() != rows())
throw std::runtime_error("In A - B, matrices A, B should have the same number of rows!");
if (mat.cols() != cols())
throw std::runtime_error("In A - B, matrices A, B should have the same number of cols!");
(rows(), cols());
Matrix result
for (int i{}; i < rows(); ++i)
{
for (int j{}; j < cols(); ++j)
{
(i, j) = at(i, j) - mat(i, j);
result}
}
return result;
}
& operator=(const Matrix& mat)
Matrix{
m_data = mat.m_data;
m_rows = mat.rows();
m_cols = mat.cols();
return *this;
}
const Matrix operator*(const Matrix& mat)
{
if (cols() != mat.rows())
throw std::runtime_error("In A * B, cols of A must equal rows of B!");
{ rows(), mat.cols() };
Matrix result
for (int i{}; i < rows(); ++i)
{
for (int k{}; k < cols(); ++k)
{
for (int j{}; j < mat.cols(); ++j)
{
(i, j) += at(i, k) * mat(k, j);
result}
}
}
return result;
}
private:
std::vector<T> m_data{};
int m_rows;
int m_cols;
};
//Matrix.cpp
#include <iostream>
#include "Matrix.h"
int main()
{
<double> A{
Matrix{1, 0},
{0, 1}
};
<double> B{
Matrix{1, 0},
{0, 1}
};
<double> result = A + B;
Matrix
std::cout << result(0, 0) << "\t" << result(0, 1) << "\n";
std::cout << result(1, 0) << "\t" << result(1, 1);
return 0;
}
Access specifiers and class hierarchies.
The private members of the base class are inaccessible to the derived class.
When the base class specifier is
public
, the access status of the inherited members remains unchanged. Thus, inheritedpublic
members arepublic
, and inheritedprotected
members areprotected
in a derived class.When the base class specifier is
protected
, both public and protected members of the base class are inherited asprotected
members in the child class.When the base class specifier is
private
, inheritedpublic
andprotected
members becomeprivate
to the derived class, so that they’re accessible by member functions of the the derived class, but they cannot be accessed if they’re inherited in another derived class.
Constructors and Destructors in derived classes.
Every constructor of the derived class always starts by invoking a constructor of the base class. And that base class constructor then invokes the constructor of its base class, and so on.
Remark. You cannot initialize the member variables of a base class in the initialization list for the derived class constructor. Not even if those members are public or protected.
#include <iostream>
#include <vector>
#include <string>
class A{
public:
(){
Astd::cout << "\nInside A's constructor";
}
~A()
{
std::cout << "\nInside A's destructor";
}
};
class B : public A
{
public:
()
B{
std::cout << "\nInside B's constructor";
}
~B()
{
std::cout << "\nInside B's destructor";
}
};
int main()
{
;
B b
return 0;
}
Inside A's constructor
Inside B's constructor
Inside B's destructor
Inside A's destructor
Suppose you have a base class Parent
, two child classes Child_1
and Child_2
that inherit from Parent
and a Grandchild
class that inherits from Child_1
and Child_2
. This is the diamond problem, named after the shape of such inheritance diagrams. The Grandchild
inherits two copies of Parent
: one through Child_1
and another through Child_2
.
To prevent the duplication of the base class, we identify to the compiler that the base class should appear only once within the derived class. We do this by specifying the class as a virtual base class using the virtual
keword. The Child_1
and Child_2
classes would be defined like this:
class Child_1 : public virtual Parent
{
//...
};
class Child_2 : public virtual Parent
{
//...
};
Polymorphism.
Every derived class object is a base class object. So, you can use a base class pointer/reference to store the address of a derived class object. It is easy to implement dynamic dispatch through virtual methods.
The below code snippet is instructive in understanding run-time polymorphism.
#include <memory>
#include <iostream>
using namespace std;
class A {
public:
void foo() {
std::cout << "\nGreetings from a!";
}
};
class B :public A {
public:
virtual void foo()
{
std::cout << "\nGreetings from b!";
}
};
class C : public B {
private:
virtual void foo()
{
std::cout << "\nGreetings from c!";
}
};
class D : public C {
public:
void foo()
{
std::cout << "\nGreetings from d!";
}
};
int main()
{
std::shared_ptr<A> a_ptr = std::make_shared<D>();
->foo();
a_ptr
std::shared_ptr<B> b_ptr = std::make_shared<D>();
->foo();
b_ptr
std::shared_ptr<C> c_ptr = std::make_shared<D>();
//c_ptr ->foo(); //will not compile, foo() is a private member is not inherited by D
std::shared_ptr<D> d_ptr = std::make_shared<D>();
->foo();
d_ptr }
Greetings from a!
Greetings from d!
Greetings from d!
When you specify a function as virtual
in a base class, you indicate to the compiler that you want dynamic binding for function calls in any class that’s derived from this base class. A function that you specify as virtual
in the base class will be virtual
in all classes that directly or indirectly derive from the base class. This is the case, whether or not you specify the function as virtual
in the derived class.
The call to a virtual function using an object is always resolved statically. You only get dybamic resolution of calls to virtual functions through a pointer or a reference. Consider the below code snippet:
{};
D d
& aRef = d;
A& bRef = d;
B; B b;
A a
.foo();
aRef.foo();
bRef
.foo();
a.foo(); b
Greetings from a!
Greetings from d!
Greetings from a!
Greetings from b!
Requirements for a virtual function.
For a function to be virtual
, its definition in a derived class must have the same signature as it has in the base class. If the base class function is const
, for instance, then the derive class function must also be const
. Generally, the return type of a virtual function in a derived class must be the same as in the base class as well, but there’s an exception when the return type in the base class is a pointer or a reference to a class type. In this case, the derived class version of a virtual function may return a pointer or a reference to a more specialized type than that of the base. This is called covariance.
Another restriction is that a virtual function can’t be a template function.
In standard object-oriented programming terms, a function in a derived class that redefines a function of the base class is said to override this function. A function with the same name as a virtual function in a base class only overrides that function if the remainder of their signatures match exactly as well; if they do not, the function in the derived class is a new function that hides the one in the base class. This means that if you try to use different parameters for a virtual function in a derived class or use different const
specifiers, then the virtual function mechanism won’t work. The function in the derived class then defines, a new different function - and this new function will therefore operate with static binding that is established and fixed at compile time.
override
specifier.
The override
specification guarantees that you don’t make mistakes in function overrides and these exactly match the virtual function signatures in base class.
final
qualifier.
Sometimes, we may want to prevent a member function from being overriden in a derived class. We can do this by specifying that a function is final
.
Virtual destructors.
Along with the other function, the destructor methods of classes should also be resolved dynamically. That is, if a Base*
pointer points to Derived
object, the Derived
class destructor method should be called first. (Object creation is top-down, destruction is bottom-up in an inheritance hierarchy). So, it’s always prudent to declare destructor methods as virtual
.
Calling the base class version of a virtual function.
It’s easy to call the derived class version of a virtual function through a pointer or reference to a derived class object - the call is made dynamically. However, what do you do when you actually want to call the base class function for a derived class object?
Consider the Box
and ToughPack
classes.
#include <iostream>
#include <memory>
#include <string>
class Box{
public:
() : Box(1.0) {}
Box(double side) : Box(side, side, side) {}
Box(double length, double width, double height) : m_length(length), m_width(width), m_height(height) {}
Box
double virtual volume()
{
return m_length * m_width * m_height;
}
~Box()
{
std::cout << "\nBox dtor";
}
protected:
double m_length;
double m_width;
double m_height;
};
class ToughPack : public Box
{
public:
() : Box() {}
ToughPack(double side) : Box(side) {}
ToughPack(double x, double y, double z) : Box(x,y,z) {}
ToughPack
//Function to calculate volume allowing for 15% of packing
double volume() override
{
return 0.85 * m_length * m_width * m_height ;
}
~ToughPack()
{
std::cout << "\nToughPack dtor";
}
};
In ToughPack
’s volume()
method, the m_length*m_width*m_height
part of the return
statement is exactly the formula used to compute the volume()
inside the base class Box
. In this case, the amount of code we had to retype was limited, but this won’t always be the case. It would therefore be much better if you could simply call the base class version of this function isntead.
A plausible first attempt to do so would be:
double volume() const override
{
return 0.85 * volume(); // Infinite recursion!
}
However, this would call volume()
override itself, which would then be calling itself again, which would then be calling itself again! This leads to infinite recursion and a crash.
Calling the base class version from within a function override like this is common. The solution is to explicitly ask the compiler to call the base class version of the function.
double volume() const override
{
return 0.85 * Box::volume();
}
When my base class’s constructor calls a virtual function on its this object, why doesn’t my derived class’s override of that virtual function get invoked?
What happens when we call virtual functions from inside constructors and destructors? Calling a polymorphic function from inside a constructor/desctructor is a recipe for disaster in most cases. It should be avoided whenver possible.
In a constructor, the virtual call mechanism is disabled, because overriding from derived classes hasn’t happened yet. Objects are constructed from Base
up, “Base before derived”.
Since Base
object must be constructed before Derived
, the call to f()
always resolves statically to Base::f()
from inside the constructor.
#include<string>
#include<iostream>
using namespace std;
class B {
public:
(const string& ss) { cout << "B constructor\n"; f(ss); }
Bvirtual void f(const string&) { cout << "B::f\n";}
};
class D : public B {
public:
(const string & ss) :B(ss) { cout << "D constructor\n";}
Dvoid f(const string& ss) { cout << "D::f\n"; s = ss; }
private:
;
string s};
int main()
{
("Hello");
D d}
B constructor
B::f
D constructor
How can I set up my class so it won’t be inherited from?
Just declare the class as final
.
Pure virtual functions.
There are situations where we require a base class with a virtual function that’s redefined in each of the derived classes, but hwere there’s no meaningful definition for the function in the base class. For example, you might define a base class Shape
, from which you derive classes definining specific shapes, such as Circle
, Ellipse
, Rectangle
, Hexagon
and so on. The Shape
class could include a virtual function area()
, that you’d call for the derived class object to compute the area of a particular shape. The Shape
class itself, though, cannot possibly provide a meaningful implementation of the area()
function, one that caters, for instance, to both Circle
s and Rectangle
s. This is a job for a pure virtual function.
The purpose of a pure virtual function is to enable the derived class versions of the function to be called polymorphically. To declare a pure virtual function rather than an ordinary virtual function that has a definition, you use the same syntax but add =0
to it’s declaration within the class.
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
() = default;
Shapevirtual double area() = 0; //pure virtual function
};
class Rectangle : public Shape {
public:
(double l, double w) : m_length(l), m_width(w) {}
Rectangle
double area() override {
return m_length * m_width;
}
private:
double m_length;
double m_width;
};
class Circle : public Shape {
public:
(double r) : m_radius(r) {}
Circle
double area() override {
return 3.14159 * m_radius * m_radius;
}
private:
double m_radius;
};
int main()
{
//Let's create a container to hold different kinds of shapes
std::vector<std::unique_ptr<Shape>> shapes{};
.push_back(std::make_unique<Rectangle>(5.0, 5.0));
shapes.push_back(std::make_unique<Circle>(3.0));
shapes.push_back(std::make_unique<Rectangle>(10.0, 12.0));
shapes.push_back(std::make_unique<Circle>(5.0));
shapes
for (int i{}; i < shapes.size(); ++i)
{
std::cout << "\nArea = " << shapes[i]->area();
}
return 0;
}
Area = 25
Area = 28.2743
Area = 120
Area = 78.5397
Abstract Classes.
An abstract class purely exists for the purpose of deriving classes from it and cannot be instantiated.
Any class that contains atleast one pure virtual function is an abstract class. Because an abstract class cannot be instantiated, you cannot pass it by value to a function, a parameter of type Shape
will not compile. Similarly, you cannot return a Shape
object from a functiojn. However, pointers or references to an abstract class can be used as parameter or return types, so types such as Shape*
std::shared_ptr<Shape>
and Shape&
are fine in these settings.
Any class that inherits from Shape
is obligated to provide an implementation of the area()
method. If it doesn’t, it too is an abstract class. More specifically, if any pure virtual function of an abstract base class isn’t in a derived class, then the pure virtual function will be inherited as such, and the derived class becomes an abstract class.
Thus, abstract base classes (ABCs) are often used as interfaces.