[c++11(一)] 右值引用以及列表初始化

发布于:2024-12-18 ⋅ 阅读:(102) ⋅ 点赞:(0)

1.前言

c++11相比于c++98来说,更新了特别多的内容,接下来的几篇文章主要讲解的是c++11中一些常见,常使用的。其余一些变化可以参考如下文章:

c++11新特性,所有知识点都在这了!-腾讯云开发者社区-腾讯云 (tencent.com)

c++11新特性,所有知识点都在这了! - 知乎 (zhihu.com)

本章重点

本章主要讲解初始化列表及其底层容器Initializer_list,并讲解右值引用,以及左值引用和右值引用的区别。

2.初始化列表

在C++11中,列表初始化(也称统一初始化或直接列表初始化)是一个重要的特性,它提供了一种简洁且一致的方式来初始化各种类型的对象。列表初始化使用花括号 {} 来初始化对象,这使得代码更具可读性和一致性。

  1. 在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定
int array1[] = {1,2,3,4,5};
int array2[5] = {0};

 而在c++11中可以不使用=号进行初始化。(也可以使用)

int arr[5]{1,2,3,4,5}

2.在c++98中,{}是无法对自定义类型进行初始化的,而在c++11中,扩大了{}的使用范围,他也可以对自定义类型进行初始化。

1.内置类型的初始化列表

// 内置类型变量
int x1 = {10};
int x2{10};//建议使用原来的
int x3 = 1+2;
int x4 = {1+2};
int x5{1+2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3,4,5};//这种初始化就很友好,不用push_back一个一个插入
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};

2.自定义类型的初始化列表

1.标准库支持单个对象的初始化列表

class Point
{
public:
	Point(int x = 0, int y = 0): _x(x), _y(y)
{}
private:
	int _x;
	int _y;
};
int main()
{
	Pointer p = { 1, 2 };
	Pointer p{ 1, 2 };//不建议
return 0;
}

2.标准库也支持多个对象的初始化列表

多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "这是日期类" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//C++11容器都实现了带有initializer_list类型参数的构造函数
	vector<Date> vd = { { 2022, 1, 18 }, Date{ 2022, 1, 19 }, { 2022, 1, 20 } };
	return 0;
}

vector容器里面有initializer_list类型参数的构造函数,所以可以使用initializer_list进行初始化。

2.1 Initializer_list容器详解 

解释:此类型用于访问c++初始化列表中的值,该列表是const T类型元素的列表。
此类型的对象由编译器从初始化列表声明中自动构造,初始化列表声明是用大括号括起来的逗号分隔的元素列表:

为什么在使用STL容器时,可以使用初始化列表来进行初始化呢?

vector:

list:

发现在STL容器中,都已经有了初始化列表来进行初始化的构造函数,当你使用初始化列表来初始化时,他就会调用STL容器里面的初始化列表函数。

初始化列表里面的值是无法被修改的,因为这些值都存储在常量区内,带有const属性。

那对于不是STL容器,而是自定义类型的类,他为什么还能够使用初始化列表进行初始化呢?

看如下例子:

class MyClass {
public:
    int x;
    double y;

    MyClass() : x(0), y(0.0) {} // 默认构造函数
};

int main() {
    MyClass obj{1, 2.5}; // 使用初始化列表初始化
    return 0;
}

上述代码是不会报错的,这是为什么呢?

在C++11中,即使你没有为自定义类型显式地编写初始化列表构造函数,编译器也会为你生成一个默认的构造函数,该构造函数支持使用初始化列表来进行初始化。这种默认构造函数被称为"默认成员初始化器列表"(default member initializer list),它允许你在类定义中为成员变量提供默认值。

同时呢,当你自定义的类,没有写任何的构造函数,那么编译器会自动帮你生成一个默认的构造函数,这个默认的构造函数也是支持使用初始化列表来初始化成员变量的。

例:

class MyClass {
public:
    int x;
    double y;
};

int main() {
    MyClass obj{1, 2.5}; // 使用初始化列表初始化
    return 0;
}

3.右值引用和移动语义

首先在正式讲解右值引用和移动语义之前,需要要先区分什么是左值,什么是右值。

3.1 左值和右值的概念

左值概念:

左值是一个表示数据的表达式(如变量名或解引用的指针),可以获取它的地址,也可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名

例:

// 以下的a、b、c、*a都是左值
int* a = new int(1);
int b = 2;
const int c = 3;
// 以下几个是对上面左值的左值引用
int*& ra = a;
int& rb = b;
const int& rc = c;
int& value = *a;

右值概念:

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名

例:

double x = 1.1, y = 2.2;
//常见右值
10;
x + y;
add(1, 2);
//右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double && rr3 = add(1, 2);
//右值引用一般情况不能引用左值,可使用move将一个左值强制转化为右值引用
int &&rr4 = move(x);
//右值不能出现在左边,错误
10 = 1;
x + y = 1.0;
add(1, 2) = 1;

不能根据一个值能否被修改,来判断是左值还是右值,因为const修饰左值的时候,左值也是无法被修改的。

如何区分一个值是左值还是右值呢?

一个值是否能够取地址,如果可以,那么就是左值,如果不可以那么就是右值。

PS:虽然无法直接对右值进行取地址,但是可以对右值引用后的变量进行取地址。

例:

int&& r = 10;
int* pr = &r;

3.2  左值引用和右值引用的区别

左值引用:

1.左值引用只能引用左值,不能引用右值

2.const 左值引用既可以引用左值,也可以引用右值

例:

// 左值引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值

//const左值引用既可以引用左值,也可以引用右值
const int& ra3 = 10;
const int& ra4 = a;

解释:因为右值都有着常属性,是无法被修改的。而不加const的左值引用表示可以 被修改,因此编译器是无法报错的。

右值引用:

右值引用一般只能引用右值,不能引用左值

如果右值引用要引用左值的话,要使用move函数

例:

int a = 10;
int b = 20;
//不能引用左值
//int&& rr1 = a;
int&& rr2 = 10;
int&& rr3 = move(a);//强制转换为右值引用

move:当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

3.3 为什么要有右值引用

有了左值引用之后,那么为什么还需要有一个右值引用呢?--这是因为左值引用是存在短板的。

但是右值引用却可以弥补这个短板。--具体过程如下:

左值引用做返回值:当返回值出了作用域还在时,那么就可以使用左值引用做返回值。

#include <iostream>
 
int& getElement(int arr[], int index) {
    return arr[index]; // 返回数组元素的引用
}
 
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    getElement(arr, 2) = 10; // 修改返回的元素
    std::cout << arr[2] << std::endl; // 输出:10
    return 0;
}

但是有一个问题,如果除了作用域,返回值就被销毁了,那么他就无法使用左值引用进行返回了,他就需要涉及到拷贝了。

例:当我们用传值返回的方式返回一个局部对象,例如下面这个函数(将整形转成字符串)

my::string to_string(int value)
{
	bool flag = true;
	if (value < 0)
	{
		flag = false;
		value = 0 - value;
	}
	my::string str; // 局部变量
	while (value > 0)
	{
		int x = value % 10;
		value /= 10;
		str += ('0' + x);
	}
	if (flag == false)
	{
		str += '-';
	}
	std::reverse(str.begin(), str.end());
	return str;
}
 
int main()
{
	my::string str = to_string(123);
}

上面这种情况会进行几次拷贝构造?但是在编译过程中,编译器说是一次,但其实是两次,这是因为编译器进行优化了:

为何是两次解释:

由于对象作为局部变量在函数结束时就会销毁,所以要想保留对象的内容,就需要一个临时对象来接收(即返回对象ret),此时就会调用一次拷贝构造,将局部变量的内容拷贝到返回对象中,返回对象也只是临时的,它的作用就是在外部需要接收时,再将内容拷贝构造给新的对象,所以总共是发生了两次拷贝构造:

不过现在的编译器会优化成一次拷贝构造,将局部对象直接作为函数临时对象拷贝给接收对象:

无论如何,至少都要进行一次深拷贝,面对较大对象时,会很大程度上影响性能,那么可不可以不多这一次拷贝构造呢,就是将局部对象的内容直接传给外部接收对象,左值引用是无法做到这些事情的。---这就是左值存在的短板。

那么对于这个短板--右值引用时可以解决的。--通过移动构造和移动赋值。

3.4 右值引用的使用场景和移动语义

右值引用时如何解决左值引用存在的短板的呢?---通过移动语义的存在,完美的解决了拷贝的问题。

什么是移动语义:

将一个对象中资源移动到另一个对象中的方式。

解决思路:

当函数在按照值返回时,设返回的是str,必须创建一个临时对象,临时对象创建好之后,str就被销毁了,str是一个将亡值,C++11认为其为右值,在用str构造临时对象时,就会采用移动构造,即将str中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到ret中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。

解决示例:

在使用了移动构造和移动赋值时:

右值引用解决的办法是这样的:他并没有直接去减少拷贝,而是通过移动构造这个函数,对于那些直接传值返回的函数,通过调用移动构造函数,利用移动语义的特性,直接把返回的值的资源转移了,这样就可以避免进行拷贝,提高效率。

使用场景:

1.资源管理:在构造函数、拷贝构造函数、赋值运算符等场景中,使用右值引用实现移动语义。

当前STL容器中都存在,右值引用来实现移动语义的情况。

2. 完美转发:用于函数模版中,实现参数的完美转发

完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样

在学习完美转发之前,先学习万能引用&&。

3.4 万能引用 &&

1、模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
2、模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
3、但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
4、我们希望能够在传递过程中保持它的左值或者右值的属性,就需要用我们下面学习的完美转发

在c++中,他是通过forward函数来实现完美转发的

void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const 左值引用" << endl; }

void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
	//Func(t);//没有使用forward保持其右值的属性,退化为左值
	Func(forward<T>(t));
}
int main()
{
	PerfectForward(1);//右值
	int a = 10;
	PerfectForward(a);
	PerfectForward(move(a));

	const int b = 20;
	PerfectForward(b);
	PerfectForward(move(b));
	return 0;
}

只写Func(t)那么这些值全部都会退化成左值。只会调用左值引用的函数,使用了forward函数,就可以实现完美转发了。

PS:如果是多层函数嵌套,那么就需要使用的是多层forward函数,否则在那一层没有使用,会被直接退化成左值。

4.总结

初始化列表和右值引用以及移动语义到这就全部阐述完毕了。

拓展:

C++11之后,类的六个默认成员函数又增加了两个,移动构造和移动赋值,对于这两个函数需要注意下面几个点:


网站公告

今日签到

点亮在社区的每一天
去签到