Insights / Embedded & Firmware

C vs C++ for Embedded Code: Differences and an Arduino Test

C and C++ compared in paradigm and features, with a four-way Arduino test of code size and speed for the same LED task.

C and C++ language logos over a circuit board background

The short answer

C is a procedural language: a program is a sequence of functions working on data. C++ keeps almost all of C and adds object-oriented features such as classes, encapsulation, overloading, references and exceptions. On a small microcontroller the size overhead of C++ is smaller than its reputation: in a four-way Arduino test it shrank as the program grew, and the C++ source stayed the most readable.

Why the C vs C++ question persists

Programming is often taught procedurally first, and some engineers never move on to object-oriented programming. Decades after object orientation appeared, the same objection is still heard on embedded projects: C++ has too much overhead. The question is whether that overhead really means slower execution and bigger executables.

C gives the machine a straightforward set of instructions and breaks a problem into smaller steps until each can be executed. That simplicity works well for a simple task in a predictable environment. When the steps depend on circumstances, or there are too many situations to write out one by one, it helps if the language can express abstract notions directly, such as classes, objects, inheritance, polymorphism and encapsulation. That is what C++ adds. In C the engineer does that translation by hand, which tends to mean copied and pasted code.

What C is

C was created at Bell Labs by Dennis Ritchie, working with Ken Thompson after the MULTICS project ended. Thompson had written the first Unix in assembly language; to port Unix to other computers, the two moved to a higher-level language and ended up designing C. Many later languages, including C++, Java, Python and Ruby, borrow from it.

C is a procedural, structured language and is still the usual choice for operating systems, drivers and embedded software.

  • Procedural: a program is a linear flow of function calls
  • Simple: a small set of keywords and a straightforward syntax
  • Modular: a large program is divided into smaller, manageable pieces
  • Dynamic memory allocation at run time
  • Portable: the same source can be compiled for different platforms
  • Fast: a compiled language, quicker than interpreted ones

The major standardised versions include C90, the first ANSI and ISO standard, C99, which added new data types and inline functions, and C11, which added atomic operations, static assertions and multithreading support.

What C++ adds

Bjarne Stroustrup created C++ in the early 1980s, first as 'C with classes', to handle what computers had become capable of. Because it is built on C, it keeps C's efficiency and low-level access to hardware, and adds a higher level of abstraction: classes and objects, exception handling, templates and a much larger standard library.

  • Object-oriented: classes, objects, inheritance and polymorphism
  • Compiled to machine code, like C
  • Dynamic memory allocation at run time
  • The same low-level access to hardware as C
  • Extensible through a large ecosystem of libraries and frameworks

Its first ISO standard appeared in 1998. Later revisions added multithreading and more powerful templates, then nested namespaces and parallel algorithms, and most recently modules, concepts, ranges and coroutines.

Similarities and differences

The two languages share their basic syntax, grammar and code structure. C++ includes almost every C operator and keyword with the same meaning. Both use manual memory management, both are compiled, and both have a memory model close to the hardware.

The main difference is the paradigm: C is procedural, C++ is object-oriented. That makes them different languages in practice, even though one grew out of the other.

How C and C++ differ in day-to-day use.
AspectCC++
ApproachTop-down: split the problem into sub-problems that can be solved directlyBottom-up: build higher-level solutions on lower-level building blocks
Typical useEmbedded software, firmware and other low-level codeNetwork and server software, game engines, media tools, and larger embedded code
Data protectionFunctions come first; data is not protectedEncapsulation, access specifiers and classes guard data
CompatibilityC code compiles with a C++ compilerC++ code does not compile as C
Data typesPrimitive and built-in typesAlso user-defined types, plus bool and string
Overloading and overridingNot supportedSupported
Error handlingNo built-in exceptions; workarounds neededtry-catch exception handling
Pointers and referencesPointers onlyPointers and references
File extension.c.cpp

An experiment: the same LED task in four styles

To test the overhead claim, the same task was written four ways for an Arduino: switch any number of LEDs on and off, each with its own on and off times. Each version was built first for two LEDs and then for ten.

  1. Direct C: a completely flat program with no functions of its own and only global variables
  2. C with a function: one function that flashes a given LED
  3. Structured C: a structure per LED and functions that operate on it, object orientation without C++
  4. C++: an LED class with its own methods
// Arduino sketch (Arduino.h is included automatically)

class LED {
public:
  byte state = 0;
  int pinnr;
  unsigned long TimeOn = 500;
  unsigned long TimeOff = 500;
  unsigned long NextTime = millis();

  void SetOn()  { state = 1; digitalWrite(pinnr, 1); }
  void SetOff() { state = 0; digitalWrite(pinnr, 0); }

  void Check() {
    if (millis() > NextTime) {
      if (state == 0) { SetOn();  NextTime = millis() + TimeOn; }
      else            { SetOff(); NextTime = millis() + TimeOff; }
    }
  }

  LED(int vPin, byte vState, int vTimeOn, int vTimeOff) {
    state = vState; pinnr = vPin; TimeOn = vTimeOn; TimeOff = vTimeOff;
    NextTime = millis();
    pinMode(pinnr, OUTPUT);
  }
};

LED L1(12, 0, 400, 500);
LED L2(13, 0, 100, 300);

void setup() { L1.SetOff(); L2.SetOff(); }

void loop() { L1.Check(); L2.Check(); }
The C++ version: each LED is an object that knows its pin and timing
struct LED {
  byte state = 0;
  int pinnr;
  unsigned long TimeOn = 500;
  unsigned long TimeOff = 500;
  unsigned long NextTime = millis();
};

void SetOn(LED *l)  { l->state = 1; digitalWrite(l->pinnr, 1); }
void SetOff(LED *l) { l->state = 0; digitalWrite(l->pinnr, 0); }

void Check(LED *l) {
  if (millis() > l->NextTime) {
    if (l->state == 0) { SetOn(l);  l->NextTime = millis() + l->TimeOn; }
    else               { SetOff(l); l->NextTime = millis() + l->TimeOff; }
  }
}
The structured C version: the same idea expressed with a struct and free functions

Results with two LEDs

With two LEDs the compiled program was 2,198 bytes in direct C, 2,214 bytes in C with a function, 2,318 bytes in structured C and 2,444 bytes in C++, about 11% larger than direct C.

10,000 loop cycles took 63 ms in direct C, C with a function and C++, and 155 ms in structured C.

Results with ten LEDs

With ten LEDs the compiled sizes converged: 2,632 bytes in direct C, 2,626 bytes in C with a function, 3,182 bytes in structured C and 2,604 bytes in C++, the smallest of the four.

The source code diverged sharply: 3,937 bytes in direct C against 1,576 bytes in C++, about 60% less. 10,000 cycles took 230 ms in direct C, 751 ms in C with a function, 1,304 ms in structured C and 449 ms in C++.

So which should you choose

The test does not show C++ to be free: with two LEDs its binary was the largest. But the overhead shrank as the program grew, and at ten LEDs C++ produced the smallest binary from by far the shortest source. Inheritance and the other benefits of object orientation were not even used. On this evidence the author's choice is C++, for readability and maintainability.

The weakest result came from imitating object orientation with structures and functions in C: the slowest and, at ten LEDs, the largest program. Choosing C for its simplicity and then rebuilding C++ features by hand makes things worse, not simpler.

'Which is better, C or C++?' is too reductive a question. C++ is more versatile but has a steeper learning curve; C is easier to start with but not the best fit for complex projects. For teaching, the author's appeal is not to stop at procedural programming but to continue with practical object-oriented examples.

Engineering trade-offs

Plain C

You gain
A small language with a straightforward toolchain; every line maps closely to what the machine does.
You pay
Abstractions have to be built by hand, which duplicates code and makes larger programs harder to read and maintain.
Choose it when
Very small or very simple firmware, or a codebase and team already built around C.

C++

You gain
Classes, encapsulation and overloading keep larger programs readable and maintainable with little extra code size.
You pay
A steeper learning curve, and features such as exceptions or virtual functions add overhead if used carelessly.
Choose it when
Firmware that will grow: many similar components, several developers and a long maintenance life.

Was this useful?

Where this shows up in our work

ExpertiseEmbedded and firmware engineeringChoosing the language is one firmware decision among many; see how firmware is engineered with the hardware, the update path and the cloud it reports to.

Apply this to your product

Talk through your device architecture

Talk through your device architecture