收集一下奇奇怪怪的玩意(包括冷门用法)。。。。

虚函数的默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class Base {
public:
virtual void show(int x = 10) = 0;

virtual ~Base() { cout << "~Base\n"; }
};


class Derive : public Base {
public:
void show(int x = 20) override {
cout << "x = " << x << endl;
}

~Derive() override { cout << "~Derive\n"; }
};

int main(int argc, char* argv[]) {
Base* bb = new Derive();
bb->show();
delete(bb);

return 0;
}

cout 的缓冲区

std::cout 默认是有缓冲的。如果你在写高性能代码,不理解缓冲就是在浪费 CPU 周期。

缓冲模式

  • 全缓冲 (Fully Buffered):缓冲区满了才输出。通常在 cout 被重定向到文件时发生。
  • 行缓冲 (Line Buffered):遇到换行符 \n 就输出。这是 cout 连接到终端时的默认行为。
  • 无缓冲 (Unbuffered):数据直接输出。std::cerr 默认就是这种模式,因为错误信息需要立即显示。

endl vs \n

这是新手最容易犯错的地方:

  • std::endl:输出 \n 并强制调用 flush()。在循环里用 endl 会导致频繁的系统调用,性能很烂。
  • \n:只输出换行符。让缓冲区自己决定什么时候该刷新。

如果你不需要立即看到输出(比如在跑一个几百万次的循环),永远用 \n。

性能优化

如果你觉得 C++ 的 I/O 慢,通常是因为它在同步老旧的 C stdio。

1
2
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

这两行代码能让你的 I/O 速度提升一个量级,但你需要明白它们在干什么:

  • sync_with_stdio(false):默认情况下,C++ 的 cout 要和 C 的 printf 同步,共享同一个缓冲区。这就像是两个人共用一个笔记本,每次写字都要互相打招呼,慢得要死。关掉它,cout 就能拥有自己的独立缓冲区,跑得飞快。代价:你不能再在同一个程序里混用 cout 和 printf,否则输出顺序会乱套。
  • cin.tie(nullptr):默认情况下 cin 和 cout 是绑定的。这意味着每次你从 cin 读数据前,cout 都会被强制刷新(flush)。这是为了保证你在输入前能看到提示语(比如 “请输入:”)。但在处理大量数据时,这种频繁的自动刷新就是性能杀手。解绑后,cout 只有在缓冲区满或者你手动刷新时才会输出。

std::cout vs C++23 std::print

C++23 引入了 <print>,这才是现代 C++ 该有的样子。

  • 类型安全与格式化:cout 靠一堆 << 拼接,代码又臭又长。std::print 使用类似 std::format 的语法,类型安全且可读性极高。
  • 性能:std::print 通常比 std::cout 快。它直接将格式化后的字符串交给底层系统调用,避免了 ostream 那套复杂的内部状态维护和多次缓冲区拷贝。
  • 原子性:在多线程环境下,多个线程同时用 cout 输出会导致字符交织(除非你加锁)。std::print 的单次调用通常是原子的,输出不会被打碎。
  • Unicode 支持:std::print 原生支持 UTF-8 输出到终端,而 cout 在 Windows 等平台上处理 Unicode 就是个噩梦。

结论:如果你的编译器支持 C++23,赶紧把那堆垃圾 cout 换成 std::print。

operator"" 用户自定义字面量

C++11 引入了用户自定义字面量(User-defined literals),通过 operator"" 让你定义自己的字面量后缀。这玩意儿能让代码读起来像自然语言,但用不好就是灾难。

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

// 整型字面量后缀
constexpr long long operator"" _KB(unsigned long long value) {
return value * 1024;
}

// 浮点型字面量后缀
constexpr long double operator"" _deg(long double degrees) {
return degrees * 3.14159265358979323846L / 180.0;
}

// 字符串字面量后缀
string operator"" _str(const char* str, size_t len) {
return string(str, len);
}

int main() {
auto size = 4_KB; // 4096
auto angle = 90.0_deg; // 1.5708...
auto text = "hello"_str; // std::string

cout << "Size: " << size << endl;
cout << "Angle: " << angle << endl;
cout << "Text: " << text << endl;

return 0;
}

参数类型规则

operator"" 的参数类型由字面量类型决定,这是编译器强制的:

字面量类型参数类型示例
整型unsigned long long123_suffix
浮点型long double3.14_suffix
字符串(const char*, size_t)"abc"_suffix
字符char'a'_suffix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

// 字符字面量后缀
char operator"" _upper(char c) {
return (c >= 'a' && c <= 'z') ? c - 32 : c;
}

int main() {
cout << 'a'_upper << endl; // 'A'
cout << 'Z'_upper << endl; // 'Z'

return 0;
}

实际应用场景

类型安全的单位系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

struct Distance {
long double meters;
constexpr Distance(long double m) : meters(m) {}
};

constexpr Distance operator"" _m(long double value) {
return Distance(value);
}

constexpr Distance operator"" _km(long double value) {
return Distance(value * 1000);
}

constexpr Distance operator"" _cm(long double value) {
return Distance(value / 100);
}

int main() {
auto d1 = 1.5_m;
auto d2 = 2.0_km;
auto d3 = 150.0_cm;

cout << "d1: " << d1.meters << " m" << endl;
cout << "d2: " << d2.meters << " m" << endl;
cout << "d3: " << d3.meters << " m" << endl;

return 0;
}

编译期字符串哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

// FNV-1a 哈希算法
constexpr unsigned long long operator"" _hash(const char* str, size_t len) {
unsigned long long hash = 14695981039346656037ULL;
for (size_t i = 0; i < len; ++i) {
hash ^= static_cast<unsigned long long>(str[i]);
hash *= 1099511628211ULL;
}
return hash;
}

int main() {
constexpr auto h1 = "hello"_hash;
constexpr auto h2 = "world"_hash;

cout << "hash(\"hello\"): " << h1 << endl;
cout << "hash(\"world\"): " << h2 << endl;

// 编译期计算,零运行时开销
// static_assert("test"_hash == 1358246366ULL, "Hash mismatch");

return 0;
}

时间字面量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;

constexpr seconds operator"" _s(unsigned long long sec) {
return seconds(sec);
}

constexpr milliseconds operator"" _ms(unsigned long long ms) {
return milliseconds(ms);
}

constexpr microseconds operator"" _us(unsigned long long us) {
return microseconds(us);
}

int main() {
auto timeout = 5_s;
auto delay = 100_ms;
auto interval = 500_us;

cout << "Timeout: " << timeout.count() << " seconds" << endl;
cout << "Delay: " << delay.count() << " milliseconds" << endl;
cout << "Interval: " << interval.count() << " microseconds" << endl;

return 0;
}

命名空间限制

用户自定义字面量必须在命名空间中定义,不能在全局作用域(除非是标准库的)。这是为了避免污染全局命名空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

namespace my_literals {
constexpr int operator"" _bin(const char* str, size_t len) {
int value = 0;
for (size_t i = 0; i < len; ++i) {
value = value * 2 + (str[i] - '0');
}
return value;
}
}

int main() {
using namespace my_literals;
cout << "1010"_bin << endl; // 10

return 0;
}

注意事项

  1. 后缀必须以下划线开头:以下划线开头的后缀保留给用户使用,不以 _ 开头的保留给标准库。
  2. 编译期计算:如果标记为 constexpr,字面量可以在编译期计算,零运行时开销。
  3. 类型安全:相比宏,用户自定义字面量是类型安全的,编译器会进行类型检查。
  4. 不要滥用:这玩意儿用得好是神器,用不好就是灾难。只在你真的需要的时候用,别为了炫技而用。

函数返回值隐式转换

C++ 允许函数返回时进行隐式类型转换,规则与变量初始化相同。这个特性用得好是便利,用不好是坑。

核心规则

返回值会按以下优先级尝试转换:

  1. 完全匹配:无需转换
  2. 标准转换:数值提升/转换、派生类指针转基类指针等
  3. 用户定义转换:通过构造函数或转换运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

// 1. 标准转换
int getInt() {
return 3.14; // double → int(窄化,截断小数)
}

// 2. 用户定义转换
struct Widget {
Widget(int x) { cout << "从 int 构造\n"; }
};

Widget getWidget() {
return 42; // int → Widget,隐式调用构造函数
}

int main() {
cout << getInt() << endl; // 3
getWidget(); // "从 int 构造"
return 0;
}

列表初始化限制

如果用 {} 初始化返回值,窄化转换会被禁止:

1
2
3
4
5
6
7
int bad() {
return {3.14}; // 编译错误:从 double 到 int 的窄化转换
}

int good() {
return {42}; // OK
}

auto 返回值

C++14 的 auto 返回值依赖实际返回表达式推导,不会发生隐式转换:

1
2
3
4
5
6
7
8
auto get() {
return 3.14; // 推导为 double
}

// 对比:显式声明返回类型
int getInt() {
return 3.14; // 转换为 int
}

用户定义转换运算符

除了构造函数,转换运算符也能让返回值隐式转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct BigInt {
long value;

// 转换运算符:BigInt → int
operator double() const {
cout << "转换到 double\n";
return static_cast<double>(value);
}
};

double getBig() {
return BigInt{10000};
}


陷阱:有符号/无符号混用

1
2
3
4
5
6
7
8
size_t getSize() {
return -1; // int → size_t,变成巨大正数
}

// 更糟糕的情况
int getIndex(const string& s) {
return s.find('x'); // size_t → int,大值溢出
}

建议

场景推荐做法
数值转换显式 static_cast,或确保不窄化
自定义类型构造函数加 explicit,需要时显式构造
容器索引返回 size_t,或用 auto 接收
现代 C++优先用 auto 返回值,避免意外转换
1
2
3
4
5
6
7
// 好的实践
auto find(const string& s) {
return s.find('x'); // 返回 size_t,安全
}

explicit Widget(int x); // 禁止隐式转换
return Widget{42}; // 意图明确

批量初始化与重置序列

std::iota、std::fill 与 std::generate 会覆写既有区间,不会创建元素或扩容容器。std::fill_n 与 std::generate_n 接受输出迭代器,也可借助插入迭代器追加元素。assign 是容器成员函数,语义是替换整个序列,元素数量可以随之改变。

目标API结果
写入递增序列std::iota从初值开始依次递增
全区间写同一值std::fill覆写 [first, last)
前 n 个元素写同一值std::fill_n从起点连续覆写 n 次
按规则生成值std::generate / std::generate_n每个位置调用生成器
替换容器全部内容container::assign重设元素和元素数量

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <algorithm>
#include <array>
#include <numeric>
#include <vector>

int main() {
std::vector<int> values(6); // 算法前先准备可写区间

std::iota(values.begin(), values.end(), 10);
std::fill(values.begin(), values.end(), -1);
std::fill_n(values.begin(), 3, 0);

auto next = [value = 10]() mutable { return value++; };
std::generate(values.begin(), values.end(), next);

const std::array<int, 3> source{2, 4, 8};
values.assign(4, 7);
values.assign(source.begin(), source.end());
values.assign({2, 4, 8});
}

iota 位于 <numeric>;其余三个算法位于 <algorithm>。iota 每次将当前值写入元素,再递增该值;因此它适合生成编号、下标或连续测试数据。fill 和 fill_n 通过输出迭代器写入值;目标是普通容器迭代器时,这会替换既有元素。

区间算法的边界

fill、iota 与 generate 都处理半开区间 [first, last)。fill_n 和 generate_n 只知道起点与数量:起点是普通容器迭代器时,调用者必须保证后续至少有 n 个可写元素,否则会越界。

若要向可增长容器追加元素,传入插入迭代器:

1
2
std::vector<int> values;
std::fill_n(std::back_inserter(values), 3, 0);

这种形式会追加三个元素;不需要事先 resize。

空容器是常见误区:

1
2
3
4
5
std::vector<int> values;
std::iota(values.begin(), values.end(), 0); // 不写入任何元素

values.resize(10);
std::iota(values.begin(), values.end(), 0); // 0 到 9

需要按值构造新容器时,直接使用构造函数通常更清晰:

1
2
3
std::vector<int> zeros(10, 0);
std::vector<int> ids(10);
std::iota(ids.begin(), ids.end(), 1);

assign、resize 与 fill 的区别

对提供 assign 的序列容器(如 std::vector),assign 覆写整个内容;resize 只调整长度;fill 只覆写已经存在的元素。

调用大小原有元素
values.assign(n, x)变为 n全部替换为 x
values.resize(n, x)变为 n保留前缀;新增部分为 x
std::fill(begin, end, x)不变指定区间替换为 x

对 std::vector,assign 后不要继续使用原来指向元素的引用或迭代器;以该 vector 自身迭代器作为 assign 的输入范围也是未定义行为。若只想重置现有缓冲区,优先 fill;若要同时重设内容和长度,使用 assign。

与 std::views::iota 区分

std::views::iota(0, 10) 是 C++20 的惰性视图,不会写入容器;它适合直接遍历连续值。std::iota 才是向既有区间实际写入值的算法。需要保存数据时,先准备容器,再调用 std::iota。

现代 C++ 的随机数

C++11 起,<random> 把随机数生成拆成两层:引擎(engine)产出均匀分布的随机位序列,分布(distribution)把这些位映射到目标分布。rand() 质量差——RAND_MAX 最低只有 32767,rand() % n 存在模偏差,且共享全局状态——新代码不应再用。

引擎与分布各司其职

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <random>

int main() {
std::random_device rd; // 非确定性熵源,只用来播种
std::mt19937 gen{rd()};
std::uniform_int_distribution<int> dice{1, 6};

for (int i = 0; i < 5; ++i)
std::cout << dice(gen) << ' ';
std::cout << '\n';
}

调用形式固定为 分布(引擎)。引擎有状态、可复用,把它当作长生命周期对象;std::shuffle、std::sample 等算法也直接接受引擎参数。

常用引擎与分布

引擎说明
mt19937 / mt19937_64Mersenne Twister,周期 219937−12^{19937}-1,默认选择
minstd_rand线性同余,状态小、速度快,质量一般
random_device非确定性熵源,慢,只用于播种

default_random_engine 是实现相关的别名,跨平台不保证同一序列。

分布值域
uniform_int_distribution整数,闭区间 [a,b][a, b]
uniform_real_distribution浮点,半开区间 [a,b)[a, b)
normal_distribution正态分布 μ,σ2\mu, \sigma^2
bernoulli_distribution布尔,概率 pp

整数分布是闭区间、浮点分布是半开区间,混用时容易记反。

播种

mt19937{rd()} 只注入 32 位熵,而引擎状态空间大得多,不同进程可能撞出相邻序列。用 seed_seq 混合并扩展熵源:

1
2
3
4
std::random_device rd;
std::seed_seq seq{rd(), rd(), rd(), rd(),
rd(), rd(), rd(), rd()};
std::mt19937_64 gen{seq};

需要可复现结果时用固定种子;此时注意下一个坑。

推荐的完整写法

seed_seq 以 random_device 为主,再掺入时间与进程号做兜底熵,取数固定写成 分布(引擎):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <ctime>
#include <random>
#include <unistd.h> // getpid,POSIX

int main() {
std::random_device rd;
std::seed_seq seq{rd(), rd(), rd(),
static_cast<unsigned>(time(nullptr)),
static_cast<unsigned>(getpid())};
std::mt19937_64 gen{seq};

std::uniform_int_distribution<int> dist{0, 100};
int value = dist(gen); // 固定形式:dist(gen)
}

需要一个可调用对象时,用 lambda 显式声明捕获的是引用:

1
2
auto next = [&gen, &dist] { return dist(gen); };
int x = next();

这份写法刻意回避了老代码里的三件套,原因如下:

  1. 不探测 entropy():它只是提示性接口,标准允许非确定性设备返回 0,也允许确定性实现返回非 0,据此分支没有意义。主流平台上直接信任 random_device 即可。
  2. 掺入时间与进程号,但交给 seed_seq 混合:老写法的问题不在用了 time 和 getpid,而在用 ^ 手工异或秒级时间种子,同一秒启动的进程只能靠进程号区分。这里把 rd()、time(nullptr)、getpid() 一并喂给 seed_seq,由它负责混合扩展;时间与进程号只是兜底熵源,主熵源仍是 random_device。
  3. 不用 std::bind:bind 按值拷贝引擎与分布,generator() 推进的是副本,外部 engine 永远停在原地;再 bind 一个会拿到完全相同的序列。lambda 把拷贝还是引用写在捕获列表里,一目了然。

引擎是长生命周期对象:构造一次,反复调用 dist(gen),不要每次取数都重建。

常见坑

  1. 每次取数都重建引擎:mt19937 构造要初始化约 2.5 KiB 状态,频繁重建既慢又让序列不断从头开始。
  2. 分布不跨实现:引擎的输出序列由标准精确定义,分布的算法却是实现定义。同一引擎加种子,libstdc++ 与 libc++ 的 uniform_int_distribution 结果可以不同。需要跨平台可复现时,自己实现分布、直接消费引擎输出。
  3. 并发:引擎不保证线程安全,多线程同时调用同一引擎是数据竞争。每线程一个 thread_local 引擎,或显式加锁。
  4. rand() % n:除非 nn 整除 RAND_MAX+1\text{RAND\_MAX}+1,低位出现概率不等,这就是模偏差。

C++26 ranges::generate_random

std::ranges::generate_random 一次调用填充整个区间,定义在 <random>,是 niebloid 而非普通函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <random>
#include <vector>

int main() {
std::mt19937 gen{std::random_device{}()};

std::vector<std::uint32_t> bits(16);
std::ranges::generate_random(bits, gen);

std::uniform_real_distribution<double> unit{0.0, 1.0};
std::vector<double> samples(16);
std::ranges::generate_random(samples, gen, unit);
}
  • 两种形式各有带分布与不带分布的重载,也接受迭代器-哨兵对替代整个区间。
  • 语义上等价于对每个元素调用 g() 或 d(g),但允许引擎或分布提供批量成员 generate_random(r),包装 SIMD 等向量化后端的用户定义生成器可借此一次产出整块数据。标准库自带的引擎与分布目前没有该成员,实际逐元素生成。
  • 目标是 sized range 时,引擎被调用的次数未指定——不能假设一次调用恰好消耗 size(r) 个引擎输出。
  • 特性测试宏:__cpp_lib_ranges_generate_random(202403L)。