Object-Oriented Programming with C++

Chapter 3
Classes, Objects, and Interfaces

Bachelor Program in Mechatronics Engineering
Object-Oriented Programming with C++   •   Bachelor

Classes, Objects, and Interfaces

Introduction

Object-oriented programming (OOP) organizes software around objects that combine state and behavior. In mechatronics, this model is particularly useful because systems contain identifiable entities—motors, sensors, actuators, controllers, trajectories, and communication channels—with properties and operations that must remain consistent.

A class is a programmer-defined type. An object is an instance of a class. A well-designed class does more than group data and functions: it protects an invariant, provides meaningful operations, and prevents clients from placing the object into an invalid state. This chapter develops the C++ mechanisms used to achieve that goal: access control, constructors, destructors, member functions, static members, friend declarations, nested types, header–implementation separation, and interface-oriented design.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish classes, objects, interfaces, state, behavior, and invariants;
  • select appropriate access control for data and operations;
  • design constructors, destructors, and member functions safely;
  • use const, static, friend, and nested types appropriately;
  • separate declarations from implementations using header and source files;
  • design interfaces that preserve class invariants;
  • apply these principles to a realistic mechatronics component.

1. Classes, Objects, and Abstraction

A class defines a set of possible objects. It specifies:

  • State: data stored by an object;
  • Behavior: operations that may inspect or modify that state;
  • Identity: the fact that two objects can have different existence even when their values are equal;
  • Invariant: a condition that must remain true whenever an object is observable by client code.

For example, a motor command class might maintain the invariant that its command lies between −1.0 and +1.0. The client should not modify the command directly; it should use a function that validates the new value.

class MotorCommand {
private:
    double value_{};

public:
    explicit MotorCommand(double value);
    void set(double value);
    double value() const;
};

The class is a type description. An object is created when storage is allocated and its constructor is executed:

MotorCommand leftMotor{0.25};
MotorCommand rightMotor{0.0};

The two objects have the same type but independent state. Abstraction means that users need to know what an operation guarantees, not how the operation is implemented.

Interfaces and implementations

An interface is the publicly visible contract of a component. It includes operation names, parameter types, return types, constness, exceptions or error behavior, and documented preconditions and postconditions. An implementation is the hidden code that fulfills that contract.

A good interface is small, coherent, and difficult to misuse. It should expose domain actions rather than representation details. For example, setDutyCycle(0.4) communicates more clearly than direct access to a member named rawValue.

2. Access Control and Encapsulation

C++ provides three access specifiers:

SpecifierAccess from class membersAccess from derived classesAccess from unrelated code
privateYesNo directlyNo
protectedYesYesNo
publicYesYesYes

In a class, members are private by default. In a struct, members are public by default. The language difference is mainly default access; both can contain data, functions, constructors, and nested types.

Encapsulation is not merely hiding data. It is the controlled management of valid state transitions. Private data prevents uncontrolled modification, while public operations define permitted transitions.

class Temperature {
private:
    double celsius_;

public:
    explicit Temperature(double celsius) : celsius_{celsius} {
        if (celsius < -273.15) {
            throw std::invalid_argument{'Below absolute zero'};
        }
    }

    double celsius() const { return celsius_; }
};

The invariant is celsius_ ≥ −273.15 °C. Every constructor and mutator must preserve it. In production code, the header would include <stdexcept>.

protected should be used cautiously. It exposes implementation details to derived classes and can make future changes difficult. Prefer private data and protected or public member functions when inheritance is genuinely required.

3. Constructors and Object Lifetime

A constructor establishes the initial invariant of an object. It has the same name as the class and no declared return type.

Common constructor forms

class SensorReading {
private:
    double value_;
    bool valid_;

public:
    SensorReading() : value_{0.0}, valid_{false} {}
    SensorReading(double value, bool valid)
        : value_{value}, valid_{valid} {}
};

The default constructor takes no arguments. A parameterized constructor receives initial values. An explicit constructor prevents unintended implicit conversions when a one-argument constructor is used:

class ChannelId {
public:
    explicit ChannelId(int number);
};

Without explicit, an integer might be silently converted to ChannelId in contexts where that conversion is not intended.

Member-initializer lists

Members should normally be initialized in a member-initializer list. Initialization occurs in declaration order, not the order written in the list. Therefore, declare members in a logical dependency order and write the initializer list consistently.

class Controller {
private:
    double gain_;
    double limit_;

public:
    Controller(double gain, double limit)
        : gain_{gain}, limit_{limit} {}
};

A member that is const, a reference, or a class without a default constructor must be initialized this way. Initialization is generally preferable to assignment because it constructs the member directly.

Delegating and defaulted constructors

A constructor can delegate to another constructor:

class Port {
public:
    Port() : Port{9600} {}
    explicit Port(int baudRate) : baudRate_{baudRate} {}

private:
    int baudRate_;
};

Use = default when the compiler-generated behavior expresses the intended design. Use = delete to prohibit an operation:

class HardwareHandle {
public:
    HardwareHandle() = default;
    HardwareHandle(const HardwareHandle&) = delete;
};

Destructors and resource ownership

A destructor runs when an object’s lifetime ends. It has the form ~ClassName() and takes no parameters. A destructor is needed when a class owns a resource requiring release, such as a file handle, socket, lock, or dynamically allocated memory.

Modern C++ favors RAII—Resource Acquisition Is Initialization. A resource is acquired in a constructor and released in a destructor. Standard library types such as std::vector, std::string, and smart pointers implement this approach.

class LogFile {
public:
    explicit LogFile(const char* path);
    ~LogFile();

    LogFile(const LogFile&) = delete;
    LogFile& operator=(const LogFile&) = delete;

private:
    std::FILE* file_{nullptr};
};

If a base class is intended for polymorphic use, its destructor should generally be virtual:

class Device {
public:
    virtual ~Device() = default;
};

Deleting a derived object through a base pointer without a virtual destructor causes undefined behavior.

4. Member Functions and Const-Correctness

A member function operates on an object. Non-static member functions have an implicit access to the current object through this.

class Encoder {
public:
    void reset() { count_ = 0; }
    int count() const { return count_; }

private:
    int count_{0};
};

The trailing const promises that the function will not modify the object’s observable state. It allows the function to be called on const objects:

const Encoder encoder;
int position = encoder.count();

Use const for functions that are observers. It improves documentation, compiler checking, and usability.

A member function may be overloaded by constness, although this should be done only when the behavior is genuinely appropriate:

class Buffer {
public:
    char& at(std::size_t index);
    const char& at(std::size_t index) const;
};

Returning a non-const reference can expose internal state and allow invariant violations. Prefer value returns or controlled mutators unless reference access is part of a carefully designed interface.

Preconditions and postconditions

A precondition states what must be true before an operation. A postcondition states what the operation guarantees afterward. For a motor command:

  • precondition: the input is finite;
  • postcondition: the stored command is within the permitted interval;
  • invariant: every observable stored command is within that interval.

An interface should specify whether invalid input is rejected with an exception, converted to a safe value, reported through a status result, or handled in another defined way. Silent, undocumented behavior is unsafe in control software.

5. Static Members

A non-static data member exists separately in every object. A static data member belongs to the class and has one shared instance.

class Device {
public:
    Device() { ++activeCount_; }
    ~Device() { --activeCount_; }

    static int activeCount() { return activeCount_; }

private:
    inline static int activeCount_{0};
};

The inline static declaration, available in modern C++, defines the member within the class definition while maintaining one shared object across translation units. In older standards, the member generally requires one out-of-class definition in a source file.

Static member functions have no this pointer and can directly access only static members. They are suitable for operations related to the class as a whole, such as factories, counters, or validation utilities.

Shared mutable state introduces risks: hidden coupling, initialization-order problems, and concurrency issues. In real-time or multithreaded systems, access may require synchronization, and the timing cost of synchronization must be considered.

6. Friend Declarations

A friend function or friend class is granted access to private and protected members. Friendship is explicit, not automatically inherited, reciprocal, or transitive.

class Voltage;

class Voltage {
public:
    explicit Voltage(double value) : value_{value} {}

    friend Voltage operator+(Voltage a, Voltage b);
    double value() const { return value_; }

private:
    double value_;
};

Voltage operator+(Voltage a, Voltage b) {
    return Voltage{a.value_ + b.value_};
}

Friendship can support symmetric operators and tightly coupled components, but excessive friendship weakens encapsulation. Before declaring a friend, ask whether a public member function, a non-member function using the public interface, or a carefully designed helper would be clearer.

7. Nested Types

A nested type is declared inside a class and is scoped within that class. It is useful when a type has meaning only in the context of its enclosing class.

class Motor {
public:
    enum class Direction { Forward, Reverse, Stopped };

    Direction direction() const;
};

Motor::Direction direction = Motor::Direction::Stopped;

Nested types improve namespace organization and express conceptual ownership. They should not be used merely to make a large class appear organized. If a type has independent meaning or will be widely reused, a namespace-level declaration may be better.

A nested class does not automatically have access to an enclosing object’s non-static members, and the enclosing class does not automatically have special access to all members of the nested class. Access must be designed explicitly.

8. Header–Implementation Separation

A conventional C++ component is divided into a header file and a source file.

MotorCommand.hpp

#ifndef MOTOR_COMMAND_HPP
#define MOTOR_COMMAND_HPP

class MotorCommand {
public:
    explicit MotorCommand(double value);
    void set(double value);
    double value() const;

private:
    static double clamp(double value);
    double value_;
};

#endif

MotorCommand.cpp

#include "MotorCommand.hpp"
#include <algorithm>
#include <cmath>
#include <stdexcept>

MotorCommand::MotorCommand(double value) : value_{clamp(value)} {
    if (!std::isfinite(value)) {
        throw std::invalid_argument{'Motor command must be finite'};
    }
}

void MotorCommand::set(double value) {
    if (!std::isfinite(value)) {
        throw std::invalid_argument{'Motor command must be finite'};
    }
    value_ = clamp(value);
}

double MotorCommand::value() const {
    return value_;
}

double MotorCommand::clamp(double value) {
    return std::max(-1.0, std::min(1.0, value));
}

This example illustrates a policy choice: finite values outside the range are clamped, whereas non-finite values are rejected. That policy must be documented because clamping can conceal control errors. A different safety-critical design might reject every out-of-range value.

Header guards prevent repeated inclusion. #pragma once is also widely supported, but traditional guards are portable. Headers should contain declarations and small, deliberately inline definitions; source files contain implementation details. This reduces recompilation and limits dependencies.

A translation unit is a source file after preprocessing. The compiler processes translation units separately, and the linker combines their object files. A declaration tells the compiler that an entity exists; a definition supplies its implementation or storage. Violating the One Definition Rule can produce linker errors or undefined behavior.

9. Designing Invariant-Preserving Interfaces

An invariant-preserving design follows a repeatable process:

  1. Identify the entity and its responsibility.
  2. State the valid state space in plain language.
  3. Make representation data private.
  4. Validate every construction path.
  5. Ensure every mutator preserves the invariant.
  6. Provide const observers for safe inspection.
  7. Define failure behavior.
  8. Test boundary values and invalid inputs.

Consider a bounded joint angle. If the permitted interval is [−90°, +90°], the invariant is expressed as:

−90° ≤ angle ≤ 90°

Here angle is measured in degrees, and the invariant applies after successful construction and after every successful update. It does not by itself determine whether invalid inputs should be rejected or saturated.

A robust interface might be:

class JointAngle {
public:
    explicit JointAngle(double degrees);
    void setDegrees(double degrees);
    double degrees() const noexcept;

private:
    double degrees_;
};

The class should not expose degrees_ publicly. If angle units could be confused, distinct types or documented naming should be used. In engineering software, unit ambiguity is a major source of faults.

Worked Example: A Safe Motor Channel

Suppose a motor driver accepts a normalized command in [−1, 1]. The class must reject NaN and infinity, preserve the range, and count all constructed channels.

#include <cmath>
#include <stdexcept>

class MotorChannel {
public:
    explicit MotorChannel(double command) : command_{validate(command)} {
        ++instances_;
    }

    ~MotorChannel() { --instances_; }

    MotorChannel(const MotorChannel&) = delete;
    MotorChannel& operator=(const MotorChannel&) = delete;

    void setCommand(double command) {
        command_ = validate(command);
    }

    double command() const noexcept { return command_; }
    static int instances() noexcept { return instances_; }

private:
    static double validate(double command) {
        if (!std::isfinite(command) || command < -1.0 || command > 1.0) {
            throw std::invalid_argument{'Command outside [-1, 1]'};
        }
        return command;
    }

    double command_;
    inline static int instances_{0};
};

The constructor validates before the object becomes usable. The setter validates before changing state, so a failed operation leaves the prior valid state unchanged. The destructor maintains the shared count. Copying is disabled because the example models a unique hardware channel; a real design would document whether copying, moving, or sharing the hardware resource is safe.

Applied Case: Encapsulating a Two-Axis Gantry

Background

An educational gantry has two linear axes, X and Y. Each axis has a position limit, and a controller receives target coordinates from a planning algorithm.

Problem

An early implementation exposed public variables. A planning routine could assign a target beyond the physical travel range, while another routine interpreted millimetres as metres. The resulting commands could cause limit switches to activate or motors to stall.

Analysis

The class should own position limits, use explicit units in names or types, validate target positions, and provide a single operation for coordinated movement. The interface should distinguish a requested target from the current measured position. It should also define failure behavior when a target is invalid or a limit switch is active.

Approach

An illustrative interface is:

class Axis {
public:
    Axis(double minimumMillimetres, double maximumMillimetres);
    void setTargetMillimetres(double target);
    double targetMillimetres() const;
    double positionMillimetres() const;
    bool atLimit() const;

private:
    double minimum_;
    double maximum_;
    double target_;
    double position_;
};

class Gantry {
public:
    Gantry(Axis x, Axis y);
    void moveTo(double xMillimetres, double yMillimetres);

private:
    Axis x_;
    Axis y_;
};

Gantry::moveTo should validate both coordinates before changing either axis. This gives the operation an all-or-nothing property: a rejected two-axis command does not leave the gantry with only one updated target. The implementation may additionally check emergency-stop state, homing status, and feedback validity.

Outcome

The revised design prevents direct corruption of limits, makes units visible in the interface, and centralizes safety decisions. Tests can construct axes with small limits and verify boundary acceptance, out-of-range rejection, and unchanged state after a failed command.

Learning points

The central lesson is that an interface is a safety boundary. Encapsulation is especially valuable where software commands physical energy. A class cannot replace hardware interlocks, watchdogs, limit switches, or formal safety processes, but it can reduce software misuse and make safety assumptions explicit.

Challenges, Limitations, and Best Practices

Classes do not automatically produce good architecture. A class can be overlarge, expose too many operations, or hide expensive behavior behind a simple function call. Important limitations include:

  • Invariants may be incomplete: physical systems have timing, thermal, electrical, and mechanical constraints that a value class cannot capture alone.
  • Exceptions may be unsuitable: hard real-time or deeply embedded systems may prefer error codes, status objects, or prevalidated configuration.
  • Static state complicates testing: shared counters and registries can create order dependence.
  • Friendship increases coupling: implementation changes may affect friends.
  • Inheritance can be fragile: prefer composition unless substitutability is clear.
  • Thread safety is not automatic: a private member can still require synchronization.

Recommended practices include using RAII, applying const consistently, preferring private data, marking single-argument constructors explicit, documenting units and failure behavior, keeping classes cohesive, deleting unsupported special members, and testing constructor and mutator boundaries. Interface reviews should consider misuse cases, not only normal cases.

Modern C++ development also benefits from static analysis, sanitizers, unit testing, code review, and continuous integration. AI-assisted coding can help generate scaffolding or test ideas, but generated code must be reviewed for lifetime errors, undefined behavior, unit mistakes, exception guarantees, and compliance with project standards. In safety-related mechatronics, responsibility remains with qualified engineers and the applicable regulatory process.

Chapter Summary

Classes define types that combine state and behavior; objects are their instances. Encapsulation uses access control to protect representation and enforce invariants. Constructors establish valid initial state, while destructors and RAII manage resource lifetime. Member functions express operations, and const observers communicate non-modifying behavior. Static members represent class-wide state, friend declarations provide narrowly controlled exceptional access, and nested types organize closely related concepts. Header–implementation separation supports modular compilation and hides implementation details. The most important design principle is to make invalid states difficult or impossible to create while providing interfaces that clearly specify units, contracts, and failure behavior. In mechatronics, these practices complement—not replace—hardware safeguards and system-level safety engineering.

Key Terms

Class: A user-defined C++ type describing data and operations.

Object: A runtime instance of a class.

Encapsulation: Controlled access to representation and behavior.

Abstraction: Presenting essential operations while hiding implementation details.

Invariant: A condition that remains true for every valid observable object state.

Interface: The public contract through which clients use a component.

Constructor: A special member function that initializes an object.

Destructor: A special member function that runs when an object’s lifetime ends.

RAII: Resource Acquisition Is Initialization; tying resource ownership to object lifetime.

Member function: A function associated with a class and usually operating on an object.

Const-correctness: Using const to express and enforce non-modifying behavior.

Static member: A member shared by the class rather than separately stored in each object.

Friend: A specially authorized function or class with private access.

Nested type: A type declared within the scope of another class.

Precondition: A requirement that must hold before an operation begins.

Postcondition: A guarantee supplied after an operation completes.

Header file: A file containing declarations and interface information.

Translation unit: A source file after preprocessing.

Composition: Building a class from objects of other classes.

One Definition Rule: C++ requirements limiting valid definitions across a program.

Review Questions

Short Answer Questions

  1. What is the difference between a class and an object?
  2. Why should representation data usually be private?
  3. What is the purpose of a constructor?
  4. What does a trailing const on a member function indicate?
  5. Why might a polymorphic base class need a virtual destructor?

Long Answer Questions

  1. Explain how access control and invariants work together in a mechatronics class.
  2. Compare non-static and static data members, including risks of shared mutable state.
  3. Explain RAII and discuss how it prevents resource leaks.
  4. Describe the advantages of separating a header from an implementation file.
  5. Discuss the benefits and limitations of friend declarations and nested types.

Analytical / Application Questions

  1. Design an interface for a temperature sensor that rejects physically impossible readings. State its invariant, failure policy, and public operations.
  2. A two-axis robot command updates the X coordinate successfully but rejects the Y coordinate. Analyze why this may be unsafe and propose an interface that provides all-or-nothing validation.
  3. A class exposes a public double speed field and allows values of NaN, negative speed, and values above the motor rating. Redesign the class and justify each access-control and validation decision.

Multiple Choice Questions with Answers

Question 1

Which statement best describes an invariant?

A. A condition that must remain true for every valid observable state of an object

B. A local variable created inside a member function

C. A function that can access private members

D. A source file containing only declarations

Correct Answer: A. A condition that must remain true for every valid observable state of an object

Question 2

What is the main purpose of a constructor?

A. To destroy an object and release its resources

B. To establish the initial state and invariant of an object

C. To make every data member public

D. To create a translation unit

Correct Answer: B. To establish the initial state and invariant of an object

Question 3

What does a trailing const on a member function communicate?

A. The function can be called only once

B. The function is shared by all objects

C. The function does not modify the object’s observable state

D. The function cannot return a value

Correct Answer: C. The function does not modify the object’s observable state

Question 4

Which member is shared by all objects of a class?

A. A non-static data member

B. A local variable

C. A reference parameter

D. A static data member

Correct Answer: D. A static data member

Question 5

Why is an explicit single-argument constructor useful?

A. It prevents unintended implicit conversions

B. It automatically makes the class abstract

C. It forces all members to be static

D. It disables destruction

Correct Answer: A. It prevents unintended implicit conversions

Question 6

Which technique ties resource release to object lifetime?

A. Function overloading

B. RAII

C. Header guarding

D. Friendship

Correct Answer: B. RAII

Question 7

What is a major risk of exposing a mutable data member publicly?

A. The compiler cannot create objects

B. The class cannot have a destructor

C. Clients can violate the class invariant directly

D. The source file cannot be compiled

Correct Answer: C. Clients can violate the class invariant directly

Question 8

What is a principal benefit of header–implementation separation?

A. It makes all functions virtual

B. It eliminates the need for constructors

C. It guarantees thread safety

D. It separates the public contract from implementation details

Correct Answer: D. It separates the public contract from implementation details

Question 9

A friend declaration primarily grants which capability?

A. Controlled access to private and protected members

B. Automatic inheritance from every base class

C. Automatic conversion between unrelated types

D. Permission to ignore all compilation errors

Correct Answer: A. Controlled access to private and protected members

Question 10

If a function validates a new value before assigning it, what useful property can result when validation fails?

A. The object is automatically destroyed

B. The prior valid state remains unchanged

C. All static members are reset

D. The function becomes virtual

Correct Answer: B. The prior valid state remains unchanged

Object-Oriented Programming with C++
Chapter 3 of 14