The Core Question: Does Arduino Use C?

When embedded developers and hobbyists ask, "Does Arduino use C?", the technically accurate answer is no. The Arduino IDE and its underlying build system compile code using a C++ compiler. For AVR-based boards like the Uno or Mega, this is avr-g++; for ARM-based boards like the ESP32 or Zero, it is arm-none-eabi-g++ or xtensa-lx106-elf-g++. However, because C++ is largely backward-compatible with C, you can write standard C code in your sketches.

The problem arises when developers assume the Arduino environment is a pure C99 or C11 compiler. This misconception is the root cause of hundreds of frustrating compilation, linkage, and runtime errors. By understanding the boundary between C and C++ in the Arduino ecosystem, you can rapidly diagnose and resolve sketch failures that seem inexplicable.

The .ino Preprocessor: Where C Code First Breaks

Before your code ever reaches the C++ compiler, the Arduino builder processes your .ino file. It performs two critical actions:

  1. It injects #include <Arduino.h> at the very top of the file.
  2. It scans your code and auto-generates function prototypes, placing them after the includes but before your function definitions.

If you are writing pure C code utilizing complex macros, function pointers, or C-specific typedefs, this auto-prototype generator frequently chokes. A classic error you will encounter is:

error: expected constructor, destructor, or type conversion before '(' token

Diagnosis: The Arduino preprocessor attempted to generate a prototype for a function that returns a custom C struct or uses a macro-defined return type, but it failed to parse the C-style syntax correctly in a C++ context.
Fix: Bypass the .ino preprocessor entirely. Rename your main sketch file from sketch.ino to sketch.cpp, manually add #include <Arduino.h> at the top, and write your own explicit function prototypes. This forces the IDE to treat the file as standard C++ without the flawed auto-generation step.

Diagnosing Linkage Errors: The 'extern "C"' Missing Link

One of the most common scenarios where the "Does Arduino use C?" question matters is when importing legacy C libraries (e.g., sensor drivers written for standard STM32 or PIC microcontrollers). You copy the .c and .h files into your sketch folder, include the header, and hit compile. The compilation succeeds, but the linking phase fails catastrophically:

undefined reference to `read_sensor_data(unsigned char)'
collect2: error: ld returned 1 exit status

Diagnosis: This is a name mangling issue. C++ supports function overloading, so the compiler "mangles" function names (e.g., changing read_sensor_data to _Z18read_sensor_datah) to encode parameter types. Standard C compilers do not mangle names. When your C++ Arduino sketch tries to call a function compiled in a .c file, it looks for the mangled name, but the C object file only provides the unmangled name.

Fix: You must instruct the C++ compiler to use C linkage for the imported header. Wrap the header's declarations in an extern "C" block. To make the header safe for both C and C++ compilers, modify the library's .h file as follows:

#ifdef __cplusplus
extern "C" {
#endif

// Original C function declarations
void read_sensor_data(uint8_t address);

#ifdef __cplusplus
}
#endif

According to the GCC Standards Documentation, this conditional compilation ensures that C++ compilers disable name mangling for the enclosed block, while pure C compilers safely ignore the __cplusplus macro.

Strict Typing: Void Pointers and Implicit Conversions

Standard C is notoriously permissive with pointer types. C++, however, enforces strict type safety. If you port C code that relies on implicit pointer casting, the Arduino C++ compiler will throw an error.

Consider this common C pattern used in buffer allocation:

uint8_t *buffer = malloc(64);

In a pure C environment, malloc returns a void*, which C automatically and implicitly casts to uint8_t*. In the Arduino C++ environment, this yields:

error: invalid conversion from 'void*' to 'uint8_t*' {aka 'unsigned char*'} [-fpermissive]

Diagnosis: The C++ standard forbids implicit downcasting from void* to prevent accidental type corruption.
Fix: Apply an explicit C-style or C++-style cast: uint8_t *buffer = (uint8_t *)malloc(64);. While some forums suggest adding the -fpermissive flag to your platform.txt file to downgrade this error to a warning, doing so is highly discouraged as it masks genuine memory corruption bugs.

Comparison Matrix: C vs C++ Quirks in Arduino

Feature / Behavior Pure C (Standard GCC) Arduino IDE (C++ Compiler) Resulting Error / Symptom
void* Assignment Implicitly allowed Strictly forbidden invalid conversion from void*
Function Overloading Not supported Supported (Name Mangling) undefined reference (Linkage error)
Struct Declarations struct MyStruct s; MyStruct s; (if typedef'd) unknown type name if C-style is used without typedef
Memory Allocation malloc() / free() new / delete (preferred) Runtime heap fragmentation if mixed improperly

Runtime Diagnostics: malloc() vs new in Embedded C++

Even if your C code compiles successfully, mixing C-style memory management with C++ objects can lead to fatal runtime errors. The AVR Libc FAQ extensively documents the dangers of heap fragmentation on microcontrollers with limited SRAM (like the ATmega328P's 2KB limit).

In C, malloc() allocates raw memory. In C++, new allocates memory and calls the object's constructor. If you use malloc() to allocate memory for a C++ class (such as the String class or a custom sensor object), the constructor is never called. When the object attempts to access its uninitialized internal pointers, the microcontroller will experience a hard fault or silent memory corruption, leading to random reboots.

Actionable Rule: If you are instantiating primitive C data types (arrays, structs without constructors), malloc() is acceptable. If you are working with C++ classes, you must use new and delete. Better yet, avoid dynamic allocation entirely in Arduino sketches and prefer statically allocated global buffers or stack-based allocation to eliminate fragmentation risks.

Step-by-Step Troubleshooting Flow for C Libraries

When importing a third-party C library into Arduino IDE 2.x, follow this diagnostic flow to resolve compilation errors systematically:

  1. Enable Verbose Output: Go to File > Preferences and check "Show verbose output during: compilation". This reveals the exact g++ command and flags being used.
  2. Check Header Guards: Ensure the C library's .h files use standard #ifndef / #define guards. Missing guards cause duplicate definition errors when C++ includes are resolved.
  3. Apply Linkage Wrappers: Add the extern "C" wrapper to the primary header file as demonstrated above.
  4. Resolve Standard Library Conflicts: C libraries often use #include <stdlib.h> or <string.h>. In Arduino C++, it is safer to use the C++ equivalents (<cstdlib>, <cstring>) or ensure the Arduino Language Reference wrappers aren't conflicting with custom macro definitions in the C library.
  5. Isolate the .c Files: If the IDE refuses to compile a .c file correctly within the sketch folder, move the C library into the global libraries folder. The Arduino builder applies slightly different compilation flags to files in the global libraries directory versus the local sketch directory.

Summary

So, does Arduino use C? It uses a C++ compiler that tolerates C syntax, but it does not provide a pure C environment. By recognizing the distinction between C and C++ linkage, strict pointer typing, and object instantiation, you can transform cryptic compiler errors into straightforward syntax corrections. Always leverage explicit casts, extern "C" wrappers, and verbose compiler logs to maintain full control over your microcontroller's firmware build process.