C++11特性(三)

C++
2.5k words

类的新增功能

默认构造

在原有的C++类中,会有6个默认的成员函数,分别是构造函数、析构函数、拷贝构造、赋值重载、取地址重载、const取地址重载

最重要并且经常用的是前四个,后两个用处不大

所谓默认成员函数是当构建一个类的时候,我们不写这些函数,编译器会帮我们生成一个默认的

在C++11中,又新增了移动构造函数和移动赋值重载,这两个我们在上一篇中有详细解释

但是生成的规则会有不同

当你没有显式的写析构、拷贝构造和赋值重载时,编译器会自动生成默认的移动构造和移动赋值重载

默认生成的移动构造函数,对于内置类型会执行按字节拷贝,对于自定义类型,就需要看这个类型是否定义移动构造,有的话就直接调用,没有的话就调用拷贝构造

移动赋值重载和上面的移动构造完全类似

但是当我们自己写了移动构造或者移动赋值,编译器就不会生成拷贝构造和赋值重载了

类成员变量初始化

C++11允许在类定义的时候给成员变量缺省值,默认生成的构造函数会使用这些缺省值进行构造,例如

1
2
3
4
5
6
7
class Date
{
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};

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

这里就是强制生成我们想要的默认函数,我认为这个关键字一是为了方便我们懒得写的时候,并且默认构造函数已经足够使用了,另一方面可以当作一个占位的作用,具体使用形式如下

1
2
3
4
5
6
7
8
9
class Date
{
public:
Date(int year, int month, int day) = default;
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};

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

在C++98中,我们可以将默认函数放在private中就可以实现限制这个函数的生成和使用,在C++11中和default的使用类似

1
2
3
4
5
6
7
8
9
10
class Date
{
public:
Date(int year, int month, int day) = default;
Date(Date& _d) = delete;
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};

继承和多态中的final和override关键字

这里我们在继承和多态中已经讲的非常详细,final可以加在类后面表示该类不能被继承,final和override也可以加在成员函数中,前者表示该函数不能被重写,后者表示必须重写该函数,否则报错

可变参数模板

C++11中的可变参数模板是可以接收可变参数的函数模板和类模板

在原先的模板中,我们只能接受一定数量的模板参数,在可变参数模板这里比较抽象,使用也有一定技巧,需要在实践中才能真正理解

这里我们主要简单介绍

一个简单的可变参数模板如下

1
2
3
template<class ...Args>
void Func(Args... args)
{}

上面的参数args前面有省略号,表示一个可变参数模板,带省略号的参数我们称之为参数包,里面包含了有限多个参数,我们无法直接从参数包args中获取每个参数,只能通过递归的方式来打开这个包,一步步获取

递归打开参数包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 递归终止函数
template <class T>
void Func(const T& t)
{
cout<< t << endl;
}
// 展开函数
template<class T, class ...Args>
void Func(T value, Args... args)
{
cout << value << endl;
Func(args...);
}

int main()
{
Func(1);
Func(1, "Morty");
Func(1, "Morty", 2);
return 0;
}

逗号表达式打开参数包

这种方式其实并不复杂,主要是利用了初始化列表和逗号表达式来进行展开的过程,其实原理非常简单,看代码就能看懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<class T>
void Func(T t)
{
cout<<t<<endl;
}

template<class ...Args>
void func(Args... args)
{
int arr[] = {(Func(args),0)...}
}

int main()
{
Func(1);
Func(1, "Morty");
Func(1, "Morty", 2);
return 0;
}

用途

我们在各个容器的C++11版本接口里面可以看到emplace,支持模板的可变参数,万能引用,那么inset和emplace有什么用呢

emplace支持可变参数,那么在我们需要一次性传入pair变量的时候,就不需要make_pair函数直接构造了,他会自动拿收到的两个参数进行构造

lambda表达式

当我们在使用仿函数的时候,为了一个简单的功能就要写一个类,这么做还是觉得稍微有点子麻烦

但是诶,他们发现这个lambda还挺好用的,用一句话就能表示出一个简单的函数

例如我们想要表示一个比较大小的函数,或者说是表示升序一个排序仿函数

1
[] (int x, int y) -> bool {return x<y;};

诶这么一写,就是一个函数

语法

这里的方括号是lambda表达式的标志,用于告诉编译器,接下来这是一个lambda表达式,你要准备好接收了

接下来圆括号里面的是参数列表,箭头指向的就是返回值和函数主体了

一般lambda表达式不要写的太长了,否则可维护性就会变道很差

但是有人要说了,那万一我要用全局的变量呢,之前我们讲过,一对大括号其实就是一个小环境,那么这个小环境里面是不能随便用外面的变量的,因为函数和类本身的设计还是需要高内聚低耦合的

那其实这个方括号里面就可以用来写“全局的变量”,表示,这些变量我要用,别给我禁了

这个方括号里面的内容我们称之为捕捉列表,就好像是从全局中捕获来的变量一样

参数列表和普通的函数列表一样,如果这个函数不需要参数列表,就可以把小括号一起省略

返回值类型在没有返回值的时候可以省略,但其实有返回值的时候也可以省略,编译器会自动进行推导

实际上lambda的本质还是一个类,也就是类似于范围for,写出来是一个样,最后还是还原成他最本来的样子,那么lambda最后还原回去其实就是一个仿函数

那么被捕捉或者定义的变量就是类中的成员变量,但是lambda的一个特性就是,他默认所有的成员变量都是const的,如果想要修改就需要在参数列表之后加上一个mutable

我们甚至可以定义一个变量接收lambda表达式对其进行复用

例如

1
2
3
4
5
6
7
8
9
int x = 3;
int y = 5;
auto myswap = [](int& x, int& y)mutable->void
{
int t = x;
x = y;
y = t;
};
myswap(x,y);

因为我们其实说了,lambda表达式的本质其实就是一个仿函数,一个类,拿我们也就可以使用typeid.name来读取这个类的名称

实际上他的名称几乎是完全随机不重复的,根据时间,mac地址等内容随机生成

lambda的捕捉列表

但是有的人又要说了,诶我不一定希望所有的成员变量都可以被改呀,我要是想要使用全部的参数怎么办

基本规则如下

语法 说明
[变量名称] 传值某一个变量
[=] 传值捕捉父作用域的所有变量,包括类中的this
[&变量名称] 传引用某一个变量
[&] 传引用捕捉父作用域的所有变量,包括this
[this] 捕捉当前的this指针

这些语法可以组合使用,用逗号分割即可,但是不允许重复传递,并且lambda表达式之间不能互相赋值,因为我们讲过他们的类名称实际上是不同的,也就是不同的类

function包装器

出现这个东西的原因主要是因为我们可以调用的东西太多了,而且形式非常相似,函数调用,函数指针调用,仿函数对象,lambda对象都是一模一样的调用方式

那我们可不可以把这些东西全部都放到一起去,包装起来,这样就能使用统一的调用方式来使用上述提到的内容,甚至达到意想不到的效果

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<class T1, class T2>
void func1(T1 t1, T2 t2)
{
cout<<"func1"<<endl;
}

void func2(int x, int y)
{
cout<<"func2"<<endl;
}

struct func3
{
void operator()(int x, int y)
{
cout<<"func3"<<endl;
}
};

int main()
{
auto func4 = [](int x, int y){cout<<"func4"<<endl;};
return 0;
}

这里我们发现其实类型都是没有问题的,但是一旦我们想要使用这些内容,将其作为类似于回调函数的部分来使用,就会出现一些问题

1
2
3
4
5
template<class Func, class T1, class T2>
void useFunc
{
Func(T1, T2);
};

这里是我们设置的函数,调用如下

1
2
3
4
5
6
7
8
int main()
{
useFunc(func1, 1, 2);
useFunc(func2, 1, 2);
useFunc(func3, 1, 2);
useFunc(func4, 1, 2);
return 0;
}

这里其实就会出现一些性能损失,因为在实例化模板的时候就会重复实例化

但是使用包装器就能解决这个问题,例如

1
2
3
4
5
6
7
8
9
10
11
#include<functional>

int main()
{
function<void(int,int)> Func1 = func1();
function<void(int,int)> Func2 = func2;
function<void(int,int)> Func3 = func3;
function<void(int,int)> Func4 = [](int x, int y){cout<<"func4"<<endl;};
useFunc(Func1, 1, 2);
return 0;
}

这样子我们在传入回调时,其实就只会实例化一份function对象的类型了

回调函数使用比较多的场景主要还是命令行之类的解释器,还有之后我们实现的线程池,创建线程的时候会用到

目前只需要了解大致的原理即可,function的本质还是一个类

Comments