C++ 面试要点笔记(续)

C++ 面试要点笔记(续)

右值引用 · 移动语义 · STL · 并发 · 模板 · 编译链接 · Coroutine


目录

  1. 右值引用
  2. 函数参数可不可以传右值
  3. [手写栈(不用 STL)](#手写栈(不用 STL))
  4. [STL 容器底层实现](#STL 容器底层实现)
  5. 完美转发
  6. [unique_lock 与 lock_guard](#unique_lock 与 lock_guard)
  7. [C 代码引用 C++ 为何报错](#C 代码引用 C++ 为何报错)
  8. 静态多态
  9. [map 为何用红黑树不用 AVL](#map 为何用红黑树不用 AVL)
  10. [inline 失效场景](#inline 失效场景)
  11. [struct 与 class 区别](#struct 与 class 区别)
  12. [防止头文件被多次 include](#防止头文件被多次 include)
  13. [lambda 表达式与捕获](#lambda 表达式与捕获)
  14. [友元 friend](#友元 friend)
  15. std::move
  16. 类模板的作用
  17. 模板和泛型的区别
  18. [new 与 malloc 区别](#new 与 malloc 区别)
  19. [new 可以重载吗](#new 可以重载吗)
  20. [map 与 unordered_map](#map 与 unordered_map)
  21. 它们是线程安全的吗
  22. 优先队列实现
  23. [gcc 编译过程](#gcc 编译过程)
  24. [C++ Coroutine](#C++ Coroutine)
  25. [extern "C" 作用](#extern “C” 作用)
  26. [memory_order / ELF / 中断](#memory_order / ELF / 中断)
  27. [C++ 符号表](#C++ 符号表)
  28. [C++ 单元测试](#C++ 单元测试)

27. 右值引用

27.1 含义

右值引用:必须绑定到右值的引用(《C++ Primer》)。

cpp 复制代码
int i = 42;
int& r = i;          // OK:左值引用绑定左值
// int&& rr = i;     // 错误:右值引用不能绑左值
int&& rri = i * 42;  // OK:表达式结果是右值

重要性质:

  • 通常绑定到即将销毁的对象(临时量、将亡值等)。
  • 右值引用变量本身是左值(有名字、可取地址),因此不能拿它再直接初始化另一个右值引用:
cpp 复制代码
// int&& rx = rri;  // 错误:rri 是具名变量 → 左值
int&& rx = std::move(rri);  // OK:显式转成右值

判断口诀:

  • 能取地址 → 左值
  • 不能取地址 → 右值(粗略但实用)

27.2 左值引用 vs 右值引用

左值引用 T& 右值引用 T&&
初始化 一般只能绑左值 只能绑右值
特例 const T& 可绑右值(延长临时量寿命) ---
使用方式 当作别名使用 使用时同样是别名;差别主要在绑定规则 与重载决议

27.3 编译器如何实现绑定

右值引用绑定到临时右值时,编译器通常会:

  1. 把临时值落到栈上某处(延长其寿命)
  2. 让右值引用指向该存储位置

因此:"右值将亡"与"具名右值引用是持久左值"并不矛盾------持久的是引用变量本身,它指向的是被延长寿命的临时对象。

27.4 用法

不建议:

cpp 复制代码
int&& test() {
    int x = 1;
    return x + 1;  // 危险:返回局部相关临时量的引用,易成悬垂
}

主要用途:

  1. 移动语义 (T&& + 移动构造/移动赋值)
  2. 完美转发 (万能引用 T&& + std::forward)

28. 函数参数可不可以传右值

28.1 什么时候值得提供 T&& 重载

同时满足:

  1. 函数内部会取得一份对象(拷贝或移动)
  2. 这份对象要活过当前函数栈帧(存进容器、成员等)

典型:vector::push_back 有 const T& 与 T&& 两套重载:

cpp 复制代码
vec.push_back(MyClass());  // 右值 → 走移动版本,避免拷贝

28.2 只满足"要拷贝一份"时

cpp 复制代码
void func1(const T& t) { T local = t; /* use local */ }
void func2(T t)        { /* use t */ }  // 左值拷贝,右值可移动进形参
写法 左值实参 右值实参
const T& 1 次拷贝 1 次拷贝
值传递 T 1 次拷贝 1 次移动(进形参)

是否该全面改成值传递?不一定。

常见写法:

cpp 复制代码
MyClass::MyClass(std::string str) : str_member(std::move(str)) {}
实参 代价
左值 1 次拷贝 + 1 次移动
右值 2 次移动

相对 const std::string&(总是 1 次拷贝):

  • 若 move 很便宜、copy 很贵 ,值传递 + std::move 往往更好
  • 对 std::string,短串可能走 SSO ;盲目值传递可能破坏更优路径(见 Effective Modern C++)
  • 小对象、平凡类型:差别可忽略,const T& 仍然很好

若不需要拷贝参数: 继续用 const T&(或需要修改时用 T&)。


29. 参考 C/C++ 堆栈实现自己的堆栈(不能用 STL)

栈:只允许在一端插入/删除的线性表。常用接口:入栈、出栈、取顶、判空/满、长度、清空、打印、销毁。

下面是修正后的数组实现 (原笔记中 delete this、析构未 delete[]、ClearStack 逻辑有问题):

cpp 复制代码
#include <iostream>
#include <stdexcept>

class Stack {
public:
    explicit Stack(int cap = 5) : capacity(cap), top(0) {
        data = new int[capacity];
    }

    ~Stack() { delete[] data; }

    // 禁止浅拷贝(简化版)
    Stack(const Stack&) = delete;
    Stack& operator=(const Stack&) = delete;

    void Push(int e) {
        if (IsFull()) {
            std::cout << "栈已满," << e << " 未入栈\n";
            return;
        }
        data[top++] = e;
    }

    void Pop() {
        if (IsEmpty()) {
            std::cout << "栈为空\n";
            return;
        }
        --top;
    }

    int GetTop() const {
        if (IsEmpty()) throw std::runtime_error("empty stack");
        return data[top - 1];
    }

    int Size() const { return top; }
    void Clear() { top = 0; }
    bool IsEmpty() const { return top == 0; }
    bool IsFull() const { return top >= capacity; }

    void Print() const {
        if (IsEmpty()) {
            std::cout << "栈为空\n";
            return;
        }
        std::cout << "栈内元素(顶→底): ";
        for (int i = top - 1; i >= 0; --i) {
            std::cout << data[i] << ' ';
        }
        std::cout << '\n';
    }

private:
    int* data;
    int capacity;
    int top;  // 下一个可写位置
};

面试可补充:固定容量数组版 vs 动态扩容版;链式栈(链表)实现。


30. STL 容器了解吗?底层如何实现

容器 底层结构 要点
vector 动态数组 连续内存;扩容搬迁;随机访问 O(1)
deque 分块连续缓冲区 两端插入 O(1);也支持下标
list 双向链表 任意位置插入删除 O(1)(已知迭代器);不可随机访问
map / set 红黑树 有序;操作 O(log n)
multimap / multiset 红黑树 允许重复键
unordered_map / unordered_set 哈希表 平均 O(1);无序;冲突用链表/树
queue 默认 deque FIFO 适配器
stack 默认 deque LIFO 适配器
priority_queue 堆(默认 vector) 默认大根堆

31. 完美转发介绍一下;去掉 std::forward 会怎样?

31.1 是什么

在函数模板中,把参数原样(类型、const/volatile、左/右值)转发给另一个函数。

实现两件套:

  1. 万能引用(转发引用) :T&&(T 为模板参数时)
  2. std::forward<T>(arg) :按 T 恢复值类别后转发
cpp 复制代码
template <typename T>
void foo(T&& arg) {
    bar(std::forward<T>(arg));
}

31.2 去掉 std::forward 会怎样?

cpp 复制代码
template <typename T>
void foo(T&& arg) {
    bar(arg);  // arg 是具名变量 → 永远当左值
}

后果:

  • 传入右值时,仍按左值 传给 bar
  • 移动构造/右值重载选不中 ,退化成拷贝(或无法绑定到 T&& 形参)
  • 完美转发失效

31.3 示例

cpp 复制代码
#include <iostream>
#include <string>
#include <utility>

void print(const std::string&) { std::cout << "Lvalue\n"; }
void print(std::string&&)      { std::cout << "Rvalue\n"; }

template <typename T>
void foo(T&& arg) {
    print(std::forward<T>(arg));
}

int main() {
    std::string s = "hi";
    foo(s);             // Lvalue
    foo(std::move(s));  // Rvalue
}

32. 介绍一下 unique_lock 和 lock_guard 区别?

二者都是对 mutex 的 RAII 封装,不负责 mutex 本身的寿命。

lock_guard unique_lock
创建即加锁 是 可选(defer_lock 等)
中途解锁 否 可以 lock() / unlock()
可移动 否 是
可复制 否 否
条件变量 一般不用 condition_variable 需要
开销 更轻 稍重,功能更全

选用建议: 够用就用 lock_guard;需要延迟加锁、临时解锁、条件变量、转移所有权时再用 unique_lock。

cpp 复制代码
std::lock_guard<std::mutex> g(m);  // 简单临界区

std::unique_lock<std::mutex> lk1(m1, std::defer_lock);
std::unique_lock<std::mutex> lk2(m2, std::defer_lock);
std::lock(lk1, lk2);  // 避免死锁的多锁获取

33. C 代码中引用 C++ 代码有时候会报错为什么?

常见原因:

  1. 名字改编(name mangling):C++ 函数名被编译器改写,C 侧按 C 链接找不到符号
  2. C++ 特性无法被 C 直接使用:类、重载、引用、模板、异常等
  3. 关键字/类型差异 :new/bool 等
  4. ABI / 编译选项不一致

解决:

cpp 复制代码
// C++ 头文件对外暴露 C 接口
#ifdef __cplusplus
extern "C" {
#endif

void foo(int x);  // 按 C 链接

#ifdef __cplusplus
}
#endif
  • C 调 C++:在 C++ 侧包一层 extern "C" 接口
  • C++ 调 C:包含 C 头时同样建议用 extern "C" 包裹

34. 静态多态有什么?

静态多态(编译期多态):调用在编译期决议。

常见形式:

  1. 函数重载
  2. 运算符重载
  3. 模板(含 CRTP)
cpp 复制代码
int add(int a, int b);
double add(double a, double b);

class Vector {
public:
    Vector operator+(const Vector& rhs) const;
};
静态多态 动态多态(虚函数)
决议时机 编译期 运行期
开销 通常更低 虚表间接调用
灵活性 类型需编译期可知 可运行时换实现

35. map 为啥用红黑树不用 AVL 树?

(面试也常对比 map vs unordered_map)

红黑树 AVL
平衡严格度 较弱(最长路径 ≤ 2×最短) 严格(平衡因子 ≤ 1)
查找 O(log n) O(log n),常数可能略优
插入/删除 旋转更少,更稳 旋转更频繁,写多场景更贵

std::map 选红黑树:综合插入、删除、查找更均衡。

map unordered_map
结构 红黑树 哈希表
复杂度 O(log n) 平均 O(1),最坏退化
有序 有 无
场景 要有序/范围查询 纯查找、可接受无序

36. inline 失效场景

inline 只是建议,最终由编译器决定。常见不内联或效果差的情况:

  1. 函数体过大 → 代码膨胀
  2. 含复杂循环 / 递归
  3. 调用点极多,导致 I-cache 压力
  4. 虚函数(动态绑定,通常不内联;个别编译器在"可知具体类型"时可能去虚化)
  5. 函数地址被取用,需生成 out-of-line 副本
  6. 跨翻译单元且无 LTO 时难以内联

37. C++ 中 struct 和 class 区别

37.1 C++ 里二者

几乎等价,主要差别是默认权限:

class struct
默认成员访问 private public
默认继承 private public

习惯:struct 偏 POD/聚合数据;class 偏有不变式的类型。

模板参数写法习惯用 class/typename,没有 template<struct T>。

37.2 对比 C 的 struct

C C++
只能有数据成员 可有函数、继承、多态
无访问控制 有 public/private/protected
使用常需写 struct Tag 标签名可直接当类型名

38. 如何防止一个头文件 include 多次

间接包含会导致重复包含:

text 复制代码
c.cpp → h1.h
c.cpp → h2.h → h1.h   // h1 被包含两次

方式一:include guard

cpp 复制代码
#ifndef MY_H1_H
#define MY_H1_H
// ... 内容 ...
#endif

方式二:#pragma once

cpp 复制代码
#pragma once
// ... 内容 ...

多数编译器支持;可移植项目可两者并用。宏名要唯一,避免冲突。


39. lambda 表达式的理解,它可以捕获哪些类型

39.1 形式

text 复制代码
[捕获](参数) mutable noexcept -> 返回类型 { 函数体 }

编译器通常生成一个匿名函数对象 (闭包类型),并实现 operator()。

39.2 捕获列表

写法 含义
[=] 按值捕获用到的自动变量
[&] 按引用捕获
[x] 只按值捕获 x
[&x] 只按引用捕获 x
[=, &x] 默认按值,x 按引用
[&, x] 默认按引用,x 按值
[this] 捕获当前对象指针
[*this] C++17:按值捕获对象副本
[x = std::move(y)] 初始化捕获(可实现 move 捕获)

注意:

  • [=, x] / [&, &x] 重复指定 → 非法
  • 全局/静态变量不靠捕获列表,可直接用
  • 按值捕获默认 const,要改副本需加 mutable
  • 引用捕获的 lambda 不要返回后继续使用(悬垂)

39.3 原理(示意)

cpp 复制代码
// [&] 近似
struct LambdaRef {
    Point& p1;
    Point& p2;
    void operator()(int n) const { /* 改的是外部对象 */ }
};

// [=] 近似
struct LambdaValue {
    Point p1;  // 拷贝
    Point p2;
    void operator()(int n) /*mutable*/ { /* 改的是副本 */ }
};

40. 友元 friend 介绍

友元:允许非成员函数 或另一个类 访问本类的 private/protected 成员。

提高访问便利,但削弱封装,应谨慎使用。

cpp 复制代码
class Point {
public:
    Point(double xx, double yy) : x(xx), y(yy) {}
    friend double Distance(Point& a, Point& b);  // 友元函数
    friend class Helper;                         // 友元类
private:
    double x, y;
};

double Distance(Point& a, Point& b) {  // 非成员,不加 Point::
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return std::sqrt(dx * dx + dy * dy);
}

规则:

  1. 友元不能继承
  2. 友元单向,不对称
  3. 友元不传递
  4. 友元函数不是成员函数,类外定义不加 Class::

41. move 函数

41.1 值类别(实用记法)

  • 左值:有身份、通常可取地址;函数形参名也是左值
  • 右值 :临时量、将亡值等;如 std::string()、a + b 的结果

41.2 移动语义

把资源"挪"给另一方,源对象置于可析构的空状态,避免深拷贝。

41.3 std::move 本身不移动

std::move = 无条件转换为右值引用;真正移动发生在移动构造/移动赋值里。

等效实现:

cpp 复制代码
template <class T>
constexpr std::remove_reference_t<T>&& move(T&& arg) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(arg);
}
cpp 复制代码
std::vector<Vector> vec;
Vector a(4, 5, 6);
vec.push_back(a);             // 左值 → 拷贝
vec.push_back(std::move(a));  // 当成右值 → 可走移动(若类型支持)

vector 有 push_back(const T&) 与 push_back(T&&);move 后选中后者。


42. 模版类的作用

类模板:用类型参数描述一族类,减少重复。

cpp 复制代码
template <class NameT, class PriceT = int>
class Car {
public:
    Car(NameT name, PriceT price) : C_name(name), C_price(price) {}
    void printCar();
    NameT C_name;
    PriceT C_price;
};

// 类模板一般不能靠实参自动推导(C++17 起部分 CTAD)
Car<std::string, int> p1("宝马", 100);
Car<std::string> p2("奔驰", 200);  // 使用默认 PriceT = int

要点:

  1. 与函数模板比:类模板通常需显式实参 ;可有默认模板实参
  2. 成员函数往往调用时才实例化
  3. 对象作函数参数的三种方式:
    • 指定类型:void f(Car<string,int>&)
    • 参数模板化:template<class A,class B> void f(Car<A,B>&)
    • 整个类型模板化:template<class T> void f(T&)

43. 模版和泛型的区别

这里常对比的是 C++ 模板 vs .NET/Java 泛型(不是"泛型编程"这个范式本身)。

C++ 模板 CLR/Java 风格泛型
实例化时机 编译期 运行期(或 JIT)保留更多泛型信息
代码生成 每个实参集可生成独立代码 引用类型常共享一份实现
非类型参数 支持(如 template<int N>) 通常不支持
特化/偏特化 支持 通常不支持或受限
默认类型实参 支持 视语言而定

C++ 里说的"泛型编程"多指:用模板写出与具体类型无关的算法/容器(如 STL)。


44. 内存管理:C++ 的 new 和 malloc 的区别

new / delete malloc / free
属性 运算符 C 库函数
大小 由类型推断 需手动传字节数
返回值 类型指针 void*,需转换
失败 默认抛 std::bad_alloc 返回 NULL
构造/析构 会调用 不会
可重载 可以 否(可换分配器实现)

流程简述:

  • new:operator new 分配 → 构造 → 返回指针
  • delete:析构 → operator delete 释放

45. new 可以重载吗,可以改写 new 函数吗

可以。通过重载 operator new / operator delete(及数组版本)做自定义分配:

  • 内存池 / 分区分配
  • 泄漏检测、统计
  • 对齐、调试哨兵等

注意:

  • 与标准用法、异常规范保持兼容
  • 配对使用 new/delete
  • 不要破坏对象生命周期语义

还有 placement new:在已有缓冲区上构造对象。


46. C++ 中的 map 和 unordered_map 的区别和使用场景

map unordered_map
头文件 <map> <unordered_map>
底层 红黑树 哈希表
复杂度 O(log n) 平均 O(1)
有序 按 key 有序 无序
空间 节点指针多 桶数组 + 节点
场景 有序遍历、范围查询、key 可比较 高频查找、key 可哈希
cpp 复制代码
std::unordered_map<int, std::string> um{{5, "张大"}, {6, "李五"}};
um[2] = "李四";
um.insert({3, "陈二"});

auto it = um.find(2);
if (it != um.end()) {
    std::cout << it->first << "," << it->second << '\n';
}

47. 他们是线程安全的吗

通常问的是 STL 容器:

不是。 标准容器一般不以线程安全方式提供写共享。

规则(简化):

  • 多线程只读同一容器:通常安全
  • 只要有线程写:需外部同步
  • 不同容器可由不同线程各自使用

保证线程安全的常见手段:

  1. 互斥锁 / lock_guard / unique_lock
  2. 原子类型 / 无锁结构(难,慎用)
  3. 把共享状态改为线程局部,减少共享

48. C++ 标准库里优先队列是怎么实现的?

std::priority_queue:容器适配器 + 堆。

  • 默认底层:vector
  • 默认比较:less<T> → 大根堆 (top() 最大)
  • 算法:make_heap / push_heap / pop_heap

手写堆要点:

cpp 复制代码
void sift_down(std::vector<int>& a, int i, int n) {
    for (;;) {
        int largest = i;
        int l = 2 * i + 1, r = 2 * i + 2;
        if (l < n && a[l] > a[largest]) largest = l;
        if (r < n && a[r] > a[largest]) largest = r;
        if (largest == i) break;
        std::swap(a[i], a[largest]);
        i = largest;
    }
}

void build_heap(std::vector<int>& a) {
    for (int i = (int)a.size() / 2 - 1; i >= 0; --i)
        sift_down(a, i, (int)a.size());
}

库用法:

cpp 复制代码
// 大根堆
std::priority_queue<int> maxq;
// 小根堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minq;

std::priority_queue<std::pair<int,int>,
                    std::vector<std::pair<int,int>>,
                    std::greater<>> pq;

自定义比较注意:priority_queue 的比较语义与 sort 习惯容易反着记,以 top() 实际结果为准。

常用操作:push / pop / top / empty / size。


49. gcc 编译的过程

四步:

text 复制代码
预处理 → 编译 → 汇编 → 链接
 .c/.cpp   .i     .s     .o     可执行文件
步骤 作用 常用命令
预处理 宏、头文件、条件编译 gcc -E file.c -o file.i
编译 语义分析,生成汇编 gcc -S file.c -o file.s
汇编 汇编 → 目标文件 gcc -c file.s -o file.o
链接 目标文件 + 库 → 可执行 gcc file.o other.o -o app

多文件示例:

bash 复制代码
gcc -E -I./inc test.c -o test.i
gcc -S -I./inc test.c -o test.s
gcc -c test.s -o test.o
gcc -c ./inc/mymath.c -o ./inc/mymath.o
gcc -o test.out test.o ./inc/mymath.o

常用选项:-c、-o、-g、-O/-O2、-I、-L、-l。


50. C++ Coroutine

含 co_await / co_yield / co_return 之一的函数即协程。

关键字 作用
co_await 等待可等待体,可能挂起
co_yield 产出一个值并挂起
co_return 结束并返回(void 或值)

编译器大致改造为:创建 promise → get_return_object → initial_suspend → 用户体 → final_suspend。

Awaitable 需提供:

  1. await_ready()
  2. await_suspend(coroutine_handle<>)
  3. await_resume()

也可通过 operator co_await 返回代理 awaitable。


51. extern "C" 有什么作用

让 C++ 编译器按 C 语言链接约定 编译声明的函数/变量,关闭名字改编,便于与 C 互操作。

cpp 复制代码
extern "C" void c_api(int);

extern "C" {
    void foo();
    int bar;
}

补充:单独的 extern 表示"声明,定义在别处":

cpp 复制代码
extern int a;      // 声明
int a;             // 定义
int a = 0;         // 定义
extern int a = 0;  // 仍是定义(有初始化)

52. C++ memory_order / ELF 文件格式 / 中断对操作系统的作用

memory_order

C++11 原子操作的内存序,约束多线程可见性与重排:

  • relaxed / acquire / release / acq_rel / seq_cst 等
  • 用于无锁/低锁同步,用错会导致可见性 bug

ELF

Linux 下可执行文件与目标文件的常见格式,含:

  • 代码/数据段
  • 符号表、重定位
  • 动态链接信息(.so 依赖等)

中断

硬件异步通知 CPU 有事件(时钟、键盘、网卡、缺页等)。OS 借中断:

  • 驱动响应设备
  • 时间片调度
  • 系统调用入口(软中断/陷阱)等

53. C++ 的符号表

编译/链接阶段记录标识符信息的表,常见字段:

  • 名称、类型(变量/函数等)
  • 大小、地址/偏移
  • 绑定(强/弱)、可见性
  • 所在节(section)

用途:语义检查、代码生成、链接时解析符号、动态链接查找。


54. C++ 的单元测试

组成

  1. 模块化:按文件/命名空间/夹具组织
  2. 夹具(Fixture):用例前后公共准备/清理
  3. 测试方法:具体用例
  4. 断言:期望值、异常、调用次数等
  5. Mock / Fake / Stub:隔离依赖

准则

准确性、单一职责、可自动化、覆盖充分、可重复、代码可测(低耦合、可注入)。

常用框架

  • GoogleTest / GoogleMock
  • Catch2
  • CppUnit
  • 打桩:如 cpp-stub

附录:本册易混点

  1. 具名的 T&& 变量是左值 ;要继续当右值需 std::move / std::forward。
  2. std::move 不移动;移动构造才移动。
  3. 去掉 forward → 右值变左值,完美转发失败。
  4. 优先 lock_guard,需要灵活性再用 unique_lock。
  5. map 红黑树有序;unordered_map 哈希更快但不保证序。
  6. STL 容器默认非线程安全。
  7. extern "C" 解决 C/C++ 链接名问题。
相关推荐
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会C/C++考级:并查集连通块与朋友圈问题解析
c++·python·算法·bfs·信息学竞赛
wyhwust2 小时前
codeblocks程序调试
c++
用户84913717547162 小时前
【技术面试高频】MySQL 存储 IP 地址:你还在用 VARCHAR?
mysql·面试
Rain的Java大神之路2 小时前
Java 三层架构,90% 的人只答对一半:Controller 不是瘦,Service 不是透传,Dao 不是只写 SQL
java·后端·面试
我命由我123452 小时前
准入类与水平类职业资格
笔记·学习·职场和发展·产品运营·求职招聘·职场发展·产品经理
He BianGu2 小时前
【笔记】在WPF中BitmapScalingMode的作用
笔记·wpf
傲世仙尊2 小时前
运算符重载-operator函数与五个不能重载的符号
c++
小李不想当小白3 小时前
USART串口数据包(STM32标准库学习笔记)
经验分享·笔记·stm32·单片机·嵌入式硬件·学习
怕浪猫3 小时前
前端转全栈最低成本的技术栈选择
前端·javascript·面试