C++面试题

72 阅读27分钟

C++

  1. C++多态,动态多态和静态多态

    多态指的是允许不同类型的对象对同一个函数调用做出不同响应的机制,有静态多态和动态多态.

    静态多态通过函数重载和模板来实现,编译器静态阶段,动态多态通过使用虚函数表与虚函数指针实现

  2. C++虚函数,虚函数指针,纯虚函数,以及虚函数指针和虚函数分别属于哪个层次

    虚函数用virtual关键字声明的成员函数,允许派生类重写,虚函数指针是存在与对象实例中的隐藏指针,指向改对象所属类的虚函数表,纯虚函数则是一个接口,强制派生类必须实现,并是该基类不可实例化

    虚函数指针属于对象,每个实例拥有一个,虚函数表属于类,由类的所有对象共享

  3. C++内存区

    内存区存放内容分配与释放时机
    栈区 (Stack)局部变量、函数参数、返回地址函数调用时分配,函数返回时释放
    堆区 (Heap)程序员用 new 等创建的对象程序员手动分配和释放
    全局/静态区全局变量、静态变量 (static)程序启动时分配,程序结束时释放
    常量区字符串字面量、全局 const 常量程序启动时分配,程序结束时释放
    代码区程序二进制代码程序加载时分配,程序结束时释放
  4. 构造函数和析构函数的调用顺序

    构造函数:基类优先:先调用基类的构造函数,再调用派生类,成员优先:先调用成员对象的构造函数,再调用自身的构造函数,基类->成员->自身

    析构函数:与构造相反,先析构自身,在逆序析构成员对象,最后析构基类

  5. C++不能被继承的类

    使用 final 关键字 (C++11及以后) :在类名后添加 final 关键字;构造函数私有化/delete

  6. 堆和栈的区别

    方面栈 (Stack)堆 (Heap)
    管理方式编译器自动管理程序员手动管理 (new/delete)
    分配效率高,速度快低,速度慢
    空间大小小,且固定大,受限于虚拟内存
    内存碎片不会产生碎片频繁操作会产生碎片
    生长方向向低地址扩展 (向下生长)向高地址扩展 (向上生长)
  7. 对深拷贝的理解,什么是深浅拷贝

    默认情况下是浅拷贝,可以考虑手动重写类的拷贝构造,拷贝复制已经对应的析构实现深拷贝

    浅拷贝 (Shallow Copy) :只复制对象成员的。如果成员是指针,则只复制指针的地址,不复制指针所指向的内存。这导致多个对象共享同一块堆内存,容易引发悬挂指针和重复释放的问题。

    深拷贝 (Deep Copy) :不仅复制对象成员的值,当遇到指针成员时,会重新分配一块内存,并将原指针所指向的内容复制到新内存中。这确保每个对象都拥有独立的内存资源,彼此互不影响。

  8. new和malloc的区别,delete和free的区别

    new/malloc:

    1. new:是一个操作符会调用operator new这个全局函数来申请足够大的原始内存,然后在指定内存上调用类的构造函数,类型安全,返回具体类型的指针,new失败抛出std::bad_alloc异常,只需要指定类型即可,不需要传入具体大小,且new可以被重载
    2. malloc:库函数,想操作系统申请一块指定大小的连续的原始内存,返回指向该内存起始地址的void指针,不会调用构造,只分配内存,类型不安全,返回void,失败返回NULL,需要显示传入所需new的字节数,不可以被重载

    delete/free:

    1. delete在释放内存之前会先调用对象的析构函数,与new相同,delete会在对象被析构后去调用operator delete的全局函数(通常是对free的封装).
    2. free只是一个库函数,与malloc对应,接受指针,告诉操作系统该指针指向的内存可以被回收了

    ps:

    1. 重载new指的就是重载operator new这个函数,可以接管内存分配的细节,类似于内存池;
    2. Placement new是在一块已经存在的,预先分配好的内存地址上调用构造函数来初始化一个对象,仅仅是放置对象
    3. new/malloc在分配内存时,会在分配的内存块前添加一个头部,头部中记录了该内存块的大小,delete时会根据传入的指针前移找到具体大小
  9. C++的sharedptr内部如何维护引用计数

    通过在对上动态分配的控制块集中管理,一个shared_ptr对象包含两个指针,一个指向管理的对象,另一个指向控制块,所有指向同一个对象的shared_ptr的内部的第二个指针都指向同一个控制块,其中包含一个强引用计数usecount和弱引用计数weakcount,当shared_ptr被拷贝,赋值或销毁时,通过珍珍访问控制块,以原子操作的方式增减引用计数值,保证线程安全,强引用计数为0则析构对象,但是为了解决weakptr的悬挂问题,控制块会在弱引用计数为0才会被释放,这允许weakptr访问依然存在的控制块,来判断指向的对象是否已被销毁.

  10. 哪些函数不可以是虚函数

    虚函数是动态多态的机制,且多态依赖于类,所以必须是动态的,属于类的函数,所以:

    构造函数:虚函数的调用依赖于对象中的虚函数指针(vptr),而 vptr 在构造函数执行完毕后才能被正确初始化。

    静态成员函数:静态函数属于类而非对象实例,它没有 this 指针,因此无法访问虚函数表来实现动态绑定。

    模板成员函数:模板的实例化是在编译期,而虚函数的动态绑定发生在运行期。编译器无法在编译时得知所有可能的模板实例化版本来填充虚函数表。

    内联函数 (Inline Functions) :虽然语法上可以通过,但内联的“编译期展开”和虚函数的“运行期决议”在本质上是冲突的。大多数情况下,编译器会忽略对虚函数的内联请求。

    非成员函数:包括友元(friend)和普通函数,它们不属于类,没有虚函数的概念。

  11. 进程与线程的区别

    进程 (Process) :操作系统进行资源分配的基本单位。它是程序的一次执行实例,拥有独立的内存空间和系统资源。

    线程 (Thread) :CPU进行调度和执行的基本单位。它是进程内的一个执行实体,也被称为轻量级进程。

    区别:

    1. 基本单位:进程是资源分配的基本单位,而线程是CPU调度的基本单位。
    2. 资源共享:进程拥有独立的地址空间,资源不共享,隔离性好。同一进程内的所有线程共享该进程的全部资源(如内存、文件句柄)。
    3. 开销:创建和切换进程的开-销远大于线程。线程的创建和切换更轻量、更高效。
    4. 健壮性:一个进程的崩溃不会影响其他进程。但一个线程的崩溃会导致其所属的整个进程退出。
    5. 关系:一个进程至少包含一个线程,线程必须依附于进程存在。
  12. C++多线程之间如何通信

    信主要通过共享内存并结合同步原语来实现:

    互斥锁 (Mutex) :用于保护共享数据,确保同一时间只有一个线程可以访问,解决“读-写”和“写-写”冲突。

    条件变量 (Condition Variable) :与互斥锁配合使用,允许线程在某个条件满足前进入等待(阻塞)状态,并在条件满足后被唤醒,实现线程间的同步。

    原子操作 (Atomic Operations) :对整型或指针等简单类型进行不可分割的、线程安全的操作,适用于实现高效的无锁数据结构或计数器。

    Future 和 Promise:用于在线程间传递异步操作的结果。一个线程(promise)承诺在未来某个时刻提供一个值,另一个线程(future)可以等待并获取这个值。

    信号量 (Semaphore) :控制同时访问某一特定资源的线程数量。

  13. C++多线程如何同步.常用哪些锁

    通过互斥量,条件变量,原子操作,信号量,C++的锁通常指mutex,以及管理mutex的锁管理器.

    互斥量:

    • std::mutex:最基本、最常用的互斥锁。
    • std::recursive_mutex:递归互斥锁。允许同一个线程对它进行多次加锁。
    • std::timed_mutex:定时互斥锁。提供尝试在一定时间内加锁的功能。
    • std::shared_mutex (C++17):读写锁。允许多个读线程同时访问,但只允许一个写线程,实现“读共享、写独占”。

    锁管理器:

    • std::lock_guard:最基础的锁管理器,功能简单直接。
    • std::unique_lock:功能更强大、更灵活的锁管理器。它支持延迟加锁、可移动等操作,是配合条件变量使用的标准选择。
    • std::shared_lock (C++17):配合 std::shared_mutex 使用的读锁管理器。
    • std::scoped_lock (C++17):可以同时管理多个互斥锁,能有效避免死锁。
  14. C++并发的std::async,std::promise,std::future分别是什么,有什么用

    std::future (未来)

    • 是一个代表异步操作未来结果的对象。它是一个“占位符”,可以从异步任务中获取最终的返回值或异常。
    • 主要用于等待和获取异步任务的结果。调用 future::get() 会阻塞当前线程,直到结果准备就绪。

    std::promise (承诺)

    • 是一个用于设置 std::future 值的对象。它与一个 future 配对,是结果的“生产者”。
    • 允许一个线程向另一个持有对应 future 的线程手动发送一个值或异常,从而建立一个一次性的线程间通信通道。

    std::async (异步)

    • 是一个高级函数模板,用于以异步方式启动一个可调用对象(如函数、lambda表达式)。
    • 简化异步任务的创建。它会自动处理线程的创建和管理,并返回一个 std::future,该 future 会在任务完成后持有其返回值。它是 std::threadstd::promise/std::future 组合的便捷封装。
  15. C++std::bind是什么

    将一个可调用对象(如函数、成员函数、函数对象)与其部分或全部参数进行绑定,从而生成一个新的、更简单的可调用对象(函数对象),可以通过占位符改变参数的传递顺序,也可以绑定成员函数

  16. Boost的asio如何使用,如何监听一个连接

    asio是一个基于 Proactor 设计模式的异步 I/O 库,大致流程为:

    创建 io_context

    • io_context 是 Asio 的核心,作为 I/O 服务的调度器。所有 I/O 对象(如 socket)都需要它的参与。

    创建 I/O 对象

    • 根据需求创建具体的 I/O 对象,例如用于网络连接的 ip::tcp::socket 或用于监听的 ip::tcp::acceptor

    发起异步操作

    • 调用 I/O 对象的异步函数(通常以 async_ 开头),例如 socket.async_read_some(...)
    • 在调用时,必须提供一个完成处理器 (Completion Handler) ,这通常是一个回调函数或 Lambda 表达式。

    运行 io_context

    • 调用 io_context.run() 来启动事件循环。这个调用会阻塞,io_context 开始在后台处理 I/O 事件。

    处理完成事件

    • 当异步操作完成时(例如,数据已收到),io_context 会调用你之前提供的完成处理器。所有实际的业务逻辑(如处理收到的数据)都在处理器中执行。

    要监听一个tcp连接,首先准备一个iocontext,和acceptor,创建一个endpoint端点对象,指定IP与Port,调用acceptor的open,bind,listen进入监听状态,调用 acceptor.async_accept(...)。这个函数不会阻塞,它告诉 io_context:“当有新连接到来时,请执行我给你的这个回调函数”,这个回调函数会接收到一个代表新连接的 socket,可以用这个socket来连接对端并通信,并且需要在回调中继续调用async_accept使得能将一个新的接收任务投递给ioc.调用 io_context.run(),Asio 开始在后台等待连接。当第一个连接到来时,步骤 3 中注册的回调被执行,然后在回调内部又注册了下一个监听任务,如此循环.

  17. C++移动语义,什么是左值右值,什么是完美转发,引用折叠,万能引用

    1. 左值右值

      • 左值:有固定内存地址,可以被取址的表达式,通常是具名变量
      • 右值:临时/将亡值,以及字面量,表达式结束后就会被销毁
    2. 移动语义:

      目的是为了避免对临时对象进行不必要的深拷贝,通过资源的转移/窃取,而不是复制,引入了右值引用&&,允许函数重载来专门处理右值,当一个对象要被一个右值初始化时,移动构造会直接窃取这个右值内部的资源,并且将右值置为可析构的空状态

    3. 万能引用

      即template T的T&&类型,只发生在类型推导的上下文中才是万能引用,万能引用既可以绑定到左值,也可以绑定到右值,例如 template<typename T> void func(T&& param); 如果传递左值给param,T会被推到为左值引用类型,如int&,如果传递一个右值给param,会被推导为 int,

    4. 引用折叠:

      只要有任意一个是左值引用,结果就是左值引用。只有当两个都是右值引用时,结果才是右值引用,使得万能引用能同时接受左值和右值,

      所以这里有三部分内容,T + && + param,T会被推导为int&或是int,那么对应的就是int& && paramint && param

    5. 完美转发;

      指在一个函数模板中,将接收到的参数以原始的值类别(lv/rv)原封不动的传递给另一个函数,但是问题在于,param在当前函数中是具名的,所以param本身始终是一个左值,所以此时,需要使用std::forward<T>(param),他会根据T的类型信息,来判断传入的这个实参是左值还是右值,实现了保留属性的转发,这样同时还保持了param参数在当前函数中正确的左值属性.

      • std::forward本质上是一个条件转换,大致等价于static_cast<T&&>(param),像是刚才的例子,T被推导为了int&和int,那么static_cast<T&&>(param)就变成了static_cast<int& &&>(param)static_cast<int&&>(param) 这里根据引用折叠规则会折叠为正确的属性

    总结与补充:万能引用和引用折叠被用在了函数参数这里,用来为模板展开不同的函数签名,使得可以同时接受左值和右值,在std::forward内部也会像:static_cast<int& &&>(param)发生引用折叠来进行类型转换.

    std::move:编译器类型转换,static_cast<Ty&&>(param) 等效

  18. 把一个左值引用的变量传递给一个模板函数,该函数形参为万能引用,那么在该函数内部,这个参数为左值还是右值

    根据之前的分析,这个参数本身将是一个左值

  19. 常见的STL容器(关联,非关联),map有哪几种,是基于什么实现的,复杂度如何

    1. 非关联;vector,array,list,deque,

    2. 关联:map,set,unordered_/multi_/ .. map/set

      • map基于红黑树,是一种自平衡二叉搜索树,键值自动有序存储,插入删除查找稳定,O(log n)
      • unordered_map基于哈希表(散列表),元素存储无序,查找快,最佳情况接近O(1).复杂度:平均O(1),最坏O(n)
  20. vector是堆上还是栈上,如何实现的动态扩容,size和capacity的区别是什么

    vector是堆上动态分配的容器,存储的数据将在堆上,而vector本身,及size,capacity,指向数据的指针根据创建的方式不同,如果,size指的是当前元素的个数,而capacity指的是vector的容量大小,size即将超过容量是,申请新内存,通常是旧容量的1.5/2倍,移动/拷贝(依赖于元素是否支持移动语义)内存中的元素到心内存中,释放旧内存,更新vector内部指针.

  21. 怎么理解移动操作,底层的移动是怎么实现的

    复制了指向资源的指针,将原始对象的指针置空,放弃了对资源的所有权。所以可以理解为:移动 = 浅拷贝 + 源对象置空(所有权转移)

  22. 什么是迭代器失效,怎么解决

    在一个容器(如 vector, list, map)上持有的迭代器,因为对该容器进行了某些会改变其内存布局或元素结构的操作(如插入、删除),导致该迭代器变得不再有效,失效后的迭代器不能再被安全地解引用、递增或递减。任何对它的使用都会导致未定义行为,通常表现为程序崩溃、数据损坏或逻辑错误.

    例如:

    // 错误示例
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        if (*it == value_to_remove) {
            vec.erase(it); // 错误!it 在这里已失效,后续的 ++it 非法
        }
    }
    ​
    // 正确示例
    for (auto it = vec.begin(); it != vec.end(); /* no increment here */) {
        if (*it == value_to_remove) {
            it = vec.erase(it); // 用 erase 的返回值更新 it
        } else {
            ++it;
        }
    }
    
  23. 什么是ECS,为什么用ECS

    ECS (Entity Component System) 是一种通过组合而非继承来构建对象的架构模式,它将程序的数据逻辑彻底分离。它由三部分构成:

    • 实体 (Entity) :可以理解为一个唯一的ID或标签。它本身不包含任何数据和逻辑,只是一个“容器”,用来关联一组组件。
    • 组件 (Component)纯数据的集合(Plain Old Data, 如 C++ 中的 struct)。它只包含描述实体某个方面的数据(例如 PositionComponent 包含x,y,z坐标;HealthComponent 包含生命值),不包含任何逻辑(方法)
    • 系统 (System)纯逻辑的集合。系统会遍历所有拥有特定组件组合的实体,并对这些组件的数据进行批量处理。例如,PhysicsSystem 会遍历所有同时拥有 PositionComponentVelocityComponent 的实体,并根据速度更新它们的位置。

    为什么:

    • 性能:相同类型的组件(例如所有的 PositionComponent)可以被紧凑地存放在连续的内存中,当系统(如 PhysicsSystem)遍历它们时,可以极大地利用CPU缓存,避免因内存随机访问导致的“缓存未命中”(Cache Miss)
    • 并行化:由于系统是无状态的,且操作的是连续的数据块,因此非常容易将计算任务(例如更新一半实体的位置)分配给多个CPU核心并行处理
    • 解耦:组件和系统高度独立,易于测试、维护和复用。你可以轻松地在不同项目中复用一个写好的 PhysicsSystem
    • 灵活性:组合而非继承,对象的行为由它所拥有的组件组合来决定。
  24. 缓存行对齐是什么,为什么要对齐,C++中怎么做,没有对齐会怎样

    缓存行 (Cache Line) 是CPU缓存和主内存之间数据传输的最小单位,在现代CPU上通常是 64字节,CPU读取数据时,并非一个字节一个字节地读,而是一次性读取一整个缓存行。

    • 如果数据是对齐的:一个完整的数据(例如一个8字节的 long long 或一个自定义的 struct)会被完整地放在一个缓存行内。CPU只需要一次内存访问,就能把整个数据读入缓存,效率很高。
    • 如果数据没有对齐:一个数据可能“跨”在两个缓存行的边界上。例如,一个8字节的数据,前4个字节在第一个缓存行的末尾,后4个字节在第二个缓存行的开头。这时,CPU为了读取这一个数据,必须发起两次内存访问,分别读取这两个缓存行,然后将结果拼接起来。这个过程的开销远大于一次访问。

    C++11以后可以使用alignas进行对齐,如果没有对齐,可能会出现多次访存,伪共享的情况。

    伪共享多线程环境中常见的问题,例如两个线程在不同的CPU核心上运行,它们分别访问不同的变量,但这两个变量碰巧位于同一个缓存行上,当核心1修改了它的变量A,缓存一致性协议会使核心2上包含变量B的整个缓存行失效,即使变量B本身并未被修改。核心2下次访问变量B时,必须重新从内存加载,反之亦然。所以会导致两个核心会不停地使对方的缓存行失效,性能急剧下降,通过缓存行对齐,将两个遍历强制放在不同的缓存行,避免伪共享。

  25. 观察者模式是什么,还用过什么设计模式

    1. 观察者模式(行为设计模式):一个对象(被称为 “主题” (Subject) 或 “可观察者” (Observable))维护一个依赖它的对象列表(被称为 “观察者” (Observers) )。当主题的状态发生任何变化时,它会自动通知所有观察者,观察者们随后会自动进行更新。就像视频平台的“订阅”功能。视频UP主(主题)发布了新视频(状态变更),平台会自动通知所有订阅了该UP主的用户(观察者)。UP主不需要知道每个订阅者的具体信息。

      创建观察者接口 (Observer Interface)

      • 定义一个抽象基类或接口,其中包含一个纯虚函数,例如 virtual void update() = 0;。所有具体的观察者都必须实现这个接口。

      创建具体观察者 (Concrete Observer)

      • 创建若干个具体类,继承自观察者接口。
      • 在这些类中,实现 update() 方法。当主题通知它们时,这个方法里的逻辑就会被执行。

      创建主题类 (Subject)

      • 在主题类内部,维护一个用于存储观察者指针的容器,例如 std::vector<IObserver*>

      • 实现三个关键的公共方法:

        • void attach(IObserver* observer):将一个观察者添加到容器中(订阅)。
        • void detach(IObserver* observer):从容器中移除一个观察者(取消订阅)。
        • void notify():遍历容器,并调用其中每一个观察者的 update() 方法。

      在主题状态变化时调用通知

      • 在主题类中任何会改变其自身状态的方法里,在状态改变后,调用自身的 notify() 方法。这样就能确保所有观察者都得到及时的通知。
    2. 命令模式(行为设计模式):将一个请求封装成一个独立的对象,包含了执行某个特定操作所需的所有信息,例如:要执行的动作、接收该动作的对象(被称为“接收者”),以及动作所需的参数,使得请求的发送者(“调用者”)和请求的接收者(执行者)之间就被完全解耦,常用与车小雨重做,请求排队,日志记录,命令组合,参数化对象

      Command 接口

      • 定义一个所有命令类都必须实现的接口,通常只包含一个方法,如 virtual void execute() = 0;

      Concrete Command (具体命令)

      • 实现 Command 接口。它内部持有一个接收者(Receiver) 的引用。
      • 它的 execute() 方法会调用接收者对应的方法来完成实际的工作。

      Receiver (接收者)

      • 知道如何执行与请求相关的具体操作。任何类都可以作为接收者,它包含了真正的业务逻辑。

      Invoker (调用者)

      • 持有一个 Command 对象,并在需要时调用该命令的 execute() 方法。
      • 关键点:调用者完全不知道接收者的任何信息,它只与抽象的 Command 接口交互。
  26. 如果可用物理内存剩余量不足,只剩10m,而代码中去new一个20M的内存,能否new成功

    大概率可以 new成功,因为程序操作的是虚拟内存,而非物理内存,而new的目标是在像OS申请20M的连续虚拟内存空间,不是物理空间,之后会根据页面交换机制处理.

    • 分配虚拟地址:如果虚拟地址空间足够,操作系统会先答应你的请求,在进程的虚拟地址空间中划出 20M 并标记为“已分配”。此时,这块虚拟内存可能还没有任何物理内存与之对应。
    • 缺页中断与映射:当你第一次尝试写入这块 20M 内存的某个地址时,CPU会发现这个虚拟地址没有对应的物理内存,这会触发一个缺页中断 (Page Fault)
    • OS 的响应:操作系统接管中断后,会为你分配一页物理内存(通常是4KB),并建立虚拟地址到物理地址的映射。
    • 物理内存不足时:如果此时像你所说,可用物理内存不足(只剩10M),操作系统会启动页面交换机制。它会找到物理内存中一些“不常用”的数据页(可能属于你的进程,也可能属于其他进程),将它们写入到硬盘的一个特殊区域(称为页面文件或交换空间, Swap Space) ,从而腾出物理内存空间。
    • 完成分配:腾出的物理内存就可以用来满足你当前的写入需求了。这个过程对你的程序是完全透明的。

    只有当虚拟地址空间耗尽,或者总可用内存(RAM + Swap)也耗尽时,new 才会失败并抛出 std::bad_alloc 异常

  27. 组合和继承的区别,怎么实现的组合

    优先使用组合,而不是继承,只有在确实需要表达 “is-a” 关系并利用多态时,才应首先考虑继承,通过在一个类中,包含另一个类的对象作为其成员变量来实现。

  28. 什么是RVO

    Return Value Optimization,返回值优化,是一种 C++ 编译器优化技术,旨在消除函数按值返回时,因创建临时对象而产生的拷贝或移动开销。正常情况下,函数返回一个对象时,会在函数内部创建一个局部对象,然后创建一个临时对象,并用局部对象初始化它,在函数外部,用这个临时对象初始化最终变量.RVO 则会省去中间步骤:编译器足够智能,它会直接跳过中间临时对象的创建,直接在调用方的目标内存位置上构造最终的返回对象。

    补充:

    • NRVO (Named RVO) :特指对函数内返回的具名局部对象进行的返回值优化。
    • C++17 的强制省略:自 C++17 起,在某些特定情况下(主要是返回纯右值时),这种拷贝/移动省略不再是可选的优化,而是语言标准强制规定的,保证了零开销的返回。
  29. std::ref

    std::ref 是一个函数模板,它的作用是将一个对象包装成 std::reference_wrapper 类型

    std::reference_wrapper 是一个可拷贝、可赋值的对象,它内部存储了对原始对象的引用。你可以把它理解为一个“行为像引用,但本质是对象”的东西。C++ 中某些模板函数(尤其是旧式的)在传参时会强制按值拷贝,导致参数的引用属性丢失。std::ref 就是为了解决这个问题,当你直接传递一个引用 & 给这类函数时,函数拷贝的是引用所指向的对象本身,而不是引用。但如果你用 std::ref 包装它,函数拷贝的是 reference_wrapper 这个包装对象,而这个包装对象内部忠实地保存了对原始对象的引用。这样,在函数内部通过这个包装对象操作时,就能间接地修改原始对象。

    例如:

    1. std::thread 构造函数

      • std::thread 的构造函数会拷贝它后面的所有参数,作为新线程的启动参数。如果你希望新线程通过引用操作主线程的某个变量,而不是操作它的一个拷贝,就必须用 std::ref 将该变量包起来。
    2. std::bind

      • std::bind 在绑定参数时,默认也是按值拷贝。如果你希望绑定的是一个对象的引用,而不是它的拷贝,也必须使用 std::ref
  30. 为什么C++ 中某些模板函数(尤其是旧式的)在传参时会强制按值拷贝?

    1. 生命周期安全 (Lifetime Safety)

      这是最关键的原因,例如thread和bind。std::thread 创建的新线程和 std::bind 生成的函数对象,它们的生命周期很可能与原始参数的生命周期不同步

      • 对于 std::thread: 假设 std::thread 的构造函数按引用接收参数。如果你传递了一个局部变量的引用,那么在主线程函数返回、局部变量被销毁后,新线程内部持有的就是一个悬挂引用 (dangling reference) 。访问它会导致未定义行为。 通过按值拷贝std::thread 将参数的副本安全地存储起来,这个副本的生命周期与新线程绑定,从而保证了线程启动后访问的数据是有效的。
      • 对于 std::bind: 同样,std::bind 创建的函数对象可能在很久以后才被调用。如果它只保存了原始参数的引用,那么在调用时原始参数可能早已不存在。通过拷贝,它保证了自己持有的数据在被调用时依然有效。
    2. 通用性 (Generality)

      函数模板的设计目标之一是能处理尽可能广泛的类型。

      • T param (按值传递) 这个签名可以接受任何类型的参数:左值(会被拷贝)、右值(会被移动)、字面量、临时对象等。
      • T& param (按左值引用传递) 这个签名不能接受右值(如临时对象或字面量),会大大限制函数的可用性。
      • const T& param (按常量左值引用传递) 虽然能接受右值,但参数变成了只读,函数内部无法对其进行修改或移动,也限制了使用场景。

      因此,T param 是最通用、最简单的签名,它把“如何创建参数副本”这个问题交给了类型的拷贝/移动构造函数去解决。

    3. C++11 移动语义的优化

      在 C++11 之后,按值传递的潜在性能问题得到了极大缓解。如果传递一个右值(临时对象),参数会通过移动构造而非拷贝构造来初始化,开销非常小。这使得“按值传递”成为一个既安全又高效的常用模式。

    总结:强制按值拷贝主要是为了解耦参数的生命周期,避免悬挂引用,同时提供最大的API通用性。而移动语义则为这种设计提供了性能保障。