C++奇迹之旅:左右值&&左右值引用&&move应用(C++11第二篇)
📝前言
c++11提供了多种简化声明的方式,尤其是在使用模板时,增加了许多方便,增加效率的语法。
🌠 auto
C++98中auto是一个存储类型说明符,表示变量是局部自动存储类型。但是在局部域中定义变量时,默认就是自动存储类型,因此C++98中auto的用法显得并不太有价值。
C++11中,auto不再是存储类型说明符,而是一种实现"自动类型推导"的关键字。也就是说,当你使用auto定义一个变量时,编译器会根据变量的初始化表达式自动推导出该变量的类型。
比如:
auto x = 5; // x的类型会被推导为int
auto y = 3.14; // y的类型会被推导为double
C++11中auto的使用需要满足两个条件:
- 必须有显式的初始化表达式,编译器才能根据这个表达式推导出变量的类型。
- . 初始化表达式必须能唯一地确定变量的类型,编译器才能进行正确的推导。
🌉decltype
在C++11中引入了decltype
关键字,它可以用来获取表达式的类型。与auto
不同,decltype
不会进行类型推导,而是直接采用表达式的类型。
使用decltype
的一般形式是:
decltype(expression) variable;
这里,expression
是一个合法的表达式,decltype
会根据表达式推导出其类型,并将该类型应用到变量的声明中。
比如:
int x = 5;
decltype(x) y = 10; // y的类型是int
std::vector<int> v = {1, 2, 3};
decltype(v.begin()) it = v.begin(); // it的类型是std::vector<int>::iterator
关键字
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,用于表示空指针
#ifndef NULL //检查是否已经定义了 `NULL` 宏。如果未定义,则执行下面的代码块
#ifdef __cplusplus//检查是否正在使用 C++ 编译器。如果是 C++ 编译器,则执行 `#define NULL 0` 语句。
#define NULL 0
#else // 如果不是 C++ 编译器,则执行 `#define NULL ((void *)0)` 语句。
#define NULL ((void *)0)
#endif
#endif
这段代码的主要目的是确保 NULL
宏被正确定义,以适应不同的编程环境。
为什么会有两种不同的定义?
在 C 语言中,NULL
通常被定义为 ((void *)0)
,这是因为 C 语言没有特定的空指针类型。使用 ((void *)0)
可以将 NULL
定义为一个空指针,可以赋值给任何指针类型。
而在 C++ 中,NULL
通常被定义为 0
。这是因为 C++ 引入了 nullptr
关键字,代表空指针,因此 NULL
可以被定义为 0
。
🌠STL中一些变化
新容器 :
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和
unordered_set。。
容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得
比较少的。
比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是
可以返回const迭代器的,这些都是属于锦上添花的操作。
实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:
- https://legacy.cplusplus.com/reference/vector/vector/push_back/
- https://legacy.cplusplus.com/reference/map/map/insert/
- https://legacy.cplusplus.com/reference/map/map/emplace/
但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的?
请看下面的右值引用和移动语义章节的讲解。另外emplace
还涉及模板的可变参数,也需要再继续深入学习后面章节的知识。
🌠 右值引用和移动语义
🌉左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名.
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),**我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。**定义时const
修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
//以下的p , b ,c, *p都是左值
//左值可以取地址
int* p = new int(0);
int b = 1;
const int c = b;
string s("1111111");
s[0];
cout << &c << endl;
cout << &s[0] << endl;
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,**右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。**右值引用就是对右值的引用,给右值取别名。
//右值:不能取地址
double x = 1.1, y = 2.2;
//以下几个都是常见的右值,敞亮临时对象,匿名对象
10;
x + y;
fmin(x, y);
string("111111");
cout << &10 << endl;
cout << &(x+y) << endl;
cout << &(fmin(x, y) << endl;
cout << &string("1111") << endl;
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
注意:的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1。如果不想rr1
被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,
这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
🌠左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是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++11标准库函数。
std::move
的作用是将一个左值转换为一个右值引用。它的定义如下:
template <class T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept;
1. `template <class T>`: 这是一个函数模板,可以接受任意类型的参数。
2. `typename std::remove_reference<T>::type&&`: 这是函数的返回类型。它使用 `std::remove_reference` 去除参数 `T` 的引用属性,然后返回一个右值引用。
3. `T&& arg`: 这是函数的参数。它使用了"完美转发"的技术,可以接受任意类型的参数,无论是左值还是右值。
4. `noexcept`: 这个关键字表示该函数不会抛出任何异常。
那么 std::move
具体是怎么工作的呢?
-
当你传递一个左值给
std::move
时,它会将这个左值转换为一个右值引用。这样可以让你"窃取"这个对象的资源,而不是复制它。 -
当你传递一个右值给
std::move
时,它会直接返回这个右值引用,不会进行任何转换。
举个例子:
假设我们有一个 std::string
类型的对象:
std::string str = "Hello, world!";
在这里,str
是一个左值。如果我们想将 str
的内容移动到另一个 std::string
对象中,而不是进行复制,我们可以使用 std::move
来实现:
std::string str2 = std::move(str);
在这个例子中:
std::move(str)
将str
这个左值转换为一个右值引用。std::string str2 = std::move(str);
将这个右值引用绑定到新的str2
对象上。- 这样做之后,
str
对象就被"窃取"了,它的内容已经转移到了str2
上。str
现在处于一种"已移动"的状态,不再拥有任何资源。
这个过程中,我们没有进行任何复制操作,只是简单地转移了对象的所有权。这种移动语义可以大大提高程序的性能,因为它避免了不必要的内存拷贝。
**另一个例子:**是在函数参数传递中使用 std::move
:
void processString(std::string&& s) {
// 在这里使用 s 对象,并可能会移动它的内容
}
std::string str = "Hello";
processString(std::move(str));
在这个例子中:
processString(std::move(str))
将str
这个左值转换为一个右值引用,然后传递给processString
函数。- 在
processString
内部,s
参数就是一个右值引用,我们可以对它进行移动操作,而不是复制操作。
🌉 右值引用临时对象
在这段代码中,int&& rr1 = x + 10;
看起来似乎违背了右值引用只能引用右值的规则,这是为什么呢?
// 底层汇编等实现和上层语法表达的意义,有时是背离的,所以不要结合到一起去理解,互相佐证
int main()
{
int x = 0;
int& r1 = x;
int&& rr1 = x + 10;
return 0;
}
这里的关键在于 C++11 引入了两种不同的引用类型:
- 左值引用(
int& r1 = x;
):r1
只能引用左值。 - 右值引用(
int&& rr1 = x + 10;
):rr1
可以引用右值。
C++11 也引入了一个特殊情况,称为==“萃取临时对象”==。当表达式x + 10
的结果是一个临时对象时,它会被视为一个右值,因此可以使用右值引用来引用它。
🚩总结
- 总结左右值区别,左右引用区别!
// 以下的p、b、c、*p都是左值
// 左值:可以取地址
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0];
// 左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值,常量临时对象,匿名对象
10;
x + y;
fmin(x, y);
string("11111");
// 右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x+y;
double&& rr3 = fmin(x,y);
- 总结左右引用区别和move的关系
///
// 左值引用引用给右值取别名:不能直接引用,但是const 左值引用可以
const int& rx1 = 10;
const double& rx2 = x+y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// void push(const T& x);
vector<string> v;
string s1("1111");
v.push_back(s1);
v.push_back(string("1111"));
v.push_back("1111");
/
// 右值引用引用给左值取别名:不能直接引用,但是move(左值)以后右值引用可以引用
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
原文地址:https://blog.csdn.net/a_hong_sen/article/details/143881461
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!