C++ 基础面试题(嵌入式方向)

精选 155 道 C++ 面试题,侧重嵌入式开发实际应用。每道题均包含详细文字解析和代码示例。


★ C++核心概念图解(先理解原理,再刷面试题)

◆ 面向对象三大特性一图流

  • 封装像胶囊——把药(数据)和壳(接口)包在一起,你不需要知道胶囊里面什么配方,只要知道”吃了治感冒”(调接口)就行。
  • 继承像”家族遗传”——儿子(派生类)自动拥有父亲(基类)的特征,还可以加自己的新特征。
  • 多态像”一个遥控器控制不同电器”——同一个”开机”按钮(虚函数),对电视是显示画面,对空调是吹风,运行时才决定执行哪个。
1
虚函数表(vtable)机制——多态的底层实现:
2
3
class Base { class Derived : public Base {
4
virtual void foo(); void foo() override; // 覆盖
5
virtual void bar(); void baz(); // 新增
6
}; };
7
8
Base对象内存: Derived对象内存:
9
┌──────────┐ ┌──────────┐
10
│ vptr ────┼──→ vtable │ vptr ────┼──→ vtable(Derived)
11
│ 成员变量 │ ┌──────┐│ Base成员 │ ┌──────────────┐
12
└──────────┘ │foo()→│└──────────┘ │foo()→Derived::foo│
13
bar()→│ Derived成员 │bar()→Base::bar │
14
└──────┘ └──────────────────┘
15
3 collapsed lines
16
Base *p = new Derived();
17
p->foo(); // 查vtable → 调用 Derived::foo() (多态!)
18
p->bar(); // 查vtable → 调用 Base::bar()

◆ 智能指针选型

1
┌──────────────┬────────────────┬────────────────┬───────────────┐
2
│ │ unique_ptr │ shared_ptr │ weak_ptr │
3
├──────────────┼────────────────┼────────────────┼───────────────┤
4
│ 所有权 │ 独占 │ 共享(引用计数) │ 不拥有 │
5
│ 拷贝 │ ✗ 不可拷贝 │ ✓ 拷贝时+1 │ ✓ │
6
│ 移动 │ ✓ 转移所有权 │ ✓ │ ✓ │
7
│ 开销 │ 零(和裸指针同)│ 引用计数(原子) │ 引用计数 │
8
│ 循环引用 │ 不会 │ 会!(内存泄漏) │ ★打破循环 │
9
│ 适用场景 │ 大多数情况★ │ 多处共享一个对象│ 观察者/缓存 │
10
│ 嵌入式 │ ★推荐 │ 慎用(原子开销)│ 少用 │
11
└──────────────┴────────────────┴────────────────┴───────────────┘
12
13
选择策略:
14
默认用 unique_ptr → 需要共享时 shared_ptr → 打破循环用 weak_ptr

◆ 移动语义与右值引用

1
// 拷贝 vs 移动:
2
String a("hello");
3
String b = a; // 拷贝构造: 分配新内存 + memcpy
4
String c = std::move(a); // 移动构造: 偷走a的指针, a变空
5
// 极快! 只是指针赋值,没有内存分配
6
7
// 完美转发(面试加分):
8
template<typename T>
9
void wrapper(T&& arg) { // 万能引用
10
real_func(std::forward<T>(arg)); // 完美转发
11
}
12
// T&&不是"右值引用", 而是"万能引用"(模板+&&)
13
// forward保持原来的左/右值属性不变

## ★ 题目分类导航

  • 一、C与C++的区别(Q1~Q15)
  • 二、类与对象(Q16~Q40)
  • 三、模板(Q41~Q60)
  • 四、C++11/14/17重要特性(Q61~Q80)
  • 五、嵌入式C++实践(Q81~Q140)
  • 六、STL与现代特性(Q141~Q144)
  • 七、C++多线程与设计模式(Q145~Q161)
  • 八、C++并发与智能指针进阶(Q162~Q164)

一、C与C++的区别(Q1~Q15)

Q1: C 和 C++ 最根本的区别是什么?

🧠 秒懂: C是面向过程的工具箱,C++在此基础上加了面向对象这把瑞士军刀。C++支持类、继承、多态、模板等特性,但嵌入式中常用C++的子集,取其精华去其臃肿。

答: C 是面向过程的语言,核心思想是”用函数把任务分解成步骤一步步执行”;C++ 是面向对象的语言,核心思想是”用类把数据和操作封装在一起”。

通俗比喻: 假设你要造一辆车:

  • C 的做法:写一堆函数 → 制造轮子()制造引擎()组装(),一步步调用
  • C++ 的做法:先定义类 → class 轮子 {...}class 引擎 {...}class 汽车 { 轮子 w; 引擎 e; },各个部件自己管理自己
1
// C 风格
2
struct Car {
3
int speed;
4
int fuel;
5
};
6
void car_accelerate(struct Car *c) { c->speed += 10; }
7
8
// C++ 风格
9
class Car {
10
int speed;
11
int fuel;
12
public:
13
void accelerate() { speed += 10; } // 数据和操作绑定在一起
14
};

C++ 比 C 多了:类/对象、继承、多态、模板、异常处理、STL 标准库、引用、函数重载、运算符重载等特性。


Q2: struct 在 C 和 C++ 中有什么区别?

🧠 秒懂: C的struct只能有数据成员,C++的struct和class几乎一样(默认访问权限不同:struct默认public,class默认private)。C++的struct支持构造函数、继承、成员函数。

答: 在 C 中,struct 只能包含数据成员(变量),不能包含函数,使用时必须写 struct 关键字。在 C++ 中,struct 和 class 几乎一样,可以包含函数、构造函数、析构函数、继承,唯一区别是 struct 默认 public,class 默认 private。

1
// C 语言中
2
struct Point {
3
int x, y; // 只能有变量
4
// void print() {} ← C中不允许!
5
};
6
struct Point p1; // C 中必须带 struct 关键字
7
8
// C++ 中
9
struct Point {
10
int x, y;
11
void print() { printf("(%d,%d)\n", x, y); } // 可以有函数
12
Point(int a, int b) : x(a), y(b) {} // 可以有构造函数
13
};
14
Point p1(3, 4); // C++ 中不需要 struct 关键字

嵌入式建议: 如果只是存数据(如寄存器映射结构体),用 struct;如果需要封装行为,用 class。


Q3: C++ 中的引用(reference)是什么?和指针有什么区别?

🧠 秒懂: 引用是变量的’别名’——给已有变量取个新名字。和指针不同:引用必须初始化、不能为NULL、不能重新绑定。本质上引用底层通常实现为const指针。

答: 引用就是给一个变量起”别名”。引用一旦绑定了一个变量,就永远指向它,不能改变。

1
int a = 10;
2
int &ref = a; // ref 是 a 的别名,ref 和 a 是同一个东西
3
ref = 20; // 等同于 a = 20
4
printf("%d\n", a); // 输出 20
5
6
// int &ref2; // 错误!引用必须初始化
7
// int &ref3 = NULL; // 错误!引用不能为空

引用 vs 指针 对比表:

特性引用指针
是否必须初始化
能否为 NULL不能可以
能否改变指向不能可以
是否占内存编译器优化可能不占占 4/8 字节
语法直接用需要 * 和 &
安全性更安全可能野指针

实际用途: 函数参数传引用避免拷贝,且比指针更安全:

1
void swap(int &a, int &b) { // 引用传参
2
int tmp = a; a = b; b = tmp;
3
}
4
int x = 1, y = 2;
5
swap(x, y); // 调用时不需要取地址,更直观

💡 面试追问:

  1. 引用能绑定到临时对象吗?const引用呢?
  2. 指针的sizeof和引用的sizeof有什么区别?
  3. 函数返回引用需要注意什么?

嵌入式建议: 嵌入式中引用常用于函数参数(避免拷贝大结构体)和返回硬件寄存器的封装。注意不要返回局部对象的引用。

Q4: 什么是函数重载(overload)?C 为什么不支持?

🧠 秒懂: 函数重载是同名函数根据参数不同选择不同版本。C不支持是因为C只用函数名做符号,C++用函数名+参数类型(名字修饰/mangling)来区分。

答: 函数重载是指同一个函数名,参数列表不同(个数不同或类型不同),编译器根据调用时传入的参数自动选择正确的版本。

1
// C++ 允许同名函数,参数不同
2
int add(int a, int b) { return a + b; }
3
double add(double a, double b) { return a + b; }
4
int add(int a, int b, int c) { return a + b + c; }
5
6
add(1, 2); // 调用第一个 int 版本
7
add(1.5, 2.5); // 调用第二个 double 版本
8
add(1, 2, 3); // 调用第三个三参数版本

C 为什么不支持: C 编译器用函数名直接作为符号名(如 add),所以同名会冲突。C++ 编译器会把参数类型编码进符号名(如 _Z3addii_Z3adddd),这叫”名称修饰(name mangling)“,所以同名不同参数不冲突。

注意: 返回值不同不构成重载!因为调用时编译器无法根据返回值判断调用哪个。


Q5: extern “C” 的作用?

🧠 秒懂: extern “C”告诉C++编译器:这段代码按C的规则编译(不做名字修饰)。混合编程时C++调用C函数或C调用C++函数都需要它来确保链接正确。

答: extern "C" 告诉 C++ 编译器”这段代码用 C 的方式编译”,不做名称修饰。主要用于 C 和 C++ 混合编程。

1
// 在 C++ 文件中调用 C 写的函数
2
extern "C" {
3
void uart_init(int baud); // 这个函数是 C 写的
4
int gpio_read(int pin);
5
}
6
7
// 或者在头文件中(同时兼容 C 和 C++)
8
#ifdef __cplusplus
9
extern "C" {
10
#endif
11
12
void hal_delay(int ms);
13
14
#ifdef __cplusplus
15
}
1 collapsed line
16
#endif

嵌入式场景: STM32 的 HAL 库是 C 写的,在 C++ 项目中调用时需要 extern “C”。中断处理函数也必须用 extern “C”:

1
extern "C" void SysTick_Handler(void) {
2
HAL_IncTick();
3
}

Q6: new/delete 和 malloc/free 的区别?

🧠 秒懂: new/delete会调用构造/析构函数,malloc/free只分配/释放原始内存。new返回正确类型指针,malloc返回void*需强转。嵌入式中可重载new来使用自定义内存池。

答: 两者都是动态分配内存,但有重要区别:

特性new/deletemalloc/free
语言C++ 运算符C 库函数
类型安全自动推导类型,返回正确指针返回 void*,需强转
构造/析构自动调用构造和析构函数不调用
失败处理抛出异常(或返回 nullptr)返回 NULL
大小自动计算需手动 sizeof
1
// malloc 方式(C 风格)
2
int *p1 = (int *)malloc(sizeof(int) * 10); // 需要强转、手动算大小
3
free(p1);
4
5
// new 方式(C++ 风格)
6
int *p2 = new int[10]; // 自动算大小、类型安全
7
delete[] p2; // 数组用 delete[]
8
9
// 对于类对象,区别巨大:
10
class Sensor {
11
public:
12
Sensor() { printf("初始化传感器\n"); } // 构造函数
13
~Sensor() { printf("关闭传感器\n"); } // 析构函数
14
};
15
5 collapsed lines
16
Sensor *s1 = (Sensor *)malloc(sizeof(Sensor)); // 不会调用构造函数!
17
free(s1); // 不会调用析构函数!
18
19
Sensor *s2 = new Sensor(); // 自动调用构造函数 → 输出"初始化传感器"
20
delete s2; // 自动调用析构函数 → 输出"关闭传感器"

嵌入式注意: 很多嵌入式系统禁止使用动态内存分配(容易碎片化),优先使用静态数组或内存池。


💡 面试追问:

  1. placement new和普通new的区别?
  2. new[]和delete[]不匹配会怎样?
  3. 嵌入式中如何替换全局的operator new?

嵌入式建议: MCU上通常完全避免动态分配(碎片/确定性问题),用内存池或静态分配替代。如果必须用new,重载到自定义内存池。

Q7: C++ 中的 const 和 C 中的 const 有什么不同?

🧠 秒懂: C的const变量本质上是’只读变量’(仍占内存),C++的const更像真正的常量(编译器可能直接用值替换)。C++中const默认内部链接,C中默认外部链接。

  • C 中 const 变量本质是”只读变量”,仍然占内存,不能用于数组大小等常量表达式
  • C++ 中 const 变量如果初始化为常量表达式,编译器直接替换(类似 #define),可用于数组大小
1
// C 中
2
const int SIZE = 10;
3
int arr[SIZE]; // C99 之前不允许!SIZE 不是真正的编译期常量
4
5
// C++ 中
6
const int SIZE = 10;
7
int arr[SIZE]; // 完全合法!编译器把 SIZE 替换为 10
8
9
// C++ 中 const 还用于成员函数
10
class Sensor {
11
int value;
12
public:
13
int getValue() const { return value; } // const 成员函数不修改对象
14
void setValue(int v) { value = v; }
15
};
4 collapsed lines
16
17
const Sensor s;
18
s.getValue(); // OK,const 对象只能调用 const 成员函数
19
// s.setValue(5); // 错误!const 对象不能调用非 const 函数

Q8: 什么是命名空间(namespace)?为什么需要?

🧠 秒懂: 命名空间是C++给标识符划分’地盘’的机制,防止不同库的同名函数/变量冲突。就像不同城市可以有同名的街道,用城市名(命名空间)区分。

答: 命名空间用于解决”名字冲突”问题。当项目很大,多个库可能有同名函数时,用命名空间区分。

1
namespace DriverA {
2
void init() { /* A 公司的初始化 */ }
3
}
4
namespace DriverB {
5
void init() { /* B 公司的初始化 */ }
6
}
7
8
DriverA::init(); // 调用 A 的 init
9
DriverB::init(); // 调用 B 的 init
10
11
// using 声明(不推荐在头文件中用)
12
using namespace DriverA;
13
init(); // 直接调用 A 的

嵌入式建议: 用命名空间包装驱动代码,避免和第三方库冲突。


Q9: bool 类型在 C 和 C++ 中?

🧠 秒懂: C99之前没有原生bool,用int模拟(0为假,非0为真)。C++原生支持bool类型(true/false)。C99通过<stdbool.h>引入bool,但本质上还是宏定义。

答: C++ 内置 bool 类型,值为 true(1) 和 false(0)。C99 之后通过 #include <stdbool.h> 也支持,但本质是宏定义。

1
// C++: 原生支持
2
bool flag = true;
3
if (flag) { /* ... */ }
4
5
// C99: 宏定义
6
#include <stdbool.h> // 定义了 bool=_Bool, true=1, false=0
7
bool flag = true;

Q10: inline 内联函数?

🧠 秒懂: inline提示编译器把函数体展开到调用处,省去函数调用开销。适合短小频繁的函数。现代编译器会自动决定是否内联,inline更像是链接属性声明。

答: inline 建议编译器把函数体直接插入到调用处(像宏展开),避免函数调用开销(压栈、跳转、返回)。

1
inline int max(int a, int b) {
2
return a > b ? a : b;
3
}
4
5
int result = max(x, y);
6
// 编译器可能展开为: int result = x > y ? x : y;
7
// 省去了函数调用的开销

inline vs 宏的优势:

  • 有类型检查
  • 可调试
  • 不会有宏的副作用(如 MAX(a++, b) 的问题)

注意: inline 只是建议,编译器可能不内联复杂函数。嵌入式中短小频繁调用的函数适合 inline。


Q11: C++ 中的默认参数?

🧠 秒懂: 函数参数可以给默认值:void init(int baud = 9600)。调用时不传参数就用默认值。注意默认参数只能从右往左设,且声明和定义不能同时写默认值。

答: 函数参数可以有默认值,调用时不传则使用默认值。默认参数必须从右到左连续声明。

1
void uart_init(int baud = 115200, int databits = 8, int stopbits = 1) {
2
// 初始化串口
3
}
4
5
uart_init(); // 使用全部默认值: 115200, 8, 1
6
uart_init(9600); // baud=9600, 其余默认
7
uart_init(9600, 7); // baud=9600, databits=7, stopbits 默认 1

注意: 默认参数只在声明(头文件)中写,定义(.cpp)中不要重复写。


Q12: 什么是 RAII?

🧠 秒懂: RAII(资源获取即初始化):在构造函数中获取资源,在析构函数中释放资源。对象离开作用域自动调用析构函数释放资源,不怕忘记释放。是C++资源管理的核心思想。

答: RAII = Resource Acquisition Is Initialization(资源获取即初始化)。核心思想:把资源的获取放在构造函数里,把资源的释放放在析构函数里。这样只要对象销毁,资源就自动释放,不会遗忘。

1
// 自己写一个 GPIO 锁(演示 RAII 思想)
2
class GPIOLock {
3
int pin;
4
public:
5
GPIOLock(int p) : pin(p) {
6
gpio_lock(pin); // 构造时获取资源(锁住 GPIO)
7
}
8
~GPIOLock() {
9
gpio_unlock(pin); // 析构时释放资源(解锁 GPIO)
10
}
11
};
12
13
void some_function() {
14
GPIOLock lock(5); // 构造 → 锁住 GPIO5
15
// 做一些操作...
10 collapsed lines
16
// 如果中间抛异常或提前 return,析构函数仍会被调用 → 自动解锁
17
} // 函数结束,lock 析构 → 自动解锁 GPIO5
18
19
// 对比 C 语言的写法(容易忘记释放):
20
void some_function_c() {
21
gpio_lock(5);
22
// 做一些操作...
23
if (error) return; // 忘记解锁!资源泄漏!
24
gpio_unlock(5);
25
}

嵌入式中的典型 RAII: 互斥锁、DMA buffer、文件句柄、中断使能/禁止。


进阶补充(合并自RAII嵌入式实践):

1
// 嵌入式RAII经典: 中断锁/GPIO锁
2
class InterruptLock {
3
uint32_t primask_;
4
public:
5
InterruptLock() { primask_ = __get_PRIMASK(); __disable_irq(); } // 构造时关中断
6
~InterruptLock() { __set_PRIMASK(primask_); } // 析构自动恢复
7
};
8
9
// 使用: 作用域结束自动恢复中断
10
{
11
InterruptLock lock; // 关中断
12
shared_data = new_value; // 安全操作
13
} // 自动恢复中断—即使异常也不会忘记!

Q13: auto 关键字(C++11)?

🧠 秒懂: auto让编译器自动推导变量类型:auto x = 42; // int。减少冗长的类型声明,配合迭代器和lambda特别好用。C++11引入,嵌入式中可安全使用。

答: auto 让编译器自动推导变量类型。代码更简洁,但类型要从初始化表达式能推导出来。

1
auto x = 10; // int
2
auto y = 3.14; // double
3
auto p = new int; // int*
4
auto &ref = x; // int&(引用)
5
6
// 在迭代器中特别有用
7
std::vector<int> v = {1, 2, 3};
8
for (auto it = v.begin(); it != v.end(); ++it) {
9
// auto 代替了 std::vector<int>::iterator
10
}
11
12
// 范围 for 循环
13
for (auto val : v) {
14
printf("%d\n", val);
15
}

注意: 嵌入式中谨慎使用 auto,因为有时需要明确知道变量占多少字节(如 uint8_t vs int)。


Q14: nullptr vs NULL?

🧠 秒懂: nullptr是类型安全的空指针(std::nullptr_t类型),NULL在C++中是整数0可能导致重载歧义。用nullptr替代NULL是C++11的推荐做法。

答: C++ 中应该用 nullptr 代替 NULL

1
// C 中 NULL 通常定义为: #define NULL ((void*)0) 或 #define NULL 0
2
// C++ 中 NULL 是 0(整数),可能导致重载歧义:
3
4
void func(int n) { printf("int版本\n"); }
5
void func(int *p) { printf("指针版本\n"); }
6
7
func(NULL); // 调用 int 版本!因为 NULL 就是 0
8
func(nullptr); // 调用指针版本!nullptr 是真正的空指针类型

Q15: 范围枚举 enum class (C++11)?

🧠 秒懂: enum class比传统enum更安全:有作用域(不会污染命名空间)、不会隐式转换为int、可指定底层类型。嵌入式中推荐用enum class替代宏定义常量组。

答: 传统 enum 的枚举值会”泄漏”到外部作用域,可能名字冲突。enum class 强类型、有作用域。

1
// 传统 enum(问题:RED 和 ERROR_RED 冲突)
2
enum Color { RED, GREEN, BLUE };
3
enum TrafficLight { RED, YELLOW, GREEN }; // 错误!RED 重复
4
5
// enum class(解决冲突)
6
enum class Color { RED, GREEN, BLUE };
7
enum class Light { RED, YELLOW, GREEN }; // 不冲突
8
9
Color c = Color::RED; // 必须加作用域
10
Light l = Light::RED;
11
12
// 不能隐式转 int(更安全)
13
// int x = Color::RED; // 错误!
14
int x = static_cast<int>(Color::RED); // 必须显式转换
15
7 collapsed lines
16
// 可以指定底层类型(嵌入式中控制大小)
17
enum class GPIO_Mode : uint8_t {
18
INPUT = 0,
19
OUTPUT = 1,
20
AF = 2,
21
ANALOG = 3
22
}; // 只占 1 字节

二、类与对象(Q16~Q40)

Q16: 什么是类(class)?什么是对象(object)?

🧠 秒懂: 类是数据和操作数据的方法的封装(蓝图/模板),对象是类的实例(按蓝图造出的具体产品)。面向对象的核心思想:封装、继承、多态。

答: 类是一个”模板”或”蓝图”,描述了某种东西有哪些数据和能做什么操作。对象是根据这个模板创建出来的”实例”。

1
// 类 = 设计图纸
2
class LED {
3
private:
4
int pin; // 数据:引脚号
5
bool state; // 数据:当前状态
6
7
public:
8
LED(int p) : pin(p), state(false) {} // 构造函数
9
void on() { state = true; gpio_set(pin, 1); } // 操作:开灯
10
void off() { state = false; gpio_set(pin, 0); } // 操作:关灯
11
bool isOn() const { return state; } // 查询状态
12
};
13
14
// 对象 = 根据设计图造出来的实物
15
LED led1(5); // 5号引脚的 LED 实例
3 collapsed lines
16
LED led2(6); // 6号引脚的 LED 实例
17
led1.on(); // 操作对象
18
led2.off();

Q17: 构造函数和析构函数?

🧠 秒懂: 构造函数在对象创建时自动调用(负责初始化),析构函数在对象销毁时自动调用(负责清理资源)。嵌入式中析构函数常用于释放硬件资源(关中断、释放锁)。

  • 构造函数: 对象创建时自动调用,用于初始化。函数名和类名相同,没有返回值。
  • 析构函数: 对象销毁时自动调用,用于清理资源。函数名是 ~类名,没有参数和返回值。
1
class UART {
2
int fd;
3
public:
4
// 构造函数:打开串口
5
UART(const char *dev, int baud) {
6
fd = open_uart(dev, baud);
7
printf("串口已打开, fd=%d\n", fd);
8
}
9
10
// 析构函数:关闭串口(自动调用,不会忘记!)
11
~UART() {
12
close(fd);
13
printf("串口已关闭\n");
14
}
15
7 collapsed lines
16
void send(const char *data) { write(fd, data, strlen(data)); }
17
};
18
19
void test() {
20
UART uart("/dev/ttyS0", 115200); // 构造 → 打开串口
21
uart.send("Hello");
22
} // 函数结束 → uart 析构 → 自动关闭串口

Q18: 初始化列表(initializer list)是什么?为什么推荐使用?

🧠 秒懂: 初始化列表在构造函数体执行前完成成员初始化:MyClass() : a(1), b(2) {}。比在函数体内赋值更高效,且const成员和引用成员只能用初始化列表。

答: 初始化列表在构造函数体执行之前就完成成员的初始化。比在函数体内赋值更高效。

1
class Timer {
2
const int period; // const 成员必须用初始化列表
3
int &ref; // 引用成员必须用初始化列表
4
int count;
5
6
public:
7
// 初始化列表语法:冒号后面 成员(值)
8
Timer(int p, int &r) : period(p), ref(r), count(0) {
9
// 函数体中做额外操作
10
}
11
12
// 如果用赋值(对于简单类型差别不大,对于类对象开销大):
13
// Timer(int p) { period = p; } // 错误!const 不能赋值
14
};

必须使用初始化列表的场景:

  1. const 成员
  2. 引用成员
  3. 没有默认构造函数的类成员
  4. 基类的构造函数参数

Q19: 拷贝构造函数和赋值运算符?

🧠 秒懂: 拷贝构造函数用已有对象创建新对象(T a = b;),赋值运算符给已存在的对象赋值(a = b;)。有指针成员时必须自定义实现深拷贝,否则两个对象共享同一块内存导致重复释放。

  • 拷贝构造: 用一个已存在的对象创建新对象时调用
  • 赋值运算符: 两个已存在的对象之间赋值时调用
1
class Buffer {
2
int *data;
3
int size;
4
public:
5
Buffer(int n) : size(n) { data = new int[n]; }
6
~Buffer() { delete[] data; }
7
8
// 拷贝构造函数(深拷贝)
9
Buffer(const Buffer &other) : size(other.size) {
10
data = new int[size]; // 分配新内存
11
memcpy(data, other.data, size * sizeof(int)); // 复制内容
12
}
13
14
// 赋值运算符(深拷贝)
15
Buffer &operator=(const Buffer &other) {
13 collapsed lines
16
if (this == &other) return *this; // 防止自赋值
17
delete[] data; // 释放旧内存
18
size = other.size;
19
data = new int[size]; // 分配新内存
20
memcpy(data, other.data, size * sizeof(int));
21
return *this;
22
}
23
};
24
25
Buffer a(100); // 构造
26
Buffer b = a; // 拷贝构造(创建新对象)
27
Buffer c(50);
28
c = a; // 赋值运算符(已有对象赋值)

为什么需要深拷贝: 如果不写拷贝构造,编译器默认浅拷贝(只复制指针值),两个对象指向同一块内存,析构时会 double free 崩溃!


Q20: this 指针是什么?

🧠 秒懂: this是隐含的指向当前对象的指针,编译器自动传递。用于区分同名成员和参数(this->x = x)、返回自身引用(return *this)实现链式调用。

答: this 是一个隐含的指针,指向调用成员函数的那个对象自身。每个非静态成员函数里都可以用 this

1
class Counter {
2
int count;
3
public:
4
Counter(int count) {
5
this->count = count; // 区分参数和成员(参数名和成员名相同时)
6
}
7
8
Counter &increment() {
9
count++;
10
return *this; // 返回自身引用 → 链式调用
11
}
12
};
13
14
Counter c(0);
15
c.increment().increment().increment(); // 链式调用,count = 3

Q21: 访问控制 public / private / protected?

🧠 秒懂: public公开给所有人用,private只有自己(类内部)能访问,protected自己和子类能访问。封装的核心——隐藏实现细节,只暴露必要接口。

  • public:任何人都能访问
  • private:只有类自己的成员函数能访问(外面看不到)
  • protected:自己和子类能访问,外面不能
1
class Sensor {
2
private:
3
int raw_value; // 只有内部能直接访问
4
5
protected:
6
void calibrate() {} // 子类可以调用
7
8
public:
9
int getValue() { return raw_value; } // 对外接口
10
void update() { raw_value = read_adc(); }
11
};
12
13
// 外部代码:
14
Sensor s;
15
s.getValue(); // OK,public
2 collapsed lines
16
// s.raw_value; // 错误!private
17
// s.calibrate(); // 错误!protected

设计原则: 数据成员用 private,对外接口用 public,给子类用的工具用 protected。


Q22: 静态成员(static)?

🧠 秒懂: static成员属于类而非对象:所有对象共享一份数据(如计数器)或函数(如工厂方法)。static成员变量需要类外定义,static成员函数没有this指针。

答: 静态成员属于类本身,而不是某个对象。所有对象共享同一份。

1
class Device {
2
static int device_count; // 静态成员变量:所有对象共享
3
int id;
4
5
public:
6
Device() {
7
id = ++device_count; // 每创建一个对象,计数+1
8
}
9
10
static int getCount() { // 静态成员函数:不需要对象就能调用
11
return device_count;
12
// 注意:不能用 this, 不能访问非静态成员
13
}
14
};
15
int Device::device_count = 0; // 静态成员必须在类外初始化
3 collapsed lines
16
17
Device d1, d2, d3;
18
printf("总设备数: %d\n", Device::getCount()); // 输出 3

Q23: 友元(friend)?

🧠 秒懂: friend(友元)允许外部函数或类访问私有成员。打破了封装但提供了灵活性——像给好朋友一把你家的钥匙。运算符重载常需要友元。

答: 友元函数或友元类可以访问另一个类的私有成员。破坏了封装性,但有时候确实需要。

1
class Temperature {
2
float celsius;
3
public:
4
Temperature(float c) : celsius(c) {}
5
6
// 声明友元函数:允许它访问 private 的 celsius
7
friend void printTemp(const Temperature &t);
8
9
// 声明友元类
10
friend class Logger;
11
};
12
13
void printTemp(const Temperature &t) {
14
printf("温度: %.1f°C\n", t.celsius); // 可以直接访问 private
15
}

Q24: 运算符重载?

🧠 秒懂: 运算符重载让自定义类型支持+、-、<<等运算符。如重载<<实现cout输出、重载==实现对象比较。本质是定义一个特殊名字的函数(operator+)。

答: 让自定义类型可以像内置类型一样使用 +、-、==、<< 等运算符。

1
class Vector2D {
2
public:
3
float x, y;
4
Vector2D(float x, float y) : x(x), y(y) {}
5
6
// 重载 +
7
Vector2D operator+(const Vector2D &rhs) const {
8
return Vector2D(x + rhs.x, y + rhs.y);
9
}
10
11
// 重载 ==
12
bool operator==(const Vector2D &rhs) const {
13
return x == rhs.x && y == rhs.y;
14
}
15
};
3 collapsed lines
16
17
Vector2D a(1, 2), b(3, 4);
18
Vector2D c = a + b; // c = (4, 6),和基本类型一样自然

Q25: explicit 关键字?

🧠 秒懂: explicit防止构造函数的隐式类型转换:explicit MyClass(int x)。避免如f(42)时42被偷偷转成MyClass对象,提高类型安全性。

答: 禁止构造函数的隐式类型转换。防止意外转换导致的 bug。

1
class Voltage {
2
int mv;
3
public:
4
explicit Voltage(int millivolts) : mv(millivolts) {}
5
int get() const { return mv; }
6
};
7
8
Voltage v1(3300); // OK,显式构造
9
// Voltage v2 = 3300; // 错误!explicit 禁止隐式转换
10
// func(3300); // 如果 func 参数是 Voltage,也会被禁止
11
12
// 如果没有 explicit,下面的危险代码会编译通过:
13
// void setVoltage(Voltage v) { ... }
14
// setVoltage(42); // 42 被隐式转换为 Voltage(42),可能不是你想要的

Q26: const 成员函数?

🧠 秒懂: const成员函数承诺不修改对象状态:int getX() const。只读操作应标记为const,这样const对象也能调用。是接口设计的好习惯。

答: 在函数后面加 const 表示”这个函数不会修改对象的任何成员变量”。const 对象只能调用 const 成员函数。

1
class ADC {
2
int last_value;
3
public:
4
int read() {
5
last_value = read_hw_adc(); // 会修改成员
6
return last_value;
7
}
8
9
int getLastValue() const { // 不修改任何成员
10
// last_value = 0; // 错误!const 函数不能修改成员
11
return last_value;
12
}
13
};
14
15
const ADC adc; // const 对象
2 collapsed lines
16
adc.getLastValue(); // OK
17
// adc.read(); // 错误!const 对象不能调用非 const 函数

Q27: mutable 关键字?

🧠 秒懂: mutable修饰的成员变量可以在const成员函数中被修改。典型场景:缓存、互斥锁、调试计数器等不影响对象’逻辑状态’但需要更新的成员。

答: mutable 修饰的成员变量即使在 const 函数中也可以修改。用于”逻辑上不变但实现上需要改变”的场景(如缓存、计数器)。

1
class Sensor {
2
mutable int read_count; // 允许在 const 函数中修改
3
int value;
4
public:
5
int getValue() const {
6
read_count++; // OK!mutable 允许
7
return value;
8
}
9
};

Q28: 什么是继承?有什么好处?

🧠 秒懂: 继承让子类复用父类的代码和接口(is-a关系)。好处是代码复用和多态。嵌入式中继承常用于抽象硬件接口:基类定义统一API,子类实现不同平台的驱动。

答: 继承就是”让一个新类(子类)获得已有类(父类)的所有数据和功能”,然后在此基础上添加或修改。好处是代码复用和建立类之间的层次关系。

1
// 基类(父类):通用外设
2
class Peripheral {
3
protected:
4
uint32_t base_addr; // 寄存器基地址
5
bool enabled;
6
7
public:
8
Peripheral(uint32_t addr) : base_addr(addr), enabled(false) {}
9
void enable() { enabled = true; write_reg(base_addr, 1); }
10
void disable() { enabled = false; write_reg(base_addr, 0); }
11
};
12
13
// 派生类(子类):UART 继承了通用外设的所有功能
14
class UART : public Peripheral {
15
int baud_rate;
9 collapsed lines
16
public:
17
UART(uint32_t addr, int baud) : Peripheral(addr), baud_rate(baud) {}
18
void send(char c) { /* UART 特有的发送功能 */ }
19
void setBaud(int b) { baud_rate = b; /* 设置波特率寄存器 */ }
20
};
21
22
UART uart1(0x40011000, 115200);
23
uart1.enable(); // 继承自 Peripheral 的方法
24
uart1.send('A'); // UART 自己的方法

Q29: public/protected/private 继承的区别?

🧠 秒懂: public继承保持接口(is-a);protected继承限制外部访问(实现继承);private继承完全隐藏(has-a替代方案)。嵌入式中99%用public继承。

继承方式父类 public → 子类中变为父类 protected →父类 private →
publicpublicprotected不可访问
protectedprotectedprotected不可访问
privateprivateprivate不可访问
1
class Base {
2
public: int a;
3
protected: int b;
4
private: int c; // 子类永远不能直接访问
5
};
6
7
class Child : public Base {
8
void test() {
9
a = 1; // OK, 仍是 public
10
b = 2; // OK, 仍是 protected
11
// c = 3; // 错误!父类 private 子类不能访问
12
}
13
};

实际中 99% 用 public 继承。 private/protected 继承很少用。


Q30: 什么是多态(polymorphism)?

🧠 秒懂: 多态是’同一接口不同实现’——通过基类指针调用虚函数时,运行时自动选择正确的子类版本。就像’说话’这个动作,不同的人有不同的声音。

答: 多态 = “同一个接口,不同的行为”。就是用基类指针/引用调用函数时,实际执行的是子类重写的版本。

1
// 基类
2
class Shape {
3
public:
4
virtual double area() { return 0; } // 虚函数 → 多态的关键
5
virtual ~Shape() {}
6
};
7
8
// 子类1
9
class Circle : public Shape {
10
double r;
11
public:
12
Circle(double radius) : r(radius) {}
13
double area() override { return 3.14159 * r * r; }
14
};
15
17 collapsed lines
16
// 子类2
17
class Rectangle : public Shape {
18
double w, h;
19
public:
20
Rectangle(double width, double height) : w(width), h(height) {}
21
double area() override { return w * h; }
22
};
23
24
// 多态:同一个函数,不同对象表现不同
25
void printArea(Shape *s) {
26
printf("面积 = %.2f\n", s->area()); // 运行时决定调用哪个 area()
27
}
28
29
Circle c(5.0);
30
Rectangle r(3.0, 4.0);
31
printArea(&c); // 输出: 面积 = 78.54
32
printArea(&r); // 输出: 面积 = 12.00

本质: 编译时不确定调用哪个函数,运行时通过虚函数表(vtable)查找正确的函数。


Q31: 虚函数(virtual)的实现原理?

🧠 秒懂: 每个含虚函数的类有一张虚函数表(vtable),每个对象有一个指向vtable的指针(vptr)。调用虚函数时通过vptr查表找到实际函数地址。多了一次间接寻址的开销。

答: 编译器给每个有虚函数的类建一个”虚函数表”(vtable),每个对象有一个隐藏的”虚表指针”(vptr)指向它。

1
类 Shape 的虚函数表: 类 Circle 的虚函数表:
2
┌────────────┐ ┌────────────┐
3
│ &Shape::area │ │ &Circle::area │ ← 重写后指向新函数
4
│ &Shape::~Shape│ │ &Circle::~Circle│
5
└────────────┘ └────────────┘
6
7
对象内存布局:
8
┌──────────────────────┐
9
│ vptr → [vtable 地址] │ ← 隐藏的虚表指针(通常在对象开头)
10
├──────────────────────┤
11
│ 成员变量 │
12
└──────────────────────┘
13
14
调用 s->area() 时:
15
1. 通过 s 找到对象
3 collapsed lines
16
2. 读取对象的 vptr
17
3. 在 vtable 中找到 area() 的地址
18
4. 跳转执行

代价: 每个对象多占一个指针大小(4/8字节);每次虚函数调用多一次间接查表。嵌入式中如果对内存/性能极度敏感,可以避免虚函数。


💡 面试追问:

  1. 虚函数表存放在哪里(Flash还是RAM)?
  2. 多重继承时对象内有几个vptr?
  3. 虚函数调用比普通调用慢多少?嵌入式在意吗?

嵌入式建议: Cortex-M上vtable在Flash(只读),每个多态对象多4字节vptr。对于极端实时场景(us级ISR),避免在热路径上使用虚函数;普通任务级使用完全没问题。

Q32: 纯虚函数和抽象类?

🧠 秒懂: 纯虚函数 = 0 没有实现,含纯虚函数的类是抽象类不能实例化。抽象类定义接口规范,子类必须实现所有纯虚函数。是C++实现’接口’的方式。

答: 纯虚函数 = 没有函数体,强制子类必须实现。包含纯虚函数的类是抽象类,不能实例化。

1
// 抽象基类:定义接口
2
class Driver {
3
public:
4
virtual int init() = 0; // 纯虚函数,= 0 表示没有默认实现
5
virtual int read(void *buf, int len) = 0;
6
virtual int write(const void *buf, int len) = 0;
7
virtual ~Driver() {}
8
};
9
10
// 具体实现:SPI 驱动
11
class SPIDriver : public Driver {
12
public:
13
int init() override { /* SPI 初始化 */ return 0; }
14
int read(void *buf, int len) override { /* SPI 读 */ return len; }
15
int write(const void *buf, int len) override { /* SPI 写 */ return len; }
6 collapsed lines
16
};
17
18
// Driver d; // 错误!抽象类不能实例化
19
SPIDriver spi; // OK
20
Driver *drv = &spi; // OK,基类指针指向子类对象
21
drv->init(); // 多态调用 SPIDriver::init()

设计用途: 定义统一接口,让不同的实现类遵循相同规范。


Q33: override 和 final 关键字(C++11)?

🧠 秒懂: override显式标记重写虚函数(编译器帮你检查签名是否匹配),final禁止子类继续重写或继承。用override是防止手误写错函数签名的好习惯。

  • override:明确告诉编译器”我要重写父类虚函数”,如果签名不匹配会报错
  • final:禁止子类再重写这个虚函数,或禁止类被继承
1
class Base {
2
public:
3
virtual void func(int x) {}
4
};
5
6
class Child : public Base {
7
public:
8
void func(int x) override {} // OK,正确重写
9
// void func(float x) override {} // 错误!父类没有 func(float)
10
// override 帮你发现拼写/参数错误
11
};
12
13
class Final : public Child {
14
public:
15
void func(int x) final {} // 这个类的子类不能再重写 func
6 collapsed lines
16
};
17
18
class FinalClass final { // 整个类不能被继承
19
// ...
20
};
21
// class X : public FinalClass {}; // 错误!不能继承 final 类

Q34: 虚析构函数为什么重要?

🧠 秒懂: 基类析构函数不是virtual时,通过基类指针delete子类对象只调用基类析构函数,子类资源不释放(内存泄漏)。有虚函数的类析构函数应该声明为virtual。

答: 如果基类指针指向子类对象,delete 时必须通过虚析构函数才能正确调用子类析构,否则内存泄漏。

1
class Base {
2
public:
3
virtual ~Base() { printf("Base 析构\n"); } // 必须是 virtual!
4
};
5
6
class Derived : public Base {
7
int *buffer;
8
public:
9
Derived() { buffer = new int[100]; }
10
~Derived() {
11
delete[] buffer; // 释放子类独有的资源
12
printf("Derived 析构\n");
13
}
14
};
15
4 collapsed lines
16
Base *p = new Derived();
17
delete p;
18
// 输出: Derived 析构 → Base 析构(正确)
19
// 如果 ~Base() 不是 virtual → 只调用 Base 析构 → buffer 泄漏!

规则: 只要一个类可能被继承,析构函数就应该声明为 virtual。


Q35: 多重继承和菱形继承问题?

🧠 秒懂: 菱形继承导致基类被继承两份(二义性和资源浪费)。解决方案:virtual继承让孙类只保留一份基类实例。嵌入式中建议避免多重继承,用组合替代。

答: C++ 允许一个类继承多个父类。但如果两个父类有共同的爷爷类,就会出现”菱形继承”问题:爷爷的成员在孙子中有两份副本。

1
Animal
2
/ \
3
Cat Dog
4
\ /
5
CatDog ← 有两份 Animal 的成员?
6
7
解决方案:虚继承(virtual inheritance)
8
cpp
9
class Animal {
10
public:
11
int age;
12
};
13
14
class Cat : virtual public Animal {}; // 虚继承
15
class Dog : virtual public Animal {}; // 虚继承
4 collapsed lines
16
class CatDog : public Cat, public Dog {};
17
18
CatDog cd;
19
cd.age = 3; // 只有一份 age,没有歧义

嵌入式建议: 尽量避免多重继承(用组合代替),更要避免菱形继承。


Q36: 组合(composition) vs 继承?

🧠 秒懂: 继承是’是一种’(is-a),组合是’有一个’(has-a)。优先使用组合——耦合度低、更灵活。嵌入式中UART类’有’一个GPIO引脚,而不是’是’一个GPIO。

  • 继承 = “是一个”(is-a) 关系:Dog is-a Animal
  • 组合 = “有一个”(has-a) 关系:Car has-a Engine
1
// 组合(推荐)
2
class Engine { public: void start() {} };
3
class Wheel { public: void rotate() {} };
4
5
class Car {
6
Engine engine; // Car 有一个 Engine
7
Wheel wheels[4]; // Car 有 4 个 Wheel
8
public:
9
void drive() {
10
engine.start();
11
for (auto &w : wheels) w.rotate();
12
}
13
};
14
15
// 继承(只在真正的 is-a 关系时用)
1 collapsed line
16
// class Car : public Engine {} ← 错误!车不是引擎!

原则: 优先使用组合。继承耦合度高,组合更灵活。


Q37: 什么是虚函数表(vtable)的内存开销?

🧠 秒懂: 每个含虚函数的类多一张vtable(存函数指针),每个对象多一个vptr(4/8字节)。在内存紧张的MCU上要权衡,几十个对象可能浪费几百字节。

  1. 每个有虚函数的类:多一个 vtable(只有一个,所有该类对象共享)
  2. 每个对象:多一个 vptr(虚表指针),通常 4 字节(32位) 或 8 字节(64位)
1
class NoVirtual { int x; }; // sizeof = 4
2
class HasVirtual { int x; virtual void f(); }; // sizeof = 8 (32位: 4+4)
3
4
// 嵌入式中1000个对象 → 多 4KB 内存开销
5
// 如果内存非常紧张(如 2KB RAM 的 MCU),避免虚函数

Q38: C++ 自动生成哪些特殊成员函数?

🧠 秒懂: 默认构造、析构、拷贝构造、拷贝赋值(C++11后还有移动构造和移动赋值)。编译器在需要时自动生成,但有指针成员时常需手动定义以实现深拷贝。

1
class MyClass {
2
// 如果你不写,编译器自动生成以下 6 个(C++11):
3
MyClass(); // 默认构造
4
~MyClass(); // 析构
5
MyClass(const MyClass &); // 拷贝构造
6
MyClass &operator=(const MyClass &); // 拷贝赋值
7
MyClass(MyClass &&); // 移动构造 (C++11)
8
MyClass &operator=(MyClass &&); // 移动赋值 (C++11)
9
};

Q39: 如何禁止拷贝?

🧠 秒懂: C++11前:将拷贝构造和赋值声明为private不实现。C++11后:= delete 更清晰。如 MyClass(const MyClass&) = delete;,编译器会直接报错。

1
// C++11: = delete
2
class Singleton {
3
public:
4
Singleton(const Singleton &) = delete; // 禁止拷贝
5
Singleton &operator=(const Singleton &) = delete; // 禁止赋值
6
7
static Singleton &getInstance() {
8
static Singleton instance;
9
return instance;
10
}
11
private:
12
Singleton() {}
13
};

Q40: 移动语义(C++11) 是什么?

🧠 秒懂: 移动语义’偷’走临时对象的资源而非拷贝——把即将销毁对象的指针直接接管过来。就像搬家时直接’端走’旧桌子而非新做一张再扔旧的,避免了不必要的拷贝开销。 答: 移动语义允许”偷走”临时对象的资源(如堆内存),而不是复制。对嵌入式来说,避免不必要的 memcpy。

1
class Buffer {
2
int *data;
3
int size;
4
public:
5
// 移动构造函数
6
Buffer(Buffer &&other) noexcept : data(other.data), size(other.size) {
7
other.data = nullptr; // "偷走"资源后,把原对象置空
8
other.size = 0;
9
}
10
};
11
12
Buffer createBuffer() {
13
Buffer b(1000);
14
return b; // 返回时触发移动(而不是拷贝1000个int)
15
}

三、模板(Q41~Q60)

💡 面试追问: “std::move真的移动了数据吗?” → 没有!std::move只是把左值强转为右值引用(static_cast<T&&>),真正的移动逻辑在移动构造/移动赋值函数中实现(偷指针+置空源对象)。move后原对象处于”有效但未指定”状态,不能再使用其数据。

Q41: 什么是模板?有什么用?

🧠 秒懂: 模板是C++的’代码生成器’——写一套代码,编译器根据不同类型自动生成对应版本。template T max(T a, T b)。零运行时开销,是泛型编程的基础。

答: 模板让你写一套代码,适用于各种数据类型。编译器根据你使用的类型自动生成对应版本。相当于”类型参数化”。

1
// 不用模板 → 每种类型写一遍
2
int max_int(int a, int b) { return a > b ? a : b; }
3
float max_float(float a, float b) { return a > b ? a : b; }
4
// 太重复了...
5
6
// 用模板 → 写一次,适用所有类型
7
template <typename T>
8
T my_max(T a, T b) {
9
return a > b ? a : b;
10
}
11
12
my_max(3, 5); // 编译器生成 int 版本
13
my_max(3.14, 2.71); // 编译器生成 double 版本

Q42: 类模板?

🧠 秒懂: 类模板让一个类适配多种数据类型:template class Stack {}。使用时指定类型Stack。STL容器(vector/map等)都是类模板的典型应用。

答: 不仅函数可以模板化,类也可以。最典型的例子是容器类。

1
// 环形缓冲区模板(嵌入式超常用)
2
template <typename T, int SIZE>
3
class RingBuffer {
4
T data[SIZE];
5
int head = 0, tail = 0, count = 0;
6
7
public:
8
bool push(const T &item) {
9
if (count >= SIZE) return false; // 满了
10
data[tail] = item;
11
tail = (tail + 1) % SIZE;
12
count++;
13
return true;
14
}
15
19 collapsed lines
16
bool pop(T &item) {
17
if (count == 0) return false; // 空的
18
item = data[head];
19
head = (head + 1) % SIZE;
20
count--;
21
return true;
22
}
23
24
bool isEmpty() const { return count == 0; }
25
bool isFull() const { return count >= SIZE; }
26
};
27
28
// 使用:
29
RingBuffer<uint8_t, 256> uart_rx_buf; // 串口接收缓冲区, uint8_t 类型, 256 大小
30
RingBuffer<int, 32> sensor_buf; // 传感器数据缓冲区
31
32
uart_rx_buf.push(0x48);
33
uint8_t ch;
34
uart_rx_buf.pop(ch);

Q43: 模板特化(specialization)?

🧠 秒懂: 模板特化为特定类型提供专门实现。完全特化:template<> class Stack{…};。偏特化:对指针类型特殊处理。让通用模板对特殊情况也能最优处理。

答: 对某些特定类型提供不同的实现。

1
// 通用版本
2
template <typename T>
3
T abs_value(T x) {
4
return x >= 0 ? x : -x;
5
}
6
7
// 针对 bool 类型的特化(全特化)
8
template <>
9
bool abs_value<bool>(bool x) {
10
return x; // bool 没有负值
11
}
12
13
// 偏特化(部分模板参数特化)— 只能用于类模板
14
template <typename T>
15
class Storage {
12 collapsed lines
16
T data;
17
public:
18
void save(T d) { data = d; }
19
};
20
21
// 对指针类型偏特化
22
template <typename T>
23
class Storage<T*> {
24
T *data;
25
public:
26
void save(T *d) { data = d; /* 可能需要深拷贝 */ }
27
};

Q44: typename vs class 在模板中的区别?

🧠 秒懂: 在模板中typename和class完全等价。但typename还有一个特殊用途:告诉编译器某个依赖名是类型(typename T::value_type),否则编译器不知道它是类型还是值。

答: 在模板参数声明中,typenameclass 完全等价,只是写法不同。

1
template <typename T> // typename 风格
2
T add(T a, T b) { return a + b; }
3
4
template <class T> // class 风格,功能完全相同
5
T sub(T a, T b) { return a - b; }
6
7
// typename 的另一个用途:指明嵌套依赖类型
8
template <typename T>
9
void func() {
10
typename T::iterator it; // 告诉编译器 T::iterator 是类型,不是变量
11
}

Q45: 模板和内联的关系?

🧠 秒懂: 模板函数默认在头文件中定义(隐式inline),因为编译器需要看到完整定义才能生成特定类型的代码。模板代码天然适合内联,不需要显式加inline。

答: 模板函数通常定义在头文件中(因为编译器需要看到完整定义才能实例化)。短小的模板函数编译器会自动内联。

my_math.h
1
// 模板通常写在 .h 文件中
2
template <typename T>
3
inline T clamp(T val, T low, T high) {
4
if (val < low) return low;
5
if (val > high) return high;
6
return val;
7
}
8
9
// 使用
10
uint8_t pixel = clamp<uint8_t>(raw_value, 0, 255);

Q46: 可变参数模板(C++11)?

🧠 秒懂: template<typename… Args>接受任意数量任意类型的参数。通过递归展开或C++17折叠表达式处理所有参数。嵌入式中常用于类型安全的日志/格式化函数。

答: 模板参数个数可以不固定(类似 printf 的可变参数,但类型安全)。

1
// 递归终止条件
2
void print() { printf("\n"); }
3
4
// 可变参数版 print
5
template <typename T, typename... Args>
6
void print(T first, Args... rest) {
7
printf("%d ", first); // 处理第一个
8
print(rest...); // 递归处理剩余
9
}
10
11
print(1, 2, 3, 4, 5); // 输出: 1 2 3 4 5

Q47: constexpr (C++11) — 编译期计算?

🧠 秒懂: constexpr函数在编译期就算出结果(如编译期计算CRC表),零运行时开销。比宏更安全(有类型检查),比普通函数更快(编译时完成)。嵌入式利器。

答: constexpr 函数/变量可以在编译期求值。嵌入式中用于编译期计算寄存器值、查找表等。

1
// 编译期计算波特率分频值
2
constexpr uint32_t calc_brr(uint32_t pclk, uint32_t baud) {
3
return pclk / baud;
4
}
5
6
// 在编译时就算好了,不占运行时间
7
constexpr uint32_t BRR_115200 = calc_brr(72000000, 115200); // = 625
8
9
// 编译期查找表
10
constexpr int factorial(int n) {
11
return n <= 1 ? 1 : n * factorial(n - 1);
12
}
13
constexpr int f5 = factorial(5); // 编译时 = 120
14
15
// 编译期 GPIO 配置
2 collapsed lines
16
constexpr uint32_t GPIO_PIN(int n) { return 1U << n; }
17
constexpr uint32_t LED_PIN = GPIO_PIN(13); // = 0x2000

进阶补充(合并自constexpr实战/编译期计算):

1
// constexpr在嵌入式中: 编译期计算波特率寄存器值
2
constexpr uint32_t calc_brr(uint32_t clk, uint32_t baud) {
3
return (clk + baud / 2) / baud; // 编译期算好,运行时零开销
4
}
5
constexpr auto BRR_115200 = calc_brr(72000000, 115200); // 编译期常量
6
7
// if constexpr (C++17): 编译期条件分支
8
template<typename T>
9
void send(T data) {
10
if constexpr (sizeof(T) <= 4) {
11
send_register(data); // 小数据走寄存器
12
} else {
13
send_dma(&data, sizeof(T)); // 大数据走DMA
14
}
15
}

Q48: SFINAE 是什么?

🧠 秒懂: SFINAE(替换失败不是错误):模板参数推导失败时不报错,而是尝试下一个候选。是enable_if等条件编译技巧的基础,让模板根据类型特征选择不同实现。

答: SFINAE = Substitution Failure Is Not An Error(替换失败不是错误)。模板匹配时如果某个特化不合适,编译器不报错而是尝试其他版本。

1
// 简单理解:根据类型特征选择不同实现
2
#include <type_traits>
3
4
// 只对整数类型有效的函数
5
template <typename T>
6
typename std::enable_if<std::is_integral<T>::value, T>::type
7
safe_add(T a, T b) {
8
// 整数加法,检查溢出
9
return a + b;
10
}
11
12
// 只对浮点类型有效的函数
13
template <typename T>
14
typename std::enable_if<std::is_floating_point<T>::value, T>::type
15
safe_add(T a, T b) {
5 collapsed lines
16
return a + b;
17
}
18
19
safe_add(1, 2); // 调用整数版本
20
safe_add(1.0, 2.0); // 调用浮点版本

新手理解: SFINAE 就像”编译器自动选择合适的模板”,不合适的自动跳过。


Q49: if constexpr (C++17)?

🧠 秒懂: if constexpr在编译期就决定走哪个分支,不满足的分支直接丢弃不编译。比传统SFINAE更直观简洁,嵌入式中可用于根据模板参数选择不同硬件操作。

答: 编译期的 if 判断。条件不满足的分支直接不编译,比 SFINAE 更易读。

1
template <typename T>
2
void process(T value) {
3
if constexpr (std::is_integral_v<T>) {
4
// 整数:位操作
5
printf("整数: %d, 最高位: %d\n", value, value >> (sizeof(T)*8 - 1));
6
} else if constexpr (std::is_floating_point_v<T>) {
7
// 浮点:不同处理
8
printf("浮点: %f\n", value);
9
} else {
10
// 其他类型
11
static_assert(false, "不支持的类型");
12
}
13
}

Q50: 模板元编程(简单示例)?

🧠 秒懂: 模板元编程是在编译期利用模板做计算(如编译期阶乘、类型推导)。图灵完备但可读性差。实际嵌入式中主要用constexpr替代,模板元编程了解概念即可。

答: 让编译器在编译期完成计算,生成的代码中只有结果(零运行时开销)。

1
// 编译期计算斐波那契数
2
template <int N>
3
struct Fib {
4
static constexpr int value = Fib<N-1>::value + Fib<N-2>::value;
5
};
6
7
template <> struct Fib<0> { static constexpr int value = 0; };
8
template <> struct Fib<1> { static constexpr int value = 1; };
9
10
// 编译期就算好了
11
constexpr int fib10 = Fib<10>::value; // = 55,运行时 0 开销
12
13
// 嵌入式应用:编译期计算 CRC 表
14
template <uint8_t byte, int bit = 7>
15
struct CRC8_Byte {
2 collapsed lines
16
static constexpr uint8_t value = /* 编译期递推... */;
17
};

Q51: 类型安全的寄存器访问?

🧠 秒懂: 用C++模板+enum class封装寄存器:编译器检查寄存器地址和位域是否匹配,错误在编译时就能发现。比C的宏方式更安全,且零运行时开销。

答: 用模板封装寄存器操作,让编译器检查类型正确性,且零运行时开销。

1
// 用模板封装寄存器地址
2
template <uint32_t ADDR>
3
struct Register {
4
static void write(uint32_t val) {
5
*(volatile uint32_t *)ADDR = val;
6
}
7
static uint32_t read() {
8
return *(volatile uint32_t *)ADDR;
9
}
10
static void setBit(int bit) {
11
*(volatile uint32_t *)ADDR |= (1U << bit);
12
}
13
static void clearBit(int bit) {
14
*(volatile uint32_t *)ADDR &= ~(1U << bit);
15
}
10 collapsed lines
16
};
17
18
// 定义具体寄存器
19
using GPIOA_ODR = Register<0x40020014>;
20
using GPIOA_IDR = Register<0x40020010>;
21
22
GPIOA_ODR::setBit(5); // 点亮 PA5 LED
23
uint32_t val = GPIOA_IDR::read();
24
25
// 编译后和直接写指针完全一样的机器码,但代码更可读、类型更安全

Q52: 编译期位域操作?

🧠 秒懂: 用constexpr函数在编译期计算位掩码和移位值,生成的代码与手写位操作完全相同。把复杂的位域计算移到编译期,运行时只剩简单的赋值操作。

1
template <int START_BIT, int WIDTH>
2
struct BitField {
3
static constexpr uint32_t MASK = ((1U << WIDTH) - 1) << START_BIT;
4
5
static uint32_t get(uint32_t reg) {
6
return (reg & MASK) >> START_BIT;
7
}
8
static uint32_t set(uint32_t reg, uint32_t val) {
9
return (reg & ~MASK) | ((val << START_BIT) & MASK);
10
}
11
};
12
13
// 使用:提取寄存器的第 4~7 位
14
using MODE_FIELD = BitField<4, 4>;
15
uint32_t mode = MODE_FIELD::get(reg_value); // 编译期计算 MASK

Q53: 静态多态(CRTP)代替虚函数?

🧠 秒懂: CRTP(奇异递归模板)让基类通过模板参数知道子类类型,实现编译期多态。无vtable开销、可内联。嵌入式中用CRTP替代虚函数是常见的零开销抽象手段。

答: CRTP(Curiously Recurring Template Pattern) 实现编译期多态,没有 vtable 开销。

1
// 基类模板:通过模板参数知道子类类型
2
template <typename Derived>
3
class DriverBase {
4
public:
5
void init() {
6
// 编译期知道调用哪个 initImpl(),不需要 vtable
7
static_cast<Derived*>(this)->initImpl();
8
}
9
};
10
11
// 子类
12
class SPIDriver : public DriverBase<SPIDriver> {
13
public:
14
void initImpl() { /* SPI 初始化 */ }
15
};
8 collapsed lines
16
17
class I2CDriver : public DriverBase<I2CDriver> {
18
public:
19
void initImpl() { /* I2C 初始化 */ }
20
};
21
22
SPIDriver spi;
23
spi.init(); // 编译期确定调用 SPIDriver::initImpl(),零开销

Q54: 模板和中断处理?

🧠 秒懂: C++的中断服务函数必须用extern “C”声明(因为中断向量表存的是C链接名)。ISR内部可以用C++对象,但要避免动态内存分配和异常。

1
// C++ 中断处理函数必须是 extern "C"(因为链接器按 C 方式找符号)
2
extern "C" void TIM2_IRQHandler(void) {
3
// 但内部可以调用 C++ 代码
4
Timer2::getInstance().handleInterrupt();
5
}
6
7
// 或者用模板包装
8
template <int TIM_NUM>
9
class HWTimer {
10
static HWTimer *instance;
11
public:
12
void handleIRQ() { /* ... */ }
13
};

Q55: 模板的编译时间和代码体积问题?

🧠 秒懂: 模板每种类型生成一份代码(代码膨胀),编译时间也会增加。嵌入式中控制实例化类型数量、用extern template避免重复实例化。在Flash紧张的MCU上需要权衡。

答: 模板会为每种类型生成独立的代码(代码膨胀)。嵌入式 Flash 有限时要注意:

1
// 如果用了:
2
RingBuffer<uint8_t, 64> buf1;
3
RingBuffer<uint16_t, 64> buf2;
4
RingBuffer<uint32_t, 64> buf3;
5
// → 生成了 3 份几乎相同的代码!
6
7
// 优化方案:内部用 void* 实现,外部模板只做类型包装

Q56: 什么是 STL?嵌入式能用吗?

🧠 秒懂: STL(标准模板库)提供容器、算法和迭代器。嵌入式中可以用——但要注意禁用异常、避免动态内存分配(或用自定义allocator)。std::array和算法库在嵌入式中完全无开销。

答: STL = Standard Template Library,包含容器(vector/map/list…)、算法(sort/find…)、迭代器。嵌入式中:

  • 小型 MCU(如 STM32F1, 64KB Flash):通常不用 STL(占 Flash 太大)
  • 大型 MCU/MPU(如 STM32H7, 2MB Flash):可以选择性使用
  • Linux 嵌入式(如 ARM9/A7):可以正常使用

Q57: vector 基本用法?

🧠 秒懂: vector是动态数组,自动扩容(容量不够时翻倍重新分配)。支持随机访问O(1)、尾部插入均摊O(1)。嵌入式中可用reserve预分配内存避免运行时扩容。

1
#include <vector>
2
std::vector<int> v; // 动态数组
3
v.push_back(10); // 添加元素
4
v.push_back(20);
5
v.push_back(30);
6
v.size(); // 3
7
v[1]; // 20(下标访问)
8
9
for (auto &val : v) { // 范围 for 遍历
10
printf("%d\n", val);
11
}
12
13
// 底层:连续内存,容量不够自动扩容(通常 ×2)
14
// 注意:扩容时会 realloc + 拷贝所有元素 → 不适合实时场景

Q58: map 基本用法?

🧠 秒懂: map是红黑树实现的有序键值对容器,查找/插入O(logn)。unordered_map是哈希表实现,平均O(1)。嵌入式中map比较安全(无需哈希函数),但内存开销较大。

1
#include <map>
2
std::map<std::string, int> config; // 键值对,按 key 排序
3
config["baud"] = 115200;
4
config["databits"] = 8;
5
6
if (config.find("baud") != config.end()) {
7
printf("baud = %d\n", config["baud"]);
8
}
9
10
// 底层:红黑树,查找 O(logn)
11
// unordered_map:哈希表,查找 O(1) 平均

Q59: smart pointer 智能指针?

🧠 秒懂: 智能指针自动管理对象生命周期:unique_ptr独占所有权(零开销)、shared_ptr共享所有权(引用计数)、weak_ptr打破循环引用。是RAII在内存管理中的直接应用。

答: 智能指针自动管理内存,不需要手动 delete(RAII 思想)。

1
#include <memory>
2
3
// unique_ptr:独占所有权,不能拷贝
4
std::unique_ptr<int[]> buf(new int[100]);
5
// buf 离开作用域自动 delete[]
6
7
// shared_ptr:共享所有权,引用计数
8
std::shared_ptr<Sensor> s = std::make_shared<Sensor>();
9
auto s2 = s; // 引用计数 = 2
10
// 最后一个 shared_ptr 销毁时才 delete
11
12
// 嵌入式注意:shared_ptr 有额外开销(引用计数、控制块)
13
// 资源受限设备优先用 unique_ptr 或完全不用动态内存

💡 面试追问:

  1. unique_ptr和shared_ptr的性能差异?
  2. shared_ptr的引用计数是线程安全的吗?
  3. 嵌入式中用智能指针有什么注意事项?

嵌入式建议: unique_ptr零开销(和裸指针一样),强烈推荐。shared_ptr有引用计数开销(atomic操作+control block),资源受限MCU上谨慎使用。优先用unique_ptr+move语义。


📊 智能指针对比表

特性unique_ptrshared_ptrweak_ptr
所有权独占共享(引用计数)不拥有(观察)
开销零(和裸指针一样)引用计数+control block同shared_ptr
拷贝❌禁止(只能move)✅(计数+1)✅(不增加计数)
线程安全非线程安全计数操作原子lock()获取shared
嵌入式推荐⭐首选谨慎使用打破循环引用
典型场景独占硬件资源多模块共享对象缓存/观察者

Q60: std::array vs C 数组?

🧠 秒懂: std::array是固定大小的数组容器,大小编译时确定,存在栈上。比C数组多了.size()/.at()等安全接口,性能完全相同。嵌入式中强烈推荐替代C数组。

1
#include <array>
2
3
std::array<int, 10> arr = {1, 2, 3}; // 固定大小,栈上分配
4
arr.size(); // 10(始终知道大小)
5
arr.at(5); // 有越界检查(debug 时有用)
6
arr.fill(0); // 全部填 0
7
8
// vs C 数组:
9
int c_arr[10];
10
// sizeof(c_arr) / sizeof(c_arr[0]) 才能得到大小
11
// 传参后退化为指针,丢失大小信息
12
13
// 嵌入式推荐 std::array:零额外开销 + 更安全

四、C++11/14/17重要特性(Q61~Q80)

Q61: auto 和 decltype?

🧠 秒懂: auto让编译器推导变量类型,decltype获取表达式的类型但不求值。auto用于简化声明,decltype用于模板中获取返回类型。C++11的两大类型推导工具。

答: auto 让编译器推导变量类型;decltype 获取表达式的类型(不求值)。

1
auto x = 42; // int
2
auto y = 3.14f; // float
3
decltype(x) z = 100; // z 的类型和 x 一样 → int
4
5
// decltype 用于返回类型
6
template <typename A, typename B>
7
auto add(A a, B b) -> decltype(a + b) {
8
return a + b;
9
}

Q62: 范围 for 循环(range-based for)?

🧠 秒懂: for (auto& x : container) 自动遍历容器所有元素。比传统for循环更简洁安全,不用操心下标越界。加&是引用避免拷贝,加const&是只读引用。

答: 简洁地遍历容器或数组。

1
int arr[] = {1, 2, 3, 4, 5};
2
for (auto val : arr) { // 拷贝遍历
3
printf("%d\n", val);
4
}
5
for (auto &val : arr) { // 引用遍历(可修改)
6
val *= 2;
7
}
8
for (const auto &val : arr) { // 只读引用(推荐大对象)
9
printf("%d\n", val);
10
}

Q63: lambda 表达式?

🧠 秒懂: lambda是匿名函数:capture{body}。捕获列表控制访问外部变量。嵌入式中常用于回调、STL算法参数、简化代码。比函数指针更灵活。

答: 匿名函数,就地定义。常用于回调、排序比较等。

1
// 语法: [捕获列表](参数) -> 返回类型 { 函数体 }
2
auto add = [](int a, int b) { return a + b; };
3
printf("%d\n", add(3, 4)); // 7
4
5
// 捕获外部变量
6
int threshold = 100;
7
auto filter = [threshold](int val) { return val > threshold; };
8
// [=] 值捕获所有 [&] 引用捕获所有 [&threshold] 引用捕获指定变量
9
10
// 嵌入式场景:中断回调注册
11
void register_callback(std::function<void()> cb);
12
register_callback([&uart]() {
13
uart.send("interrupt!\n");
14
});

Q64: std::function 是什么?

🧠 秒懂: std::function是通用的可调用对象包装器:能存储普通函数、lambda、函数对象、成员函数。类似于’万能函数指针’,但有一定的内存和性能开销。

答: 通用的函数包装器,能存储函数指针、lambda、函数对象等任何可调用对象。

1
#include <functional>
2
3
// 可以存储不同类型的"可调用对象"
4
std::function<int(int, int)> op;
5
6
op = [](int a, int b) { return a + b; };
7
printf("%d\n", op(3, 4)); // 7
8
9
op = [](int a, int b) { return a * b; };
10
printf("%d\n", op(3, 4)); // 12
11
12
// 嵌入式中用于回调函数表
13
std::function<void()> callbacks[8]; // 8 个中断回调
14
callbacks[0] = []() { handle_uart_irq(); };

进阶补充(合并自function/bind嵌入式回调):

1
// std::function实现通用回调框架
2
class EventBus {
3
std::map<int, std::function<void(int)>> handlers_; // 事件ID → 回调
4
public:
5
void on(int event_id, std::function<void(int)> handler) {
6
handlers_[event_id] = std::move(handler);
7
}
8
void emit(int event_id, int data) {
9
if (handlers_.count(event_id))
10
handlers_[event_id](data);
11
}
12
};
13
14
// 用bind绑定成员函数
15
bus.on(EVT_UART_RX, std::bind(&UartDriver::onRx, &uart, std::placeholders::_1));
  • 注意: std::function有内存开销(~32字节),资源紧张时用函数指针替代

Q65: 右值引用和移动语义详细解释?

🧠 秒懂: 右值引用(T&&)绑定到将要销毁的临时对象,实现移动语义:‘偷’走临时对象的资源而非拷贝。就像搬家时运走旧家具而非买新的再扔旧的,大幅减少不必要的拷贝。

  • 左值: 有名字、能取地址的东西(如变量)
  • 右值: 临时值、不能取地址(如 x+1、函数返回值)
  • 右值引用(&&): 绑定到右值,允许”偷走”临时对象的资源
1
// 为什么需要移动?
2
// 假设有个大数组类:
3
class BigArray {
4
int *data;
5
int size;
6
public:
7
BigArray(int n) : size(n), data(new int[n]) {}
8
~BigArray() { delete[] data; }
9
10
// 拷贝构造:需要 memcpy 整个数组(慢!)
11
BigArray(const BigArray &other) : size(other.size) {
12
data = new int[size];
13
memcpy(data, other.data, size * sizeof(int));
14
}
15
13 collapsed lines
16
// 移动构造:直接"偷走"指针(快!)
17
BigArray(BigArray &&other) noexcept : data(other.data), size(other.size) {
18
other.data = nullptr; // 原对象不再拥有这块内存
19
other.size = 0;
20
}
21
};
22
23
BigArray createArray() {
24
BigArray arr(10000);
25
return arr; // 返回临时对象 → 触发移动构造(偷指针,不拷贝)
26
}
27
28
BigArray a = createArray(); // 高效!没有拷贝万级数组

进阶补充(合并自移动语义底层/零拷贝设计):

  • 移动语义本质: 资源所有权转移(指针/句柄),避免深拷贝
  • std::move不移动任何东西——它只是static_cast到右值引用
  • 嵌入式零拷贝: DMA缓冲区用unique_ptr管理,通过move传递所有权
1
auto buf = std::make_unique<uint8_t[]>(1024); // 分配DMA缓冲区
2
dma_send(std::move(buf)); // 所有权转给DMA模块,当前代码不再持有

💡 面试追问:

  1. 左值和右值怎么区分?
  2. std::move之后原对象还能用吗?
  3. 移动语义在嵌入式中有实际应用吗?

嵌入式建议: 移动语义在容器扩容、资源转移(如DMA buffer ownership)中有用。即使不用STL容器,自己写的Buffer类加move构造可以高效转移所有权。

Q66: std::move 是什么?

🧠 秒懂: std::move不移动任何东西——它只是把一个左值强转为右值引用,告诉编译器’这个对象可以被偷走了’。真正的移动操作由移动构造/赋值函数完成。

答: std::move 不移动任何东西!它只是把左值强制转换为右值引用,然后移动构造/赋值函数才会被触发。

1
BigArray a(1000);
2
BigArray b = std::move(a); // a 被"移走了"
3
// 之后 a.data == nullptr,不能再使用 a!
4
5
// 常见用法:把不再需要的对象传给容器
6
std::vector<std::string> v;
7
std::string s = "hello world";
8
v.push_back(std::move(s)); // s 的内容被移到 vector 中
9
// s 现在是空字符串

Q67: noexcept 的作用?

🧠 秒懂: noexcept标记函数不会抛出异常,编译器可据此优化(如移动构造)。嵌入式中禁用异常后通常所有函数都隐式noexcept,但显式标注是好习惯。

答: 声明函数不会抛出异常。对移动构造特别重要(STL 容器扩容时只在 noexcept 时用移动)。

1
class Buffer {
2
public:
3
Buffer(Buffer &&other) noexcept { /* ... */ } // 承诺不抛异常
4
// 如果不加 noexcept,vector 扩容时会用拷贝而不是移动(为了安全)
5
};

Q68: structured bindings (C++17)?

🧠 秒懂: auto [x, y] = pair; 可以直接将结构体/pair/tuple的成员绑定到独立变量中。减少.first/.second的使用,代码更清晰。C++17特性。

答: 结构化绑定,可以把结构体/pair/tuple 的成员直接绑定到变量名。

1
struct SensorData {
2
float temp;
3
float humidity;
4
};
5
6
SensorData read_sensor() { return {25.5f, 60.0f}; }
7
8
auto [temperature, hum] = read_sensor(); // 直接拆出两个变量
9
printf("温度: %.1f, 湿度: %.1f%%\n", temperature, hum);
10
11
// 遍历 map
12
std::map<std::string, int> config = {{"baud", 115200}};
13
for (auto &[key, value] : config) {
14
printf("%s = %d\n", key.c_str(), value);
15
}

Q69: std::optional (C++17)?

🧠 秒懂: optional表示’可能有值也可能没有’——比使用特殊值(如-1、nullptr)表示’无’更安全明确。嵌入式中适合表示可选配置参数或可能失败的查询结果。

答: 表示”可能有值也可能没有”,比返回 -1 或 nullptr 更安全。

1
#include <optional>
2
3
std::optional<int> find_sensor(int id) {
4
if (id < NUM_SENSORS)
5
return sensor_values[id];
6
return std::nullopt; // 没找到
7
}
8
9
auto result = find_sensor(5);
10
if (result.has_value()) {
11
printf("传感器值: %d\n", result.value());
12
} else {
13
printf("传感器不存在\n");
14
}
15
2 collapsed lines
16
// 或者用 value_or 提供默认值
17
int val = find_sensor(5).value_or(-1);

进阶补充(合并自optional/variant嵌入式错误处理):

1
// std::optional替代魔数返回值
2
std::optional<int> read_sensor() {
3
if (i2c_error) return std::nullopt; // 替代 return -1
4
return sensor_value;
5
}
6
7
// std::variant实现安全的联合类型
8
using SensorData = std::variant<int, float, std::string>;
9
SensorData data = 42;
10
if (std::holds_alternative<int>(data)) {
11
int val = std::get<int>(data);
12
}
  • 嵌入式优势: 比C的union更安全,编译期类型检查

Q70: std::variant (C++17)?

🧠 秒懂: variant是类型安全的union——可以存储多种类型之一(如int或string),运行时知道当前存的是哪种类型。比C的union安全得多,不会读错类型。

答: 类型安全的 union。能持有多种类型中的一种。

1
#include <variant>
2
3
std::variant<int, float, std::string> data;
4
data = 42;
5
data = 3.14f;
6
data = "hello";
7
8
// 获取值
9
if (std::holds_alternative<int>(data)) {
10
printf("int: %d\n", std::get<int>(data));
11
}
12
13
// 用 visit 模式匹配
14
std::visit([](auto &&val) {
15
printf("值: ");
2 collapsed lines
16
// 根据实际类型处理
17
}, data);

Q71: constexpr if 在嵌入式的实用场景?

🧠 秒懂: if constexpr根据编译期条件选择代码分支,不满足的分支不会被编译。嵌入式中可根据模板参数针对不同芯片/外设生成不同代码,零运行时开销。

1
// 根据平台编译不同代码
2
template <int PLATFORM>
3
void gpio_init() {
4
if constexpr (PLATFORM == STM32F4) {
5
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
6
} else if constexpr (PLATFORM == STM32H7) {
7
RCC->AHB4ENR |= RCC_AHB4ENR_GPIOAEN;
8
}
9
// 不满足条件的分支完全不编译 → 零代码体积
10
}

Q72: static_assert 编译期断言?

🧠 秒懂: static_assert在编译时检查条件:static_assert(sizeof(int)==4, “需要32位int”)。在嵌入式中用于确保数据类型大小、对齐等编译时假设成立。

1
// 编译时检查条件,不满足直接报错
2
static_assert(sizeof(int) == 4, "需要 32 位 int");
3
static_assert(sizeof(void*) == 4, "只支持 32 位平台");
4
5
template <typename T>
6
class DMABuffer {
7
static_assert(std::is_trivially_copyable_v<T>,
8
"DMA 缓冲区只能存放可直接拷贝的类型(POD)");
9
T data[256];
10
};

Q73: using 别名 vs typedef?

🧠 秒懂: using别名比typedef更清晰,尤其对模板:using Vec = std::vector;。typedef对函数指针和模板的语法晦涩,using语法更直观统一。

1
// 旧方式: typedef
2
typedef void (*CallbackFunc)(int);
3
typedef std::vector<std::pair<int,int>> PairVector;
4
5
// C++11 新方式: using(更清晰,支持模板)
6
using CallbackFunc = void(*)(int);
7
using PairVector = std::vector<std::pair<int,int>>;
8
9
// using 最大优势:模板别名
10
template <typename T>
11
using Vec = std::vector<T>; // typedef 做不到这个!
12
Vec<int> v;

Q74: 委托构造(delegating constructor)?

🧠 秒懂: 委托构造让一个构造函数调用同类的另一个构造函数:MyClass() : MyClass(0, 0) {}。消除构造函数之间的重复初始化代码。C++11引入。

1
class UART {
2
int baud;
3
int port;
4
public:
5
UART(int b, int p) : baud(b), port(p) {
6
// 实际初始化代码
7
configure_hardware();
8
}
9
UART(int b) : UART(b, 0) {} // 委托给上面的构造函数
10
UART() : UART(115200, 0) {} // 委托给上面的
11
};

Q75: 列表初始化(initializer_list)?

🧠 秒懂: initializer_list支持花括号初始化:MyClass({1,2,3})。让自定义容器也能像内置数组一样用花括号初始化。STL容器的花括号初始化就依赖它。

1
#include <initializer_list>
2
3
class SensorArray {
4
std::vector<int> pins;
5
public:
6
SensorArray(std::initializer_list<int> pin_list) : pins(pin_list) {}
7
};
8
9
SensorArray sensors({1, 2, 3, 4, 5}); // 用花括号列表初始化
10
// 或者
11
SensorArray sensors = {1, 2, 3, 4, 5};

Q76: 尾置返回类型(trailing return type)?

🧠 秒懂: auto func(int x) -> int {} 把返回类型写在参数后面。在模板中返回类型依赖参数类型时特别有用(C++11)。C++14后auto可以直接推导返回类型。

1
// 当返回类型依赖于参数类型时
2
template <typename A, typename B>
3
auto multiply(A a, B b) -> decltype(a * b) {
4
return a * b;
5
}
6
7
// C++14 后可以简化为:
8
template <typename A, typename B>
9
auto multiply(A a, B b) {
10
return a * b; // 编译器自动推导返回类型
11
}

Q77: 属性标记 [[nodiscard]], [[maybe_unused]] 等?

🧠 秒懂: [[nodiscard]]标记返回值不应被忽略(如错误码),[[maybe_unused]]抑制未使用警告,[[deprecated]]标记已弃用。编译器根据属性给出有用的警告。

1
[[nodiscard]] int read_sensor() {
2
return adc_read();
3
}
4
// read_sensor(); // 警告!返回值被丢弃了(可能是 bug)
5
int val = read_sensor(); // OK
6
7
[[maybe_unused]] int debug_flag = 1; // 不会警告"变量未使用"
8
9
[[deprecated("请使用 new_init()")]]
10
void old_init() {}

Q78: std::string_view (C++17)?

🧠 秒懂: string_view是字符串的只读’视图’——不拥有数据、不拷贝、不分配内存。性能极佳,适合传递字符串参数。但要确保底层字符串的生命周期足够长。

1
#include <string_view>
2
3
// 不拥有字符串,只是"视图"(指针+长度),零拷贝
4
void process(std::string_view sv) {
5
printf("长度: %zu, 内容: %.*s\n", sv.size(), (int)sv.size(), sv.data());
6
}
7
8
process("hello"); // 字面量
9
std::string s = "world";
10
process(s); // std::string
11
process(std::string_view(buf, len)); // 原始缓冲区
12
// 不分配内存!只传递指针和长度

Q79: 折叠表达式(C++17)?

🧠 秒懂: 折叠表达式简化可变参数模板的展开:(args + …)自动展开为arg1+arg2+arg3+…。替代了C++11中复杂的递归模板展开。C++17特性。

1
// 对可变参数做"折叠"运算
2
template <typename... Args>
3
auto sum(Args... args) {
4
return (args + ...); // 展开为 a1 + a2 + a3 + ...
5
}
6
7
int total = sum(1, 2, 3, 4, 5); // 15
8
9
// 打印所有参数
10
template <typename... Args>
11
void printAll(Args... args) {
12
((printf("%d ", args)), ...); // 逗号表达式折叠
13
printf("\n");
14
}

Q80: volatile 在 C++ 中的正确使用?

🧠 秒懂: C++中volatile不保证原子性也不保证内存序——只能用于MMIO寄存器等硬件访问。多线程同步应该用std::atomic而非volatile。这是C++和C的重要区别。

答: volatile 告诉编译器”这个变量可能在代码看不到的地方被改变”(如硬件寄存器、ISR修改的变量),不要优化掉对它的读写。

1
// 正确使用:硬件寄存器
2
volatile uint32_t *GPIOA_IDR = (volatile uint32_t *)0x40020010;
3
while (*GPIOA_IDR & (1 << 0)) {} // 编译器每次循环都会重新读
4
5
// 正确使用:ISR 修改的标志
6
volatile bool data_ready = false;
7
8
void USART_IRQHandler() {
9
data_ready = true;
10
}
11
void main_loop() {
12
while (!data_ready) {} // 如果没有 volatile,编译器可能优化为死循环
13
process_data();
14
}
15
2 collapsed lines
16
// 注意:volatile 不保证原子性!不能替代互斥锁/原子操作
17
// 多线程或多核场景应该用 std::atomic



五、嵌入式C++实践(Q81~Q140)

Q81: C++ 适合嵌入式开发吗?

🧠 秒懂: 完全适合!控制异常和RTTI(-fno-exceptions -fno-rtti),用constexpr替代运行时计算,用CRTP替代虚函数,编译结果与C一样高效。Chromium、ROS都用C++。

答: 适合,但要谨慎使用特性。现代嵌入式广泛使用 C++:Arduino 全是 C++、mbed OS 是 C++、很多 DSP/ISP 算法用 C++。

适合使用的 C++ 特性(零/低开销):

  • 类和封装
  • 引用
  • constexpr 编译期计算
  • 模板(注意代码膨胀)
  • enum class
  • 初始化列表
  • RAII

谨慎使用(有开销或不确定性):

  • 虚函数(vtable 开销)
  • 异常处理(闪存+RAM 开销大,通常关闭 -fno-exceptions)
  • RTTI(运行时类型信息,通常关闭 -fno-rtti)
  • STL 容器(动态内存分配)
  • std::string(动态内存)

Terminal window
1
C++ 嵌入式适用性分析:
2
3
可以用(推荐): 避免使用:
4
class封装寄存器 异常(try/catch)
5
RAII管理资源 RTTI(dynamic_cast/typeid)
6
模板(零开销抽象) STL容器(动态内存)
7
constexpr编译期计算 iostream(代码膨胀)
8
enum class(类型安全) 多重继承(复杂)
9
命名空间 虚函数过多(间接调用开销)
10
引用替代指针
11
12
编译选项:
13
-fno-exceptions # 禁用异常(省Flash)
14
-fno-rtti # 禁用RTTI
15
-fno-threadsafe-statics # 静态变量不加锁
4 collapsed lines
16
-Os # 优化大小
17
18
结论: C++非常适合嵌入式, 关键是选择性使用特性
19
"You don't pay for what you don't use"

Q82: 嵌入式中如何封装 GPIO?

🧠 秒懂: 用C++类封装GPIO:构造函数初始化引脚,方法set/reset控制电平,模板参数指定端口和引脚号。编译器优化后与直接操作寄存器性能完全相同。

答: 用 C++ 类封装 GPIO 操作,让代码更可读,且不增加运行时开销。

1
class GPIO {
2
volatile uint32_t *base;
3
int pin;
4
public:
5
GPIO(uint32_t port_addr, int pin_num)
6
: base(reinterpret_cast<volatile uint32_t*>(port_addr)), pin(pin_num) {}
7
8
// 设置为输出模式
9
void setOutput() {
10
// MODER 寄存器:每个 pin 占 2 位, 01=输出
11
base[0] &= ~(3U << (pin * 2));
12
base[0] |= (1U << (pin * 2));
13
}
14
15
// 设置高电平
24 collapsed lines
16
void high() {
17
base[6] = (1U << pin); // BSRR 寄存器低 16 位置位
18
}
19
20
// 设置低电平
21
void low() {
22
base[6] = (1U << (pin + 16)); // BSRR 高 16 位复位
23
}
24
25
// 读取电平
26
bool read() const {
27
return (base[4] >> pin) & 1; // IDR 寄存器
28
}
29
30
// 翻转
31
void toggle() {
32
base[5] ^= (1U << pin); // ODR 异或
33
}
34
};
35
36
// 使用(和直接操作寄存器一样的机器码!)
37
GPIO led(0x40020000, 13); // GPIOA, Pin 13
38
led.setOutput();
39
led.high();

关键: 编译器会把这些简单函数全部内联,最终生成的代码和直接写寄存器操作完全一样。


Q83: C++ 中断处理怎么写?

🧠 秒懂: 中断处理函数必须用extern “C”声明(因为向量表中存的是C链接名)。ISR内部可以安全使用C++对象,但禁止动态内存分配和异常抛出。

答: 中断向量表是 C 链接的,所以中断处理函数必须用 extern "C"。但内部可以调用任何 C++ 代码。

1
// 中断处理函数必须 extern "C"
2
extern "C" {
3
void TIM2_IRQHandler(void) {
4
Timer::getInstance().onInterrupt(); // 调用 C++ 单例
5
}
6
7
void USART1_IRQHandler(void) {
8
uart1_driver.handleRxInterrupt(); // 调用 C++ 对象方法
9
}
10
}
11
12
// 单例模式封装定时器
13
class Timer {
14
static Timer *instance;
15
volatile uint32_t tick_count;
14 collapsed lines
16
17
public:
18
static Timer &getInstance() {
19
static Timer t;
20
return t;
21
}
22
23
void onInterrupt() {
24
tick_count++;
25
TIM2->SR &= ~TIM_SR_UIF; // 清除中断标志
26
}
27
28
uint32_t getTicks() const { return tick_count; }
29
};

Q84: Singleton 模式(单例)在嵌入式中的实现?

🧠 秒懂: 单例保证全局只有一个实例:Meyers’ Singleton用局部static变量(C++11保证线程安全)。嵌入式中单例常用于硬件资源管理(如唯一的UART实例)。

答: 确保某个类只有一个实例(如 UART 驱动、系统定时器)。

1
// Meyers' Singleton(C++11 后线程安全)
2
class SystemClock {
3
uint32_t frequency;
4
5
SystemClock() : frequency(72000000) {} // 私有构造
6
SystemClock(const SystemClock&) = delete; // 禁止拷贝
7
SystemClock& operator=(const SystemClock&) = delete;
8
9
public:
10
static SystemClock& getInstance() {
11
static SystemClock instance; // 第一次调用时创建,之后返回同一个
12
return instance;
13
}
14
15
uint32_t getFreq() const { return frequency; }
5 collapsed lines
16
void setFreq(uint32_t f) { frequency = f; }
17
};
18
19
// 使用
20
SystemClock::getInstance().getFreq();

Q85: 观察者模式在嵌入式中?

🧠 秒懂: 观察者模式:一个对象(传感器)状态变化时自动通知所有订阅者。嵌入式中替代轮询——传感器数据就绪时主动推送给所有需要的模块,降低耦合。

答: 当一个事件发生时通知所有注册的”监听者”。比如按键事件通知多个模块。

1
// 简化版观察者(不用动态内存)
2
class ButtonObserver {
3
public:
4
virtual void onPress() = 0;
5
virtual void onRelease() = 0;
6
virtual ~ButtonObserver() {}
7
};
8
9
class Button {
10
ButtonObserver *observers[4]; // 最多 4 个观察者
11
int count = 0;
12
13
public:
14
void addObserver(ButtonObserver *obs) {
15
if (count < 4) observers[count++] = obs;
23 collapsed lines
16
}
17
18
void scan() {
19
if (is_pressed()) {
20
for (int i = 0; i < count; i++)
21
observers[i]->onPress();
22
}
23
}
24
};
25
26
// LED 模块监听按键
27
class LEDController : public ButtonObserver {
28
public:
29
void onPress() override { toggle_led(); }
30
void onRelease() override {}
31
};
32
33
// 蜂鸣器模块也监听按键
34
class Buzzer : public ButtonObserver {
35
public:
36
void onPress() override { beep(100); }
37
void onRelease() override {}
38
};

Q86: 状态机模式在嵌入式中?

🧠 秒懂: 状态机模式:将不同状态封装为类,状态转换通过切换对象实现。比巨大的switch/case更清晰更易扩展。嵌入式中常用于协议解析、设备控制流程。

答: 用类封装状态转换,比 switch-case 更清晰、易扩展。

1
// 状态基类
2
class State {
3
public:
4
virtual void enter() {}
5
virtual void update() = 0;
6
virtual void exit() {}
7
virtual ~State() {}
8
};
9
10
// 状态机
11
class StateMachine {
12
State *current = nullptr;
13
public:
14
void transition(State *next) {
15
if (current) current->exit();
18 collapsed lines
16
current = next;
17
current->enter();
18
}
19
void update() {
20
if (current) current->update();
21
}
22
};
23
24
// 具体状态
25
class IdleState : public State {
26
public:
27
void enter() override { printf("进入空闲态\n"); }
28
void update() override {
29
if (has_command()) {
30
machine.transition(&running_state);
31
}
32
}
33
};

Q87: C 和 C++ 混合编程完整示例?

🧠 秒懂: C++调C函数:extern “C” void c_func();。C调C++函数:C++端用extern “C”导出。核心是确保函数名不被C++修饰,链接器才能正确找到符号。

1
// === hal_uart.h (C 头文件, 兼容 C 和 C++) ===
2
#ifndef HAL_UART_H
3
#define HAL_UART_H
4
5
#ifdef __cplusplus
6
extern "C" {
7
#endif
8
9
void HAL_UART_Init(int baud);
10
int HAL_UART_Send(const uint8_t *data, int len);
11
int HAL_UART_Recv(uint8_t *buf, int len, int timeout);
12
13
#ifdef __cplusplus
14
}
15
#endif
15 collapsed lines
16
17
#endif
18
19
// === uart_driver.cpp (C++ 实现, 内部调用 C 的 HAL) ===
20
#include "hal_uart.h" // C 接口
21
22
class UARTDriver {
23
public:
24
UARTDriver(int baud) { HAL_UART_Init(baud); }
25
int send(const std::string &msg) {
26
return HAL_UART_Send(
27
reinterpret_cast<const uint8_t*>(msg.c_str()),
28
msg.size());
29
}
30
};

Q88: volatile 和 C++ 对象?

🧠 秒懂: volatile对象的成员函数必须声明为volatile才能调用。实践中很少对整个对象用volatile,通常只对硬件寄存器的指针类型使用volatile修饰。

答: volatile 对象只能调用 volatile 成员函数。实际中很少把整个对象声明为 volatile。

1
class HWRegister {
2
volatile uint32_t reg;
3
public:
4
uint32_t read() volatile { return reg; } // volatile 成员函数
5
void write(uint32_t val) volatile { reg = val; }
6
};
7
8
// 更常见的做法:按指针访问
9
volatile uint32_t *REG = (volatile uint32_t *)0x40000000;

Q89: 嵌入式 C++ 编译选项?

🧠 秒懂: -fno-exceptions禁用异常、-fno-rtti禁用运行时类型信息、-Os优化代码大小、-ffunction-sections配合—gc-sections去掉未用代码。嵌入式C++的标配选项。

Terminal window
1
arm-none-eabi-g++ -std=c++17 -Os -fno-exceptions -fno-rtti \
2
-fno-threadsafe-statics -ffunction-sections -fdata-sections ...
3
# -fno-exceptions: 禁用异常(省 Flash 几十 KB)
4
# -fno-rtti: 禁用运行时类型信息(省空间,dynamic_cast 不可用)
5
# -Os: 优化大小

Q90: 为什么嵌入式要禁用异常?

🧠 秒懂: 异常处理需要运行时支持(栈展开、类型匹配),增加代码体积(几KB到几十KB)且执行时间不确定。嵌入式系统对代码体积和实时性要求严格,所以通常禁用。

嵌入式禁用C++异常(-fno-exceptions)的原因:

  1. ROM开销大:异常表(.eh_frame)通常占用10-20%的代码空间
  2. 时间不确定:throw时需要栈展开(stack unwinding),耗时不可预测(违反实时性)
  3. 需要RTTI支持dynamic_casttypeid也占空间
  4. 无OS支持:裸机环境没有C++运行时库支持异常传播
1
// 嵌入式C++替代方案: 用返回值/错误码代替异常
2
enum class Error { OK, TIMEOUT, OVERFLOW };
3
4
Error sensor_read(float *value) {
5
if (!i2c_ready()) return Error::TIMEOUT;
6
*value = read_raw() * 0.01f;
7
return Error::OK;
8
}

Q91: placement new 是什么?

🧠 秒懂: placement new在指定内存地址上构造对象(不分配内存):new(addr) MyClass()。嵌入式中用于内存池、共享内存、DMA缓冲区等预分配内存上创建对象。

在已有的内存上构造对象(不分配新内存)。嵌入式中用于内存池。

1
uint8_t buffer[sizeof(Sensor)]; // 预分配内存
2
Sensor *s = new (buffer) Sensor(5); // 在 buffer 上构造
3
s->~Sensor(); // 手动调用析构(不 delete,因为内存不是 new 分配的)

💡 面试追问:

  1. placement new需要手动调析构函数吗?
  2. 在内存池中如何使用placement new?
  3. placement new能保证对齐吗?

嵌入式建议: 嵌入式内存池标准用法:static uint8_t pool[N]; new(pool) MyObject();。析构时手动调obj->~MyObject()。必须确保pool地址满足MyObject的对齐要求(alignas)。

Q92: std::atomic 在嵌入式中?

🧠 秒懂: std::atomic保证操作的原子性和内存可见性:atomic flag。嵌入式多核MCU或中断与主循环共享变量时使用,比volatile+关中断更规范安全。

1
#include <atomic>
2
std::atomic<bool> flag(false);
3
4
// ISR 中
5
void IRQHandler() {
6
flag.store(true, std::memory_order_release);
7
}
8
9
// 主循环中
10
while (!flag.load(std::memory_order_acquire)) {}
11
// 编译器保证原子性+正确的内存序

Q93: C++ 中的 static 变量初始化问题?

🧠 秒懂: C++11保证局部static变量初始化是线程安全的(Magic Statics)。但在嵌入式裸机中,如果没有RTOS,初始化顺序问题(Static Initialization Order Fiasco)仍需注意。

全局 C++ 对象的构造函数在 main() 之前执行。嵌入式中硬件可能还没初始化 → 不要在全局对象构造函数中访问硬件。

1
// 危险!main 之前 UART 硬件可能没准备好
2
UART global_uart(115200); // 构造函数在 main 前调用!
3
4
// 安全方案:延迟初始化
5
UART *uart_ptr = nullptr;
6
int main() {
7
HAL_Init(); // 先初始化硬件
8
static UART uart(115200); // 然后才构造
9
uart_ptr = &uart;
10
}

Q94: 嵌入式中 new 失败怎么办?

🧠 秒懂: 嵌入式中new失败通常意味着内存耗尽——应该避免到这一步。可以重载new抛出或返回nullptr,或用placement new+静态内存池从根本上避免分配失败。

禁用异常后 new 失败不会抛 bad_alloc,需要重载 operator new:

1
void *operator new(size_t size) {
2
void *p = pvPortMalloc(size); // 用 FreeRTOS 的堆
3
if (!p) {
4
// 错误处理:闪灯/打印/复位
5
error_handler();
6
}
7
return p;
8
}
9
void operator delete(void *p) noexcept {
10
vPortFree(p);
11
}

Q95: MISRA C++ 是什么?

🧠 秒懂: MISRA C++是汽车/航空等安全关键行业的C++编码规范。限制使用动态内存、异常、多重继承等特性,确保代码可预测可分析。类似于’嵌入式C++的交规’。

std::function 是通用可调用对象包装器,可以存储函数指针、lambda、bind 表达式、仿函数。有类型擦除开销(堆分配/虚调用)。模板(template)是零开销抽象——编译期确定类型,内联优化。性能敏感场景用模板,需要运行时多态(回调注册/事件系统)用 std::function。

1
/* std::function vs 模板 */
2
void callbackFunc(std::function<void()> f) { f(); } // 有开销
3
template<typename F>
4
void callbackTmpl(F f) { f(); } // 零开销, 内联

Q96: C++ 如何实现类型安全的标志位操作?

🧠 秒懂: 用enum class定义标志位,重载位运算符(|、&、~)使其类型安全。编译器阻止不同类型的标志混用:GPIO_Flag | UART_Flag会编译报错。比C的宏定义安全得多。

1
enum class IRQ_Flag : uint32_t {
2
TIMER = 1 << 0,
3
UART_RX = 1 << 1,
4
DMA = 1 << 2,
5
};
6
7
// 重载 | 运算符
8
inline IRQ_Flag operator|(IRQ_Flag a, IRQ_Flag b) {
9
return static_cast<IRQ_Flag>(
10
static_cast<uint32_t>(a) | static_cast<uint32_t>(b));
11
}
12
inline bool operator&(IRQ_Flag a, IRQ_Flag b) {
13
return (static_cast<uint32_t>(a) & static_cast<uint32_t>(b)) != 0;
14
}
15
2 collapsed lines
16
IRQ_Flag pending = IRQ_Flag::TIMER | IRQ_Flag::UART_RX;
17
if (pending & IRQ_Flag::TIMER) { /* 处理定时器 */ }

Q97: C++ 回调函数的几种实现方式?

🧠 秒懂: C++回调:①函数指针(C兼容) ②std::function(灵活但有开销) ③模板参数(零开销) ④虚函数(面向对象) ⑤lambda(简洁)。嵌入式中函数指针和模板最常用。

constexpr 函数/变量在编译期求值(如果参数是常量)。constexpr int factorial(int n) { return n<=1?1

*factorial(n-1); } 编译器直接计算结果。consteval(C++20)强制编译期求值。constexpr 比 #define 和 const 更强——支持复杂计算且有类型检查。嵌入式中用于编译期计算查表、CRC 校验表等。

1
constexpr int factorial(int n) {
2
return n <= 1 ? 1 : n * factorial(n - 1);
3
}
4
static_assert(factorial(5) == 120); // 编译期计算

Q98: C++ 代码如何调试 HardFault?

🧠 秒懂: 1.查看HardFault寄存器(SCB->CFSR/BFAR/MMFAR)确定原因 2.从栈帧恢复PC知道崩溃位置 3.反汇编map文件定位到C++代码行 4.检查虚函数调用和内存越界。

std::array<int,5> 固定大小,栈上分配,和 C 数组性能相同但有 size()/at()/迭代器等接口。std::vector 动态大小,堆上分配(可 reserve 预分配减少扩容)。嵌入式中 vector 的堆分配可能有问题(碎片/确定性)——用 array 或预分配的 vector。std::span(C++20)引用任意连续内存不拥有数据。

Q99: 嵌入式 C++ 项目推荐目录结构?

🧠 秒懂: 推荐:src/(按模块分目录) + include/(公共头文件) + drivers/(HAL封装) + app/(业务逻辑) + tests/。CMake构建。接口与实现分离,方便单元测试。

Terminal window
1
project/
2
├── src/ # .cpp 源文件
3
├── include/ # .h/.hpp 头文件
4
├── drivers/ # 硬件驱动(可用 C 或 C++)
5
├── platform/ # 平台相关代码
6
├── CMakeLists.txt
7
└── startup.s # 启动汇编

Q100: C++ 代码大小比 C 大多少?

🧠 秒懂: 合理使用C++特性(模板/constexpr/RAII等)生成的代码与C几乎一样大。代码膨胀主要来自模板过度实例化、异常处理表和虚函数表。禁用异常+RTTI后差距很小。

Lambda 捕获陷阱:[=] 值捕获拷贝当前值(后续修改无效),[&] 引用捕获(lambda 存活期间原变量必须有效——悬垂引用风险)。捕获 this 时如果对象被销毁 lambda 访问会崩溃。RTOS 回调中传 lambda 要特别注意生命周期——通常值捕获更安全。mutable 允许修改值捕获的副本。

1
int x = 10;
2
auto f1 = [x]() { return x; }; // 值捕获, 拷贝
3
auto f2 = [&x]() { return x; }; // 引用捕获, 危险!
4
// x 销毁后 f2() 未定义行为

Q101: 以下代码输出什么?

🧠 秒懂: 面试陷阱题考察你对隐式转换、运算符优先级、未定义行为等细节的理解深度。关键是分析代码执行顺序和类型转换规则,不要想当然。

1
class Base {
2
public:
3
Base() { printf("Base构造\n"); } // 基类构造函数
4
virtual ~Base() { printf("Base析构\n"); } // 虚析构函数(关键!保证正确析构)
5
};
6
class Derived : public Base {
7
public:
8
Derived() { printf("Derived构造\n"); } // 派生类构造函数
9
~Derived() { printf("Derived析构\n"); } // 派生类析构函数
10
};
11
12
int main() {
13
Base *p = new Derived(); // 基类指针指向派生类对象(多态)
14
delete p; // 因为虚析构→正确调用派生类+基类析构
15
// 输出: Base构造 → Derived构造 → Derived析构 → Base析构
1 collapsed line
16
}

答: 输出顺序:

  1. Base构造(先构造基类)
  2. Derived构造(再构造派生类)
  3. Derived析构(先析构派生类——因为虚析构)
  4. Base析构(再析构基类)

Q102: 以下代码有什么问题?

🧠 秒懂: 常见代码问题:悬挂引用、迭代器失效、对象切片、隐式转换、未定义的求值顺序等。能看出bug说明你理解C++的陷阱,这是高级程序员的标志。

1
class String {
2
char *data;
3
public:
4
String(const char *s) {
5
data = new char[strlen(s) + 1];
6
strcpy(data, s);
7
}
8
~String() { delete[] data; }
9
};
10
11
String a("hello");
12
String b = a; // ← 问题在这里!

答: 没有自定义拷贝构造函数,编译器默认浅拷贝 → a.data 和 b.data 指向同一块内存 → 析构时 double free 崩溃!

修复: 需要实现深拷贝的拷贝构造函数和赋值运算符(Rule of Three)。


Q### Q103: 虚函数和默认参数的陷阱?

1
class Base {
2
public:
3
virtual void show(int x = 10) { printf("Base: %d\n", x); }
4
};
5
class Derived : public Base {
6
public:
7
void show(int x = 20) override { printf("Derived: %d\n", x); }
8
};
9
10
Base *p = new Derived();
11
p->show(); // 输出什么?

答: 输出 Derived: 10。虚函数调用 Derived 的版本,但默认参数在编译时确定,根据指针类型(Base)取 10。教训:不要在虚函数中使用默认参数。

Q### Q104: sizeof 类?

1
class A {}; // sizeof = 1(空类占1字节)
2
class B { int x; }; // sizeof = 4
3
class C { virtual void f(); };// sizeof = 4或8(一个 vptr)
4
class D : public C { int y; };// sizeof = 8或12(vptr + int)

Q### Q105: 构造函数调用虚函数?

1
class Base {
2
public:
3
Base() { init(); } // 构造函数中调用虚函数
4
virtual void init() { printf("Base::init\n"); }
5
};
6
class Derived : public Base {
7
public:
8
void init() override { printf("Derived::init\n"); }
9
};
10
11
Derived d; // 输出什么?

答: 输出 Base::init!构造函数中调用虚函数不会多态,因为此时 Derived 部分还没构造完成。教训:不要在构造/析构函数中调用虚函数。

Q103: 智能指针循环引用问题?

🧠 秒懂: 陷阱题:虚函数可以有默认参数,但默认参数按”静态类型”解析(编译期确定)而非运行时类型。所以派生类重写虚函数时别改默认参数值——否则行为和直觉不一致。

1
class B;
2
class A {
3
public:
4
std::shared_ptr<B> b_ptr;
5
~A() { printf("A destroyed\n"); }
6
};
7
class B {
8
public:
9
std::shared_ptr<A> a_ptr; // 循环引用!
10
~B() { printf("B destroyed\n"); }
11
};
12
13
auto a = std::make_shared<A>();
14
auto b = std::make_shared<B>();
15
a->b_ptr = b; // a 引用 b, b 引用计数=2
2 collapsed lines
16
b->a_ptr = a; // b 引用 a, a 引用计数=2
17
// a,b 离开作用域后引用计数各自减1变成1, 永远不为0 → 内存泄漏!

答: A 持有 B 的 shared_ptr,B 又持有 A 的 shared_ptr,形成循环引用。两个对象的引用计数永远降不到 0,析构函数永远不会被调用。解决: 把其中一个改成 std::weak_ptr(不增加引用计数)。

Q104: const_cast 误用的危险?

🧠 秒懂: 空类sizeof=1(要有独立地址)。有虚函数+一个vptr(4/8字节)。成员按对齐规则排列可能有padding。sizeof只看成员布局,不包括动态分配的堆内存。

1
const int x = 10;
2
int *p = const_cast<int*>(&x);
3
*p = 20;
4
printf("%d, %d\n", x, *p); // 输出什么?

答: 未定义行为(UB)!编译器可能直接在使用 x 的地方替换为 10(常量折叠),所以 x 可能输出 10 而 *p 输出 20(同一地址读出不同值)。教训:const_cast 去掉的 const 必须是”本来不是 const 但被 const 引用传递”的情况,不能用来修改真正的常量。

Q105: static_cast vs dynamic_cast?

🧠 秒懂: 构造函数中调用虚函数不会多态——因为此时子类还没构造好,vtable指向的是当前类自己的版本。这是C++的安全机制,避免用到未初始化的子类成员。

static_cast 在编译时做类型转换,不做运行时检查——如果你把 Base* 转成 Derived*,但实际指向的是 Base 对象,程序不会报错但访问派生类成员是未定义行为。dynamic_cast 在运行时通过 RTTI(类型信息)检查——如果转换不合法返回 nullptr(指针)或抛 bad_cast(引用)。代价是需要启用 RTTI(嵌入式通常禁用)且有性能开销。嵌入式中一般用 static_cast + 自己保证类型正确。

Q106: 对象切片(Slicing)问题?

🧠 秒懂: 基类按值接收子类对象时,子类独有的数据被’切掉’,只留下基类部分。解决方案:用基类指针/引用传递。对象切片是多态使用中的常见陷阱。

1
class Base { public: int x; virtual void show() { } };
2
class Derived : public Base { public: int y; void show() override { } };
3
4
Derived d;
5
Base b = d; // 切片!Derived 部分(y)被丢弃
6
b.show(); // 调用 Base::show,不是 Derived::show

答: 把派生类对象按值赋给基类对象时,派生类独有的成员被”切掉”了(只复制了基类部分)。这就是对象切片。多态失效。教训:多态必须通过指针或引用,不能用值传递。

Q107: 多线程中的对象生命周期?

🧠 秒懂: 多线程中对象可能在一个线程被销毁时另一个线程仍在使用。解决:shared_ptr保证生命周期、mutex保护访问、使用线程安全的通知机制。

多线程中一个常见崩溃:线程 A 还在使用对象,线程 B 已经 delete 了它。对策:(1) shared_ptr 保证最后一个使用者释放 (2) 析构前先通知所有线程停止使用(如设标志位+join) (3) 使用线程安全的引用计数。嵌入式中资源有限,优先用方案(2)——让对象的生命周期覆盖所有使用它的线程的生命周期。


Q108: 重载(overload) vs 重写(override) vs 隐藏(hide)?

🧠 秒懂: 重载(overload):同名不同参数,编译时选择;重写(override):子类覆盖父类虚函数,运行时选择;隐藏(hide):子类同名函数隐藏父类所有同名函数。三者容易混淆。

重载重写隐藏
作用域同一个类父子类父子类
函数名相同相同相同
参数不同相同不一定
virtual不需要需要不需要
效果编译期选择运行期多态隐藏父类同名

Q109: 深拷贝 vs 浅拷贝?

🧠 秒懂: 浅拷贝只复制指针(两个对象指向同一块内存),深拷贝复制指针指向的数据(各自拥有独立内存)。有动态资源的类必须实现深拷贝,否则重复释放导致崩溃。

嵌入式 C++ 常用功能:RAII(自动资源管理)、constexpr(编译期计算)、enum class(强类型枚举)、std::array(替代 C 数组)、模板(零开销抽象)。避免的功能:异常(-fno-exceptions 禁用)、RTTI(-fno-rtti 禁用)、动态分配(用自定义 allocator 或避免)、虚函数(性能敏感路径)。

对比浅拷贝(Shallow)深拷贝(Deep)
指针成员只复制指针值(共享数据)分配新内存+复制数据
析构风险⚠️ 双重释放(double free)✅ 各自独立释放
实现方式编译器默认生成手写拷贝构造+赋值运算符

💡 面试追问: “C++11的Rule of Five是什么?” → 如果类需要自定义析构/拷贝构造/拷贝赋值中的任何一个,就应该全部五个都定义:析构+拷贝构造+拷贝赋值+移动构造+移动赋值。移动语义能避免不必要的深拷贝。


Q110: 堆 vs 栈 vs 全局/静态?

🧠 秒懂: 栈:自动管理、快、空间小;堆:手动管理、慢、空间大;全局/静态:程序启动到结束。嵌入式中栈空间有限(几KB),堆容易碎片化,多用全局/静态分配。

全局/静态
分配自动手动(new/malloc)编译时
大小有限(嵌入式1~8KB)受限于RAM编译时确定
速度快(移动SP)慢(算法搜索)-
生命周期函数内到 delete整个程序

Q111: 虚表(vtable)和虚表指针(vptr)详解?

🧠 秒懂: 每个有虚函数的类有一张vtable(函数指针数组),每个对象有一个vptr指向自己类的vtable。调用虚函数时:obj->vptr->vtableindex。vptr占4/8字节。

当一个类有虚函数时,编译器会为该类创建一张虚函数表(vtable)——一个函数指针数组,每个元素指向该类对应虚函数的实际实现。每个含虚函数的对象内部有一个隐藏指针 vptr,指向所属类的 vtable。调用虚函数时,编译器生成的代码是 obj->vptr[index](),即通过 vptr 查 vtable 间接调用。派生类 override 虚函数时,它的 vtable 中对应位置被替换为新函数地址,这就是运行时多态的底层机制。

1
Base vtable: Derived vtable:
2
┌──────────────┐ ┌──────────────┐
3
&Base::show │ │ &Derived::show│ ← override 了
4
&Base::foo │ │ &Base::foo │ ← 没 override, 继承
5
└──────────────┘ └──────────────┘
6
7
Base obj → vptr → Base vtable
8
Derived obj → vptr → Derived vtable

Q112: 多态的条件?

🧠 秒懂: 多态三个条件:①有继承关系 ②基类有虚函数 ③通过基类指针/引用调用虚函数。三者缺一不可——直接用对象调用不会有多态效果。

C++ 运行时多态必须同时满足三个条件:(1) 基类中函数声明为 virtual (2) 派生类 override 了该函数 (3) 通过基类指针或引用调用。直接用对象(值语义)调用不会多态(编译期就确定了)。这也是为什么工厂模式总是返回指针/引用而不是对象。

Q113: 接口(纯虚类)设计原则?

🧠 秒懂: 纯虚类作为接口:只定义虚函数签名不实现、虚析构函数、无数据成员。接口应该小而专(接口隔离原则)。嵌入式中常用于驱动层抽象。

C++ 没有 interface 关键字,用纯虚类(只有纯虚函数没有数据成员)模拟接口。设计原则:(1) 所有成员函数都是纯虚的(=0) (2) 提供虚析构函数(防止通过基类指针 delete 时内存泄漏) (3) 没有数据成员(只定义行为契约) (4) 命名常用 I 前缀(如 IDevice)。嵌入式中接口用于硬件抽象层(HAL),不同芯片实现同一接口。

1
class IDevice {
2
public:
3
virtual ~IDevice() = default;
4
virtual int read(uint8_t *buf, size_t len) = 0;
5
virtual int write(const uint8_t *buf, size_t len) = 0;
6
};

Q114: 模板 vs 继承 怎么选?

🧠 秒懂: 需要运行时多态(不同类型对象统一处理)用继承+虚函数;需要编译时多态(最高性能)用模板。嵌入式中优先模板(零开销),需要存储不同类型时用继承。

模板(编译期多态):类型在编译时确定,零运行时开销,但会代码膨胀(每个类型生成一份代码)。适合泛型算法、容器。继承(运行时多态):类型在运行时确定,有虚函数调用开销(间接跳转),但代码只有一份。适合需要动态选择实现的场景(如插件、驱动)。嵌入式中资源紧张优先用模板(零开销),需要运行时灵活性时用继承。

Q115: 空类大小为什么是 1?

🧠 秒懂: 空类大小为1字节——确保每个对象有唯一地址(两个不同对象的地址必须不同)。如果是0字节,数组中的不同对象会有相同地址,违反C++对象模型。

C++ 规定每个对象必须有唯一的地址(才能区分不同对象)。如果空类大小为 0,那么一个空类数组的所有元素地址相同,无法区分。所以编译器给空类至少分配 1 字节。但如果空类作为基类被继承,编译器可以优化为 0 大小(空基类优化 EBO)。

Q116: struct 内存对齐规则?

🧠 秒懂: 与C语言对齐规则相同:成员偏移是自身对齐值的倍数,结构体大小是最大对齐值的倍数。C++特殊之处是虚函数表指针(vptr)也参与对齐计算。

(1) 每个成员的起始地址必须是其对齐值(通常等于其大小)的倍数。(2) struct 总大小必须是最大成员对齐值的倍数(尾部填充)。(3) 可以用 #pragma pack(n)__attribute__((packed)) 修改对齐。

1
struct A {
2
char a; // 偏移0, 大小1
3
// 3字节padding(int要4对齐)
4
int b; // 偏移4, 大小4
5
char c; // 偏移8, 大小1
6
// 3字节尾部padding(总大小要4的倍数)
7
}; // sizeof(A) = 12, 不是 6!
8
9
struct __attribute__((packed)) B {
10
char a; int b; char c;
11
}; // sizeof(B) = 6, 紧凑但可能性能下降(非对齐访问)

Q117: 位域(bit-field)?

🧠 秒懂: 与C语言位域相同,但C++支持在类中使用。可以指定成员占用的位数:unsigned int flag : 1;。注意位域的大小端和填充方式依赖编译器和平台。

允许 struct 成员指定占用的位数,节省内存。嵌入式中常用于映射硬件寄存器。但位域的内存布局依赖编译器和平台(大端/小端、填充规则),不可移植。

1
struct RegCtrl {
2
uint32_t enable : 1; // 1 bit
3
uint32_t mode : 3; // 3 bits
4
uint32_t speed : 4; // 4 bits
5
uint32_t reserved: 24; // 24 bits
6
}; // sizeof = 4 bytes (32 bits total)

Q118: union 在 C++ 中?

🧠 秒懂: C++11后union可以包含非POD类型成员(如string),但需要手动管理构造和析构。实践中推荐用std::variant替代union,更安全且类型可追踪。

union 的所有成员共享同一块内存,大小等于最大成员。C++11 开始 union 可以包含有构造/析构的类型(但需要手动管理)。常用于:(1) 类型双关(reinterpret) (2) 节省内存(互斥的数据共享空间) (3) 协议解析(同一块数据按不同格式解析)。C++17 的 std::variant 是类型安全的 union 替代。

1
union Data {
2
float f;
3
uint32_t u;
4
};
5
Data d; d.f = 3.14f;
6
printf("0x%08X\n", d.u); // 查看 float 的二进制表示

Q119: POD 类型(Plain Old Data)?

🧠 秒懂: POD类型的内存布局与C兼容——可以用memcpy复制、用memset清零。判断标准:trivial(简单构造/析构) + standard layout(标准布局)。嵌入式与C混编时需要POD。

POD 类型是和 C 兼容的类型——没有虚函数、没有自定义构造/析构/拷贝、没有非 POD 成员、没有继承。POD 对象可以用 memcpy 复制、memset 清零、直接写入文件/网络。嵌入式中协议数据包通常设计为 POD 以便直接 cast。C++11 把 POD 细分为 trivial + standard-layout。

Q120: std::aligned_storage?

🧠 秒懂: aligned_storage提供一块正确对齐的原始内存,配合placement new使用。嵌入式中用于实现内存池或延迟构造(先分配空间,稍后再构造对象)。

提供一块对齐的原始内存,用于手动构造对象(placement new)。在实现内存池、variant、optional 等底层组件时使用。C++23 已废弃,推荐用 alignas + std::byte 数组替代。

Q121: static_cast 详解?

🧠 秒懂: static_cast做编译时已知的安全转换:基本类型转换、向上/向下转换(不检查)、void*转换。是最常用的cast,替代C风格强转的首选。

编译期类型转换,用于”合理”的转换:基本类型转换(int→float)、向上转型(Derived*→Base*)、向下转型(Base*→Derived*, 不安全需确保类型正确)、void*→具体类型。不做运行时检查,比 C 风格 cast 安全(编译器会拒绝不合理的转换)。


📊 四种类型转换对比表

转换类型检查时机安全性典型用途嵌入式使用频率
static_cast编译期较安全基本类型转换/向上转型⭐⭐⭐
dynamic_cast运行时最安全多态向下转型❌(需RTTI)
const_cast编译期中等去除const⭐(配合旧API)
reinterpret_cast无检查最危险地址→指针/位模式重解释⭐⭐⭐(寄存器访问)

Q122: dynamic_cast 详解?

🧠 秒懂: dynamic_cast运行时检查多态类型转换:成功返回正确指针,失败返回nullptr(指针)或抛异常(引用)。需要RTTI支持,嵌入式中因禁用RTTI而较少使用。

运行时类型安全转换,主要用于多态类型的向下转型(Base*→Derived*)。依赖 RTTI(运行时类型信息):如果实际类型不匹配,指针版返回 nullptr,引用版抛出 std::bad_cast。有运行时开销。嵌入式中常关闭 RTTI(-fno-rtti)来省空间,此时 dynamic_cast 不可用。

1
Base *p = getObject();
2
Derived *d = dynamic_cast<Derived*>(p);
3
if (d) { /* 转换成功, 确实是 Derived */ }
4
else { /* 转换失败, p 不是 Derived */ }

Q123: const_cast 详解?

🧠 秒懂: const_cast唯一能去掉const/volatile的转换。合法场景:const接口调用非const遗留函数。注意:去掉const后修改原始const对象是未定义行为。

移除或添加 const/volatile 限定。唯一合法场景:你知道对象原本不是 const 的,但拿到的是 const 引用/指针(如 C 库回调)。如果对象本身是 const 的(如 const int x = 1), 用 const_cast 修改它是未定义行为

Q124: reinterpret_cast 详解?

🧠 秒懂: reinterpret_cast做最底层的位模式重解释:任意指针类型互转、整数与指针互转。嵌入式中用于将地址转为寄存器指针。最危险的cast,仅在必要时使用。

最危险的转换:把任意指针/整数按位重新解释为另一种类型。不做任何检查,完全信任程序员。嵌入式中最常见用途:把整数地址转为寄存器指针 volatile uint32_t *reg = reinterpret_cast<volatile uint32_t*>(0x40020000);

  • Q128: RTTI(运行时类型信息)typeiddynamic_cast 的基础设施。编译器在 vtable 附近存储类型信息。嵌入式中通常关闭(-fno-rtti)以节省 Flash/RAM。
  • Q129: C++ 中的 volatile:和 C 一样防止编译器优化掉对变量的访问。但 volatile 不保证原子性,也不保证多核/多线程可见性。硬件寄存器访问必须加 volatile,多线程同步应该用 std::atomic
  • Q130: extern “C” 在头文件中的用法:允许 C++ 代码调用 C 函数(或被 C 调用)。加上后编译器不做 name mangling。惯用写法:#ifdef __cplusplus extern "C" { #endif ... #ifdef __cplusplus } #endif

Q125: Rule of Three / Rule of Five / Rule of Zero?

🧠 秒懂: Rule of Three:有析构/拷贝构造/拷贝赋值之一就应该三个都写。Rule of Five:C++11加上移动构造和移动赋值共五个。Rule of Zero:优先用智能指针让编译器全自动管理。

  • Rule of Three: 如果你定义了 析构函数/拷贝构造/拷贝赋值 中的任何一个,就应该定义全部三个(因为这说明类管理了资源)
  • Rule of Five(C++11): 加上 移动构造/移动赋值 → 共 5 个
  • Rule of Zero: 尽量使用智能指针和 RAII 容器,让类不需要手写任何特殊成员函数

💡 面试追问:

  1. 什么情况下会触发Rule of Five?
  2. Rule of Zero怎么实现——靠什么管资源?
  3. 嵌入式中Resource Wrapper类需要Rule of Five吗?

嵌入式建议: 封装硬件资源(GPIO/DMA handle)的类通常禁止拷贝(Rule of Five中delete copy),只允许move。这保证只有一个owner控制硬件。

Q126: 什么是对象切片(Object Slicing)?

🧠 秒懂: 基类按值接收子类对象时子类部分被切掉。核心要点:传递多态对象永远用指针或引用,不要按值传递。

1
class Base { public: int x; virtual void show() {} };
2
class Derived : public Base { public: int y; void show() override {} };
3
4
Derived d;
5
Base b = d; // 切片!d 的 Derived 部分(y)被丢弃,只剩 Base 部分
6
b.show(); // 调用 Base::show(),多态失效!
7
8
// 正确做法:用指针或引用
9
Base *p = &d;
10
p->show(); // 调用 Derived::show(),多态正确

Q127: 内存对齐(alignment)规则?

🧠 秒懂: 成员按自然对齐排列,编译器插入padding。用#pragma pack(1)或__attribute__((packed))可以取消填充,常用于网络协议和文件格式。

1
struct Example {
2
char a; // 1 字节 + 3 字节填充
3
int b; // 4 字节
4
char c; // 1 字节 + 3 字节填充
5
};
6
// sizeof = 12 (不是 6!)
7
8
// 优化:按大小排列成员
9
struct Optimized {
10
int b; // 4 字节
11
char a; // 1 字节
12
char c; // 1 字节 + 2 字节填充
13
};
14
// sizeof = 8
1
// static_cast: 编译时检查的常规转换(最常用)
2
int a = static_cast<int>(3.14); // double → int
3
Base *bp = static_cast<Base*>(&derived);
4
5
// dynamic_cast: 运行时检查的安全向下转换(需要 RTTI)
6
Base *p = getSomePointer();
7
Derived *dp = dynamic_cast<Derived*>(p);
8
if (dp) { /* 转换成功 */ }
9
10
// const_cast: 去掉或加上 const
11
const int *cp = &x;
12
int *p = const_cast<int*>(cp); // 去 const(慎用!)
13
14
// reinterpret_cast: 强制重新解释位模式(最危险)
15
uint32_t addr = 0x40020000;
1 collapsed line
16
volatile uint32_t *reg = reinterpret_cast<volatile uint32_t*>(addr);

Q128: 嵌入式中 HAL 层用 C++ 封装的完整例子?

🧠 秒懂: 完整的HAL层封装:基类定义纯虚接口(init/read/write),子类针对具体芯片实现。工厂函数返回基类指针。配合CRTP可实现零开销的编译期多态。

1
// 这个例子展示如何用 C++ 类封装硬件抽象层(HAL)
2
// 通俗解释:把对寄存器的直接操作包装成方便使用的类方法
3
4
class GPIO {
5
// base 指向这组 GPIO 的寄存器基地址(如 GPIOA = 0x40020000)
6
// 声明为 volatile 告诉编译器:这些地址的值可能被硬件随时改变,
7
// 每次必须真正去读/写,不能优化掉
8
volatile uint32_t *base;
9
10
public:
11
// 构造函数:传入寄存器基地址
12
// reinterpret_cast 把整数地址转换为指针(这是操作寄存器的标准做法)
13
GPIO(uint32_t addr) : base(reinterpret_cast<volatile uint32_t*>(addr)) {}
14
15
// set() 方法:将指定引脚设为高电平
28 collapsed lines
16
// 原理:向 BSRR 寄存器(偏移 0x18,即 base[6]) 写入对应位
17
// BSRR 寄存器的低 16 位:写 1 置位,写 0 无影响
18
void set(int pin) {
19
base[6] = (1U << pin); // base[6] 就是 *(base + 6) 即地址偏移 24 字节
20
}
21
22
// clear() 方法:将指定引脚设为低电平
23
// BSRR 高 16 位:写 1 复位
24
void clear(int pin) {
25
base[6] = (1U << (pin + 16));
26
}
27
28
// read() 方法:读取引脚电平
29
// IDR 寄存器(偏移 0x10,即 base[4]):输入数据寄存器
30
bool read(int pin) const {
31
return (base[4] >> pin) & 1;
32
}
33
};
34
35
// 使用示例:
36
GPIO gpioa(0x40020000); // 创建 GPIOA 对象
37
gpioa.set(5); // PA5 输出高电平(点亮 LED)
38
gpioa.clear(5); // PA5 输出低电平(熄灭 LED)
39
bool btn = gpioa.read(0); // 读取 PA0 按键状态
40
41
// ★ 关键理解 ★
42
// 编译后的机器码和直接写 *(volatile uint32_t*)(0x40020000 + 0x18) = (1<<5)
43
// 完全一样!C++ 类封装不增加任何运行时开销,只是让代码更易读。

Q129: CRTP(奇异递归模板) 替代虚函数?

🧠 秒懂: template class Base中,基类通过static_cast<Derived*>(this)调用子类方法。编译器直接内联,无vtable开销。

CRTP 是一种”编译期多态”技术。虚函数在运行时通过 vtable 查函数地址(有开销),CRTP 在编译时就确定了调用哪个函数(零开销)。

1
template <typename Derived>
2
class SensorBase {
3
public:
4
int read() {
5
// 编译时确定 Derived 是谁,直接调用它的 readImpl
6
return static_cast<Derived*>(this)->readImpl();
7
}
8
};
9
10
class TempSensor : public SensorBase<TempSensor> {
11
public:
12
int readImpl() { return adc_read(TEMP_CHANNEL); }
13
};
14
15
class LightSensor : public SensorBase<LightSensor> {
6 collapsed lines
16
public:
17
int readImpl() { return adc_read(LIGHT_CHANNEL); }
18
};
19
20
TempSensor ts;
21
ts.read(); // 编译器直接生成 adc_read(TEMP_CHANNEL) 的调用,无 vtable!

Q130: 类型安全的寄存器操作(模板 + enum class)?

🧠 秒懂: 模板参数传入寄存器地址和位域定义,编译器在编译期检查类型匹配。错误使用(如把GPIO寄存器地址用于UART操作)直接编译报错。

1
// 目的:让编译器帮我们检查寄存器设置是否正确,防止写错值
2
enum class GPIO_Mode : uint8_t {
3
INPUT = 0, OUTPUT = 1, ALT_FUNC = 2, ANALOG = 3
4
};
5
6
enum class GPIO_Speed : uint8_t {
7
LOW = 0, MEDIUM = 1, HIGH = 2, VERY_HIGH = 3
8
};
9
10
template <uint32_t PORT_ADDR>
11
class TypedGPIO {
12
public:
13
static void setMode(int pin, GPIO_Mode mode) {
14
volatile uint32_t *MODER = (volatile uint32_t *)PORT_ADDR;
15
*MODER &= ~(3U << (pin * 2));
7 collapsed lines
16
*MODER |= (static_cast<uint8_t>(mode) << (pin * 2));
17
}
18
};
19
20
using GPIOA = TypedGPIO<0x40020000>;
21
GPIOA::setMode(5, GPIO_Mode::OUTPUT); // 正确
22
// GPIOA::setMode(5, GPIO_Speed::HIGH); // 编译错误!类型不匹配,防止犯错

Q131: C++ 中断处理函数为什么必须 extern “C”?

🧠 秒懂: 中断向量表是C语言级别的数组,元素是函数指针。C++编译器会修饰函数名(name mangling),导致链接器找不到ISR函数。extern “C”禁止名字修饰,解决这个链接问题。

因为中断向量表是在启动文件 (startup.s) 中用弱符号(weak)定义的,链接器按 C 命名规则查找函数名(如 TIM2_IRQHandler)。C++ 编译器会对函数名做 name mangling(加类型后缀),导致链接器找不到,所以必须用 extern “C” 告诉编译器不做 name mangling。

Q132: Singleton 模式(嵌入式版)?

🧠 秒懂: Meyers’ Singleton(局部static)最简洁安全。嵌入式变体:可以用placement new在指定内存区构造单例,或禁止拷贝+移动确保唯一性。

嵌入式 OTA 升级 C++ 实现:固件包解析用类封装(头部校验/CRC/版本比较)。双分区(A/B)方案——下载新固件到非活动分区,校验通过后修改引导标志切换启动分区。C++ 的 RAII 管理 Flash 擦写会话(析构函数自动关闭/清理)。状态机模式管理升级流程(下载→校验→写入→重启)。

Q133: 工厂模式在嵌入式中的应用?

🧠 秒懂: 工厂模式根据参数创建不同的对象:create(“SPI”)返回SPI驱动,create(“I2C”)返回I2C驱动。隐藏创建细节,上层代码只依赖基类接口。

工厂模式的核心思想:调用者不需要知道具体创建哪个类,只通过一个”工厂函数”传入参数就能得到正确的对象。在嵌入式中的典型应用——根据硬件型号创建不同的驱动对象:

1
std::unique_ptr<ISensor> createSensor(SensorType type) {
2
switch (type) {
3
case SensorType::BME280: return std::make_unique<BME280>();
4
case SensorType::BMP180: return std::make_unique<BMP180>();
5
default: return nullptr;
6
}
7
}
8
9
auto sensor = createSensor(SensorType::BME280);
10
sensor->read(); // 不关心具体是哪个传感器

好处:新增传感器类型只需修改工厂函数,使用传感器的代码完全不变。


Q134: 策略模式(Strategy)?

🧠 秒懂: 策略模式将算法封装为独立的类,运行时切换:不同的滤波算法、不同的通信协议可以随时替换而不改动调用代码。比if/else更优雅,更容易扩展新策略。

策略模式把算法的选择算法的使用分离。例如数据传输模块可以选择不同的校验算法(CRC16/CRC32/简单累加和):

1
// 通过模板(编译期策略, 零开销)
2
template<typename ChecksumPolicy>
3
class DataLink {
4
ChecksumPolicy checksum;
5
public:
6
void send(const uint8_t *data, size_t len) {
7
auto crc = checksum.compute(data, len);
8
// ... 发送 data + crc
9
}
10
};
11
12
DataLink<CRC16> link_crc16; // 用 CRC16
13
DataLink<SumCheck> link_sum; // 用累加和

也可以用虚函数(运行时策略)实现,适合需要动态切换算法的场景。


Q135: 适配器模式(Adapter)?

🧠 秒懂: 适配器模式将一个类的接口转换为另一个接口:把第三方库的接口适配成项目统一的接口。嵌入式中常用于让不同厂商的驱动库统一到同一个HAL接口。

适配器模式把一个已有接口”包装”成另一个期望的接口。嵌入式中最常见的例子:项目使用统一的 IDevice 接口,但有个第三方库的传感器驱动接口完全不同,用适配器包一层:

1
class ThirdPartyDriver { // 第三方接口(不能修改)
2
public:
3
int tp_read(char *buf, int size);
4
};
5
6
class SensorAdapter : public IDevice { // 适配器
7
ThirdPartyDriver driver;
8
public:
9
int read(uint8_t *buf, size_t len) override {
10
return driver.tp_read(reinterpret_cast<char*>(buf), len);
11
}
12
};

适配器让新旧接口兼容,不需要修改任何一方的代码。


Q136: 头文件 include guard?

🧠 秒懂: #ifndef HEADER_H / #define HEADER_H / … / #endif 防止头文件被重复包含。#pragma once是简化写法。每个头文件都必须有include guard。

防止同一头文件被重复包含(导致重复定义错误)的两种方式:(1) #pragma once(简洁,现代编译器都支持) (2) 传统宏保护 #ifndef MY_FILE_H / #define MY_FILE_H / ... / #endif。推荐 #pragma once 因为不用想唯一的宏名。嵌入式中如果用很老的编译器才需要用 #ifndef 方式。


Q137: 前向声明(forward declaration)?

🧠 秒懂: 前向声明只声明不定义:class B;。用于打破头文件循环依赖——A.h和B.h互相包含时,用前向声明替代#include。能减少编译时间和依赖。

class Foo;——告诉编译器 Foo 是一个类,不需要知道它的完整定义。适用场景:头文件中只用到了 Foo 的指针/引用(不需要知道 Foo 有多大、有什么成员)。好处:减少头文件互相包含(降低编译依赖),加快编译速度。注意:如果需要创建 Foo 对象、调用 Foo 的方法或者 sizeof(Foo),则必须包含完整头文件。


Q138: pimpl 模式(Pointer to Implementation)?

🧠 秒懂: pimpl(指向实现的指针):头文件只暴露接口,实现细节放在.cpp中的私有类。修改实现不需要重新编译依赖头文件的代码,减少编译时间和接口暴露。

把类的私有数据和实现细节藏在 .cpp 文件中,头文件只暴露一个指向”实现类”的指针:

widget.h
1
class Widget {
2
public:
3
Widget();
4
~Widget();
5
void doSomething();
6
private:
7
struct Impl; // 前向声明
8
std::unique_ptr<Impl> pImpl; // 指向实现
9
};
10
11
// widget.cpp
12
struct Widget::Impl { int x; float y; /* 实际数据 */ };
13
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}

好处:修改 Impl 不需要重新编译使用 Widget 的代码;隐藏实现细节(如不想暴露第三方库的头文件)。


Q139: C++ 在内存受限 MCU(64KB Flash, 20KB RAM) 上如何使用?

🧠 秒懂: 方法:禁用异常和RTTI、用模板替代虚函数、所有内存静态分配、constexpr编译期计算、-Os优化大小。合理使用C++特性在64KB Flash上也可以工作得很好。

完全可以在资源紧张的 MCU 上用 C++,关键是禁用有额外开销的特性:(1) 编译选项禁用异常(-fno-exceptions)和 RTTI(-fno-rtti)——这两个各省几 KB (2) 不用 STL 容器(std::vector/std::map 等依赖动态内存),只用 std::array(零开销) (3) 不用 new/delete(或自定义分配器)——嵌入式中动态内存是大忌 (4) 大量使用 constexpr——编译期计算,运行时零开销 (5) 用 CRTP/模板代替虚函数——避免 vtable 开销。这样写出来的 C++ 和 C 一样高效,但代码更安全更易维护。


Q140: 嵌入式 C++ 学习路线?

🧠 秒懂: 路线:C基础→C++核心(RAII/模板/智能指针)→嵌入式C++实践(HAL封装/零开销抽象)→阅读开源项目(mbed-os/Zephyr)。循序渐进,每步都在MCU上实践。

(1) 先打牢 C 基础——指针、内存管理、位操作(嵌入式的根基) (2) 学习 C++ 核心概念——类与封装、继承与多态、RAII 资源管理 (3) 掌握现代 C++ 实用特性——auto/constexpr/enum class/nullptr/范围for/lambda (4) 深入学习模板基础——函数模板/类模板/SFINAE(理解编译期多态) (5) 了解嵌入式限制下的 C++ 最佳实践——禁异常/禁RTTI/静态多态/零成本抽象 (6) 在实际项目中练手——用 C++ 封装 GPIO/UART/Timer 驱动类,实现一个小型 RTOS 任务管理框架。推荐书:《Effective C++》《嵌入式C++编程实践》《Real-Time C++》。


本文档共收录 155 道 C++ 面试题(嵌入式方向),每题均包含详细文字解析、代码示例和通俗说明。覆盖面向对象、模板、C++11/14/17、嵌入式实践。

六、STL与现代特性(Q141~Q144)

Q141: 智能指针 unique_ptr/shared_ptr/weak_ptr 的区别?

🧠 秒懂: unique_ptr独占(零开销,不能拷贝只能移动)、shared_ptr共享(引用计数,计数归零时释放)、weak_ptr观察(不增引用计数,用于打破循环引用)。嵌入式中优先unique_ptr。

三种智能指针解决手动 delete 问题,嵌入式中 unique_ptr 零开销最常用。

1
#include <memory>
2
3
/* unique_ptr: 独占所有权, 零开销(等价裸指针) */
4
auto p1 = std::make_unique<int>(42);
5
// auto p2 = p1; // 编译错误! 不能复制
6
auto p2 = std::move(p1); // 移动所有权, p1变空
7
8
/* shared_ptr: 共享所有权, 引用计数 */
9
auto sp1 = std::make_shared<int>(42); // 引用计数=1
10
auto sp2 = sp1; // 引用计数=2
11
sp1.reset(); // 引用计数=1
12
// sp2析构时, 引用计数=0, 释放内存
13
14
/* weak_ptr: 不增加引用计数, 解决循环引用 */
15
std::weak_ptr<int> wp = sp2;
8 collapsed lines
16
if (auto locked = wp.lock()) { // 尝试获取shared_ptr
17
*locked = 100; // 成功, 对象还活着
18
}
19
20
/* 嵌入式中:
21
* unique_ptr: 推荐(零开销, 编译期保证资源释放)
22
* shared_ptr: 少用(引用计数有开销+不确定何时释放)
23
*/

进阶补充(合并自智能指针嵌入式实践):

  • unique_ptr零开销(和原始指针一样大),嵌入式优先使用
  • shared_ptr有引用计数开销(~16字节),避免在ISR或频繁调用的路径中使用
  • 循环引用: A持有B的shared_ptr, B持有A的shared_ptr → 内存泄漏。解决: 将一个改为weak_ptr
1
// 嵌入式推荐: unique_ptr管理外设资源
2
auto uart = std::make_unique<UartDriver>(USART1);
3
uart->send("hello"); // 作用域结束自动释放

Q142: C++ 异常处理(try/catch/throw)?

🧠 秒懂: try{}catch(exception& e){}——try中的代码抛出异常时跳转到匹配的catch处理。嵌入式中通常禁用,因为栈展开(stack unwinding)的开销和不确定性不可接受。

1
#include <stdexcept>
2
3
/* 基本用法 */
4
double divide(double a, double b) {
5
if (b == 0.0)
6
throw std::invalid_argument("Division by zero!");
7
return a / b;
8
}
9
10
try {
11
double result = divide(10, 0);
12
} catch (const std::invalid_argument &e) {
13
std::cerr << "参数错误: " << e.what() << std::endl;
14
} catch (const std::exception &e) {
15
std::cerr << "其他异常: " << e.what() << std::endl;
15 collapsed lines
16
} catch (...) {
17
std::cerr << "未知异常" << std::endl;
18
}
19
20
/* 异常安全级别:
21
* 1. 基本保证: 不泄漏资源, 对象处于有效状态
22
* 2. 强保证: 操作要么成功, 要么状态回滚(如copy-and-swap)
23
* 3. 不抛保证: 绝不抛异常(noexcept)
24
*
25
* 嵌入式注意:
26
* - 异常有栈展开开销(代码体积+运行时间)
27
* - 很多嵌入式项目禁用异常(-fno-exceptions)
28
* - 替代方案: 错误码返回值 + if检查
29
* - C++11 noexcept: 标记函数不抛异常(编译器可优化)
30
*/

Q143: CRTP(奇异递归模板模式)是什么?

🧠 秒懂: CRTP让基类通过模板参数知道子类类型。Derived继承Base,基类可以在编译期调用子类方法,实现零开销的静态多态。

基类以派生类作为模板参数,实现编译期多态(无虚函数开销)。嵌入式中用于零开销接口抽象。

1
/* CRTP: Curiously Recurring Template Pattern */
2
template <typename Derived>
3
class Sensor {
4
public:
5
int read() {
6
return static_cast<Derived*>(this)->read_impl();
7
}
8
};
9
10
class TempSensor : public Sensor<TempSensor> {
11
public:
12
int read_impl() { return adc_read(TEMP_CHANNEL); }
13
};
14
15
class PressSensor : public Sensor<PressSensor> {
11 collapsed lines
16
public:
17
int read_impl() { return i2c_read(PRESS_ADDR); }
18
};
19
20
/* 对比虚函数多态:
21
* 虚函数: 运行时通过vtable查找 → 间接调用, 不可内联
22
* CRTP: 编译期静态分派 → 直接调用, 可内联, 零开销
23
*
24
* 嵌入式中: 驱动HAL层/传感器接口用CRTP
25
* 缺点: 不能用基类指针存不同派生类(无运行时多态)
26
*/

Q144: emplace_back 和 push_back 的区别?

🧠 秒懂: emplace_back在容器末尾原地构造对象(直接传构造参数),push_back先构造临时对象再拷贝/移动进去。emplace_back少一次构造+析构,对大对象更高效。

emplace_back 直接在容器内部构造对象(原地构造),避免拷贝/移动,性能更好。

1
struct Point {
2
int x, y;
3
Point(int x, int y) : x(x), y(y) {}
4
};
5
6
std::vector<Point> v;
7
8
/* push_back: 先构造临时对象, 再移动/拷贝到容器 */
9
v.push_back(Point(3, 4)); // 构造临时Point → 移动进vector
10
11
/* emplace_back: 直接在vector内存中构造 */
12
v.emplace_back(3, 4); // 直接构造, 无临时对象!
13
14
/* 嵌入式注意:
15
* emplace_back对简单类型和有拷贝的场景有优势
3 collapsed lines
16
* 但vector本身涉及动态内存分配(malloc)
17
* 嵌入式中一般不用STL容器, 用固定大小数组
18
*/

七、C++多线程与设计模式(Q145~Q161)

Q145: std::thread 基本用法?

🧠 秒懂: std::thread t(func, args); 创建线程。t.join()等待结束或t.detach()分离。嵌入式多核MCU上可能用到,但更多使用RTOS的线程API。

C++11 引入标准线程库,取代平台相关的 pthread/Windows API。

1
#include <thread>
2
#include <mutex>
3
#include <iostream>
4
5
std::mutex mtx;
6
int counter = 0;
7
8
void worker(int id, int times) {
9
for (int i = 0; i < times; i++) {
10
std::lock_guard<std::mutex> lock(mtx); // RAII锁
11
counter++;
12
// lock_guard析构时自动unlock, 异常安全
13
}
14
}
15
8 collapsed lines
16
int main() {
17
std::thread t1(worker, 1, 1000);
18
std::thread t2(worker, 2, 1000);
19
t1.join(); // 等待线程结束
20
t2.join();
21
std::cout << counter << std::endl; // 2000
22
// 注意: 必须join()或detach(), 否则析构时std::terminate
23
}

Q146: std::condition_variable 生产者-消费者模型?

🧠 秒懂: 生产者-消费者:mutex保护共享队列,condition_variable实现等待/通知。生产者放入数据后notify_one(),消费者wait()直到有数据。经典多线程协作模式。

条件变量配合互斥锁实现线程间的等待/通知机制,经典的同步模式。

1
#include <queue>
2
#include <mutex>
3
#include <condition_variable>
4
#include <thread>
5
6
std::queue<int> buffer;
7
std::mutex mtx;
8
std::condition_variable cv;
9
const int MAX_SIZE = 10;
10
11
void producer() {
12
for (int i = 0; i < 100; i++) {
13
std::unique_lock<std::mutex> lock(mtx);
14
cv.wait(lock, [] { return buffer.size() < MAX_SIZE; }); // 满了等
15
buffer.push(i);
16 collapsed lines
16
cv.notify_one(); // 通知消费者
17
}
18
}
19
20
void consumer() {
21
while (true) {
22
std::unique_lock<std::mutex> lock(mtx);
23
cv.wait(lock, [] { return !buffer.empty(); }); // 空了等
24
int val = buffer.front();
25
buffer.pop();
26
cv.notify_one(); // 通知生产者
27
lock.unlock();
28
process(val);
29
}
30
}
31
/* wait的lambda防止虚假唤醒(spurious wakeup) */

Q147: 类型转换(四种cast)的区别和使用场景?

🧠 秒懂: static_cast(安全常用)、dynamic_cast(多态检查)、const_cast(去const)、reinterpret_cast(位级重解释)。按安全性从高到低排列。

C++ 提供四种显式类型转换,替代 C 风格的强制转换,各有明确语义。

1
/* 1. static_cast: 编译期已知的安全转换 */
2
int n = static_cast<int>(3.14); // double→int
3
Base *bp = static_cast<Base*>(derived_ptr); // 上行转换(安全)
4
// Derived *dp = static_cast<Derived*>(base_ptr); // 下行(不安全,无检查)
5
6
/* 2. dynamic_cast: 安全的多态下行转换(运行时检查) */
7
Base *bp = get_object();
8
Derived *dp = dynamic_cast<Derived*>(bp); // 失败返回nullptr
9
if (dp) dp->derived_method();
10
// 要求Base有虚函数(RTTI), 嵌入式中通常禁用
11
12
/* 3. const_cast: 去除/添加 const */
13
const int *cp = &val;
14
int *p = const_cast<int*>(cp); // 去const(谨慎使用!)
15
// 通常只用于兼容老API: void old_api(char *s);
10 collapsed lines
16
// old_api(const_cast<char*>(str.c_str()));
17
18
/* 4. reinterpret_cast: 底层位模式重新解释 */
19
uint32_t *reg = reinterpret_cast<uint32_t*>(0x40020000);
20
// 嵌入式寄存器访问
21
22
/* 原则:
23
* 优先 static_cast > dynamic_cast > const_cast > reinterpret_cast
24
* 禁止C风格: (int)x → 用static_cast<int>(x)
25
*/

Q148: 模板特化和偏特化?

🧠 秒懂: 完全特化为特定类型提供完全不同的实现,偏特化对部分模板参数特化(如指针类型特殊处理)。注意:函数模板只能完全特化不能偏特化,用重载替代。

模板特化允许为特定类型提供不同实现,嵌入式中常用于编译期优化。

1
/* 全特化: 为特定类型提供完全不同的实现 */
2
template<typename T>
3
T max_val(T a, T b) { return (a > b) ? a : b; }
4
5
template<> // 全特化: const char*比较字符串内容
6
const char* max_val<const char*>(const char* a, const char* b) {
7
return strcmp(a, b) > 0 ? a : b;
8
}
9
10
/* 偏特化: 对部分模板参数特化(仅类模板) */
11
template<typename T, typename U>
12
struct Pair { T first; U second; };
13
14
template<typename T>
15
struct Pair<T, T> { // 偏特化: 两个类型相同
14 collapsed lines
16
T first, second;
17
T sum() { return first + second; } // 额外方法
18
};
19
20
/* 编译期分发(嵌入式HAL常用) */
21
template<int PIN> struct GPIO {};
22
23
template<> struct GPIO<0> {
24
static void set() { GPIOA->BSRR = (1<<0); }
25
};
26
template<> struct GPIO<1> {
27
static void set() { GPIOA->BSRR = (1<<1); }
28
};
29
// 编译期确定具体寄存器操作, 零运行时开销

Q149: 深拷贝vs浅拷贝以及拷贝消除(RVO)?

🧠 秒懂: 增加RVO(返回值优化):编译器直接在目标位置构造对象,跳过拷贝/移动。C++17保证RVO(强制拷贝消除),是编译器最重要的优化之一。

理解拷贝语义和移动语义对写出高效C++代码至关重要。

1
/* 浅拷贝: 默认拷贝构造只复制指针值 */
2
class ShallowBad {
3
int *data;
4
public:
5
ShallowBad(int v) : data(new int(v)) {}
6
// 默认拷贝: 两个对象的data指向同一块内存
7
~ShallowBad() { delete data; } // 两次delete → 崩溃!
8
};
9
10
/* 深拷贝: 拷贝指针指向的内容 */
11
class DeepGood {
12
int *data;
13
public:
14
DeepGood(int v) : data(new int(v)) {}
15
DeepGood(const DeepGood &o) : data(new int(*o.data)) {} // 深拷贝
19 collapsed lines
16
DeepGood& operator=(const DeepGood &o) {
17
if (this != &o) { *data = *o.data; }
18
return *this;
19
}
20
~DeepGood() { delete data; }
21
};
22
23
/* RVO/NRVO(返回值优化): 编译器直接在调用者栈上构造 */
24
std::vector<int> make_vec() {
25
std::vector<int> v{1,2,3};
26
return v; // NRVO: 不会拷贝, 直接构造到调用者
27
}
28
auto v = make_vec(); // 零拷贝(C++17 保证)
29
30
/* 规则: 如果类管理资源(指针/文件/socket)
31
* → 必须实现: 拷贝构造、拷贝赋值、析构(Rule of Three)
32
* → C++11: 加移动构造、移动赋值(Rule of Five)
33
* → 或者: 用智能指针, 让编译器生成(Rule of Zero)
34
*/

Q150: 手写 string 类?

🧠 秒懂: 手写string类考察RAII和Rule of Three/Five:构造函数分配内存,析构函数释放,拷贝构造深拷贝,赋值运算符先释放再拷贝(注意自赋值检查)。面试高频手写题。

string 类的实现考查深浅拷贝、RAII、运算符重载等核心知识。

1
class MyString {
2
char *data_;
3
size_t size_;
4
public:
5
MyString(const char *s = "") {
6
size_ = strlen(s);
7
data_ = new char[size_ + 1];
8
strcpy(data_, s);
9
}
10
~MyString() { delete[] data_; }
11
12
/* 深拷贝 */
13
MyString(const MyString &o) : size_(o.size_) {
14
data_ = new char[size_ + 1];
15
strcpy(data_, o.data_);
24 collapsed lines
16
}
17
18
/* 拷贝赋值(copy-and-swap) */
19
MyString& operator=(MyString o) { // 值传递(已拷贝)
20
std::swap(data_, o.data_);
21
std::swap(size_, o.size_);
22
return *this;
23
} // o析构释放旧data
24
25
/* 移动构造 */
26
MyString(MyString &&o) noexcept : data_(o.data_), size_(o.size_) {
27
o.data_ = nullptr;
28
o.size_ = 0;
29
}
30
31
size_t size() const { return size_; }
32
const char* c_str() const { return data_; }
33
char& operator[](size_t i) { return data_[i]; }
34
35
friend std::ostream& operator<<(std::ostream &os, const MyString &s) {
36
return os << s.data_;
37
}
38
};
39
/* 面试重点: 深拷贝/Rule of Five/copy-and-swap/异常安全 */

Q151: 手写 shared_ptr?

🧠 秒懂: 手写shared_ptr考察引用计数管理:构造加1,析构减1到0时删除对象和计数器。拷贝构造加1,赋值先减旧的再加新的。考察对生命周期管理的深入理解。

shared_ptr 的引用计数实现是面试经典题。

1
template<typename T>
2
class SharedPtr {
3
T *ptr_;
4
int *count_; // 引用计数(堆上分配, 所有副本共享)
5
public:
6
SharedPtr(T *p = nullptr) : ptr_(p), count_(new int(1)) {}
7
8
~SharedPtr() {
9
if (--(*count_) == 0) {
10
delete ptr_;
11
delete count_;
12
}
13
}
14
15
SharedPtr(const SharedPtr &o) : ptr_(o.ptr_), count_(o.count_) {
24 collapsed lines
16
++(*count_);
17
}
18
19
SharedPtr& operator=(const SharedPtr &o) {
20
if (this != &o) {
21
if (--(*count_) == 0) { delete ptr_; delete count_; }
22
ptr_ = o.ptr_;
23
count_ = o.count_;
24
++(*count_);
25
}
26
return *this;
27
}
28
29
T& operator*() const { return *ptr_; }
30
T* operator->() const { return ptr_; }
31
int use_count() const { return *count_; }
32
};
33
34
/* 真正的shared_ptr还有:
35
* weak_ptr(打破循环引用)
36
* make_shared(一次分配, 更高效)
37
* deleter(自定义删除器)
38
* 线程安全的引用计数(atomic)
39
*/

Q152: std::atomic 和内存序?

🧠 秒懂: std::atomic保证操作原子性。内存序控制可见性:memory_order_relaxed(最弱,只保证原子)、acquire/release(同步点)、seq_cst(最强,全局顺序一致)。

C++11 原子操作是多线程无锁编程的基础。

1
#include <atomic>
2
3
std::atomic<int> counter(0);
4
5
/* 原子操作(无需mutex) */
6
counter++; // fetch_add(1)
7
counter.store(42); // 原子写
8
int val = counter.load(); // 原子读
9
int old = counter.exchange(10); // 原子交换
10
11
/* CAS(Compare-And-Swap, 无锁算法核心) */
12
int expected = 0;
13
bool ok = counter.compare_exchange_strong(expected, 1);
14
// 如果counter==0(expected), 则设为1, 返回true
15
// 如果counter!=0, expected被更新为当前值, 返回false
17 collapsed lines
16
17
/* 内存序(Memory Order) */
18
// memory_order_relaxed: 只保证原子性, 不保证顺序
19
// memory_order_acquire: 读操作, 后面的读写不能重排到前面
20
// memory_order_release: 写操作, 前面的读写不能重排到后面
21
// memory_order_seq_cst: 完全顺序一致(默认, 最安全)
22
23
/* 无锁栈(示例) */
24
struct Node { int val; Node *next; };
25
std::atomic<Node*> head{nullptr};
26
27
void push(int val) {
28
Node *n = new Node{val, nullptr};
29
n->next = head.load();
30
while (!head.compare_exchange_weak(n->next, n));
31
// CAS失败: 其他线程改了head, n->next被更新, 重试
32
}

Q153: map vs unordered_map 的区别?

🧠 秒懂: map(红黑树):有序、O(logn)、迭代器稳定;unordered_map(哈希表):无序、平均O(1)、需要好的哈希函数。小数据量map更简单,大数据量unordered_map更快。

STL 容器选择是 C++ 面试必考题。

1
/* std::map: 红黑树实现 */
2
std::map<std::string, int> m;
3
m["hello"] = 1;
4
// 查找/插入/删除: O(logN)
5
// 有序遍历: ✓ (按key排序)
6
// 内存: 每个节点有指针开销
7
8
/* std::unordered_map: 哈希表实现 */
9
std::unordered_map<std::string, int> um;
10
um["hello"] = 1;
11
// 查找/插入/删除: O(1) 平均, O(N) 最差
12
// 有序遍历: ✗ (无序)
13
// 内存: 哈希桶+链表/开放寻址
14
15
/* 选择:
17 collapsed lines
16
* 需要有序 → map
17
* 纯查找性能 → unordered_map
18
* key是自定义类型 → map更方便(只需operator<)
19
* unordered_map需要hash函数+operator==
20
*
21
* multimap: 允许重复key
22
* set: 只有key没有value
23
*/
24
25
/* 自定义key的哈希 */
26
struct Point { int x, y; };
27
struct PointHash {
28
size_t operator()(const Point &p) const {
29
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
30
}
31
};
32
std::unordered_map<Point, int, PointHash> pm;

Q154: vector 底层实现原理?

🧠 秒懂: vector底层是连续的动态数组。size()是当前元素数,capacity()是已分配空间。扩容策略通常是2倍增长,触发时需要重新分配+移动所有元素(迭代器失效)。

vector 是 C++ 最常用的容器,理解其内存管理机制是面试高频题。

1
/* vector内存模型 */
2
// [元素0][元素1]...[元素N-1][未使用空间...]
3
// ↑start ↑finish ↑end_of_storage
4
//
5
// size() = finish - start (已有元素)
6
// capacity() = end_of_storage - start (总容量)
7
8
/* 扩容策略 */
9
// 当size()==capacity()时push_back → 触发扩容
10
// 1. 分配新空间(通常×2, MSVC是×1.5)
11
// 2. 移动/拷贝旧元素到新空间
12
// 3. 释放旧空间
13
// 4. 所有迭代器/指针/引用失效!
14
15
/* 性能分析 */
17 collapsed lines
16
// push_back: 均摊O(1), 但扩容那一次是O(N)
17
// operator[]: O(1) (连续内存)
18
// insert(begin): O(N) (后面元素全部后移!)
19
// erase: O(N) (后面元素全部前移)
20
21
/* 面试技巧 */
22
std::vector<int> v;
23
v.reserve(1000); // 预分配容量(避免多次扩容)
24
v.shrink_to_fit(); // 释放多余容量
25
26
// 删除元素的正确方式:
27
v.erase(std::remove(v.begin(), v.end(), target), v.end());
28
// remove将非target前移, erase删除尾部(Erase-Remove Idiom)
29
30
// emplace_back vs push_back:
31
v.emplace_back(args...); // 原地构造, 避免拷贝/移动
32
v.push_back(MyClass(args)); // 先构造临时对象再移动

Q155: 完美转发(std::forward)的原理与应用?

🧠 秒懂: 完美转发保持参数的值类别(左值/右值)不变地传递给另一个函数。std::forward(arg)——左值进来转发为左值,右值进来转发为右值。模板+右值引用的核心应用。

完美转发是C++11的核心特性之一,它允许函数模板将参数”原样”转发给另一个函数,保持参数的左值/右值属性不变。配合万能引用(universal reference)T&&使用。

1
#include <iostream>
2
#include <utility>
3
4
void process(int& x) { std::cout << "左值引用: " << x << std::endl; }
5
void process(int&& x) { std::cout << "右值引用: " << x << std::endl; }
6
7
// 不完美转发(错误示范)
8
template<typename T>
9
void wrapper_bad(T&& arg) {
10
process(arg); // arg是具名变量,永远是左值!
11
}
12
13
// 完美转发(正确做法)
14
template<typename T>
15
void wrapper_good(T&& arg) {
25 collapsed lines
16
process(std::forward<T>(arg)); // 保持原始值类别
17
}
18
19
// 工厂函数中的完美转发
20
template<typename T, typename... Args>
21
std::unique_ptr<T> make_unique_custom(Args&&... args) {
22
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
23
}
24
25
struct Widget {
26
std::string name;
27
int value;
28
Widget(const std::string& n, int v) : name(n), value(v) {
29
std::cout << "Widget(" << name << ", " << value << ")" << std::endl;
30
}
31
};
32
33
int main() {
34
int a = 42;
35
wrapper_good(a); // 左值 → process(int&)
36
wrapper_good(100); // 右值 → process(int&&)
37
38
auto w = make_unique_custom<Widget>("test", 10);
39
return 0;
40
}

引用折叠规则:T& &T&T& &&T&T&& &T&T&& &&T&&std::forward<T> 本质上就是一个有条件的 static_cast<T&&>

Q156: C++中的内存模型与原子操作细节?

🧠 秒懂: C++内存模型定义了多线程中操作的可见性和顺序规则。原子操作+内存序(memory order)替代了平台相关的内存屏障指令,是编写可移植并发代码的基础。

多线程环境下使用 std::atomic 时,不同的内存序(memory order)对性能和正确性有重大影响。这是大厂面试C++多线程方向的高频题。

1
#include <iostream>
2
#include <atomic>
3
#include <thread>
4
#include <cassert>
5
6
// 1. 自旋锁实现(使用atomic_flag)
7
class SpinLock {
8
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
9
public:
10
void lock() {
11
while (flag_.test_and_set(std::memory_order_acquire))
12
; // 自旋等待
13
}
14
void unlock() {
15
flag_.clear(std::memory_order_release);
44 collapsed lines
16
}
17
};
18
19
// 2. 无锁单生产者-单消费者队列(SPSC)
20
template<typename T, size_t N>
21
class SPSCQueue {
22
std::array<T, N> buffer_;
23
std::atomic<size_t> head_{0}; // 写索引
24
std::atomic<size_t> tail_{0}; // 读索引
25
public:
26
bool push(const T& val) {
27
size_t h = head_.load(std::memory_order_relaxed);
28
size_t next = (h + 1) % N;
29
if (next == tail_.load(std::memory_order_acquire))
30
return false; // 满
31
buffer_[h] = val;
32
head_.store(next, std::memory_order_release);
33
return true;
34
}
35
bool pop(T& val) {
36
size_t t = tail_.load(std::memory_order_relaxed);
37
if (t == head_.load(std::memory_order_acquire))
38
return false; // 空
39
val = buffer_[t];
40
tail_.store((t + 1) % N, std::memory_order_release);
41
return true;
42
}
43
};
44
45
int main() {
46
SpinLock sl;
47
int counter = 0;
48
auto inc = [&]() {
49
for (int i = 0; i < 100000; i++) {
50
sl.lock();
51
counter++;
52
sl.unlock();
53
}
54
};
55
std::thread t1(inc), t2(inc);
56
t1.join(); t2.join();
57
std::cout << "counter=" << counter << std::endl; // 200000
58
return 0;
59
}

内存序总结relaxed只保证原子性;acquire保证本线程后续读写不被重排到此之前;release保证本线程之前的读写不被重排到此之后;seq_cst(默认)是最强保证但性能最低。

Q157: C++模板在嵌入式外设驱动抽象中的零开销应用?

🧠 秒懂: 用模板参数传入外设基地址和配置,编译器在编译期完成所有计算——生成的机器码与手写寄存器操作完全一样。零开销抽象的典型应用,C++在嵌入式中的核心优势。

C++模板在编译期实例化,生成与手写C代码等效的机器码,实现了”零开销抽象(Zero Overhead Abstraction)“。在嵌入式中,模板可用于:(1)编译期确定的外设配置(如不同GPIO端口、不同UART实例)无需运行时多态开销;(2)类型安全的寄存器操作;(3)编译期计算(如波特率分频器)。相比虚函数多态(需要vtable查表+间接调用),模板多态在编译期完成分派,无运行时开销,特别适合资源受限的MCU。

1
#include <cstdint>
2
3
/* 通过模板参数区分不同UART实例 */
4
template<uint32_t BASE_ADDR>
5
class Uart {
6
static volatile uint32_t& reg(uint32_t offset) {
7
return *reinterpret_cast<volatile uint32_t*>(BASE_ADDR + offset);
8
}
9
static constexpr uint32_t DR = 0x00;
10
static constexpr uint32_t SR = 0x04;
11
static constexpr uint32_t BRR = 0x08;
12
13
public:
14
static void init(uint32_t baud, uint32_t pclk) {
15
/* 编译期可优化为常量 */
35 collapsed lines
16
reg(BRR) = pclk / baud;
17
}
18
19
static void send(uint8_t ch) {
20
while (!(reg(SR) & 0x80)) {}
21
reg(DR) = ch;
22
}
23
24
static bool recv(uint8_t &ch) {
25
if (reg(SR) & 0x20) {
26
ch = static_cast<uint8_t>(reg(DR));
27
return true;
28
}
29
return false;
30
}
31
};
32
33
/* 实例化: 编译器生成两套独立代码,性能等同直接操作寄存器 */
34
using Uart1 = Uart<0x40011000>;
35
using Uart3 = Uart<0x40004800>;
36
37
/* 编译期波特率计算 */
38
template<uint32_t PCLK, uint32_t BAUD>
39
constexpr uint32_t calc_brr() {
40
static_assert(PCLK / BAUD > 0, "Invalid baud rate");
41
return PCLK / BAUD;
42
}
43
44
void example() {
45
Uart1::init(115200, 84000000);
46
Uart1::send('A');
47
48
constexpr auto brr = calc_brr<84000000, 115200>();
49
/* brr = 729, 编译期常量, 零运行时开销 */
50
}

Q158: 【华为】C++11的智能指针循环引用问题?

🧠 秒懂: 两个shared_ptr互相引用导致引用计数永远不为0。解决:将其中一个改为weak_ptr。华为面试高频题,必须手画引用关系图讲清楚。

关键要点: A持有B的shared_ptr, B持有A的shared_ptr → 引用计数永远不为0 → 内存泄漏。解决: 将其中一个改为weak_ptr,打破循环。

1
// 循环引用: 两个shared_ptr互指→引用计数永远不为0→内存泄漏!
2
struct A {
3
std::shared_ptr<B> b_ptr;
4
~A() { std::cout << "A destroyed\n"; }
5
};
6
struct B {
7
std::shared_ptr<A> a_ptr; // ← 问题!
8
~B() { std::cout << "B destroyed\n"; }
9
};
10
11
{
12
auto a = std::make_shared<A>();
13
auto b = std::make_shared<B>();
14
a->b_ptr = b; // a→b引用计数2
15
b->a_ptr = a; // b→a引用计数2
7 collapsed lines
16
} // 出作用域: 计数各减1→变成1→永远不释放!
17
18
// 解决: 一方用weak_ptr
19
struct B {
20
std::weak_ptr<A> a_ptr; // ← weak_ptr不增加引用计数!
21
~B() { std::cout << "B destroyed\n"; }
22
};

Q159: 【大疆】嵌入式C++中为什么要禁用异常?

🧠 秒懂: 异常处理的栈展开(stack unwinding)增加代码体积(异常表)且执行时间不确定。大疆等嵌入式公司都用-fno-exceptions + 错误码方式处理错误。

关键要点: 异常处理需要运行时支持(展开栈帧),增加代码体积和执行开销。嵌入式资源有限,通常用返回错误码+assert替代异常。编译选项: -fno-exceptions

1
// 嵌入式场景禁用异常的原因:
2
// ① 代码膨胀: 异常表+展开信息增加5~20% Flash
3
// ② 时间不确定: throw/catch耗时不确定(不适合实时系统)
4
// ③ 堆依赖: 异常对象可能需要动态分配
5
// ④ 编译器支持: 某些嵌入式编译器不完全支持
6
7
// 禁用方法:
8
// GCC: -fno-exceptions -fno-rtti
9
// Keil: 默认不启用
10
11
// 替代方案:
12
enum class Error { None, Timeout, InvalidParam, HwFault };
13
14
struct Result {
15
Error err;
8 collapsed lines
16
int value;
17
};
18
19
Result sensor_read() {
20
if (!sensor_ready())
21
return {Error::Timeout, 0};
22
return {Error::None, read_data()};
23
}

Q160: 【小米】C++中static_cast/dynamic_cast/reinterpret_cast的区别?

🧠 秒懂: static_cast编译时安全转换,dynamic_cast运行时多态检查(需RTTI),reinterpret_cast底层位级转换。嵌入式中static_cast最常用。

关键要点: static_cast: 编译期类型转换(安全); dynamic_cast: 运行期多态转换(需RTTI); reinterpret_cast: 底层位模式重解释(危险); const_cast: 去/加const。

1
// static_cast: 编译时检查的安全转换
2
int i = 42;
3
float f = static_cast<float>(i); // int→float
4
Base *b = static_cast<Base*>(derv); // 子类→基类(安全)
5
6
// dynamic_cast: 运行时类型检查(需要RTTI+虚函数)
7
Base *b = get_object();
8
Derived *d = dynamic_cast<Derived*>(b);
9
// 如果b实际不是Derived → 返回nullptr
10
// 嵌入式一般不用(需要RTTI,开销大)
11
12
// reinterpret_cast: 位模式重解释(嵌入式最常用!)
13
volatile uint32_t *reg =
14
reinterpret_cast<volatile uint32_t*>(0x40010000);
15
// 整数→指针,指针→整数
5 collapsed lines
16
17
// const_cast: 去掉const属性
18
void legacy_api(char *s);
19
const char *msg = "hello";
20
legacy_api(const_cast<char*>(msg)); // 去const(小心UB!)

Q161: 【紫光展锐】嵌入式中如何替换new/delete?

🧠 秒懂: 重载全局operator new/delete或类特定版本,让new从自定义内存池分配而非堆。嵌入式中用静态内存池替代malloc,实现确定性分配和零碎片。

关键要点: 重载全局operator new/delete,底层使用静态内存池或FreeRTOS的pvPortMalloc。避免堆碎片,适合长时间运行的嵌入式系统。

1
// 嵌入式避免频繁malloc → 自定义内存池
2
3
class PoolAllocatable {
4
private:
5
static uint8_t pool[1024];
6
static size_t offset;
7
public:
8
void* operator new(size_t size) {
9
if (offset + size > sizeof(pool))
10
return nullptr; // 内存不足
11
void *p = &pool[offset];
12
offset += (size + 3) & ~3; // 4字节对齐
13
return p;
14
}
15
void operator delete(void *p) {
11 collapsed lines
16
// 简单内存池: 不回收(整体释放)
17
}
18
static void reset() { offset = 0; }
19
};
20
uint8_t PoolAllocatable::pool[1024];
21
size_t PoolAllocatable::offset = 0;
22
23
// 或用placement new(在指定地址构造):
24
uint8_t buf[sizeof(MyObj)] __attribute__((aligned(4)));
25
MyObj *obj = new (buf) MyObj(args...);
26
obj->~MyObj(); // 手动析构(不释放内存)

八、C++并发与智能指针进阶(Q162~Q164)

💡 大疆/海康/字节IoT的C++嵌入式岗位面试中,智能指针和原子操作是必考题

Q162: C++智能指针有哪些?使用场景和坑分别是什么?

📎 基础对比见 Q66, 本题深入讲解使用陷阱和嵌入式实战

🧠 秒懂: unique_ptr独占零开销(首选)、shared_ptr共享(引用计数开销)、weak_ptr打破循环。坑:shared_ptr循环引用泄漏、数组需要自定义删除器。

1
#include <memory>
2
#include <iostream>
3
4
/* ===== unique_ptr: 独占所有权(最常用, 零开销) ===== */
5
// 一本书只能被一个人借, 不能复制, 只能移动(转让)
6
void demo_unique() {
7
// 创建(推荐make_unique, 异常安全)
8
auto sensor = std::make_unique<Sensor>("IMU");
9
sensor->init();
10
11
// ❌ 不能复制:
12
// auto sensor2 = sensor; // 编译错误!
13
14
// ✅ 可以移动(转让所有权):
15
auto sensor2 = std::move(sensor);
52 collapsed lines
16
// 此时 sensor == nullptr, sensor2 拥有对象
17
18
// 常用场景: 函数返回动态对象(零拷贝)
19
// auto dev = create_device("uart0"); // 工厂函数返回unique_ptr
20
} // 离开作用域自动delete, 无需手动释放
21
22
/* ===== shared_ptr: 共享所有权(引用计数) ===== */
23
// 一本书可以被多人借(副本), 最后一个人还了才真正回收
24
void demo_shared() {
25
std::shared_ptr<Buffer> buf = std::make_shared<Buffer>(1024);
26
// use_count() == 1
27
28
{
29
auto buf2 = buf; // 引用计数+1 → 2
30
auto buf3 = buf; // 引用计数+1 → 3
31
std::cout << buf.use_count(); // 输出3
32
} // buf2, buf3析构 → 引用计数减到1
33
34
// buf是最后一个持有者, buf析构时delete Buffer
35
}
36
37
/* ===== weak_ptr: 弱引用(解决循环引用) ===== */
38
// 观察者模式中, Subject持有Observer的shared_ptr,
39
// Observer也持有Subject的shared_ptr → 循环引用 → 永远不释放!
40
// 解决: 把其中一端改为weak_ptr
41
42
struct Node {
43
std::shared_ptr<Node> next; // 强引用
44
std::weak_ptr<Node> prev; // 弱引用(不增加引用计数)
45
~Node() { std::cout << "Node destroyed
46
"; }
47
};
48
49
void demo_weak() {
50
auto a = std::make_shared<Node>();
51
auto b = std::make_shared<Node>();
52
a->next = b; // a强引用b
53
b->prev = a; // b弱引用a(不增加a的计数)
54
} // a先析构(count→0, delete) → b也析构. 无泄漏!
55
56
/* ===== 面试常见坑 ===== */
57
// 坑1: 不要用raw pointer构造多个shared_ptr
58
int *raw = new int(42);
59
// std::shared_ptr<int> p1(raw);
60
// std::shared_ptr<int> p2(raw); // 双重释放! 崩溃!
61
62
// 坑2: 不要在容器中存unique_ptr后用拷贝
63
// std::vector<std::unique_ptr<Obj>> vec;
64
// vec.push_back(std::move(ptr)); // 必须move
65
66
// 坑3: shared_ptr有性能开销(引用计数原子操作)
67
// 嵌入式中优先用unique_ptr(零开销), 只在确需共享时用shared
类型拷贝移动开销场景
unique_ptr零(和裸指针一样)独占资源(驱动句柄/文件)
shared_ptr引用计数(原子操作)多处共享(缓冲区/连接)
weak_ptr同shared_ptr打破循环引用/观察者

Q163: 什么是无锁编程?CAS原子操作怎么用?

🧠 秒懂: 无锁编程用原子操作(CAS: Compare-And-Swap)替代互斥锁,避免锁的开销和死锁风险。CAS:atomic.compare_exchange_strong(expected, desired)——预期值匹配才更新。

1
#include <atomic>
2
#include <thread>
3
4
/* ===== 基本原子操作 ===== */
5
// atomic保证读-改-写是一个不可分割的操作(硬件级保证)
6
std::atomic<int> counter{0}; // 原子计数器
7
8
void increment_safe() {
9
// fetch_add是原子操作, 多线程同时调用也不会丢失计数
10
counter.fetch_add(1, std::memory_order_relaxed);
11
// 等价于 counter++; 但前者是原子的, 后者不是
12
}
13
14
/* ===== CAS(Compare-And-Swap)核心原理 ===== */
15
// CAS语义: "如果当前值 == 期望值, 则更新为新值, 返回true; 否则不修改, 返回false"
48 collapsed lines
16
// 硬件在ARM上对应 LDREX/STREX 指令
17
18
std::atomic<int> shared_data{0};
19
20
void cas_update(int new_val) {
21
int expected = shared_data.load(); // 读取当前值
22
// 尝试更新: 如果没人改过(仍然是expected), 就写入new_val
23
while (!shared_data.compare_exchange_weak(expected, new_val)) {
24
// 失败说明其他线程已修改, expected被自动更新为最新值
25
// 循环重试(自旋)
26
}
27
}
28
29
/* ===== 实战: 无锁环形缓冲区(单生产者-单消费者) ===== */
30
// 嵌入式中ISR写数据 → 主循环读数据 的典型场景
31
template<typename T, size_t N>
32
class LockFreeRingBuf {
33
T buffer_[N];
34
std::atomic<size_t> head_{0}; // 写指针(只有生产者修改)
35
std::atomic<size_t> tail_{0}; // 读指针(只有消费者修改)
36
37
public:
38
bool push(const T& item) {
39
size_t h = head_.load(std::memory_order_relaxed);
40
size_t next_h = (h + 1) % N;
41
// 满了?(写指针追上读指针)
42
if (next_h == tail_.load(std::memory_order_acquire)) {
43
return false;
44
}
45
buffer_[h] = item;
46
head_.store(next_h, std::memory_order_release); // 发布新数据
47
return true;
48
}
49
50
bool pop(T& item) {
51
size_t t = tail_.load(std::memory_order_relaxed);
52
// 空了?(读指针追上写指针)
53
if (t == head_.load(std::memory_order_acquire)) {
54
return false;
55
}
56
item = buffer_[t];
57
tail_.store((t + 1) % N, std::memory_order_release);
58
return true;
59
}
60
};
61
62
// 使用: ISR中push, 主循环中pop, 无需任何锁
63
LockFreeRingBuf<uint8_t, 256> uart_rx_buf;

面试关键:说出CAS的”比较-交换”语义 + 知道ARM上是LDREX/STREX实现 + 能写一个无锁SPSC队列 = 满分。

Q164: C++11的RAII思想在嵌入式中有哪些典型应用?

🧠 秒懂: RAII典型应用:①lock_guard自动加解锁 ②智能指针自动内存管理 ③GPIO初始化/反初始化 ④中断开关保护。核心思想:构造获取资源,析构释放资源,绝不泄漏。

1
/* ===== 典型RAII应用1: 互斥锁保护(替代手动lock/unlock) ===== */
2
#include <mutex>
3
4
std::mutex mtx;
5
6
// ❌ 传统写法: 容易忘记unlock, 或在异常路径漏掉
7
void bad_function() {
8
mtx.lock();
9
// ... 如果这里抛异常或提前return, mtx永远不会unlock!
10
if (error) return; // BUG: 未解锁!
11
mtx.unlock();
12
}
13
14
// ✅ RAII写法: lock_guard构造时lock, 析构时自动unlock
15
void good_function() {
56 collapsed lines
16
std::lock_guard<std::mutex> guard(mtx); // 构造 → lock
17
// ... 无论如何退出(return/throw/正常结束), guard析构 → unlock
18
if (error) return; // 安全! guard析构会自动unlock
19
}
20
21
/* ===== 典型RAII应用2: GPIO资源管理 ===== */
22
// 嵌入式场景: 进入临界区时关中断, 离开时恢复
23
class InterruptGuard {
24
uint32_t saved_primask_;
25
public:
26
InterruptGuard() {
27
saved_primask_ = __get_PRIMASK();
28
__disable_irq(); // 构造时关中断
29
}
30
~InterruptGuard() {
31
__set_PRIMASK(saved_primask_); // 析构时恢复中断状态
32
}
33
// 禁止拷贝
34
InterruptGuard(const InterruptGuard&) = delete;
35
InterruptGuard& operator=(const InterruptGuard&) = delete;
36
};
37
38
// 使用: 临界区自动保护, 不可能忘记开中断
39
void access_shared_resource() {
40
InterruptGuard guard; // 关中断
41
shared_counter++; // 安全访问共享资源
42
} // guard析构 → 自动恢复中断
43
44
/* ===== 典型RAII应用3: 文件/外设句柄管理 ===== */
45
class SpiDevice {
46
SPI_HandleTypeDef *hspi_;
47
GPIO_TypeDef *cs_port_;
48
uint16_t cs_pin_;
49
public:
50
// 构造时拉低CS(选中设备)
51
SpiDevice(SPI_HandleTypeDef *h, GPIO_TypeDef *port, uint16_t pin)
52
: hspi_(h), cs_port_(port), cs_pin_(pin) {
53
HAL_GPIO_WritePin(cs_port_, cs_pin_, GPIO_PIN_RESET); // CS=0
54
}
55
// 析构时拉高CS(释放总线)
56
~SpiDevice() {
57
HAL_GPIO_WritePin(cs_port_, cs_pin_, GPIO_PIN_SET); // CS=1
58
}
59
60
void transfer(uint8_t *tx, uint8_t *rx, uint16_t len) {
61
HAL_SPI_TransmitReceive(hspi_, tx, rx, len, 100);
62
}
63
};
64
65
// 使用: CS信号自动管理
66
void read_flash_id() {
67
SpiDevice flash(&hspi1, GPIOA, GPIO_PIN_4); // 自动CS=0
68
uint8_t cmd = 0x9F;
69
uint8_t id[3];
70
flash.transfer(&cmd, id, 3);
71
} // 自动CS=1, 即使中间异常也不会忘记释放CS

面试总结:RAII的三个经典嵌入式应用 = ①lock_guard(自动解锁) ②中断保护(自动恢复) ③外设CS管理(自动释放)。核心思想是”把资源生命周期绑定到对象生命周期”。