Understanding C++ Classes: Constructors, Static Members, and Friend Functions

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 public and private members to maintain proper data hiding.
  • Const Correctness: Use const for 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::complex when available to reduce code complexity and improve maintainability.

Tags: C++ Classes Static Members Constructors destructor

Posted on Thu, 10 Sep 2026 16:27:23 +0000 by pikemsu28