cpp这门语言与其他语言设计上的不同之处,在于cpp为了高效运行,没有采用在堆上分配对象通过GC管理对象。而是选择了一种更加高效但是更加困难的方式,通过尽量在栈上分配内存。因为栈上分配和释放内存只需要几条cpu指令就能完成,而在栈上则需要调用分配函数这需要更多的时间。而且栈的内存布局更加紧凑,对缓存更加的友好。但是也因为栈的先进先出的特性,这让对象的生命周期更加的难以管理,因为栈帧返回后直接就释放了。而且函数参数的传参也变得复杂,有很多的方式减少传参带来的开销,许多cpp特性就是为了解决这个问题产生的。

cpp为了管理堆上的对象,使用了一个栈上的局部对象代表或者管理一个堆上的对象。这就是raii,当对象出了作用域,自动调用析构函数释放堆上的资源,这是编译器添加的。但是这样就多了一个问题,该如何跨函数作用域甚至是跨线程传递对象,于是又有了shareptr,move等方法。

说到智能指针,他们可以指定deleter函数,来控制资源释放的方式,这里可以用很多黑魔法。但是因为deleter函数指指针对象的内部成员,并不是存的函数指针,也就是说不同的deleter函数会导致有不同的内存布局,造成不同的abi,这里一个很坑的地方。另外shareptr其实是cpp的一个有点妥协的设计,cpp的理念就是明确所有权,生命周期可预测,零成本抽象。但是shareptr使对象的生命周期变得不可预测,引入了隐藏成本,破坏了局部推理,必须全局理解才能判断对象行为。所以一定要谨慎使用shareptr。

学习cpp的过程中要始终牢记:内存布局,生命周期,所有权

内存中的值语义

在语法层面上,cpp将内存中的值分为了左值,纯右值,将亡值。左值就是一个有稳定地址的量,将亡值是有稳定地址的值但是即将被移动的值,右值即使没有稳定地址的值,这里的稳定地址是语法层面的可取地址,而不是真的物理地址。

函数操作的行为:函数的输入参数也就是形参有以下:

  • T a ,值拷贝
  • T& a, 左值引用
  • const T& a,常量左值引用
  • T&& a, 右值引用

T a这种形式的形参行为复杂,他可以接受左值也可以接受右值,接受的值的类型如果不一致会尝试隐式转换。1. 当实参是一个左值的时候,他会调用对象的拷贝函数,成本高。2. 如果实参是一个临时对象类型的右值,在17之前通常会被优化成直接构造在形参中,在17之后这点被强制执行了,所以传入的右值几乎没有开销,只有一次构造的开销。如果是一个将亡值类型的右值,则会调用移动构造到形参处。总结:如果要拷贝副本,或者想要移动资源就使用值拷贝的形参

T& a这种形式的形参,只能传入左值的实参,其他类型会变异报错。所以他的因为较为简单,直接在源数据处操作

const T& a, 可以接受左值和右值,是最安全高效的一种形参。当实参为左值时行为和T& a类似,但是不能修改源数据;当实参是右值的时候,他会延长这个右值的生命周期直到引用周期结束为止。如果是将亡值他的的行为和正常的左值引用一样,不会有什么拷贝和移动操作。ps:对于小的可复制平凡类型直接值拷贝比这种形式更高效

T&& a这种形式的形参只能接受右值实参,他的行为会绑定传入的右值实参,这一点和const T&相同,但是他可以修改移动这个右值,而const T&不行。所以通常使用在移动函数上。如果是模板的话这个则代表完美转发。

执行层和状态层

  • 执行层: thread, stack, scheduler, coroutine,control flow
  • 状态层:object,heapmemory,ownership,resource,cachestate

程序的执行就是执行层不断操作状态层

对象和线程

  • 为什么协程困难

  • 异步为什么破坏直觉