深圳幻海软件技术有限公司 欢迎您!

【C++】C++11新特性——类的改进|lambda表达式

2023-04-15

文章目录一、类的改进1.1默认生成1.2移动构造函数1.3移动赋值重载函数1.4成员变量缺省值1.5强制生成默认函数的关键字default1.6禁止生成默认函数的关键字delete1.6.1C++98防拷贝1.6.1C++11防拷贝二、lambda表达式2.1对比2.2lambda表达式语法2.3捕

文章目录

  • 一、类的改进
    • 1.1 默认生成
    • 1.2 移动构造函数
    • 1.3 移动赋值重载函数
    • 1.4 成员变量缺省值
    • 1.5 强制生成默认函数的关键字default
    • 1.6 禁止生成默认函数的关键字delete
      • 1.6.1 C++98防拷贝
      • 1.6.1 C++11防拷贝
  • 二、lambda表达式
    • 2.1 对比
    • 2.2 lambda表达式语法
    • 2.3 捕捉列表
    • 2.4 函数对象与lambda表达式

一、类的改进

1.1 默认生成

C++ 的类有四类特殊成员函数, 它们分别是: 默认构造函数、 析构函数、 拷贝构造函数以及拷贝赋值运算符。
C++11 新增了两个:移动构造函数和移动赋值运算符重载
这两个成员函数在上一面介绍过:【C++】C++11新特性——右值引用

这些类的特殊成员函数负责创建、 初始化、 销毁, 或者拷贝类的对象。如果没有显式地为一个类定义某个特殊成员函数, 而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。

但是, 如果程序员为类显式的自定义了非默认构造函数, 编译器将不再会为它隐式地生成默认无参构造函数。

class A
{
public:
A(const A& aa)
: _a(aa._a)
{}
private:
int _a;
};

int main()
{
A a;// 不存在默认的构造函数
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

而对于移动构造函数和移动赋值运算符重载函数:

1.2 移动构造函数

如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器就会自动生成一个默认的移动构造函数
这个默认生成的移动构造函数:
对内置类型按照内置类型的字节序拷贝
对自定义类型则先看这个类型是否实现了移动构造,实现了就用移动构造,否则用拷贝构造

namespace yyh
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}

iterator end()
{
return _str + _size;
}

string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}

void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}

// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}

// 移动构造
string(string&& s)
{
swap(s);
cout << "string(string&& s) -- 移动构造" << endl;
}

// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}

// 移动赋值
string& operator=(string&& s)
{
swap(s);
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
return *this;
}

~string()
{
delete[] _str;
_str = nullptr;
}

char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}

void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;

_capacity = n;
}
}

void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}

_str[_size] = ch;
++_size;
_str[_size] = '\0';
}

string& operator+=(char ch)
{
push_back(ch);
return *this;
}

const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}

class A
{
public:
private:
int _a = 1;
yyh::string _s;
};
  • 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
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125


1.3 移动赋值重载函数

如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器就会自动生成一个默认的移动赋值重载函数。
而编译器生成的移动赋值对内置类型和自定义类型的处理方法跟上面一样。



但是只要我们加上了析构函数 、拷贝构造、拷贝赋值重载中的任意一个。编译器就不会默认生成。

class A
{
public:
~A() {}
private:
int _a = 1;
yyh::string _s;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

1.4 成员变量缺省值

class A
{
public:
~A() {}
private:
int _a = 1;// 缺省值
yyh::string _s = "aaa";// 缺省值
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里的缺省值会在构造/拷贝构造的初始化列表使用:
如果在构造/拷贝构造有这两个的初始化就不会走缺省值,谁不在初始化列表初始化就走谁的缺省值。

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

上面我们举例子写了析构函数后编译器就不会默认生成移动构造和移动赋值。
但是如果我们必须要写析构,有没有什么办法让编译器默认生成呢?
答案是可以使用default关键字。

class A
{
public:
A(int a = 1, const char* s = "")
: _a(a)
, _s(s)
{}
// 只有移动构造没有拷贝构造会匹配歧义
A& operator=(const A& aa) = default;
A& operator=(A&& aa) = default;

A(A&& a) = default;

~A() {}
private:
int _a = 1;
yyh::string _s;
};

int main()
{
A a;
A b;
A c;
// 无default
A d(move(b));
c = move(a);
return 0;
}
  • 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

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

1.6.1 C++98防拷贝

我们怎么让一个类对象不能被其他类拷贝呢?
首先我们能想到把拷贝构造写成私有,这样在外部就无法被拷贝。
但是如果有一个内部成员函数会调用拷贝函数怎么办?
在C++98中使用的方法是只声明不定义:

class A
{
public:
A(){}
// 只声明不定义
A(const A& aa);

~A()
{
delete[]_p;
}

private:
int* _p = new int[10];
};

int main()
{
A a;
A b(a);
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

1.6.1 C++11防拷贝

C++11是使用delete关键字来防止拷贝

class A
{
public:
A(){}

A(const A& aa) = delete;

~A()
{
delete[]_p;
}

private:
int* _p = new int[10];
};

int main()
{
A a;
A b(a);
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

二、lambda表达式

2.1 对比

struct gift
{
std::string _name;// 名称
int _vol;// 体积
double _val;// 价值

gift(const char* name, int vol, double val)
: _name(name)
, _vol(vol)
, _val(val)
{}
};

int main()
{
std::vector<gift> v = { {"苹果", 10, 20.0}, {"梨子", 15, 12.8}, {"香蕉", 11, 10.2} };
// 比较
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

当我们分别想按照名称、体积、价格来进行排序的时候,我们一般会写三个仿函数。

struct CmpNameLess
{
bool operator()(const gift& g1, const gift& g2)
{
return g1._name < g2._name;
}
};

struct CmpVolLess
{
bool operator()(const gift& g1, const gift& g2)
{
return g1._vol < g2._vol;
}
};

struct CmpValLess
{
bool operator()(const gift& g1, const gift& g2)
{
return g1._val < g2._val;
}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

我们可以看到这样会写很多的仿函数。我们可能遇到过这种代码:

int main()
{
std::vector<gift> v = { {"苹果", 10, 20.0}, {"梨子", 15, 12.8}, {"香蕉", 11, 10.2} };
//sort(v.begin(), v.end(), CmpNameLess());
sort(v.begin(), v.end(), [](const gift& g1, const gift& g2){
return g1._name < g2._name; });
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里就叫lambda表达式。

2.2 lambda表达式语法

lambda表达式书写格式:

[capture-list](parameters)mutable->return-type{statement}

说明:

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters): 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable: 默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype: 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}: 函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

明白了这些语法我们就可以写一个比较大小的lambda表达式:

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

而lambda的本质是一个可调用的对象,我们只能用auto接收。可以像仿函数一样调用。

int main()
{
// [](int x, int y)->bool {return x < y; };
auto cmp = [](int x, int y){return x < y; };
cout << cmp(1, 2) << endl;
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.3 捕捉列表

[]捕捉列表:

[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

这几个都可以在捕捉列表里混合使用,例如:
auto fun = [=, &b]() {};// 所有对象都用传值捕捉,只有b用引用捕捉

我们可以使用lambda来捕获上面定义过的变量。

int main()
{
int a = 1, b = 2;
auto add = [a](int x) {return x + a; };
cout << add(b);
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

mutable:

当我们想要交换两个变量时:

int main()
{
int a = 1, b = 2;
auto Swap = [a, b]()
{
int tmp = a;
a = b;
b = tmp;
};
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11


所以这里我们要加上mutable,让传进来的参数可以被修改。

int a = 1, b = 2;
auto Swap = [a, b]()mutable
{
int tmp = a;
a = b;
b = tmp;
};
Swap();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

但是我们调试发现a和b的值都没有被改变,原因是捕获列表默认是传值,并不会改变原来的值。

int main()
{
int a = 1, b = 2;
auto Swap = [&a, &b]()
{
int tmp = a;
a = b;
b = tmp;
};
Swap();
cout << a << " " << b << endl;
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

当然也可以使用[&]引用传递所有内容

auto Swap = [&]()
{
int tmp = a;
a = b;
b = tmp;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里就可以看出来mutable没有什么用处。

2.4 函数对象与lambda表达式

struct Add
{
int operator()(int x, int y)
{
return x + y;
}
};

int main()
{
// 函数对象
Add sum1;
sum1(1, 2);
// lambda表达式
auto sum2 = [](int a, int b) {return a + b; };
sum2(1, 2);
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18


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



文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览44139 人正在系统学习中