C++11-14新标准

😄C++11-14新标准

记录C++11&14中出现的新特性,主要分为语言和标准库两部分

语言

介绍C++11中新出现的语言特性

Variadic Templates

总结来说就是可以接收变长参数,在标准库中的万用哈希函数hash_val以及tuple等知识都用到了Variadic Templates这个新特性,接收变长参数,将其一层一层的处理

举一个简单的例子:

函数print接收的参数个数不定,假设传递n个参数,print将n个参数分成1n-1个,先输出一个

然后在函数体内调用自身,传递n-1个参数,print将n-1个参数分成1n-2个,输出一个。。。

就这样一层一层的调用自身,每次调用之前先减少一个参数

最后一层剩下一个参数,调用自身传递0个参数,到了结束标志,整个打印结束

递归调用自身,调用过程中剥离参数进行处理,使得参数变得越来越少

总结一个变长参数的函数模板:

1
2
3
4
5
6
7
8
template<typename T,typename... Types>
void func(T& firstArg,Types& otherArgs){
    // 处理firstArg
    //递归调用自身,少传递一个参数
    func(otherArgs...);
}
//处理边界条件
void func(){}

如果想要知道后面的n-1个参数的大小,可以使用sizeof...(otherArgs)

...是一个包,出现在不同参数的后面就是不同的包:

  1. typename...,模板参数包
  2. Types... otherArgs,函数参数类型包
  3. otherArgs...,函数参数包

不仅仅是函数,tuple也借用了变长参数的理论来实现自身,tuple可以存放不同的元素,在调用构造函数时,将传递进来的参数分成1和n-1,然后剩下的n-1以继承的方式交给新的tuple

最后使用一个空tuple处理边界条件

获取tuple中的元素时,使用head和tail,head返回1,tail返回n-1形成的tuple,在代码中先返回this,再对this进行转型

具体的实现细节如图所示:

image-20230604102943163

函数和类的处理思路是一样的,一个是调用自身,一个是继承自身,在这个过程中减少参数量

hash_val的处理方式也是这样,对于自定义类型的哈希,将自定义类型拆分成多个系统内置类型,之后使用hash_val将这些拆分类型传递过去进行处理

由于不同自定义类型使用的系统内置类型的个数不定,所以使用了variadic templates的思想接收变长参数,可以实现万用性

测试例子

一共有三个地方需要使用...

  1. typaname… Types
  2. const Types&… otherAgrs
  3. otherArgs…
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
using namespace std;

//定义边界条件
void printX() {}
//测试万用打印函数
template <typename T, typename... Types>//1
void printX(const T &firstArg, const Types &...otherArgs)//2
{
    //处理第一个元素
    cout << firstArg << endl;
    //递归处理剩下的元素
    printX(otherArgs...);//3
}
//定义一个人类
class Person
{
public:
    string Name;
    int Age;
    Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
    //因为使用的格式为cout<<person,所以是cout调用,不能定义成成员函数
    friend ostream &operator<<(ostream &output, const Person &p)
    {
        output << "person : " << p.Name << " " << p.Age << endl;
        return output;
    }
};
int main()
{
    //一个printX实现任意类型,任意长度的打印
    printX(1, 2.2, "hello", 'c', Person("张三", 20));
    cout << "-------" << endl;
    printX(1, 2.2, "hello", 'c', Person("张三", 20),Person("李四",30));
    system("pause");
    return 0;
}

容器嵌套容器

最开始c++容器嵌套容器时,需要使用空格区分是容器还是输出运算符,但是c++11新标准之后,编译器已经可以智能的区分了,所以不用加空格

1
2
vector<list<int> > //c++11之前需要加一个空格
vector<list<int>>  //c++11之后不需要加空格

auto

编译器可以自动推导变量的类型,和模板或者函数重载一样,可以自动推导出参数的类型

1
2
3
4
5
6
auto i=42//i是int类型
double f();
auto d=f();//d是double类型
vector<int>v1;
auto it=v1.begin()//it是vector<int>::iterator类型
auto num;//编译失败    

当参数的类型名很长,例如迭代器,或者参数的类型实在想不起来的时候,就可以使用auto,但是不推荐任何地方都是用auto,这样会降低代码的可读性

统一初始化

c++11之前对于初始化的操作有多种不统一的方式,(){}=都可以用来初始化,并且每个变量支持初始化的方式不同,这就导致了初始化时会造成一些不必要的错误

基于这个原因,c++11提出了一个统一初始化,规定任何变量都可以使用{}初始化,当然以前的方式都保留,只是给一些没有{}赋值的变量增加了这种方式

不知道怎么初始化就用{}

背后的原理就是将{}中的内容传递给一个initializer_list,之后initializer_list将元素传递给arrayarray会判断变量的构造函数能否接受initializer_list这种类型,可以的话直接初始化,不可以的话就将{}中的元素依次取出传递给要初始化的变量

能接受initializer_list就传递initializer_list,接受不了就依次取出进行初始化

Initializer Lists

使用initializer lists可以对变量进行统一初始化,底层使用array

1
2
3
4
5
6
int i;//i未定义
int j{};//j有初值0
int *p;//p未定义
double d1={2.2};//d1有初值2.2
int *q{};//q有初值nullptr
//相当于调用构造函数时生成了一个initializer_list<>

为了实现统一初始化,规定可以使用{},编译器将{}中的内容转化成一个initializer_list<>类型的容器,其中存放初始化的值

相比与variadic templates,这个容器只能存放类型一样的元素,但是元素的个数不定

使用initializer_list初始化时,如果构造函数能接受initializer_list<>就直接调用这个版本的构造函数

如果没有的话需要将initializer_list拆分,拆分出来n个元素,但是没有接收n个元素的构造函数,此时初始化就失败

底层使用一个迭代器

没有匹配的initializer_list就拆分之后有没有匹配的构造函数

容器可以接受任意数量的参数就是因为构造函数可以接受initializer_list<>,并且有的参数可以接收变长参数也是因为重载了接收initializer_list<>的版本,例如maxmin


小总结

c++11提供两种处理变长参数的方式:

  1. variadic templates:接收的参数不定,类型任意
  2. initializer_list:接受的参数不定类型一致

explicit

如果使用explicit修饰构造函数,那么编译器就不会隐式的将一些动作转化成对构造函数的调用,只有显式的声明需要调用构造函数时才会调用

c++11之前只有单个参数的非explicit构造函数有时才会提供explicit的修饰,但是c++11之后,一个以上参数的构造函数也可以使用explicit修饰

image-20230604140224719

使用explicit修饰的构造函数只能显式调用

emplace

使用emplace来将元素存入容器中时,容器直接在底层创建对象并将其存入,而不是像push_back一样,先创建一个临时对象,之后使用临时对象接收传入的参数,举例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Test
{
public:
    std::string name;
    explicit Test( const std::string &name):name(std::move(name))
    {
        std::cout << "I am being constructed.\n";
    }
    Test(const Test &other):name(std::move(other.name))
    {
         std::cout << "I am being copy constructed.\n";
    }
    //右值引用
    Test( Test &&other):name(std::move(other.name))
    {
        std::cout << "I am being moved.\n";
    }
};
int main()
{
	//1>
    std::cout<<"emplace_back:"<<std::endl;
    std::vector<Test>T1;
    T1.emplace_back("yang");
	//输出 	I am being constructed

	//2>
    std::cout<<"emplace_back2:"<<std::endl;
    std::vector<Test>T2;
    T2.emplace_back(Test("aaaa"));
    //输出 	I am being constructed   表面调用构造函数,
    //		I am being moved         底层调用构造函数

	//3>为什么和emplace一样
    std::cout<<"push_back1:"<<std::endl;
    std::vector<Test>T3;
    T3.push_back(Test("a"));
    //输出 	I am being constructed   表面调用构造函数
    //		I am being moved		  表面调用构造函数创建临时对象
}
  1. 在底层直接调用Test的构造函数创建一个Test对象,将其存入容器中,即使构造函数有explicit修饰也可以运行,因为这是在底层显式的调用构造函数,不会发生转换

  2. 先在局部创建一个临时对象,底层容器收到临时对象之后,创建一个新的对象接收临时对象,调用move函数进行初始化,之后存储这个新的对象,临时对象被销毁

    存储的对象与传入的对象之间只是值相同

  3. 先在局部创建一个局部对象,之后创建一个新的对象接收这个临时对象,二者只是值相同

range for

范围for循环,可以遍历容器,语法如下:

1
2
3
4
5
6
7
for(auto elem:coll){
    cout<<elem<<endl;
}
//或者
for(auto& elem:coll){
    elem+=3;
}

第一种拷贝方式无法修改元素,第二种引用方式读取到的elem可以修改,并且可以影响到原容器

底层编译器还是使用普通的for循环实现,将容器中的元素依次取出来交给elem

范围for循环和explicit关键字:

image-20230604141101113

原本vs中的string类型的元素可以隐式的调用C的构造函数,从而将string转化成C,但是增加了explicit关键字修饰之后,就无法转换

=default,=delete

如果想让编译器提供默认的构造函数(包括有参和无参),只需要在对应的构造函数后加上一个=default即可,但是如果有自定义的构造函数,就不能使用=default再让编译器给一个默认的,不然会出现二义性

=delete使用的较少,因为不想要可以直接不写,没有必要写出来又告诉编译器我不要

对于同一个函数,=default和=delete不能并存

image-20230604143155171

主要就是使用=default告诉编译器提供一个默认版本的函数


如果只定义了一个空类,编译器会自动地提供一些构造函数和析构函数,这些函数都是inline的,所以在调用这些函数时,会在调用处直接展开,而不是一层一层的递归调用,减少递归的开销

如果想要其他成员不能拷贝自己的内容,就需要将拷贝构造和赋值函数定义为私有,c++11中提供了一个noncopyable类,这个类中的拷贝构造和赋值函数都是私有的,继承这个类就可以不倍拷贝

哪些类需要自定义上述构造函数?

image-20230604144047168

若类中有指针成员,就需要自定义上述几个函数,若没有指针成员,基本上就不需要自定义这些函数

因为指针的拷贝设计到深拷贝和浅拷贝的问题,所以需要自定义这些函数

深拷贝和浅拷贝

简单来说浅拷贝只是对指针的拷贝,拷贝完成之后两个指针指向同一块内存,此时还不会出现问题,一旦调用析构函数,由于存在两个指针,会调用两次析构函数去释放堆区的内存,但是两个指针指向同一块内存,所以会造成同一块内存的二次释放问题

深拷贝就是新创建一个指针,将指针中的值拷贝过来放到新指针中,两个指针除了值一样,指向的内存是不一样的,所以调用析构函数时不会出现问题

Alias Template

化名模板,也就是说给模板一个别名,后期在使用时就可以使用这个别名从而间接使用这个模板,当模板的名称太长时就可以使用简短的别名

但是特化是还是需要使用最初的模板进行特化,不能使用别名

例如:

1
2
3
4
5
6
template <typename T>
using Vec = std::vector<T,MyAlloc<T>>;
//后期可以直接使用Vec作为别名
Vec<int> coll;
//等价于
std::vector<int,MyAlloc<int>> coll;

化名模板并不单单是为了使用模板时少写几个字母

当希望一个函数可以接受不同的参数具有通用性时,第一想法是使用函数模板,接受不同的参数就可以实现通用性,但是对于容器来说,传递不同的容器,想要插入时,还需要使用萃取器得到他能插入什么类型的元素,并不能直接传元素

image-20230605134936481

想要插入元素需要先使用萃取器得到能插入什么元素,才能插入,所以插入元素的步骤很繁琐,并且插入的元素已经固定,不能动态指定

容器->迭代器->迭代器类型->插入元素类型

那么有没有一种模板,接收一个参数,这个参数也是一个模板,并且能够取出这个模板的参数

例如模板接受vector\<string>,而vector\<string>本身也是一个模板,可以取出其中的string,这就是template template parameter

模板模板参数

模板模板参数template template parameter,也就是说,模板中的参数又是模板,例如:

1
2
3
4
5
template<typename T,
		//模板的参数又是模板
		template<class> //用来修饰下面的class Container
		class Container
		>

此时就可以直接传递容器名就可以进行测试,并且可以动态的指定容器插入什么元素

不用像上面一样传递一个容器对象,插入的元素已经固定,具体的使用例子如下:

image-20230605141436064

但是直接传递容器名又会出现问题,因为容器有两个参数,我们只希望传递一个模板即可,所以需要使用化名模板alias template来解决这个问题

此时就可以实现不直接传递容器对象,而是传递一个容器名,在函数内部创建容器对象,并且可以动态指定容器插入的元素类型,不用再使用萃取器一层一层获得元素的类型

小总结

  1. 化名模板Alias Template可以给模板取一个别名

  2. 模板模板参数template template parameter可以让模板中接收的参数也是一个模板

  3. 综上,模板模板参数可以接受化名模板作为参数

Type Alias

类似于 typedef,给类型取一个别名,使用using取别名

1
2
3
4
5
6
7
8
//指明func是一个接受两个int参数的函数指针
//使用typedef
typedef void (*func)(int,int)
//使用type alias
using func=void (*)(int,int)
//例如
void add(int a,int b)   {return a + b;}
func=add;//使用函数指针指向add函数

using

使用using可以定义化名模板alias template、化名类型type alias、命名空间namespace、某个类的某个函数

1
2
3
4
5
6
7
8
9
//化名模板
template <class T>
using Vec=vector<T,allocator<T>>
//化名类型
using func=void (*)(int,int)
//命名空间
using namespace std;
//某个类的某个参数
using Person::ShowPerson();

noexcept

在某一个函数后面加上noexcept关键字,这个函数就不会丢出异常

1
void foo() noexcept

由于程序的调用是一层一层调用,如果执行foo的过程中出现了异常,会在出现错误这一层尝试处理,如果这一层没有处理这个异常,那么就向上抛出异常,如果上层还没有处理,就继续向上,一直到foo这一层,由于foo增加了noexcept关键字,所以这一层也不会处理

由于foo这一层是最外层,所以再向上就是std::terminate(),会默认调用std::abort(),程序中断

如果类中有move function,就需要使用noexcept关键字修饰这两个函数

image-20230605153918892

override

子类继承父类,重写父类中的方法时,如果方法名后面加上override,编译器就会知道此时是重写父类方法,如果参数列表与父类中的参数列表不一样,那么就会出现错误

image-20230605155107443

对于1来说,本意是重写父类中的方法,但是参数列表写错了,编译器认为这是子类自定义的新方法,不会报错

对于2来说,本意是重写父类中的方法,并且指明override,所以参数列表写错编译器提示错误信息

把继承的本意告诉编译器

final

final可以作用到两个地方:

  1. 作用到类上,指明这个类是最终的类,不能再被继承了
  2. 作用到虚函数上,子类继承父类后,父类中使用final修饰的虚函数不能在子类中被重写

decltype

就像是typeof,可以得到一个表达式的类型,c++中其实也可以用typeof获得一个表达式的类型,但是由于typeof主要在c语言中使用,在c++中并不完整,实现也不完全,所以在c++11新标准中提出了一个decltype来替代typeof

1
2
3
4
5
6
map<string,float> coll;
decltype(coll)::value_type elem
//等价于
map<string,float> coll;
map<string,float>::value_type elem 
//也就是decltype将coll的类型map<string,float>给推导了出来    

假如知道coll是一个容器,但是忘了他是什么类型的容器,想要使用value_type这个属性时,就可以使用decltype将coll的类型推到出来,从而使用value_type

decltype可以应用在三个地方:

  1. 应用在返回值类型中

    image-20230605162555854

    如上图所示,函数模板中两个不同类型的参数相加,不知道相加之后的返回结果,所以返回值类型不知道怎么写,此时就可以使用decltype进行推导

    但是x,y在函数体内定义,编译器会先看到decltype(x,y),所以不认识x,y会报错,此时需要使用auto进行配合

    先指定auto,编译器知道x,y之后,在使用decltype(x,y)推导尾置返回值类型,

  2. 应用在模板中

    模板相互调用之后,对象的类型可能并不是那么明朗,此时可使用decltype推导出对象的类型

    image-20230605163446809

    decltype(obj)的结果为T,typename指明后面的部分是一个类型而不是一个函数,

    1
    2
    3
    
    typedef typename decltype(ob  j)::iterator iType
    //等价于
    typedef typename T::iterator iType
    
  3. 应用在lambda中

有时候需要使用lambda的类型时,由于lambda的类型太长,所以可以使用decltype来推导出lambda的类型

image-20230605164445951

lambdas

类似于内联函数,语法上有所不同,使用lambda可以很方便的声明一个匿名函数(与匿名对象一样),可以将其理解为一个未命名的内联函数

一个lambda表达式的基本语法为:

1
[capture list] (params list) mutable exception -> return type {function body}

其中捕获列表可以将lambda范围内的变量进行捕获并在函数体内使用,如果加上mutable就可以对其进行修改,加不加mutable的影响主要是在值捕获的变量上

函数体内想要修改值捕获的变量,就需要使用mutable,引用捕获(除了const)本身可修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int a = 10;
int b = 20;
auto f = [&a, b](int c, int d) mutable -> int
{
    cout << "c+d=" << c + d << endl;//3
    //不加mutable,引用捕获的a还是可以修改
    cout << "++a=" << ++a << endl;//++a=11
    //不加mutable,值捕获的b无法修改
    cout << "++b=" << ++b << endl;//++b=21
};
f(1, 2) ;
//引用捕获,函数体内的修改影响到外部的a
cout << "a=" << a << endl;//a=11
//值捕获,函数体内的修改影响不到外部的b
cout << "b=" << b << endl;//b=20

剩下的就是返回值类型,lambda必须使用尾置返回类型

上面的lambda更简单的形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int a = 10;
int b = 20;
[&a, b](int c, int d) mutable ->int{
    cout << "c+d=" << c + d << endl;//3
    //不加mutable,引用捕获的a还是可以修改
    cout << "++a=" << ++a << endl;//++a=11
    //不加mutable,值捕获的b无法修改
    cout << "++b=" << ++b << endl;//++b=21
}(1,2);
//引用捕获,函数体内的修改影响到外部的a
cout << "a=" << a << endl;//a=11
//值捕获,函数体内的修改影响不到外部的b
cout << "b=" << b << endl;//b=20

相当于创建了一个匿名对象,使用后即销毁

lambda最简单的形式为:

1
2
3
4
[capture list] {function body }
//例如
[]{cout<<"hello"<<endl;}();
//相当于使用[]{cout<<"hello"<<endl;}创建了一个对象,控制台输出hello

typename

一旦使用::,前面就需要加上typename

string类型转换

string可以和其他类型进行相互转换,有时我们希望字符串代表的含义是数字,就可以使用类型转换

1
2
3
4
int i=44;
string si=to_string(i);//si="44";
int newi=stoi(si);//newi=44;
//还有stod,stof等函数,将string转化为对应的类型

标准库

右值引用

基础

右值就是只能出现在=右边的值,左值就是可以出现在=号左边的值,也就是左值也可以出现在=右边

可以取地址的就是左值,无法取地址的就是右值

相对的,指向左值的引用就是左值引用,指向右值的引用就是右值引用

但是由于const无法修改指向值,所以const左值引用可以指向右值,这是一种特殊情况

左值引用,使用&修饰

1
2
3
4
int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败
const int &ref_a = 5;  // 编译通过

右值引用,使用&&修饰

1
2
3
4
5
int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
// 右值引用的用途:可以修改右值,相当于原来存放5的地址中现在存放6
ref_a_right = 6; 

个人理解右值引用的目的就是为了重新利用地址空间

move

左值引用可以通过加上const修饰指向右值,那么右值有什么方式可以指向左值呢——–>move函数

1
2
3
4
5
6
7
8
int &&ref_a = 5;
ref_a = 6; 
 
//等同于以下代码:
int temp = 5;
//move的目的就是将一个左值转化为右值,这样右值引用就可以指向它
int &&ref_a = std::move(temp);
ref_a = 6;

左值引用一直是一个左值,右值引用作为名称就是左值,作为返回值就是右值

例如std::move(temp)返回值肯定是一个右值,所以std::move(temp)是一个右值,但是ref_a是一个左值

右值引用的应用场景就是避免深拷贝,而是直接移动,将地址过来并且避免深浅拷贝的问题

浅拷贝就是防止两个指针指向同一块内存,深拷贝就是新建一块地址将值全部拿过来

上述说的偷在图中表现就是原指针断掉,新指针指向原内存:

image-20230607163936864

1
2
3
4
5
6
 Array(Array&& temp_array) {
     data_ = temp_array.data_;
     size_ = temp_array.size_;
     // 为防止temp_array析构时delete data,提前置空其data_      
     temp_array.data_ = nullptr;//很重要!!!!  
 }

forward

与move类似,forward也可以做类型转换,但是比move的功能更加强大,move只能将左值转化为右值,forward都可以转换,STL标准库中实现了完美的forward转发,那么什么是不完美转发呢?

不完美转发

image-20230607170017389

如上图所示,1调用insert函数传递一个右值,会调用2处的insert函数,因为他接受一个右值,我们最终的想法是可以调用3处的构造函数,因为他也接受一个右值

但是2处将右值接收过来之后变成了左值,因为有一个参数x接收右值,右值有了名字就变成了左值,转发时会转发左值,变成了不完美转发

STL中的forward可以实现完美转发,也就是右值传递的过程中一直都是右值,不会改变类型

对容器的影响

右值引用和普通深拷贝的构造函数,对于不同容器的影响是不同的

  1. 对vector:右值引用的的速度相比深拷贝更快
  2. 其他容器影响不大

只要容器中的元素以节点和指针的形式连接起来,那么有没有右值引用版本的构造函数影响不大,如果容器中的元素是连续的地址形式存放,就会有影响

因为vector有右值引用的构造函数时,可以将地址的映射直接拿过来,而不是一个一个的拷贝

总结

上述代码可以实现移动构造函数,不用深拷贝,直接传递右值引用实现深拷贝的效果,并且可以通过move函数的处理接收左值,功能强大

但是转发的过程中由于右值有形参数接收,也就相当于有了名字,右值变成了左值,再转发就变成了左值,造成了不完美转发,解决的办法就是forward完美转发

但是涉及到右值引用的函数体内部一定要有原指针置空的行为,因为他是右值引用,可以改变右值,并且有移动语义,所以可以将原指针置空,相当于浅拷贝+原指针置空 ,之后原指针不能再用

所以在自己定义类并实现移动构造函数时,需要注意将原指针置空,否则会出现浅拷贝的问题,总之要么右值引用要么深拷贝,不能浅拷贝

新增容器

array

是一个不支持动态扩容的数组,定义时指定容量,使用时就和数组一样使用

在普通数组的基础上,增加了一些成员函数和成员变量

array的初始化

1
2
3
4
array<int> a1(10,2);//错误,目的是定义一个array,初始时存放10个2,但是报错
array<int,10> a2(2);//错误,不能使用小括号初始化
array<int,10> a3{};//正确,可以使用花括号,初始化为10个0
array<int,10> a4{1,2,3,4};//正确,前四个初始化为1,2,3,4,后面的初始化为0

HashTable

哈希表,给unordered容器做底层支撑,使用链地址法解决冲突 ,当元素的个数大于数组的个数时就需要扩容,此时 的元素需要再哈希

对于系统内置的类型,可以直接调用哈希函数得到哈希值,但是对于自定义数据类型,系统不知道如何哈希,所以需要自定义哈希函数

自定义类型有了哈希函数之后( 四种方式 ),对应的容器就可以使用这个哈希函数存放自定义类型

只需要将哈希函数作为参数传递过去即可

总结

将c++11/14中的新特性列举了出来,并不全面,但是可以供后期复习使用