C++11-你不知道的(上)

列表初始化

在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:

int a[] = { 1,2,3 };

对于一些自定义的类型,却无法使用这样的初始化。比如:

vector<int> v{1,2,3,4,5};

就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

在这里插入图片描述
v就初始化好了,方便初始化。

为什么可以用{}初始化呢?

库里面增加了initializer_list类型的构造函数。

在这里插入图片描述
initlizer_list里有size,和迭代器,就可以用初始化列表构造了。
在这里插入图片描述

变量类型推导

有些地方的代码需要写很长,有时候std是不能展开的,在练习的时候可以展开。当不能展开是时遇到迭代器写起来很长。

std::map<std::string, std::string> m{ {"apple", "苹果"}, {"banana","香蕉"} };
	std::map<std::string, std::string>::iterator it = m.begin();
	while (it != m.end())
	{
		cout << it->first << " :" << it->second << endl;
		++it;
	}
	cout << endl;
	//用auto就很方便,让编译器自己推导即可,可以少些很多的代码
	auto it1 = m.begin();
	while (it1 != m.end())
	{
		cout<<it1->first<<" :"<< it1->second << endl;
		++it1;
	}
	cout << endl;

在这里插入图片描述
auto用起来就很香。

decltype类型推导(知道即可)

decltype是根据表达式的实际类型推演出定义变量时所用的类型。

  1. 推演表达式类型作为变量的定义类型
	int a = 10;
	int b = 20;

	// 用decltype推演a+b的实际类型,作为定义c的类型
	decltype(a + b) c;
	cout << typeid(c).name() << endl;
  1. 推演函数返回值的类型
void* GetMemory(size_t size)
{
 return malloc(size);
}
int main()
{
 // 如果没有带参数,推导函数的类型
 cout << typeid(decltype(GetMemory)).name() << endl;
 
 // 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
 cout << typeid(decltype(GetMemory(0))).name() <<endl;
 
 return 0; }

在这里插入图片描述

新增加的容器

范围for,final,override,前面的博客有讲解。新增加的容器:
在这里插入图片描述
感觉array静态数组和数组没什么区别而且用的很少,只不过array可以用迭代器,但是它是在栈上,栈上的空间有限还不如使用vector,开在堆上。

int main()
{
	int a[] = { 1,2,3,4 };
	array<int,5> a1 = { 5,6,7,8,9 };
	auto it = a1.begin();
	while(it != a1.end())
	{
		cout<< *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

用迭代器打印:
在这里插入图片描述
单链表的话跟这个array差不多,下面的2个关联式容器作用很大前面的博客有详细的讲解。

默认成员函数控制

在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。

显式缺省函数

在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
例如:

class A {
public:
 A(int a): _a(a)
 {}
 // 显式缺省构造函数,由编译器生成
 A() = default;
 
 // 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
 A& operator=(const A& a);
private:
 int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
 A a1(10);
 A a2;
 a2 = a1;
 return 0; }

删除默认函数

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class A {
public:
	A(int a) : _a(a)
	{}

	// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
	A& operator (const A&) = delete;
private:
	int _a;
};

int main()
{
	A a1(10);
	// 编译失败,因为该类没有拷贝构造函数
	A a2(a1);
	 // 编译失败,因为该类没有赋值运算符重载
	A a3(20);
	a3 = a2;
	return 0;
}

右值引用

左值引用和右值引用

传统的C++语法中就有引用,而C++11中新增了的右值引用语法特性,之前的引用都是左值引用。无论是左值引用还是右值引用都是给对象取别名。

什么是左值?什么是左值引用?

左值是1个表示数据的表达式(如变量名或解引用的指针),一般情况我们可以取到它的地址,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。特殊情况:定义const修饰符后的左值不能给它赋值但是可以取它的地址。左值引用就是给左值取别名。
例如:

int main()
{
	//以下的p,b,c,*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	//以下是对上面左值的引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;

	return 0;
}

什么是右值?什么是右值引用?

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

int main()
{
	double x = 1.1, y = 2.2;

	//以下几个是常见的右值
	10;
	x + y;
	fmin(x, y);

	//以下是对右值的引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x + y);
	//下面的编译会报错,左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

小结:

左值:
1.左值可以取地址
2.一般情况下可以修改,(const修饰的不能修改)
右值:
1.不能取地址
2.不能出现在=的左边,不能修改

注意:右值是不能取地址的,但是给右值去别名后,会导致右值被存储到特定的位置,且可以取到该位置的地址,了解一下。

左值引用和右值引用比较

左值引用小结:

1.左值引用只能引用左值,不能引用右值
2.const左值引用既可以引用左值也可以引用右值

例如:

int main()
{
	//左值引用只能引用左值,不能引用右值
	//const左值引用既可以引用左值也可以引用右值
	int a = 10;
	int& ra1 = a;

	//int& ra2 = 10; 10是右值
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

右值值引用小结:

1.右值引用 只能引用右值,不能引用最值
2.但是右值可以引用左值move后的左值

int main()
{
 	int&& r1 = 10;
	int a = 10;
	//int&& r2 = a;

	int&& r3 = std::move(a);
	return 0;
}

右值引用使用的场景1

知道了右值引用后,我们要知道为什么C++11增加了右值引用。

左值引用的使用场景
1.做参数
2.做函数的返回值

我们知道左值引用做参数可以减少深拷贝,传值就会造成深拷贝,一些简单的传值无所谓但是遇到很复杂的结构就不能传值,需要传引用。左值引用在做参数是解决的很彻底。但是做返回值就解决的不彻底了。
有的函数返回对象出了作用域就不存在了,不能用左值引用了,就存在深拷贝的情况。

namespace bit
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的
	};

	bit::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		bit::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;
	}
}

在这里插入图片描述

在这里插入图片描述
编译器做了优化,其实是2次深拷贝。还有这种情况:我先接收返回值,再把它赋值
在这里插入图片描述

那我们用右值引用做返回值可不可以呢?不是的,我们要增加移动构造和移动赋值。

右值分为纯右值,和将亡值纯右值例如:10,x+y。将亡值就是函数返回的临时对象、匿名对象。移动构造和移动赋值就是将你的资源转移,反正你出了作用域就不存在了。
我们在上面的类中加上移动构造和移动赋值。

	// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			this->swap(s);
		}

		// 移动赋值 
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			this->swap(s);

			return *this;
		}

我们再来试试:
在这里插入图片描述
就是一次移动构造。

在这里插入图片描述
这就是1次移动构造加1次移动赋值,效率比深拷贝要高的多。
总结:

右值引用并不是直接减少深拷贝,而是支持深拷贝的类提供移动构造和移动赋值。

在这里插入图片描述
在库中C++11都增加了移动构造和移动赋值。

右值引用使用的场景2

在push_back中用右值不会深拷贝,左值会深拷贝。
在这里插入图片描述

move过的对象会空了需要注意。
在这里插入图片描述
s1move后资源转移了,变成了空的了。

库里的容器中都增加了右值引用的insert,push_bak等,具体的可以看库
在这里插入图片描述

完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) { Fun(t); }
int main()
{
	PerfectForward(10); // 右值

	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

不用完美转发,右值引用的对象再作为实参传递时,属性会退化为左值。

在这里插入图片描述

此时我们就要用完美转发

void PerfectForward(T&& t) { Fun(std::forward<T>(t); }//加上完美转发

在这里插入图片描述

注意:模板中的&&并不是右值引用,而是万能引用,它既能接受左值也能接收右值。

代码地址:

https://gitee.com/ge-xiaoyu/testc-plus/tree/master/C++11%E4%B8%8A

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>