C++ Classes in Arduino: Led and Button Without a Messy loop()
Once you have several buttons and LEDs, global variables turn into a mess. We write two small classes with constructors and methods, put the objects into arrays — and loop() fits into five lines.
About the project
In the button article, one button needed three global variables: the previous state, the time of change and the LED state. Two buttons already need six variables and duplicated code, and five are chaos. The right answer is classes: every object keeps its own state, and the code is written once.
What you need
- ESP32 DevKit
- 2 buttons, 2 LEDs, 2 × 220 Ω resistors
Wiring
| Part | GPIO |
|---|---|
| Button 1 (red) | GPIO4 ↔ GND |
| Button 2 (green) | GPIO0 ↔ GND |
| LED 1 | GPIO23 → 220 Ω → LED → GND |
| LED 2 | GPIO22 → 220 Ω → LED → GND |
GPIO0 is the BOOT button on the board, so you can even skip the second button. Just don't hold it at power-up, or the ESP32 enters flashing mode.
Anatomy of a class
class Led {
public: ← what the outside can use: the interface
Led(int pin); ← constructor: runs when the object is created
void toggle(); ← methods: what the object can do
private: ← internal state, hidden from the outside
int pin;
bool state;
};
- The initializer list
: pin(pin)fills the fields before the constructor body runs. constafter a method (isOn() const) promises that the method changes nothing in the object.- Default arguments (
debounceMs = 30) let you writeButton(4)orButton(4, 50).
An important detail on Arduino: global objects are created before setup(), when the peripherals are not ready yet. So pinMode() is not called in the constructor but in a separate begin() method. Serial.begin() and Wire.begin() work the same way.
Code
// Object-oriented Arduino: small classes keep loop() tidy
class Led {
public:
Led(int pin) : pin(pin) {}
void begin() { pinMode(pin, OUTPUT); off(); }
void on() { set(true); }
void off() { set(false); }
void toggle() { set(!state); }
bool isOn() const { return state; }
private:
int pin;
bool state = false;
void set(bool s) { state = s; digitalWrite(pin, s ? HIGH : LOW); }
};
class Button {
public:
Button(int pin, unsigned long debounceMs = 30) : pin(pin), debounce(debounceMs) {}
void begin() { pinMode(pin, INPUT_PULLUP); }
// true once per press
bool pressed() {
int now = digitalRead(pin);
if (now != last && millis() - changedAt > debounce) {
changedAt = millis();
last = now;
return now == LOW;
}
return false;
}
private:
int pin;
unsigned long debounce;
int last = HIGH;
unsigned long changedAt = 0;
};
Led leds[] = { Led(23), Led(22) };
Button buttons[] = { Button(4), Button(0) };
void setup() {
Serial.begin(115200);
for (Led &l : leds) l.begin();
for (Button &b : buttons) b.begin();
}
void loop() {
for (int i = 0; i < 2; i++) {
if (buttons[i].pressed()) {
leds[i].toggle();
Serial.printf("Button %d → LED %s\n", i + 1, leds[i].isOn() ? "ON" : "OFF");
}
}
}
Why the & in the loop
for (Led &l : leds) walks the array by reference. Without the &, every iteration would get a copy of the object, and begin() would set up the copy instead of the real LED. It is a classic mistake with classes that keep state.
What next
- Add a
blink(periodMs)method toLedthat runs onmillis()withoutdelay(), with anupdate()call inloop(). - Move the classes into their own
Led.h/Led.cppfiles: the first step towards your own library, as the Arduino guide linked below explains.