C++的拓展语法(goto、lambda表达式、reserve()、iota()、move()、lower_bound()、upper_bound())
goto的作用
记住:
happy:
...
goto happy:
反着也行。
通常用于过多循环判错解的情况:
for ...
for ...
for ...
if (!check()) goto wrong;
...
wrong:
puts("NO");
只能在同一个函数使用,即使你用lambda表达式也不行。不能跳过变量的初始化。
lambda表达式
定义
形如:[capture](parameters) -> return_type { body }
- [capture]:捕获列表,用于捕获当前作用域中的变量。
- (parameters):参数列表,与普通函数相同。
- -> return_type:返回类型(可省略,由编译器推导)。
- { body }:函数体。
用法
最简单的用法:
#include <iostream>
int main() {
auto greet = []() {
std::cout << "Hello from lambda!" << std::endl;
};
greet(); // 调用
return 0;
}
带参数:
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << add(3, 4) << std::endl; // 输出: 7
如果返回类型可以推导可以省略 ->int,但确保可以被编译器自动推导。
按值捕获:
int x = 10;
auto func = [x]() { // 按值捕获
std::cout << "x = " << x << std::endl;
};
func(); // 输出: x = 10
| 写法 | 含义 |
|---|---|
| [] | 不捕获任何变量 |
| [x] | 按值捕获 x |
| [&x] | 按引用捕获 x |
| [x, &y] | 混合捕获 |
| [=] | 按值捕获所有局部变量 |
| [&] | 按引用捕获所有局部变量 |
⚠️ 注意:[=] 和 [&] 是“全量捕获”,适用于简单情况,但可能带来性能或生命周期问题。
如果用捕获(不引用)不想使外部被修改需要加上 mutable:
int x = 10;
auto f = [x]() mutable {
x += 5; // 修改的是副本,不影响外部
return x;
};
std::cout << f() << std::endl; // 15
std::cout << x << std::endl; // 10(外部不变)
注意,如果要 -> 请记住:
[捕获](参数) mutable -> 返回类型 { 函数体 }
修改被捕获变量(需使用引用)
int count = 0;
auto inc = [&count]() { // 按引用捕获
++count;
};
inc(); inc();
std::cout << count << std::endl; // 输出: 2
作为STL算法的参数(一般也用于sort中的cmp)
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
// 找出大于3的元素
auto it = std::find_if(nums.begin(), nums.end(), [](int n) {
return n > 3;
});
if (it != nums.end())
std::cout << *it << std::endl; // 输出: 4
return 0;
}
1. mutable 关键字——修改按值捕获的副本(前面讲过)
按值捕获的变量在 lambda 内部是只读的(类似 const)。如果想修改其副本而不影响外部,需要加上 mutable:
int x = 10;
auto f = [x]() mutable {
x += 5; // 修改的是副本
return x;
};
std::cout << f() << std::endl; // 15
std::cout << x << std::endl; // 10(外部不变)
2. 捕获初始化(C++14 起)
可以在捕获列表中定义并初始化新变量,常用于移动语义:
std::unique_ptr<int> p = std::make_unique<int>(42);
auto f = [p = std::move(p)]() { // 将 p 移动进 lambda
return *p;
};
// 原 p 已被移走,不能再使用
3. 泛型 Lambda(C++14 起)
参数可以用 auto,自动生成模板化的函数对象:
auto add = [](auto a, auto b) { return a + b; };
std::cout << add(3, 4) << std::endl; // int
std::cout << add(3.14, 2.72) << std::endl; // double
4. 生命周期与悬挂引用(重要警告)
按引用捕获局部变量后,如果 lambda 的生命周期超过了变量的生命周期(比如被存储为回调),将产生悬挂引用,导致未定义行为:
std::function<int()> getFunc() {
int x = 42;
return [&x]() { return x; }; // ❌ 危险:x 被销毁,lambda 引用失效
}
解决方法:
- 按值捕获
[=]或[x]。 - 若必须按引用,确保 lambda 在变量销毁前使用(如在同一作用域内立即调用)。
5. std::function vs auto 的性能考量
auto存储具体类型,无类型擦除开销,编译器可以内联,性能最好。std::function可以存储任意可调用对象,但会引入运行时开销(类型擦除、虚函数调用)。
建议:绝大多数情况用 auto,除非你需要将 lambda 存入容器或作为多态接口。
6. 混合默认捕获与显式捕获
你可以组合使用默认捕获和显式捕获,注意顺序:
[=, &y] // 默认按值捕获所有,但 y 按引用
[&, x] // 默认按引用捕获所有,但 x 按值
7. constexpr Lambda(C++17 起)
如果 lambda 满足编译期求值要求,可以用于 constexpr 上下文:
constexpr auto square = [](int n) { return n * n; };
static_assert(square(5) == 25);
8. 捕获 this 的注意事项
在类成员函数中使用 lambda 时,如果要用到成员变量,需要捕获 this:
class Foo {
int val = 10;
public:
void bar() {
auto f = [this]() { return val; }; // 显式捕获 this
// 或者 [=] 在 C++20 前也会隐式捕获 this(有风险),建议显式写
}
};
最佳实践:避免使用 [=] 隐式捕获 this(C++20 已修正),明确写出 [this] 或 [&]。
总结小贴士
goto:在简单多层跳出时可用,但更推荐封装函数;注意不能跳越初始化。- Lambda:掌握基础捕获后,多练习
mutable、泛型、捕获初始化等现代特性,注意生命周期问题,优先用auto而非std::function以提升性能。
reserve(),iota(),move()
1. reserve() —— 容器的“内存预约”(后面要使用 push_back() 等)
- 作用:为容器(如
vector、string)预先分配足够的底层内存,只改变capacity(容量),不改变size(大小)。 - 好处:减少动态扩容带来的内存重新分配和元素拷贝/移动开销,提升性能。
#include <vector>
std::vector<int> v;
v.reserve(100); // 预分配 100 个 int 的内存
std::cout << v.size() << ", " << v.capacity(); // 输出: 0, 100
for (int i = 0; i < 100; ++i) {
v.push_back(i); // 期间不会再触发扩容,效率很高
}
| 对比 | reserve(n) |
resize(n) |
|---|---|---|
改变 size |
❌ 不改 | ✅ 改成 n |
改变 capacity |
✅ 至少为 n | ✅ 可能扩容 |
| 默认初始化元素 | ❌ 不创建元素 | ✅ 会创建或删除元素 |
⚠️ 注意:reserve() 只适用于支持容量概念的容器(vector、string、deque 不完全保证),对 list、map 等节点式容器无效(它们不需要预留连续内存)。
2. iota() —— 连续序列填充器
- 头文件:
<numeric> - 作用:用从起始值开始的连续递增序列填充一个区间(左闭右开)。
- 本质:
*first = value; ++value;循环执行。
#include <numeric>
#include <vector>
#include <iostream>
std::vector<int> arr(5);
std::iota(arr.begin(), arr.end(), 0); // arr 变成 {0, 1, 2, 3, 4}
// 也可以不从 0 开始
std::iota(arr.begin(), arr.end(), 10); // arr 变成 {10, 11, 12, 13, 14}
常用场景:
- 快速生成测试数据。
- 配合
reserve()和下标赋值,避免push_back的重复检查。 - 用于离散化、并查集初始化等算法(如
parent[i] = i)。
对比手动 for 循环:
// 传统写法
for (int i = 0; i < n; ++i) arr[i] = i;
// iota 更简洁、语义清晰,且内部高度优化
std::iota(arr.begin(), arr.end(), 0);
3. move() —— 小心!它有两个“分身”
(1) std::move(类型转换,头文件 <utility>)
- 作用:将左值强制转换为右值引用,暗示“这个对象可以转移资源”,从而触发移动构造函数或移动赋值运算符。
- 目的:实现资源所有权的转移,避免深拷贝。
#include <utility>
#include <vector>
#include <string>
std::string str1 = "hello";
std::string str2 = std::move(str1); // str1 的资源被“偷”给了 str2
// 此时 str1 处于“有效但未指定”的状态(通常为空),再次访问需小心
std::cout << str2; // 输出 "hello"
⚠️ 致命误区:std::move 本身不移动任何东西,它只是一个类型转换!真正的移动发生在构造函数或赋值运算符中。
(2) std::move(范围搬运算法,头文件 <algorithm>)
- 作用:将一段范围内的元素移动到另一段范围(类似于
std::copy,但调用的是移动赋值)。 - 返回值:指向目标区间末尾的迭代器。
#include <algorithm>
#include <vector>
std::vector<int> src = {1, 2, 3};
std::vector<int> dst(3);
std::move(src.begin(), src.end(), dst.begin()); // dst 变成 {1, 2, 3}
// 注意:src 中的元素被移走后,仍处于有效但未指定状态(通常是原始值,但不保证)
注意区分:
std::move(转换)通常用于单个对象。std::move(算法)用于容器之间的批量搬运。
移动后的对象(重要警告!)
被移动后的对象(无论是单个还是容器元素)都处于 “有效但未指定” 的状态:
- 你可以对它调用不依赖当前值的操作,比如
clear()、size()、析构。 - 不要直接读取它的值(除非你确定它被重置为了什么,比如标准库保证
std::string移后为空,但自定义类不一定)。
安全示范:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// v1 的大小可能变为 0,但不保证
v1.clear(); // ✅ 安全,clear() 不依赖内容
// std::cout << v1[0]; // ❌ 危险!不要这样做
三者的巧妙组合(实战小彩蛋)
这三个工具经常可以配合使用,写出高效的初始化代码:
#include <vector>
#include <numeric>
#include <utility>
std::vector<int> makeData(int n) {
std::vector<int> v;
v.reserve(n); // 1. 预留空间,避免扩容
// 这里假设你要生成 0~n-1,可以:
// 方法一:直接 resize + iota
// v.resize(n);
// std::iota(v.begin(), v.end(), 0);
// 方法二:如果不想先初始化两次,可以用移动语义从临时数组转移(略复杂)
return v; // 返回时自动触发移动(RVO/移动语义),效率极高
}
总结速查表
| 函数 | 头文件 | 核心用途 | 易错点 |
|---|---|---|---|
reserve() |
<vector>等 |
预分配内存,优化性能 | 别和 resize() 混淆,它不创建元素 |
iota() |
<numeric> |
填充递增序列 | 记得包含 <numeric>,别写成 itoa(那是 C 库函数) |
std::move(转换) |
<utility> |
启用移动语义,避免拷贝 | 移动后对象不可直接使用原值 |
std::move(算法) |
<algorithm> |
批量移动范围元素 | 目标区间大小要足够,移动后源区间状态不确定 |
两个问题
第一个问题:为什么 cout << v1[0] 不对?
先回顾我之前的示例代码:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// std::cout << v1[0]; // ❌ 为什么危险?
根本原因:被移动后的对象处于“有效但未指定”的状态。
具体到 std::vector,大多数标准库(如 GCC 的 libstdc++ 和 MSVC)在实现移动构造函数时,内部是这样做的:
- 把
v2的内部指针指向v1的堆内存。 - 把
v1的内部指针置为nullptr,并把v1的size和capacity置为0。
所以当你执行 v1[0] 时,相当于解引用一个空指针(nullptr[0]),这属于未定义行为(UB),程序大概率会直接崩溃(段错误)。
但是要注意区分:
- 对于
std::vector,移完后访问元素肯定错,因为实现会把原容器掏空。 - 对于
std::string,标准库通常保证移完后字符串为空(""),此时str[0]是安全的(返回'\0')。 - 对于自定义类,移完后到底剩什么完全取决于你怎么写移动构造函数。
安全原则:永远不要假设被移动后对象的值,你可以安全地调用 size()、clear() 或析构它,但绝对不要直接读取其内部元素,除非你查阅了该类型的标准库保证文档。
第二个问题:std::move 算法是什么?
你注意到了,std::move 有两个完全不同的身份:
1. 类型转换(头文件 <utility>)
就是刚才用的 std::move(v1),它只是一个“演员”,把左值变成右值引用,触发移动构造/赋值。
2. 范围搬运算法(头文件 <algorithm>)
这是你问的“算法”,它的声明是:
template<class InputIt, class OutputIt>
OutputIt move(InputIt first, InputIt last, OutputIt d_first);
- 作用:将
[first, last)区间内的元素,一个一个地移动到从d_first开始的目标区间。 - 内部原理:循环中执行
*d_first = std::move(*first)(注意这里调用的是std::move类型转换,从而触发元素的移动赋值运算符)。
代码示例:
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<std::string> src = {"apple", "banana", "cherry"};
std::vector<std::string> dst(3); // 必须提前分配好空间!
// 算法版 move:把 src 里的字符串“偷”到 dst 中
std::move(src.begin(), src.end(), dst.begin());
// 输出 dst:apple, banana, cherry
for (const auto& s : dst) std::cout << s << ", ";
// src 中的字符串现在处于“未指定”状态(通常是空字符串 "")
for (const auto& s : src) std::cout << "[" << s << "] "; // 可能输出 [][][]
}
和 std::copy 算法的区别:
std::copy:保留src原封不动(深拷贝)。std::move(算法):把src的元素资源偷走,src失去原值(浅拷贝指针 + 置空对方)。
⚠️ 算法版 std::move 的两大致命陷阱
陷阱 1:目标空间必须足够大(但是搞到函数参数中不用,直接fun(move(ls)))
和 std::copy 一样,std::move 算法不会自动扩容。你必须确保 dst 的 size 大于等于 src 的元素个数。
std::vector<int> src = {1,2,3};
std::vector<int> dst;
std::move(src.begin(), src.end(), dst.begin()); // ❌ 崩溃!dst 是空的
正确做法:要么 dst.resize(3),要么用 std::back_inserter(但 back_inserter 调用的是 push_back,同样会触发移动,可行):
std::move(src.begin(), src.end(), std::back_inserter(dst)); // ✅ 自动扩容
陷阱 2:源和目标不能重叠(范围重叠)
如果源区间和目标区间在同一个容器中且重叠,std::move 算法结果是未定义的(可能用后面覆盖前面,乱掉)。如果你想把元素移动到重叠的右侧位置,请使用 std::move_backward(和 copy_backward 同理)。
std::vector<int> v = {1, 2, 3, 4, 5};
// 想把前 3 个移动到后 3 个位置(覆盖 3,4,5)
// std::move(v.begin(), v.begin()+3, v.begin()+2); // ❌ 危险!重叠了
std::move_backward(v.begin(), v.begin()+3, v.end()); // ✅ 从后往前移动,安全
// v 可能变成 {1, 2, 1, 2, 3}
总结两个 move 的口诀
| 写法 | 头文件 | 功能 | 用途 |
|---|---|---|---|
std::move(obj) |
<utility> |
类型转换(变左值为右值) | 单个对象转移所有权 |
std::move(begin, end, out) |
<algorithm> |
批量搬运元素(调用移动赋值) | 容器间或容器内的批量转移 |
使用(后缀数组)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stdlib.h>
#include <vector>
#define int long long
#define uint unsigned long long
#define N 300005
using namespace std;
char s[N];
uint seed = 231;
uint h[N],Pow[N];
int rk[N],sa[N],height[N];
signed main(){
scanf("%s",s + 1);
int n = strlen(s + 1);
Pow[0] = 1;
s[n + 1] = '$';
for (int i = 1;i <= n;i ++) Pow[i] = Pow[i - 1] * seed;
for (int i = 1;i <= n;i ++) h[i] = h[i - 1] * seed + s[i];
for (int i = 1;i <= n;i ++) sa[i] = i;
auto gethash = [&](int l,int r) {
return h[r] - h[l - 1] * Pow[r - l + 1];
};
stable_sort(sa + 1,sa + 1 + n,[n,gethash](const int &a,const int &b) {
int lena = n - a + 1,lenb = n - b + 1;
int l = 1,r = min(lena,lenb),res = r + 1;
while(l <= r) {
int mid = l + r >> 1;
if (gethash(a,a + mid - 1) != gethash(b,b + mid - 1)) res = mid,r = mid - 1;
else l = mid + 1;
}
if (res == min(lena,lenb) + 1) return lena < lenb;
return s[a + res - 1] < s[b + res - 1];
});
auto print = [](vector<int> a) -> void {
for (auto i : a) printf("%lld ",i);
putchar('\n');
};
vector<int> ans;
ans.reserve(n);
for (int i = 1;i <= n;i ++) ans.push_back(sa[i] - 1);
print(move(ans));
for (int i = 1;i <= n;i ++) rk[sa[i]] = i;
for (int i = 1,k = 0;i <= n;i ++) {
if (k) k --;
while(s[i + k] == s[sa[rk[i] - 1] + k]) k ++;
height[rk[i]] = k;
}
ans.reserve(n);
for (int i = 1;i <= n;i ++) ans.push_back(height[i]);
print(move(ans));
return 0;
}
注意到,stable_sort()什么时候比sort()更快呢?当且仅当比较操作比较昂贵的时候,其他情况时空均会更大。
lower_bound()、upper_bound()
在 C++ 标准库中,lower_bound 和 upper_bound 的比较函数必须基于严格弱序(通常用 <),不能直接使用 <= 或 >,否则会破坏二分查找的正确性。
1. 标准库的约定
| 函数 | 比较器调用方式 | 语义 |
|---|---|---|
lower_bound(first, last, value, comp) |
comp(*it, value) |
查找第一个 不小于 value 的元素(即 *it >= value) |
upper_bound(first, last, value, comp) |
comp(value, *it) |
查找第一个 大于 value 的元素(即 *it > value) |
默认的比较器是 std::less<>,即使用 <。
2. 为什么是 < 而不是 <= 或 >?
- 二分查找依赖“小于”关系的严格弱序(传递性、非对称性)。
- 如果用
<=,则当两个元素相等时,comp(a,b)和comp(b,a)都可能为true,破坏了严格弱序,导致二分查找错误。 - 因此,比较器内部通常只使用
<(或自定义等价关系),并通过组合来实现<=、>=、>的效果。
3. 如何实现不同的查找目标?
假设元素类型为 T,值类型为 V:
| 想要查找的目标 | 使用函数 | 比较器写法 |
|---|---|---|
第一个 >= value 的位置 |
lower_bound |
[](const T& elem, const V& val) { return elem < val; }(默认) |
第一个 > value 的位置 |
upper_bound |
[](const V& val, const T& elem) { return val < elem; }(默认) |
第一个 <= value 的位置(即最后一个 <= value 的后继) |
upper_bound 并自定义 |
[](const V& val, const T& elem) { return val < elem; } 仍然是 >,但将 value 改为 value+1 或利用 upper_bound 配合 greater 等,但更简单的做法是使用 lower_bound 配合反向迭代器。 |
第一个 < value 的位置(即第一个 >= value 的位置减1) |
lower_bound,然后 --it |
用默认 lower_bound 找到 >= value,其前一个就是 < value 的最后位置。 |
注意:upper_bound 默认查找 > value,但通过修改比较器可以让它查找其他条件,但必须保证严格弱序。最常见的是保持默认 value < *it,因为它已经实现了严格 >。
4. 您代码中的用法(分块)
-
lower_bound(qid.begin(), qid.end(), val, [&](int id, int val){ return q[id].a < val; })
这里比较器是comp(element, value),即q[id].a < val,正是lower_bound的标准写法,查找第一个a >= val的询问编号。 -
upper_bound(edges+L, edges+R+1, val, [](int val, const Edge& e){ return val < e.a; })
这里比较器是comp(value, element),即val < e.a,正是upper_bound的标准写法,查找第一个a > val的边。
如果误写成 e.a < val,则相当于 lower_bound 的逻辑,会得到第一个 a >= val,而不是 a > val,导致块内多包含一条 a == val 的边,从而漏判或误判。
5. 总结
- 始终使用
<(或自定义的严格弱序)作为比较核心。 lower_bound用comp(element, value)实现element < value。upper_bound用comp(value, element)实现value < element。- 不要尝试直接用
<=或>=,可以通过反转比较或调整查找值来达到目的。

浙公网安备 33010602011771号