什么时候用堆区?什么时候用栈区?

堆和栈的主要区别由如下几点:
一、管理方式不一样;
二、空间大小不一样;
三、可否产生碎片不一样;
四、生长方向不一样;
五、分配方式不一样;
六、分配效率不一样;
(1)管理方式操作系统

对于栈来说,是由编译器自动管理,无需咱们手工控制;对于堆来讲,释放工做由程序员控制,容易产生内存泄露。
(2)空间大小:

通常来说在32位系统下,堆内存能够达到4G的空间,从这个角度来看 堆内存几乎是没有什么限制的。可是对于栈来说,通常都是有必定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。固然,这个值能够修改。
(3)碎片问题

对于堆来说,频繁的new/delete势必会形成内存空间的不连续,从而形成大量的碎片,使程序效率下降。对于栈来说,则不会存在这个问题,由于栈是先进后出的队列,他们是如此的一一对应,以致于永远都不可能有一个内存块从栈中间弹出,在他弹出以前,在他上面的后进的栈内容已经被弹出,详细的能够参考数据结构。
(4)生长方向

对于堆来说,生长方向是向上的,也就是向着内存地址增长的方向;对于栈来说,它的生长方向是向下的,是向着内存地址减少的方向增加。
(5)分配方式

堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的, 好比局部变量的分配。动态分配由malloc函数进行分配,可是栈的动态分配和堆是不一样的,他的动态分配是由编译器进行释放,无需咱们手工实现。
(6)分配效率

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照必定的算法(具体的算法能够参考数据结构/操做系统)在堆内存中搜索可用的足够大小的空间,若是没有足够大小的空间(多是因为内存碎片太多),就有可能调用系统功能去增长程序数据段的内存空间,这样就有机会分到足够大小的内存,而后进行返回。显然,堆的效率比栈要低得多。

(1)与堆相比,栈不会致使内存碎片,分配效率高。

因此栈在程序中是应用最普遍的,就算是函数的调用也利用栈去完成,函数调用过程当中的参数,返回地址, EBP和局部变量都采用栈的方式存放。若是少许数据须要频繁的操做,那么在程序中动态申请少许栈内存(例如使用alloca函数),会得到很好的性能提高。

(2)堆能够申请的内存大不少。与堆相比,栈的使用不是那么灵活,若是分配大量的内存空间,推荐使用堆内存。

堆区、栈区的内存限制?

我们可以在终端用ulimit -a 察看stack内存的限制,得到结果为8MB(大部分限制,实际操作中不能申请到这么大的内存)

用malloc是在堆上申请内存,申请的内存可能不是连续的,所以可以申请很大内存。但stack申请的内存是连续的,所以一次不能申请太多。

struct和class的区别?

  1. 默认的继承访问权。class默认的是private,strcut默认的是public
  2. 默认访问权限:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
  3. class”这个关键字用于定义模板参数,就像“typename”。但关建字“struct”不用于定义模板参数
  4. class和struct在使用大括号{ }上的区别
    关于使用大括号初始化
    1.)class和struct如果定义了构造函数的话,都不能用大括号进行初始化
    2.)如果没有定义构造函数,struct可以用大括号初始化
    3.)如果没有定义构造函数,且所有成员变量全是public的话,class可以用大括号初始化

重载搜索匹配优先级是什么样的?

重载函数的调用匹配,依次按照下列规则来判断:

  • 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;

  • 提升匹配:即整数提升(如bool到int、char到int、short到int),float到double;

  • 使用标准转换匹配:如int到double、double到int、double到long double、Derived到Base、T到void、int到unsigned int;

  • 使用用户自定义匹配;

  • 使用省略号匹配:类似于printf中省略号参数。

虚函数虚表指针的内存分配是什么样的?

虚函数指针和虚函数表的创建时机:

​ 对于虚函数表来说,在编译的过程中编译器就为含有虚函数的类创建了虚函数表,并且编译器会在构造函数中插入一段代码,这段代码用来给虚函数指针赋值。因此虚函数表是在编译的过程中创建

​ 对于虚函数指针来说,由于虚函数指针是基于对象的,所以对象在实例化的时候,虚函数指针就会创建,所以是在运行时创建。由于在实例化对象的时候会调用到构造函数,所以就会执行虚函数指针的赋值代码,从而将虚函数表的地址赋值给虚函数指针。

左值右值对拷贝构造函数和移动构造函数的影响?

拷贝构造函数

拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据。

移动构造函数

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。

我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

读者可能会问,如果使用左值初始化同类对象,但也想调用移动构造函数完成,有没有办法可以实现呢?

默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

拷贝构造函数和移动构造函数 - 简书 (jianshu.com)

C++11移动构造函数详解 (biancheng.net)

实现一个智能指针?

面试题:简单实现一个shared_ptr智能指针

简单的说,注意以下方面:

  • 重载操作符=,->,*(注意是在=的时候引用计数增加);
  • 写好得到应用计数和最后释放的函数;
  • 要做好shared_ptr的线程安全问题:
    • 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
    • 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

怎么避免内存泄露?

  • 尽量避免在堆上分配内存
  • 使用 Arena
  • 使用 Coroutine
  • 善用 RAII
  • 代码要便于 Debug

C++ 如何避免内存泄漏

写的程序CPU占用率比较高,如何分析?

C/C++ 性能优化背后的方法论:TMAM

  • 代码尽可能减少代码的footprint
  • 分支预测优化
  • 合理使用缓存行对齐(define CACHE_LINE __attribute__((aligned(64)))
  • 避免间接跳转或者调用

Linux内核空间和用户空间?

以32位为例:

对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G。

针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。

每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。

如何从用户空间进入内核空间

其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。

内核空间和用户空间切换的性能开销大?

当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去的系统调用号,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当执行int中断执行时就会由用户态,栈转向内核栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。

系统调用一般都需要保存用户程序得上下文(context), 在进入内核得时候需要保存用户态的寄存器,在内核态返回用户态得时候会恢复这些寄存器得内容。这是一个开销的地方。 如果需要在不同用户程序间切换的话,那么还要更新cr3寄存器,这样会更换每个程序的虚拟内存到物理内存映射表的地址,也是一个比较高负担的操作。

如何降低内核空间和用户空间切换的开销:

  • 设立I/O 缓冲区
  • 零拷贝 (Zero-copy):零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。
    • 减少甚至避免用户空间和内核空间之间的数据拷贝
    • 绕过内核的直接 I/O

select,poll, epoll

linux poll epoll的区别?

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll:

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

select、poll、epoll 区别总结:

1、支持一个进程所能打开的最大连接数

select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、FD剧增后带来的IO效率问题

select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll:同上

epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

select:内核需要将消息传递到用户空间,都需要内核拷贝动作

poll:同上

epoll:epoll通过内核和用户空间共享一块内存来实现的。

select、poll、epoll之间的区别

介绍你所知道的锁?

为了保证数据并发访问时的一致性和有效性,任何一个数据库都存在锁机制。锁机制是为了解决数据库的并发控制问题而产生的。

按锁级别分类,可分为共享锁排他锁意向锁。也可以按锁粒度分类,可分为行级锁、表级锁和页级锁。下面我们先介绍共享锁、排他锁和意向锁。

1. 共享锁

共享锁的代号是 S,是 Share 的缩写,也可称为读锁。是一种可以查看但无法修改和删除的数据锁。

共享锁的锁粒度是行或者元组(多个行)。一个事务获取了共享锁之后,可以对锁定范围内的数据执行读操作。会阻止其它事务获得相同数据集的排他锁。

2. 排他锁

排他锁的代号是 X,是 eXclusive 的缩写,也可称为写锁,是基本的锁类型。

排他锁的粒度与共享锁相同,也是行或者元组。一个事务获取了排他锁之后,可以对锁定范围内的数据执行写操作。允许获得排他锁的事务更新数据,阻止其它事务取得相同数据集的共享锁和排他锁。

如有两个事务 A 和 B,如果事务 A 获取了一个元组的共享锁,事务 B 还可以立即获取这个元组的共享锁,但不能立即获取这个元组的排他锁,必须等到事务 A 释放共享锁之后才可以。

如果事务 A 获取了一个元组的排他锁,事务 B 不能立即获取这个元组的共享锁,也不能立即获取这个元组的排他锁,必须等到 A 释放排他锁之后才可以。

3. 意向锁

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁。

意向锁是一种表锁,锁定的粒度是整张表,分为意向共享锁(IS)意向排他锁(IX)两类。

意向共享锁表示一个事务有意对数据上共享锁或者排他锁。“有意”表示事务想执行操作但还没有真正执行。

锁和锁之间的关系,要么是相容的,要么是互斥的。

  • 锁 a 和锁 b 相容是指:操作同样一组数据时,如果事务 t1 获取了锁 a,另一个事务 t2 还可以获取锁 b;
  • 锁 a 和锁 b 互斥是指:操作同样一组数据时,如果事务 t1 获取了锁 a,另一个事务 t2 在 t1 释放锁 a 之前无法释放锁 b。

锁模式的兼容情况

其中共享锁、排他锁、意向共享锁、意向排他锁相互之间的兼容/互斥关系如下表所示,其中 Y 表示相容,N 表示互斥。

参数 X S IX IS
X(排他锁) N N N N
S(共享锁) N Y N Y
IX(意向排他锁) N N Y Y
IS(意向共享锁) N Y Y Y

如果一个事务请求的锁模式与当前的锁兼容,InnoDB 就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。

为了尽可能提高数据库的并发量,需每次锁定的数据范围越小越好,越小的锁其耗费的系统资源越多,系统性能下降。为在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度”的概念。

MySQL锁机制

乐观锁、悲观锁?

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

乐观锁的实现方式主要有两种:CAS机制版本号机制,下面详细介绍。

CAS(Compare And Swap)

CAS操作包括了3个操作数:

  • 需要读写的内存位置(V)
  • 进行比较的预期值(A)
  • 拟写入的新值(B)

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

版本号机制

版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。

ssh什么原理?

加密的方式主要有两种:

  1. 对称加密(也称为秘钥加密)
  2. 非对称加密(也称公钥加密)

所谓对称加密,指加密解密使用同一套秘钥(加密 :A->oxaa,解密:oxaa->A)。

对称加密的加密强度高,很难破解。但是在实际应用过程中不得不面临一个棘手的问题:如何安全的保存密钥呢?为了解决这个问题,非对称加密应运而生。

非对称加密流程:

  1. 远程Server收到Client端用户TopGun的登录请求,Server把自己的公钥发给用户。
  2. Client使用这个公钥,将密码进行加密。
  3. Client将加密的密码发送给Server端。
  4. 远程Server用自己的私钥,解密登录密码,然后验证其合法性。
  5. 若验证结果,给Client相应的响应。
基于公钥认证:
  1. Client将自己的公钥存放在Server上,追加在文件authorized_keys中。
  2. Server端接收到Client的连接请求后,会在authorized_keys中匹配到Client的公钥pubKey,并生成随机数R,用Client的公钥对该随机数进行加密得到pubKey(R)
    ,然后将加密后信息发送给Client。
  3. Client端通过私钥进行解密得到随机数R,然后对随机数R和本次会话的SessionKey利用MD5生成摘要Digest1,发送给Server端。
  4. Server端会也会对R和SessionKey利用同样摘要算法生成Digest2。
  5. Server端会最后比较Digest1和Digest2是否相同,完成认证过程。

RSA非对称加密

  1. id_rsa:保存私钥
  2. id_rsa.pub:保存公钥
  3. authorized_keys:保存已授权的客户端公钥
  4. known_hosts:保存已认证的远程主机ID

图解SSH原理

cookies:由服务器生成,在客户端以key-value形式保存用户信息

  • 组成:主要是由Name(名字,相当于key) + Value(值,即当前用户信息) + Domian(域名) + Path(路径) + Expires/Max-Age(过期时间) + Size(大小)
  • 使用:用于响应头和请求头中:由服务器在响应头中设置,客户端保存,并在发送请求时请求头中带上cookie
  • 有效期:如果有设置过期时间,那么只要时间还没过期,即使关闭浏览器cookies也还会存在,反之,会在浏览器关闭时消失

优缺点:优点是可以保存客户相关信息和状态,这对于无状态的http请求来说是很重要的(但也不是不可或缺,cookie通过http请求报文head部分中的,而在http请求报文中,数据除了可以通过head传递,也可以通过url或请求体传递)因为由客户端保存,可以被人修改,而且在传递过程中容易被人拦截(一些重要信息需要通过加密传输,而用session则可以把用户相关信息和状态保存在服务器,所以能避免信息外泄的问题),具有安全隐患;且在某些浏览器上能保存的cookies数量大小限制;还有就是不支持跨域访问(Token可解决这个问题)。

session:在服务端生成,以key-value形式保存用户信息

  • 组成:session保存在服务器内存中,维持一个hash表保存用户相关信息(也是key-value形式)
  • 使用:一个用户对应一个session,每个session都有它独一无二的sessionid,sessionid随响应set-cookie保存到客户端的cookies中。客户端发送请求时带上cookies,服务端从cookies中拿到sessioid,然后根据sessionid从内存中找到对应用户的session获取相关用户信息
  • 有效期:session默认30分钟超时,即如果在30分钟内session没有被访问过,那么就失效了。
  • 优缺点:能够解决cookies的安全隐患,但因为保存在服务器内存中,当同时访问的用户很多时内存占用争夺,性能会受到影响

token:访问令牌–> 一个服务端生成的独一无二的字符串

  • 组成:登录时由服务端生成,一般组成形式:uuid(用户唯一身份标志)+time(时间戳)+sign(签名=uuid+time+salt根据hash算法生成的字符串)+[常用的固定参数(可选)]
  • 使用:服务端生成后随http响应保存在客户端的cookies或local storage中,随客户端请求发送至服务端,用于单点登录的身份验证,防止跨站点请求伪造等
  • 有效期:根据token中的时间戳跟当前时间对比计算,看过期与否,有效期默认7天,用户退出时直接销毁token(???)
  • 优缺点:支持跨域访问,防止信息外泄,可以在多个服务间共享。且不像session存储于服务器内存中,不影响服务器的性能,但是需要额外的时间开销(cpu需要每次去校验传过来的token是否有效(保存在服务器内存中性能是不是会更好,还是说有多种实现方案,自己衡量???))


实习      C++ 面经

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!