【C++11】{}列表初始化,左右值,万能引用

目录

1.{}列表初始化

2.左右值 

2.1左值 

2.2右值 

1.纯右值(prvalue)

2.将亡值(xvalue)

3.生命周期的延长 

(1)无生命周期延长

(2)有生命周期延长

 4.万能引用

万能引用


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,也就是右值引用绑定到了左值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值