欢迎来到Cefler的博客😁
🕌博客主页:那个传说中的man的主页
🏠个人专栏:题目解析
🌎推荐文章:题目大解析(3)
目录
- 👉🏻 统一的列表初始化
- 👉🏻 声明
- auto
- decltype
- nullptr
- 👉🏻范围for循环
- 👉🏻智能指针
- 👉🏻左右值引用和移动语义
- 左值引用和右值引用认识
- 左值引用与右值引用比较
- move函数
- 右值引用的移动语义
- 👉🏻完美转发
👉🏻 统一的列表初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
👉🏻 声明
auto
auto
是 C++11 引入的关键字,用于进行自动类型推导。使用 auto
关键字可以让编译器自动推断变量的类型,而无需显式指定。这使得代码更简洁、可读性更强,并且有助于提高代码的灵活性。
使用 auto
的主要优势在于简化代码,特别是在涉及复杂的类型或模板时,可以避免手动书写冗长的类型声明。此外,auto
的使用还使得代码更容易维护,因为类型信息是根据初始化表达式自动生成的。
在使用 auto
时需要注意一些事项,例如:
-
自动类型推导:
auto
关键字根据变量的初始化表达式推导其类型。因此,如果初始化表达式类型发生变化,auto
推导的类型也会相应变化。 -
类型推导规则:
auto
会忽略顶层的const
和引用,而保留底层const
。这意味着auto
推导的类型可能不同于直接指定的类型。 -
避免过度使用: 尽管
auto
可以简化代码,但过度使用可能会降低代码的可读性。在一些情况下,显式指定类型可能更有利于代码的清晰性。
总体来说,auto
是 C++ 中的一项强大特性,能够简化代码并提高代码的灵活性。在使用时需要权衡简洁性和可读性,确保代码清晰易懂。
decltype
关键字decltype将变量的类型声明为表达式指定的类型。
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
👉🏻范围for循环
范围for
循环(Range-based for loop)是 C++11 引入的一种循环结构,它用于遍历容器(如数组、std::vector
、std::list
等)中的元素,提供了一种更为简洁和直观的方式。范围for
循环的语法如下:
for (declaration : range) {
// 循环体
}
其中,declaration
是一个变量声明,range
是一个可迭代的范围,例如容器。
举例来说,假设有一个整数数组,使用范围for
循环遍历该数组:
#include <iostream>
int main() {
int numbers[] = {1, 2, 3, 4, 5};
// 范围for循环遍历整数数组
for (int num : numbers) {
std::cout << num << " ";
}
return 0;
}
在这个例子中,int num
是在每次循环中声明的变量,而numbers
是要遍历的整数数组。
范围for
循环的特点和优势包括:
-
简洁: 语法相对传统的
for
循环更为简洁,避免了索引的繁琐操作。 -
类型安全: 循环变量的类型由编译器自动推导,避免了类型错误。
-
容器遍历: 可以方便地遍历容器中的元素,包括数组、STL 容器等。
-
避免越界: 由于遍历的是容器的元素而不是索引,减少了越界的风险。
需要注意的是,范围for
循环并不总是适用于所有情况,例如需要修改容器中的元素或需要访问元素的索引时,传统的for
循环可能更为合适。
👉🏻智能指针
这个我们后续再讲。
👉🏻左右值引用和移动语义
左值引用和右值引用认识
左值(lvalue)和右值(rvalue)是表达式的两种基本分类,而左值引用和右值引用是与这两种表达式相关的引用类型。
- 左值(lvalue): 左值是一个具有标识符的表达式,可以取地址,即有确定的内存位置。通常来说,
变量
、数组元素
、对象成员
等都是左值。
对于左值,我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边
int x = 42; // x 是左值
- 右值(rvalue): 右值是一个没有标识符、即将被销毁或没有确定内存位置的表达式。通常来说,
字面常量
、表达式返回值
,函数返回值
(这个不能是左值引用返回)等都是右值。
对于右值,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
int result = 2 + 3; // 2 + 3 是右值
左值和右值的区分主要体现在它们的身份和生命周期上。
对应的,C++中引入了左值引用(lvalue reference)和右值引用(rvalue reference):
-
左值引用(lvalue reference): 左值引用绑定到左值,使用
&
表示。int x = 42; int &ref = x; // ref 是左值引用,绑定到左值 x
左值引用主要用于传递可修改的左值参数或实现拷贝构造函数等场景。
-
右值引用(rvalue reference): 右值引用绑定到右值,使用
&&
表示。int &&rref = 2 + 3; // rref 是右值引用,绑定到右值 2 + 3
右值引用主要用于实现移动语义、完美转发等高效的资源管理。
在C++11之后,右值引用的引入对于实现移动语义和避免不必要的复制操作非常重要。移动语义通过将资源的所有权从一个对象转移到另一个对象,避免了深层次的复制。通过右值引用和移动语义,可以提高性能,尤其在处理大型数据结构时。
左值引用与右值引用比较
🥫 左值引用总结
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。因为这里做到了权限的平移
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
🥫 右值引用总结
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
move函数
std::move
是 C++ 标准库中的一个函数,用于将一个左值强制转换为右值引用。其声明位于头文件 <utility>
中。
template <typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept;
std::move
接受一个模板参数 T
,并返回 T&&
,即右值引用类型。它的主要作用是告诉编译器将一个左值强制转换为右值引用,以便利用移动语义,避免不必要的拷贝操作。
移动语义是 C++11 引入的一个特性,通过将资源的所有权从一个对象转移到另一个对象,提高了对于大型数据结构的效率。
#include <utility>
#include <iostream>
#include <vector>
int main() {
std::vector<int> source = {1, 2, 3, 4, 5};
// 使用 std::move 将左值 source 转换为右值引用
std::vector<int> destination = std::move(source);
// source 不再拥有数据,被移动到 destination 中
std::cout << "Source size: " << source.size() << std::endl; // 输出 0
std::cout << "Destination size: " << destination.size() << std::endl; // 输出 5
return 0;
}
在这个例子中,std::move(source)
将左值 source
转换为右值引用,允许 destination
利用移动语义将 source
的内容移动到自己身上,而不进行深层次的拷贝。
为什么可以将移动后的右值引用赋值给左值呢?
在 C++ 中,右值引用并不意味着它只能绑定到右值。右值引用是一种引用类型,可以绑定到右值,也可以绑定到左值(例如使用 std::move
转换后)。这种设计允许在需要移动语义的地方使用右值引用,同时保持对左值的兼容性。在移动后的对象上,其状态通常为“有效但未定义”的状态,所以为了防止在移动后继续使用被移动的对象,通常会将其置为一种已知的状态,如空或默认状态。
右值引用的移动语义
右值引用的移动语义是 C++ 中引入的一种资源管理机制,通过移动对象的底层资源而不是进行深层次的复制,以提高性能。这是通过使用右值引用和移动构造函数来实现的。
右值引用(Rvalue Reference):
在 C++11 中,引入了右值引用(Rvalue Reference),用 &&
表示。与左值引用不同,右值引用主要用于绑定到临时对象、将要销毁的对象、字面常量等右值。
int&& x = 42; // x 是右值引用,绑定到右值 42
移动构造函数:
移动构造函数是一种特殊的构造函数,它允许从一个对象“窃取”底层资源而不是进行深层次的拷贝。移动构造函数通过右值引用参数接受被移动的对象,并将其底层资源从源对象“移动”到目标对象。通常,移动构造函数的实现会将源对象的指针置为空,以避免资源的重复释放。
class MyString {
public:
// 移动构造函数
MyString(MyString&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 将源对象的指针置为空
other.size = 0;
}
// 其他构造函数和成员函数
private:
char* data;
size_t size;
};
移动语义的应用:
移动语义通常应用于需要大量内存操作的场景,例如处理大型容器、字符串等。通过移动而非拷贝,可以避免不必要的资源开销,提高程序的性能。
#include <iostream>
#include <vector>
int main() {
std::vector<int> source = {1, 2, 3, 4, 5};
// 移动构造函数被调用,避免了深层次的拷贝
std::vector<int> destination = std::move(source);
// source 不再拥有数据,被移动到 destination 中
std::cout << "Source size: " << source.size() << std::endl; // 输出 0
std::cout << "Destination size: " << destination.size() << std::endl; // 输出 5
return 0;
}
在这个例子中,std::move(source)
使用了移动语义,将 source
的内容移动到 destination
中,避免了不必要的拷贝。这对于大型数据结构来说是非常高效的。
👉🏻完美转发
🌍模板中的&& 万能引用
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
完美转发是 C++ 中一个重要的概念,它允许保持参数的值类别(左值或右值)并正确地将其传递给其他函数。完美转发通常与引用折叠一同使用,它在 C++11 中引入了右值引用和模板的概念之后变得更为重要。
实现完美转发:
实现完美转发通常涉及到引用折叠、std::forward
和模板类型推导。
-
引用折叠: 引用折叠是 C++ 中的一种规则,它决定了在某些情况下引用的最终类型。例如,
T& &&
折叠为T&
,T&& &&
折叠为T&&
。 -
std::forward:
std::forward
是一个模板函数,用于在函数中保持参数的原始值类别。它通常与模板参数结合使用,确保参数以正确的方式传递。
🫧 例子:
#include <utility>
#include <iostream>
template <typename T>
void wrapper(T&& arg) {
// 在这里使用 std::forward 保持参数的原始值类别
process(std::forward<T>(arg));
}
// 假设有一个处理函数 process
void process(int& i) {
std::cout << "Lvalue reference: " << i << std::endl;
}
void process(int&& i) {
std::cout << "Rvalue reference: " << i << std::endl;
}
int main() {
int x = 42;
// wrapper 函数通过 std::forward 完美转发参数到 process 函数
wrapper(x); // 传递左值,调用 process(int&)
wrapper(42); // 传递右值,调用 process(int&&)
return 0;
}
在这个例子中,wrapper
函数通过 std::forward
完美转发参数到 process
函数。process
函数通过重载,可以正确地处理传递进来的左值和右值。
完美转发的能力是 C++ 中泛型编程的一个强大特性,它在很大程度上提高了代码的灵活性和可复用性。
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长