【计算机基础理论知识】C++篇(二)
【计算机基础理论知识】C++篇(二)
🔥个人主页:大白的编程日记
🔥专栏:计算机基础理论知识
文章目录
- 【计算机基础理论知识】C++篇(二)
- 前言
- 左值&&右值&&左值引用&&右值引用
- 左值&&右值
- 左值引用和右值引用
- 移动语义&&引用折叠&&万能引用&&完美转发
- 移动语义&&引用折叠&&万能引用&&完美转发
- 为什么有移动语义
- 移动语义的实现
- 引用折叠
- 万能引用
- 完美转发
- 多态的原理 && 虚表
- 内存对齐
- 对齐规则
- 为什么要内存对齐?
- STL六大组件
- C++11以来有哪些新特性,标准库增加了什么新功能?
- 后言
前言
哈喽,各位小伙伴大家好!今天我们来讲一下【计算机基础理论知识】C++篇(二)。话不多说,我们进入正题!向大厂冲锋
左值&&右值&&左值引用&&右值引用
左值&&右值
- 左值是一个表达式,它表示一个持久存储的对象
- 可以被取地址(
&
)操作符操作,并且可以出现在赋值运算符的左边和右边。 - 例如:普通变量
int a
、数组元素arr[0]
、解引用的指针*p
。
- 可以被取地址(
- 右值是一个表达式,它表示一个临时的、不可持久的对象,不能被取地址,
- 通常出现在赋值运算符的右边,而不能在赋值运算符的左边。
- 通常是一些字面量或表达式求值和函数返回值创建的临时对象。因此右值通常具有常性,不能修改。
- 例如:字面量
10
、3.14
、"hello"
,函数返回值fun()
,表达式的结果a + b
。
特性 | 左值 | 右值 |
---|---|---|
存储位置 | 持久存储位置(可以取地址) | 临时存储位置(不能取地址) |
是否可赋值 | 可以出现在赋值运算符左边 | 通常出现在赋值运算符右边 |
是否可取地址 | 可以被取地址操作符 & 操作 | 不能被取地址操作符 & 操作 |
常见例子 | 变量名、数组元素、解引用的指针 | 字面量、函数返回值、求值表达式结果 |
左值引用和右值引用
- 左值引用可以引用左值
int a = 10; int& ref = a;
const左值引用可以引用右值const int& red=10;
通常用于函数参数传参和非局部对象返回值返回减少拷贝 - 右值引用可以引用右值
int&& rref=10;
右值引用可以引用move移动后的左值
move是一个万能引用类模版 原理就是强转
int a=10; int&& ref=move(a);
用于解决局部对象无法左值引用返回的拷贝效率问题 右值通常是即将销毁的对象 可以结合移动语义进行资源的转移 - 左值引用和右值引用的属性都是左值
移动语义&&引用折叠&&万能引用&&完美转发
- 为什么有移动语义
- C++中复制对象可能会带来巨大的性能消耗 特别是自定义类型对象的深拷贝复制
- 为了减少这种开销 C++11后引入了移动语义 允许我们在对右值拷贝时移动资源而不是复制他
- 因为右值都是即将要销毁的对象 所以我们可以通过交换指针来掠夺他的资源
- 同时这也就是为什么右值引用的属性是左值 因为是右值我们就无法需修改指针进行掠夺!
- 移动语义的实现
通过移动构造和移动赋值接收右值引用参数 将右值的资源进行交换 掠夺资源 - 引用折叠
- 当出现引用的引用 由引用折叠的规则确定多个引用折叠后的类型
- 只能通过
typedef
和模版实现 不能显示定义引用的引用 - 只有当右值引用的右值引用折叠后才是右值引用 其他都是左值引用
- 万能引用
- 由于引用折叠的存在 右值引用T&&的模版参数既可以匹配左值 也可以匹配右值 下面以int类型举例
-当传入左值时T
被推导为int&
引用折叠后 变为左值引用匹配左值 - 当传入右值时
T
被推导为int
引用折叠后 变为右值引用匹配右值
- 由于引用折叠的存在 右值引用T&&的模版参数既可以匹配左值 也可以匹配右值 下面以int类型举例
移动语义&&引用折叠&&万能引用&&完美转发
为什么有移动语义
C++中复制对象可能会带来巨大的性能消耗,特别是自定义类型对象的深拷贝复制。为了减少这种开销,C++11后引入了移动语义,允许我们在对右值拷贝时移动资源,而不是复制它。因为右值都是即将要销毁的对象,所以我们可以通过交换指针来掠夺它的资源。
- 同时,这也就是为什么右值引用的属性是左值,因为是右值我们就无法修改指针进行掠夺!
移动语义的实现
通过移动构造和移动赋值接收右值引用参数,将右值的资源进行交换,掠夺资源。
引用折叠
当出现引用的引用时,由引用折叠的规则确定多个引用折叠后的类型。只能通过typedef和模板实现,不能显示定义引用的引用。
只有当右值引用的右值引用折叠后才是右值引用,其他都是左值引用。
万能引用
由于引用折叠的存在,右值引用 T&&
的模板参数既可以匹配左值,也可以匹配右值。下面以 int
类型举例:
- 当传入左值时,
T
被推导为int&
,引用折叠后变为左值引用,匹配左值。 - 当传入右值时,
T
被推导为int
,引用折叠后变为右值引用,匹配右值。
template<typename T>void
func(T&& arg)
{ // T&& 是一个通用引用if constexpr (std::is_lvalue_reference_v<T>) {
}
完美转发
- 因为右值引用的属性也是左值,所以在模板编程中我们希望确保参数能保持原来的属性转发给其他函数
- 左值就作为左值转发,右值就作为右值转发。
- 可是右值引用为了能进行交换修改,属性是左值。所以当万能引用接收右值参数时,此时向下传递参数时属性变为左值。
- 所以我们可以在转发时通过
forward
完美转发模板保持参数属性。
通过参数 T 推导出参数是左值还是右值:
-
T
是int
,说明是右值,强转为T&&(int&&)
返回。 -
T
是int&
,说明是左值,强转为T&&
,引用折叠为int&
。 -
问题:强转为右值引用后属性不还是左值吗?
虽然强转以后还是右值引用,属性还是左值,但是转发时会匹配右值版本的函数。可以认为是规定,这样就可以保持参数的属性转发了!
template<typename T>constexpr T&& forward(typename std::remove_reference<T>::type& t)
noexcept {return static_cast<T&&>(t);}
多态的原理 && 虚表
多态分为 静态多态 和 动态多态
- 静态多态:在 编译期间 决定函数调用的绑定方式,主要通过 函数重载 和 函数模板 实现
-
函数重载:同一作用域可以定义多个 函数名相同、参数列表不同(参数个数、参数顺序、参数类型)的函数。
编译器在 编译时 根据调用时传入的参数决定调用哪一个函数。
-
函数模板:通过 模板 编写泛型函数。
编译器根据传入的参数决定要生成哪一个函数实例。
- 动态多态:在 程序运行期间 确定函数调用的绑定方式,主要通过 虚函数 和 继承 实现
-
条件:
- 满足 继承关系
- 派生类 重写 基类的虚函数(函数名和参数列表都相同)
- 使用 基类的指针或引用 调用虚函数
-
虚函数表(虚表):
- 编译器会为每个声明了虚函数的类生成一个 虚函数表(本质是一个 函数指针数组),里面存放着该类的虚函数地址。
- 派生类的虚函数表会包含:
- 从父类继承的虚函数
- 自己新增的虚函数
- 如果派生类 重写了 基类的虚函数,虚函数表中对应的函数地址会被替换为派生类的实现。
-
虚函数表指针(vptr):
- 每个对象都有一个隐藏的指针成员,称为 虚函数表指针。
- 编译器会将该指针指向对象所属类的虚函数表。
-
动态多态的实现过程:
当通过 基类指针或引用 调用虚函数时,运行时会:
- 通过对象的
vptr
找到对应的虚函数表 - 在虚函数表中查找对应的函数地址
- 调用实际的函数,实现 “指向谁,调用谁” 的动态绑定
- 通过对象的
内存对齐
对齐规则
对齐数
- 结构体的成员变量都要对齐到 偏移量为对齐数的整数倍
- 对齐数 =
min(编译器默认对齐数(VS默认为8),该结构体成员大小)
- 修改对齐数:
#pragma pack(对齐数)
最大对齐数
- 结构体的 总大小 必须为所有成员中 最大对齐数的整数倍
结构体嵌套
- 如果结构体嵌套,嵌套的结构体要对齐到自己所有成员的 最大对齐数
- 整个结构体的大小就是包含嵌套结构体所有成员 最大对齐数的整数倍
为什么要内存对齐?
内存对齐不是在浪费空间吗?
平台硬件限制 && 可移植性
- 不是所有硬件平台都能访问任意地址上的任意数据
- 某些平台只能在特定地址开始读取数据,否则抛出 硬件异常
效率问题
- CPU 访问内存是以 字长 为单位,而不是以字节为单位:
- 32 位机器:字长为 4 字节
- 64 位机器:字长为 8 字节
- 如果变量地址没有对齐,可能需要 多次访问 才能读取完整变量
- 内存对齐可以减少 CPU 访问内存的次数,提高效率!
STL六大组件
1.容器:用于存储和管理数据 STL提供了多种类型的容器
-
序列容器:元素按顺序存储 可以通过索引访问
vector
:动态数组,支持随机访问,中间插入删除需要挪动数据,O(N)复杂度,效率低list
:双向链表,支持快速插入和删除,不需要挪动数据,时间复杂度O(1),但是不支持随机访问array
:固定大小的数组,支持随机访问forward_list
:单向链表,支持快速插入和删除操作
-
关联式容器:元素按特定顺序存储,通常基于平衡二叉树实现
set
和multiset
:有序键集合,set
存储唯一键元素,multiset
支持存储重复键元素,底层使用红黑树实现unordered_set
和unordered_multiset
:无序键值对映射,支持唯一键和重复键,底层使用哈希表实现map
和multimap
:有序键值对集合,支持唯一键和重复键,底层使用红黑树实现unordered_map
和unordered_multimap
:无序键值对映射,支持唯一键和重复键,底层使用哈希表实现
2.迭代器:用于遍历容器的元素 提供一种统一的方式访问容器 底层就是仿函数 所有容器都支持迭代器
迭代器可以分为以下几类:
- 输入迭代器:只能向前移动,用于读取数据,支持
++
- 输出迭代器:只能向前移动,用于写入数据,支持
++
- 前向迭代器:只能前向遍历,支持
++
,forward_list
容器 - 双向迭代器:可以向前遍历,也可以向后遍历,支持
++
、--
,list
、vector
、deque
等容器 - 随机迭代器:支持随机访问,即可以像数组下标一样跳转到容器的任意位置,支持
++
、--
、+
、-
,vector
、deque
容器的底层结构决定和迭代器的性质,例如
forward_list
是单链表所以无法找到上一个节点,就只能是前向迭代器;vector
是数组空间连续支持随机访问,所以就是随机迭代器。而迭代器的性质决定了可以使用那些算法,例如快排要求支持随机迭代器,因为快速排序需要支持下标随机访问。
3.算法:STL中提供了对容器的数据进行操作的函数模版可以应用于任何支持对应迭代器的容器
包括:排序(sort
)、查找(find
)、变换(transform
)、复制(copy
)、删除(remove
)、替换(replace
)、反转(reverse
)
4.仿函数:实现了operator()
的类对象可以像函数一样调用通常用于在算法中作为参数 自定义算法的行为 例如sort
中可以传一个仿函数自定义排序顺序
常见仿函数有:
std::equal_to<T>
:检查两个值是否相等。std::not_equal_to<T>
:检查两个值是否不相等。std::greater<T>
:检查第一个值是否大于第二个值。std::less<T>
:检查第一个值是否小于第二个值。std::greater_equal<T>
:检查第一个值是否大于或等于第二个值。std::less_equal<T>
:检查第一个值是否小于或等于第二个值。
5.适配器:为现有的容器、迭代器或函数对象提供不同的接口。
适配器用于改变现有容器迭代器或函数对象的行为,STL中提供几种类型的适配器:
- 容器适配器:
stack
:后进先出的栈queue
:先进先出的队列priority_queue
:优先级队列- 函数适配器:
bind
:绑定函数和参数,主要用于改变参数个数和顺序mem_fn
:调用成员函数
6. 分配器:管理内存分配和释放的组件
-
STL使用new和delete操作符管理内存
-
但是也可以通过自定义分配器以适应特定的需求
-
C++标准库提供了一个默认的分配器
std::allocator
,它是一个模板类 -
如果需要对内存分配进行更精细的控制,可以自定义分配器。
C++11以来有哪些新特性,标准库增加了什么新功能?
- 自动类型推导(
auto
):允许编译器推导变量的类型,使代码更加简洁。 - 范围
for
:遍历所有容器,简化对容器的遍历,底层就是迭代器,所有容器都支持迭代器,所以所有容器都支持范围for
。 - 智能指针:引入了
std::shared_ptr
和std::unique_ptr
等智能指针,用于管理动态分配的内存,帮助防止内存泄漏。 - Lambda 表达式:允许在函数内部定义匿名函数,可以捕捉外部变量并且修改,提高代码可读性和灵活性。
nullptr
:引入了空指针常量nullptr
,用于替代传统的空指针NULL
。避免NULL
字面量为0
默认为整形,但是可以隐式类型转换为void*
类型转换导致类型不匹配问题。nullptr
表示为空指针,隐式转换为任何指针类型,但不能转为整形,可以保证类型安全,增强代码可读性。- 强制类型转换(Type Casting):引入了
static_cast
、dynamic_cast
、const_cast
、reinterpret_cast
等更安全和灵活的类型转换操作符。 - 右值引用和移动语义:支持通过右值引用实现移动语义,提高了对临时对象的处理效率,解决局部变量无法传引用返回的拷贝问题,提高效率。
- 新的容器和算法:引入了新的容器,如
std::unordered_map
、std::unordered_set
,以及所有容器都加上移动构造和移动赋值,以及容器的std::initializer_list
构造。
后言
这就是【计算机基础理论知识】C++篇(二)。大家自己好好消化!今天就分享到这!感谢各位的耐心垂阅!咱们下期见!拜拜~