8 Classes and Introductory Object-Oriented Programming

A progressive guide to designing and using C++ classes, from encapsulated state and constructors through inheritance, runtime polymorphism, and sound object-oriented design.

Types, State, and Behavior

A is a user-defined type that combines state with operations on that state. A definition describes the structure and behavior of objects; it does not, by itself, create an object.

A can contain the same broad kinds of members as a , but their defaults differ:

  • Members of a are public by default.

  • Members of a are private by default.

This makes a convenient for a simple group of related values, while a is often preferable when the type must protect its internal state. The choice should communicate the intended interface, not merely reflect personal style.

Takeaway: Both forms define types, but their default access levels encourage different design conventions.

Data and Operations

A represents information stored in an object, such as an account balance or a rectangle’s dimensions. A represents an operation associated with that object, such as depositing money or computing an area.

Non-static member functions operate on a particular object. They can access that object’s non-static data members through the implicit this pointer. A read-only operation should usually be marked const; for example, a function declared as double getBalance() const promises not to modify the object’s observable state and can be called on a const object.

A may be declared inside the and defined later. An out-of- definition uses the , as in Counter::increment(), to identify the to which the function belongs.

Takeaway: Data members describe state, member functions define behavior, and const communicates when an operation is read-only.

and Access Control

combines data and operations while controlling how other code can access the data. The goal is not merely to hide variables; it is to make the responsible for preserving valid state.

C++ provides three main access specifiers:

  • public members are accessible from code that can access the object.

  • private members are accessible only to members and friends of the .

  • protected members are accessible to members and friends of the and to members of derived classes.

For example, a temperature can keep its stored value private and expose a setter that rejects values below absolute zero. Client code cannot directly bypass that validation. The rule that the stored temperature cannot be below absolute zero is a invariant.

A practical convention is to keep data members private and expose a small public interface of operations. Protected data should also be used cautiously because it exposes implementation details to derived classes.

Takeaway: Access control turns boundaries into enforceable rules and helps preserve invariants.

Constructing Valid Objects

A initializes an object during object creation. It has the same name as the , has no return type, and is called automatically as part of initialization. Constructors may take parameters and may be overloaded.

Member initializer lists are the preferred way to initialize data members because those members are initialized before the body runs. For example, a rectangle can initialize its width and height directly before any additional statements execute.

A with no required arguments is a default . It may be written explicitly or generated by the compiler when the language rules permit it. A single-argument can be marked explicit when implicit conversion would be undesirable; this prevents an integer, for example, from being silently treated as a distance in an unintended context.

Common initialization forms include direct initialization with parentheses, list initialization with braces, and copy-list initialization. The important design question is whether every newly created object begins in a valid state.

Takeaway: Constructors establish initial validity, and initializer lists make that initialization direct and reliable.

Using Objects and Managing Lifetime

After an object exists, its public members are commonly accessed with the dot operator, as in box.area(). A pointer to an object uses the arrow operator, as in pointer->area(). The arrow expression is equivalent to (*pointer).area().

Automatic objects are often the simplest and safest choice because their lifetime is tied to their scope. Dynamic allocation is sometimes necessary, but it introduces ownership responsibilities. When dynamic ownership is needed, a smart pointer such as one created by std::make_unique can release the object automatically when it is no longer needed.

Avoid unnecessary dynamic allocation. Standard library resource-managing types and automatic objects usually make object lifetime clearer and reduce the risk of leaks.

Takeaway: Choose access syntax based on whether you have an object or a pointer, and make ownership explicit.

and Relationships

allows a derived to extend or specialize a base . Public expresses an “is-a” relationship: a Dog is an Animal. The derived receives the accessible interface of the base and can add behavior of its own.

Base- private members are not directly accessible in a derived . Public and protected base members may be available according to the and access rules. Even when protected access is available, keeping representation data private and exposing carefully designed functions usually makes the base easier to change safely.

is appropriate when the derived type is genuinely substitutable for the base type. When the relationship is instead “has-a” or when reuse does not represent substitutability, composition—one object containing another—is often clearer.

Takeaway: Use for a meaningful substitutable relationship; prefer composition when one type simply contains or uses another.

Runtime

lets one interface work with objects that have different implementations. A base- function declared as a may be overridden by a derived . When the function is called through a base- pointer or reference, the implementation selected at runtime can match the object’s actual derived type.

For example, a function accepting const Animal& can call speak() on either a dog or a cat and receive the appropriate overridden behavior. A derived implementation should use the , which asks the compiler to verify that the intended override matches a virtual base function.

A used polymorphically should generally have a virtual destructor when objects may be destroyed through a base pointer. This allows destruction to proceed safely through the base interface.

Takeaway: Virtual dispatch separates the interface used by client code from the implementation supplied by the actual object.

Abstract Interfaces and Design Practice

A pure is declared by placing = 0 in its declaration, such as virtual double area() const = 0. A with at least one pure is an . It defines an interface but cannot be instantiated directly.

Derived classes must provide implementations for required pure virtual functions before they can be instantiated, unless they remain abstract themselves. This lets a base specify capabilities while leaving concrete details to specialized types.

Good design usually follows a small set of principles:

  1. Give each one clear responsibility.

  2. Keep representation data private unless direct access is genuinely appropriate.

  3. Use constructors to establish valid initial state.

  4. Validate inputs at the boundary.

  5. Mark read-only member functions const.

  6. Use the when overriding virtual functions.

  7. Use public only for a genuine “is-a” relationship.

  8. Prefer composition when does not model the relationship clearly.

  9. Avoid unnecessary dynamic allocation.

Takeaway: A well-designed exposes a clear contract, protects its state, and uses and only where they clarify the model.