栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++11特性:大括号初始化、右值引用和移动语义

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++11特性:大括号初始化、右值引用和移动语义

目录
1. 统一的列表初始化
2. 右值引用和移动语义
2.1右值引用的详细讲解
2.2右值引用中move的使用
2.3完美转发

统一的列表初始化

在C++98标准下,和C语言一样允许对数组和结构体用{}进行初始化。

struct A
{
	int _a;
	int _b;
};

int main()
{
	int arr[] = { 3,5,6,23,2,5 };
	A a = { 1,2 };
	A aa[] = { {3,2},{4,1},{4,6} };
	return 0;
}

上述初始化都是语法允许的,对于class 类的成员变量默认是私有的用大括号初始化,就必须实现公共的构造函数,struct中的成员变量默认是公共的所以不用实现构造函数即可

class A
{
private:
	int _a;
	int _b;
};

int main()
{
	A a = { 2,2 };   //报错
	A aa[] = { {1,3},{2,4} };  //报错
	return 0;
}


下面这样就不会报错

class A
{
public:
	A(int a,int b)   //实现一个构造函数,就可以用大括号初始化对象
		:_a(a)
		,_b(b)
	{}
private:
	int _a;
	int _b;
};

int main()
{
	A a = { 2,2 };
	A aa[] = { {1,3},{2,4} };
	return 0;
}

C++98对于stl中的容器是不支持用大括号来初始化的,比如vector,我们创建一个vector的对象,要么就是用默认构造创建,容器中没有存储元素,或者是创建对象时指定n个特定值,或者拷贝构造一个其他vector的副本,还可以使用resize成员方法。C++11实现了stl中的容器同样可以用大括号来初始化。

int main()
{
	vectorv = { 2,4,6,3,6 };
	mapm= { {2, "sort"}, { 3,"left" }, { 4,"right" }};
	listls{ 5,3,6,7 };
	auto e = { 4,3,5 };
	cout << typeid(e).name() << endl;
	return 0;
}

运行上述代码会打印大括号的类型

我们再看看这些容器的构造函数看看它们为什么能支持大括号初始化



可以发现stl的容器都实现了一个用initializer_list的对象来构造的函数,我们前面也打印了大括号类型的数据就是initializer_list。下面我们来验证是不是因为有这个类型的构造函数才支持的大括号初始化,我们写一个简单的vector,看看不写initializer_list版本的构造函数,和写了initializer_list版本的构造函数的情况是什么样的。

template
class myvector
{
public:
	myvector()
		:_a(nullptr)
		,index(0)
		,capacity(0)
	{}
	void push_back(const T& val)
	{
		if (index == capacity)
		{
			int capa = capacity == 0 ? 10 : 2 * capacity;
			expain(capa);
		}
		_a[index++]=val;
	}
	~myvector()
	{
		if (_a)
		{
			delete[]_a;
			index = capacity = 0;
		}
	}
private:
	void expain(size_t capa)
	{
		if (capa > capacity)
		{
			T* tem = new T[capa];
			for (int i = 0; i < index; ++i)
				tem[i] = _a[i];
			delete[]_a;
			_a = tem;
			capacity = capa;
		}	
	}
	T* _a;
	size_t index;
	size_t capacity;
};
int main()
{
	myvectorv1;
	for (int i = 1; i <= 9; ++i)
		v1.push_back(i);
	myvectorv2 = { 2,4,5 }; //报错
	return 0;
}


实现一个initializer_list版本的构造函数后的完整代码如下

{
template
class myvector
{
public:
	myvector()
		:_a(nullptr)
		,index(0)
		,capacity(0)
	{}
	myvector(initializer_listli)
		:_a(nullptr)
		, index(0)
		, capacity(0)
	{
		for (auto e : li)
			push_back(e);
	}
	size_t size()
	{
		return index;
	}
	T& operator[](size_t i)
	{
		assert(i < index);
		return _a[i];
	}
	void push_back(const T& val)
	{
		if (index == capacity)
		{
			int capa = capacity == 0 ? 10 : 2 * capacity;
			expain(capa);
		}
		_a[index++]=val;
	}
	~myvector()
	{
		if (_a)
		{
			delete[]_a;
			index = capacity = 0;
		}
	}
private:
	void expain(size_t capa)
	{
		if (capa > capacity)
		{
			T* tem = new T[capa];
			for (int i = 0; i < index; ++i)
				tem[i] = _a[i];
			delete[]_a;
			_a = tem;
			capacity = capa;
		}	
	}
	T* _a;
	size_t index;
	size_t capacity;
};
int main()
{
	myvectorv1;
	for (int i = 1; i <= 9; ++i)
		v1.push_back(i);
	myvectorv2 = { 2,4,5 };
	for (int i = 0; i < v2.size(); ++i)
		cout << v2[i] << " ";
	cout << endl;
	return 0;
}

实现了initializer_list版本就可以用大括号来初始化了

右值引用和移动语义

我们知道引用就是给变量取别名,操作同一块内存空间,右值引用就是给右值取别名,先谈一谈什么是左值和右值:

左值通常就是可以放在等号左边的值,我们在栈上和堆上定义的变量,解引用的指针,我们可以给左值赋值,取左值的地址,左值也可以放在等号右边。

右值是只能放在等号右边,右值也是一个表示数据的表达式,如临时变量、表达式的返回值和函数返回值,这些都是右值,右值不能取地址,但是可以给右值取别名,取别名后,就有了一块稳定的空间存放右值数据,可以对被引用后的别名取地址。能不能取地址是区分左右值的关键。

int main()
{
	int x = 2, y = 3;
	int a = 5   //a ,x,y都是左值
		10     //  10   x+y    add(x,y)都是右值
		const int& f = 10;   //const 的左值引用可以引用右值 , 右值引用只能引用右值,不能引用左值
		x + y;
		x + y = a;     //右值是不能被赋值的,不能放在等号左边
		add(x, y) = a;
		10 = a;
	add(x, y);
	int&& b = 10;          //右值引用
	int&& c = x + y;
	int&& d = add(x + y);
	return 0;
}

移动语义

C++11提供右值引用如果只是单纯想给右值取别名,那用处是不大的。这就要谈到左值引用的短板了,当在函数中的一个对象出了作用域就要销毁,如果要用这个对象去函数外构建一个新的对象,我们要用值返回的方式返回这个对象,要返回的对象会拷贝构造一个临时对象,然后再用这个临时对象去拷贝构造一个新的对象,当对象中都涉及资源管理时,两次拷贝都只是为了创建一个拥有返回对象资源的对象,两次深拷贝,开空间,释放空间,再去开空间又释放空间,这样的场景效率有很大的提升空间。右值引用的移动语义就是当我要用一个即将销毁的对象去构造新的对象时,实现一个右值引用该类对象的构造函数即移动构造,来构造新对象,函数返回函数内创建的对象,临时对象,都是将亡值,出了作用域就销毁和执行下一步就销毁的对象,编译器认为其是右值,所以用它们构造对象时会走移动构造,在移动构造里,我们只要简单的交换资源就行了,还有移动赋值,没有深拷贝的开销从而提高了效率。

实现一个string类来说明移动语义的作用

class mystring
{
public:
	mystring(const char* str = "")
		:sz(strlen(str))
		,capacity(sz)
	{
		_str = new char[capacity + 1];
		strcpy(_str, str);
	}
	mystring(const mystring& s)
		:sz(strlen(s._str))
		,capacity(sz)
	{
		_str = new char[capacity + 1];
		strcpy(_str, s._str);
		cout << "拷贝构造" << endl;
	}
	void swap(mystring& s)
	{
	std::swap(_str, s._str);
	std::swap(sz, s.sz);
	std::swap(capacity, s.capacity);
	}
	mystring(mystring&& s) 
		:_str(nullptr)
		,sz(0)
		,capacity(0)
	{
		this->swap(s);
		cout << "移动构造,资源转移" << endl;
	}
	mystring& operator=(const mystring& s)
	{
		mystring tem(s);
		this->swap(tem);
		cout << "拷贝赋值" << endl;
		return *this;
	}
	mystring& operator=(mystring&& s)
	{
		this->swap(s);
		cout << "移动赋值" << endl;
		return *this;
		
	}
	char* c_str()const
	{
		return _str;
	}
	void push_back(char ch)
	{
		if (sz == capacity)
		{
			int newpan = capacity == 0 ? 10 : 2 * capacity;
			reserve(newpan);
		}
		_str[sz++] = ch;
	}
	mystring operator+(const char* str)
	{
		int len = strlen(str);
		mystring tem(_str);
		if (sz + len == capacity)
		{
			tem.reserve(2 * (sz + len));
		}
		strcpy(tem.c_str()+sz, str);
		tem.sz += len;
		return tem;
	}
	mystring operator+(const mystring& s)
	{
		mystring tem(_str);
		if (sz + s.sz == capacity)
		{
			tem.reserve(2 * (sz + s.sz));
		}
		strcpy(tem.c_str() + sz, s.c_str());
		tem.sz += s.sz;
		return tem;
	}
	size_t size()
	{
		return sz;
	}
	char& operator[](size_t t)
	{
		assert(t < sz);
		return _str[t];
	}
	void reserve(size_t newpan)
	{
		if (newpan > capacity)
		{
			char* tem = new char[newpan + 1];
			strcpy(tem, _str);
			delete[]_str;
			_str = tem;
			capacity = newpan;
		}
	}
private:
	char* _str;
	size_t sz;
	size_t capacity;
};
mystring func1()
{
	mystring tem("hello world");
	return  tem;
}
int main()
{
	mystring s1("hello");
	mystring s2("world");
	mystring s3; 
	s3= s1 + s2;   //这里值返回方式返回operator+中的局部对象会移动构造一个临时对象再用临时对象移动赋值给s3
	mystring s5; 
	s5=(mystring("vs2019")); //移动赋值
	s5 = func1();//也是先移动构造临时对象,临时对象再移动赋值给s5
	mystring s4 = func1(); //这里本来是移动构造临时对象,临时对象再移动构造s4,编译器优化只有一次移动构造
	return 0;
}

右值引用中move的使用

const 左值引用可以引用右值,但是不能右值引用左值,如果非要去引用一个左值的话,必须将一个左值强制转化为一个右值,可以通过move函数将一个左值转化为一个右值,传入该右值去移动构造和移动赋值,完成移动构造或移动赋值后,原先的左值资源就被转移走了,要谨慎使用move。

mystring s1("hello");
	mystring s2("world");
	mystring s3(move(s2));   //s1和s2都是左值,使用move后移动构造s3,再用s1移动赋值s3, 此后s2和s3中的资源都被转移走了。
	s3 = move(s1);

完美转发

在一些场景中外层函数接受一下参数,再用这些参数来调用内层函数时,如果原先接受了一个右值,到函数中也会被认为是左值,完美转发就是要保持参数原先的属性然后去调用其他函数。下面实验一下没有完美转发和有完美转发的效果。

void Func(int& a) { cout << "lvalue" << endl; }
void Func(int&& a) { cout << "rvalue" << endl; }
void Func(const int& a) { cout << "const lvalue" << endl; }
void Func(const int&& a) { cout << "const rvalue" << endl; }
template
void perfectfunc(T&& a)// 函数模板中的&&是万能引用,传入的是左值就是左值引用传入右值就是右值引用 
{
	Func(a);  
}
int main()
{
	perfectfunc(10);
	int a = 10;
	const int b = a;
	perfectfunc(a);
	perfectfunc(b);
	perfectfunc(move(b));
	return 0;
}

没有完美转发,传给Func的参数都被认为是左值。

将函数模板加上完美转发的语法修饰

void perfectfunc(T&& a)
{
	Func(std::forward(a));  //加上forward原来传入右值给外函数也会保持其右值属性传给目标函数,是左值就保持左值属性
}

现在的效果

达到了保持传入参数属性的效果。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/997968.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号