3. Classes, Objects and EncapsulationIntroduction and ContextObject-oriented programming (OOP) provides a way of organizing software around objects that combine state and behaviour. In C++, the class is the principal mechanism for defining such objects. A class specifies what data an object owns, what operations may be performed on it, and which parts of its representation are visible to other components. An object is a concrete instance of a class created during program execution. The importance of classes extends beyond syntax. A well-designed class establishes a boundary around a concept, protects its internal representation, and exposes a controlled interface. This boundary is known as encapsulation. Encapsulation is therefore not merely the use of the `private` keyword; it is a design discipline concerned with responsibility, invariants, coupling, substitutability, and safe change. At postgraduate level, class design should be evaluated not only by whether a program compiles, but also by whether its abstractions are cohesive, its state remains valid, its ownership rules are explicit, and its interfaces support reliable evolution. This chapter develops those concerns through constructors, member functions, static members, friends, nested types, operator overloading, and conversion control. Learning ObjectivesAfter studying this chapter, the reader should be able to: - distinguish classes, objects, state, behaviour, interfaces, and implementations;
- design classes with appropriate access control and strong invariants;
- use constructors, destructors, member-initializer lists, and special member functions correctly;
- explain the roles of `static` members, nested types, and friend declarations;
- overload operators consistently and safely;
- control implicit and explicit conversions;
- assess class designs in terms of cohesion, coupling, ownership, exception safety, and maintainability;
- apply these ideas to a realistic software design problem.
From Data Structures to AbstractionsA class declaration defines a new type. Its public members form an interface, while private and protected members constitute implementation details. For example: ```cpp class Temperature { private: double celsius_; public: explicit Temperature(double celsius) : celsius_(celsius) {} double celsius() const { return celsius_; } double fahrenheit() const { return celsius_ * 9.0 / 5.0 + 32.0; } }; ``` `Temperature` expresses a domain concept more clearly than a bare `double`. The constructor establishes the initial state, and the observer functions provide controlled access. The trailing `const` promises that these functions do not modify the object. An object has identity, state, and behaviour. Two objects may contain equal values but remain distinct objects. A class may also contain invariants: conditions that must remain true whenever an object is observable. If a class represents a bank account, its balance may be required never to be negative, or a closed account may be forbidden from accepting deposits. Every public operation should preserve such conditions. Access Control and Information HidingC++ provides `public`, `private`, and `protected` access. Public members are available to clients. Private members are accessible only to the class and its designated friends. Protected members are also available to derived classes, but their use can increase coupling between a base class and its subclasses. A common professional default is to keep data private and expose behaviour rather than unrestricted getters and setters. A setter that accepts every value weakens an abstraction; a named operation such as `withdraw`, `resize`, or `activate` can validate input and preserve the domain rules. Public data may be appropriate for simple passive structures, but it should be a deliberate choice rather than a shortcut. Encapsulation has several dimensions: 1. Representation hiding: clients do not depend on storage details. 2. Invariant protection: all state-changing operations validate their effects. 3. Operation grouping: data and the operations governing it are placed together. 4. Change isolation: internal implementation can change without breaking clients. Encapsulation is not absolute secrecy. A class can expose a carefully designed view, iterator, or read-only query while still hiding ownership and mutation mechanisms. Constructing and Destroying ObjectsA constructor initializes an object into a valid state. It has the class name, no return type, and may be overloaded. Initialization should normally occur in a member-initializer list: ```cpp class Account { std::string owner_; long number_; double balance_; public: Account(std::string owner, long number, double openingBalance) : owner_(std::move(owner)), number_(number), balance_(openingBalance) { if (openingBalance < 0.0) throw std::invalid_argument("negative opening balance"); } }; ``` Members are initialized in the order of their declaration, not the order written in the initializer list. Consequently, declarations should be arranged in dependency order, and initializer lists should follow that order. A reference member, a `const` member, and a member without a default constructor must be initialized there. A default constructor takes no arguments, either explicitly or through default arguments. A converting constructor has one non-default argument and may permit conversion from another type. Marking it `explicit` prevents unintended implicit conversions. A copy constructor creates an object from another object of the same class; a move constructor transfers resources from a temporary or expiring object. A destructor releases resources when an object’s lifetime ends. With RAII (Resource Acquisition Is Initialization), ownership is represented by objects whose destructors perform cleanup. Standard library types such as `std::string`, `std::vector`, and smart pointers usually eliminate the need for manual resource management. The special member functions are the default constructor, destructor, copy constructor, copy assignment operator, move constructor, and move assignment operator. Their correct design is summarized by the Rule of Zero: if a class’s resources are managed by suitable members, define none of these functions manually. If a class directly owns a resource, the Rule of Five warns that all copying and moving operations may need deliberate treatment. A class can also use `=delete` to prohibit copying or moving. Member Functions and Object StateA non-static member function operates on a particular object and has an implicit `this` pointer. Const-correctness distinguishes observers from mutators: ```cpp class Counter { int value_ = 0; public: int value() const { return value_; } void increment() { ++value_; } }; ``` A `const Counter` can call only const member functions. This makes interfaces more expressive and allows objects to be used through const references without losing access to observers. Overloading a member function on constness is useful when mutable and read-only access must differ, although returning a mutable reference can compromise invariants. Member functions may be defined inside or outside the class. Functions defined inside the class are implicitly inline in the language sense, though inlining as a machine-code optimization remains a compiler decision. A class should generally expose a small, meaningful public interface and keep implementation-oriented helpers private. `static` data members belong to the class rather than to individual objects. They are appropriate for class-wide information, such as a count or a shared configuration, but shared mutable state introduces synchronization and testing concerns. Since C++17, an `inline static` data member can be defined in the class definition: ```cpp class Session { inline static unsigned live_ = 0; public: Session() { ++live_; } ~Session() { --live_; } static unsigned live() { return live_; } }; ``` A static member function has no `this` pointer and can directly access only static members. It is often useful for named construction functions such as `Date::from_iso_string`, especially when construction requires validation or multiple alternative forms. Interfaces, Invariants, and Contract ThinkingAn interface is the set of operations and guarantees visible to clients. A robust interface specifies valid inputs, observable results, failure behaviour, ownership, and object-state conditions. These guarantees form a contract. Consider a bounded queue. Its invariant may be `0 <= size <= capacity`. `push` must reject or report insertion into a full queue; `pop` must define the empty-queue behaviour. If a public method temporarily breaks the invariant internally, it must restore it before returning or propagating an exception. This is the basis of the basic exception-safety guarantee: an operation that fails does not leak resources and leaves the object in a valid, though possibly changed, state. The strong guarantee promises no observable state change on failure; the nothrow guarantee promises that the operation cannot fail through an exception. Cohesion asks whether the responsibilities of a class belong together. A `Report` class that formats documents, authenticates users, writes files, and sends email has low cohesion. Coupling asks how strongly a class depends on other classes. Private representation, stable interfaces, dependency injection, and value-oriented design can reduce unnecessary coupling. Friends and Nested TypesA friend function or class is granted access to private and protected members without becoming a member. Friendship is not inherited, reciprocal, or automatically transferable. It should be used selectively when a close relationship genuinely improves the interface or implementation. Non-member operator functions are often declared friends when they need symmetric access to private state. For example, equality can be implemented as a non-member because neither operand is conceptually more important: ```cpp class Point { int x_, y_; public: Point(int x, int y) : x_(x), y_(y) {} friend bool operator==(const Point& a, const Point& b) { return a.x_ == b.x_ && a.y_ == b.y_; } }; ``` Excessive friendship exposes implementation and increases coupling. A public accessor or a carefully designed member function is preferable when it communicates the abstraction more clearly. A nested type is declared within a class and is useful when the type has meaning only in the context of its enclosing class. Examples include `iterator`, `Builder`, `Error`, or a private helper policy. A nested type does not automatically have access to an enclosing object’s non-static members; it is a type-scope relationship, not an object-composition relationship. A nested type may nevertheless access private names according to C++ access rules when it has an appropriate object or context. Operator Overloading as Interface DesignOperator overloading allows user-defined types to participate in familiar expressions. It should be used only when the operator’s meaning is natural and unsurprising. A matrix may reasonably overload `+`; a network connection should not overload `+` merely because the syntax is available. Operators such as assignment, subscript, function call, and arrow often belong as members because they directly manipulate the object. Symmetric arithmetic and comparison operators are commonly non-members. Modern C++ supports the three-way comparison operator `<=>`, which can reduce repetitive relational definitions when the ordering model is appropriate. An overloaded operator should preserve expected properties. If `a == b` is true, equality should normally be reflexive, symmetric, and transitive. A value-like `operator+` should not unexpectedly mutate its operands. Prefix and postfix increment have different conventions: postfix usually returns the previous value and may require a dummy `int` parameter. Stream operators are usually non-member functions: ```cpp std::ostream& operator<<(std::ostream& out, const Point& p); ``` Returning the stream by reference supports chaining. Operators should avoid surprising implicit conversions and should provide exception behaviour consistent with their underlying operations. Conversion Rules and Type SafetyConversions can improve expressiveness but can also create ambiguity and hidden behaviour. A non-explicit single-argument constructor, a conversion operator, or a standard numeric conversion may enable implicit conversion. For domain types, implicit conversion should be permitted only when it is safe, unsurprising, and information-preserving. ```cpp class FileSize { std::uint64_t bytes_; public: explicit FileSize(std::uint64_t bytes) : bytes_(bytes) {} std::uint64_t bytes() const { return bytes_; } }; ``` Here, `FileSize size = 10;` is rejected, making units explicit. A conversion operator can also be declared `explicit`; contextual conversions, such as use in an `if`, are treated specially by the language. The safe-bool idiom is therefore generally unnecessary in modern C++. Narrowing conversions deserve particular caution. Brace initialization rejects many narrowing cases, and compiler warnings should be enabled. Overloaded functions can become ambiguous when several conversion paths exist. Good interfaces minimize conversion magic and prefer named functions for transformations with semantic significance. Applied Case: Designing a Money Value TypeBackgroundThis illustrative case concerns a payment service that originally represents monetary values with `double`. Different modules round at different points, currencies are mixed accidentally, and negative amounts are sometimes accepted by refund code. Problem and AnalysisBinary floating-point representation is not generally exact for decimal fractions. More importantly, a raw `double` communicates neither currency nor rounding policy. The design requires a cohesive value type with a clear invariant: an amount is represented in the smallest currency unit, and its currency code is valid. ApproachA possible design is: ```cpp class Money { std::int64_t minor_; std::string currency_; public: Money(std::int64_t minor, std::string currency) : minor_(minor), currency_(std::move(currency)) { if (currency_.size() != 3) throw std::invalid_argument("currency must have three letters"); } std::int64_t minor_units() const noexcept { return minor_; } const std::string& currency() const noexcept { return currency_; } Money operator+(const Money& other) const { if (currency_ != other.currency_) throw std::invalid_argument("currency mismatch"); return Money(minor_ + other.minor_, currency_); } friend bool operator==(const Money&, const Money&) = default; }; ``` In production, currency validation would also check the permitted alphabet and arithmetic would address overflow. The constructor establishes the representation invariant. Addition is a value operation and refuses to combine incompatible currencies. The data remains private, and accessors do not permit arbitrary mutation. A safer interface might make currency a nested or separate `Currency` type rather than a string. Named factory functions could distinguish `Money::from_major_units` from `Money::from_minor_units`. If negative values are disallowed for a particular business concept, a separate `NonNegativeMoney` type or validated operation may be preferable to relying on convention. Outcome and Learning PointsThe revised design makes invalid combinations harder to express, centralizes validation, and gives reviewers a visible location for monetary rules. It does not solve every financial problem: tax policy, exchange rates, rounding modes, overflow, persistence, and audit requirements remain separate concerns. The principal lesson is that encapsulation is valuable because it places domain decisions at the boundary of a type instead of scattering them across callers. Common Design Errors and LimitationsSeveral mistakes recur in class-based C++ systems. Public data often allows invariants to be bypassed. A class with dozens of unrelated getters may merely expose a database row rather than provide an abstraction. Constructors that perform extensive I/O or call overridable functions make object creation fragile. A destructor should not normally throw, because exception propagation during stack unwinding can terminate the program. Returning pointers or references to internal storage may expose mutation or create dangling references. Returning a reference to a local object is always invalid. Raw owning pointers obscure lifetime responsibility; prefer values, containers, or smart pointers when ownership is required. The Rule of Three/Five is frequently misunderstood. Adding a destructor to a resource-owning class does not automatically make copying safe. Copying must either duplicate ownership correctly, transfer it, or be disabled. The Rule of Zero is generally the strongest design when applicable. Operator overloading can reduce readability when operators are given domain-specific meanings that differ from conventional expectations. Friend declarations can become a back door around encapsulation. Static mutable state can cause order-of-initialization problems, hidden dependencies, and data races. These are design risks, not merely stylistic concerns. Professional and Ethical ConsiderationsClass design affects safety, privacy, auditability, and regulatory compliance. A class representing identity, payment, health, or access-control information should restrict exposure and avoid unnecessary copying of sensitive data. Logging operators such as `operator<<` must not accidentally print credentials, tokens, or personal identifiers. Strong invariants can support security by rejecting malformed states early, but encapsulation is not a substitute for authorization, encryption, input validation, or threat modelling. Interfaces should document failure modes and avoid leaking sensitive information through exception messages. In concurrent systems, class invariants must be protected by an appropriate synchronization strategy; `static` does not mean thread-safe. Modern C++ development also benefits from static analysis, sanitizers, unit testing, property-based testing, code review, and compiler warnings. Contracts and assertions can document assumptions, but assertions should not replace validation of untrusted external input. Contemporary Direction and Best PracticesCurrent C++ practice favours value types, RAII, const-correctness, strong types, concepts, ranges, and standard-library facilities over manual resource management. Modules can improve interface boundaries, although class design remains the primary abstraction concern. `constexpr` and `consteval` permit some validation and computation at compile time. Attributes such as `[[nodiscard]]` can prevent ignored results from important operations. A disciplined class-design workflow is: 1. Identify the domain responsibility and write its invariant. 2. Decide whether the type is a value, an entity with identity, or a resource owner. 3. Keep representation private and define the smallest useful public interface. 4. Establish valid state through constructors and named factories. 5. Make observers `const` and operations explicit about ownership and failure. 6. Prefer the Rule of Zero; otherwise define or delete special members deliberately. 7. Add operators only when their conventional meaning is preserved. 8. Make conversions explicit unless implicit conversion is demonstrably safe. 9. Test boundary conditions, invalid states, copying, moving, and exception behaviour. Chapter SummaryClasses define user-defined types whose objects combine state and behaviour. Encapsulation protects representation, preserves invariants, reduces coupling, and allows implementation change. Constructors establish valid state; destructors and RAII manage lifetime. Const-correct member functions distinguish observation from mutation, while static members represent class-wide facilities and must be used cautiously. Friends and nested types are specialized mechanisms for expressing close relationships, but excessive use weakens boundaries. Operator overloading should make domain objects behave naturally, and implicit conversions should be restricted when they can hide errors or lose meaning. Strong class design combines access control with contracts, exception safety, ownership clarity, testing, and professional attention to security and privacy. Key TermsClass: A user-defined type that specifies data, operations, and access rules. Object: A concrete instance of a class with identity, state, and behaviour. Encapsulation: The integration and protection of representation and operations behind an interface. Invariant: A condition that must remain true for a valid object. Interface: The publicly observable operations and guarantees of a type. Access control: Rules determining which code may use class members. Constructor: A special function that initializes an object. Destructor: A special function invoked when an object’s lifetime ends. RAII: Resource management through object lifetime and deterministic destruction. Const-correctness: The disciplined use of `const` to prevent unintended modification. Static member: A class-level member shared by all objects of the class. Friend: A separately declared function or class granted private access. Nested type: A type declared within the scope of another class. Operator overloading: Defining operator syntax for user-defined types. Conversion: A transformation from one type to another. Explicit constructor: A constructor that does not generally participate in implicit conversion. Cohesion: The degree to which a class’s responsibilities belong together. Coupling: The degree of dependency between software components. Rule of Zero: The practice of relying on resource-managing members rather than defining special members manually. Exception safety: Guarantees about validity, state, and resource preservation when operations fail. Review QuestionsShort Answer Questions1. What is the relationship between a class, an object, and an invariant? 2. Why should member initialization normally use a member-initializer list? 3. What distinguishes a static member function from a non-static member function? 4. Why are symmetric comparison operators often implemented as non-member functions? 5. What problem does an `explicit` constructor help prevent? Long Answer Questions1. Explain encapsulation as a design discipline rather than merely an access-control mechanism. 2. Discuss constructors, destructors, RAII, and the Rule of Zero in resource-safe C++ design. 3. Explain const-correctness and show how it influences the design of a class interface. 4. Evaluate the benefits and risks of friend declarations and nested types. 5. Describe how operator overloading and conversion control can improve or damage type safety. Analytical / Application Questions1. Design a class for a bounded queue. State its invariant, public operations, failure behaviour, and relevant special-member decisions. 2. A class exposes public fields `price`, `currency`, and `discount`, and several modules modify them directly. Analyze the design and propose an encapsulated replacement. 3. A resource-owning class defines a destructor but leaves compiler-generated copying enabled. Explain the likely risks and recommend a redesign using either the Rule of Zero or deliberate copy/move operations. 22. Multiple Choice Questions with AnswersQuestion 1Which design most directly preserves a class invariant? A. Making all data public for easy access B. Providing operations that validate state changes C. Using only static member functions D. Returning mutable references to every data member Correct Answer: B. Providing operations that validate state changes Question 2Why should a constructor commonly use a member-initializer list? A. It guarantees virtual dispatch during construction B. It initializes members before the constructor body and is required for some members C. It makes all members public D. It prevents the destructor from running Correct Answer: B. It initializes members before the constructor body and is required for some members Question 3What is a principal benefit of the Rule of Zero? A. It requires every class to define five special functions B. It eliminates the need for constructors C. It lets resource-managing members handle special operations safely D. It permits unrestricted copying of raw pointers Correct Answer: C. It lets resource-managing members handle special operations safely Question 4A static member function cannot directly access a non-static data member because it: A. has no implicit this pointer B. is always private C. cannot return a value D. is executed only at compile time Correct Answer: A. has no implicit this pointer Question 5Which operator design is most appropriate for a value-like Money class? A. Addition silently combines different currencies B. Addition mutates both operands C. Addition checks currency compatibility and returns a new value D. Addition converts all values implicitly to double Correct Answer: C. Addition checks currency compatibility and returns a new value Question 6What is the main purpose of declaring a single-argument constructor explicit? A. To allow implicit conversion in every context B. To prevent unintended implicit conversions C. To make the constructor virtual D. To prevent object destruction Correct Answer: B. To prevent unintended implicit conversions Question 7Which statement about friendship in C++ is correct? A. Friendship is automatically inherited by subclasses B. A friend declaration makes all members public C. Friendship grants selected access and should be used deliberately D. Friendship is always reciprocal Correct Answer: C. Friendship grants selected access and should be used deliberately Question 8For a const object, which member function can normally be called? A. A non-const mutator only B. A const-qualified observer C. A function that modifies private state D. A destructor explicitly invoked by the client Correct Answer: B. A const-qualified observer Question 9A class representing a file owns a raw resource and defines only a destructor. What is the chief risk? A. The class cannot be instantiated B. Compiler-generated copying may duplicate ownership incorrectly C. The destructor will never execute D. The class becomes automatically thread-safe Correct Answer: B. Compiler-generated copying may duplicate ownership incorrectly Question 10Which statement best describes a nested type? A. It is always an object contained within the enclosing class B. It is a type declared in the enclosing class’s scope C. It automatically has a pointer to every enclosing object D. It cannot be private Correct Answer: B. It is a type declared in the enclosing class’s scope
|