[C++面试] 你了解视图吗?

一、入门

1、什么是 C++ 视图(View)?请简要说明其概念和用途

它提供了对序列(如数组、容器等)的非拥有性、只读或可写的访问。(就像是个透明的放大镜,它能让你去看一组数据,但它自己不拥有这些数据。)

  • 避免数据的复制,提高性能。
  • 提供统一的接口来处理不同类型的序列。
  • 实现延迟计算,只有在需要时才进行实际的数据处理。
cpp 复制代码
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    // 创建一个视图,它是对numbers向量的一个抽象表示
    auto view = std::views::all(numbers); 
    for (auto num : view) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}

在这个例子中,std::views::all 创建了一个视图,它包含了 numbers 向量中的所有元素。通过视图可以遍历这些元素,而不需要复制数据。

2、std::views::transform 是干啥的?

td::views::transform 是个很有用的工具,它能对一组数据里的每个元素做转换。你可以把它想象成一个小魔法师,能把每个元素变成你想要的样子

3、 传统容器的区别

  • 不持有数据:视图仅引用底层数据,不进行复制或所有权管理。
  • 惰性计算 :操作(如transformfilter)在遍历时才会执行。
  • 常量时间复杂度操作:构造、复制和移动的时间复杂度为O(1)
cpp 复制代码
auto square = [](int x) { 
    std::cout << "Processing " << x << std::endl; 
    return x * x; 
};
auto view = data | std::views::transform(square);
// 此时未调用square,直到遍历view时才输出日志

"|" 是范围(ranges)库中的管道运算符,它能够把一个范围(如 std::vector)和视图适配器进行连接。

cpp 复制代码
auto squared = data | std::views::transform([](int x) { return x * x; });
  • std::views::transform 是一个视图适配器,其作用是对范围中的每个元素执行转换操作。它接收一个可调用对象(通常是 lambda 表达式)作为参数,这个可调用对象会对每个元素进行处理。
  • [](int x) { return x * x; } 是一个 lambda 表达式,它接收一个 int 类型的参数 x,并返回 x 的平方。
  • auto squared 借助 auto 关键字自动推导 squared 的类型。squared 实际上是一个视图对象,它代表了对 data 中每个元素进行平方操作后的结果。需要注意的是,这里并没有立即计算平方值,而是在遍历时才会进行计算,这就是延迟计算。
cpp 复制代码
std::vector<int> data{1, 2, 3, 4, 5};
auto result = data 
    | std::views::filter([](int x) { return x % 2 == 0; })  // 过滤偶数
    | std::views::transform([](int x) { return x * 2; });   // 每个数乘2
// 遍历时才执行:先过滤出2,4 → 再转换为4,8
  • 执行顺序:从左到右依次处理(先过滤再转换)。
  • 零拷贝优化 :中间不生成临时容器,内存效率高

4、视图与for_each的区别

  • 视图 :延迟计算,不修改原数据,支持链式操作(如 filter + transform)。
  • std::for_each:立即执行,需显式处理每个元素,无法组合操作。
cpp 复制代码
// 视图:延迟计算,链式操作
auto result = data | std::views::filter(is_even) | std::views::transform(square);

// std::for_each:立即执行,需手动处理
std::for_each(data.begin(), data.end(), [](int x) { /* 处理逻辑 */ });

二、进阶

1、如何使用视图进行数据过滤和转换?

cpp 复制代码
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    // 过滤出偶数
    auto even_view = numbers | std::views::filter([](int x) { return x % 2 == 0; });
    // 将过滤后的偶数乘以2
    auto transformed_view = even_view | std::views::transform([](int x) { return x * 2; });
    for (auto num : transformed_view) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}
  • 首先使用 std::views::filter 视图适配器过滤出 numbers 向量中的偶数
  • 然后使用 std::views::transform 视图适配器将过滤后的偶数乘以 2
  • 最后遍历转换后的视图并输出结果

2、视图的惰性求值如何实现?在性能优化中有何利弊?

实现机制 :视图通过封装迭代器和函数对象,在遍历时动态应用操作(如transform_view存储转换函数,filter_view存储谓词)

  • 优点 :避免中间数据拷贝,节省内存;支持提前终止遍历(如break时跳过后续计算)
  • 缺点 :多次遍历同一视图可能导致重复计算(如transform函数被多次调用),可通过std::ranges::to<std::vector>缓存结果优化

3、视图可能的问题 与 风险

若底层容器被修改、发生插入/删除(如vector扩容导致迭代器失效),视图可能引用无效内存。(图通过封装迭代器和函数对象)

最佳实践

  • 修改容器后,重新生成视图。
  • 避免在视图生命周期内修改容器容量(如使用std::list或预留容量)

4、如何实现视图的原地修改?

cpp 复制代码
std::vector<int> data = {1, 2, 3};
data | std::views::transform([](int& x) { x *= 2; });  // 原地修改

三、高阶

1、自定义视图适配器需要注意哪些方面?请给出一个自定义视图适配器的示例。

自定义视图适配器时需要注意以下几点:

  • 继承 std::ranges::view_interface 类,以确保视图符合视图的接口要求。
  • 实现必要的迭代器和范围相关的类型别名,如 iteratorsentinel 等。
  • 实现视图的构造函数和访问方法。
cpp 复制代码
#include <iostream>
#include <ranges>
#include <vector>

// 自己做的视图工具
template <std::ranges::input_range R>
class custom_view : public std::ranges::view_interface<custom_view<R>> {
private:
    R base_;
public:
    custom_view(R r) : base_(std::move(r)) {}

    auto begin() { return std::ranges::begin(base_); }
    auto end() { return std::ranges::end(base_); }
};

// 做视图工具的工厂函数
template <std::ranges::input_range R>
auto make_custom_view(R&& r) {
    return custom_view<std::views::all_t<R>>(std::forward<R>(r));
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    auto view = make_custom_view(numbers);
    for (auto num : view) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}

定义了一个自己的视图工具 custom_view,它继承了 std::ranges::view_interface。写了 beginend 方法,这样就能一个一个地去看数据了。还写了一个工厂函数 make_custom_view 来创建这个视图工具。最后用这个工具去看 numbers 里的数字,并且把它们都输出出来。

2、实现一个"步长视图"(每隔N个元素取一个)

cpp 复制代码
template <std::ranges::view V>
class StrideView : public std::ranges::view_interface<StrideView<V>> {
    V base_;
    size_t stride_;
public:
    StrideView(V base, size_t stride) : base_(base), stride_(stride) {}
    
    auto begin() { 
        return std::ranges::begin(base_);  // 实际需自定义迭代器步进逻辑
    }
    auto end() { return std::ranges::end(base_); }
};

// 使用示例:每隔2个元素取一个
auto view = data | StrideView(2);
  • 继承view_interface,定义迭代器步进逻辑。
  • 实现begin()end(),控制迭代器步长。

3、 视图如何与协程结合处理无限数据流?

场景:生成无限序列(如传感器数据),按需消费。内存友好,避免缓存全部数据。

cpp 复制代码
generator<int> infinite_stream() {
    for (int i : std::views::iota(0) | std::views::transform(process_data)) {
        co_yield i;  // 每次协程恢复时生成一个处理后的数据
    }
}

4、如何设计一个线程安全的视图?

cpp 复制代码
template <typename T>
class thread_safe_view : public std::ranges::view_interface<thread_safe_view<T>> {
    std::mutex mtx;
    T& data;
public:
    auto begin() {
        std::lock_guard<std::mutex> lock(mtx);
        return std::begin(data);
    }
    // 类似实现 end()
};
  • 在底层容器外部加锁,确保视图遍历时容器不被修改。

补充:视图的典型应用场景

  1. 大数据处理:避免复制GB级数据,用视图切片处理。
  2. 实时日志过滤 :用filter视图动态筛选关键日志。
  3. 游戏引擎 :用transform视图批量计算粒子效果位置。
相关推荐
郭涤生31 分钟前
第二章:影响优化的计算机行为_《C++性能优化指南》notes
开发语言·c++·笔记·性能优化
pursue_my_life40 分钟前
Golang中间件的原理与实现
开发语言·后端·中间件·golang
@小匠41 分钟前
使用 Python包管理工具 uv 完成 Open WebUI 的安装
开发语言·python·uv
code bean42 分钟前
【C#】关键字 volatile
开发语言·c#
若汝棋茗44 分钟前
C# 异步方法设计指南:何时使用 await 还是直接返回 Task?
开发语言·c#
程序员yt1 小时前
双非一本Java方向,学完感觉Java技术含量不高,考研换方向如何选择?
java·开发语言·考研
小宋要上岸1 小时前
基于 Qt / HTTP/JSON 的智能天气预报系统测试报告
开发语言·qt·http·json
MobiCetus1 小时前
如何一键安装所有Python项目的依赖!
开发语言·jvm·c++·人工智能·python·算法·机器学习
凌冰_1 小时前
Java 集合中ArrayList与LinkedList的性能比较
java·开发语言
思麟呀1 小时前
String类的模拟实现
开发语言·c++·算法