简说 Arduino 库与 C++

发布于:2025-06-21 ⋅ 阅读:(21) ⋅ 点赞:(0)

简说 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 PrintStream 类:提供 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_stringto_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)
内存 静态分配,循环覆盖,无动态分配 默认基于 dequelist,动态分配
占用 极小(例如 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

假如写一个简单串口回显程序,使用 WStringstd::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 intconstexpr 类内能,普通静态类外分。
👉 若是 inline static(C++17),类内定义真方便。



网站公告

今日签到

点亮在社区的每一天
去签到