Lab 10: Classes Continued

More Object Oriented Fundamentals

This lab explores a few more C++ class fundamentals, setting the stage for learning inheritance. We will examine how constructors initialize an object’s first values, how class static members work, and how access specifiers (public, protected, private) regulate what code can reach each data member or method. We will also use getters and setters to expose or restrict access to internal state, giving you fine-grained control over how objects are created, viewed, and modified in an object-oriented design.

§1 Class Initialization vs Assignment #

In C++, there is a clear distinction between initialization and assignment.

class MyClass {
public:
  int x;
  MyClass(int val) : x(val) {}  // Constructor (Initialization)

  MyClass& operator=(int val) {  // Override Assignment Operator
    x = val;
    return *this;  // return reference to self
  }

  void print(){ std::cout << "x: " << x << std::endl; }
};

int main() {
  MyClass obj1(5);    // Object Initialization (constructor)
  obj1.print();       // x: 5
  obj1 = 10;          // Object Assignment (uses custom operator=)
  obj1.print();       // x: 10

  MyClass obj2(55);
  obj1 = obj2;        // obj1 values replaced with copy of obj2 (value of 55)
  // this is NOT using the operator= defined above, object copy uses different parameters.
  // we will cover object deep-copying in a later section.
  obj1.print();       // x: 55
}

When obj1 = obj2 executes, the compiler supplies a default copy-assignment operator that performs a member-wise copy. It does not invoke the operator= we defined. This default copy is a “shallow copy”, it would leave both objects sharing the same memory if any pointers are used within the objects. Deep copying is needed to avoid this issue (we will cover this more in a future lab).

§2 Classes and Static Members #

In C++, static data members are shared across all instances of a class rather than being unique to each object. This is useful when you want to maintain a value common to all objects, such as a count of how many instances of a class have been created.

Static Data Members:
A static data member belongs to the class itself rather than to any object. This means:

Static Member Example:

class MyClass {
public:
  static int objectCounter;  // Static data member

  MyClass() {
    objectCounter++;  // Increment the count for each new object
  }
};
// Initialize member var immediately *outside* the class definition
int MyClass::objectCounter = 0;

int main() {
  MyClass obj1, obj2;
  // we can access static members from the class itself without any instances.
  std::cout << "Total objects created: " << MyClass::objectCounter << std::endl;
  // we can also access the static member vars on any class instance
  std::cout << "Total objects created: " << obj1.objectCounter << std::endl;
}

output:

Total objects created: 2
Total objects created: 2

In the above example, objectCounter is shared among all instances of MyClass, but needs to be initialized at time of definition outside of the class.

§3 Public vs Private Members #

In C++, class members (both data members and member functions) can be designated as either public, private, or protected. This access control mechanism ensures encapsulation and controls how data is accessed or modified from outside the class.

Three types of access specifiers:

Example:

class MyClass {
public: // all members under this section are "public" and accessible anywhere
  int publicInt;
  void publicFunction() {
    std::cout << "Public function" << std::endl;
    privateFunction();  // Can call private function from within the class
  }

private: // members under this section are "private" and accessible only within the class
  int privateInt;
  void privateFunction() {
    std::cout << "Private function" << std::endl;
  }
};

int main() {
  // interacting with encapsulated class members
  MyClass obj;
  obj.publicFunction();  // Valid
  obj.publicInt = 1;  // Also Valid
  // obj.privateFunction();  // Error: 'privateFunction' is private
  // obj.privateInt = 2;  // Error: 'privateInt' is private
}
Public function
Private function

In this example, the public members can be accessed directly from main(), but the private members cannot. Code inside a public function can access all private members of the class.

§4 Implementation Hiding #

Implementation hiding refers to concealing the details of how a class is implemented from the user. Users interact with the class through its public interface without needing to know how it works under the hood. Implementation hiding utilizes public and private access specifiers to control interaction with the class.

This allows authors to change the class’s internal implementation without changing any outside code that uses the class.

As an example, let’s create a class BankAccount that hides its internal balance and provides public methods to deposit, withdraw, and check the account balance.

#include <iostream>
using namespace std;

class BankAccount {
public:
  // Constructor to initialize account with a given balance
  BankAccount(double initialBalance) {
    if (initialBalance >= 0) {
      // Convert dollars to cents
      balanceInCents = static_cast<int>(initialBalance * 100);
    } else {
      balanceInCents = 0;  // Prevent negative initial balance
    }
  }

  // Public method to get the balance in dollars
  double getBalance() const {
    return balanceInCents / 100.0; // Convert cents back to dollars
  }

  // Public method to deposit an amount in dollars
  void deposit(double amount) {
    if (amount > 0) {
      balanceInCents += static_cast<int>(amount * 100);
    }
  }

  // Public method to withdraw an amount in dollars
  bool withdraw(double amount) {
    int amountInCents = static_cast<int>(amount * 100);
    if (amountInCents > 0 && amountInCents <= balanceInCents) {
      balanceInCents -= amountInCents;
      return true;
    }
    return false;  // Not enough balance
  }

private:
  int balanceInCents;  // Private variable storing the balance in cents for accuracy
  // (floating point numbers can cause problems for high-accuracy use-cases due to rounding errors)
};

int main() {
  BankAccount account(100.50);  // Initialize account with $100.50

  // Deposit money
  account.deposit(25.75);
  cout << "Balance after deposit: $" << account.getBalance() << endl;

  // Withdraw money
  if (account.withdraw(50.00)) {
    cout << "Balance after withdrawal: $" << account.getBalance() << endl;
  } else {
    cout << "Insufficient balance for withdrawal" << endl;
  }
}
Balance after deposit: $126.25
Balance after withdrawal: $76.25

Explanation:

In this example, the internal representation of the balance (in cents) is hidden from external code, and external code only interacts with the balance in dollars via public methods.

§5 Getters and Setters Pattern #

The “Getter/Setter” pattern provides a way to encapsulate and control access to class variables by using public member functions (getters and setters) to access private data members. All of the above examples utilize this pattern.

What are Getters and Setters?

Why Use Getters and Setters?

Simple Example:

class MyClass {
private:
  int x;

public:
  // Getter (read-only access to private data)
  int getX() const {  // use const to prevent accidental modification of data in your code
    return x;
  }

  // Setter (write access to private data)
  void setX(int val) {
    x = val;
  }
};

int main() {
  MyClass obj;
  obj.setX(10);
  std::cout << "Value of x: " << obj.getX() << std::endl;
}
Value of x: 10

§6 More Reading #

These resources provide more information on topics covered in this lecture

§7 Questions #

  1. What is the difference between class initialization and assignment in C++?
  1. In a C++ class, what is the difference between a static member and a non-static member?
  1. Why must a static data member be initialized outside of the class definition?
  1. What error do you get if you try to access a static data member before it has been defined outside the class?
  1. Can static member functions access non-static data members of a class? Why or why not?
  1. What is the difference between public and private members in a class?
  1. Can a class private data member be accessed in the constructor of a class?
  1. What is the purpose of implementation hiding in object-oriented programming?
  1. How does implementation hiding improve software design?
  1. What are the benefits of using setters to modify private data members instead of making the data member public?
  1. What is the role of the const keyword in getter functions like int getX() const?
  1. Why might you choose not to provide a setter for a data member?
  1. What are the risks of allowing direct public access to data members?
  1. How can you ensure that a class always maintains a valid state (e.g., no negative balance in a BankAccount)?
  1. Can you override the assignment operator (operator=) for a class? Why might you want to do that?