【C++11】C++11常用特性详解


目录

一、列表初始化

1、一切皆可大括号

2、 std::initializer_list

2.1initializer_list的原理

2.2使用initializer_list仿写vector的构造函数 

二、auto、decltype、nullptr、范围for

1、auto

2、decltype

3、nullptr

4、范围for

三、STL新增容器和已有容器的新接口

1、STL新增容器

1.1array

1.2forward_list

2、已有容器的新接口

2.1cbegin()、cend()

2.2vector、string和deque的shrink_to_fit

2.3移动构造和移动赋值

2.4emplace_xxx插入接口

四、final和override

五、lambda表达式

1、lambda表达式介绍

2、lambda表达式的捕捉列表、mutable

3、lambda的底层就是仿函数

六、容器中新增的emplace系列

1、可变参数模板

参数包的解析

2、emplace系列的高效性 

七、包装器

1、function包装器

1.1包装器的意义

1.2包装器的使用

1.3包装器求解力扣150题《逆波兰表达式求值》

2、bind


自C++11开始。C++的语法就开始不太像C++98和C了。

一、列表初始化

1、一切皆可大括号

struct Point
{
	int _x;
	int _y;
};
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Point p1 = { 1,2 };
	Point P2{ 1,2 };

	int arr1[] = { 1,2 };
	int arr2[]{ 1,2 };

	int a1 = 1;
	int a2 = { 2 };
	int a3{ 3 };
    
	// C++11中列表初始化也可以适用于new表达式中
	int* p3 = new int[10];
	int* p4 = new int[10] {1, 2, 3};

	Point* p3 = new Point[2]{ { 1,1 },{ 2,2 } };

	//创建对象时也可以使用列表初始化方式调用构造函数初始化
	Date d1(1, 2, 3);
	//C++11支持的列表初始化,这里会调用构造函数初始化
	Date d2 = { 1,2,3 };//因为日期类支持三个参数的构造函数
	Date d3{ 1,2,3 };
	return 0;
};

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

2、std::initializer_list

2.1initializer_list的原理

initializer_list相当于一个容器,和vector很像,区别在于它不存储数据,有指针指向存储于常量区的数组。

这里的{}被识别成了一个类,这个类叫std::initializer_list。

上面的Date d2 = { 1,2,3 };是因为日期类支持了三个参数的构造函数,如果给它4个参数进行构造,将会报错。那为什么vector、list等支持不定参数的构造呢?并不是因为它们实现了多种不同参数的构造函数,而是因为这些容器中增加了std::initializer_list作为参数的构造函数。

int main()
{
    //支持不定参数的构造
	vector<int> v1 = { 1,2,3,4 };
	vector<int> v2{ 1,2,3,4,5,6 };
	return 0;
}

vector中有一个initializer_list的构造函数,外部传参构造vector时,使用传入的参数先构造了一个initializer_list对象,initializer_list再将接收到的参数“push_back”进vector,这样vector就能够拥有不定参数的构造能力了。

//里面的花括号是调用Date的构造函数,外面的花括号类型是initializer_list<Date>
vector<Date> v={
   
   {1,1,1,},{2,2,2},{3,3,3}};
//里面的花括号将生成pair的匿名对象,外面的花括号将生成initializer_list<pair<string,string>>的对象
map<string,string> dict = {
   
   {"字符串","string"},{"排序","sort"}};

2.2使用initializer_list仿写vector的构造函数 

class vector
{
public:
    vector(const initializer_list<T>& il)//initializer_list<T>& il是不行的,il具有常属性
        :_start(nullptr)
        ,_finish(nullptr)
        ,_endofstorage(nullptr)
    {
        typename initializer_list<T>::iterator it = il.begin();
        while (it != il.end())
        {
            push_back(*it);
            ++it;
        }
    }
	//......
};

二、auto、decltype、nullptr、范围for

1、auto

自动推导类型。非常好用。

2、decltype

typeid(a).name()返回的是变量类型的字符串;

而decltype只能用于推导一个变量的类型,给另一个变量的类型进行定义。decltype的使用场景:

template<class T1, class T2>
void F(T1 t1, T2 t2)
{
    //暂时不确定ret的类型,因为你不知道外部传入的T1,T2哪个会被提升
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}

当然这个例子你用auto也行,所以decltype是一个没啥用的特性。

3、nullptr

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

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

4、范围for

语法糖,本质是替换成迭代器。非常好用。

三、STL新增容器和已有容器的新接口

1、STL新增容器

C++11中STL新增了<array>、<forward_list>、<unordered_set>、<unordered_map>,后面两个在往期博客中已经专门介绍过了。

1.1array

array是固定大小的顺序容器。和vector相比,array中的数据并不要求挨着存储,所以并没有push_back。

array并不会对数组进行初始化,而vector的构造函数会对vector进行初始化。

array和普通数组在越界方面的比较:

普通数组对数组的边界是必检的,但对于其他位置则是抽查,如上图数组a1成功访问a1[20]的数据。

而array不一样,只要数组越界编译器必崩溃。

无论使用哪种方式玩定长数组都是可以的,不过个人认为写代码写出数组越界纯属个人菜,得认。

对于大多数的C系程序员来说,更愿意接受普通数组的写法,因为用习惯了,通俗易懂。其次,array并不会对数组进行初始化,它对数组越界进行了严格的检查,势必增加了开销,相对于普通数组唯一的优势是有错必报,但是vector不是比array更香吗?

1.2forward_list

这是一个单链表,只支持头插头删,不支持尾插尾删。

对比list的优点就是每一个节点可以节省一个指针的空间。

2、已有容器的新接口

2.1cbegin()、cend()

让人一看就知道这里调用的是const迭代器。

2.2vector、string和deque的shrink_to_fit

给vector提供了缩容方法。这可能会导致重新分配空间,但是不会影响vector的size。谁没事会去缩容啊。

2.3移动构造和移动赋值

很有用的特性,见【C++11】左值引用和右值引用

2.4emplace_xxx插入接口

性能强于C++98的push_back和insert等接口。具体见本文第六节。

四、final和override

final修饰类,表示该类不能被继承,final修饰虚函数,表示该虚函数不能被重写。

override修饰子类的虚函数,用于检查子类是否完成对该函数的重写。

五、lambda表达式

1、lambda表达式介绍

lambda表达式的作用是创建一个匿名函数,可以在需要函数对象的地方使用。它通常用于函数式编程中,可以避免定义一个命名函数,从而简化代码。使用lambda表达式可以在代码中直接定义一个函数对象,并将其作为参数传递给其他函数或算法,或者直接调用它。lambda表达式可以捕获其所在作用域的变量,并将其作为函数对象的成员变量,从而实现闭包功能。使用lambda表达式可以使代码更加简洁、清晰,提高程序的可读性和可维护性。

lambda本质是一个匿名函数,其表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,必须写

(parameters)参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略;

mutable默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空);

->returntype返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导

{statement}函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,必须写

2、lambda表达式的捕捉列表、mutable

这是一段lambda表达式对外部参数b的捕获:

int main()
{
	int a = 0, b = 1;
	auto add = [b](int a) {return a + b; };//把上文的b捕捉到lambda表达式中
	return 0;
}

捕捉的真的是b吗?其实捕捉的仅仅是带const属性的b的拷贝罢了。可使用mutable取消拷贝对象的const属性:

auto add = [b](int a) mutable{return a + b; };//取消b的const属性

现在我想用lambda表达式交换一下外部两个变量的值,那么就需要在捕捉列表中引用这两个变量:

int main()
{
	int a = 0, b = 1;
	auto swap = [&a, &b]()//这里是引用的方式捕捉,不是取地址
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	swap();
	std::cout << a << " " << b << std::endl;
	return 0;
}

上面的lambda表达式的捕捉列表中的&a和&b其实是a和b的引用,不要理解成取地址。因为捕捉只有两种方式:传值捕捉和传引用捕捉静态变量和全局变量无法捕捉,但可以在lambda表达式中直接使用并自带引用属性

[var]

传值捕捉

[&var]

传引用捕捉

[=]

表示值传递方式捕获上文已出现的变量(包括this)

[&]

表示引用传递捕捉上文已出现的变量(包括this)

[this]

表示值传递方式捕捉当前的this指针

[var1,&var2]、[=,&var]等

混合捕捉

无法重复捕捉,如:[=,var]

捕捉列表不允许变量重复传递,否则就会导致编译错误。

lambda表达式之间不能互相赋值

/

3、lambda的底层就是仿函数

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

六、容器中新增的emplace系列

1、可变参数模板

C++11的新增特性可变参数模板能够让使用者创建可以接受可变参数的函数模板和类模板。

参数包的解析

方式一:

方式二:这种方式不能解析0个参数的参数包

template <class T>
int PrintArgs(T t)
{
	std::cout << t << " ";
	return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
	//int arr[] = { (PrintArgs(args),0)... };//逗号表达式
    //参数包有几个值,数组就开多大,函数就被调用几次,函数调用时依次展开参数包中内容
	int arr[] = { PrintArgs(args)... };
	std::cout << std::endl;
}
int main()
{
	//ShowList();
	ShowList(1);
	ShowList(1,std::string("xxxxx"));
	return 0;
}

2、emplace系列的高效性 

int main()
{
	std::list<int> list1;
	list1.push_back(1);
	list1.emplace_back(2);
	list1.emplace_back();//会插入0
    
	std::list< std::pair<int, char> > mylist;
	mylist.push_back(make_pair(1, 'a'));  // 构造+拷贝构造(如果是右值就是构造+移动构造)
	//mylist.push_back(1, 'a');//push_back不允许传两个参数
    mylist.push_back({ 40, "sort" });//列表初始化
	mylist.emplace_back(1, 'a'); // empalce支持多参数,直接构造
	return 0;
}
int main()
{
	pair<int, std::string> kv(20, "sort");
	std::list< std::pair<int, std::string> > mylist;

	mylist.push_back(kv); // 左值
	mylist.push_back(make_pair(30, "sort")); // 右值
	mylist.push_back({ 40, "sort" }); // 右值

    mylist.emplace_back(kv); // 左值
	mylist.emplace_back(make_pair(20, "sort")); // 右值(简化为一次构造)
	mylist.emplace_back(10, "sort"); // 构造pair参数包(简化为一次构造)

	return 0;
}

无论是push_back还是emplace_back,尽量不要使用左值进行插入操作,插入左值必发生拷贝构造。

  1. push_back方法:

对于非深拷贝类型,push_back会调用构造+拷贝构造,如果元素是深拷贝的对象,就是构造+移动构造。

  1. emplace_back方法:

emplace_back方法会在容器尾部直接构造一个新元素,它的参数是元素的构造函数参数列表。emplace系列能将参数包展开,将过程简化为一次构造。

所以从效率上来说,emplace系列会高效一点.如果一个深拷贝的类没有实现移动构造,这个时候push_back的效率将远不如emplace_back。

七、包装器

1、function包装器

1.1包装器的意义

function包装器,也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

function的作用是对各种可调用对象进行类型统一

函数模板useF被实例化了三份,使用包装器可以解决这一问题。

只要返回值和参数一样就可以用function进行统一封装。

1.2包装器的使用

int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};
class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	// 函数名(函数指针)的三种写法
	function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;

	function<int(int, int)> func2;
	func2 = f;
	cout << func2(1, 2) << endl;

	function<int(int, int)> func3(f);
	cout << func3(1, 2) << endl;
	// 函数对象
	function<int(int, int)> func4 = Functor();
	cout << func4(1, 2) << endl;
	// lamber表达式
	function<int(int, int)> func5 = [](const int a, const int b)
	{return a + b; };
	cout << func5(1, 2) << endl;

	// 类的成员函数,包装器的静态成员函数可以不加&取地址,普通函数需要加&取地址,建议一律加上
	function<int(int, int)> func6 = &Plus::plusi;//类静态成员函数指针,&可加可不加
	cout << func6(1, 2) << endl;
	function<double(Plus, double, double)> func7 = &Plus::plusd;//类成员函数指针,&必须加,并且需要传类名
	cout << func7(Plus(), 1.1, 2.2) << endl;//调用的时候得传对象,不是传&Plus的地址
	//function包装带捕获的lambda表达式
	Plus plus;
	function<double(double, double)> func8 = [&plus](double x, double y)->double {return plus.plusd(x, y); };
	cout << func8(1.1, 2.2) << endl;
	return 0;
}

1.3包装器求解力扣150题《逆波兰表达式求值》

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        map<string,function<int(int,int)>> opFuncMap
        {
            {"+",[](int x,int y)->int{return x+y;}},
            {"-",[](int x,int y)->int{return x-y;}},
            {"*",[](int x,int y)->int{return x*y;}},
            {"/",[](int x,int y)->int{return x/y;}}
        };
        for(auto& str:tokens)
        {
            if(opFuncMap.count(str)==0)//说明是数字
            {
                st.push(stoi(str));
            }
            else//说明是运算符
            {
                int right=st.top();
                st.pop();
                int left=st.top();
                st.pop();
                //执行算术操作,算完后将数据重新push进栈
                st.push(opFuncMap[str](left,right));
            }
        }
        return st.top();
    }
};

2、bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。可以用于固定绑定参数调整参数的顺序

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
int Plus(int a, int b)
{
	return a + b;
}
int SubFunc(int a, int b)
{
	return a - b;
}
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
	//表示绑定函数Plus 参数分别由调用 func1 的第一,二个参数指定
	::function<int(int, int)> func1 = ::bind(Plus, placeholders::_1,placeholders::_2);
	cout << func1(1, 2) << endl;
	function<int(int, int)> func2 = bind(SubFunc, placeholders::_1, placeholders::_2);
	cout << func2(1, 2) << endl;
	//调整参数的顺序
	function<int(int, int)> func3 = bind(SubFunc, placeholders::_2, placeholders::_1);
	cout << func3(1, 2) << endl;
	function<bool(int, int)> gt = bind( less<int>(), placeholders::_2, placeholders::_1);//降序
	cout << gt(1, 2) << endl;
	//固定绑定参数
	function<int(Sub, int, int)> func4 = &Sub::sub;
	cout << func4(Sub(),1, 2) << endl;//这样写总是要传入一个类匿名对象进行打印
	function<int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);//固定sub()
	cout << func5(1, 2) << endl; 
	return 0;
}

猜你喜欢

转载自blog.csdn.net/gfdxx/article/details/130050532