The Core Question: Is Arduino C or C++?

When makers first ask, "Is Arduino C?", the short answer is no: Arduino is fundamentally built on C++. However, the reality of the Arduino ecosystem is a hybrid environment. The Arduino IDE compiles your .ino sketches using a C++ compiler (specifically avr-g++ for classic AVR boards like the Uno R3, or arm-none-eabi-g++ for ARM-based boards like the Nano 33 BLE). Yet, the underlying hardware abstraction libraries and the standard AVR Libc are written in pure C.

This hybrid nature is the root cause of the most frustrating compilation and linker errors in the Arduino IDE. When you mix pure C constructs, external C libraries, and C++ object-oriented paradigms, the compiler gets confused by name mangling, linkage specifications, and scope rules. According to the Arduino Language Reference, the IDE attempts to hide this complexity, but as your projects scale into multi-file architectures, you must understand how to bridge the C and C++ divide.

Top 4 C vs C++ Compilation Errors and How to Fix Them

Below is a deep dive into the most common errors triggered by the C/C++ language clash in the Arduino IDE, complete with exact error strings and actionable fixes.

1. "Undefined Reference" Linker Errors with Pure C Libraries

The Scenario: You download a third-party sensor library or a legacy cryptographic module written in pure C (consisting of .c and .h files). You include the header in your C++ sketch (.ino or .cpp) and call a function. The code compiles, but fails at the linking stage.

The Exact Error:

/tmp/arduino_build_xxx/sketch.ino.cpp.o: In function `setup':
sketch.ino.cpp:14: undefined reference to `my_c_function(int)'
collect2: error: ld returned 1 exit status

The Root Cause: C++ supports function overloading, so the compiler "mangles" function names to include their parameter types (e.g., _Z15my_c_functioni). Pure C does not mangle names; it expects the exact symbol my_c_function. The C++ linker cannot find the mangled name in the compiled C object file.

The Fix: You must wrap the C header inclusion in an extern "C" block. This tells the C++ compiler to use C linkage for those specific declarations. For detailed linkage specifications, refer to the C++ Reference on Language Linkage.

// In your .ino or .cpp file
#ifdef __cplusplus
extern "C" {
#endif

#include "legacy_c_library.h"

#ifdef __cplusplus
}
#endif

void setup() {
    my_c_function(42); // Now links correctly
}

2. Missing Arduino.h in Custom C++ Files

The Scenario: You decide to organize your monolithic sketch by moving classes into separate .cpp and .h files. Suddenly, standard Arduino functions like digitalWrite() or constants like HIGH throw compilation errors.

The Exact Error:

MyCustomClass.cpp:12:5: error: 'digitalWrite' was not declared in this scope
     digitalWrite(pin, HIGH);

The Root Cause: The Arduino IDE automatically injects #include <Arduino.h> into your main .ino file, but it does not do this for secondary .cpp files. Without this header, the compiler doesn't know about the Arduino core API.

The Fix: Always include the core header at the top of every custom .cpp file you create.

#include "MyCustomClass.h"
#include <Arduino.h> // Mandatory for .cpp files

void MyCustomClass::init() {
    pinMode(13, OUTPUT);
}

3. Preprocessor Prototype Generation Failures

The Scenario: You write a function in your .ino sketch that uses a function pointer or a complex C-style struct as a parameter, and you call it inside loop().

The Exact Error:

sketch.ino:24:1: error: variable or field 'executeCallback' declared void
 void executeCallback(void (*callback)(int)) {

The Root Cause: Before compiling, the Arduino preprocessor scans your .ino file and automatically generates function prototypes at the top of the file. Its regex-based parser is notoriously bad at understanding C-style function pointers or complex templates, resulting in malformed prototypes.

The Fix: Bypass the preprocessor by writing your own manual prototype at the very top of the sketch, or move the function into a separate .cpp file where the preprocessor doesn't interfere.

// Manual prototype overrides the preprocessor
void executeCallback(void (*callback)(int));

void executeCallback(void (*callback)(int)) {
    callback(10);
}

4. Struct and Enum Scope Clashes

The Scenario: You define a struct in a header file and try to use it as a type without the struct keyword in your C++ code.

The Root Cause: In pure C, you must use the struct keyword when declaring variables of a struct type (unless you use typedef). In C++, the struct name is automatically injected into the namespace as a type. If you share headers between a pure C file (compiled with avr-gcc) and a C++ file (compiled with avr-g++), you will encounter type resolution errors.

The Fix: Use the standard C/C++ compatible typedef pattern in your shared headers to ensure both compilers interpret the type identically.

// SharedHeader.h
typedef struct {
    uint8_t id;
    float temperature;
} SensorData_t;

// Now works in both .c and .cpp files without the 'struct' keyword
SensorData_t mySensor;

Memory Management: malloc() vs new in Microcontrollers

A frequent point of confusion when asking "Is Arduino C?" revolves around memory allocation. C uses malloc() and free(), while C++ uses new and delete. On a microcontroller like the ATmega328P (which has only 2,048 bytes of SRAM), heap fragmentation can cause catastrophic runtime failures long after the code successfully compiles.

According to the AVR Libc User Manual, the standard malloc() implementation does not include a memory compactor. If you allocate and free varying block sizes, your heap will fragment, and subsequent allocations will return NULL even if total free memory seems sufficient.

Comparison of Dynamic Allocation on Arduino AVR (ATmega328P)
Feature C Style (malloc) C++ Style (new) Best Practice (Static)
Syntax int* p = (int*)malloc(sizeof(int)); int* p = new int; static int p; or global array
Constructor Calls No (memory only) Yes (initializes objects) N/A
Failure Return Returns NULL Throws exception (usually crashes AVR) Never fails (compile-time check)
Fragmentation Risk High High Zero
Overhead (Bytes) ~2 bytes per block + heap ptr ~2 bytes + C++ runtime overhead 0 bytes runtime overhead
Expert Tip: In 95% of Arduino applications, dynamic allocation should be avoided entirely. If you need a buffer for a sensor reading, allocate a global or static array at compile time. If you absolutely must use a dynamic pool, implement a fixed-size block memory pool in C to eliminate fragmentation entirely.

Advanced Debugging: When Serial.print() Isn't Enough

When C/C++ linkage errors are fixed, but the code still exhibits undefined behavior (e.g., random reboots, stack overflows from deep C recursion), Serial.print() debugging often alters the timing and masks the bug.

For professional troubleshooting on classic AVR boards, use an Atmel-ICE (approximately $90-$110) connected via the 6-pin ICSP header. This allows you to use hardware breakpoints and inspect the C/C++ call stack in real-time via Microchip Studio or the Arduino IDE 2.x debugger. For modern ARM-based boards like the Arduino Nano ESP32, utilize the built-in JTAG interface via the USB port to step through mixed C and C++ assembly instructions directly in the IDE.

Quick-Reference Troubleshooting Matrix

Keep this matrix handy when the Arduino IDE throws cryptic errors related to C and C++ interoperability.

Error Message Snippet Root Cause Actionable Fix
undefined reference to... C++ name mangling clashing with pure C object files. Wrap the C header include in extern "C" { ... }.
was not declared in this scope (in .cpp) Missing Arduino core definitions in secondary files. Add #include <Arduino.h> at the top of the .cpp file.
variable or field declared void Arduino preprocessor failing to parse function pointers. Write manual prototypes at the top of the .ino sketch.
invalid conversion from 'void*' to 'int*' Using C malloc without casting in a C++ file. Cast the result: (int*)malloc(...) or switch to new.
multiple definition of... Defining (not just declaring) variables in a shared .h file. Use extern in the .h file, and define the variable in exactly one .cpp file.

Conclusion

So, is Arduino C? It is a C++ environment that heavily relies on C libraries and C-style hardware constraints. Mastering the Arduino platform requires more than just knowing how to wire a breadboard; it requires understanding how the avr-g++ toolchain bridges these two languages. By applying proper linkage specifications, managing memory statically, and bypassing the IDE's preprocessor flaws, you can eliminate compilation errors and build robust, production-grade firmware.