类的新功能

默认成员函数

在以前的C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

比较重要的是前4个,后两个的用处并不大,默认的成员函数就是我们不写编译器会生成一个默认的。

在C++11中,新增了两个默认成员函数

  1. 移动构造函数
  2. 移动赋值运算符重载

针对这两个新的默认成员函数,有了新的注意事项:

1、如果没有自己实现移动构造且没有实现析构函数、拷贝构造、拷贝赋值重载中的任何一个,那么编译器会自动生成一个默认移动构造

让我们来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
st::string _name;
int _age;
};

int main()
{
Person s1("张三", 7);
Person s2 = std::move(s1); // 移动构造 (没有移动构造,再调用拷贝构造)
return 0;
}

这里的这个Person类,只自己实现了一个构造函数,析构、拷贝构造、拷贝赋值重载都未手动实现,Person的_name成员用的自己模拟实现的string类。

可以看到,调用了string的移动构造,那么如果我们在Person类中加个析构结果是什么样呢?

这里就调用了拷贝构造,原因是有了析构,就不再生成默认的移动构造函数。

2、默认生成的移动构造

  • 对于内置类型会逐字节拷贝
  • 对于自定义类型,则需要看这个成员是否实现了移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。(如下图所示)

当删除了string类的移动构造后

3、默认生成的移动赋值和默认生成的移动构造完全类似。

4、如果提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

default和delete

default

强制生成默认函数

我们知道对于类来说有时候会需要生成默认成员函数,比如我们实现了有参的构造,无参构造就不会默认生成,或者实现了拷贝构造,默认生成的移动构造就不会生成。

上图场景是我们实现了拷贝构造,那么默认的移动构造就不会生成,我们强制生成之后实现了我们想要的效果。

delete

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

比如要求delete关键字实现,一个类,只能在堆上创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HeapOnly
{
public:
HeapOnly()
{
_str = new char[10];
}

~HeapOnly() = delete;

void Destroy()
{
delete[] _str;

operator delete(this);
}

private:
char* _str;
//...
};

只需要:~HeapOnly() = delete;

这样在栈和静态区都不能创建对象。如果要创建对象只能在堆上创建。

注意,destroy() 函数,第一是释放 char* 的那个空间,第二个是要释放ptr指向的那段空间。释放ptr指向的空间要用 operator delete(),因为 delete会调用析构

可变参数模板

C++11的新特性可变参数模板能够创建可以接受可变参数的函数模板和类模板

我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数,比如args里面有三个参数包,这里需要注意,参数包里面的内容不能直接取出来。也不能用方括号直接访问

可以用sizeof…(args) 计算参数包有几个参数。

递归函数方式展开参数包

逗号表达式展开参数包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class T>

void PrintArg(const T& x)
{
cout << x << " ";
}

template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1, 'A', 3.14);
return 0;
}
C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),  (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(args)],也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

也可以这样:

STL容器中的empalce相关接口函数

push_back

下边这种场景,是一个构造加拷贝构造,或者构造加移动构造

1
2
vector<pair<bit::string, int>> v;
v.push_back(make_pair("sort", 1));

emplace_back

1
2
v.emplace_back(make_pair("sort", 1));
v.emplace_back("sort", 1);

对比

可以看到vector,测试两种方式没有区别,但是list测试,emplace_back更高效,因为只有一次构造,参数传递的参数包直接构造到List的节点上。

总结

对于内置类型来说,两种并无区别,对自定义类型来说,一些容器,比如list可以直接将参数包构造到容器的对象上面。更高效一些。

lambda表达式

像函数使用的对象/类型

  1. 函数指针
  2. 仿函数/函数对象
  3. lambda

lambda语法

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

来个例子:

看一下f的类型:

具体说明:

  • 捕捉列表:译器根据 [] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • 参数列表:和普通函数列表一致,如果不需要参数传递可以省略()。
  • mutable :默认情况下,lambda函数总是一个const函数,mutable可以取消常量性,使用该修饰符时,不能省略()
  • 返回值类型:与常规函数一样,可以省略
  • 函数体:除了使用参数外,还可以使用捕捉的变量。

注意:

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

看一个mutable的使用

这里值捕获x和y,但是这个lambda默认是const的函数。修改了x和y所以会报错。

1
2
3
4
5
6
auto swap3 = [x, y]()mutable
{
int tmp = x;
x = y;
y = tmp;
};

加上mutable就可以,不过也没有起到交换的作用,因为是值捕捉。

捕捉列表

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针
1
2
3
4
5
6
auto swap3 = [&x, &y]
{
int tmp = x;
x = y;
y = tmp;
};

一些常见用法看下边例子:

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
42
//1、生命周期(存储区域)
//2、作用域(编译器编译,用的地方能否找到)
int f = 1;

int func()
{
static int x = 0;
cout << x << endl;
return 0;
}


int main()
{
int a, b, c;
a = b = c = 1;

// 全部传值捕捉
auto f1 = [=]() {
cout << a << b << c << endl;
};

f1();

// 混合捕捉
auto f2 = [=, &a]() {
a++;
cout << a << b << c << endl;
};

f2();

auto f4 = [&]()
{
f++; //没问题,可以捕捉全局变量
x++; //错误,x是静态变量,但是它的作用域是fun
cout << f << " " << x << endl;
};

f4();
return 0;
}

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
25
26
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}

double operator()(double money, int year)
{
return money * _rate * year;
}

private:
double _rate;
};

int main()
{
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);

auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);

return 0;
}

这段代码r1是个函数对象,r2是lambda,转到反汇编看看,这两个的区别。

由上图可以看出,r1和r2都是调用了重载的方括号,实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()

编译器生成了个类似下边的东西。

1
2
3
4
5
// lambda_uuid
class lambda_xxxx
{

};

包装器

function包装器引入

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。为什么需要function呢?可以看一个场景

1
ret = func(x);

func可能是函数名、函数指针、函数对象(仿函数对象)、也有可能
是lamber表达式对象,这些都有可能。

看个demo

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

template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;

return f(x);
}

double f(double i)
{
return i / 2;
}

struct Functor
{
double operator()(double d)
{
return d / 3;
}
};

int main()
{
// 函数指针
cout << useF(f, 11.11) << endl;

// 函数对象
cout << useF(Functor(), 11.11) << endl;

// lamber表达式对象
cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl;

return 0;
}
useF是个函数模板,依次将函数指针,函数对象,lambda传入,运行看一下

可以看到静态变量count三次的地址都不一样,说明这个函数模板实例化出来三份代码。

我们上包装器:

function包装器用法

function在在头文件 < functional >

模板参数说明:

  • Ret: 被调用函数的返回类型
  • Args…:被调用函数的形参

我们改造上面的程序,用包装器包装起来。注意模板参数那里的写法,先是返回值类型然后小括号里面的参数列表的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
// 函数指针
function<double(double)> f1 = f;
cout << useF(f1, 11.11) << endl;

// 函数对象
function<double(double)> f2 = Functor();
cout << useF(f2, 11.11) << endl;

// lamber表达式对象
function<double(double)> f3 = [](double d)->double { return d / 4; };
cout << useF(f3, 11.11) << endl;

return 0;
}

看下运行结果

可以看到count的三个地址相同,说明只实例化出了一份代码。一定程度的解决模板效率低下,实例化多份的问题。

用法注意:对于类的静态成员函数和普通成员函数用法的区别。

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
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)> f1 = Plus::plusi;
f1(1, 2);

function<double(Plus, double, double)> f2 = &Plus::plusd;
f2(Plus(), 1.1, 2.2);

return 0;
}
静态成员函数只需要加域限定符即可,但是类的成员函数,不仅仅需要域限定符,调用成员函数,前面还需要加取地址的符号,参数列表要增加类名。

普通的类成员函数包装又有了新的问题,同样以上方代码为例,两个功能类似的函数,包装器确不一样,这个可以解决吗?这时候就引入了我们的bind

bind

std::bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作

 _1 _2.... 定义在placeholders命名空间中,代表绑定函数对象的形参,
 _1,_2...分别代表第一个形参、第二个形参...

看个具体用法:

绑定后,形参的顺序得以改变(用处不是很大)

更实用的用法:调整个数,绑定死固定参数

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
42
43
44
45
46
47
48
49
50
51
52
53
54
int Div(int a, int b)
{
return a / b;
}

int Plus(int a, int b)
{
return a + b;
}

int Mul(int a, int b, double rate)
{
return a * b * rate;
}

class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};

using namespace placeholders;

int main()
{
function<int(int, int)> funcPlus = Plus;
//function<int(Sub, int, int)> funcSub = &Sub::sub;
// 注意
function<int(int, int)> funcSub = bind(&Sub::sub, Sub(), _1, _2);
// 注意
function<int(int, int)> funcMul = bind(Mul, _1, _2, 1.5);
map<string, function<int(int, int)>> opFuncMap =
{
{ "+", Plus},
{ "-", bind(&Sub::sub, Sub(), _1, _2)}
};

cout <<"funcPlus(1, 2)=" << funcPlus(1, 2) << endl;
cout << "funcSub(1, 2)=" << funcSub(1, 2) << endl;
cout <<"funcMul(2, 2)="<< funcMul(2, 2) << endl;

cout << endl;

cout <<"opFuncMap[\" + \"](1, 2)=" << opFuncMap["+"](1, 2) << endl;
cout << "opFuncMap[\" - \"](1, 2)=" << opFuncMap["-"](1, 2) << endl;
cout << endl;

int x = 2, y = 10;
cout << "Div(2,10)=" << Div(x, y) << endl;
return 0;
}