招银网络科技
C++的内存管理
答:在C++中,内存分为:栈、堆、自由存储区、全局/静态存储区、常量存储区。
new/delete与malloc/free的区别
- new是从自由存储区上分配内存,malloc从堆上分配(自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。)
- new内存分配成功后,返回的是对象类型的指针,故new是符合类型安全性的操作符;malloc返回的是void *,需要强制类型转换将void * 转换为需要的类型。
- new内存分配失败,会有bac_alloc异常,不会返回null;malloc会返回null。
- new不需要指定内存大小,malloc需要显式指定内存大小。
- new会调用构造/析构函数,而malloc不会
- operator new/delete的实现可以基于malloc,而malloc不可以调用new.
- new/delete可以重载,malloc/free不可以重载
- malloc可以通过realloc函数直观进行内存扩充,而new没有配套设施来扩充。
进程和线程的通信方式有哪些
(1)进程:
- 管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 - 有名管道 (namedpipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 - 信号量(semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 - 消息队列( messagequeue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 - 信号 (sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 - 共享内存(shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。 - 套接字(socket ) :
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
(2)线程:
- 锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 - 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
- 信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
C++的强制类型转换
static_cast<new_type> (expression) dynamic_cast<new_type> (expression) const_cast<new_type> (expression) reinterpret_cast<new_type> (expression)
static_cast
该运算符把expression转换为new_type类型,用来强迫隐式转换,例如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。double *c = new double; void *d = static_cast<void*>(c);//正确,将double指针转换成void指针
dynamic_cast
主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(cross cast)。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。而且是唯一一个在运行阶段进行转换的函数。void f(const Base &b){ try{ const Derived &d = dynamic_cast<const Base &>(b); //使用b引用的Derived对象 } catch(std::bad_cast){ //处理类型转换失败的情况 } }
const_cast
该运算符用来修改类型的const(唯一有此能力的C++-style转型操作符)或volatile属性。除了const 或volatile修饰之外, new_type和expression的类型是一样的。const int g = 20; int *h = const_cast<int*>(&g);//去掉const常量const属性 const int g = 20; int &h = const_cast<int &>(g);//去掉const引用const属性 const char *g = "hello"; char *h = const_cast<char *>(g);//去掉const指针const属性
reinterpret_cast
new_type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针.// expre_reinterpret_cast_Operator.cpp // compile with: /EHsc #include <iostream> // Returns a hash code based on an address unsigned short Hash( void *p ) { unsigned int val = reinterpret_cast<unsigned int>( p ); return ( unsigned short )( val ^ (val >> 16)); } using namespace std; int main() { int a[20]; for ( int i = 0; i < 20; i++ ) cout << Hash( a + i ) << endl; }
智能指针
定义*
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。unique_ptr
(替换auto_ptr)unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。class MyClass { private: // MyClass owns the unique_ptr. unique_ptr<ClassFactory> factory; public: // Initialize by using make_unique with ClassFactory default constructor. MyClass() : factory ( make_unique<ClassFactory>()) { } void MakeClass() { factory->DoSomething(); } };
shared_ptr
实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。成员函数:
use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针)
int main() { string *s1 = new string("s1"); shared_ptr<string> ps1(s1); shared_ptr<string> ps2; ps2 = ps1; cout << ps1.use_count()<<endl; //2 cout<<ps2.use_count()<<endl; //2 cout << ps1.unique()<<endl; //0 string *s3 = new string("s3"); shared_ptr<string> ps3(s3); cout << (ps1.get()) << endl; //033AEB48 cout << ps3.get() << endl; //033B2C50 swap(ps1, ps3); //交换所拥有的对象 cout << (ps1.get())<<endl; //033B2C50 cout << ps3.get() << endl; //033AEB48 cout << ps1.use_count()<<endl; //1 cout << ps2.use_count() << endl; //2 ps2 = ps1; cout << ps1.use_count()<<endl; //2 cout << ps2.use_count() << endl; //2 ps1.reset(); //放弃ps1的拥有权,引用计数的减少 cout << ps1.use_count()<<endl; //0 cout << ps2.use_count()<<endl; //1 }
- weak_ptr
share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
单线程并发
并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。
并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生。
多线程锁
线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。
- 互斥锁
互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。
在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。 - 条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。 - 自旋锁
自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。 - 读写锁
我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。 - 递归锁
就是在同一线程上该锁是可重入的(同一个线程可以多次获取同一个递归锁,不会产生死锁。),对于不同线程则相当于普通的互斥锁。
多线程的资源分配
使用锁机制来避免资源竞争。
如何进行负载均衡
负载均衡(Load Balance)是集群技术(Cluster)的一种应用。负载均衡可以将工作任务分摊到多个处理单元,从而提高并发处理能力。目前最常见的负载均衡应用是Web负载均衡。根据实现的原理不同,常见的web负载均衡技术包括:DNS轮询、IP负载均衡和CDN。
- DNS轮询
DNS轮询是最简单的负载均衡方式。以域名作为访问入口,通过配置多条DNS A记录使得请求可以分配到不同的服务器。 - CDN
CDN(Content Delivery Network,内容分发网络)。通过发布机制将内容同步到大量的缓存节点,并在DNS服务器上进行扩展,找到其中用户最近的缓存节点作为服务提供节点。 - IP负载均衡
IP负载均衡可以使用硬件设备,也可以使用软件实现。硬件负载均衡设备可以将核心部分做成芯片,性能和稳定性更好,而且商用产品的可管理性、文档和服务都比较好。唯一的问题就是价格。软件负载均衡通常是开源软件。自由度较高,但学习成本和管理成本会比较大。
怎样使数据同步
数据同步是多线程编程中不可避免的话题。之所以需要线程同步,是因为多个线程同时对一个数据对象进行修改操作时,可能会对数据造成破坏。方法主要有四种:
- 使用标准库的thread、mutex,即通过互斥量来进行同步
- 使用windows API的临界区对象
- 使用windows API的事件对象
- 使用windows API的互斥对象
死锁的四个必要条件
定义:多个并发进程因争夺系统资源而产生相互等待的现象。
必要条件
- 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
- 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
- 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
- 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
如何解决死锁
产生死锁需要四个条件,破坏其中的至少一个条件,则不会发生死锁,但,由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保护,所以主要破坏其他三个条件:
- 破坏“占有且等待”条件
允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。 - 破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。 - 破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防。
线程池的概念
因为程序边运行边创建线程是比较耗时的,所以我们通过池化的思想:在程序开始运行前创建多个线程,这样,程序在运行时,只需要从线程池中拿来用就可以了.大大提高了程序运行效率。
网站常用的协议
- http协议
超文本传输协议,这是互联网应用最广泛的一种网络协议,客户端和服务器端请求和答应的标准。客户端正常情况是指终端用户,而服务器端是指网站。终端用户通过浏览器、蜘蛛等向服务器指定端口发送http请求,这能看到是否成功、服务器类型、页面最近更新时间等内容。 - https协议
这是加强版http协议,是一种更加安全的数据传输协议。 - UA属性
就是用户代理,是http协议中的一个属性,代表终端的身份,像服务器表明身份,然后服务器可以根据不同身份做出不同的反馈结果。 - robots协议
搜索引擎访问网站的第一个文件夹就是robots.txt,这样可以确定网站哪些页面是可以抓取的,哪些页面是不能抓取的。robots.txt必须放在网站根目录下,而且文件名称要小写。
http与https的区别
HTTP协议传输的数据都是未加密的,也就是明文的,简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
加密在哪一层
理论上每一层都可以加密,在哪一层加密需要确定攻击来自哪里:链路层加密只有在广播域内有效,物理层加密主要防止对方窃取无线电波,ip加密作用到主机,传输层加密作用到进程,应用层加密作用到应用。
一般的加密解密都是在网络层、运输层、应用层。
数据索引,作用与结构
数据索引作用是为了快速查找数据
- B树索引
通过B树的多节点叶子进行查找 - Hash索引
主要用于等值索引,只需要一次哈希算法可以定位到形影的位置,速度快。 - 主键与聚集索引
主键是一种约束,主要用来保证数据的完整性,而聚集索引是一种文件(数据记录)的组织形式,索引的目的是查询优化。在SQL SERVER默认是在主键上建立聚集索引的。创建主键,不可以在允许为Null值的列上创建,并且既有的数据记录中不可以有重复值,否则报错。聚集索引没有限制建立聚集索引的列一定必须 not null ,并且数据即可以唯一,也可以不唯一。 - 聚集索引与非聚集索引
聚集索引叶子层:具体的数据,按照聚集键顺序存储
非聚集索引叶子层:指针,指针有2类数据:RID(堆表)或者是聚集键。
B+树的原理
B+树是B树的一种变形,它把数据都存储在叶子节点,内部只存关键字(其中叶子节点的最小值作为索引)和孩子指针,简化了内部节点;B+树的遍历高效,将所以叶子节点串联成链表即可从头到尾遍历。
map和set的区别
map存储的是键-值,而set存储的只有值。
介绍一下红黑树
是一种自特殊的平衡二叉查找树,复杂度是O(logn),用红黑颜色来标记数据,便于查找与删除。
堆排序的原理
堆排序是一种选择排序,利用堆的性质,复杂度为O(nlogn),是不稳定排序。基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
类和结构体的区别
- 结构体(sturct)是一种值类型,而类(class)是引用类型。区别在于复制方式,值类型的数据是值复制,引用类型的数据是引用复制。
- 结构体使用栈存储(Stack Allocation),而类使用堆存储(Heap Allocation)。
- 结构体使用完之后就自动解除内存分配,类实例有垃圾回收机制来保证内存的回收处理。
redis底层原理
redis,远程字典服务,是一个KEY-VALUE存储系统,字典的底层是通过哈希表来实现的。
内存泄漏,如何判断,如何处理
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
可以使用vs调试器和CRT库来检测,原理大致为:内存分配要通过CRT在运行中实现,只要在分配内存和释放内存时做好记录,结束时对比分配内存和释放内存的记录就可以判断。
可以通过一些工具来处理内存泄漏的问题,比如valgrind、pmap+gbd等。