A common convention is to use 2. Encapsulation, Information Hiding and Invariants2.1 Encapsulation and information hidingEncapsulation groups related state and behavior into one abstraction. Information hiding conceals design decisions that clients should not depend upon. The concepts are related but not identical. For example, a bank account could expose its balance publicly: struct UnsafeAccount {
std::int64_t balance_cents;
};Any client could assign A protected design makes the balance private and allows changes only through validated operations. 2.2 Class invariantsA class invariant is a condition that must hold for every publicly observable, fully constructed object. Consider an account storing an owner and a balance measured in cents: I(a) ≡ (a.balance ≥ 0) ∧ (a.owner ≠ empty) Here, Public operations may temporarily manipulate internal values, but the invariant must hold when control returns to the client. Constructors establish it, member functions preserve it, and destructors safely end the object's lifetime. 2.3 Why data members are usually privatePrivate data provides several benefits:
Providing a setter for every data member does not automatically create good encapsulation. An operation such as Figure: Class interface protecting private representation Figure: A public interface mediates all access to a class's private representation. 3. Access ControlC++ provides three access levels.
Public members constitute client-visible services. Private members support the implementation. Protected members are accessible in derived classes, but excessive protected data weakens encapsulation because every derived class becomes dependent on the representation. Access control is checked by the compiler. It is not, by itself, an encryption or hostile-process security mechanism. Memory corruption, unsafe casts or privileged debugging tools may still expose data. 4. Constructors and Object InitializationA constructor initializes an object and establishes its invariant. It has the class name and no return type. Temperature::Temperature(double celsius)
: celsius_{celsius} {
if (celsius < -273.15) {
throw std::invalid_argument{"temperature below absolute zero"};
}
}4.1 Member-initializer listsData members are initialized before the constructor body executes. A member-initializer list constructs them directly and is required for references, const data members and base classes. Members are initialized in their declaration order, not in the textual order of the initializer list. Initializer lists should therefore follow declaration order to avoid misleading code. 4.2 Default and converting constructorsA constructor callable with no arguments is a default constructor. A constructor callable with one argument may define an implicit conversion unless marked class EmployeeId {
public:
explicit EmployeeId(int value);
private:
int value_;
};
4.3 Delegating constructorsOne constructor may delegate to another constructor of the same class: class Point {
public:
Point() : Point{0, 0} {}
Point(int x, int y) : x_{x}, y_{y} {}
private:
int x_;
int y_;
};Delegation avoids duplicating initialization logic. 4.4 Constructor failureIf a constructor throws an exception, the object is not considered fully constructed, and its destructor is not called. However, already constructed members and base subobjects are destroyed automatically. A constructor should validate inputs before performing externally visible side effects whenever practical. 5. Member Functions and Const-CorrectnessA non-static member function receives an implicit pointer named void Counter::increment() {
++this->value_;
}The explicit A const member function promises not to modify the object's logical state through that interface: [[nodiscard]] std::int64_t balance() const noexcept; Such a function can be called on const and non-const objects. A non-const member function cannot normally be called on a const object.
A 6. A Robust Account ClassThe following class stores monetary values as signed 64-bit integer cents. Integer cents avoid binary floating-point rounding for currencies with two-decimal-place accounting in this simplified model. Real financial systems must additionally define currency, rounding, taxation and regulatory rules. 6.1 Interface// Account.hpp
#ifndef ACCOUNT_HPP
#define ACCOUNT_HPP
#include <cstdint>
#include <string>
class Account {
public:
using Money = std::int64_t; // cents
explicit Account(std::string owner, Money opening_balance = 0);
[[nodiscard]] const std::string& owner() const noexcept;
[[nodiscard]] Money balance() const noexcept;
void deposit(Money amount);
void withdraw(Money amount);
void transfer_to(Account& target, Money amount);
private:
std::string owner_;
Money balance_;
};
#endif6.2 Implementation// Account.cpp
#include "Account.hpp"
#include <limits>
#include <stdexcept>
#include <utility>
Account::Account(std::string owner, Money opening_balance)
: owner_{std::move(owner)}, balance_{opening_balance} {
if (owner_.empty()) {
throw std::invalid_argument{"owner must not be empty"};
}
if (balance_ < 0) {
throw std::invalid_argument{"opening balance must be nonnegative"};
}
}
const std::string& Account::owner() const noexcept {
return owner_;
}
Account::Money Account::balance() const noexcept {
return balance_;
}
void Account::deposit(Money amount) {
if (amount <= 0) {
throw std::invalid_argument{"deposit must be positive"};
}
const Money maximum = std::numeric_limits<Money>::max();
if (balance_ > maximum - amount) {
throw std::overflow_error{"balance would overflow"};
}
balance_ += amount;
}
void Account::withdraw(Money amount) {
if (amount <= 0) {
throw std::invalid_argument{"withdrawal must be positive"};
}
if (amount > balance_) {
throw std::domain_error{"insufficient funds"};
}
balance_ -= amount;
}
void Account::transfer_to(Account& target, Money amount) {
if (&target == this) {
throw std::invalid_argument{"source and target must differ"};
}
if (amount <= 0) {
throw std::invalid_argument{"transfer must be positive"};
}
if (amount > balance_) {
throw std::domain_error{"insufficient funds"};
}
const Money maximum = std::numeric_limits<Money>::max();
if (target.balance_ > maximum - amount) {
throw std::overflow_error{"target balance would overflow"};
}
balance_ -= amount;
target.balance_ += amount;
}6.3 Preventing signed-integer overflowSigned-integer overflow is undefined behavior in C++. Code must check whether addition is representable before performing it. Because balance ≤ MAX − amount Here, For example, the maximum signed 64-bit value is 9,223,372,036,854,775,807. If the balance is 9,223,372,036,854,775,767 cents, depositing 50 cents would exceed the maximum by 10 cents. The operation therefore throws before addition occurs, leaving the original balance unchanged. Merely changing the type from 6.4 Preserving transfer consistency
If the source balance is 12,500 cents and the target balance is 3,000 cents, transferring 2,750 cents produces:
This conserves the amount represented by the two objects. It does not create a durable database transaction and is not thread-safe without external synchronization. 7. Design by ContractDesign by contract describes an operation using preconditions, postconditions and invariants.
For a successful deposit, let Pre: x > 0 ∧ b ≤ MAX − x Post: b′ = b + x If a public API accepts untrusted input, checking preconditions at runtime is generally safer than relying only on documentation. Exceptions, error codes or result types may report violations; the choice should be consistent across the interface. The account operations provide the strong exception guarantee: if validation fails and an exception is thrown, the observable account state remains unchanged. Assertions such as 8. Static Members and Object LifetimesA static data member belongs to the class rather than to each object. A static member function has no A common application is counting live instances. Copy and move semantics must be handled explicitly: if an implicitly copied object does not increment the count but its destructor decrements it, the counter becomes incorrect. #include <atomic>
#include <cstddef>
class Session final {
public:
Session() noexcept {
active_.fetch_add(1, std::memory_order_relaxed);
}
~Session() {
active_.fetch_sub(1, std::memory_order_relaxed);
}
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
Session(Session&&) = delete;
Session& operator=(Session&&) = delete;
[[nodiscard]] static std::size_t active() noexcept {
return active_.load(std::memory_order_relaxed);
}
private:
inline static std::atomic_size_t active_{0};
};Copy and move construction are deleted, so every live The atomic counter prevents a data race when sessions are created and destroyed by multiple threads. Relaxed ordering is sufficient for an independent count because the counter is not being used to publish or synchronize other data. A returned count is a valid snapshot, but it may immediately become outdated as other threads create or destroy sessions. An alternative design could implement copy or move operations explicitly. A copy constructor would need to increment the count. A move design would require a clearly specified ownership model and a moved-from state. Deleting unsupported operations is often safer than accepting incorrect compiler-generated behavior. 9. Copy and Move ControlC++ may generate a destructor, copy constructor, copy assignment operator, move constructor and move assignment operator. These are often called special member functions. Compiler-generated operations are suitable when each data member already has correct value semantics. This is the rule of zero: prefer members such as If a class directly manages a resource, special operations require deliberate design. Common choices are:
Deleting an operation communicates the restriction at compile time: Session(const Session&) = delete; 10. Friend DeclarationsA class may grant a function or another class access to its private and protected members. #include <ostream>
class Coordinate {
public:
Coordinate(int x, int y) : x_{x}, y_{y} {}
friend std::ostream& operator<<(std::ostream&, const Coordinate&);
private:
int x_;
int y_;
};The output operator can access Friendship should be narrow and intentional. It is appropriate when an operation is conceptually part of an abstraction but is better represented as a non-member, as with symmetric operators. Broad friendship increases coupling and weakens the class boundary. Objects of the same class may access one another's private members without friendship. This is why 11. Separating Interface and ImplementationTraditional C++ projects place class declarations in header files and non-inline definitions in source files. 11.1 Header responsibilitiesA header normally contains:
11.2 Source-file responsibilitiesA source file normally contains:
This separation improves readability and reduces unnecessary recompilation. It also allows implementation changes that do not alter the public interface or object layout. However, private data in a conventional declaration can still create build and binary dependencies because clients need the object size and layout. The Pointer to Implementation, or Pimpl, idiom hides representation behind a private pointer. Pimpl can improve binary-interface stability, but it introduces allocation, indirection and implementation complexity. C++20 modules offer a modern alternative for organizing exported interfaces while reducing textual inclusion. Modules improve dependency management, although they do not eliminate the need for careful abstraction and compatibility design. 12. Cohesion and CouplingCohesion measures how strongly the responsibilities within a module or class belong together. Coupling measures how strongly one module depends on another. A well-designed class generally has high cohesion and low coupling.
A class should have a focused reason to change. Excessively large “god classes” become difficult to test and maintain. Conversely, dividing behavior into many trivial classes can create needless navigation and interaction complexity. Good design balances responsibility boundaries against practical simplicity. 13. Illustrative Case: A Digital Wallet Transfer ServiceBackgroundThis fictional case concerns a campus digital-wallet application. Students use wallet balances, represented in cents, to pay for meals and printing. ProblemAn early implementation exposed balances publicly. Application code subtracted from one wallet and later added to another. Defects included negative balances, partially completed transfers and overflow when a corrupted destination balance approached the integer limit. AnalysisThe public balance allowed every client to become responsible for validation. Consequently, rules were duplicated and inconsistently enforced. The two transfer updates could also be separated by an exception, producing lost value. The required invariants were:
Approach and SolutionThe development team replaced public balances with an The public header exposed only domain operations. Storage details and validation logic remained in the source file. Unit tests covered zero and negative amounts, insufficient funds, self-transfer, maximum representable balances and successful transfers. For the production system, database transactions and row-level concurrency controls were also required. An in-memory C++ class can preserve local invariants, but it cannot alone ensure durability or consistency across processes. OutcomeInvalid balance assignments became impossible through the ordinary public API. Failed transfers left both wallet objects unchanged, and overflow attempts were rejected before unsafe arithmetic occurred. Centralized rules also simplified auditing and testing. Learning Points
14. Testing Class ContractsTests should examine normal behavior, boundary conditions and invalid inputs. For
Property-based testing can generate many input combinations and verify properties such as nonnegative balances and transfer conservation. Static analysis and sanitizers can detect additional problems, although overflow must still be prevented by correct program logic. 15. Professional, Ethical and Governance ConsiderationsClass interfaces often enforce rules with legal, financial or safety consequences. Developers should document units, limits, rounding policies, error behavior and thread-safety guarantees. Misleading names or undocumented conversions can cause material harm. Financial and personal data should be exposed only when required. A getter returning sensitive information may preserve language-level encapsulation while violating privacy principles. Audit logs should avoid leaking credentials or unnecessary personal data. AI-assisted tools can propose class designs and generate tests, but generated code must be reviewed for incorrect lifetime assumptions, arithmetic overflow, accidental copying, race conditions and overly broad access. Responsibility for deployed behavior remains with the engineering organization. 16. Design Guidelines and LimitationsEffective class design commonly follows these practices:
Encapsulation has limits. It cannot by itself prevent memory corruption, ensure database durability, provide network authorization or make an unsynchronized object thread-safe. These concerns require additional system-level mechanisms. Chapter SummaryA C++ class is a user-defined type whose public interface controls access to its representation. Encapsulation combines state and behavior, while information hiding prevents clients from depending on implementation decisions. Constructors establish invariants, and member functions must preserve them. Access control, const-correctness, explicit constructors and carefully designed domain operations support reliable interfaces. Arithmetic must be checked before an operation that could overflow. Static members represent class-wide state, but correct instance counting requires explicit treatment of copy and move construction. Friend declarations offer selective access but increase coupling. Separating declarations from definitions improves modularity and build organization. High cohesion, low coupling, design by contract, exception safety and deliberate special-member-function policies produce classes that are easier to test and maintain. These local design principles must be combined with system-level concurrency, persistence, security and governance controls in professional software. Key TermsAbstraction: A simplified model exposing essential behavior while suppressing unnecessary detail. Access control: Language rules determining where class members may be used. Class invariant: A condition that holds for every publicly observable valid object. Cohesion: The degree to which responsibilities within a module belong together. Const-correctness: Consistent use of Constructor: A special member function that initializes an object. Coupling: The degree of dependency between software components. Design by contract: Specification of operations through preconditions, postconditions and invariants. Encapsulation: Combining state and operations within a controlled abstraction. Exception guarantee: A stated condition concerning program state when an operation throws. Friend: A function or class granted access to another class's non-public members. Information hiding: Concealing implementation decisions that clients should not depend upon. Interface: The publicly available operations and types through which clients use an abstraction. Member-initializer list: Constructor syntax that directly initializes bases and data members. Postcondition: A condition guaranteed after successful completion of an operation. Precondition: A condition required before an operation is invoked. Rule of zero: The guideline that resource management should be delegated so custom special member functions are unnecessary. Static member: A member associated with the class as a whole rather than each object. Review QuestionsShort Answer Questions
Long Answer Questions
Analytical / Application Questions
Multiple Choice Questions with AnswersQuestion 1Which statement best defines a class invariant? A. A condition that must hold only before construction B. A condition that must hold for every publicly observable valid object C. A comment describing private data D. A rule applied only to static members Correct Answer: B. A condition that must hold for every publicly observable valid object Question 2Why does A. To prevent signed-integer overflow before it occurs B. To convert the balance to floating point C. To ensure the amount is negative D. To make the function thread-safe Correct Answer: A. To prevent signed-integer overflow before it occurs Question 3What is the effect of marking a single-argument constructor A. It makes the constructor private B. It deletes the default constructor C. It prevents unintended implicit conversions through that constructor D. It guarantees that construction cannot throw Correct Answer: C. It prevents unintended implicit conversions through that constructor Question 4Why are copying and moving deleted in the chapter's A. Atomic variables cannot appear in classes B. Every session must have a distinct counted lifetime C. Destructors cannot access static members D. Static functions require immovable objects Correct Answer: B. Every session must have a distinct counted lifetime Question 5What would be wrong with an active-object counter that increments only in the ordinary constructor but permits implicit copying? A. Copied objects would increment the counter twice B. Copied objects would never be destroyed C. Copied objects would later decrement a count they never incremented D. The counter would become a non-static member Correct Answer: C. Copied objects would later decrement a count they never incremented Question 6Which member function declaration is an observer callable on a const object? A. B. C. D. Correct Answer: A. Question 7Which statement about C++ friendship is correct? A. Friendship is automatically reciprocal B. Friendship is inherited by derived classes C. Friendship is transitive between classes D. Friendship grants specified access but is neither reciprocal, inherited nor transitive Correct Answer: D. Friendship grants specified access but is neither reciprocal, inherited nor transitive Question 8A source account contains 12,500 cents and transfers 2,750 cents to an account containing 3,000 cents. What is the destination balance after a successful transfer? A. 250 cents B. 5,750 cents C. 9,750 cents D. 15,500 cents Correct Answer: B. 5,750 cents Question 9Which design most directly supports high cohesion? A. A class responsible for accounts, email delivery and screen rendering B. A class exposing references to all private members C. A class focused on account operations such as deposits and withdrawals D. A class declaring every other class as a friend Correct Answer: C. A class focused on account operations such as deposits and withdrawals Question 10What does the strong exception guarantee mean for a failed A. The program must terminate immediately B. The balance remains observably unchanged C. The exception is silently ignored D. The balance is reset to zero Correct Answer: B. The balance remains observably unchanged
Object-Oriented Programming (C++)
|
Chapter 3 of 15 |