目录
C++11简介
{} 初始化
std::initializer_list
auto
decltype
nullptr
范围for循环
C++98的循环方式:
C++11的范围遍历:
智能指针
1. 为什么需要智能指针?
2.什么是内存泄漏?
2.1内存泄漏的分类
2.2如何避免内存泄漏
3.智能指针的使用及原理
3.1RAII
3.2智能指针的原理
还有常见的三种智能指针类型:
STL中的一些变化
右值引用和移动语义
什么是左值?什么是左值引用呢?
什么是右值?什么是右值引用?
左值引用和右值引用的比较
左值引用总结:
右值引用的总结:
右值引用的场景和意义:
右值引用左值:
完美转发
新的类功能
强制生成的默认关键词default:
禁止生成默认成员函数的关键词delete:
可变参数模板
用递归的方式展开参数包:
逗号表达式展开参数包:
编辑
STL中的emplace接口:
lambda表达式
lambda表达式语法:
函数对象与lambda表达式:
总结:
C++11简介
在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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更
强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个
重点去学习。
由于C++11的新增语法特性篇幅特别多,所以文章也讲解实际中比较实用的语法
{} 初始化
在C++98中,标准允许使用{}对数组或者结构体元素进行统一的列表初始值设定,比如:
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11中扩大了{}的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表的时候 = 可加可不加
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
//标准容器
vector v{1,2,3,4,5};//代替了push_back一个一个插入提高代码效率
map m{{1,1}, {2,2,},{3,3},{4,4}};
return 0;
}
创建对象的时候也可以使用列表初始化方式调用构造函数初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
std::initializer_list
initializer 的使用:
int main()
{
vector v = { 1,2,3,4 };
list lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = {10, 20, 30};
return 0;
}
让模拟实现的vector也可以实现用{}赋值
template
class vector {
public:
typedef T* iterator;
vector(initializer_list l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename initializer_list::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
}
vector& operator=(initializer_list l)
{
vector tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将
其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map dict = { {"sort", "排序"}, {"insert", "插入"} };
auto it = dict.begin();
cout<
decltype
作用:将变量的声明类型作为表达式指定的类型
使用场景:
template
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;
}
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
范围for循环
int arr[] = { 3,4,2,1,5,6};
// 下标遍历
for(int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
std::cout << arr[i] << " ";
}
// 指针遍历
for(int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); ++p)
{
std::cout << *p << " ";
}
C++98的for循环遍历的特点就是要写明一个明确的范围,如果我们需要完整的遍历一个数组或者集合或者容器(map)的时候,可以让编译器来自动推算范围。这就引出C++11的范围for。
int arr[] = { 3,4,2,1,5,6};
// 下标遍历
for(int &e : arr)
{
cout << e <<" ";
}
//使用auto的话会更简单
for(auto &e : arr)
{
cout << e <<" ";
}
//不明确的区间不能范围for
int *p = arr;
for(auto &e : p)//范围不确定,运行报错
{
cout << e <<" ";
}
但是范围遍历并不适用于所有情况,范围遍历的前提条件就是迭代的区间范围都是确定的。比如一般的容器vector,map,set,string,list.用户自己实现的类要想实现的话需要自行提供自增运算符重载和赋值运算符重载
智能指针
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int; //当p1和p2被正常的new出来
int* p2 = new int;
cout << div() << endl;//调用div时候如果输入的b为0,那么根据异常的特性会在这里直接返回
//这样直接跳过下面的delete了,导致了内存泄漏
delete p1;
delete p2;
}
int main()
{
try
{
Func();//这里捕捉异常
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
分析上述代码。
内存泄漏指的是,人为的疏忽导致程序未能释放已经不再使用的内存的情况。内存泄漏指的并不是内存在物理上的消失,而是程序员分配某段内存后,失去了对该段内存的控制,造成内存浪费
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:
这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智
能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象
优点:1.不需要显示的释放资源 2.这种方式同时可以保证对象所需的资源在其生命周期内始终有效
// 使用RAII思想设计的SmartPtr类
template
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
上述的SmartPtr还不能称作智能指针,因为他还不具备指针的行为。指针可以解引用,可以通过->访问他的成员。因此AutoPtr模板中还需要重载 * ,->
template
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr sp1(new int);
*sp1 = 10
cout<<*sp1< sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
总结一下:智能指针的原理 1.RAII 2.将类重载*,->使其具备指针的行为
STL中的一些变化
实际中最有用的就是:unordered_map和unordered_set在上片博客讲了unordered_map和map的区别。https://blog.csdn.net/Obto_/article/details/128388643?spm=1001.2014.3001.5502
实际上更新了C++11后,容器中增加的新方法->插入接口函数的右值版本,都说这样可以增加效率,具体的看下文对右值的详细讲述。
右值引用和移动语义
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
左值是一个数据的表达式,我们可以取他的地址,可以对他赋值,左值可以出现在赋值符号的左边,而右值不能出现在赋值符号的左边。定义const的左值就不能给他赋值,但可以对他取地址。
左值引用就是给左值取别名。
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& rp = *p;
return 0;
}
右值也是一个表达数据的表达式,比如:字面常量,表达式返回值,函数返回值(这个不能是左值引用的返回值)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
需要注意的是:右值是不能取地址,但可以取别名,当给右值取别名后会导致右值被存储到特定的位置。可以对右值的别名进行左值的操作。
左值引用和右值引用的比较
1.左值引用只能引用左值,不能引用右值
2.但是const左值既可以引用左值,也可以引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
1.右值引用只能引用右值,不能引用左值
2.但是右值引用可以用move后的左值
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
#include
namespace gch
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
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)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
int main()
{
gch::string s1("ai");
return 0;
}
左值引用的使用场景:
void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
bit::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
//但左值引用能做的不够全面,比如传值返回的时候就会存在深拷贝
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
左值引用的短板: 当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用进行返回,只能传值返回。例如:gch::string to_string(int value)函数中可以看到,这里只能传值返回,传值返回会导致至少一次的深拷贝(旧的编译器如果没有优化的话可能会有两次深拷贝)
右值引用和移动语义解决上述的问题:
在gch::string中新增了移动构造,移动构造本质使用其他人的资源来构造自己,这样就不用深拷贝了。
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
int main()
{
bit::string ret2 = bit::to_string(-1234);
return 0;
}
在移动构造中没有开空间,没拷贝数据,所以效率提高了
下列是移动赋值:
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
bit::string ret1;
ret1 = bit::to_string(1234);
return 0;
}
::STL容器都增加了移动拷贝和移动赋值
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值的吗?答案显然是否定的,在很多场景下需要右值引用去引用左值实现移动语义,当需要右值引用一个左值的时候,可以使用move函数,move(左值)这样可以将左值强转成右值,就可以进行对左值的移动了
int main()
{
gch::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
gch::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
gch::string s3(std::move(s2));
return 0;
}
完美转发
模板中的&&万能引用
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
运行结果:
完美转发会在传参的过程中对象的原生类型属性
新的类功能
原来C++的类中,有6个默认成员函数。C++11后新增两个移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。
C++11为了让你更好的控制函数,假设有一个你需要用到的默认成员函数,因为某一些原因没有生成,那就可以用default来强制生成默认成员函数。比如:我们提供了拷贝构造就不会再生成移动构造了,可以加个default来强制它生成
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person(Person&& p) = default;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
可变参数模板
template
void ShowList(Args... args)
{}
Args是一个模板参数包,args是一个函数形参参数包,声明一个参数包Args...args,这个参数包可以包含0到任意个模板参数。
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
// 递归终止函数
template
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template
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;
}
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,
{(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
template
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
emplace接口支持模板的可变参数,并且万能引用。相对于insert他的优势在哪里呢?
int main()
{
std::list< std::pair > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
emplace_back支持可变参数,拿到pair对象的参数后自己去创建对象。和push_back没有太大区别
区别就是,emplace_back是直接构造对象出来在list中,push_back是先构造一个对象然后再移动构造,效率对于今天来说影响并不是很大。
lambda表达式
在C++98中想对一个数据进行排序,一般使用std::sort
int main()
{
int array[] = {3,1,2,4,6,8,5,9};
// 默认是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));默认是less()
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater());
return 0;
}
而lambda表达式的 出现给程序员提供了很大的便携;
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector 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; });
}
看的出来其实lamda表达式就是一个匿名函数。
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
[ capture_list ] : 捕捉列表,出现在lambda表达式开头,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中变量供lambda函数使用。
(parameters) : 参数列表。与普通函数的参数列表一致,如果不需要传递参数可以连()一起省了
mutable : 默认情况下,lambda函数总是一个const函数,mutable可以取消他的常性。使用该修饰符时,参数列表不能省略(即使参数为空)
return -type : 返回值类型。用追踪返回值类型形式声明函数的返回值类型,没有返回值可省略,有返回值也可以省略让编译器自动推导。
{statement} : 函数体。在该函数体内,可以使用其参数外,还可以使用所有捕捉到的变量。
注意:lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表可以为空。所以C++11中最简单的lambda表达式为 []{}
lass 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);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year;};
r2(10000, 2);
return 0;
}
从使用方式上看,函数对象与lambda表达式完全一样
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
C++11的更新的特性非常多,本篇博客讲述了C++11中使用较多的新语法。
1.{}更方便了类的初始化,使自定义类型的类更方便的初始化
2.auto废弃了原先的用法,取而代之的是自动类型推断,并要求显示的初始化,当定义类型的名字很长的时候就可以使用auto自动推断,亦或者在使用范围for的时候更为方便
3.内存泄露的问题会导致程序的崩溃,新增的智能指针就可以在进程不正常结束的时候,能够保证指针所指向的区域正常的释放(free);智能指针本质就是定义了一个类,让他具有指针的功能(因为类会自动调用析构函数)
4.在SLT中新增了移动构造和移动拷贝,这就引入了左值和右值的概念,左值可以取他的地址,可以对他赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边,比如一般的字面常量就是右值{1,'a',"str"}都算是右值。一般情况下左值的值都是有用的,而右值是可以被夺取的,在类的构造的时候会识别参数是左值还是右值,如果是左值就进行正常的构造,如果是右值就进行移动构造,移动构造就是用参数的资源来构建自己,有点成全他人牺牲自己,所以移动构造只是资源的转移而不会新增资源。
5.lambda表达式很大程度上方便了代码的控制,就像sort函数时一般情况下是默认升序,而想让sort后降序就需要传递一个仿函数过去,这样就导致我们要特地去写一个仿函数仅仅为了让sort排降序,所以lambda表达式就是一个匿名函数可以在一定程度上代替掉仿函数。
如果文章对你有帮助还请不要浪费你的点赞哦,谢谢支持