auto
1.首先要认识到auto的“自动推导”类型只能用于“初始化”的场合;
其中包括“赋值初始化”和“花括号初始化”,如果不是初始化的形式,只是“纯”变量声明,就无法使用auto;
2.这里有一个特殊情况,在类的成员变量初始化的时候,目前的C++标准不允许使用auto初始化
3.auto可以推导出值类型,绝不会是引用
4.auto的最佳实践”range-based for”,最好使用const auto&/auto&
decltype
decltype自带表达式,所以不需要变量后面再有表达式,可以直接声明变量
圆括号里就是可以用于计算类型的表达式;
int x=0;
decltype(x) x1; decltype(x)& x2;//x2是int&
decltype(x2) x6=x2; //x6是int&,引用必须赋值
decltype(&x) x4;
decltype(auto)//C++14
定义函数指针;定义类
unique_ptr
unique_ptr是最简单,最容易使用的一个智能指针,在声明的时候必须用模板参数指定类型
unique_ptr[HTML_REMOVED] ptr1(new int(10));//int智能指针
unique_pre[HTML_REMOVED] ptr2(new string(“hello”));//string智能指针
unique_ptr实际上并不是指针,而是一个“对象”;
所以不要对他调用delete,他会自动管理初始化时的指针,在离开作用域时候释放内存
不能随意移动指针
一定要初始化以后才可以使用,未初始化的unique_ptr表示空指针,直接操作空指针会产生致命的错误
为了避免这种错误,可以使用make_unique()工厂函数,强制创建智能指针的时候必须初始化(C14)
auto ptr3=make_unique[HTML_REMOVED](42);//工厂函数创建智能指针
unique_ptr的所有权问题——“唯一”
为了实现这个目的,unique_ptr应用了C的转移(move)语义,同时禁止了拷贝赋值
在向另一个unique_ptr赋值的时候,必须用std::move()函数显示的声明所有权转移
赋值操作之后,所有权就被转走了,原来的unique_ptr变成了空指针
新的unique_ptr接替了管理权,保证了所有权的唯一性
shared_ptr
他是一个比unique_ptr更智能的指针,它的所有权是可以被安全“共享”的。
shared_ptr[HTML_REMOVED] ptr1(new int(10));
assert(ptr=10);
shared_ptr[HTML_REMOVED] ptr2(new string(“hello”));
assert(pr2=”hello”);
auto ptr3=make_shared[HTML_REMOVED](42);
shared_ptr支持安全共享的秘密在于内部使用了“引用计数”
引用计数最开始的时候是1,表示只有一个持有者,
如果发生拷贝赋值——也就是共享的时候,引用计数就增加;
发生析构的时候,引用计数就减少;
只有当引用计数减到0的时候,也就是说没有一个人使用这个指针了,此时调用delete释放内存
因此shared_ptr具有完整的“值语义”(即可以拷贝赋值),所以可以在任何场合替代原始指针,不用在担心资源回收的问题
虽然shared_ptr非常“智能”,但是天下没有免费的午餐,他是有代价的
引用计数的存储和管理都是成本,这是shared_ptr不如unique_ptr的地方
另外要注意shared_ptr的销毁动作,注意析构函数,不要有复杂,阻塞的操作,否则会阻塞整个进程或线程
shared_ptr的引用计数也有一个新的问题,就是“循环引用”,这在把shared_ptr作为类成员的时候最容易出现,典型的例子就是链表节点
要想从根本上杜绝循环引用,就必须要用到它的小帮手——weak_ptr;
weak_ptr
weak_ptr顾名思义,功能很弱,它专门为打破循环引用而设计,只观察指针,不会增加引用计数,在需要的时候;
可以调用成员函数lock(),获取shared_ptr
异常
三个特点:
1.异常的处理流程是完全独立的,彻底分离了业务逻辑与错误逻辑,看起来更清楚
2.异常是绝对不能被忽略的,必须被处理
3.异常可以用在错误码无法使用的场合
C里异常的用法
用try把可能发生异常的代码“包”起来,然后编写catch块捕获异常并处理
C为处理异常设计了一个配套的异常处理类型体系,定义在标准库的[HTML_REMOVED]里
void func_noexcept() noexcept//声明绝不会抛出异常,但不能保证,(如果有异常,请让我直接crash
lambda函数式编程
在C/C++里,函数都是平级的,不允许定义嵌套函数,函数套函数
因为lambda表达式是一个变量,所以,我们就可以随时随地的调用“就地”定义函数
限制它的作用域和证明周期,实现函数的局部化
lambda表达式可以“捕获”外部变量
lambda的形式
[],术语叫“lambda”引出符
auto f1={};//圆括号声明入口参数,花括号定义函数体
lambda的变量捕获
[=]表示按值捕获所有外部变量,表示内部是值的拷贝,并且不能修改
[&]是按引用捕获所有外部变量,内部以引用的方式使用,可以修改
泛型的lambda
C++14多了泛型化,相当于简化了的模板函数
auto f=//参数使用auto声明,泛型化
容器
顺序容器:array,vector,deque,list,forward_list
有序容器
C++的有序容器使用的是树结构,通常是红黑树——有着最好查找性能的二叉树
标准库里一共有四种有序容器:set/multiset map/multimap
set是集合,map是关联数组(也叫“字典”)
有multi前缀的容器表示可以容纳重复的key,内部结构与无前缀的相同;
因为有序容器的数量很少,所以要理解“有序”概念
容器是如何判断两个元素的“先后次序”,知道了这一点,才能正确的排序
在定义容器的时候必须指定key的比较函数:一种是重载,一种是自定义模板参数
无序容器
unordered_set/unordered_multiset,unordered_map/unordered_multimap
无序容器同样也是集合和关联数组,用法上与有序容器几乎一样
区别在于内部数据结构,他不是红黑树,而是散列表(哈希表,hashtable)
因为他采用的是散列表存储数据,元素的位置取决于计算的散列值,没有规律可言
所以就是“无序”的,也可以理解为“乱序”的
总结:
标准容器可分为三大类,即顺序容器,有序容器,无序容器
所有容器中最优先选择的是array和vector,他们的速度最快,开销最低
list是链表结构,插入删除的效率高,但查找效率低
有序容器是红黑树结构,对key自动排序,查找效率高,但有插入成本
无序容器是散列表结构,由hash值计算存储位置,查找和插入的成本都很低
有序容器和无序容器都属于关联容器,元素有key的概念,操作概念实际上是在操作key,所以要定义对key的比较函数或者散列函数
多线程
在C语言里,线程就是一个能够独立运行的函数,
“读而不写”就不会有数据竞争
所以在C多线程编程里读取const总是安全的,对类调用const成员函数,对容器调用只读算法也总是线程安全的
“最好的并发就是没有并发,最好的多线程就是没有线程”
四个原则:
1.仅调用一次
首先声明一个once_flag类型的变量,最好是静态,全局的(线程可见),作为初始化的标志
然后调用call_once()函数,以函数式编程的方式,传递这个标志和初始化函数
这样C就会保证,即使多个线程重入call_once(),也只有一个线程会成功运行初始化
call_once()完全消除了初始化时的并发冲突,在他的调用位置根本看不到并发和线程
2.线程局部存储
有时候,全局变量并不一定是必须共享的,换句话说,这应该是线程独占所有权
不应该在多线程之间共享,术语叫做“线程局部存储”
这个功能在C里由关键字thread_local实现,他是一个和static,extern,同级的变量存储说明
有thread_local标记的变量在每个线程都会有一个独立的副本,是“线程独占的”,所以就不会有竞争读写的问题
3.原子变量
解决同步问题,不能让两个线程同时写,也就是“互斥”
互斥量(Mutex),但它的成本太高,对于小数据,应该“原子化”
原子就是,在多线程领域里的意思就是不可分的。
操作要么完成,要么未完成,不能被外部打断
目前,C仅让一些基本的类型原子化
atomic_int,atomic_long等,初始化只能用圆括号或者花括号,因为禁用了拷贝构造函数。
3.线程
call_once,thread_local,atomic都不与线程直接相关
C标准库里有专门的线程类thread,使用它就可以简单地创建线程