static_cast/dynamic_cast等四种转换
1、static_cast<目标数据类型>原数据类型
1)用于类层次结构中基类和派生类之间指针或引用的转换
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的
2)用于基本数据类型之间的转换,如把int转换成char。这种转换的安全由开发人员来保证
3)把空指针转换成目标类型的空指针
4)把任何类型的表达式转换为void类型
2、const_cast<目标数据类型>原数据类型
cost_cast即用于强制转换指针或者引用的const或volatile限制,特别注意的是,const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
3、reinterpret_cast<目标数据类型>原数据类型
reinterpret_cast运算符用于处理无关类型之间的转换,他会产生一个新的值,这个值会有与原始参数(原数据类型)有完全相同的比特位
- 从指针类型到一个足够大的整数类型
- 从整数类型或者枚举类型到指针类型
- 从一个指向函数的指针到另一个不同类型的指向函数的指针
- 从一个指向对象的指针到另一个不同类型的指向对象的指针
- 从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针
- 从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针
4、dynamic_cast<目标数据类型>原数据类型
(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。
(2)不能用于内置的基本数据类型的强制转换。
(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。
(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。
基类中需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。
这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,
只有定义了虚函数的类才有虚函数表。
(5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
向上转换,即为子类指针指向父类指针(一般不会出问题);向下转换,即将父类指针转化子类指针。
向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。
在C++中,编译期的类型转换有可能会在运行时出现错误,特别是涉及到类对象的指针或引用操作时,更容易产生错误。Dynamic_cast操作符则可以在运行期对可能产生问题的类型转换进行测试。
深度优先DFS(Depth First Search)和广度优先BFS(Breath First Search)
DFS的原理:从节点一直往下一条路走到底,当无路可走的时候,返回上一个节点然后从另外一条路开始,不断重复此过程,直到所有的点都遍历完,核心思想是不撞南墙不回头。
DFS非递归实现:利用stack的后进先出的属性
void StackDFS(const Node*pNode )
{
string str;
Node *p = const_cast<Node*>(pNode);
std::stack<Node*>q;
q.push(p);
while (!q.empty())
{
Node *pTemp = q.top();
q.pop();
if (pTemp->l)
q.push(pTemp->l);
if (pTemp->r)
q.push(pTemp->r);
char szMsg[20] = { 0 };
sprintf_s(szMsg,20,"%d ", pTemp->val);
str += szMsg;
}
printf(str.c_str());
}
BFS非递归实现:利用queue的先进先出的属性
void QueueBFS(const Node*pNode)
{
Node *p = const_cast<Node*>(pNode);
std::queue<Node*> q;
q.push(p);
string str;
while (!q.empty())
{
Node *pTemp = q.front();
q.pop();
if( pTemp->l )
q.push(pTemp->l);
if( pTemp->r )
q.push(pTemp->r);
char szMsg[20] = { 0 };
sprintf_s(szMsg,20, "%d ", pTemp->val);
str += szMsg;
}
printf(str.c_str());
}
红黑树特征
1.根节点为黑
2.叶节点为黑
3.红色节点下必须为黑
4.从节点出发,任意路径到叶节点包含相同数目的黑色节点
5.节点要么是红要么是黑
基本操作是添加、删除和旋转。在对红黑树进行添加或删除后,会用到旋转方法。旋转的目的是让树保持红黑树的特性。旋转包括两种:左旋 和 右旋。
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的查找、插入和删除操作的时间复杂度是O(lgn)
vector底层内存分配原理
采用两层配置器
第一层malloc/free
第二层:为了避免内存泄露,如果要分配的区块大于128bytes,则移交给第一级配置器处理 如果要分配的区块小于128bytes,则以 内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。类似于buddy算法
线程/进程间同步方法
互斥锁:pthread_mutex_t
提供3种形式,
PTHREAD_MUTEX_NORMAL :不提供死锁检测。尝试重新锁定互斥锁会导致死锁
PTHREAD_MUTEX_ERRORCHECK :提供错误检查。如果某个线程尝试重新锁定的互斥锁已经由该线程锁定,则将返回错误。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误
PTHREAD_MUTEX_RECURSIVE:保留锁定计数这一概念。线程首次成功获取互斥锁时,锁定计数会设置为 1。线程每重新锁定该锁一次,锁定计数就增加 1。线程每解除锁定该互斥锁一次,锁定计数就减小 1。 锁定计数达到 0 时,该互斥锁即可供其他线程获取。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
PTHREAD_MUTEX_DEFAULT:则尝试以递归方式锁定该互斥锁将产生不确定的行为。对于不是由调用线程锁定的互斥锁,如果尝试解除对它的锁定,则会产生不确定的行为。如果尝试解除锁定尚未锁定的互斥锁,则会产生不确定的行为。
mutex请求时,阻塞睡眠在内核态。
自旋锁:pthread_mutex_spin
自旋锁不会引起内核睡眠,如果别的进程或者线程在使用,则不断消耗cpu去轮询。通常在锁定时间非常短但又需要及时响应的时候才用自旋锁,当唤起线程的消耗小于等待自旋锁的消耗的时候使用。
条件锁:pthread_cond_t
条件锁主要用来等待条件,避免忙等,而非互斥。多个线程共享一个条件变量的时候,避免忙等待使用条件锁,等待的线程陷入睡眠,条件成立后唤醒。
1.消费者基本代码如下:
pthread_mutex_lock(&mutex); // 拿到互斥锁,进入临界区
while( 条件为假)
pthread_cond_wait(cond, mutex); // 令线程等待在条件变量上
修改条件
pthread_mutex_unlock(&mutex); // 释放互斥锁
2.生产者基本代码如下:
pthread_mutex_lock(&mutex); // 拿到互斥锁,进入临界区
pthread_cond_signal(cond); // 通知等待在条件变量上的消费者
pthread_mutex_unlock(&mutex); // 释放互斥锁
读写锁
pthread_rwlock_t
读写锁的分配规则:
(1)只要没有线程占用写锁,那么任意数目的线程都可以持有这个读锁。
(2)只要没有线程占用读写锁,那么才能为一个线程分配写锁。
读锁相当于一个共享锁,写锁i相当于独占锁。
初始化:pthread_rwlock_init
读取:pthread_rwlock_rdlock
尝试读取:pthread_rwlock_tryrdlock
写锁定:pthread_rwlock_wrlock
尝试写:pthread_rwlock_trywrlock
解锁:pthread_rwlock_unlock
销毁:pthread_rwlock_destroy
信号变量:sem_t
进程和线程同步对象,内核睡眠。信号量是一个计数,p/v操作。
(1)P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
(2)V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
posix接口标准中:sem_init的时候可以指定是否进程间共享该变量、sem_wait、sem_post、sem_trywait、
SystemV:semget、semctl 主要用在进程间通信
临界区
linux并没有相关接口,windows有critical_section,linux可以使用sem_t计数为0 和mutex来模拟。
可重入锁:同一线程如果对锁申请占用后继续执行重复申请占用不会导致死锁,这种称为可重入也叫可递归锁,mutex中设置recursive属性即可实现。
用户态内存池设计
借用内核slab和buddy算法
< 128kb使用slab 采用缓存结构,为小对象分配
>128kb使用buddy--2^0到2^n的内存池设计,采用多级链表,内存通过链表拆和并实现管理。
c++多态实现的底层原理 非ftable
函数栈调用原理 入栈原理和出栈原理
栈的发展方向是由高地址到低地址进行
栈帧指针放在寄存器:32位系统ebp 64位系统放在rbp
栈顶指针放在寄存器:32位系统esp 64位系统放在rsp
当函数调用的时候,先将栈帧地址入栈pushl
接着根据调用过程,将变量依次放到栈中,(有些放到栈中,有些放到寄存器中)栈中每放一个变量,栈顶指针就向下移动sizeof变量长度,
函数返回的时候,将esp+sizeof,而ebp读取值(指向上一个ebp地址),则完成了函数返回,返回值放到寄存器eax中。
内存原理
伙伴算法buddy 和 slab是linux用来管理物理内存的算法,伙伴减少外部碎片 slab减少内部碎片,物理内存是页框,虚拟内存是分页
buddy是维护了从1page~1024page(4kB/page)的11个list,最多分配4MB。
slab是维护小对象的方法,由缓存功能,申请内存的时候优先从缓存中申请,如果空间不够则再从buddy申请内存。
malloc是linux库函数,主要是通过brk和mmap系统调用实现,brk分配128k以下的内存,分配的都是虚拟内存,实际访问时才会进行物理内存和虚拟内存的映射
virtual memory 和 physical memory的映射依赖mmu,虚存页面到物理页框的映射叫做页面的加载,首先会先到ram的页表中查询映射关系,查询不到发生缺页中断,判断有无空闲物理页框,有的话建立物理页框和虚拟内存映射并写入页表,再次执行指令,没有的话,需要触发页面置换算法,一般是lru算法,将最不常用的页面置换到磁盘空间,腾出空间继续操作
页表是内存转换的基础,为了增加页表的访问速度,cpu增加TLB寄存器支持页表快速访问,由于程序运行过程中页表也需要装载到内存中,页表过大无法装载,又引入了多级页表的概念
malloc:用户态内存申请---通过进程中的(堆、栈、常量区、静态区、代码段)对应的结构体vm_area_struct进行内存池的实现方式,先申请一大块内存,然后将内存分成不同大小的内存块,然后用户申请内存时,直接从内存池中选择一块相近的内存块分配出去。
brk:当内存申请小于128kb的时候,调用brk.函数族改变调用程序的虚拟内存中数据段的结束位置(program break)增加这个位置来增加数据区大小以达到分配空间的目的。glibc的brk是brk系统调用的一个包装,返回值略有不同,实作是一样的
malloc:当大于128KB的时候调用mmap从虚拟空间去分配,(task->mm->vm_area_struct
malloc使用空闲链表组织堆中的空闲区块,空闲链表有时也用双向链表实现。每个空闲区块都有一个相同的首部,称为“内存控制块” mem_control_block
,其中记录了空闲区块的元信息,比如指向下一个分配块的指针、当前分配块的长度、或者当前区块是否已经被分配出去。这个首部对于程序是不可见的,malloc
返回的是紧跟在首部后面的地址,即可用空间的起始地址。
物理内存结构图:
ZONE_DMA的范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。
ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。
ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。
TCP原理算法
tcp算法有很多变种,常见的有tcp reno,tcp new reno,他们对拥塞避免及恢复有着不一样的定义方式,详情参考rfc文档793,2581,2582等
TCP三次握手
-
第一次握手:
客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT
状态,等待服务器端确认。 -
第二次握手:
服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD
状态。 -
第三次握手:
客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED
状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
TCP四次挥手
挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起:
- 第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入
FIN_WAIT_1
状态,这表示Client端没有数据要发送给Server端了。 - 第二次分手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入
FIN_WAIT_2
状态,Server端告诉Client端,我确认并同意你的关闭请求。 - 第三次分手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入
LAST_ACK
状态。 - 第四次分手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入
TIME_WAIT
状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。 - 为什么要进行四次挥手?因为TCP是全双工,必须保证双方都停止资源。为什么等待2MSL?如果网络超时,server重发FIN,由于客户端已经关闭链接,就会找不到;如果重新建立的连接端口号与上一次端口号相同,则会造成混乱。
TCP拥塞控制算法
四种算法:
- 慢开始 假设当前发送方拥塞窗口cwnd的值为1,而发送窗口swnd等于拥塞窗口cwnd,因此发送方当前只能发送一个数据报文段(拥塞窗口cwnd的值是几,就能发送几个数据报文段),接收方收到该数据报文段后,给发送方回复一个确认报文段,发送方收到该确认报文后,将拥塞窗口的值变为2,当前的拥塞窗口cwnd的值已经等于慢开始门限值,之后改用拥塞避免算法。
- 快恢复
- 快重传
- 拥塞控制
TCP粘包
什么是TCP粘包
发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
(1)发送方原因
TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组 2)收集多个小分组,在一个确认到来时一起发送。Nagle算法造成了发送方可能会出现粘包问题。
(2)接收方原因
TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
如何处理粘包现象?
(1)发送方
对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。
(2)接收方
接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。
应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。
解决办法:循环处理,应用程序从接收缓存中读取分组时,读完一条数据,就应该循环读取下一条数据,直到所有数据都被处理完成,但是如何判断每条数据的长度呢?
格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。
发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。
类模板虚拟化
hash算法和冲突解决算法
内联函数是否可以为虚函数
编写string实现类
信号槽原理
thread condition_变量
orderd_map的实现原理
map和orderd_map的时间复杂度
clone和fork
clone和frok最终都调用的是do_fork,但是有所不同
fork是进程创建,父进程返回子进程Id,子进程返回0,创建进程的时候复制内核空间,采用写时复制的方法,避免浪费页资源等。
clone 不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程
clone和fork的区别:(1) clone和fork的调用方式很不相同,clone调用需要传入一个函数,该函数在子进程中执行。(2)clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。 (void *child_stack,)也就是第二个参数,需要分配栈指针的空间大小,所以它不再是继承或者复制,而是全新的创造。clone函数允许用户指定子进程继承或者拷贝父进程的某些特定资源,如信号量、文件描述符、文件系统、内存等
clone创建线程:
int thread_func(void *lparam){return 0;}
void *pstack = (void *)mmap(NULL,STACK_SIZE,PROT_READ | PROT_WRITE ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_ANON , -1, 0);
ret = clone(thread_func, (void *)((unsigned char *)pstack + STACK_SIZE),
CLONE_VM | CLONE_FS | CLONE_THREAD | CLONE_FILES | CLONE_SIGHAND | SIGCHLD, (void *)NULL);
clone创建进程:
int child_progress(void *arg){return 0;}
char *stack = malloc(4096);
int mask = CLONE_VM|CLONE_VFORK|CLONE_CHILD_SETTID; ret = clone(child_progress,stack+4096,mask,NULL,NULL,NULL,&tid);
数据库增加索引的原理
可重入函数
可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个线程调用此函数时,很有可能使有关全局变量变为不可知状态。
线程安全函数
线程安全就是说多线程访问同一代码,不会产生不确定的结果。换句话说,线程安全就是多线程访问时,采用加锁机制,当一个线程访问该类的某个数据时,用锁对数据进行保护,其他线程不能访问该数据直到该线程读取完,其他线程才可使用,线程安全不会出现数据不一致或者数据污染。反之, 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
accept发生在三次握手的哪个阶段
第二次握手:客户端connect
第三次握手:服务端accept
构造函数和析构函数调用虚函数
当实例化一个派生类对象时,首先进行基类部分的构造,然后再进行派生类部分的构造。即创建Derive对象时,会先调用Base的构造函数,再调用Derive的构造函数。所以在构造函数中调用虚函数,只会调用基类的函数,而不会调用派生类的函数。
进程调度
linux将进程分为普通进程和实时进程
调度器的优先级顺序为 Stop_ask > Real_Time > Fair > Idle_Task
普通调度:使用的是 CFS 的调度算法,即完全公平调度器
实时调度:分为两种:SCHED_FIFO 和 SCHED_RR:
这两种进程都比任何普通进程的优先级更高(SCHED_NORMAL),都会比他们更先得到调度。
SCHED_FIFO : 一个这种类型的进程出于可执行的状态,就会一直执行,直到它自己被阻塞或者主动放弃 CPU;它不基于时间片,可以一直执行下去,只有更高优先级的 SCHED_FIFO 或者 SCHED_RR 才能抢占它的任务,如果有两个同样优先级的 SCHED_FIFO 任务,它们会轮流执行,其他低优先级的只有等它们变为不可执行状态,才有机会执行。
根据设置的优先级来优先调用1000+rt_priority
SCHED_RR : 与 SCHED_FIFO 大致相同,只是 SCHED_RR 级的进程在耗尽其时间后,不能再执行,需要接受 CPU 的调度。当 SCHED_RR 耗尽时间后,同一优先级的其他实时进程被轮流调度。
根据优先级=counter+20-nice 来优先调用
QT信号槽原理
emit的时候,事件在当前发信号的线程执行和在绑定时候的线程执行两种情况:
如果connect的时候是directconnect,则相当于emit后执行回调函数
如果是queconnect等情况,相当于在connect的绑定线程中注入事件,emit后绑定线程才开始执行事件。
STL内存管理
当我们new一个对象时,实际做了两件事情:
(1)使用malloc申请了一块内存。
(2)执行构造函数。在SGI中.
这两步独立出了两个函数:allocate申请内存,construct调用构造函数.
STL分为两级配置器:
第一级配置器
如果要分配的区块大于128bytes,则移交给第一级配置器处理。以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。
第二级配置器
如果要分配的区块小于128bytes,则以 内存池管理(memory pool)。每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
STL失效
迭代器失效分三种情况考虑,也是分三种数据结构考虑,分别为数组型,链表型,树型数据结构。
数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);
链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).
树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
注意:经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*ite
智能指针
智能指针的行为类似常规指针,重要的区别是它负责自动释放所指的对象。C++11标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则"独占"所指向的对象。C++11标准库还定义了一个名为weak_ptr的辅助类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。智能指针是模板类而不是指针。类似vector,智能指针也是模板,当创建一个智能指针时,必须提供额外的信息即指针可以指向的类型。默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
智能指针实质就是重载了->和操作符的类,由类来实现对内存的管理,确保即使有异常产生,也可以通过智能指针类的析构函数完成内存的释放。
shared_ptr的类型转换不能使用一般的static_cast,这种方式进行的转换会导致转换后的指针无法再被shared_ptr对象正确的管理。应该使用专门用于shared_ptr类型转换的 static_pointer_cast() , const_pointer_cast() 和dynamic_pointer_cast()。
使用shared_ptr避免了手动使用delete来释放由new申请的资源,标准库也引入了make_shared函数来创建一个shared_ptr对象,使用shared_ptr和make_shared,你的代码里就可以使new和delete消失,同时又不必担心内存的泄露。shared_ptr是一个模板类。
C++开发处理内存泄漏最有效的办法就是使用智能指针,使用智能指针就不会担心内存泄露的问题了,因为智能指针可以自动删除分配的内存。
智能指针是指向动态分配(堆)对象指针,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数。每次使用它,内部的引用计数加1,每次析构一次,内部引用计数减1,减为0时,删除所指向的堆内存。
每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候, 内存才会被释放。
可以通过构造函数、赋值函数或者make_shared函数初始化智能指针。
shared_ptr基于”引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,会自动销毁其所指对象(通过delete操作符)。
shared_ptr的默认能力是管理动态内存,但支持自定义的Deleter以实现个性化的资源释放动作。
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。当要用make_shared时,必须指定想要创建的对象的类型,定义方式与模板类相同。在函数名之后跟一个尖括号,在其中给出类型。例如,调用make_shared时传递的参数必须与string的某个构造函数相匹配。如果不传递任何参数,对象就会进行值初始化。
通常用auto定义一个对象来保存make_shared的结果。
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其它shared_ptr指向相同的对象。
可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数析构函数(destructor)来完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
链表操作
#define LISTSIZE 10
typedef int LinkNodeType;
typedef struct LinkNode
{
LinkNodeType data;
struct LinkNode* next;
}LinkNode;
LinkNode *GetList()
{
LinkNode *pHead = new LinkNode;
LinkNode *p = pHead;
p->data = 0;
p->next = NULL;
for (int i = 0; i < LISTSIZE-1; i++)
{
LinkNode *pItem = new LinkNode;
pItem->next = NULL;
pItem->data = i+1;
p->next = pItem;
p = pItem;
}
return pHead;
}
void DiguiPrint(LinkNode *pHead)
{
if (NULL == pHead)
{
return;
}
else
{
DiguiPrint(pHead->next);
}
printf("%d ",pHead->data);
}
void PrintList(LinkNode *pHead)
{
while (pHead)
{
printf("%d ",pHead->data);
pHead = pHead->next;
}
printf("\n");
}
LinkNode * GetItemByIndex(LinkNode *pHead, int nIndex)
{
for (int i = 0; i < nIndex; i++)
{
pHead = pHead->next;
}
return pHead;
}
void InsertAfter( LinkNode *pItem, int nVal)
{
if ( !pItem) return;
LinkNode *p = new LinkNode;
p->next = NULL;
p->data = nVal;
int temp = pItem->data;
pItem->data = p->data;
p->data = temp;
p->next = pItem->next;
pItem->next = p;
}
LinkNode *YuesefuDel(LinkNode *p, int nCnt)
{
if (!p) return NULL;
if (p == p->next) return NULL;
while (p->next != p)
{
//get index nCnt-1
for (int i = 0; i < nCnt; i++)
{
p = p->next;
}
LinkNode *pDel = p->next;
p->next = pDel->next;
delete pDel;
}
return p;
}
LinkNode* Reverse(LinkNode *p1)
{
if (!p1) return NULL;
LinkNode *pPre = NULL, *pCur = p1;
while (pCur->next)
{
LinkNode *pTemp = pCur->next;
pCur->next = pPre;
pPre = pCur;
pCur = pTemp;
}
return pPre;
}
void Swap(LinkNode *p1, LinkNode *p2)
{
LinkNode *pTemp = p2;
p2 = p1;
p1 = pTemp;
}
LinkNode *Maopao(LinkNode *pHead )
{
if (!pHead) return NULL;
LinkNode *pPre = NULL, *pCur = pHead, *pTail = NULL;
//趟数
while (pCur->next)
{
LinkNode *p = pHead;
while (p->next&&p->next!=pTail)
{
if (p->data > p->next->data)
{
int t = p->data;
p->data = p->next->data;
p->next->data = t;
}
pTail = p;
p = p->next;
}
pCur = pCur->next;
}
return pHead;
}
LinkNode *Merge(LinkNode *p1, LinkNode *p2)
{
if (!p1) return p2;
if (!p2) return p1;
LinkNode *pNew = NULL,*pCur=NULL;
while (p1&&p2)
{
if (p1->data < p2->data)
{
if (!pNew) pNew = pCur=p1;
else
{
pCur->next = p1;
pCur = p1;
}
p1 = p1->next;
}
else
{
if (!pNew) pNew = pCur=p2;
else
{
pCur->next = p2;
pCur = p2;
}
p2 = p2->next;
}
}
if (p1) pCur->next = p1;
if (p2) pCur->next = p2;
return pNew;
}
LinkNode *FindMiddle(LinkNode *pHead)
{
LinkNode *pFast = pHead, *pLow = pHead;
while (pFast&&pFast->next)
{
pFast = pFast->next->next;
pLow = pLow->next;
}
return pLow;
}
LinkNode *FindK(LinkNode *pHead,int k)
{
LinkNode *pK = pHead, *pFast = pHead;
if (!pHead) return NULL;
for (int i = 0; i < k; i++)
{
if (!pFast) return NULL;
pFast = pFast->next;
}
while (pFast)
{
pFast = pFast->next;
pK = pK->next;
}
return pK;
}
bool CheckCycle(LinkNode *p)
{
if (!p) return false;
LinkNode *p1 = p, *p2 = p;
while (p->next)
{
p1 = p1->next->next;
p2 = p->next;
if (p1 == p2)
{
return true;
}
}
return false;
}
bool CheckJiao(LinkNode *p1, LinkNode *p2)
{
return true;
}
int main()
{
LinkNode * p = GetList();
//1.递归逆序打印单链表
DiguiPrint(p);
//2.不许遍历链表在Pos之前插入一个元素:传值操作 不修改
LinkNode *pNode = GetItemByIndex(p, 4);
LinkNode n1;
n1.data = 99;
n1.next = NULL;
InsertAfter(pNode, 100);
printf("\n");
PrintList(p);
//3.约瑟夫环的实现
//先构建环
pNode = GetItemByIndex(p, LISTSIZE);
pNode->next = p;
pNode = YuesefuDel(p, 5);
//4.单链表的逆序
LinkNode *p1 = GetList();
p1 = Reverse(p1);
//5.冒泡排序
p1 = Maopao(p1);
//6.合并链表
LinkNode *pM1 = GetList();
LinkNode *pM2 = GetList();
LinkNode *pMerge = Merge(pM1, pM2);
//7.查找中间节点-快慢指针
LinkNode *pF1 = GetList();
LinkNode *pMiddle = FindMiddle(pF1);
//8.找倒数第K个结点快慢指针多走K步
LinkNode *pKHead = GetList();
LinkNode *pK = FindK(pKHead, 8);
//判断是否有环
LinkNode *pCycle = GetList();
//bool bCycle = CheckCycle(pCycle);
//10.判断两个链表是否相交
LinkNode *pJ1 = GetList();
LinkNode *pJ2 = GetList();
bool bJiao = CheckJiao(pJ1, pJ2);
return 0;
}
查找算法
//11.二分查找
int HalfSearch(int arr[], int nSize, int nVal)
{
int l = 0, r = nSize;
while (l < r)
{
int nMid = l + (r - l) / 2;
if ( arr[nMid] == nVal ) return nMid;
else if (arr[nMid] < nVal) l = nMid+1;
else r = nMid;
}
return -1;
}
int arr[1000] = { 0 };
for (int i = 0; i < 1000; i++)
{
arr[i] = i;
}
int nIndex = HalfSearch(arr,1000,700 );
虚拟地址和物理地址的转换
QT信号槽原理
qt信号连接分为:Direct、queue连接 这两大核心思想。
编写对象的时候:定义宏Q_OBJECT,
#define Q_OBJECT \
public: \
Q_OBJECT_CHECK \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_WARNING_POP \
QT_TR_FUNCTIONS \
private: \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
struct QPrivateSignal {};
connection:将receiver加入到sender的map<receiver>中。connection所在线程如果有exec()函数,则表明进入了消息循环,后续的queue相关的操作才能实现(普通的线程没法完成此操作)
direct连接:在emit发送信号的时候,直接执行回调。
emit-->执行moc_x文件中的信号函数-->QMedaObject::Active(this,&staticMetaObject,..->遍历所有的槽函数-->执行receiver的槽函数。
queue连接:在emit发送信号的时候,将消息推送给connect时候所在的线程。构造一个消息,发送给所在线程QApplication:
emit-->执行moc_x文件中的信号函数-->QMedaObject::Active(this,&staticMetaObject,..->QCoreApplicationPrivate::SendPostEvents-->....->QObject::event->QPrivate::QSlotObject-->receiver::slot
总结:信号槽是通过发送者线程直接调用和消息推送两种方式实现,消息推送的前提是消息绑定所在线程有消息循环。
Epoll/select
水平触发:读-当数据有的时候一直触发
写-有数据未发送的时候一直触发
边缘触发:读-当新数据到来的时候触发
写有数据被发送走的时候触发
原理:红黑树存放socket
rdlist存放就绪列表
回调
优点: 1.速度快 就序列表
2.遍历方式-select是全遍历 epoll是就绪遍历
3.select最大数量小于epoll
4.边缘触发效率高 减少无效调用
5.select采用轮询 epoll采用回调 损耗小
lamda多种捕获方式
operator new
swap方法
linux查看内存状态:cat /proc/meminfo 查看系统状态 vmstat
协程的实现原理
c++11
pthread_join和detach
陷入内核的过程