【C++11】C++11相关特性的简单介绍(一)


【C++11】C++11相关特性的简单介绍(一)


  • 相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。
  • 相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,实际项目开发中用得也比较多。
  • 编译环境:vs2013


一、列表初始化

1. {}的初始化

其实对于列表的初始化我们早已经见过,如数组的初始化。
在C++98中,标准使用{}对数组或者结构体元素来进行统一的列表初始值设定。

struct Point
{
	int x;
	iny y;
};
int main()
{
	int array[5] = { 1, 2, 3, 4, 5 };
	Point p={ 0, 1 };
	return 0;
}

类中成员变量在声明时带初始值(在C++11之前并不支持)。

struct Date
{
	//...
	int year = 1900;
	int month = 1;
	int day = 1;
};

申请空间并将空间内容进行初始化。

int* p=new int[5]={0,1,2,3,4};

2. std::initializer_list

在这里插入图片描述
查阅文档,我们可知,initializer_list为一类模板,验证一下。
在这里插入图片描述
实际上,std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
在这里插入图片描述
对于很多容器都支持大括号来进行容器对象的初始化。

vector<int> v{ 1, 2, 3, 4 };
list<int> l{ 1, 2, 3, 4 };
//这里{"math", "数学"}会先初始化构造一个pair对象
map<string, string> m={ { "math", "数学" }, { "chinese", "语文" } };
//大括号对容器进行赋值
v = { 10, 20 };

二、对象的声明

1. auto进行类型推断

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11中废弃auto原来的用法,将其用于实现自动类型推断。auto不可做函数参数,也不可做函数返回值,且必须给出初始表达式。
当然,auto作用也并非万能的。

map<string, string> m={ { "math", "数学" }, { "chinese", "语文" } };
//map<string,string>::iterator it=m.begin();
auto it = m.begin();
cout << typeid(it).name() << endl;

auto自动推断出it的类型为map<string,string>::iterator。
这对于那种类型复杂且长的类型就会大大的简化代码。

2. decltype

decltype将变量的类型声明为表达式指定的类型。
在这里插入图片描述
如上例子,定义变量z,且变量z的类型为表达式(x*y)的类型。
也可推演函数的类型。
在这里插入图片描述
当然也可模板参数。
在这里插入图片描述

3. nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

4. 范围for

在C++98中,我们遍历数组一般方法就是下标遍历和指针遍历。而此遍历方法都得明确循环的范围,而若要完整的遍历一个有范围的数组或集合时,完全可以让编译器自动计算出范围,而加上auto会更加简单。
在这里插入图片描述
在这里插入图片描述

三、STL中新增容器和方法

C++11新增了一些容器,而实际上最有用的是unordered系列的关联式容器,如unordered_map和unordered_set。

新增容器名底层实现
array固定大小不可自动扩容的数组
forward_list单向链表
unordered_map哈希桶
unordered_set哈希桶

四、右值引用和移动语义

在C++11中引入了右值和右值引用,为区分开来,C++98中引用便称为左值和左值引用。

1. 左值和左值引用

左值:

  • 在 = 的左侧的值
  • 有名字且可取地址
  • = 左侧的一定为左值,而 = 右侧的不一定为右值

左值引用只能引用左值。

int main()
{
	int* p = new int(1);
	//rp为对p的引用
	int*& rp = p;

	int b = 1;
	//rb为对b的引用
	int& rb = b;

	const int c = 2;
	//rc为对c的引用
	const int& rc = c;
}

2. 右值和右值引用

右值:

  • 常量数值
    如: 10、12.34、“abc”
  • 表达式进行的中间结果
    如: a+b=10;//出错
    因为a+b的结果为一个右值,而"="的左操作数必须为左值,a+b为一个右值。
  • 匿名对象
    如:string(“hello”)=“world”;
  • 函数以值的方式返回,返回值为右值

右值引用只能引用右值。

int fmin(int a, int b)
{
	return a > b ? b : a;
}
int main()
{
    int x = 1;
	int y = 2;

    //常量
	10;
	//rr1为10的引用
	int&& rr1 = 10;
	
	//表达式中间结果
	x + y;
	//rr2为(x+y)的引用
	int&& rr2 = x + y;

    //函数以值的方式返回
	fmin(x, y);
	//rr3为fmin(x,y)的引用
	int&& rr3 = fmin(x, y);
	
	return 0;
}

3. 万能引用

const引用既可引用左值,又可引用右值。

int main()
{
    const int d = 10;
	//引用左值
	const int& rd1 = d;
	//引用右值
	const int&& rd2 = 10;
	return 0;
}

那右值引用想引用左值时该如何?
通过std::move()将左值转换成右值。
在这里插入图片描述

int a = 10;
//int&& ra = a;//程序报错
int&& ra = std::move(a);

4. 右值引用使用场景

首先我们先来看一下我们原来的引用,即左值引用。
(1)左值引用做参数和做返回值都可以提高程序的运行效率。

void fun1(string s){}
void fun2(const string& s){}
int main()
{
	string s1("hello");
	//左值引用做参数减少了拷贝这一工作,提高了程序运行效率
	fun1(s1);
	fun2(s1);
	cout << s1 << endl;

	//string operator+=(char ch) 传值返回存在深拷贝
	//string& operator+=(char ch) 传左值引用没有拷贝提高了程序运行效率
	s1 += '!';
	cout << s1 << endl;

	system("pause");
	return 0;
}

(2)左值引用的短处
在如上的例子中,若函数的返回对象为一个局部变量,此变量在出了函数作用域就不存在,此时就不能使用左值引用返回,只能传值返回。
我们自己实现string来进行场景的说明,其中涉及到对象的拷贝工作都是通过深拷贝来进行。

namespace na
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		//空构造
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::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 * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

定义函数to_string,将数字转化为字符串,string to_string(int value)函数,其中函数只能使用传值进行返回,而传值返回的过程至少得一次的拷贝构造,实际上为两次,有些编译器会进行优化。

namespace na
{
    //数字转化为字符串
	na::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		na::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()
{
	na::string ret = na::to_string(666);
	system("pause");
	return 0;
}

编译器进行了优化,只调用了一次拷贝构造函数。
在这里插入图片描述

在这里插入图片描述
程序的实现整个流程是:函数以值的方式返回,局部对象调用拷贝构造函数构造一份临时对象,即重新开辟一空间给临时对象,将自身对象中资源拷贝一份到该空间中,然后再将自身资源给释放掉,第二次的拷贝构造过程也是如此。

🤔:那既然在拷贝构造工作完成后,原来对象的空间要进行释放,而l构造的对象又得使用空间,那既然一个不使用空间,一个要使用空间,我们能否直接将原来对象的空间不进行释放,而是给新对象来使用呢?即将资源进行转移

在此思想基础上,这就引入了右值引用和移动语义。
所谓移动语义,即C++11中新增的移动构造和移动赋值。移动构造本质是将参数右值的资源窃取过来,占为已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

	// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) --> 移动语义" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) --> 移动语义" << endl;
			swap(s);
			return *this;
		}

当进行新对象的构造的时候,不将原对象的空间释放掉,而是进行空间资源的转移,将原对象的空间给新对象使用,这就少却了空间的开辟和数据的拷贝工作,提高了程序运行效率。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. 完美转发

在有些场景下,我们需要将左值转化为右值去实现移动语义,这就通过std::move()函数来讲左值转化为右值。而何为完美转发,看下面例子。

void fun(int& x)
{
	cout << "左值引用" << endl;
}
void fun(const int& x)
{
	cout << "const 左值引用" << endl;
}
void fun(int&& x)
{
	cout << "右值引用" << endl;
}
void fun(const int&& x)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void Perfect(T&& t)
{
	fun(t);
}

在这里插入图片描述
我们可以看到好像程序运行结果和我们所设想的不太一样,函数的传参都是作为左值来进行传递的。
这是在调用Perfect函数时,调用fun函数时,编译器将传入的参数t都是作为左值来进行传递。

std::forward完美转发:在参数的传递过程中保留对象的原生态属性

将函数模板修改一下,保留传入参数的原生态属性。

template<class T>
void Perfect(T&& t)
{
	fun(std::forward<T>(t));
}

在这里插入图片描述
这样就不会改变对象的原有属性。根据对象的属性来进行函数的匹配。

五、新的类功能

1. 默认的成员函数

在C++98中,在一个类中有六个默认的成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝运算符重载函数
  5. 取地址重载
  6. const取地址重载函数

当用户不显式的定义这些函数时,编译器会默认的生成一份。而在实际的操作过程中,编译器也不是只要用户没进行显式定义的话就一定会生成这些函数,如果生成默认的一份和没生成并没什么区别的话,编译器这时是不会进行生成的。

在C++11中新增加了两个:
7. 移动构造函数
8. 移动赋值运算符重载函数

编译器对于此新增的两函数的生成默认函数规则如下:

  • 若用户未实现移动构造函数,且未实现析构函数 、拷贝构造、赋值运算符重载中 的任意一个。那么编译器会自动生成一个默认移动构造。 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 若用户未实现移动赋值运算符重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
  • 若用户提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

2. 强制生成默认函数关键字default

关键字default用来表示,强制生成某函数。
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。
如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
vs2019验证此特性。

//对于Person类,强制编译器生成一份默认的移动拷贝构造函数
Person(Person&& p) = default;

3. 禁止生成默认函数关键字delete

delete关键字作用刚好与default相反,表示强制将某函数进行删除,禁止使用。
在这里插入图片描述


续:C++11相关特性介绍(二)

完!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值