C++11
1. C++11新特性
- 自动类型推导(auto)
- 智能指针(提供更安全和更高效的内存管理)
- 移动语义和右值引用 (move语义 &&,使得对象移动而非拷贝,在处理大量数据时提高程序性能)
- Lambda 表达式(允许在代码中定义匿名函数对象)
- 多线程支持(thread,mutex)
- 增加新容器和算法
2. 智能指针
用于管理动态分配的对象的生命周期,自动释放内存,避免内存泄漏和悬挂指针等问题.
常用的智能指针有unique_ptr(独占,不能复制,只能移动)、shared_ptr(共享,多个ptr可指向同一个对象) 和 weak_ptr(用于观察)。
-
std::unique_ptr:提供对对象的唯一拥有权,具有简单的内存管理机制,支持对象的自动释放。底层使用原始指针和移动构造。
独占空间:std::unique_ptr 提供了对动态对象的唯一拥有权,确保对象在 std::unique_ptr 被销毁时自动释放内存。 转移所有权:std::unique_ptr 支持通过移动构造函数和移动赋值操作符将所有权从一个 std::unique_ptr 转移到另一个,源 std::unique_ptr 被置为空。
-
std::shared_ptr:支持多个智能指针共享同一个对象,通过引用计数机制管理对象的生命周期。底层使用控制块来存储引用计数。
std::shared_ptr 的引用计数机制通过控制块和两个计数器(强引用计数和弱引用计数)来管理对象的生命周期。 强引用计数确保对象在所有 std::shared_ptr 实例销毁后才会被释放,而弱引用计数则用于 std::weak_ptr 的观察和防止循环引用。 强引用计数:在 std::shared_ptr 的构造函数中初始化为 1。当一个新的 std::shared_ptr 通过复制或赋值操作创建时,强引用计数增加;当一个 std::shared_ptr 被销毁时,强引用计数减少。如果强引用计数减少到 0,对象会被销毁,控制块也会被释放(如果弱引用计数也为 0)。 弱引用计数:在创建 std::shared_ptr 时,弱引用计数初始化为 0。每当一个 std::weak_ptr 创建时,弱引用计数增加;每当一个 std::weak_ptr 被销毁时,弱引用计数减少。弱引用计数用于管理 std::weak_ptr 实例,但不影响对象的生命周期。
-
std::weak_ptr:用于观察 std::shared_ptr 管理的对象,避免循环引用,使用锁定机制访问对象。
3. 左值引用、右值引用
-
左值和右值
左值:
- 定义:左值(Locator Value)表示内存中的一个具体位置,可以获取其地址。它是表达式中表示对象的持久性位置。
- 特征:可以出现在赋值语句的左边,即
=
运算符的左边。 - 示例:
int x = 10; // x 是一个左值 x = 20; // x 依然是左值 右值:
- 定义:右值(Read Value)表示一个临时值,通常没有持久的内存位置。它通常表示一个计算结果或者常量。
- 特征:不能出现在赋值语句的左边,即 = 运算符的右边。右值通常是临时的,不可以取得地址。
- 示例:
int x = 10; // 10 是一个右值 x = x + 5; // x + 5 是一个右值
-
左值引用和右值引用
左值引用:
- 定义:左值引用是传统的引用类型,用于绑定左值。
- 语法:使用
T&
。 - 特征:可以绑定到左值,但不能绑定到右值。
- 示例:
int x = 10; int& ref = x; // ref 是一个左值引用,绑定到 x 右值引用:
- 定义:右值引用是 C++11 引入的引用类型,用于绑定右值。
- 语法:使用
T&&
。 - 特征:可以绑定到右值,但不能绑定到左值。主要用于实现移动语义和完美转发。
- 示例:
int&& rref = 20; // rref 是一个右值引用,绑定到右值 20
4. move移动语义
将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制,对象的移动语义需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)
假如我们有两个指针 一个指针A,一个指针B,指针A指向了一个很复杂的内容,此时我们需要指针B指向这个很复杂的内容,之后我们就不需要指针A了,它可以滚蛋了,可以析构掉了,这个就是移动语义,结果就是将原来指针A指向的内存交给了指针B,指针A不指向任何内存。相当于B偷走了A的东西。
相对的有移动语义就有复制语义,复制语义就是B指针要想获得同样的内容就会发生拷贝,大部分都是深拷贝(浅拷贝,深拷贝有机会我会补上一篇博客的),结果就是指针A指向一片内存,指针B指向了另一片内存,但两片内存中存储的内容是相同的,大大的浪费性能。
5. forward完美转发
完美转发是指在模板函数中将传递给函数的参数以原样的值类别(左值或右值)转发给另一个函数。完美转发的目标是避免不必要的复制或移动操作,同时保持参数的值类别。
高效传参技术
-
实现方式
-
使用模板函数:定义一个模板函数来接收参数。
-
使用 std::forward:将参数转发给另一个函数,保持其值类别。
std::forward 是一个标准库函数模板,主要用于完美转发。 它的作用是根据参数的值类别(左值或右值)转发参数,从而确保转发过程中不会发生不必要的复制或移动。
-
6. std::move 和 std::forward
- std::move:用于将左值强制转换为右值引用,从而允许移动语义。它不保留参数的值类别,仅用于移动。
- std::forward:用于在模板中完美转发,保持参数的原始值类别(左值或右值),确保转发过程中不会改变值类别。
7. C++11中nullptr和NULL的区别
-
类型安全:nullptr 提供更好的类型安全性,相比 NULL 可以避免类型混淆。
-
定义:nullptr 是 C++11 的新关键字,NULL 是 C 语言中的宏。
-
兼容性:nullptr 只能用于指针类型,而 NULL 可以是整型常量,可能导致不明确的类型推断。
8. C++中两种自动类型推导
auto关键字告诉编译器根据变量的初始化表达式自动推导变量的类型。这在声明变量时非常有用,特别是当你不确定变量的确切类型,或者类型比较复杂难以书写时。
decltype关键字允许你根据一个表达式的类型来定义一个新的类型名。
decltype(auto)允许你在声明变量的同时推导出变量的类型,并且保留表达式的CV限定符(const和volatile限定符)以及引用类型。这对于确保类型正确性和避免不必要的类型转换特别有用。
9. atomic底层实现机理
Atomic(原子操作) 是指一种操作在执行的过程中不会被中断或干扰。它确保在并发的环境中,一个操作要么全部完成,要么根本不执行,从而避免竞态条件(race condition)的发生。
在多线程编程中,多个线程可能会同时操作同一个数据。为了避免不同线程对共享数据的操作出现冲突,必须确保某些操作是原子的,意思是这些操作不能被分割或中途中断。
C++11 的 std::atomic 概述:
std::atomic 是 C++11 提供的一个模板类,用于在多线程环境中安全地操作共享数据而无需显式使用互斥锁(如 std::mutex)。它确保对变量的读写操作是原子的,避免竞态条件(race condition)的发生。
-
类型
- 基本类型:int, bool, char等
- 指针类型:int*
- 具备无锁特性的自定义类型(需满足特定的条件,如可拷贝、可移动等)
-
内存序模型
C++11 的原子操作允许开发者指定内存序,以控制操作之间的可见性和顺序性。
-
常见原子操作
-
底层实现原理
C++11 的 std::atomic 底层实现依赖于现代处理器提供的原子操作指令和内存屏障,结合编译器对这些硬件特性的支持。它通过直接与硬件交互,保证了多线程环境下对共享数据的并发访问的原子性。
-
硬件原子操作指令
现代 CPU 架构都提供硬件支持的原子指令,用于实现 std::atomic。这些指令可以确保在多处理器环境中对共享内存的操作不会出现竞争条件(race condition),即不会在其他处理器中间插入对该内存地址的访问
-
内存屏障
内存屏障(又称为内存栅栏)用于确保指令的执行顺序。
不同的硬件架构和编译器可能会对程序指令进行乱序执行(out-of-order execution)以优化性能。内存屏障可以防止这种乱序执行,确保某些关键操作按预期顺序执行。
-
缓存一致性
现代多核处理器使用缓存来加速数据访问,但这也带来了缓存一致性问题。为了确保多线程对共享变量的一致性,处理器使用缓存一致性协议(如 MESI 协议)来同步不同核的缓存。
原子操作会触发缓存一致性协议,确保某个内存地址的更新会被所有处理器核立即看到。
-
编译器优化
C++11 的 std::atomic 实现依赖于编译器提供的内建函数(intrinsic functions)来生成适当的硬件原子指令和内存屏障。
-
10. shared_ptr计数线程安全怎么实现
在 C++ 中,std::shared_ptr 是一种智能指针,它通过引用计数(reference counting)来管理对象的生命周期。每次 shared_ptr 被复制、赋值或销毁时,引用计数会相应增加或减少,当引用计数变为零时,shared_ptr 所指向的对象会被自动销毁。
11. 为什么引入右值、纯右值、将亡值
-
右值 是一种短暂存在的值,它分为纯右值(临时值、字面量)和将亡值(即将销毁的对象)。
-
引入这些值类别的目的是为了优化性能,通过移动语义避免不必要的拷贝。
-
右值引用(T&&) 和 将亡值(xvalue) 支持开发者在处理临时对象时转移资源,降低内存开销。
-
这些改进使得 完美转发 和其他高级技术成为可能,提升了 C++ 的表达能力和执行效率。
右值及其分类的引入使得 C++ 程序可以更精细地管理对象生命周期和资源,从而在性能和安全性上都得到了显著的提升。
12. 对一个const容器使用move会发生什么
对 const 容器使用 std::move 时,虽然 std::move 将容器转换为右值引用,但由于 const 限制,移动操作无法改变容器的状态,因此不会真正发生移动。结果是,会回退到拷贝操作,即对 const 容器进行拷贝构造或拷贝赋值。
13. 传参能否传const&&,会发生什么
14. unordered_map如何将一个大数映射到bucket数量上
-
unordered_map 使用哈希函数将大数(或任何类型的键)转换为一个哈希值。
-
然后通过 hash_value % bucket_count 的方式将这个大数映射到哈希表中的桶。
-
如果发生冲突,unordered_map 使用链地址法(通常是链表)来存储冲突的键值对。
-
在负载过高时,unordered_map 会自动进行重哈希以保持性能。
15. 对vector的earse方法具体怎么实现的
std::vector 是 C++ 标准库中的动态数组类,提供了许多方法来操作其元素。erase 是 std::vector 中用于删除元素的方法。这个方法的核心是在保持动态数组连续性的情况下删除元素,并重新调整容器的大小。理解 erase 的实现过程需要了解动态数组的特性以及如何在数组中删除元素并维护顺序。
-
erase 方法用于删除 std::vector 中的一个或多个元素。
-
删除操作通过将删除位置之后的元素向左移动来保持数组的连续性。
-
单元素删除和多元素删除的核心逻辑相似,区别在于是否批量移动。
-
erase 方法具有线性时间复杂度,且返回删除位置后第一个元素的迭代器。