c++11语法特性

c++11

1.c++11发展简介

第一个比较正式的c++标准是1998提出的c++98标准 。之后定了5年计划,每5年来一次大更新。在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。 从C++0x到C++11 ,C++标准10年磨一剑,第二个真正意义上的标准 珊珊来迟。相比C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。

2.列表初始化

​ 总结:对于自定义类型,1.可以使用花括号传参直接调用构造;2.也可以用花括号构造initializer_list对象,然后在调用使用initializer_list对象的构造或者赋值;对于内置类型使用花括号直接传参构造;

2.1{}初始化

​ 在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

即一切皆可以使用花括号初始化,并且可以不写等号。这样就可以支持多参数的隐式类型转换,减少使用匿名对象进行构造;主要是为了支持多参数的构造。

2.2initializer_list

​ 类模板,存放的是常量区数组,不支持写。底层设计是存放了两个const T*指针start(指向开始)和finish(指向最后一个位置的下一个位置),注意不是连续的空间,所以类对象的大小是8。

c++ 复制代码
template<class T> class initializer_list;
//
const int* ptr = { 1,2,3 };//不支持,因为右边会被识别成initializer_list<T>对象,无法进行转换

​ 各种容器都支持initializer_list类对象的构造函数,进行初始化;赋值也支持了initializer_list类对象;

3.声明

3.1auto

​ c++11提供了多种简化声明的方式,尤其是在使用模板时。auto用于定义变量

​ 在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

3.2decltype

​ 用于声明变量,不需要定义,关键字decltype将变量的类型声明为表达式指定的类型;

​ 还可以声明函数指针类型变量decltype(malloc) pf,或者用来进行模板实例化B<decltype(变量名)>;

​ decltype还可以与lambda表达式配合着使用;

c++ 复制代码
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
    decltype(t1 * t2) ret;
    cout << typeid(ret).name() << endl;
}
int main()
{
    const int x = 1;
    double y = 2.2;
    decltype(x * y) ret; // ret的类型是double
    decltype(&x) p;      // p的类型是int*
    cout << typeid(ret).name() << endl;
    cout << typeid(p).name() << endl;
    F(1, 'a');
    return 0;
}

3.3nullptr

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

c++ 复制代码
#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

4.范围for

​ 除了原生数组是使用的指针,其他类型被替换成了迭代器

5.STL新变化

5.1新容器

​ c++11新增了array、forward_list、unordered_map、unordered_set这些容器;

​ array唯一的优势就是强制越界检查,较为鸡肋;

​ forward_list只支持了头插头删,不支持尾插尾删,因为尾插,尾删要找前一个位置,效率低,insert和erase也只是支持了当前位置的后一个位置插入删除;真正使用的意义可能就是哈希桶的子结构,但是实现起来并不复杂,所以这个结构也是较为冗余;

5.2新接口

​ 1.关于迭代器,const版本的迭代器都加了cbegin类似这种风格,较为鸡肋;

2 .提供了使用initializer_list对象进行初始化和赋值;

​ 3.还增加了一些个性化接口,如缩容等;

4 .所有的容器都提供了emplace系列的接口 ,涉及到了右值引用和模板的可变参数;好处就是有了较大的性能提升

5 .所有的容器都提供了移动构造和移动赋值版本;使得深拷贝的性能提高了90%;

6.右值引用和移动语义

​ const左值对象既可以引用左值属性,又可以引用右值属性,但是不可以修改,而右值引用的出现就是为了可以修改这种具有常性的临时对象,这样就可以支持移动构造,使得平时深拷贝的临时对象或者将亡对象的资源让别的变量获取;

6.1左值引用和右值引用

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

​ 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般可以对它赋值,左值可以出现赋值符号的左边或者右边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

​ 常用如:指针变量、解引用指针变量,普通变量,const变量

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

​ 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址和修改。右值引用就是对右值的引用,给右值取别名。 即都是一些不可以取地址的数据;

​ 常用如:字面常量:10、表达式返回值、函数返回值、临时对象也是右值,对于"xxx",被编译器识别成了首元素的地址,所以可以&"xxx",即常量字符串是左值;

​ 以前使用的引用都是左值引用,语法上不开空间,取别名;

​ 左值引用:就是给左值取别名,右值引用:就是给右值取别名;

6.2右值引用的使用

​ const&既可以引用左值又可以引用右值,权限的缩小或者平移,即左值引用是可以引用右值的,但是右值引用一般是不可以引用左值的,使用move之后就可以了,但是会带来一些影响;

​ 左值引用存在缺陷,局部对象不能引用返回,即必须是拷贝返回,但是要解决深拷贝的问题,使用移动拷贝(资源只产生了一份),移动拷贝对于浅拷贝没有意义;

​ 一般将内置类型的右值叫做:纯右值,自定义类型的右值叫做:将亡值;

​ 场景一:传值返回;

​ 右值引用使用移动拷贝,将右值的资源拿来,如果是移动赋值将本身的资源交给右值,让它析构,移动构造将自身置为空,即使用移动拷贝比深拷贝代价小;

​ 以前传值返回与传值接收是两次深拷贝,使用移动拷贝之后就变成了深拷贝加移动拷贝;经过编译器优化之后就变成了一次移动拷贝;编译器优化了两次,第一次优化成了一次拷贝构造,第二次优化是将局部变量识别成右值,本来是左值;

对于识别成左值就会走拷贝构造,识别成右值就会走移动构造,被move之后进行构造,就会将被move的值识别成右值

c++ 复制代码
int&&a=10;

​ 场景二:作为参数。

​ list<string>,外界push_back (string对象时)每一个节点的构造都会调用string的深拷贝,如果使用右值引用,进行移动拷贝只使用一个资源,可以很好的利用空间;

​ 移动语义就是利用右值实现移动构造和移动赋值;

7.完美转发forward<T>(t)

​ 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,即int&&a=move(b),a的属性是左值,move(b)的属性是右值,实际上a开辟了空间可以支持修改;

​ 我们希望能够在传递过程中保持它的左值或者右值的属性,使用完美转发 std::forward<T>(t)在传参的过程中保持了t的原生类型属性。

​ 实参是左值则进行引用折叠;

7.1完美转发的使用场景

​ 有的地方需要使用左值属性如:传参修改,而有的地方需要使用右值属性如:继续多层函数传参实现移动语义;

​ 右值引用右值,但是需要能够对引用的值进行修改或者给左值函数进行传参,所以默认保证了右值引用变量是左值属性;如果右值引用变量作为参数需要调用移动构造,但是本身是左值属性的,这是就需要使用完美转发,保持右值属性,才能调用到移动构造,否则调用的就是深拷贝;

​ 总结:c++11之后使用右值引用,类模板里面的函数就需要写成函数模板使用万能引用,每一个要传递的值使用forward<T>(变量),如果仅仅是当前使用则不需要加完美转发;

8.lambda表达式

​ 用来替代函数指针,甚至是某些场景下用来替代仿函数;

​ lambda是一个匿名函数的对象,用固定的语法就不需要写专门的仿函数类再实例化对象了;

​ 最好的使用场景就是和线程进行配合;

c++ 复制代码
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 
                                                       3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
    return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
    return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
    return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
    return g1._evaluate > g2._evaluate; });

8.1对比不同的函数可调用对象

1.函数指针,能不用就不用,因为使用起来较为复杂

​ 返回值类型(*函数指针变量名)(参数)

2.仿函数,可以使用,较为麻烦的点就是,每次需要时都需要写一个类,并且使用时需要实例化对象,使用时较为""重";

​ 类 重载operator(), 对象可以像函数一样使用;

3.lambda表达式,是一个匿名的函数对象

​ 直接在函数的内部使用;

8.2lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

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

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

​ mutable :默认情况下,lambda函数总是一个const函数,mutable可以取消其常量

性。使用该修饰符时,参数列表不可省略(即使参数为空)。主要的使用场景就是捕获列表的拷贝值可以修改;

​ ->:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。一般都不需要写返回值,除非进行强制类型转换;

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

​ 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

​ 对于局部域内,想要使用lambda外部的对象,但是又不通过传参的方式使用,需要将其捕获到捕获列表,而全局域的函数和对象是可以使用的;

8.2.1捕获列表的使用方式

​ [var]:表示值传递方式捕捉变量var,是一种拷贝 ;

​ [=]:表示值传递方式捕获所有父作用域中的变量(包括this) ;

​ [&var]:表示引用传递捕捉变量var ,包括const引用,注意并不是以前的取地址;

​ [&]:表示引用传递捕捉所有父作用域中的变量(包括this),包括const引用 ;

​ [this]:表示值传递方式捕捉当前的this指针 ;

可以组合使用如:[&,a],[=,a],前面是默认设置,后面是显式捕获;

注意:

a. 父作用域指包含lambda函数的语句块 ;

b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 ,[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 ;

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 ;

d. 在块作用域以外的lambda函数捕捉列表必须为空;

e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错,即在全局定义时,捕获列表必须为空;

f. lambda表达式之间不能相互赋值,即使看起来类型相同,但是生成的类是不一样的 ;

8.3底层实现

​ lambda表达式的底层就是仿函数;类型时lambda+uuid字符串,uuid时通用唯一识别码,是使用一种算法生成的一个字符串;

​ 其实捕捉列表就是调用了构造,将捕获的变量都生成了默认const成员变量,mutable之后就没有加const修饰;

9.模板可变参数

​ 总结一下:1.函数包的展开方式需要嵌套调用其他函数来完成;2.最大的价值就是作为中间函数一次性接收所有参数,一次性传递所有参数;

​ printf系列函数就是使用了可变参数;可变模板参数不是使用数组的方式实现的;

​ C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

c++ 复制代码
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
//查看参数包里面参数的个数
sizeof...(args);

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

9.1递归方式展开参数包

​ 递归编译,最后形成的结果还是按照递归编译的顺序来的;

c++ 复制代码
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
 cout << t << endl;
}
//对于结束函数可以写一个无参的非函数模板,只需要函数名相同即可
void ShowList()
{
    cout << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
 cout << value <<" ";
 ShowList(args...);//这样传递的就是所有的参数
}
int main()
{
 ShowList(1);
 ShowList(1, 'A');
 ShowList(1, 'A', std::string("sort"));
 return 0;
}

9.2逗号表达式展开参数包

​ 逗号表达式执行的结果是最后一部分,即借用数组的初始化来完成展开;

c++ 复制代码
template <class T>
void PrintArg(T t)
{
    cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
    int arr[] = { (PrintArg(args)/*此处是传递的一个值*/, 1)... };//后面的...是其他参数,本质上使用的是多参数的构造;
    cout << endl;
}
int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
}

9.3使用子函数的方式展开参数包

c++ 复制代码
template <class T>
void _ShowList(const T& t)
{
    cout << t << endl;
}
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
    cout << value <<" ";
    ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{
    _ShowList(args...);
}
int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
    return 0;
}

10.包装器

10.1fuction

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

​ 有了包装器之后就可以统一类型可以用于,将函数调用对象存放到容器中,这样使用调用对象时就可以去容器中进行查找;

​ 包装器解决了调用对象的类型问题,统一了调用对象的类型,使得容器中存放的都是类模板实例化之后的包装器类型;

c++ 复制代码
std::function在头文件<functional>
// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;//function<返回值(可变参数列表)>
模板参数说明:
Ret: 被调用函数的返回类型
Args...:被调用函数的形参

​ 存在一个指令对应一个动作的场景,即使用map<string,包装器实例化出来的对象>;

10.2bind

​ std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器)**,**接受一个可调用对象(callable object),生成一个新的可调用对象来"适应"原对象的参数列表。

​ 即bind是用来调整参数的(数量和参数方面的);

​ placeholder(占位符),placeholders命名空间内存放了_n参数,按被包装函数的参数列表顺序,分别代表着第1个参数...,调整_n顺序,则可以调整参数顺序;

c++ 复制代码
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);
double ala(int x, int y,double rate)
{
	return (x + y) / rate;
}
function<double(int,int)> f1 =  bind(ala,placeholders::_1,placeholders::_2,4.3);
//1.不写_2 _3是因为按照的是函数的实参来的,固定的数就不参与排序
//2.绑定类的静态员函数要加类名::,如果是普通成员函数需要&Sub::sub,即在类名前面加&,但是要注意其实普通成员函数还有隐含的this指针,所以要定义一个对象,然后把对象地址传入,或者直接传对象/匿名对象;
function<double(int,int)> f1 =  bind(ala,4.3,placeholders::_1,placeholders::_2);
10.2.1bind的底层实现

​ 和lambda类似,底层实现是一个仿函数,传入对象是用对象调用函数,传入对象地址是用指针进行调用;

11.emplace/emplace_back函数

​ 与push_back效率其实相差不大,真正的意义是对于多层复杂结构可以将参数包传到最底层,调用构造,而push_back则会是用对象调用拷贝;

c++ 复制代码
emplace_back(make_pair(1,2));//右值调用移动语义拷贝构造
emplace_back({1,2});//右值调用移动语义构造
emplace_back(1,2);//直接将参数包传递给构造函数构造

12.新的类功能

12.1默认成员函数

​ c++11类一共有8个默认成员函数,增加了移动构造和移动赋值;

对于这两个函数,如果是深拷贝则需要自己实现移动构造和移动赋值,如果是浅拷贝编译器生成的默认拷贝构造即可满足要求;

对于编译器自动生成的移动构造:

​ 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(其实需要自己实现析构,拷贝构造,拷贝赋值重载,那么说明这个类的对象需要在堆上开空间,是一个深拷贝的对象)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝),自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

对于编译器自动生成的移动赋值:

​ 默认移动赋值跟与默认移动构造完全类似;

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

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

c++ 复制代码
Person(Person&& p) = default;

禁止生成默认函数的关键字delete:

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

c++ 复制代码
Person(const Person&p) = delete;

12.2类成员变量初始化

​ C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化;

相关推荐
羊小猪~~32 分钟前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
脉牛杂德1 小时前
多项式加法——C语言
数据结构·c++·算法
legend_jz1 小时前
STL--哈希
c++·算法·哈希算法
CSUC1 小时前
【C++】父类参数有默认值时子类构造函数列表中可以省略该参数
c++
Vanranrr1 小时前
C++ QT
java·c++·qt
鸿儒5172 小时前
C++ lambda 匿名函数
开发语言·c++
van叶~3 小时前
算法妙妙屋-------1.递归的深邃回响:二叉树的奇妙剪枝
c++·算法
knighthood20013 小时前
解决:ros进行gazebo仿真,rviz没有显示传感器数据
c++·ubuntu·ros
半盏茶香3 小时前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
小堇不是码农3 小时前
在VScode中配置C_C++环境
c语言·c++·vscode