Implementing a Basic Class with Static Members and Friend Functions
When designing a class in C++, it's essential to understand the lifecycle of objects, including how they are created, copied, moved, and destroyed. The following example demonstrates a Counter class that tracks the number of active instances using static members.
Header File (Counter.h)
#pragma once
#include <string>
class Counter {
public:
// Constructors and destructor
Counter(int val1 = 0, int val2 = 0);
Counter(const Counter& other);
Counter(Counter&& other) noexcept;
~Counter();
// Member functions
void scale(int factor);
void print() const;
// Static members
static int getActiveCount();
static const std::string description;
static const int maxInstances = 999;
private:
int value1, value2;
static int activeCount;
// Friend declaration
friend void demonstrateFriend();
};
void demonstrateFriend();
Implementation File (Counter.cpp)
#include "Counter.h"
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Initialize static members outside the class
const std::string Counter::description{"A demonstration class for object counting"};
int Counter::activeCount = 0;
// Default and parameterized constructor
Counter::Counter(int val1, int val2) : value1{val1}, value2{val2} {
++activeCount;
cout << "Counter constructor invoked.\n";
}
// Copy constructor
Counter::Counter(const Counter& other) : value1{other.value1}, value2{other.value2} {
++activeCount;
cout << "Counter copy constructor invoked.\n";
}
// Move constructor
Counter::Counter(Counter&& other) noexcept : value1{other.value1}, value2{other.value2} {
++activeCount;
cout << "Counter move constructor invoked.\n";
}
// Destructor
Counter::~Counter() {
--activeCount;
cout << "Counter destructor invoked.\n";
}
void Counter::scale(int factor) {
value1 *= factor;
value2 *= factor;
}
void Counter::print() const {
cout << "(" << value1 << ", " << value2 << ")";
}
int Counter::getActiveCount() {
return activeCount;
}
// Friend function can access private members
void demonstrateFriend() {
Counter temp(42);
temp.value2 = 2049;
cout << "temp = ";
temp.print();
cout << endl;
}
Main Program (main.cpp)
#include "Counter.h"
#include <iostream>
using std::cout;
using std::endl;
void runTests();
int main() {
runTests();
cout << "\nMain function:\n";
cout << "Current active Counter objects: " << Counter::getActiveCount() << endl;
return 0;
}
void runTests() {
cout << "Testing Counter class:\n";
cout << "Description: " << Counter::description << endl;
cout << "Maximum instances allowed: " << Counter::maxInstances << endl;
cout << "Current active count: " << Counter::getActiveCount() << endl << endl;
Counter c1;
cout << "c1 = "; c1.print(); cout << endl;
Counter c2(3, 4);
cout << "c2 = "; c2.print(); cout << endl;
Counter c3(c2);
c3.scale(2);
cout << "c3 = "; c3.print(); cout << endl;
Counter c4(std::move(c2));
cout << "c4 = "; c4.print(); cout << endl;
cout << "Current active count: " << Counter::getActiveCount() << endl;
demonstrateFriend();
}
Key Observations
Constructor Types:
- Default/Parameterized Constructor: Accepts two integer parameters with default values of 0. Creates a new object with specified or default values.
- Copy Constructor: Creates a new object as a copy of an existing one. Essential for proper object duplication.
- Move Constructor: Transfers resources from a temporary object (rvalue). Improves performance by avoiding unnecessary copies.
- Destructor: Automatically called when an object goes out of scope, performing cleanup operations.
Friend Function Declaration: The friend function declaration inside the class grants access to private members but does not declare the function itself. A separate declaration outside the class is required for the function to be callable.
Implementing a Complex Number Class
A complex number consists of a real part and an imaginary part. This implementation demonstrates operator overloading and friend functions for arithmetic operations.
Header File (ComplexNumber.h)
#pragma once
#include <string>
class ComplexNumber {
public:
ComplexNumber(double r = 0, double i = 0);
ComplexNumber(const ComplexNumber& c);
static const std::string doc;
double getReal() const;
double getImaginary() const;
ComplexNumber add(const ComplexNumber& c);
friend ComplexNumber add(const ComplexNumber& a, const ComplexNumber& b);
friend bool isEqual(const ComplexNumber& a, const ComplexNumber& b);
friend bool isNotEqual(const ComplexNumber& a, const ComplexNumber& b);
friend double magnitude(const ComplexNumber& c);
friend void display(const ComplexNumber& c);
~ComplexNumber();
private:
double real, imag;
};
Implementation File (ComplexNumber.cpp)
#include "ComplexNumber.h"
#include <iostream>
#include <cmath>
using namespace std;
const string ComplexNumber::doc{"A simplified complex number class"};
ComplexNumber::ComplexNumber(double r, double i) : real{r}, imag{i} {}
ComplexNumber::ComplexNumber(const ComplexNumber& c) : real{c.real}, imag{c.imag} {}
double ComplexNumber::getReal() const { return real; }
double ComplexNumber::getImaginary() const { return imag; }
ComplexNumber ComplexNumber::add(const ComplexNumber& c) {
real += c.real;
imag += c.imag;
return *this;
}
ComplexNumber add(const ComplexNumber& a, const ComplexNumber& b) {
return ComplexNumber(a.real + b.real, a.imag + b.imag);
}
bool isEqual(const ComplexNumber& a, const ComplexNumber& b) {
return a.real == b.real && a.imag == b.imag;
}
bool isNotEqual(const ComplexNumber& a, const ComplexNumber& b) {
return !(a.real == b.real && a.imag == b.imag);
}
double magnitude(const ComplexNumber& c) {
return sqrt(c.real * c.real + c.imag * c.imag);
}
void display(const ComplexNumber& c) {
cout << c.real;
if (c.imag >= 0)
cout << " + " << c.imag << "i" << endl;
else
cout << " - " << -c.imag << "i" << endl;
}
ComplexNumber::~ComplexNumber() {}
Testing the ComplexNumber Class
#include "ComplexNumber.h"
#include <iostream>
using std::cout;
using std::endl;
using std::boolalpha;
void testComplex() {
cout << "Class member test: " << endl;
cout << ComplexNumber::doc << endl;
cout << "\nComplexNumber object tests: " << endl;
ComplexNumber c1;
ComplexNumber c2(3, -4);
const ComplexNumber c3(3.5);
ComplexNumber c4(c3);
cout << "c1 = "; display(c1);
cout << "c2 = "; display(c2);
cout << "c3 = "; display(c3);
cout << "c4 = "; display(c4);
cout << "c4.real = " << c4.getReal() << ", c4.imag = " << c4.getImaginary() << endl;
cout << "\nComplex arithmetic tests: " << endl;
cout << "magnitude(c2) = " << magnitude(c2) << endl;
c1.add(c2);
cout << "c1 += c2, c1 = "; display(c1);
cout << boolalpha;
cout << "c1 == c2: " << isEqual(c1, c2) << endl;
cout << "c1 != c3: " << isNotEqual(c1, c3) << endl;
c4 = add(c2, c3);
cout << "c4 = c2 + c3, c4 = "; display(c4);
}
int main() {
testComplex();
return 0;
}
Using the Standard Library complex Template
The C++ standard library provides a built-in std::complex template class that offers comprehensive support for complex number operations.
#include <iostream>
#include <complex>
using std::cout;
using std::endl;
using std::boolalpha;
using std::complex;
void testStandardComplex() {
cout << "Testing std::complex template class: " << endl;
complex<double> c1;
complex<double> c2(3, -4);
const complex<double> c3(3.5);
complex<double> c4(c3);
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
cout << "c3 = " << c3 << endl;
cout << "c4 = " << c4 << endl;
cout << "c4.real = " << c4.real() << ", c4.imag = " << c4.imag() << endl;
cout << "\nComplex arithmetic tests: " << endl;
cout << "abs(c2) = " << abs(c2) << endl;
c1 += c2;
cout << "c1 += c2, c1 = " << c1 << endl;
cout << boolalpha;
cout << "c1 == c2: " << (c1 == c2) << endl;
cout << "c1 != c3: " << (c1 != c3) << endl;
c4 = c2 + c3;
cout << "c4 = c2 + c3, c4 = " << c4 << endl;
}
int main() {
testStandardComplex();
return 0;
}
The standard library implementation provides built-in operatosr (==, !=, +) and direct stream output, significantly reducing code complexity and improving readability.
Implementing a Fraction Class
A fraction (rational number) consists of a numerator and denominator. This implementation supports basic arithmetic operations with automatic simplification.
Header File (Fraction.h)
#pragma once
#include <string>
class Fraction {
public:
Fraction(int numerator = 1, int denominator = 1);
Fraction(Fraction& f);
~Fraction();
static const std::string doc;
int getNumerator();
int getDenominator();
Fraction negate();
friend void display(Fraction& f);
friend Fraction add(Fraction& a, Fraction& b);
friend Fraction subtract(Fraction& a, Fraction& b);
friend Fraction multiply(Fraction& a, Fraction& b);
friend Fraction divide(Fraction& a, Fraction& b);
private:
int num, den;
};
void display(Fraction& f);
Fraction add(Fraction& a, Fraction& b);
Fraction subtract(Fraction& a, Fraction& b);
Fraction multiply(Fraction& a, Fraction& b);
Fraction divide(Fraction& a, Fraction& b);
Implementation File (Fraction.cpp)
#include "Fraction.h"
#include <iostream>
#include <cstdlib>
using namespace std;
const string Fraction::doc{"Fraction class v0.01. Supports construction, output, and arithmetic operations."};
// Helper function to find greatest common divisor
static int gcd(int a, int b) {
a = abs(a);
b = abs(b);
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
// Helper function to simplify fraction
static void simplify(int& n, int& d) {
if (d == 0) return;
int g = gcd(n, d);
if (g != 0) {
n /= g;
d /= g;
}
// Ensure denominator is positive
if (d < 0) {
n = -n;
d = -d;
}
}
Fraction::Fraction(int numerator, int denominator) : num{numerator}, den{denominator} {
simplify(num, den);
}
Fraction::Fraction(Fraction& f) : num{f.num}, den{f.den} {}
Fraction::~Fraction() {}
int Fraction::getNumerator() { return num; }
int Fraction::getDenominator() { return den; }
Fraction Fraction::negate() {
return Fraction(-num, den);
}
void display(Fraction& f) {
if (f.den == 0) {
cout << "Error: denominator cannot be zero" << endl;
} else if (f.den == 1) {
cout << f.num << endl;
} else if (f.den < 0) {
cout << -f.num << "/" << -f.den << endl;
} else {
cout << f.num << "/" << f.den << endl;
}
}
Fraction add(Fraction& a, Fraction& b) {
int newNum = a.num * b.den + b.num * a.den;
int newDen = a.den * b.den;
return Fraction(newNum, newDen);
}
Fraction subtract(Fraction& a, Fraction& b) {
int newNum = a.num * b.den - b.num * a.den;
int newDen = a.den * b.den;
return Fraction(newNum, newDen);
}
Fraction multiply(Fraction& a, Fraction& b) {
int newNum = a.num * b.num;
int newDen = a.den * b.den;
return Fraction(newNum, newDen);
}
Fraction divide(Fraction& a, Fraction& b) {
int newNum = a.num * b.den;
int newDen = a.den * b.num;
return Fraction(newNum, newDen);
}
Implementing a Savings Account Class
This example demonstrates a banking class that calculates interest based on the balance and time period.
Header File (SavingsAccount.h)
#pragma once
class SavingsAccount {
private:
int accountId;
double balance;
double interestRate;
int lastTransactionDate;
double accumulatedBalance;
static double totalBalance;
void recordTransaction(int date, double amount);
double calculateAccumulation(int date) const {
return accumulatedBalance + balance * (date - lastTransactionDate);
}
public:
SavingsAccount(int date, int id, double rate);
int getAccountId() const { return accountId; }
double getBalance() const { return balance; }
double getInterestRate() const { return interestRate; }
static double getTotalBalance() { return totalBalance; }
void deposit(int date, double amount);
void withdraw(int date, double amount);
void settleInterest(int date);
void displayAccount() const;
};
Implementation File (SavingsAccount.cpp)
#include "SavingsAccount.h"
#include <cmath>
#include <iostream>
using namespace std;
double SavingsAccount::totalBalance = 0;
SavingsAccount::SavingsAccount(int date, int id, double rate)
: accountId(id), balance(0), interestRate(rate),
lastTransactionDate(date), accumulatedBalance(0) {
cout << date << "\t#" << id << " is created" << endl;
}
void SavingsAccount::recordTransaction(int date, double amount) {
accumulatedBalance = calculateAccumulation(date);
lastTransactionDate = date;
amount = floor(amount * 100 + 0.5) / 100;
balance += amount;
totalBalance += amount;
cout << date << "\t#" << accountId << "\t" << amount << "\t" << balance << endl;
}
void SavingsAccount::deposit(int date, double amount) {
recordTransaction(date, amount);
}
void SavingsAccount::withdraw(int date, double amount) {
if (amount > balance) {
cout << "Error: insufficient funds" << endl;
} else {
recordTransaction(date, -amount);
}
}
void SavingsAccount::settleInterest(int date) {
double interest = calculateAccumulation(date) * interestRate / 365;
if (interest != 0) {
recordTransaction(date, interest);
}
accumulatedBalance = 0;
}
void SavingsAccount::displayAccount() const {
cout << "#" << accountId << "\tBalance: " << balance;
}
Main Program
#include "SavingsAccount.h"
#include <iostream>
using namespace std;
int main() {
SavingsAccount account1(1, 21325302, 0.015);
SavingsAccount account2(1, 58320212, 0.015);
account1.deposit(5, 5000);
account2.deposit(25, 10000);
account1.deposit(45, 5500);
account2.withdraw(60, 4000);
account1.settleInterest(90);
account2.settleInterest(90);
account1.displayAccount(); cout << endl;
account2.displayAccount(); cout << endl;
cout << "Total: " << SavingsAccount::getTotalBalance() << endl;
return 0;
}
Best Practices
- Encapsulation: Clearly distinguish between
publicandprivatemembers to maintain proper data hiding. - Const Correctness: Use
constfor member functions that don't modify object state and for values that shouldn't change. - Static Members: Use static members for class-wide data that should be shared among all instances.
- Standard Library: Leverage standard library classes like
std::complexwhen available to reduce code complexity and improve maintainability.