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.
- Initialization: the process of giving a variable or object its first value when it is created. For classes, initialization can happen in several ways, including constructor initialization lists.
- Assignment: when an object already exists, assignment overwrites its current state with a new value. This behavior can be altered by overriding
operator=.
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:
- Only one instance of the static member exists, regardless of how many objects of the class are created.
- Static data members must be defined outside the class definition.
- Static data members can be initialized only once.
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:
- Public: members declared as
publicare accessible from outside the class. - Private: members declared as
privateare only accessible from within the class. They cannot be accessed directly from outside the class. - Protected:
protectedmembers are accessible within the class and its derived classes (inheritance). (We will learn more about this later.)
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:
- Private Data Member (
balanceInCents): we hide the balance by storing it as an integer in cents to avoid floating-point precision issues. ThebalanceInCentsvariable isprivate, so external code cannot directly modify or access it. - Public Methods: the class provides public methods (
getBalance,deposit, andwithdraw) that allow controlled access to the balance. External code interacts with the class internals through these methods.
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?
- Getters are
publicmember functions that allow you to retrieve the value of aprivatemember variable. They provide a way to accessprivatedata members without exposing them directly. - Setters are
publicmember functions that allow you to modify the value of aprivatemember variable. They provide a controlled way to set or change the value ofprivatedata members, often with validation or constraints.
Why Use Getters and Setters?
- Encapsulation: getters and setters allow you to hide the internal representation of a class while exposing a controlled interface to access or modify data. This enforces encapsulation, protecting the integrity of an object’s state.
- Data Validation: you can include validation logic inside setters to ensure that the values assigned to private members are valid. For example, you can ensure that an age cannot be set to a negative value.
- Abstraction: by using getters and setters, you abstract how data is stored internally. You have the ability to change the internal implementation later without affecting the external interface.
- Flexibility: you can add additional logic when data is retrieved or set, such as logging, security checks, or conversions.
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
- https://cplusplus.com/doc/tutorial/classes/
- https://learn.microsoft.com/en-us/cpp/cpp/initializers?view=msvc-170
- https://en.cppreference.com/w/cpp/language/static.html
- https://learn.microsoft.com/en-us/cpp/cpp/static-members-cpp?view=msvc-170
- https://cppscripts.com/public-vs-private-cpp
§7 Questions #
- What is the difference between class initialization and assignment in C++?
- In a C++ class, what is the difference between a static member and a non-static member?
- Why must a static data member be initialized outside of the class definition?
- What error do you get if you try to access a static data member before it has been defined outside the class?
- Can static member functions access non-static data members of a class? Why or why not?
- What is the difference between
publicandprivatemembers in a class?
- Can a class private data member be accessed in the constructor of a class?
- What is the purpose of implementation hiding in object-oriented programming?
- How does implementation hiding improve software design?
- What are the benefits of using setters to modify private data members instead of making the data member public?
- What is the role of the
constkeyword in getter functions likeint getX() const?
- Why might you choose not to provide a setter for a data member?
- What are the risks of allowing direct public access to data members?
- How can you ensure that a class always maintains a valid state (e.g., no negative balance in a BankAccount)?
- Can you override the assignment operator (
operator=) for a class? Why might you want to do that?