C++ 面试要点笔记(续)
右值引用 · 移动语义 · STL · 并发 · 模板 · 编译链接 · Coroutine
目录
- 右值引用
- 函数参数可不可以传右值
- [手写栈(不用 STL)](#手写栈(不用 STL))
- [STL 容器底层实现](#STL 容器底层实现)
- 完美转发
- [unique_lock 与 lock_guard](#unique_lock 与 lock_guard)
- [C 代码引用 C++ 为何报错](#C 代码引用 C++ 为何报错)
- 静态多态
- [map 为何用红黑树不用 AVL](#map 为何用红黑树不用 AVL)
- [inline 失效场景](#inline 失效场景)
- [struct 与 class 区别](#struct 与 class 区别)
- [防止头文件被多次 include](#防止头文件被多次 include)
- [lambda 表达式与捕获](#lambda 表达式与捕获)
- [友元 friend](#友元 friend)
- std::move
- 类模板的作用
- 模板和泛型的区别
- [new 与 malloc 区别](#new 与 malloc 区别)
- [new 可以重载吗](#new 可以重载吗)
- [map 与 unordered_map](#map 与 unordered_map)
- 它们是线程安全的吗
- 优先队列实现
- [gcc 编译过程](#gcc 编译过程)
- [C++ Coroutine](#C++ Coroutine)
- [extern "C" 作用](#extern “C” 作用)
- [memory_order / ELF / 中断](#memory_order / ELF / 中断)
- [C++ 符号表](#C++ 符号表)
- [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 编译器如何实现绑定
右值引用绑定到临时右值时,编译器通常会:
- 把临时值落到栈上某处(延长其寿命)
- 让右值引用指向该存储位置
因此:"右值将亡"与"具名右值引用是持久左值"并不矛盾------持久的是引用变量本身,它指向的是被延长寿命的临时对象。
27.4 用法
不建议:
cpp
int&& test() {
int x = 1;
return x + 1; // 危险:返回局部相关临时量的引用,易成悬垂
}
主要用途:
- 移动语义 (
T&&+ 移动构造/移动赋值) - 完美转发 (万能引用
T&&+std::forward)
28. 函数参数可不可以传右值
28.1 什么时候值得提供 T&& 重载
同时满足:
- 函数内部会取得一份对象(拷贝或移动)
- 这份对象要活过当前函数栈帧(存进容器、成员等)
典型: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、左/右值)转发给另一个函数。
实现两件套:
- 万能引用(转发引用) :
T&&(T为模板参数时) 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++ 代码有时候会报错为什么?
常见原因:
- 名字改编(name mangling):C++ 函数名被编译器改写,C 侧按 C 链接找不到符号
- C++ 特性无法被 C 直接使用:类、重载、引用、模板、异常等
- 关键字/类型差异 :
new/bool等 - 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. 静态多态有什么?
静态多态(编译期多态):调用在编译期决议。
常见形式:
- 函数重载
- 运算符重载
- 模板(含 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 只是建议,最终由编译器决定。常见不内联或效果差的情况:
- 函数体过大 → 代码膨胀
- 含复杂循环 / 递归
- 调用点极多,导致 I-cache 压力
- 虚函数(动态绑定,通常不内联;个别编译器在"可知具体类型"时可能去虚化)
- 函数地址被取用,需生成 out-of-line 副本
- 跨翻译单元且无 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);
}
规则:
- 友元不能继承
- 友元单向,不对称
- 友元不传递
- 友元函数不是成员函数,类外定义不加
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
要点:
- 与函数模板比:类模板通常需显式实参 ;可有默认模板实参
- 成员函数往往调用时才实例化
- 对象作函数参数的三种方式:
- 指定类型:
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 容器:
不是。 标准容器一般不以线程安全方式提供写共享。
规则(简化):
- 多线程只读同一容器:通常安全
- 只要有线程写:需外部同步
- 不同容器可由不同线程各自使用
保证线程安全的常见手段:
- 互斥锁 /
lock_guard/unique_lock - 原子类型 / 无锁结构(难,慎用)
- 把共享状态改为线程局部,减少共享
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 需提供:
await_ready()await_suspend(coroutine_handle<>)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++ 的单元测试
组成
- 模块化:按文件/命名空间/夹具组织
- 夹具(Fixture):用例前后公共准备/清理
- 测试方法:具体用例
- 断言:期望值、异常、调用次数等
- Mock / Fake / Stub:隔离依赖
准则
准确性、单一职责、可自动化、覆盖充分、可重复、代码可测(低耦合、可注入)。
常用框架
- GoogleTest / GoogleMock
- Catch2
- CppUnit
- 打桩:如 cpp-stub
附录:本册易混点
- 具名的
T&&变量是左值 ;要继续当右值需std::move/std::forward。 std::move不移动;移动构造才移动。- 去掉
forward→ 右值变左值,完美转发失败。 - 优先
lock_guard,需要灵活性再用unique_lock。 map红黑树有序;unordered_map哈希更快但不保证序。- STL 容器默认非线程安全。
extern "C"解决 C/C++ 链接名问题。