lamda表达式
Lambda
表达式来源于函数式编程,说白就了就是在使用的地方定义函数,有的语言叫“闭包。如果lambda
函数没有传回值(例如void
),其回返类型可被完全忽略。 定义在与 lambda 函数相同作用域的变量参考也可以被使用。这种的变量集合一般被称作closure
(闭包)。C++引入Lambda的最主要原因就是:
- 可以定义匿名函数
- 编译器会把其转成函数对象
Lambda表达式完整的声明格式如下:
[capture list] (params list) mutable exception-> return type { function body }
各项具体含义如下
- capture list:捕获外部变量列表
- params list:形参列表
- mutable指示符:用来说用是否可以修改捕获的变量
- exception:异常设定
- return type:返回类型
- function body:函数体
此外,我们还可以省略其中的某些成分来声明“不完整”的Lambda表达式,常见的有以下几种:
[capture list] (params list) -> return type {function body}
[capture list] (params list) {function body}
[capture list] {function body}
- 格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值。
- 格式2省略了返回值类型,但编译器可以根据以下规则推断出Lambda表达式的返回类型:
(1):如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类型确定;
(2):如果function body中没有return语句,则返回值为void类型。 - 格式3中省略了参数列表,类似普通函数中的无参函数。
简单的例子:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
bool cmp(int a, int b) {
return a < b;
}
int main(int argc, char** argv) {
vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
vector<int> lbvec(myvec);
sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
cout << "predicate function:" << endl;
for (int it : myvec)
cout << it << ' ';
cout << endl;
sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; }); //Lambda表达式
cout << "lambda expression:" << endl;
for (auto it : lbvec)
cout << it << ' ';
}
自动类型推导auto
auto并没有让C++成为弱类型语言,也没有弱化变量,只是使用auto的时候,编译器根据上下文情况,确定auto变量的真正类型。auto可以用来定义变量,也可以作为函数的返回值。
auto addTest(int a, int b) {
return a + b;
}
int main(int argc, char** argv) {
auto index = 10;
auto ret = addTest(1,2);
std::cout << "index: " << index << std::endl;
std::cout << "res: " << ret << std::endl;
}
如上定义函数时可以使用auto作为返回值。但是auto只能用于定义函数,不能用于声明函数。如果声明在头文件中,定义在代码文件中,那么编译将无法通过,但是可以把定义和声明都在头文件中,那么就可以了。所以一般不要这么使用auto,在定义一些变量的时候使用就可。
而我一般不使用auto,因为代码习惯我想知道每个变量是什么类型,特别是一些涉及到隐式转换时,会相当的不妙。
另外auto与for循环也是相当的简洁。
int main(int argc, char** argv) {
int numbers[] = { 1,2,3,4,5 };
for (auto n : numbers) {
std::cout << n << std::endl;
}
}
以上用法不仅仅局限于数据,STL容器都同样适用。
自动化推导decltype
decltype是类型的推导,颇有一点跟auto是相反的意思。decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,而是总是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值。
如下:
int i = 4;
decltype(i) a; //推导结果为int。a的类型为int。
与using/typedef
合用,用于定义类型:
using size_t = decltype(sizeof(0)); //sizeof(a)的返回值为size_t类型
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
vector<int >vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin(); i != vec.end(); i++) {
//...
}
重用匿名类型,在C++中,我们有时候会遇上一些匿名类型,而借助decltype,我们可以重新使用这个匿名的结构体:
struct {
int d ;
doubel b;
} anon_s;
decltype(anon_s) as; //定义了一个上面匿名的结构体
最好的用处,泛型编程中结合auto,用于追踪函数的返回值类型:
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty) {
return x * y;
}
decltype推导四规则
- 如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误。
- 假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&&
- 假设e的类型是T,如果e是一个左值,那么decltype(e)为T&。
- 假设e的类型是T,则decltype(e)为T。
nullptr
C/C++的NULL宏是个有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好,但是在C++的时代,这就会引发很多问题。例如test(int a)
这个函数,传入了a=0
,这个时候你就不知道是传入的NULL还是整数0。
delete和default函数
我们知道C++的编译器在你没有定义某些成员函数的时候会给你的类自动生成这些函数,比如,构造函数,拷贝构造,析构函数,赋值函数。有些时候,我们不想要这些函数,比如,构造函数,因为我们想做实现单例模式。传统的做法是将其声明成private类型。
在新的C++11
中引入了两个指示符,delete
意为告诉编译器不自动产生这个函数,default
告诉编译器产生一个默认的。例下:
struct A {
A() = default; //C++11
virtual ~A()=default; //C++11
};
struct NoCopy {
NoCopy & operator =( const NoCopy & ) = delete;
NoCopy ( const NoCopy & ) = delete;
};
NoCopy a;
NoCopy b(a); //compilation error, copy ctor is deleted
问题出现了,我们为什么需要加default,难道本身不就是default吗?不全然是,比如构造函数,因为只要你定义了一个构造函数,编译器就不会给你生成一个默认的。所以,为了要让默认的和自定义的共存,才引入这个参数。如下:
struct SomeType {
SomeType() = default; // 使用编译器生成的默认构造函数
SomeType(OtherType value);
};
关于delete还有两个有用的地方:
- 让你的对象只能生成在栈内存上
struct NonNewable {
void *operator new(std::size_t) = delete;
};
- 阻止函数的其形参的类型调用
void f(int i);
void f(double) = delete;
若尝试以 double 的形参调用 f(),将会引发编译期错误, 编译器不会自动将 double 形参转型为 int 再调用f(),如果传入的参数是double,则会出现编译错误。
线程库
std::thread为C++11的线程类,使用方法和boost接口一样,非常方便。同时,C++11的std::thread解决了boost::thread中构成参数限制的问题,我想着都是得益于C++11的可变参数的设计风格。
#include <thread>
void work_thread1() {
std::cout << "work_thread1 - 1\r\n" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "work_thread1 - 2" << std::endl;
}
void work_thread2(int iParam, std::string sParam) {
std::cout << "work_thread2 - 1" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "work_thread2 - 2" << std::endl;
}
int main(int argc, char** argv) {
std::thread t1(work_thread1);
std::thread t2(work_thread2, 10, "abc");
t1.join();
std::cout << "join" << std::endl;
t2.detach();
std::cout << "detach" << std::endl;
}
新型智能指针
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
理解智能指针需要从下面三个层次:
- 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
- 智能指针还有一个作用是把值语义转换成引用语义。
包含在头文件<memory>中,shared_ptr
、unique_ptr
、weak_ptr
shared_ptr
shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
- 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
- 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象
- get函数获取原始指针
- 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
- 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍
例:
#include <iostream>
#include <memory>
int main() {
int a = 10;
std::shared_ptr<int> ptra = std::make_shared<int>(a);
std::shared_ptr<int> ptra2(ptra); //copy
std::cout << ptra.use_count() << std::endl;
int b = 20;
int *pb = &a;
//std::shared_ptr<int> ptrb = pb; //error
std::shared_ptr<int> ptrb = std::make_shared<int>(b);
ptra2 = ptrb; //assign
pb = ptrb.get(); //获取原始指针
std::cout << ptra.use_count() << std::endl;
std::cout << ptrb.use_count() << std::endl;
}
运行结果:
unique_ptr
unique_ptr唯一拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
例:
#include <iostream>
#include <memory>
int main(int argc, char** argv) {
{
std::unique_ptr<int> uptr(new int(10)); //绑定动态对象
//std::unique_ptr<int> uptr2 = uptr; //不能赋值
//std::unique_ptr<int> uptr2(uptr); //不能拷贝
std::unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
uptr2.release(); //释放所有权
}
//超过uptr的作用域,內存释放
}
weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
例:
#include <iostream>
#include <memory>
int main(int argc, char** argv) {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;
if(!wp.expired()) {
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;
}
}
//delete memory
}
右值引用和move语义
在老版的C++中,临时性变量(称为右值”R-values”,位于赋值操作符之右)经常用作交换两个变量。比如下面的示例中的tmp变量。示例中的那个函数需要传递两个string的引用,但是在交换的过程中产生了对象的构造,内存的分配还有对象的拷贝构造等等动作,成本比较高。
void naiveswap_(string &a, string &b) {
string temp = a;
a=b;
b=temp;
}
C++ 11增加一个新的引用(reference)类型称作右值引用(R-value reference),标记为typename &&。他们能够以non-const值的方式传入,允许对象去改动他们。这项修正允许特定对象创造出move语义。
举例而言,上面那个例子中,string类中保存了一个动态内存分存的char指针,如果一个string对象发生拷贝构造(如:函数返回),string类里的char内存只能通过创建一个新的临时对象,并把函数内的对象的内存copy到这个新的对象中,然后销毁临时对象及其内存,这是原来C++性能上重点被批评的事。
能过右值引用,string的构造函数需要改成“move构造函数”,如下所示。这样一来,使得对某个stirng的右值引用可以单纯地从右值复制其内部C-style的指针到新的string,然后留下空的右值。这个操作不需要内存数组的复制,而且空的暂时对象的析构也不会释放内存,其更有效率。
class string {
string (string&&); //move constructor
string&& operator=(string&&); //move assignment operator
};
The C++11 STL中广泛地使用了右值引用和move语议。因此,很多算法和容器的性能都被优化了。