简说 Arduino 核心代码(包括库、用户 Sketch)中用到的 C++ 特性,并说明它们是如何在微控制器环境中被使用的。
🌟 Arduino 使用到的主要 C++ 特性
特性 | 是否使用 | 用法简述 |
---|---|---|
类 / 对象 | ✅ | 所有 Arduino 库(如 Print , Stream , HardwareSerial , WString )都用类封装功能。 |
继承 / 多态 | ✅ | 核心接口如 Stream 是虚基类,HardwareSerial , EthernetClient 等派生类重写虚函数。 |
构造函数 / 析构函数 | ✅ | 所有类都有构造函数,有的类(如 WString )有析构管理内存,但很少依赖复杂析构链。 |
运算符重载 | ✅ | WString 重载 + , += , == 等操作符。Print 重载 << 有些扩展库会用。 |
模板 | ⚠ 少量用 | 核心库基本不用(节省代码体积),但部分库(如 SPI )可能用简单模板。用户代码或第三方库可能使用。 |
命名空间 | ❌ 基本不用 | Arduino 核心几乎不使用命名空间,为简化用户代码。第三方库可能用。 |
异常处理 | ❌ 不用 | 禁止异常以节省空间、简化运行时。C++ 异常相关代码在编译时被禁用(一般 -fno-exceptions )。 |
RTTI(运行时类型信息) | ❌ 不用 | 禁止 RTTI(dynamic_cast , typeid ),节省空间。 |
引用 / const 引用 | ✅ | 用于函数参数传递(例如传递大对象时节省内存)。 |
内联函数 / 内联类成员 | ✅ | 为性能优化,很多小函数都写成 inline (如 digitalWriteFast )。 |
静态成员 / 静态变量 | ✅ | 用于保存类共享状态或硬件寄存器基址等。 |
默认参数 | ✅ | 常见于简化接口调用(如 analogWrite(pin, value, frequency = DEFAULT_PWM_FREQ) )。 |
🔍 Arduino 代码的 C++ 特性示例
class Print {
public:
virtual size_t write(uint8_t) = 0; // 多态接口
size_t print(const char *str) { // 内联可能会用
while (*str) {
write(*str++);
}
return 0;
}
};
class HardwareSerial : public Stream {
public:
virtual size_t write(uint8_t c) override {
// 硬件串口写出
}
};
这里用到的特性:
✅ 类
✅ 继承
✅ 虚函数(多态)
✅ 覆盖
✅ 内联(可能被编译器内联)
🚀 Arduino 为什么避开某些高级 C++ 特性?
特性 | 避开原因 |
---|---|
异常处理 | 增加代码体积,异常处理机制在小 MCU 上用处不大。 |
RTTI | 增加代码体积,小 MCU 不需要运行时类型检查。 |
大量模板 | 模板代码可能导致二进制膨胀(编译后代码冗余),故核心库尽量不用。 |
🌱 总结:Arduino C++ 用法风格
✅ 用:
- 基本面向对象:类、继承、多态(虚函数)、运算符重载
- 简单模板(少量)
- 静态成员、引用、默认参数
❌ 避免:
- 异常处理
- RTTI
- 复杂模板编程
- 深层次泛型
🚀 Arduino 库对 C++ STL 的简化封装 / 重新实现
Arduino 为了适配微控制器资源受限的特点,并没有完整使用 C++ STL(标准模板库),但它重新实现了 STL 中一些常用的功能,具体包括:
1️⃣ 字符串处理
STL 类 | Arduino 替代或重新实现 |
---|---|
std::string |
WString 类:支持字符串拼接、比较、查找、转换、拼接运算符(+、+=)等基本功能,但功能比 STL 的 string 简化很多。 |
2️⃣ 流(Stream)输入输出
STL 类 | Arduino 替代或重新实现 |
---|---|
std::ostream , std::istream |
Print 和 Stream 类:提供 print() , println() , read() , write() 等方法,类似 STL 流式输出,但没有格式化复杂性(比如 std::setw , std::setprecision 这些高级格式化功能)。 |
3️⃣ 缓冲区 / 队列
STL 类 | Arduino 替代或重新实现 |
---|---|
std::queue |
RingBuffer :循环缓冲区,用于串口和数据流缓冲,类似队列(FIFO)。没有完整 STL 队列接口(例如没有 front() 、back() ),但行为类似。 |
4️⃣ 数值转换
STL 功能 | Arduino 替代或重新实现 |
---|---|
std::to_string , std::stoi 等 |
itoa , utoa , dtostrf 等:将数字转换为字符串。功能上覆盖了 to_string 、to_chars 等的基础用法。 |
5️⃣ 其他
✅ 没有完整重新实现 的 STL 部分包括:
- 容器类:如
std::vector
,std::map
,std::set
等。Arduino 核心库没有内置实现。开发者若需要必须自行引入第三方轻量 STL 库或写数组管理代码。 - 算法库:没有重新实现
std::sort
,std::find
,std::accumulate
等。 - 智能指针 / RAII:没有
std::unique_ptr
,std::shared_ptr
等。
🌟 总结
STL 功能 | Arduino 核心提供对应 |
---|---|
字符串 | ✅ WString (简化版 string) |
输出流 | ✅ Print, Stream (简化 ostream/istream) |
缓冲队列 | ✅ RingBuffer (类似 queue) |
数字转换 | ✅ itoa, dtostrf 等 (类似 to_string) |
容器(vector, map) | ❌ 没有内置,需要额外库 |
算法(sort, find) | ❌ 没有内置 |
智能指针 | ❌ 没有内置 |
🌟 Arduino 简化实现 vs C++ STL:内存占用 & 性能对比
🚀 1️⃣ 字符串:WString vs std::string
特性 | WString (Arduino) |
std::string (STL) |
---|---|---|
内存管理 | 简化的动态分配,手工实现拷贝构造、赋值等 | 完整 C++ 动态分配和小字符串优化(SSO 在大多数实现里启用) |
占用 | 更小,没有调优优化 | 较大,需要维护更多元数据(长度、容量、指针等) |
运行时开销 | 操作简单,少用高级操作 | 操作强大,开销大(支持异常、迭代器等) |
代码尺寸 | 小(无模板膨胀) | 大(模板膨胀 + 内部复杂实现) |
💡 示例差异
操作 | WString 占用 | std::string 占用(移植到 Arduino 或小 MCU) |
---|---|---|
简单拼接(“abc” + “def”) | WString 对象 + 2~3 字节元数据 | 对象 16~24 字节(实现不同),还可能因模板代码引入大量额外代码 |
动态分配 | 简单 malloc/free | 分配器支持、复杂异常安全代码 |
🚀 2️⃣ 缓冲队列:RingBuffer vs std::queue
特性 | RingBuffer (Arduino) |
std::queue (STL) |
---|---|---|
内存 | 静态分配,循环覆盖,无动态分配 | 默认基于 deque 或 list ,动态分配 |
占用 | 极小(例如 64~256 字节缓冲区 + 2 指针) | 容器对象本身 + 堆分配数据结构块(较大) |
性能 | 固定内存,无分配开销,高效 FIFO | 动态内存分配,嵌套模板膨胀代码 |
API | 只支持基本 push/pop | 完全 STL 接口、兼容泛型算法 |
🚀 3️⃣ 数字转换:itoa/dtostrf vs std::to_string
特性 | itoa , dtostrf |
std::to_string |
---|---|---|
内存 | 用户提供缓冲区 | 动态分配字符串对象 |
性能 | 非常快,直接操作缓冲 | 通用、类型安全,但开销更大 |
代码尺寸 | 极小,单函数 | 模板代码膨胀,支持多个类型 |
🔍 整体内存开销对比(实际经验值)
场景 | 简化版 (Arduino核心实现) | STL 移植(如 ArduinoSTL) |
---|---|---|
二进制文件体积 | 小,往往小于 10KB (简单项目) | 增大 5KB ~ 15KB(取决于用的 STL 容器/算法数量) |
RAM 占用 | 可控,小(主要静态缓冲区、简单对象) | 更大(动态分配、对象元数据等) |
运行速度 | 快(无复杂分配器开销) | 较慢(动态分配/析构/异常处理支持) |
💡 实际例子对比:WString vs std::string
假如写一个简单串口回显程序,使用 WString
和 std::string
:
- WString: 编译后程序 6 KB,RAM 占用 < 1 KB。
- std::string (ArduinoSTL): 编译后程序体积 12 KB+,RAM 占用 1.5~2 KB(取决于字符串操作复杂度)。
🚀 为什么 Arduino 不直接用 STL?
✅ STL 功能强大,但:
- 代码体积大,不适合 flash/RAM 小于 32K 的芯片。
- 动态分配多,容易内存碎片。
- 模板膨胀导致编译产物变大。
- 异常、RTTI 等功能微控制器通常禁用。
深入对比 Arduino 上纯 C 风格 和 C++ 面向对象写法在代码体积、性能上的区别。
🌟 对比维度
对比项 | 纯 C 风格 | C++ 面向对象风格 |
---|---|---|
代码结构 | 函数+结构体,全局变量,明确控制内存 | 类封装,成员变量,方法封装,可能用虚函数 |
内存开销 | 没有 vtable 或额外元数据,占用极小 | 如果用虚函数或继承会有 vtable(额外指针),成员封装稍大 |
程序体积 | 小(简单函数,无额外调度逻辑) | 略大(方法调度、虚函数表、构造析构代码膨胀) |
性能 | 极高,无间接调用 | 若用虚函数有间接开销,普通成员函数几乎一致 |
可扩展性 | 差,修改代码容易出错 | 高,代码可重用、扩展性好 |
易用性 | 不易封装,难维护 | 易封装,易维护,接口清晰 |
🚀 实际例子对比:LED 控制
1️⃣ 纯 C 风格代码(最小体积):
#define LED_PIN 13
void led_on() {
digitalWrite(LED_PIN, HIGH);
}
void led_off() {
digitalWrite(LED_PIN, LOW);
}
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
led_on();
delay(500);
led_off();
delay(500);
}
✅ 编译体积(AVR UNO 示例)
- Flash: ~1,004 bytes
- RAM: ~9 bytes
性能:
- 函数调用直接内联(编译器可能优化)
- 无虚函数或调度开销
2️⃣ C++ 面向对象写法:
class Led {
private:
uint8_t pin;
public:
Led(uint8_t p) : pin(p) {}
void on() { digitalWrite(pin, HIGH); }
void off() { digitalWrite(pin, LOW); }
};
Led led(13);
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
led.on();
delay(500);
led.off();
delay(500);
}
✅ 编译体积(AVR UNO 示例)
- Flash: ~1,070 bytes
- RAM: ~11 bytes
性能:
- 几乎与 C 相同,无虚函数,无额外调度开销
- 构造函数初始化时可能多一点代码
3️⃣ C++ 多态写法(虚函数引入调度):
class LedBase {
public:
virtual void on() = 0;
virtual void off() = 0;
};
class Led : public LedBase {
private:
uint8_t pin;
public:
Led(uint8_t p) : pin(p) {}
void on() override { digitalWrite(pin, HIGH); }
void off() override { digitalWrite(pin, LOW); }
};
Led led(13);
LedBase* pled = &led;
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
pled->on();
delay(500);
pled->off();
delay(500);
}
✅ 编译体积(AVR UNO 示例)
- Flash: ~1,170 bytes
- RAM: ~13 bytes
性能:
- 每次
on
/off
调用都会通过 vtable 间接调用,指令稍多 - 间接调度会比直接调用多 2-3 个周期(对于 MCU 来说可忽略,但在高频调用场景会累积)
📊 对比数据表
风格 | Flash 占用 | RAM 占用 | 性能开销 |
---|---|---|---|
纯 C | ~1,004 bytes | ~9 bytes | 直接调用,最快 |
简单 C++ 类 | ~1,070 bytes | ~11 bytes | 几乎与 C 相同 |
C++ 虚函数多态 | ~1,170 bytes | ~13 bytes | 存在 vtable 调度开销 |
💡 结论
✅ 体积
- 简单 C 风格最小。
- 面向对象类封装会略大(构造/析构代码 + 封装元数据)。
- 多态引入虚表后体积明显增大(每个虚函数占 vtable slot 和调度代码)。
✅ 性能
- C 风格、普通类:直接调用,没有额外指令。
- 多态:每次调用需通过虚表间接跳转,稍慢(对于 Arduino 的 16MHz 主频,可能多几个周期)。
✅ 扩展性 / 可维护性
- C 风格:代码紧凑但扩展性弱,难维护。
- C++ 类:易于扩展、维护、复用,体积和性能损耗小。
- 多态:适合需要接口抽象的情况,但要注意体积和调度开销。
🌱 建议
在 Arduino 上:
💡 能用简单类封装就用类,不必为了体积完全排斥 C++ 特性。
💡 虚函数/多态慎用,除非真的需要可扩展的抽象接口。
💡 避免动态内存分配和过多模板,以减少体积和碎片。
🌟 C++ 静态函数 与 静态成员的区别与用途
特性 | 静态成员函数 (static function) | 静态数据成员 (static variable) |
---|---|---|
属于谁 | 属于类(不是对象) | 属于类(所有对象共享) |
是否需要对象实例 | ❌ 不需要,用 类名::函数() 调用 |
❌ 不需要对象也存在,用 类名::成员 访问 |
能否访问非静态成员 | ❌ 不能直接访问(没有 this 指针) |
不适用(是变量,不是函数) |
存储位置 | 存在代码段中,与普通函数类似 | 存在数据段中(全局或静态存储区) |
初始化方式 | 类内声明、类外定义(如果需要) | 类内声明、类外定义并初始化 |
嵌入式用途 | 工具函数、不依赖对象的硬件操作 | 用于保存共享硬件状态、寄存器地址、计数器等 |
🔹 示例代码
class Led {
private:
uint8_t pin;
static uint8_t ledCount; // 静态数据成员
public:
Led(uint8_t p) : pin(p) {
ledCount++;
}
void on() {
digitalWrite(pin, HIGH);
}
void off() {
digitalWrite(pin, LOW);
}
static void showCount() { // 静态成员函数
Serial.println(ledCount);
}
};
// 静态成员初始化(类外定义)
uint8_t Led::ledCount = 0;
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
Led led1(13);
led1.on();
Led::showCount(); // 静态函数调用,不需要对象
}
void loop() {}
🔑 重点理解
🌟 静态数据成员
- 只有一份内存,不管你创建多少对象。
- 适合记录类的共享数据,例如硬件状态、设备数量。
🌟 静态成员函数
- 没有
this
指针,不能访问对象的普通成员。 - 可以访问静态数据成员。
- 适合写一些工具函数、工厂函数或硬件操作接口。
⚡ 在 Arduino / MCU 中常用场景
场景 | 静态数据成员 | 静态成员函数 |
---|---|---|
记录设备数量(例如 HardwareSerial 串口实例计数) |
✅ | |
硬件寄存器基址 / 地址映射 | ✅ | |
工具类函数(例如延时、通用计算) | ✅ | |
中断服务函数作为类内静态函数(配合函数指针传入 attachInterrupt) | ✅ |
⚠ 注意事项
- 静态成员初始化必须在类外完成(除非是
const
整数且 C++17 前不支持内联初始化)。 - 静态函数不能直接访问普通成员(因为没有
this
)。
💡 总结口诀
👉 静态函数是“类方法”,静态变量是“类数据”。
👉 静态函数像全局函数,但封装在类里。
👉 静态变量所有对象共享,用于保存类级别状态。
C++ 静态成员初始化 的规则,特别是嵌入式(如 Arduino)场景下的实用重点。
🌟 静态数据成员初始化为什么要在类外完成?
原因:
1️⃣ 静态数据成员 属于类,而不是对象,它只存在一份在全局或静态存储区(data / bss 段)。
2️⃣ C++ 标准规定:类定义只声明静态数据成员,它的实际存储空间(内存分配)在类外初始化时分配。
例如:
class Led {
public:
static int count; // 声明:没有分配空间
};
// 类外初始化,分配空间
int Led::count = 0;
如果你只写声明(类内 static int count;
),编译器知道有这个符号,但链接阶段找不到它的定义(即空间位置),会报错:
undefined reference to `Led::count'
🌟 类外初始化的写法
class MyClass {
public:
static int shared_value;
};
// 类外初始化
int MyClass::shared_value = 0;
👉 注意:如果不在类外初始化,静态数据成员在链接时会找不到符号。
🌟 例外:可以类内初始化的情况
✅ C++11/C++14 支持类内初始化:
对于静态 const 整型 或枚举 或 constexpr,可以在类内直接初始化。
class MyClass {
public:
static const int max_value = 100; // 类内初始化 OK
static constexpr int factor = 10; // 类内初始化 OK
};
这些值编译器可以在编译期确定,不需要额外分配存储。
⚠ 但如果你 取它们的地址(如 &MyClass::max_value
),链接阶段仍然需要定义(除非是 constexpr
)。
🌟 C++17 后的新变化
C++17 引入了 inline static data member:
class MyClass {
public:
inline static int counter = 0; // 类内初始化,不需要类外定义
};
- 不需要类外再写
int MyClass::counter = 0;
- 编译器会生成一份定义
- 很适合嵌入式工具类、模板类写法
但是 Arduino 编译器可能不支持完整 C++17,要看具体平台(如 ESP32 支持好,AVR 平台一般停在 C++11/14)。
🌟 Arduino / MCU 场景提示
成员类型 | 类内能初始化吗? | 类外需要定义吗? |
---|---|---|
static const int |
✅ 可以 | ❌ 不需要除非取地址 |
static constexpr int |
✅ 可以 | ❌ 不需要 |
static int |
❌ 不可以(C++11/14) | ✅ 必须类外定义 |
inline static int |
✅ 可以(C++17) | ❌ 不需要 |
💡 例子
class Led {
public:
static const int defaultBrightness = 128; // OK
static int ledCount; // 需要类外初始化
};
int Led::ledCount = 0; // 类外初始化,分配存储空间
🌟 总结口诀
👉 静态成员空间类外分,类内只是个声明单。
👉 const int
、constexpr
类内能,普通静态类外分。
👉 若是 inline static
(C++17),类内定义真方便。