Abstract base class ABC

Ellipse class

Develop a graphics program to display circles and ellipses. A Circle is a special case of an Ellipse. The major axis and minor axis are ellipses of equal length. Therefore, all circles are ellipses, and the Circle class can be derived from the Ellipse class. The data members include the coordinates of the Ellipse center, semi major axis, short semi axis and direction angle. It also includes some methods to move the Ellipse, return the area of the Ellipse, rotate the Ellipse, and scale the long and short semi axes.

class Ellipse
{
private:
	double x; //x coordinate of ellipse center
	double y; //y coordinate of ellipse center
	double a; //Semimajor axis
	double b; //Short half axis
	double angle; //Direction angle
	...

public:
	void Move(int nx, int ny) { x = nx; y = ny; }
	virtual double Area() const { return 3.14159 * a * b; }
	virtual void Rotable(double nang) { angle += nang; }
	virtual void Scale(double sa, double sb) { a *= sa; b *= sb; }
	...
};

Now derive a Circle class from the Ellipse class:

class Circle : public Ellipse
{
	...
};

Although a Circle is an ellipse, this derivation is clumsy. For example, a Circle needs only one value (radius) to describe its size and shape, and needs to have a long semi axis and a short semi axis. The Circle constructor can take care of this situation by assigning the same value to members a and b, but this will lead to information redundancy. The angle parameter and the Rotate() method have no practical meaning for circles; The Scale() method will scale the two axes differently and turn the Circle into an ellipse. Some techniques can be used to fix these problems, such as including the redefined Rotate() method in the private part of the Circle class, so that Rotate() cannot be used for circles publicly. In general, however, inheritance cannot be used. It is simpler to define the Circle class directly:

class Circle : public Ellipse
{
private:
	double x;
	double y;
	double r;
	...

public:
	...
	void Move(int nx, int ny) { x = nx; y = ny; }
	double Area() const { return 3.14159 * r * r; }
	void Scale(double sr) { r *= sr; }
	...
};

Class contains only the required members. But this solution is not efficient. The Circl and Ellipse classes have a lot in common. Defining them separately ignores this fact.

Another solution is to abstract their commonalities from the Ellipse and Circle classes and put these features into an ABC. The Circle and Ellipse classes are then derived from this ABC. You can use the base class pointer array to manage both Circle and Ellipse objects, that is, you can use polymorphic methods. What these two classes have in common are the central coordinates, the Move() method (which is the same for the two classes), and the Area() method (which is different for the two classes). You can't even implement the Area() method in ABC because it doesn't contain the necessary data members.

C + + provides unimplemented functions by using pure virtual functions. The declaration of a pure virtual function ends with = 0.

class BaseEllipse //Abstract base class
{
private:
	double x;
	double y;
	...

public:
	BaseEllipse(double x0 = 0, double y0 = 0) : x(x0),y(y0) {}
	virtual ~BaseEllipse() {}
	void Move(int nx, int ny) { x = nx; y = ny; }
	virtual double Area() const = 0; //Pure virtual function
	...
};

When a class declaration contains a pure virtual function, you cannot create an object of the class. Classes containing pure virtual functions are used only as base classes. To be a true ABC, you must include at least one pure virtual function. In principle, = 0 makes the virtual function a pure virtual function. The method Area() here is not defined, but C + + even allows pure virtual functions to be defined.

For example, all base class methods, like Move(), can be defined in the base class, but this class still needs to be declared abstract. In this case, you can declare the prototype as virtual:

void Move(int nx, int ny) = 0;

This will make the base class abstract, but you can still implement the definition of the method provided in the file:

void BaseEllipse::Move(int nx, int ny) { x = nx; y = ny; }

Use = 0 in the prototype to indicate that the class is an abstract base class, and the function can not be defined in the class.

You can derive the Ellipse class and the Circle class from the BaseEllipse class, and add the required members to complete each class. Note that the Circle class always represents a Circle, while the Ellipse class always represents an Ellipse. However, the Ellipse class Circle can be rescaled to a non Circle, while the Ciecle class must always be a Circle.

Programs of these classes can create Ellipse objects and Circle objects, but cannot create BaseEllipse objects. Since the base classes of Circle and Ellipse objects are the same, you can use BaseEllipse pointer array to manage both objects at the same time. Classes like Circle and Ellipse are sometimes referred to as concrete classes, which means that these types of objects can be created.

ABC describes an interface that uses at least one pure virtual function. Classes derived from ABC will use conventional virtual functions to implement this interface according to the specific characteristics of the derived classes.

ABC

First, define an ABC named AcctABC. This class contains all methods and data members common to Brass and Brass plus classes, and those methods that behave differently in Brass plus and Brass classes should be declared as virtual functions. At least one virtual function should be a pure virtual function, so that AcctABC can become an abstract class.

acctabc.h

To help derived classes access base class data, AcctABC provides some protection methods; Derived class methods can call these methods, but they are not part of the public interface of the derived class object. AcctABC provides a protected member function to handle formatting. In addition, the AcctABC class has two pure virtual functions, so it is indeed an abstract class.

#ifndef ACCTABC_H_
#define ACCTABC_H_
#include <iostream>
#include <string>

//Abstract base class
class AcctABC
{
private:
	std::string fullName;
	long acctNum;
	double balance;

protected:
	struct Formatting
	{
		std::ios_base::fmtflags flag;
		std::streamsize pr;
	};
	const std::string & FullName() const { return fullName; }
	long AcctNum() const { return acctNum; }
	Formatting SetFormat() const;
	void Restore(Formatting & f) const;

public:
	AcctABC(const std::string & s = "Nullbody", long an = -1, double bal = 0.0);
	void Deposit(double amt);
	virtual void Withdraw(double amt) = 0;
	double Balance() const { return balance; };
	virtual void ViewAcct() const = 0;
	virtual ~AcctABC() {}
};

//Brass Account class
class Brass :public AcctABC
{
public:
	Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0) : AcctABC(s, an, bal) {}
	virtual void Withdraw(double amt);
	virtual void ViewAcct() const;
	virtual ~Brass() {}
};

//Brass Plus Account class
class BrassPlus :public AcctABC
{
private:
	double maxLoan;
	double rate;
	double owesBank;

public:
	BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.10);
	BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);
	virtual void ViewAcct() const;
	virtual void Withdraw(double amt);
	void ResetMax(double m) { maxLoan = m; }
	void ResetRate(double r) { rate = r; }
	void ResetOwes() { owesBank = 0; }
};

#endif

acctabc.cpp

#include <iostream>
#include "acctabc.h"
using std::cout;
using std::ios_base;
using std::endl;
using std::string;

AcctABC::AcctABC(const std::string & s, long an, double bal)
{
	fullName = s;
	acctNum = an;
	balance = bal;
}

void AcctABC::Deposit(double amt)
{
	if (amt < 0)
		cout << "Negative deposit not allowed; "
		<< "deposit is cancelled.\n";
	else
		balance += amt;
}

void AcctABC::Withdraw(double amt)
{
	balance -= amt;
}

AcctABC::Formatting AcctABC::SetFormat() const
{
	Formatting f;
	f.flag = cout.setf(ios_base::fixed, ios_base::floatfield);
	f.pr = cout.precision(2);
	return f;
}

void AcctABC::Restore(Formatting & f) const
{
	cout.setf(f.flag, ios_base::floatfield);
	cout.precision(f.pr);
}

void Brass::Withdraw(double amt)
{
	if (amt < 0)
		cout << "Withdrawal amount must be positive; "
		<< "withdrawal canceled.\n";
	else if (amt <= Balance())
		AcctABC::Withdraw(amt);
	else
		cout << "Withdrawal amount of $" << amt
		<< " exceeds your balance.\n"
		<< "Withdrawal canceled.\n";
}

void Brass::ViewAcct() const
{
	Formatting f = SetFormat();
	cout << "Brass Client: " << FullName() << endl;
	cout << "Account Number: " << AcctNum() << endl;
	cout << "Balance: $" << Balance() << endl;
	Restore(f);
}

BrassPlus::BrassPlus(const string & s, long an, double bal, double ml, double r) : AcctABC(s, an, bal)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}

BrassPlus::BrassPlus(const Brass & ba, double ml, double r) : AcctABC(ba)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}

void BrassPlus::ViewAcct() const
{
	Formatting f = SetFormat();
	cout << "Brass Client: " << FullName() << endl;
	cout << "Account Number: " << AcctNum() << endl;
	cout << "Balance: $" << Balance() << endl;
	cout << "Maximum loan: $" << maxLoan << endl;
	cout << "Owed to bank: $" << owesBank << endl;
	cout.precision(3);
	cout << "Loan Rate: " << 100 * rate << "%\n";
	Restore(f);
	Restore(f);
}

void BrassPlus::Withdraw(double amt)
{
	Formatting f = SetFormat();
	double bal = Balance();
	if (amt <= bal)
		AcctABC::Withdraw(amt);
	else if (amt <= bal + maxLoan - owesBank)
	{
		double advance = amt - bal;
		owesBank += advance * (1.0 + rate);
		cout << "Bank advance: $" << advance << endl;
		cout << "Finance charge: $" << advance * rate << endl;
		Deposit(advance);
		AcctABC::Withdraw(amt);
	}
	else
		cout << "Credit limit exceeded. Transaction cancelled.\n";
	Restore(f);
}

The protection methods FullName() and AcctNum() provide read-only access to the data members fullName and acctNum, making it possible to further customize the ViewAcct() of each derived class.

After improving the output format, a structure is defined to store two format settings; And use this structure to set and restore the format, so only two function calls are required:

struct Formatting
{
	std::ios_base::fmtflags flag;
	std::streamsize pr;
};
....
Formatting f = SetFormat();
...
Restore(f);

These functions and structure Formatting are placed in a separate namespace, but they protect access rights, so these structures and functions are placed in the protection part of the class definition. This makes them available to base and derived classes while hiding them out.

main.cpp

#include <iostream>
#include <string>
#include "acctabc.h"

const int CLIENTS = 4;

int main()
{
	using std::cin;
	using std::cout;
	using std::endl;

	AcctABC * p_clients[CLIENTS];
	std::string temp;
	long tempnum;
	double tempbal;
	char kind;

	for (int i = 0; i < CLIENTS; i++)
	{
		cout << "Enter client's name: ";
		getline(cin, temp);
		cout << "Enter client's account number: ";
		cin >> tempnum;
		cout << "Enter opening balance: $";
		cin >> tempbal;
		cout << "Enter 1 for Brass Account or "
			<< "2 for BrassPlus Account: ";
		while (cin >> kind && (kind != '1' && kind != '2'))
			cout << "Enter either 1 or 2: ";
		if (kind == '1')
			p_clients[i] = new Brass(temp, tempnum, tempbal);
		else
		{
			double tmax, trate;
			cout << "Enter the overdraft limit: $";
			cin >> tmax;
			cout << "Enter the interest rate "
				<< "as a decimal fraction: ";
			cin >> trate;
			p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
		}
		while (cin.get() != '\n')
			continue;
	}
	cout << endl;
	for (int i = 0; i < CLIENTS; i++)
	{
		p_clients[i]->ViewAcct();
		cout << endl;
	}
	for (int i = 0; i < CLIENTS; i++)
	{
		delete p_clients[i];
	}
	cout << "Done.\n";
	return 0;
}

Keywords: C++

Added by joelg on Mon, 08 Nov 2021 05:31:05 +0200