C++20新特性:Range库

之前我们已经学习过C++20的概念与约束。今天我们就来学习继承了概念与约束的标准库新的功能库Range库

相关代码上传至gitee:

官方文档:Standard library header <ranges> (C++20) - cppreference.com

目录

Range库介绍

前言

包含内容

[1. 核心概念(Concepts)------ 类型系统的基石](#1. 核心概念(Concepts)—— 类型系统的基石)

[2. 算法(Algorithms)------ std::ranges:: 命名空间](#2. 算法(Algorithms)—— std::ranges:: 命名空间)

[3. Range 适配器(Range Adaptors)------ 惰性管道](#3. Range 适配器(Range Adaptors)—— 惰性管道)

[4. 工厂(Factories / Range Factories)](#4. 工厂(Factories / Range Factories))

优势

消灭迭代器对------代码量减半

管道组合------从命令式到声明式

惰性求值------零开销抽象

更丰富的返回值

[投影(Projection)------无需 Lambda 包装器](#投影(Projection)——无需 Lambda 包装器)

[Concepts 约束------编译期安全带](#Concepts 约束——编译期安全带)

[对内置数组和 std::span 的原生支持](#对内置数组和 std::span 的原生支持)

Range视图

视图的核心特点与优势

适配器视图

工厂视图

综合实践


Range库介绍

前言

Ranges 是对 STL 算法与迭代器体系的一次根本性重构。 传统 STL 算法用一对迭代器 [begin, end) 表示操作范围,而 Ranges 将这个"范围"抽象为一等公民------一个可以直接传递的对象

核心变化:

cpp 复制代码
// C++17 传统写法:必须传两个迭代器
std::sort(vec.begin(), vec.end());

// C++20 Ranges 写法:传一个 range 对象即可
std::ranges::sort(vec);

这个看似微小的语法变化背后,是整个泛型编程范式的升级。

包含内容

整个库基于 <ranges> 头文件,分为四大层次:

1. 核心概念(Concepts)------ 类型系统的基石

Concept 含义
std::ranges::range 任何有 begin()end() 的类型
std::ranges::sized_range 可以在常数时间获取大小的 range
std::ranges::view 轻量级、O(1) 拷贝/移动的 range
std::ranges::input_range 至少支持输入迭代器的 range
std::ranges::forward_range 支持前向迭代器
std::ranges::bidirectional_range 支持双向迭代器
std::ranges::random_access_range 支持随机访问迭代器
std::ranges::contiguous_range 内存连续布局(如 vector, array, span
std::ranges::common_range begin()end() 返回相同类型的 range

2. 算法(Algorithms)------ std::ranges:: 命名空间

所有传统 STL 算法的 Range 版本 ,约束在 std::ranges 命名空间下。与传统算法有两个关键区别:

  • 第一个参数是 range 对象,而非迭代器对

  • 返回类型更丰富:不再只返回一个迭代器,而是返回包含更多信息的结构化结果

    // 传统 STL:返回迭代器,要找插入位置还得再算
    auto it = std::find(vec.begin(), vec.end(), 42);

    // C++20 Ranges:接受 range,返回迭代器
    auto it = std::ranges::find(vec, 42);

3. Range 适配器(Range Adaptors)------ 惰性管道

这是 Ranges 库最具革命性 的部分。适配器是惰性求值的视图转换,通过 | 管道运算符串联,构成声明式的数据处理流水线:

适配器 作用
std::views::filter(pred) 按谓词过滤元素
std::views::transform(fn) 对每个元素应用函数
std::views::take(n) 取前 n 个元素
std::views::drop(n) 跳过前 n 个元素
std::views::take_while(pred) 取满足条件的连续前缀
std::views::drop_while(pred) 丢弃满足条件的连续前缀
std::views::reverse 反转范围
std::views::join 展平嵌套 range
std::views::split(delim) 按分隔符切分
std::views::common 转换为 common_range
std::views::keys / values 提取 pair/tuple 的 key 或 value
std::views::iota 生成递增序列
std::views::all 将 range 包装为 view

4. 工厂(Factories / Range Factories)

不需要输入 range,直接生成 range 的工具:

  • std::views::iota(start, end) --- 生成 [start, end) 整数序列
  • std::views::empty<T> --- 空 range
  • std::views::single(x) --- 只包含单一元素的 range
  • std::views::istream<T>(stream) --- 从输入流读取

优势

消灭迭代器对------代码量减半

cpp 复制代码
// C++17:写两遍容器名
std::sort(v.begin(), v.end());
auto it = std::find_if(v.begin(), v.end(), pred);

// C++20:容器名只出现一次
std::ranges::sort(v);
auto it = std::ranges::find_if(v, pred);

这不是语法糖------它消除了迭代器不匹配的可能性,从类型系统层面杜绝了 sort(v1.begin(), v2.end()) 这类经典 bug。

管道组合------从命令式到声明式

这是最重要的心智模型转变。传统写法是命令式 的:你需要一步步告诉 CPU 要做什么。管道写法是声明式的:你描述数据应该经过怎样的变换。

cpp 复制代码
// C++17 命令式:需要中间容器,多次遍历
std::vector<int> even;
std::copy_if(v.begin(), v.end(), std::back_inserter(even),
             [](int x) { return x % 2 == 0; });
std::vector<int> squared;
std::transform(even.begin(), even.end(), std::back_inserter(squared),
               [](int x) { return x * x; });
auto it = std::find_if(squared.begin(), squared.end(),
                       [](int x) { return x > 100; });

// C++20 声明式:一次遍历,零拷贝,惰性求值
auto result = v
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::drop_while([](int x) { return x <= 100; });

for (auto x : result) { /* 只在遍历时才真正计算 */ }

惰性求值------零开销抽象

管道中的每个适配器在定义时不执行任何计算 。计算被推迟到消费时才发生,且所有变换融合成单次遍历。没有中间分配,没有额外拷贝。

更丰富的返回值

传统 std::find 只返回一个迭代器,还需要重新计算到开头的距离等操作。Range 算法返回结构化的结果对象:

cpp 复制代码
// C++17
auto it = std::find(v.begin(), v.end(), 42);
auto pos = std::distance(v.begin(), it); // 又遍历一次

// C++20:直接通过结构化绑定获取所需信息
struct find_result { iterator found; bool success; }; // 伪代码示例
// 实际例子------ranges::sort 返回 borrowed_iterator_t<R>

投影(Projection)------无需 Lambda 包装器

每个 Range 算法都支持一个 Projection 参数,可以在比较/操作之前对元素做变换,而无需手动写 lambda:

cpp 复制代码
// C++17:lambda 包装比较
std::sort(v.begin(), v.end(),
          [](const Person& a, const Person& b) { return a.age < b.age; });

// C++20:投影 + 默认比较
std::ranges::sort(v, std::less{}, &Person::age);
//                    ^^^^^^^^^  ^^^^^^^^^^^^
//                    比较函数      投影(要对什么排序)

Concepts 约束------编译期安全带

Range 算法通过 Concept 对其输入做了严格约束。如果一个类型不满足 input_range,调用 ranges::find 在编译期就会给出清晰的错误信息,而不是在实例化后抛出几十行的模板错误。

复制代码
int x = 42;
// std::ranges::sort(x);  // 编译错误,清晰提示 int 不是 range

对内置数组和 std::span 的原生支持

cpp 复制代码
int arr[] = {3, 1, 4, 1, 5};
std::ranges::sort(arr);  // 直接对 C 数组排序,不需要 std::begin/std::end

Range视图

视图的核心特点与优势

视图 (View) 是 Ranges 库的核心抽象,具有以下关键特征:

  1. O(1) 拷贝/移动 --- 视图不拥有数据,只持有底层 range 的引用或轻量级状态
  2. 惰性求值 --- 定义管道时不执行任何计算,消费时才按需计算
  3. 融合遍历 --- 多个视图通过 | 串联后,编译器将其融合为等效的单次手写循环
  4. 不修改原数据 --- 视图是"数据的观察窗口",变换操作生成新视图而不修改底层容器
  5. 编译期类型安全 --- 基于 Concepts 约束,视图间的非法连接会在编译期被拦截

适配器视图

需要输入 range,对数据做变换

适配器 功能 典型场景
filter(pred) 保留满足谓词的元素 筛选偶数、有效记录
transform(fn) 对每个元素应用映射函数 类型转换、提取字段
take(n) 取前 n 个元素后停止 分页、TOP-N 查询
drop(n) 跳过前 n 个元素 跳过表头、去除前导
take_while(pred) 取满足条件的连续前缀 读取到终止标记
drop_while(pred) 丢弃满足条件的连续前缀 去除前导空白
reverse 反转 range(需 bidirectional) 逆序遍历
split(delim) 按分隔符切割(range of ranges) 字符串分词
join 展平嵌套 range 二维转一维
keys 提取 pair/tuple 的第一个元素 遍历 map 的 key
values 提取 pair/tuple 的第二个元素 遍历 map 的 value
all 将 range 包装为 view 显式开启管道
common 统一 begin() 和 end() 返回类型 适配遗留接口

代码示例:

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <map>
#include <list>
#include <sstream>

namespace r = std::ranges;
namespace v = std::views;

template<typename R>
void print_range(std::string_view name, R&& r) {
    std::cout << name << ": ";
    for (auto&& e : r) std::cout << e << ' ';
    std::cout << '\n';
}

// ----------------------------------------------------------------------------
int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // ================================================================
    // 1. filter  ------ 按谓词过滤,保留满足条件的元素
    // ================================================================
    {
        auto even = data | v::filter([](int x) { return x % 2 == 0; });
        print_range("filter(偶数)", even);  // 2 4 6 8 10
    }

    // ================================================================
    // 2. transform ------ 对每个元素施加映射函数
    // ================================================================
    {
        auto squared = data | v::transform([](int x) { return x * x; });
        print_range("transform(平方)", squared);  // 1 4 9 16 25 36 49 64 81 100
    }

    // ================================================================
    // 3. 管道组合:filter + transform ------ 惰性求值,单次遍历
    // ================================================================
    {
        auto pipeline = data
            | v::filter([](int x) { return x % 2 == 0; })       // 先过滤偶数
            | v::transform([](int x) { return x * x; });        // 再平方
        print_range("filter + transform(偶数的平方)", pipeline); // 4 16 36 64 100
    }

    // ================================================================
    // 4. take(n) ------ 取前 n 个元素,达到 n 个后立即停止遍历
    //    drop(n) ------ 跳过前 n 个元素
    // ================================================================
    {
        auto first3 = data | v::take(3);
        print_range("take(3)", first3);  // 1 2 3

        auto skip3 = data | v::drop(3);
        print_range("drop(3)", skip3);   // 4 5 6 7 8 9 10

        // 与 filter 组合:取前3个偶数
        auto first3even = data
            | v::filter([](int x) { return x % 2 == 0; })
            | v::take(3);
        print_range("前3个偶数", first3even);  // 2 4 6
    }

    // ================================================================
    // 5. take_while ------ 取满足条件的连续前缀
    //    drop_while ------ 丢弃满足条件的连续前缀
    // ================================================================
    {
        auto prefix = data | v::take_while([](int x) { return x < 5; });
        print_range("take_while(x < 5)", prefix);  // 1 2 3 4

        auto suffix = data | v::drop_while([](int x) { return x < 5; });
        print_range("drop_while(x < 5)", suffix);  // 5 6 7 8 9 10
    }

    // ================================================================
    // 6. reverse ------ 反转 range(要求 bidirectional_range)
    // ================================================================
    {
        auto reversed = data | v::reverse;
        print_range("reverse", reversed);  // 10 9 8 7 6 5 4 3 2 1
    }

    // ================================================================
    // 7. split ------ 按分隔符切割 range
    //    返回的是一个 range of ranges,每个子 range 是一段
    // ================================================================
    {
        std::string text = "hello world cpp ranges";
        auto words = text | v::split(' ');
        std::cout << "split(' '): ";
        for (auto word : words) {
            std::cout << '\'';
            for (char ch : word) std::cout << ch;
            std::cout << "' ";
        }
        std::cout << '\n';  // 'hello' 'world' 'cpp' 'ranges'
    }

    // ================================================================
    // 8. join ------ 展平嵌套的 range(range of ranges → flat range)
    // ================================================================
    {
        std::vector<std::vector<int>> nested = {{1, 2}, {3, 4, 5}, {6}};
        auto flat = nested | v::join;
        print_range("join(展平嵌套)", flat);  // 1 2 3 4 5 6

        // join 与 split 配合:先切割再展平(类似于去除分隔符)
        std::string text = "a-b-c";
        auto parts = text | v::split('-');
        // 把 '-' 替换为 ' '
        // 注意:这里 split 产生的 subrange 再 join 需要 C++23 的 join_with
        // 此处演示 join 的基本用法
    }

    // ================================================================
    // 9. keys / values ------ 提取关联容器的键/值
    // ================================================================
    {
        std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};

        std::cout << "keys: ";
        for (auto k : m | v::keys) std::cout << k << ' ';     // 1 2 3
        std::cout << '\n';

        std::cout << "values: ";
        for (auto v : m | v::values) std::cout << v << ' ';   // one two three
        std::cout << '\n';
    }

    // ================================================================
    // 10. all ------ 将任意 range 包装为 view
    //     当你想使用管道语法但手上的容器不是 view 时,all 是桥梁
    // ================================================================
    {
        auto v = data | v::all;          // vector → view
        auto r = v | v::reverse;         // 管道串联
        print_range("all | reverse", r); // 10 9 8 7 6 5 4 3 2 1
        // 实际上 | 运算符已经隐式做了这个转换,显式使用 all 的场景较少
    }

    // ================================================================
    // 11. common ------ 将 sentinel 类型与 iterator 类型统一
    //     某些遗留代码要求 begin() 和 end() 返回相同类型
    // ================================================================
    {
        auto filtered = data | v::filter([](int x) { return x > 5; });
        // filtered 的 begin() 和 end() 类型不同(sentinel != iterator)
        // static_assert(!r::common_range<decltype(filtered)>);

        auto common = filtered | v::common;
        // common 的 begin() 和 end() 返回相同类型
        static_assert(r::common_range<decltype(common)>);

        print_range("common", common);  // 6 7 8 9 10
    }

    // ================================================================
    // 12. 投射(Projection) ------ 不是适配器,但常与 Range 算法配合
    //     所有 std::ranges 算法都支持 projection 参数
    // ================================================================
    {
        struct Person { std::string name; int age; };
        std::vector<Person> people = {
            {"Charlie", 35}, {"Alice", 30}, {"Bob", 25}
        };

        // 按年龄排序,使用 projection 而非手写 lambda
        r::sort(people, std::less{}, &Person::age);

        std::cout << "projection(按age排序): ";
        for (auto& p : people) std::cout << p.name << '(' << p.age << ") ";
        std::cout << '\n';
    }

    return 0;
}

工厂视图

自身生成 range

工厂 功能 典型场景
iota(start) 无限递增序列 [start, ∞) 配合 take 生成索引
iota(start, end) 有限递增序列 [start, end) 生成数值区间
empty<T> 空 range 占位/空结果
single(x) 只包含单一元素的 range 插入单元素到管道
istream<T>(stream) 从输入流惰性读取 处理大数据文件
counted(it, n) 迭代器 + 计数构造 range 对接指针+长度 API

代码示例:

cpp 复制代码
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <sstream>
#include <iterator>
#include <cassert>

namespace r = std::ranges;
namespace v = std::views;

template<typename R>
void print_range(std::string_view name, R&& r) {
    std::cout << name << ": ";
    for (auto&& e : r) std::cout << e << ' ';
    std::cout << '\n';
}

// ----------------------------------------------------------------------------
int main() {

    // ================================================================
    // 1. iota ------ 生成递增序列,类似 Python 的 range()
    //    语法: v::iota(start, end)  生成 [start, end) 的整数
    //    也可: v::iota(start)       生成 [start, ∞) 的无界序列
    // ================================================================
    {
        // 有界 iota:指定上限
        auto seq = v::iota(1, 11);        // 1, 2, 3, ..., 10
        print_range("iota(1, 11)", seq);  // 1 2 3 4 5 6 7 8 9 10

        // 无界 iota:从某个值开始,无限延伸(必须配合 take 使用)
        // 直接迭代无界 iota 会导致无限循环!
        auto inf_seq = v::iota(100);               // 100, 101, 102, ...
        auto first5 = inf_seq | v::take(5);
        print_range("iota(100) | take(5)", first5); // 100 101 102 103 104
    }

    // ================================================================
    // 1b. iota 不只是整数 ------ 任何支持 ++ 和 < 的类型都可用
    // ================================================================
    {
        // 生成大写字母序列
        auto alpha = v::iota('A', 'H');     // A, B, C, D, E, F, G
        std::cout << "iota('A','H'): ";
        for (char c : alpha) std::cout << c << ' ';
        std::cout << '\n';
    }

    // ================================================================
    // 2. empty<T> ------ 生成空 range(不包含任何元素)
    //    常用于需要"空结果"占位的场景
    // ================================================================
    {
        auto e = v::empty<int>;
        std::cout << "empty<int> size: " << r::distance(e) << '\n'; // 0
        assert(r::empty(e));  // 确认是空 range
    }

    // ================================================================
    // 3. single(x) ------ 生成只包含单一元素的 range
    //    常与 join 配合使用,将单元素插入管道流
    // ================================================================
    {
        auto s = v::single(42);
        print_range("single(42)", s);  // 42

        static_assert(r::size(s) == 1);
        assert(*s.begin() == 42);
    }

    // ================================================================
    // 4. istream<T> ------ 从输入流读取,作为 range
    //    按需从流中惰性读取,不一次性加载所有数据
    // ================================================================
    {
        std::istringstream iss("10 20 30 40 50");
        auto numbers = v::istream<int>(iss);

        std::cout << "istream<int>: ";
        for (int x : numbers) std::cout << x << ' ';  // 10 20 30 40 50
        std::cout << '\n';
    }

    // ================================================================
    // 4b. istream 管道组合 ------ 从流读取后立即过滤/变换
    //     无需先存到 vector,直接惰性处理
    // ================================================================
    {
        std::istringstream iss("1 2 3 4 5 6 7 8 9 10");
        auto result = v::istream<int>(iss)
            | v::filter([](int x) { return x % 2 == 0; })
            | v::transform([](int x) { return x * x; })
            | v::take(3);

        std::cout << "istream管道(偶数的平方前3个): ";
        for (int x : result) std::cout << x << ' ';  // 4 16 36
        std::cout << '\n';
    }

    // ================================================================
    // 5. counted ------ 从迭代器 + 计数构造 range
    //    接收 (iterator, count) 对,生成大小固定的 range
    //    常用于处理遗留代码中"指针+长度"的 API
    // ================================================================
    {
        std::vector<int> v = {10, 20, 30, 40, 50, 60, 70, 80};
        // 从第3个元素(30)开始,取4个
        auto sub = v::counted(v.begin() + 2, 4);
        print_range("counted(begin+2, 4)", sub);  // 30 40 50 60

        // 实质等价于 r::subrange(begin, begin + count)
    }

    // ================================================================
    // 6. 综合示例:工厂 + 适配器 构建完整管道
    //    生成前20个自然数,保留3的倍数,平方后取前5个
    // ================================================================
    {
        auto pipeline = v::iota(1)                     // 工厂: 1,2,3,...
            | v::filter([](int x) { return x % 3 == 0; })  // 保留3的倍数: 3,6,9,...
            | v::transform([](int x) { return x * x; })     // 平方: 9,36,81,...
            | v::take(5);                                   // 取前5个

        print_range("综合管道", pipeline);  // 9 36 81 144 225
    }

    // ================================================================
    // 6b. 综合示例:斐波那契数列生成
    //     利用 iota 和 transform 可以生成任意序列
    // ================================================================
    {
        // 技巧: iota(0) 提供索引 n=0,1,2,...,transform 将 n 映射为 Fib(n)
        // 但这里需要状态,用单行管道做不直观,展示另一种思路:
        // 用 iota 产生索引,通过迭代计算 Fib

        // 更直接的 Fibonacci:使用结构化绑定 + iota
        auto fib = v::iota(0)
            | v::transform([a = 0ull, b = 1ull](int) mutable {
                  auto result = a;
                  a = b;
                  b = result + b;
                  return result;
              })
            | v::take(10);

        std::cout << "Fibonacci(前10项): ";
        for (auto f : fib) std::cout << f << ' ';  // 0 1 1 2 3 5 8 13 21 34
        std::cout << '\n';
    }
    return 0;
}

综合实践

按部分筛选员工然后加薪资

cpp 复制代码
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <map>
#include <ranges>
#include <string>
#include <vector>

namespace r = std::ranges;
namespace v = std::views;

// ============================================================================
// 员工薪资分析系统 --- C++20 Ranges 综合应用
// ============================================================================

struct Employee {
    std::string name;
    std::string department;
    double salary;
};

// ============================================================================
// 功能1: 按部门分组统计平均薪资
// ============================================================================
// C++20 没有 std::views::group_by(那是 C++23 的特性),因此采用 map 聚合,
// 配合 range-based for 遍历。每个部门的 salary 被累加后求均值。

void avgSalaryByDept(const std::vector<Employee>& employees) {
    // <部门, {薪资总和, 人数}>
    std::map<std::string, std::pair<double, int>> dept_stats;

    for (const auto& emp : employees) {
        auto& [total, count] = dept_stats[emp.department];
        total += emp.salary;
        ++count;
    }

    std::cout << "\n===== 各部门平均薪资 =====\n";
    std::cout << std::left;
    for (const auto& [dept, stats] : dept_stats) {
        const auto& [total, count] = stats;
        std::cout << "  " << std::setw(12) << dept
                  << " : " << std::fixed << std::setprecision(2)
                  << total / count << '\n';
    }
}

// ============================================================================
// 功能2: 找出薪资最高的员工
// ============================================================================
// 使用 std::ranges::max_element,通过 projection 指定比较对象为 salary 成员。
// 比传统 lambda 版本更简洁: r::max_element(v, less{}, &Employee::salary)

void findHighestPaid(const std::vector<Employee>& employees) {
    auto it = r::max_element(employees, std::less{}, &Employee::salary);

    std::cout << "\n===== 薪资最高员工 =====\n";
    if (it == employees.end()) {
        std::cout << "  (无数据)\n";
        return;
    }

    std::cout << "  " << it->name
              << " (" << it->department << ")"
              << " : " << std::fixed << std::setprecision(2)
              << it->salary << '\n';
}

// ============================================================================
// 功能3: 筛选技术部员工并加薪10%,然后打印
// ============================================================================
// filter 视图筛选技术部,展开 for 循环中修改 salary 后输出新旧对比。
// 注意: filter 返回的是 view,对 view 中元素的修改会反映回原容器。

void raiseITDept(std::vector<Employee>& employees) {
    std::cout << "\n===== 技术部加薪10% =====\n";
    std::cout << std::left;

    // 管道: 保留技术部 → 遍历修改
    auto it_emps = employees
        | v::filter([](const Employee& e) { return e.department == "技术部"; });

    for (auto& emp : it_emps) {
        double old_salary = emp.salary;
        emp.salary *= 1.10;
        std::cout << "  " << std::setw(10) << emp.name
                  << " : " << std::setw(8) << old_salary
                  << " -> " << std::fixed << std::setprecision(2)
                  << emp.salary << '\n';
    }
}

// ============================================================================
// 统一入口: 依次执行上述三项分析
// ============================================================================

void analyzeEmployees(std::vector<Employee>& employees) {
    avgSalaryByDept(employees);
    findHighestPaid(employees);
    raiseITDept(employees);
}

// ============================================================================
// main
// ============================================================================

int main() {
    std::vector<Employee> staff = {
        {"张三",   "技术部", 25000.0},
        {"李四",   "技术部", 28000.0},
        {"王五",   "技术部", 22000.0},
        {"赵六",   "市场部", 18000.0},
        {"孙七",   "市场部", 20000.0},
        {"周八",   "人事部", 15000.0},
        {"吴九",   "技术部", 35000.0},
        {"郑十",   "人事部", 16000.0},
        {"冯十一", "市场部", 19000.0},
    };

    std::cout << std::fixed << std::setprecision(2);
    std::cout << "===== 员工薪资分析系统 =====\n";
    std::cout << "共 " << staff.size() << " 名员工\n";

    analyzeEmployees(staff);
}

本期内容到这里就结束了,喜欢请点个赞谢谢

封面图自取:

相关推荐
字节高级特工1 小时前
【Linux】深入理解C语言命令行参数与环境变量
linux·c++·人工智能·后端
hdsoft_huge1 小时前
以2026世界杯晋级逻辑,生动拆解SpringBoot软件架构
java·spring boot·后端
念恒123061 小时前
Python 函数完全指南:定义与调用
开发语言·python
程序员契奇1 小时前
10_Agent的使用OverAllState和RunnableConfig
后端·agent
曹牧1 小时前
Java:Unix时间戳
java·开发语言
linux开发之路1 小时前
C++项目推荐:eBPF+调度器性能分析框架
linux·c++·ebpf·火焰图·调度器
神奇小汤圆1 小时前
一条命令让你这辈子彻底解决"LF will be replaced by CRLF"(建议收藏)
后端
段一凡-华北理工大学1 小时前
工业领域的Hadoop架构学习~系列文章02:HDFS架构深度剖析
大数据·人工智能·hadoop·学习·架构·高炉炼铁
会编程的土豆1 小时前
Go 里的 error 接口 + 假 nil(超级重点)
开发语言·后端·golang