目录
1.{}列表初始化
在C++98中,只允许使用{}初始化列表初始化数组或者结构体元素
#include <iostream>
#include <vector>
#include <string>
using namespace std;
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); // 老写法
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
初始化列表std::initializer_list的介绍文档
这里告诉我们 initializer_list是一个模板类,它用于C++初始化列表,这个列表必须是const T类型的数据的集合
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便,也可以作为operator=
的参数,这样就可以用大括号赋值
#include <list>
#include <map>
int main()
{
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = { 10, 20, 30 };
return 0;
}
小试一下模拟实现vector的列表初始化
//模拟实现vector支持initializer_list
namespace wrt
{
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
for (auto e : l)
(*vit)++ = e;
}
vector<T>& operator=(initializer_list<T> l) {
vector<T> tmp(l); //复用
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
2.左右值
右值是C++从C继承来的概念,最初是指=号右边的值。但现在C++中的右值已经与它最初的概念完全不一样了。在C++中右值指的的临时值或常量,更准确的说法是保存在CPU寄存器中的值为右值,而保存在内存中的值为左值
可能有很多同学对计算机系统的底层不太了解,计算机是由CPU、内存、主板、总线、各种硬件等组成的,这个大家应该都清楚,而CPU又是由逻辑处理器,算术单元、寄存器等组成的。我们的程序运行时并不是直接从内存中取令运行的,因为内存相对于CPU来说太慢了。一般情况下都是先将一部分指令读到CPU的指令寄存器,CPU再从指令寄存器中取指令然后一条一条的执行。对于数据也是一样,先将数据从内存中读到数据寄存器,然后CPU从数据寄存器读数据。以Intel的CPU为例,它就包括了 EAX、EBX、ECX、EDX…多个通用寄存器,这样就可以让CPU更高效的工作。
比如说一个常数5,我们在使用它时不会在内存中为其分配一个空间,而是直接把它放到寄存器中,所以它在C++中就是一个右值。再比如说我们定义了一个变量 a,它在内存中会分配空间,因此它在C++中就是左值。那么a + 5是左值还是右值呢?当然是右值对吧,因为a + 5的结果存放在寄存器中,它并没有在内存中分配新空间,所以它是右值
2.1左值
左值(lvalue):An
lvalue is an expression that refers to a memory location and allows us to take the address of that memory location via the
&
operator. 左值是会分配内存的,可以通过&进行取地址
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
- 变量、函数或数据成员
- 返回左值引用的表达式
如 ++x、x = 1、cout << ' '
int x = 0;
cout << "(x).addr = " << &x << endl;
cout << "(x = 1).addr = " << &(x = 1) << endl;
cout << "(++x).addr = " << &++x << endl;
//cout << "(x++).addr = " << &x++ << endl; // error
cout << "(cout << ' ').addr=" << &(cout << ' ') << endl; - 字符串字面量是左值,而且是不可被更改的左值。字符串字面量并不具名,但是可以用&取地址所以也是左值。
如 "hello",在c++中是 char const [6] 类型,而在c中是 char [6] 类型
cout << "(\"hello\").addr=" << &("hello") << endl; - 如果一个表达式的类型是一个lvalue reference (例如,
T&
或const T&
, 等.),那这个表达式就是一个lvalue。
2.2右值
右值(rvalue) An rvalue is an expression that is not an lvalue.
右值不能取地址,并且不能出现在赋值符号的左侧 ,当我们想对右值进行引用的时候不能只用一个&,而是&&
1.纯右值(prvalue)
反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:
- 返回非引用类型的表达式
如 x++、x + 1 - 除字符串字面量之外的字面量如 42、true
int main(int argc, char* argv[]) {
int&& a = 5; // 正确,5会被直接存放在寄存器中,所以它是右值
int b = 10;
int&& c = b; // 错误,b在内存中有空间,所以是右值;右值不能赋值给左值
int&& d = b + 5; // 正确,虽然 b 在内存中,但 b+5 的结果放在寄存器中,它没有在内存中分配空间,因此是右值 、
//int&& e = a;
//e虽然接收的必须是右值,但它本身是左值。换句话说e是一种特殊的变量,它是只能接收右值的变量。我们再从左值的本质来看,e也是占内存空间的,所以它肯定是左值。
//应该更改成
int&& e = move(a);
}
2.将亡值(xvalue)
将亡值是在c++ 11中跟右值引用相关的表达式,这种表达式通常是被移动的对象(移为他用),比如返回右值引用T&&的函数返回值 ,std::move的返回值,或者转换为T&&的类型转换函数的返回值,而剩余的可以标识函数
- 隐式或显式调用函数的结果,该函数的返回类型是对所返回对象类型的右值引用
int&& f(){
return 3;
}
int main()
{
f(); // The expression f() belongs to the xvalue category, because f() return type is an rvalue reference to object type.
return 0;
}
- 对对象类型右值引用的转换
这里的static_cast是C++中的强制类型转换
强制类型转换运算符 <要转换到的类型> (待转换的表达式)
例如:
double d = static_cast <double> (3*5); //将 3*5 的值转换成实数
static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换
static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换
举例子:
#include <iostream>
using namespace std;
class A
{
public:
operator int() { return 1; }
operator char*() { return NULL; }
};
int main()
{
A a;
int n;
char* p = "New Dragon Inn";
n = static_cast <int> (3.14); // n 的值变为 3
n = static_cast <int> (a); //调用 a.operator int,n 的值变为 1
p = static_cast <char*> (a); //调用 a.operator char*,p 的值变为 NULL
n = static_cast <int> (p); //编译错误,static_cast不能将指针转换成整型
p = static_cast <char*> (n); //编译错误,static_cast 不能将整型转换成指针
return 0;
}
将亡值对对象右值引用类型的转换
int main()
{
static_cast<int&&>(7); // The expression static_cast<int&&>(7) belongs to the xvalue category, because it is a cast to an rvalue reference to object type.
std::move(7); // std::move(7) is equivalent to static_cast<int&&>(7).
return 0;
}
- 类成员访问表达式,指定非引用类型的非静态数据成员,其中对象表达式是xvalue
struct As
{
int i;
};
As&& f(){
return As();
}
int main()
{
f().i; // The expression f().i belongs to the xvalue category, because As::i is a non-static data member of non-reference type, and the subexpression f() belongs to the xvlaue category.
return 0;
}
xvalue有标识符,所以也被称为lvalue。跟左值 lvalue 不同,xvalue 仍然是不能取地址的——这点上,xvalue 和 prvalue 相同。所以,xvalue 和 prvalue 都被归为右 值 rvalue。如下所示:
_有标识符_ _无标识符号_
/ X \
/ / \ \
| l | x | pr |
\ \ / /
\_________X________/
gl r
图中的gl代表广义的左值,包含狭义左值和将亡值,都是有标识符的
将亡值和纯右值一起构成右值
3.生命周期的延长
一个变量的生命周期在超出作用域时结束。如果一个变量代表一个对象,当然这个对象的生命周期也在那时结束。临时对象生命周期C++ 的规则是:一个临时对象 会在包含这个临时对象的完整表达式估值完成后、按生成顺序的逆序被销毁,除非有生命周期延长发生。
(1)无生命周期延长
#include <iostream>
using namespace std;
class shape {
public:
shape() { cout << "shape" << endl; }
virtual ~shape() {
cout << "~shape" << endl;
}
};
class circle : public shape {
public:
circle() { cout << "circle" << endl; }
~circle() {
cout << "~circle" << endl;
}
};
class triangle : public shape {
public:
triangle() { cout << "triangle" << endl; }
~triangle() {
cout << "~triangle" << endl;
}
};
class rectangle : public shape {
public:
rectangle() { cout << "rectangle" << endl; }
~rectangle() {
cout << "~rectangle" << endl;
}
};
class result {
public:
result() { puts("result()"); }
~result() { puts("~result()"); }
};
result process_shape(const shape &shape1, const shape &shape2) {
puts("process_shape()");
return result();
}
int main() {
process_shape(circle(), triangle());
}
先构造triangle,在构造circle,这两个都继承自shape,所以前面都会先构造shape,后面依次,析构的时候最后构造的,最先析构
输出:
shape
triangle
shape
circle
process_shape()
result() //这里要返回吧临时对象return()
~result() //临时对象返回之后立马销毁
~circle
~shape
~triangle
~shape
(2)有生命周期延长
为了方便对临时对象的使用,C++ 对临时对象有特殊的生命周期延长规则。这条规则是:如果一个 prvalue 被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长
result &&r = process_shape(circle(), triangle());
输出结果如下:
shape
triangle
shape
circle
process_shape()
result()
~circle
~shape
~triangle
~shape
~result()
result析构被延到最后了
需要万分注意的是,这条生命期延长规则只对 prvalue 有效,而对 xvalue 无效。如果由于某种原因,prvalue 在绑定到引用以前已经变成了 xvalue,那生命期就不会延长。不注意这点的话,代码就可能会产生隐秘的 bug。比如,我们如果这样改一下代码,结果就不对了:
result &&r = std::move(process_shape(circle(), triangle()));
输出结果回到无延迟的结果
4.万能引用
1.引入万能引用
这个问题的本质实际上是,类型声明当中的“&&
”有的时候意味着rvalue reference,但有的时候意味着rvalue reference 或者 lvalue reference。因此,源代码当中出现的 “&&
” 有可能是 “&
” 的意思,即是说,语法上看着像rvalue reference (“&&
”),但实际上却代表着一个lvalue reference (“&
”)。在这种情况下,此种引用比lvalue references 或者 rvalue references都要来的更灵活。
Rvalue references只能绑定到右值上,lvalue references除了可以绑定到左值上,在某些条件下还可以绑定到右值上
[1] 这里某些条件绑定右值为:常左值引用绑定到右值,非常左值引用不可绑定到右值!
规则简记如下
左值引用 可以引用 {左值}
右值引用 可以引用 {右值}
常左值引用 可以引用 {右值,左值}
万能引用
If a variable or parameter is declared to have type T&& for some deduced type
T
, that variable or parameter is a universal reference.
如果一个变量或者参数被声明为T&&,其中T是被推导的类型,那这个变量或者参数就是一个universal reference。
Widget&& var1 = someWidget; // here, “&&” means rvalue reference
auto&& var2 = var1; // here, “&&” does not mean rvalue reference
template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference
template<typename T>
void f(T&& param); // here, “&&”does not mean rvalue reference
在实践当中,几乎所有的universal references都是函数模板的参数。因为auto
声明的变量的类型推导规则本质上和模板是一样的,所以使用auto的时候你也可能得到一个universal references。
和所有的引用一样,你必须对universal references进行初始化,而且正是universal reference的initializer决定了它到底代表的是lvalue reference 还是 rvalue reference:
- 如果用来初始化universal reference的表达式是一个左值,那么universal reference就变成lvalue reference
- 如果用来初始化universal reference的表达式是一个右值,那么universal reference就变成rvalue reference
template<typename T>
void f(T&& param);
int a;
f(a); // 传入左值,那么上述的T&& 就是lvalue reference,也就是左值引用绑定到了左值
f(1); // 传入右值,那么上述的T&& 就是rvalue reference,也就是右值引用绑定到了左值