1、智能指针
自主回收机制
1.作用,目的
自动管理资源的释放,防止由于疏忽或程序逻辑错误导致的资源泄漏问题;
在作用域到期后,智能指针自动析构,在析构函数中自动释放被托管的资源,
以防止资源泄漏的各种问题;
(智能指针是一个类,它将裸指针(带*的指针)进行了封装,实现的指针的自动释放,
它的高明之处就在于程序员只需要一次性的设计出一个具有良好功能的智能指针类,
用它实例化出来的对象会自动对对象内存的堆资源进行管理,
而不需要程序员去干涉,它自己就可以很好的完成对堆内存的管理;)
2.设计思想
利用当栈对象的生存周期结束时,会自动调用析构函数,来进行对象的销毁;
3.分类:
auto_ptr : 所有权唯一 :旧指针所有权赋予新的智能指针,旧智能指针的所有权取消;
unique_ptr : 所有权唯一 :限制拷贝和移地接口;
shared_ptr : 强智能指针 :引用计数;强智能指针相互引用,内存泄漏(引入weak_ptr来解决);
weak_ptr :弱智能指针 :不加引用计数,不能单独使用(配合shared_ptr使用);
必须给每一个资源都添加一个相应的引用计数,只有当资源的引用计数为0时,
智能指针才能够去释放它所管理的资源,否则什么也不做;
一个资源只能对应一个引用计数,因此引用计数肯定是在堆上创建的,
引用同一个资源的所有智能指针是共享这个引用计数的,才不至于在增减资源引用计数时导致错乱;
智能指针交叉引用:
含义:强智能指针的交叉引用; shared_ptr<B>_ptrb; shared_ptr<A>_ptra;
引用计数还是1,并没有达到析构的条件;导致A、B两个对象无法析构;
导致的结果是:对象无法析构,资源无法释放,问题严重;
解决交叉引用:使用强弱智能指针的时候,创建对象的时候,持有它的强智能指针;
当其他地方想用这个对象的时候,应该持有该对象的弱智能指针;
原因: 因为弱智能指针相当于观察者,不会引起对象的引用计数增加的,
当通过弱智能指针访问对象时,需要先进行lock提升操作,
提升成功,证明对象还存在,再通过强智能指针访问对象;
C++如何防止内存泄漏:
1、良好的代码设计和逻辑处理:如代码检查,使用合理的异常处理,智能指针等;
2、静态分析检测工具:包括手动检测和静态工具分析,这是代价最小的调试方法;
3、动态检测:包括常用的Valgrind,Rational purify工具可在程序运行时检测代码
是否存在内存泄漏,也可以借助Linux shell的强大命令,如top命令查看
进程内存使用情况,cat/proc/meminfo来查看当前系统内存使用量、
交换分区虚拟内存使用量等等来判断内存的异常使用;
2、STL
容器:
顺序容器:
vector :向量容器,底层内存可增长的数组,每次以2倍的方式增长;
deque :双端队列容器,底层动态开辟的二维数组;
list :列表容器,底层带头结点的双向链表容器;
vector : 从后面快速插入删除,直接访问任何元素;
list : 支持任意位置的插入删除,访问效率低;
deque : 支持从前面、后面快速插入删除,直接访问任何元素;
关联容器:
set : 单重集合,底层红黑树;
multiset : 多重集合,底层红黑树;
map :单重映射表,底层红黑树;
multimap :多重映射表,底层红黑树;
STL中常用的关联容器有四种:set、map、multiset、multimap。
这四种容器中的元素都是按照键有序排列的,
向容器中插入元素时会将元素插入到适当的位置,插入删除操作都不会破坏键的有序性。
关联容器中元素的键必须是可比较的。如果键是基本类型,可以直接使用,因为基本类型都是可比较的;
如果键是自定义类型,则需要定义带有比较谓词的构造函数才能作为关联容器的类型参数;
容器适配器:
stack :栈,底层依赖deque容器来实现先进先出的栈结构;
queue :队列,底层依赖deque实现一个先进先出的队列结构;
priority_queue :优先级队列, 底层依赖vector实现一个大根堆结构,默认值越大,优先级越高;
容器适配器将原容器进行了一层封装,
底层基于普通容器,上层对外提供封装后的新接口,满足不同使用者的需求
迭代器失效(iterator):分三种情况考虑:
数组型数据结构 :插入点和删除点之后的迭代器全部失效;
链表型数据结构 :仅仅删除使指向删除位置的迭代器失效;
树型数据结构 :用红黑树,仅仅删除使指向删除位置的迭代器失效;
map的底层实现:
C++STL 的 set 和 map 容器底层都是由红黑树来实现的, 因此map和set的实现原理就是红黑树的实现原理,
其中 map 是用来存储键值映射对的,它把 key_value 打包成 pair 对象存储在红黑树结构上,
元素都是经过排序的,因此可以在 O(log2n)的时间复杂度内 对 set 和 map 进行增删查操作,效率非常高。
对于红黑树的阐述,以下罗列一些基本信息, 更详细的红黑树理解:
红黑树是一棵非严格的平衡二叉树,左右子树的高度差不能超过较短子树高度的 2 倍,
数据的增删查效率都比较高,平均时间复杂度在 O(log2n)。
在此给出红黑树的定义:
1)每个结点要么是红的,要么是黑的;
2)根结点是黑的;
3)每个叶结点,即空结点(NIL)是黑的;
4)如果一个结点是红的,那么它的俩个儿子都是黑的;
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点;
红黑树不像 AVL(平衡二叉树)树那样维持了二叉树的高度平衡(左右子树的高度差不能超过 1),
因此在插入删除数据时,所做的旋转操作比起红黑树来说,那就少很多了,因此其 效率也比 AVL 树高;
红黑树插入一个新节点,旋转的次数最多 2 次,删除一个节点旋转的次数最多 3 次;
在 C++STL 中,map 和 multimap,set 和 multiset 这四种关联容器的底层都是由红黑树 来实现的,
因此如果要把自定义类类型作为 set 和 map 的元素类型的话,
一定要给自定义 类型提供operator>或者operator<比较运算符的重载函数,
因为红黑树是一棵二叉排序树,入 set 和 map 的元素都是要经过排序的;
3、设计模式:
1、单例模式:
类只能实例化一个对象,并提供一个访问它的全局访问点,该实例被所有程序模块共享;
1.屏蔽构造函数(私有化它的构造函数,以防止外界创建单例类的对象)
2.提供接口生成唯一的对象
(使用类的私有静态指针变量指向类的唯一实例,使用一个公有的静态方法获取该实例)
不能依赖对象调用
不能返回类类型
2、工厂模式:
1.生成对象;
1.简单工厂模式;(所有产品由一个工厂生产)
2.工厂模式;一个工厂可以生产一种类型的多个不同产品;
3.抽象工厂模式;一个抽象工厂可以生产多个实际工厂,每个实际工厂可以生产不同类型的产品;
3、代理模式/装饰者模式区别
篮球运动员和经纪人:代理(帮你处理一些你不关注的事务,签合同谈薪资等)
篮球运动员和教练:装饰(帮助你提升自己的能
4、观察者模式:
监听者:处理事件;
观察者:观察事件;通知感兴趣的监听者处理事件;
对象之间的一对多的依赖关系,多个观察者对应一个被观察者对象,
当被观察者的状态改变时,观察者会接收到响应的通知,然后去进行一系列的操作
4、异常机制
OOP面向对象语言基本上都提供了 try catch 异常处理机制,
那么在C++里面,异常处理是怎么进行的:
异常处理的含义:
当程序运行发生错误时,不能简单的结束掉程序,而因该抛出异常,
再通过用户捕获异常,如果该异常是在预期范围之内发生的错误,
那么用户的异常处理代码处理完这个异常,程序是可以继续运行的;
如果这个错误不可挽回,那么可以结束掉程序;
5、C++什么时候出现访问越界
内存访问越界(同内存非法访问):程序代码访问了系统已回收的 或 未经分配的内存;
坏处:一般是不可预期的错误,程序有可能正常运行,结果也正常;
或者程序运行正确,但结果不正确;
严重一点的结果就是非法访问了受保护的内存,程序直接挂掉了;
内存访问越界的示例:
1.访问数据越界,常见的;
2.字符串操作函数如:strcpy,strcat等函数在操作字符串数组的时候,
由于字符串数组空间不够,而导致的内存访问越界;
3.字符串没有末尾的 '\0' :导致进行strcpy,用指针遍历字符串等操作的时候,
出现内存访问越界错误;
4.通过new或者malloc开辟的内存,在代码访问时,却没有在申请长度之内访问,
出现了内存访问越界;或者又访问了被free或者delete掉的内存;
5.函数返回了局部变量或临时变量的地址或引用,函数返回后在调用方通过地址或引用
进行内存越界访问(非法访问),造成不可预期的后果;
6.通过指针指向一个临时对象,当语句完成后,临时对象可能就被析构掉了,
如果此时通过这个指针调用对象的成员,那就可能出现内存非法访问了;
7.使用了不合理的类型强转,让较大类型的指针指向较小类型指针指向的内存,
此时通过大类型指针间接访问内存,肯定出现内存越界访问;
比如把基类类型指针强转成派生类类型指针,也就是让派生类类型指针指向了一个基类对象;
(默认是不允许的,但是你可以通过类型强转做到)
8.在多线程中访问共享内存时,当在某一个线程中访问这个对象成员时,
不确定这个对象是否已经在其他线程中被析构掉了,可能出现内存访问越界;
(这个问题可以通过线程安全的带引用计数的智能指针来解决shared_ptr和weak_ptr)
内存泄漏:系统分配给你的内存,你却找不到它了(一般是由于保存这块内存的指针备覆盖了),
因此导致这块内存资源无法释放;
坏处:随着程序的运行,会导致系统的内存占用量不断增高,
整个系统由于内存紧张而造成卡顿或者假死,如果是服务器的话,结果就严重了;
6、函数指针的作用
指针的优点
1.可以动态分配内存;
2.进行多个相似变量的一般访问;
3.为动态数组结构,尤其是树和链表,提供支持;
4.遍历数组,如解析字符串;
5.高效的按引用“复制”数组与结构,特别是作为函数参数的时候,
可以按照引用传递函数参数,提高开发效率;
其实函数指针与普通指针没什么差别,只是指向的内容不同而已。
主要还是一个简化结构和程序通用性的问题
函数指针的主要作用:
1.实现面向对象编程中的多态性;(命名不会重复)
2.回调函数;
(Windows编程中的事件handle函数,即回调函数,在事件队列都是一个函数指针来保存的;
程序可以通过扫描这个事件队列来获取每个事件对应的处理函数,然后调用它,即为回调函数。)