c++11常用的新特性(上初学者必备)

x33g5p2x  于2022-05-16 转载在 其他  
字(11.9k)|赞(0)|评价(0)|浏览(327)

一.统一的列表初始化

统一列表初始化的使用

统一列表初始化的原理

列表初始化的其他优点

二.推导返回类型auto和decltype

auto 

decltype

三.范围for

四.nullptr

五.新增容器

std::array

std::forward_list

unordered系列

六.默认成员函数控制

七.右值引用

八.可变参数模板

参数包展开

一.统一的列表初始化

统一列表初始化的使用

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

int array1[] = {1,2,3,4,5};
int array2[5] = {0};

而对应一些自定义类型却不行比如说:

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

在c98中是无法编译成功的,我们只能够定义vector对应之后通过循环进行插入元素达到这个目的.C11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定 义的类型,使用初始化列表时,可添加等号(=),也可不添加

2.代码演示:

#include<iostream>
#include<algorithm>
#include<array>
#include<vector>
#include<map>
#include<initializer_list>
using namespace std;
class Point
{
public:
	Point(int x = 0, int y = 0) : _x(x), _y(y)
	{}
private:
	int _x;
	int _y;
};

int main()
{
	int x1 = { 0 };//定于内置类型
	int x2{ 3 };//也可以不加=
	//数组
	int arr1[5] = { 1,3,4,5,6 };
	int arr2[] = { 4,5,6,7,8 };
	
	//STL中的容器
	vector<int>v{ 12,2 };
	map<int, int>mp{ {1,2},{3,4} };
	//自定义类型初始化
	Point p{ 1, 2 };
	

	return 0;
}

统一列表初始化的原理

通在自定义类中也支持这种花括号就需要用到 C++11 引入的新对象 std::initializer_list,由表达式可知这是一个模板对象,接收一个自定义参数类型 T,T 既可以是基础数据类型(如编译器内置的 bool、char、int 等)也可以是自定义复杂数据类型。为了使用 std::initializer_list,需要包含头文件 initializer_list。

下面通过一段代码展示列表初始化的的原理:

template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		vector<T>(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();
			取类模板里面的内嵌类型,类模板没有被实例化
			typename 告诉编译器iterator是一个类型等到initializer_list实例化之后你再去取

			   //while (lit != l.end())
			   //{
			   //	*vit++ = *lit++;
			   //}
			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;
	};

注意:如果我们想要取类模板里面的内嵌类型而类模板没有实例化我们可以使用typename关键字告诉编译器等类模板实例化之后再去去暂时通过编译。还有就是initializer_list 的对象是常量,不能被修改initializer_list 对象中的元素必须完全一致。

列表初始化的其他优点

使用列表初始化还有一个很大的优势,可以防止类型收窄。类型收窄一般是指一些可以使得数据变化或者精度丢失的隐式类型转换。可能导致类型收窄的场景有:

1.从浮点数隐式转换成整数。比如: int a=1.2
2.从高精度的浮点数转换成低精度的浮点数,比如 long long double 转换成 long double ,或者 double 转换成 float
3.从整形转换成浮点型。如果整形的大小已经超过浮点型的表示范围 ,也属于类型收窄。
4.从整形转换成更低长度的整形,如 char a = 1024;

int a = 1024;
int b = 254;

char e = a;  //编译通过
char d{a};	 //编译不通过
char e{b};	 //编译不通过

二.推导返回类型auto和decltype

auto 

1.做类型推导
auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用,对 auto 的语义变更也就非常自然了。

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。在以前我们需要这样来书写一个迭代器
 

int main()
{
	vector<int>v{ 1,2,3,4,5,6 };
	vector<int>::iterator it = v.begin();//

	return 0;
}

给我们的感觉就是名字特别的长,感觉使用不方便。 

但是有了auto之后:

int main()
{
	vector<int>v{ 1,2,3,4,5,6 };
	//vector<int>::iterator it = v.begin();
	auto it = v.begin();
	//相比上面这种方式就简洁不少
	return 0;
}

2.const和auto

int  x = 0;
const  auto n = x;  //n 为 const int ,auto 被推导为 int
auto f = n;      //f 为 const int,auto 被推导为 int(const 属性被抛弃)
const auto &r1 = x;  //r1 为 const int& 类型,auto 被推导为 int
auto &r2 = r1;  //r1 为 const int& 类型,auto 被推导为 const int 类型`在这里插入代码片`

解释一下:

1.第 2 行代码中,n 为 const int,auto 被推导为 int。
2.第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时, auto 不会使用 const 属性,而是直接推导出 non-const 类型。
3.第 4 行代码中,auto 被推导为 int 类型,这个很容易理解,不再赘述。
4.第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结合时,auto 的推导将保留表达式的 const 类型。
总结:

1.当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;

2.当类型为引用时,auto 的推导结果将保留表达式的 const 属性。

3.auto的限制

1.auto 不能在函数的参数中使用。

auto func(int a,int b)
{
  
  return a+b;
}

2.auto 还不能用于推导数组类型:

#include <iostream>

int main() {
 auto i = 5;

 int arr[10] = {0};
 auto auto_arr = arr;
 auto auto_arr2[10] = arr;

 return 0;
}

3.auto 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中。

4.auto 不能作用于模板参数,请看下面的例子:

template <typename T>
class A{
    //TODO:
};
int  main(){
    A<int> C1;
    A<auto> C2 = C1;  //错误
    return 0;
}

4.auto的高级用法

auto 除了可以独立使用,还可以和某些具体类型混合使用,这样 auto 表示的就是“半个”类型,而不是完整的类型。请看下面的代码:

int  x = 0;
auto *p1 = &x;   //p1 为 int *,auto 推导为 int
auto  p2 = &x;   //p2 为 int*,auto 推导为 int*
auto &r1  = x;   //r1 为 int&,auto 推导为 int
auto r2 = r1;    //r2 为  int,auto 推导为 int

解释:

第 2 行代码中,p1 为 int* 类型,也即 auto * 为 int ,所以 auto 被推导成了 int 类型。
第 3 行代码中,auto 被推导为 int
类型,前边的例子也已经演示过了。
第 4 行代码中,r1 为 int & 类型,auto 被推导为 int 类型。
第 5 行代码是需要重点说明的,r1 本来是 int& 类型,但是 auto 却被推导为 int 类型,这表明当=右边的表达式是一个引用类型时,auto 会把引用抛弃,直接推导出它的原始类型。

decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 sizeof 很相似.在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。有时候,我们可能需要计算某个表达式的类型,例如:

auto x = 1;
auto y = 2;
decltype(x+y) z;

或者是后面的lambda表达式如果我们想要使用它的类型我们就需要使用decltype。

c++在泛型编程中,可能需要通过参数的运算来得到返回值的类型。考虑下面这个场景:

template <typename R, typename T, typename U>
R add(T t, U u)
{
    return t+u;
}
int a = 1; float b = 2.0;
auto c = add<decltype(a + b)>(a, b);

们并不关心 a+b 的类型是什么,因此,只需要通过 decltype(a+b) 直接得到返回值类型即可。但是像上面这样使用十分不方便,因为外部其实并不知道参数之间应该如何运算,只有 add 函数才知道返回值应当如何推导。那么,在 add 函数的定义上能不能直接通过 decltype 拿到返回值呢?

template <typename T, typename U>
decltype(t + u) add(T t, U u)  // error: t、u尚未定义
{
    return t + u;
}

当然,直接像上面这样写是编译不过的。因为 t、u 在参数列表中,而 C++ 的返回值是前置语法,在返回值定义的时候参数变量还不存在。

可行的写法如下:

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
    return t + u;
}

为了进一步说明这个语法,再看另一个例子:

int& foo(int& i);
float foo(float& f);
template <typename T>
auto func(T& val) -> decltype(foo(val))
{
    return foo(val);
}

注意:从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:

template<typename T, typename U>
auto add(T x, U y) {
    return x+y;
}

三.范围for

C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句。最常用的 std::vector 遍历将从原来的样子:

int main()
{
	vector<int>v{ 1,2,3,4,5,6 };
	for (auto x : v)//范围for
	{
		cout << x << endl;
	}
	return 0;
}

四.nullptr

1.nullptr 出现的目的是为了替代 NULL。在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。

2.C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译char *ch = NULL;时,NULL 只好被定义为 0。

而这依然会产生问题,将导致了 C++ 中重载特性会发生混乱,下面我们来看一个例子:

void func(int* ptr)
{
	cout << "匹配指针" << endl;
}
void func(int ptr)
{
	cout << "匹配的是整型" << endl;
}
int main()
{
	func(NULL);
	return 0;
}

可以会有人认为匹配的是匹配指针但是实际上匹配的是整型:

对于这两个函数来说,如果 NULL 又被定义为了 0 那么 func(NULL); 这个语句将会去调用 func(int),从而导致代码违反直观。

为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。所以一般建议使用nullptr代替NULL。

五.新增容器

std::array

1.std::array 保存在栈内存中,相比堆内存中的 std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。

2.std::array 会在编译时创建一个固定大小的数组,std::array 不能够被隐式的转换成指针,使用 std::array只需指定其类型和大小即可:
 

std::array<int, 4> arr= {1,2,3,4};

int len = 4;
std::array<int, len> arr = {1,2,3,4}; // 非法, 数组大小参数必须是常量表达式

下面在举个小案例进行演示:

void foo(int *p, int len) {
    return;
}

std::array<int 4> arr = {1,2,3,4};

// C 风格接口传参
// foo(arr, arr.size());           // 非法, 无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());

// 使用 `std::sort`
std::sort(arr.begin(), arr.end());

std::forward_list

std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。
和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率.但其实相比list而言也没啥用。

unordered系列

C++11 引入了两组无序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。前面以及介绍过了。无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(1).

六.默认成员函数控制

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

6.1显式缺省函数

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;

当我们写了构造函数时,编译器就不会给我们生产默认的成员函数了。如果此时我们依然想要编译器给我们生成此时我们就可以使用=delete

6.2 删除默认函数

如果我们想要限制一些默认函数的生成,在C98中,可以把该函数设为私有,不定义,这样,如果有人调用就会报错。在C11中,可以给该函数声明加上=delete就可以。

class A
{
A(int a)
	:_a(a)
{}
A(constA&) = delete;  // 禁止编译器生成默认的拷贝构造函数
private:
	int _a;
};

七.右值引用

C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通

过指针来实现的,因此使用引用,可以提高程序的可读性。 下面我们通过引用交换两个变量的值

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

为了提高程序运行效率C++11****中引入了右值引用,右值引用也是别名,但其只能对右值引用。

示例:

int Add(int a, int b)
{
	return a + b;
}
int main()
{
    //右值示例
    const int&& ra = 10;
    // 引用函数返回值,返回值是一个临时变量,为右值
    int&& rRet = Add(10, 20);
    return 0;
}

注:为了与C98中的引用进行区分,C11将该种方式称之为右值引用

** 左值和右值:**

  1. 左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式
  2. 一般认为:左值可放在赋值符号的左边,右值可以放在复制符号的右边;或者能够取地址的称为左值,不能取地址的称为右值
  3. :左值也能放在赋值符号的右边,右值只能放在赋值符号的右边

示例:

int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()
{
	return g_a;
}
int main()
{
	int a = 10;
	int b = a;
    int* p=new int(0);
	// a和b,p和*p都是左值,说明:左值既可放在=的左侧,也可放在=的右侧
	const int c = 30;
	//c = a;
	// 特例:c虽然是左值,但是为const常量,只读不允许被修改
	cout << &c << endl;
	// c可以取地址,所以c严格来看也是左值
	//b + 1 = 20;
	// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
	GetG_A() = 100;
	return 0;
}

下面让我们一起来探讨一下为什么c++11要引入右值

  1. 本质上引用都是用来减少拷贝移动资源,提高效率的
  2. 左值引用来解决大部分的场景,比如参数引用,返回值引用
  3. 右值引用是堆左值引用在一些盲区的补充,比如将亡值返回
  4. 如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错。
  5. 右值引用的作用实现移动语义(移动构造与移动赋值),给中间临时变量取别名

示例:

class String
{
public:
	String(const char* str)
	{
		if (nullptr == str)
			return;

		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
        cout << "构造" << endl;
	}
	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
        cout << "拷贝构造" << endl;
	}
	String & operator=(const String & s)
	{
		if (this != &s)
		{
			char* pTemp = new char[strlen(s._str) + 1];
			strcpy(pTemp, s._str);
			delete[] _str;
			_str = pTemp;
		}
        cout << "拷贝赋值" << endl;
		return *this;
	}
	String operator+(const String& s)
	{
		char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
		strcpy(pTemp, _str);
		strcpy(pTemp + strlen(_str), s._str);
		String strRet(pTemp);
		return strRet;
	}
	~String()
	{
		if (_str) delete[] _str;
	}
private:
	char* _str;
};
int main()
{
	String s1("hello");
	String s2("world");
	String s3(s1 + s2);
	return 0;
}

在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了
也就是说strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大。

移动构造语义:

C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。

解释:对于像strRet本来是左值,但是这样的在函数体内出作用域即销毁的变量,编译器会优化识别为是一种将亡值,即为右值
此处为值传递,会进行临时变量的拷贝,对于右值来说既能匹配参数类型是
const左值引用的拷贝构造函数,也能匹配参数类型是右值引用的拷贝构造函数,但是编译器会进行匹配类型最合适的函数,即右值引用拷贝构造函数这里的参数为右值引用的拷贝构造函数也叫做移动构造,即对将亡值进行资源的转移,转移到新的构造对象上,而对于将亡值是没有影响的即在用strRet构造临时对象时,就会采用移动构造。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率

示例:

String(String&& s)
	: _str(s._str)
{
	s._str = nullptr;
    cout << "移动拷贝" << endl;
}

总结:

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己,也就是说资源的生命周期被延长了(对象的生命周期不会改变)

下面我们来谈一下移动构造和移动赋值:

1.如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造
2.默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值
3.默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(和默认移动构造完全类似)
4.如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

示例:

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	/*Person(const Person& p)
	:_name(p._name)
	,_age(p._age)
	{}*/
	/*Person& operator=(const Person& p)
	{
	if(this != &p)
	{
	_name = p._name;
	_age = p._age;
	}
	return *this;
	}*/
	/*~Person()
	{}*/
private:
	String _name;
	int _age;
};
int main()
{
	Person s1{"zhangsan",18};
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

八.可变参数模板

  1. C++98/03,类模版和函数模版中只能含固定数量的模版参数
  2. C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模

由于可变参数模板比较复杂在这里我们只简单的学习。

//函数模板的参数个数为0到多个参数,每个参数的类型可以各不相同
template<class...T>
void func(T...args) {//args一包形参,T一包类型
	cout << sizeof...(args) << endl;//sizeof...固定语法格式计算获取到模板参数的个数
	cout << sizeof...(T) << endl;//注意sizeof...只能计算...的可变参
}

解释说明:

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。sizeof...固定语法格式计算获取到模板参数的个数,注意:sizeof...只能计算...的可变参

参数包展开

方法一:递归函数方式展开参数包

实现一个递归终止函数
void func() {
	cout << "递归终止函数" << endl;
}
template<class T,class ...U>
void func(T frist,U...others) {
	cout << "收到的参数值:" << frist << endl;
	func(others...);//注意这里传进来的是一包形参不能省略...
}

方法二:使用if constexpr

//c++17中新增一个语句叫做编译期间if语句( constexpr if)
//if constexpr...()//constexpr代表的是常量的意思或者是编译时求值

template<class T,class...U>
void func(T frist, U...args) {
	cout << "收到参数:" << frist << endl;
	if constexpr(sizeof...(args)>0) {//constexpr必须有否则无法编译成功,圆括号里面是常量表达式

		func(args...);
	}

}

注意:

1.可变参数模板中,传递进来的一包实参如何展开:一般采用递归函数的方式展开参数包   要展开,要求,在变参函数模板代码中有一个参数展开包函数以及一个同名的递归终止函数是允许的。

  1. 深入理解if constexpr不满足条件的分支也会被编译器进行语法检查,f constexpr理解成普通if语句只是判断条件从执行期间到了编译期间
       if constexpr完善了模板与泛型编程中程序执行路径的选择问题。

相关文章