Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

C has no built-in class inheritance or virtual dispatch; C++ does. In C, you can assemble similar behavior from structs and function pointers, but you must define the interface, dispatch, and object lifetime yourself. In C++, inheritance can provide a typed runtime interface, but it is a design choice—not a default—and its timing, memory, code-size, and safety implications depend on your target and toolchain.

What inheritance means in C versus C++

C provides structs, not derived classes

A C struct is an ordered sequence of members. The language does not provide classes, access control, constructors, destructors, or virtual functions. To represent several interchangeable device implementations, a C program typically combines state with function pointers, or passes operations as separate functions. That is a manually constructed interface, not language-level inheritance.

Layout still matters. A struct member’s position and the rules for converting pointers to and from a struct’s initial member do not make arbitrary, unrelated structs interchangeable. A cast does not create a valid subtype relationship: code must respect the C object representation and aliasing rules.

C++ provides explicit, typed inheritance

C++ lets a class derive from one or more base classes, with public, protected, or private inheritance. It also supports virtual inheritance. A public base is the usual choice for an “is-a” relationship that callers should be able to use through the base interface. A derived class can override virtual functions; an abstract base can define an interface without being directly instantiated. Microsoft’s C++ inheritance documentation describes these mechanisms.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Inheritance does not automatically make every call polymorphic. A virtual function enables runtime dispatch when called through a base pointer or reference. A nonvirtual call is resolved according to the static type of the expression.

How to model an interface in C

For a small embedded driver interface, one explicit pattern is an operations table plus a context pointer. The operations table identifies the functions; the context points to the concrete driver’s state. This avoids pretending that one concrete struct is another type, and makes the dispatch and association with state visible.

struct sensor_ops {
    int (*read)(void *context, int *value);
};

struct sensor {
    const struct sensor_ops *ops;
    void *context;
};

struct i2c_sensor {
    /* I2C-specific state */
};

static int i2c_sensor_read(void *context, int *value)
{
    struct i2c_sensor *device = context;
    /* Read from this device and set *value. */
    (void)device;
    (void)value;
    return 0;
}

static const struct sensor_ops i2c_sensor_ops = {
    .read = i2c_sensor_read
};

int sensor_read(struct sensor *sensor, int *value)
{
    if (sensor == NULL || sensor->ops == NULL ||
        sensor->ops->read == NULL) {
        return -1;
    }
    return sensor->ops->read(sensor->context, value);
}

This sketch leaves hardware access and error conventions to the project. Initialization must set a valid operations table and context before use. The code that constructs the sensor also has to ensure the context remains alive for every call. If the application does not need runtime substitution, ordinary functions taking a concrete device pointer may be simpler and easier to audit.

Keep layout and lifetime explicit

  • Use a shared state struct or an explicitly embedded interface member when sharing layout is genuinely useful; document how the interface relates to the concrete object.
  • Do not cast an arbitrary object pointer to an unrelated struct pointer and treat it as a base object.
  • Define who initializes and owns each object, how long its context remains valid, and whether an operation can be called before initialization.
  • Decide how failures, optional operations, and cleanup are represented. A function table does not supply constructors, destructors, or ownership rules for you.

How virtual dispatch works in C++

Microsoft Learn defines a virtual function as “a member function that you expect to be redefined in derived classes.” When a virtual function is called through a base pointer or reference, the selected implementation belongs to the object’s dynamic type. The same expression can therefore call different driver implementations without changing the caller. With a nonvirtual function, the call follows the type known at the call site.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Here is a small interface for devices selected at runtime:

class Sensor {
public:
    virtual int read(int& value) = 0;
    virtual ~Sensor() = default;
};

class I2cSensor final : public Sensor {
public:
    int read(int& value) override {
        // Read from the I2C device and set value.
        value = 0;
        return 0;
    }
};

class SpiSensor final : public Sensor {
public:
    int read(int& value) override {
        // Read from the SPI device and set value.
        value = 0;
        return 0;
    }
};

int sample(Sensor& sensor, int& value) {
    return sensor.read(value);
}

override asks the compiler to check that a function really overrides a base virtual function. final prevents further derivation from I2cSensor in this example. The virtual destructor matters if an object may be destroyed with delete through a Sensor*: it ensures destruction proceeds through the derived type. If the program never deletes through a base pointer, choose and document a different lifetime strategy rather than adding heap allocation merely to use an interface.

Rank #4

Polymorphism does not require heap allocation. Firmware can create concrete drivers with static or automatic storage and pass a base reference or pointer to code that needs the common interface. The selection policy and lifetime remain application design decisions.

Do virtual functions cost too much on a microcontroller?

There is no universal byte or cycle penalty that applies to every MCU and compiler. C++ does not prescribe one object layout or ABI for virtual functions. Common implementations use per-object metadata and an indirect call for virtual dispatch, but the exact representation, generated code, and optimization depend on the compiler, ABI, target, and call site. Code size also depends on which implementations are included and what the optimizer can see.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The practical question is whether the cost and behavior fit the project’s budgets. For a timing-critical path or a tight memory budget, inspect the generated output and measure on the selected compiler and MCU under the relevant build settings. Record what was measured; a result for one target, ABI, or optimization configuration is not a general C++ overhead figure.

  • Worst-case timing: determine whether an indirect call and the possible target implementations fit the timing analysis and interrupt constraints.
  • Memory and code size: inspect actual object layouts, maps, and binaries for the target build rather than relying on a rule of thumb.
  • Predictability: identify which implementations can be selected at runtime and how that selection is constrained.
  • Testing and changeability: a shared interface can make it easier to substitute a fake driver in tests or add an implementation, if those are real needs.
  • Project constraints: check the applicable coding standard, compiler support, and static-analysis profile before adopting a pattern.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When composition, templates, or tagged dispatch fit better

Approach Best fit Main trade-off
Composition A device has a policy, service, or helper, rather than being substitutable for a base type. Keeps responsibilities separate, but does not by itself provide runtime substitution between different concrete types.
Templates or concepts The concrete type is known at build time and callers can be specialized for it. Avoids runtime virtual dispatch, but may produce more code for multiple instantiations and does not provide the same open runtime interface.
Closed tagged dispatch The set of implementation kinds is deliberately fixed and the caller can switch on a tag. Makes the supported set explicit, but adding a kind may require updating dispatch sites.
Virtual interface Different implementations need to be selected behind one base pointer or reference, including implementations added independently of callers. Supports open runtime substitution, with toolchain-dependent representation and call behavior to account for.
C function table A C codebase needs explicit runtime dispatch through a stable operations interface. Provides manual dispatch without C++ classes; initialization, context validity, and lifetime remain explicit responsibilities.

Composition is usually the clearer choice for a “has-a” relationship. If a sensor owns a calibration policy or uses a bus service, containing or referencing that collaborator expresses the relationship without making it a subtype.

When the concrete type is available at compile time, templates—and concepts where supported by the project’s C++ toolchain—can express requirements and let the compiler generate type-specific code. LLVM’s Programmer’s Manual calls generic programming “compile-time duck typing” or “static polymorphism” and recommends it for one class of interface problem. LLVM also describes closed, tag-dispatched hierarchies as a fit when consumers should not extend the set of types. These options can generate more efficient code than open virtual dispatch, though the result still depends on the program and toolchain.

Inheritance and safety-oriented firmware

For projects using MISRA C++:2023, review inheritance against the project’s adopted profile and the full rule text. A published rule summary identifies these relevant requirements:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Rule 13.1.1, advisory: “Classes should not be inherited virtually.”
  • Rule 13.1.2, required: “A base class shall not be both virtual and non-virtual in the same hierarchy.”
  • Rule 13.3.1, required: user-declared member functions shall use the virtual, override, and final specifiers appropriately.

The MISRA C++:2023 summary also includes restrictions concerning casts involving virtual bases and rules addressing dynamic memory, with the status depending on the particular rule. Check the applicable rule text and project configuration rather than treating the summary as a complete compliance interpretation.

A practical decision path

  1. Ask whether callers need runtime substitution. If there is only one implementation or the concrete type is known at build time, ordinary functions, composition, or templates may be enough.
  2. Choose a representation that matches the language. In C, define a function table or pass operations explicitly; in C++, consider a small base interface when derived objects must be used through a common handle.
  3. Write down object lifetime and ownership. Specify initialization, destruction, context validity, and whether objects are statically allocated, automatically allocated, or dynamically allocated.
  4. Check target behavior against project limits. Build with the selected compiler and settings, inspect code and memory use, and measure relevant timing paths on the actual target.
  5. Check compliance and maintainability. Review the project’s coding rules and ask whether adding a new implementation is expected to be open-ended or controlled by a fixed set.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.