Welcome! Today's subject is Encapsulation, a cornerstone of Object-Oriented Programming (OOP) in C++. Encapsulation is the process of bundling data and the operations that modify them into one unit — commonly an object — thereby protecting the data from unwanted alterations. This level of data protection ensures the creation of robust and maintainable software.
Prepare yourself for an exciting journey as we delve into how encapsulation functions in C++ and explore the critical role it plays in safeguarding data privacy.
Starting with the basics, encapsulation involves wrapping data and the methods that modify this data into a single compartment known as a class
. It protects the internal state of an object from undesired external interference.
To illustrate, consider a C++ class representing a bank account. Without encapsulation, the account balance could be directly altered. With encapsulation, however, the balance can only change through specified methods such as deposit
or withdraw
.
C++1class BankAccount { 2public: 3 void deposit(double amount); 4 void withdraw(double amount); 5 double checkBalance() const; 6 7private: 8 int accountNumber; 9 double balance; 10};
Encapsulation restricts direct access to an object's data and prevents unwanted data alteration. This principle is comparable to window blinds, allowing you to look out while preventing others from peeping in.
In C++, encapsulation is achieved through access specifiers like private
and public
. By default, class members are private
, restricting direct access. Data intended to be hidden from outside manipulation is placed under the private
access specifier, while public
members are accessible.
Consider a class Person
with a private attribute name
.
C++1class Person { 2public: 3 Person(std::string name) : name(name) {} 4 std::string getName() const; 5 6private: 7 std::string name; // Private attribute 8}; 9 10std::string Person::getName() const { 11 return name; 12}
In this example, name
is private, and getName()
enables us to access it safely.
While encapsulating, C++ uses getter and setter methods to access or modify private attributes. A getter method retrieves an attribute's value, while a setter alters it. Let's illustrate this:
C++1class Dog { 2public: 3 Dog(std::string name) : name(name) {} 4 void setName(std::string newName); 5 std::string getName() const; 6 7private: 8 std::string name; // Private attribute 9}; 10 11void Dog::setName(std::string newName) { 12 name = newName; 13} 14 15std::string Dog::getName() const { 16 return name; 17}
Here, setName()
and getName()
serve as the setter and getter methods, respectively, for the private attribute name
.
Let's apply the principle of encapsulation to our BankAccount
class, which includes private attributes like account number and balance, along with public methods for withdrawals, deposits, and balance checks.
C++1#include <iostream> 2 3class BankAccount { 4public: 5 BankAccount(int account_no, double initial_balance) 6 : account_no(account_no), balance(initial_balance) {} 7 8 void withdraw(double amount) { 9 if (balance >= amount) { 10 balance -= amount; 11 } else { 12 std::cout << "Insufficient funds." << std::endl; 13 } 14 } 15 16 void deposit(double amount) { 17 balance += amount; 18 } 19 20 double checkBalance() const { 21 return balance; 22 } 23 24private: 25 int account_no; 26 double balance; 27}; 28 29int main() { 30 BankAccount account(1, 500.0); 31 account.withdraw(100.0); // Withdraws 100.0 from the balance 32 account.deposit(50.0); // Deposits 50.0 to the balance 33 std::cout << "Current balance: " << account.checkBalance() << std::endl; 34 // Output: Current balance: 450 35 return 0; 36}
In the above code, the BankAccount
class encapsulates account details, while the public methods manage the balance in a controlled manner.
In larger projects, it's common practice in C++ to separate class declarations and implementations into different files: a header file (.hpp
) and a source file (.cpp
). This separation improves code organization and maintainability.
Let's revisit the BankAccount
class using this approach:
BankAccount.hpp
C++1class BankAccount { 2public: 3 BankAccount(int account_no, double initial_balance); 4 void withdraw(double amount); 5 void deposit(double amount); 6 double checkBalance() const; 7 8private: 9 int account_no; 10 double balance; 11};
BankAccount.cpp
C++1#include "BankAccount.hpp" 2#include <iostream> 3 4BankAccount::BankAccount(int account_no, double initial_balance) 5 : account_no(account_no), balance(initial_balance) {} 6 7void BankAccount::withdraw(double amount) { 8 if (balance >= amount) { 9 balance -= amount; 10 } else { 11 std::cout << "Insufficient funds." << std::endl; 12 } 13} 14 15void BankAccount::deposit(double amount) { 16 balance += amount; 17} 18 19double BankAccount::checkBalance() const { 20 return balance; 21} 22 23// Example usage in a main function 24int main() { 25 BankAccount account(1, 500.0); 26 account.withdraw(100.0); // Withdraws 100.0 from the balance 27 account.deposit(50.0); // Deposits 50.0 to the balance 28 std::cout << "Current balance: " << account.checkBalance() << std::endl; 29 // Output: Current balance: 450 30 return 0; 31}
By using separate files, you define your class interface in the header file and implement its functionality in the corresponding source file. This method helps manage complex projects by making your code more modular and easier to navigate.