C++11常用特性

C++11常用特性,第1张

文章目录
  • 🌲C++11
    • 🍃什么是C++11
    • 🍃新特性
      • 🌴{}初始化
        • 🌵简单实例
        • 🌵{}的类型
      • 🌴auto
        • 🌵简单实例
      • 🌴decltype
      • 🌴nullptr
      • 🌴范围for
      • 🌴左右值引用
        • 🌵左值和右值是什么
        • 🌵左值和右值的区分
        • 🌵左右值引用
        • 🌵左右值引用的作用
        • 🌵移动语义
          • 🥝场景
          • 🥝move
        • 🌵完美转发
      • 🌴default、delete、final、override
      • 🌴可变参数模板
        • 🌵递归展开
        • 🌵逗号表达式展开
        • 🌵emplace_back和push_back
      • 🌴STL增加的容器
      • 🌴lambda表达式
        • 🌵lambda参数解释
          • 🥝捕捉列表
          • 🥝参数列表
          • 🥝mutable
          • 🥝->返回值类型
          • 🥝{函数体}
          • 🥝关于省略
      • 🌴包装器
        • 🌵用法
        • 🌵bind
          • 🥝绑定普通函数
          • 🥝绑定成员函数
  • 🌲总结

🌲C++11 🍃什么是C++11

C++11标准是 ISO/IEC 14882:2011 - Information technology – Programming languages – C++ 的简称 [1] 。

C++11标准由国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C++标准委员会(ISO/IEC JTC1/SC22/WG21)于2011年8月12日公布 [2] ,并于2011年9月出版。2012年2月28日的国际标准草案(N3376)是最接近于C++11标准的草案(仅编辑上的修正)。此次标准为C++98发布后13年来第一次重大修正。–百度百科

翻译一下,2011年发布的C++标准,对C++98的一次重大修正。

🍃新特性

下面的新特性有{}初始化,auto,declytype,nullptr,范围for,STL的变化,左右值引用,lambda表达式,包装器

智能指针和线程库之后补充。

🌴{}初始化

C++11提供了{}初始化的方法。

🌵简单实例
int main()
{
	//{}的简单使用
	using namespace std;
	int arr1[] = { 1,2,3,4,5 };
	int arr2[]{ 1,2,3,4,5 };

	pair(1, 1);
	pair{1, 1};

	pair* pr = new pair[2]{ {"sort",1} ,{"good",2} };

	return 0;
}

🌵{}的类型

{}在C++11里面是一个类型,initializer_list,initializer_list - C++ Reference (cplusplus.com),也叫初始化列表。

template<class T> 
class initializer_list;

initializer_list提供了迭代器,所以可以遍历列表内的元素。

以vector为例,里面提供了这种类型的构造函数,所以vector < int > v{1,2,3}这种用法就是在调用构造函数

我们可以想想可以怎么实现这个功能,遍历initializer_list的元素,挨个赋值给vector容器即可。(没看库里怎么实现的,不过是阐述一种可行的思路

STL库中的大多容器都增加了initializer_list相关的构造函数

用初始化列表可以加强类型安全。比如int a=1.2,最后a存储的是1,这就是类型收窄。一些初始化列表的使用会导致了类型收窄,这是不合法的(编译器报错),总之建议使用初始化列表初始化。

🌴auto

C++98是有auto的,不过在C++11被废弃了,我们现在的int a,就是C++98的auto int a

auto是一个存储类型的说明符,作用是推导类型。

🌵简单实例
int main()
{
	auto a = 0;
	auto b = 1L;
	auto c = 1.1;
	std::vector<std::string>::iterator it;
	auto d = it;
	
	return 0;
}
//如果有一个类型很长比如std::vectorstring_v;还要去写它的迭代器就更麻烦了,这种情况下就可以用auto去代替

auto一定程度的降低了程序的可读性,并且我们不应该滥用auto.

滥用auto看起来更方便,实际上会造成很多问题,比如难以维护,再比如

string str="ccl";
auto s=str;
s.size();
//上面的代码是合理的,但是当我们有一天将string改为const char*时,虽然auto方面没有问题,但是依赖于string类型的代码全部出问题了。
🌴decltype

将变量的类型指定为一个表达式的类型

typeid(z).name返回的是一个字符串,所以肯定不能拿其返回值当类型了

🌴nullptr

nullptr是一个对象,nullptr的类型是nullptr_t,并不等同于((void*)0),nullptr可以隐式的转为任意类型的指针,同时nullptr不能取地址。此外由于nullptr有类型所以是可以捕捉异常的。

//网上找的一种可能的实现
struct nullptr_t
{
   void operator&() const = delete;  
   template <class T>
   inline operator T*() const
   {
       return 0;
   }
   template <class C, class T>
   inline operator T C::*() const
   {
       return 0;
   }
};

nullptr_t nullptr;
🌴范围for

遍历容器内的所有元素,如果要修改需要加引用,本质上就是迭代器的遍历。

VS监视小技巧

🌴左右值引用 🌵左值和右值是什么

左值就是可以取地址的,右值就是不可以取地址的。

我们经常可以知道一些关于左右值的判断,而很少听到其真正的定义的一个原因是很难归纳,而且就算归纳了,也需要大量的解释。–《深入理解C++11》

右值由两个概念构成,一个是将亡值,一个是纯右值,也有人说右值的别名就是将亡值,因为生动形象,这里我们采用后一种说法。

也有人把左右值定义为,左值可以读写,右值只能读不能写。

🌵左值和右值的区分
  • 赋值表达式中,左边的全是左值,右边的不一定都是右值,但是右值一定在右边。听起来有点绕
int main()
{
	int i = 1;
	int j = i;
	return 0;
}

比如a=b+c,&(b+c)编译时报错,即(b+c)的结果是一个临时变量不能取地址,所以b+c是右值,a是左值。

  • 将亡值,听名字就知道是一个即将死亡的值,换句话说,生命周期结束的值。比如一些临时变量的值,一些字面值,lambda表达式,都可以看做右值(将亡值)。

🌰 1,‘a‘ , true之类的都是字面值,也都是右值

🌰 一些临时变量,比如一些函数的返回值(返回值是左值引用的另说),1+3,“123”+“456”,lambda表达式,函数sum(1,2)的返回值,这些都是右值,将亡值的概念用在这就十分合适,用完就销毁了。

单个变量的引用也算左值

🌵左右值引用
  • 左值引用,接收左值的引用+const引用

比如T& 和const T&,const T&可以接受右值

const引用可以理解为接收一个临时的左值,const左值引用只能取地址不能赋值,const左值引用既能接收左值也能接收右值,算是一个例外。

move(左值)的作用是把这个左值转为一个右值,但是也可能带来一些问题,比如数据丢失。

  • 右值引用,T&&,两个&是为了区分是左值引用还是右值引用
int main()
{
	int a = 1;

	//左值引用
	int& b = a;
	const int& x = 1;//const引用是例外
	int& c = 5;//err

	//右值引用
	int&& m = 1;
	int&& n = a;//err
	int&& p = move(a);
	

	return 0;
}

左右值引用也符合精准匹配原则,哪个最符合参数类型就会选择哪个函数。

右值引用后被存储到一个特定的位置,如下所示

🌵左右值引用的作用

看到这,大概对左右值有了一个大概的认识。左值引用我们之前就知道,作用是可以减少拷贝,一定程度上替代了指针(指针有些地方是无法替代的),右值引用的作用是实现移动语义和完美转发。

移动语义:减少拷贝,提高效率

完美转发:保持原来的值属性不变。比如传参,左值传过去还是左值,右值传过去还是右值,利用这一点也可以提高程序的性能,比如减少拷贝。

简单来说,右值引用提高了程序的性能。(在某些场景下提升很大)

🌵移动语义

假如现在需要一个一次性对象A来拷贝构造另一个生命周期更长的对象B,一次性对象A用完就销毁了,但是拷贝构造B前还是得去构造这个一次性对象A,是不是觉得有点浪费。

反正这个一次性对象A的作用也是拷贝出另一个对象B,反正一次性对象A用完就要销毁了,那我们有没有一种办法把A的资源“偷走”给拷贝出来的对象B,这样不就可以减少一次拷贝构造。

移动语义可以解决这个问题。

🥝场景

看下面的场景

class String
{
public:
	String() = default;
	String(const char* string)
	{
		printf("构造\n");
		_size = strlen(string);
		_string = new char[_size];
		memcpy(_string, string, _size);
	}
	String(const String& other)
	{
		printf("深拷贝\n");
		_size = other._size;
		_string = new char[_size];
		memcpy(_string, other._string, _size);
	}
	~String()
	{
		delete[] _string;
	}
	void Print()
	{
		for (size_t i = 0; i < _size; i++)
		{
			printf("%c", _string[i]);
		}
		printf("\n");
	}
private:
	char* _string;
	size_t _size;
};
class Entity
{
public:
	Entity(const String& name)//可以看出构造出来的就只在这里用了
		:_name(name)//拷贝构造
	{
		printf("构造Entity\n");
	}
	void PrintName()
	{
		_name.Print();
	}
private:
	String _name;
};
int main()
{
	Entity entity("ccl");
	entity.PrintName();
	return 0;
}

场景中打印entity对象的名字就得需要先构造一个临时对象name,再拿临时对象name去拷贝构造出Entity类的成员_name,这就是两次深拷贝。

运行结果也印证了深拷贝两次。

那移动语义怎么解决这个问题呢?

或者说移动语义怎么做到偷走它的资源的呢。思路上就是把要用的指针指向本该销毁的内存,再把一次性对象的指针置空,之后一次性对象生命周期结束调用析构函数也不过是delete nullptr,这是合法的。

具体代码可以这样做:

String增加一个移动构造,Entity的构造也增加一个右值的版本由于匹配右值“ccl”

看看具体的更改:

代码汇总:

class String
{
public:
	String() = default;
	String(const char* string)
	{
		printf("构造\n");
		_size = strlen(string);
		_string = new char[_size];
		memcpy(_string, string, _size);
	}
	String(const String& other)
	{
		printf("深拷贝\n");
		_size = other._size;
		_string = new char[_size];
		memcpy(_string, other._string, _size);
	}
	String(String&& other) noexcept//提供一个移动构造的函数 参数是右值
	{
		_string = other._string;
		_size = other._size;
		other._string = nullptr;//必须处理这个临时的字符串,不然两个指针指向同一块内存
		other._size = 0;
	}
	~String()
	{
		delete[] _string;
	}
	void Print()
	{
		for (size_t i = 0; i < _size; i++)
		{
			printf("%c", _string[i]);
		}
		printf("\n");
	}
private:
	char* _string;
	size_t _size;
};
class Entity
{
public:
	Entity(const String& name)//可以看出构造出来的就只在这里用了
		:_name(name)//拷贝构造
	{
		printf("构造Entity\n");
	}
	Entity(String&& name)
		:_name(std::move(name))//move可以把左值转换为右值,用于匹配String的移动构造
	{
		printf("移动构造Entity\n");
	}
	void PrintName()
	{
		_name.Print();
	}
private:
	String _name;
};
int main()
{
	Entity entity("ccl");
	entity.PrintName();
	return 0;
}

再来看看结果

比对一下增加移动拷贝前后的结果

随笔记录:编译器优化,如果存在连续的构造等就会进行优化。

  • 为什么不直接进行优化?当没有对象接收时如果不产生临时变量会出问题,拿到的就是随机值

  • 利用将亡值的特点提高效率,接管即将销毁的空间,减少了一次深拷贝构造

  • 不优化的情况下,不管是左值还是右值都是两次构造,只是移动构造效率比左值效率要高不少

  • 一个深拷贝的类可以进一步实现移动拷贝,面对一些函数值返回的场景(返回的值是会借助临时变量,临时变量是右值),可以进一步的减少深拷贝,提升效率。

  • 很多旧容器都增加了一些右值引用相关的接口提升效率。比如vector

最后,移动语义本质上允许我们移动对象,换句话说你可以吧一个已经存在的变量转成临时变量,你可以从这个特定的变量中窃取资源,也就达到了提高性能的目的。

🥝move

move的作用就是将左值转为右值。

上面通过拷贝构造窃取了资源,那假如我们想通过赋值来直接窃取资源呢?重载=即可。

注意点:处理旧数据,不能窃取自己的资源,防止一块内存被析构两次

String& operator=(String&& other)
{
	if (this != &other)//窃取的不是自己的资源
	{
		//处理旧数据,防止内存泄漏
		delete[] _string;
		//窃取资源
		_string = other._string;
		_size = other._size;
		//防止一块内存被析构两次
		other._string = nullptr;
		other._size = 0;
        return *this;
	}
}

调用时得用move转成右值取匹配赋值的右值版本。

用move时得注意可能把数据转走了,毕竟是把左值转成了右值去使用,要是资源被窃取走了还去用就会造成一下问题。

🌵完美转发

模板里的&&叫做万能引用,而不是右值引用。

void Test(int&)
{
	cout << "左值引用" << endl;
}
void Test(const int&)
{
	cout << "const 左值引用" << endl;
}
void Test(int&&)
{
	cout << "右值引用" << endl;
}
void Test(const int&&)
{
	cout << "const 右值引用" << endl;
}
template
void PerfectForward(T&& t)//万能引用
{
	Test(t);//万能引用  右值接受后变成左值
}
int main()
{
	int a = 1;
	const int b = 1;
	PerfectForward(a);
	PerfectForward(b);
	PerfectForward(1);
	return 0;
}

万能引用改变了值的左右值属性,该怎么保留之前的属性呢,此时就需要std::forward完美转发保留传参过程中对象的原生类型属性。

使用万能引用

template
void PerfectForward(T&& t)//万能引用
{
	Test(std::forward(t));
}

运行结果,发现右值保留了其属性。

C++11的类里面新增了两个默认成员函数,即移动构造函数和移动赋值运算符重载。

  • 移动构造函数:自己没有实现移动构造且没有实现析构、拷贝构造、拷贝赋值重载才会生成默认的移动构造。

默认的移动构造对于内置类型逐字节拷贝,自定义类型调用其移动构造,若没有实现移动构造就调用其拷贝构造。

  • 移动赋值重载函数:自己没有实现且没有实现析构、拷贝构造、拷贝赋值重载会生成默认的移动赋值。

默认的移动赋值对于内置类型逐字节拷贝,自定义类型调用其移动赋值,若没有实现移动构造就调用其拷贝赋值。

🌴default、delete、final、override

成员函数加上=default表示强制生成默认的函数。

成员函数加上=delete表示删除这个函数,表现为只声明不实现。

final,被final修饰的类不能继承,修饰的变量成为常量且经初始化后不能改变,修饰的虚函数子类无法重写。

override:写在函数参数列表后面表示这个函数重写了父类的虚函数,编译器就会去检查是否重写了,如果没有重写就会报错。

🌴可变参数模板

可变参数就是参数的个数和类型都是任意的。

最典型的可变参数的使用就是printf了,不在乎有几个参数,反正都能接收。

除了这个外,还可以想想Linux里的命令,后面可以加好几个修饰符,参数的个数类型等都是不定的。

下面就是一个可变参数的函数模板。

template//...表示模板的个数是不固定的
void ShowArguList(Args...agrs)
{}

我们把…成为参数包,我们无法直接获取参数包里的具体参数,需要一点特别的办法来展开参数包获取具体的参数,下面列举两种方法:递归式和逗号表达式展开

可以拿到每个参数的值,自然也可以拿到值的类型。

🌵递归展开
template
void ShowArguList(T value)//递归终止
{
	cout << value << endl;
	
}
template//...表示模板的个数是不固定的
void ShowArguList(T value,Args...args)
{
	cout << value << " ";
	ShowArguList(args...);
}

int main()//调用
{
	ShowArguList(1);
	ShowArguList(2, 'b');
	ShowArguList(3, 'c', 3.33);
	ShowArguList(4, 'd', "hello");
	return 0;
}

🌵逗号表达式展开
template
void ShowArguList(T value)
{
	cout << value <<" ";
}
template
void ShowArguList(Args ... args)
{
	int arr[] = { (ShowArguList(args),0)... };
	cout << endl;
}
int main()
{
	/*ShowArguList(1);*/
	ShowArguList(2, 'b');
	ShowArguList(3, 'c', 3.33);
	ShowArguList(4, 'd', "hello");
	return 0;
}

逗号表达式那一行可以理解为一种语法,理解不了就记

(ShowArguList(args),0)…是C++17里的折叠表达式(fold expression),有兴趣可以百度。

🌵emplace_back和push_back

C++11之后就有了一系列的emplace函数,以STL中list的emplace_back(尾插)为例。

可以看到参数列表是可变参数和万能引用。直接给结论:emplace_back的效率确实比push_back高一点。

一般对于传过来的参数都是就地构造(不需要移动或者拷贝),而push_back需要先构造出来再拷贝,如果传的是右值且存在移动构造,那emplace_back比push_back高效一点(可以少进行一次移动构造,详情可以参考官方文档的实例👉std::list::emplace_back - cppreference.com)

总的来说,插入右值且存在移动拷贝的情况下其实两者效率中emplace_back小优,但是差不了太多,因为只节约了一次移动构造。我的VS2022对push_back还进行了优化,下面是vs2022对push_back的优化。。。

下面是我碰到没解决的一个问题,编译器是VS2022,背地里做了太多优化,在此也希望大家不必陷入这种语法细节的泥沼!

🌴STL增加的容器

可以看出加了四个容器。

  • array,这个容器可以放越界,因为重载了operator[],越界直接报错,这也算是一个优点了…

原生数组的越界的检查得看编译器,有时检查的到有时检查不到,有点玄学

  • forward_list,单链表,list也是链表,不过底层是双向循环链表。所以forward_list的好处就是每个元素少存储了一个指针,少四个字节,一百万个元素也就是四百万个字节,也就是4M左右。也算是优点吧。。。此外forward_list没有提供尾插尾删,单链表尾插尾删要找尾,效率不高,所以没有提供相应的接口。

  • unordered_map和unordered_set就比较有用了,传送门👉unordered_set与set的比较

🌴lambda表达式

lambda表达式可以看做是一次性的匿名的仿函数(对我们匿名,编译器内部会给他一个名字)

关于lambda表达式的场景,大多能用函数指针的地方都能用lambda替代。此外lambda也是有类型的,可以用auto接收。

这里简单提一下可调用类型

  • 什么是可调用类型?

类型定义的对象可以像函数一样去调用

  • 可调用类型包含哪几种?

函数指针,仿函数,lambda,包装器。

下面是lambda表达式的实例,说明lambda是有类型的,但是类型的字符串是编译器内部给的,这也是为什么说对于我们来说是"匿名"的,对于编译器来说不是匿名的原因。

没学lambda之前给一个数组排序:

学lambda之后给一个数组排序:

个人认为最大的好处是不用取名了,程序的可读性也有很大提升,别人一看就知道是升序还是降序

官方文档:Lambda expressions (since C++11) - cppreference.com

微软:C++ 中的 Lambda 表达式 | Microsoft Docs

官方文档已经讲得很详细了,这里给一下常用的用法。

微软给的图:

[capature-list](parameters)mutable->return-type {statement}

[捕捉列表](参数列表)->返回值类型{函数体}

🌵lambda参数解释 🥝捕捉列表

捕捉列表,捕捉列表能够捕捉上文中的变量,编译器根据[]来判断下面的代码是否是lambda表达式,且只能捕捉父作用域 。

捕捉列表使得lambda十分灵活,捕捉列表使得lambda可以拿到外部的变量。

  • []:什么都不捕捉,代表不能访问局部变量,只能访问全局变量。

  • [=]:当前函数栈帧内的所有变量以按值传递的方式捕捉,不能捕捉全局变量,但是可以用。

按值捕捉:

不能捕捉全局变量:

  • [&]:当前函数栈帧内的所有变量以按值传递的方式捕捉,不能捕捉全局变量,但是可以用。

  • [var]:以按值传递的方式捕捉变量var
  • [&var]:以引用的方式捕捉变量var

捕捉变量可以混着用:

简单来说,捕捉列表可以拿到上文数据,全局变量可以随便用(读写都行),捕捉主要针对的是局部变量,值传递的变量可读不能写(有const属性),引用捕捉的变量可读可写。

具体的多用用就会了。

🥝参数列表

和函数的一样

[](int a, int b) {return a < b; };
//(int a, int b)就是参数列表
🥝mutable

取消lambda函数的const属性,一般来说lambda都是一个const函数,mutable可以取消其常量属性,但是用了mutable参数列表就不能省略

auto f = [=, &c]()mutable {c = 4, cout << b << " " << c << endl; };
🥝->返回值类型
auto f = [=, &c]()->int {c = 4, cout << b << " " << c << endl; return 0; };
//->int表示返回值为int

可以省略,省略后编译器自动推导,但是写了的话必须就必须和参数列表一起。

🥝{函数体}

这个就不用多说了吧。

🥝关于省略
int main()
{
	[] {};
	[]() {};
	[]() ->int {return 0; };
	[]() mutable->int {return 0; };	
	return 0;
}

要是写lambda经常报错就给他写全了,看个人习惯,我的习惯是写全,这样可读性好一点。

lambda的实现还是仿函数,就像范围for的本质也不过是编译器,只不过编译器帮我们把很多事做了。

随笔记录:static在函数结束后还在

std::string析构时把size和capacity都置为0了

C++11之后STL库里的swap和algorithm里面的效率差不多了,因为有了右值引用。

const静态成员可以给缺省值(静态成员直接给缺省值是不行的)

🌴包装器

function - C++ Reference (cplusplus.com)

function包装器,也叫适配器,本质上是类模板。

头文件是functional。

🌵用法

function<返回值(参数列表)>包装器的名字=

你可能在想,这不就是给函数起个名字吗,有什么大不了的。

但是!function是一个类啊,类即类型,自定义类型就可以和STL容器等等交互,那场景一下就大了很多。

function因为是一个类型,所以可以玩出很多花样,比如下面的

int main()
{
	std::map>m
	{ {"+",[](int a,int b)->int {return a + b; }},
	  {"-",[](int a,int b)->int {return a - b; }},
	  {"*",[](int a,int b)->int {return a * b; }},
	  {"/",[](int a,int b)->int {return a / b; }}
	};
	cout << m["+"](1, 2) << endl;
	cout << m["-"](1, 2) << endl;
	cout << m["*"](1, 2) << endl;
	cout << m["/"](1, 2) << endl;
	return 0;
}

🌵bind

本质上是一个函数模板,不过未作探究

直接看用法吧

🥝绑定普通函数

固定(绑定)参数的值

int Sum(double a, double b)
{
	return a + b;
}
struct Sum2
{
	int operator()(double a, double b)
	{
		return a + b;
	}
};

int main()
{
	using namespace std::placeholders;
	auto func_sum2 = std::bind(Sum, 5, _2);
	cout << func_sum2(1, 2) << endl;

	return 0;
}

bind也可以调换参数的顺序,我暂时没发现使用场景…

bind也可以绑定函数模板、成员函数等等…

🥝绑定成员函数

绑定非静态的成员函树需要传this指针,用法如下

class Plus
{
public:
	int Sum(int a, int b)
	{
		return a + b;
	}
};

int main()
{
	using namespace std::placeholders;
	Plus plus;
	auto Plus_Sum = std::bind(&Plus::Sum, &plus, _1, _2);
	cout << Plus_Sum(1, 2) << endl;
	return 0;
}

C++11线程库的之后会单写一篇记录。

🌲总结
  • C++11增加了{}初始化的方式,某种程度上加强了类型安全

  • auto推导类型,decltype拿到类型。

  • nullptr不是指针,而是一个有类型的对象,范围for本质也不过是迭代器。

  • 左右值引用里的右值引用用于移动构造,提高效率。移动构造本质上是浅拷贝,一句话概括就是接管即将销毁的资源,也可以说”偷资源“

  • 给类生成默认的函数版本,删除某个成员函数(合理的有声明无定义)。

  • 可变参数模板拿到任意个参数,有两种方式进行解包。

  • STL增加了一些容器,比如unordered_map,unordered_set,某些场景下比map/set性能更优秀。

  • lambda可以看做是一个一次性的函数,用法很灵活。

  • 包装器可以包装函数,仿函数,lambda,给他们一个名字,由于包装器是一个类型,所以可以和一些容器用在一起实现一些花哨的 *** 作,包装器可以与bind一起用,bind可以用来绑定数据。

github代码汇总

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1353387.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-14
下一篇 2022-06-14

发表评论

登录后才能评论

评论列表(0条)

保存