1、static
全局静态变量、局部静态变量、静态函数、类的静态成员、类的静态函数
-----------c++其他修饰符:const、volatile(修饰符 volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量。对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率。)、restrict(由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。)
https://www.runoob.com/cplusplus/cpp-modifier-types.html
2、内存管理(内存分配)
堆、栈、自由存储区、全局/静态存储区、常量存储区
例子:
int a=0; //全局初始化区
char *p1; //全局未初始化区
main() {
int b;//栈
char s[]="abc"; //栈
char *p2; //栈
char *p3="123456"; //123456 \0在常量区,p3在栈上。
static int c=0; //全局(静态)初始化区
p1 = (char*)malloc(10);
p2 = (char*)malloc(20); //分配得来得10和20字节的区域就在堆区。
strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。
}
3、线程池
传统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务。任务执行完毕后,线程退出,这就是是“即时创建,即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。
因此线程池的出现正是着眼于减少线程池本身带来的开销。线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N1个线程都在处理任务后,缓冲池自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。
线程池:https://www.cnblogs.com/cpper-kaixuan/p/3640485.html
线程池:https://www.cnblogs.com/yangang92/p/5485868.html
mutex简介:https://blog.csdn.net/WizardtoH/article/details/81452066
4、detch和join的区别
https://blog.csdn.net/liunan199481/article/details/83055901
5、condition_variable
线程的同步方式:信号量、互斥量、条件变量
notify_one():唤醒处于wait中的其中一个条件变量
wait():可以让线程陷入休眠状态---------(与sleep的区别)
https://www.jianshu.com/p/c1dfa1d40f53
6、Hash、HashTable、Hash_map、map
https://blog.csdn.net/lyq_csdn/article/details/80630872
7、TCP三次握手和四次挥手
为什么不是两次握手或者四次握手?窗口机制/快速重传?拥塞控制?慢启动、拥塞避免?与UDP区别?
8、string 与char* 和char[]区别及转化
- string----->const char * : .c_str()或者data()
- string-----> char * :copy()
- char * 或者char[] ----->string : 直接赋值
- string-----> char[] : 循环赋值
https://blog.csdn.net/bestcleaner/article/details/81516771
9、指针和引用的区别
指针 | 引用 | |
---|---|---|
空间 | 有自己的空间 | 只是一个别名 |
sizeof | 4 | 对象大小 |
初始化 | NULL/nullptr | 必须初始化为已有对象的引用 |
参数传递 | 解引用 | 直接对引用修改 |
const指针 | 有 | 没有 |
指向 | 可以指向其他 | 只能引用一个 |
级数 | 多级指针 | 一级引用 |
++ | 地址++(4字节) | 值++ |
动态内存分配 | 必须使用指针 | 可能内存泄露 |
10、智能指针
https://blog.csdn.net/zhou753099943/article/details/52412381
auto_ptr:管理权的转移 ->不建议使用
scoped_ptr ( unique_ptr ):防拷贝 ->简单粗暴
shared_ptr: 引用计数 ->增减引用计数,直到对象的引用计数为0时再释放
weak_ptr:辅助shared_ptr解决循环引用的问题(内存泄漏问题)。
原理:智能指针实现技术是使用引用计数。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。
- 每次创建类的新对象时,初始化指针就将引用计数置为1;
- 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;
- 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;
- 调用析构函数时,析构函数减少引用计数(如果引用计数减至0,则删除基础对象)。
11、虚函数
11.1什么是虚函数?
https://www.cnblogs.com/jianyungsun/p/6361670.html
实现多态所必须的,基类指针指向派生类的实例,执行的时候会执行派生类中的定义。(否则只会执行基类部分)
11.2析构函数可以是虚函数吗?
如果有派生类的话,析构函数必须是虚函数,否则会派生类无法析构引起内存泄露。
11.3多态的实现?
简而言之编译器根据虚函数表找到恰当的虚函数。对于一个基类对象指针类型变量,如果给他赋基类对象的指针,那么他就调用基类中的函数,如果给他赋派生类对象的指针,他就调用子类中的函数。函数执行之前查表。
11.4虚函数表是针对类还是针对对象的?
虚函数表是针对类的,一个类的所有对象的虚函数表都一样。
11.5纯虚函数和虚函数有什么区别?
纯虚函数就是定义了一个虚函数但并没有实现,原型后面加"=0"。包含纯虚函数的类都是抽象类,不能生成实例。
11.6构造函数可以是虚函数吗?
每个对象的虚函数表指针是在构造函数中初始化的,因为构造函数没执行完,所以虚函数表指针还没初始化好,构造函数的虚函数不起作用。 所以在构造函数中调用虚函数,和调用一般的成员函数一样。
11.7析构函数中可以调用虚函数吗?
析构函数中调用虚函数也不起作用,调用虚函数同调用一般的成员函数一样。析构函数的顺序是先派生类后基类,有可能内容已经被析构没了,所以虚函数不起作用。
11.8虚继承和虚基类?
虚继承是为了解决多重继承出现菱形继承时出现的问题(钻石问题)。例如:类B、C分别继承了类A。类D多重继承类B和C的时候,类A中的数据就会在类D中存在多份。通过声明继承关系的时候加上virtual关键字可以实现虚继承。
虚继承是一种机制,类通过虚继承指出它希望共享虚基类的状态。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。构造时先构造虚基类,再构造非虚基类。
12、fork函数
成功调用fork()函数会创建一个新的进程,子进程中成功调用返回0,父进程返回子进程的pid;如果出现负值,返回一个负值。
13、静态函数和虚函数的区别
静态函数在编译时就已经确定了运行时机,虚函数在运行时动态绑定。因为虚函数使用了虚函数表,调用会增加一次内存开销。
14、重写和重载的区别
重载:函数名相同,参数列表不同,在同一作用域;
重写:子类继承父类,子类重新定义了父类的虚函数;
15、IO多路复用:select、poll、epoll(都是同步I/O)
IO多路复用:通过一种机制,一个进程监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作
。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小;
select
:
调用后select函数会阻塞,直到有描述符就绪(或者超时),函数返回,返回后遍历fdset;优点:跨平台支持;
缺点:①单个进程监视的文件描述符数量存在上限;
②轮询的方式效率低;
③需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
poll:本质上和select没有区别,它将用户传入的数组拷贝到内核空间;轮询fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。
优点:没有最大连接数限制(以为是基于链表存储的)
缺点:①大量的fd的数组被整体复制于用户态和内核地址空间之间 ;
②
“水平触发”
------如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll:epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
优点:①没有最大连接数设置
②效率提升,不是轮询的方式
③epoll使用mmap减少复制开销
模式:LT(level trigger)和ET(edge trigger)
- LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序可以不立即处理该事件
。下次调用epoll_wait时,会再次响应应用程序并通知此事件。- ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序必须立即处理该事件
。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
IO复用:https://www.cnblogs.com/wt645631686/p/8528912.html
select、poll、epoll:https://www.cnblogs.com/jeakeven/p/5435916.html
16、同步、异步、阻塞、非阻塞概念:
- 同步:指进程调用接口时,需要等待接口处理完数据,进程才能继续执行。
- 异步:指进程调用接口后,不必等待数据准备完成,进程可以继续执行。后续数据准备好通过一定的方式获得,例如回调。服务器也必须支持异步操作,否则无法返回数据。
- 阻塞:指的是进程调用接口后,如果接口没有准备好数据,那么这个进程会被挂起,直到有数据返回时唤醒。
- 非阻塞:就是进程调用接口后,如果接口没有准备好数据,进程也能处理后续的操作。但是需要不断的去轮询检查数据是否已经处理完成。
同步异步针对的是,如果接收不到数据,当前代码逻辑的状态。
阻塞非阻塞针对的是,如果接收不到数据,当前进程的状态。
17、map、unordered_map和set、unordered_set的区别
map | unordered_map | set | unordered_set | |
---|---|---|---|---|
底层实现 | 红黑树 | 哈希表 | 红黑树 | 哈希表 |
存储结构 | K-V | K-V | K | K |
迭代器 | 可修改V | 不可修改 | ||
下标操作 | 支持(K作为下标) | 不支持 | ||
查找复杂度 | O(logN) | O(1) | O(logN) | O(1) |
哈希冲突 | 链地址法 | |||
遍历打印 | 顺序 | 逆序 | 顺序 | 逆序 |
- map使用[]将K值作为下标去执行查找,若K值不存在,则插入一个具有该K值和默认V值的元素至map中,对于不需要查找时插入的情况,应尽量使用find。
- 解决哈希冲突:开放地址法、链地址法、再哈希法、建立公共溢出区。
18、一致性哈希:常用于负载均衡
- 随机策略:无论是时间效率还是空间效率都非常不好。
- hash取模:容错性(服务器不可用)和扩展性(增加服务器)不好。
- 一致性哈希:服务节点的增减不会造成大量哈希重定位
一致性哈希:
①将整个哈希值空间组织成一个虚拟的圆环;
②下一步将各个服务器使用H进行一个哈希;
③将数据使用相同的H计算出哈希值h,确定此数据在环上的位置,顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。
④服务节点太少的时候,分布不均匀,采用虚拟节点技术;
https://www.cnblogs.com/moonandstar08/p/5405991.html
https://leileiluoluo.com/posts/consistent-hashing-and-high-available-cluster-proxy.html
19、STL迭代器
- 为什么有了指针还要有迭代器?
迭代器提供了一种方法顺序访问一个聚合对象的各个元素,而不需要暴露该对象的内部结构;
- 迭代器与指针的区别?
迭代器是类模板,表现地像指针。模拟了指针的一些功能,重载了->、*、++、--等。
- 迭代器如何删除元素?
对于顺序容器vector、deque来说,使用erase后,后边的每个迭代器都会失效,后边的每个元素都会往前移动一个位置,erase返回下一个有效的迭代器。
对于关联容器map、set来说,使用erase后,当前的迭代器失效,但其结构是红黑树,删除当前元素不会影响到下一个元素的迭代器,所以在调用erase之前记录下一个元素的迭代器即可。
对于list来说,它使用了不连续分配的内存,以上两种方法都可以。
20:类成员的访问权限
- public:可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问;
- protected:可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问;
- private:只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问;
水平控制权限:
类中属性 | public | protected | private |
---|---|---|---|
内部可见性 | √ | √ | √ |
外部可见性 | √ | × | × |
垂直控制权限:
基类属性 | public | protected | private | public | protected | private | public | protected | private |
---|---|---|---|---|---|---|---|---|---|
继承方式 | public | protected | private | ||||||
内部可见性 | public | protected | 不可见 | protected | protected | 不可见 | protected | protected | 不可见 |
外部可见性 | √ | × | × | × | × | × | × | × | × |
派生类对基类成员的访问形式主要有以下两种: ①内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。 ②外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。 |
https://blog.csdn.net/weixin_42018112/article/details/82427071
21、四种cast转换
static_cast dynamic_cast const_cast reinterpret_cast
static_cast:用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
dynamic_cast:用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
const_cast:用于将const变量转为非const
reinterpret_cast:几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
-----为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
https://www.cnblogs.com/ider/tag/C%2B%2B/
22、多态
- 静态多态(编译期绑定):通过模板和函数重载来实现;
- 动态多态(运行期绑定):通过继承、虚函数、指针来实现;
23、指针和const组合用法
对于const与指针组合在一起,可以将const理解为内容,*理解为指针,誰在前面谁不能改变,誰在前面先读谁
- const int *a与int const *a:const在前面所以内容不可以改变,但是指针指向可以改变。也就是常量指针 ;
- int *const a:表示的是指针指向不可改变,但是指针所存放的内容可以改变,也即是指针常量;
- const int &ri=i:定义的是引用是另一变量的别名,它本身就是一个常量,也就是说不能再让一个引用成为另一个变量的别名。 我们不能通过ri去改变它所代表的内存区域;
ps:
const作用:①用于定义常量 ②保护传参时参数不被修改 ③节约内存空间(只分配一次内存,define分配多次)④类中使用const修饰函数防止修改非static类成员变量⑤修饰指针⑥修饰函数返回值⑦修饰类的成员变量⑧限制作用域(本文件使用)
24、类String的构造函数、析构函数和赋值函数
//普通构造函数
String::String(const char *str) {
if(str==NULL) {
m_data = new char[1]; // 对空字符串自动申请存放结束标志'\0'的空
//对m_data加NULL 判断
*m_data = '\0';
} else {
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
//String的析构函数
String::~String(void) {
delete [] m_data; // 或delete m_data;
}
//拷贝构造函数
String::String(const String &other) { //输入参数为const型
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
//赋值函数
String & String::operator =(const String &other) { // 输入参数为const型
if(this == &other) //检查自赋值
return *this;
delete [] m_data;
//释放原有的内存资源
int length = strlen( other.m_data );
m_data = new char[length+1];
strcpy( m_data, other.m_data );
return *this; //返回本对象的引用
}
25、STL的allocaotr
new 一个对象的时候两个动作是:先调用::operator new 分配一个对象大小的内存,然后在这个内存上调用构造函数构造对象。同样,当你 delete 一个对象的时候两个动作是:先调用析构函数析构掉对象,再调用::operator delete将对象所处的内存释放。为了精密分工,STL 将allocator决定将这两个阶段分开。分别用 4 个函数来实现:
1.内存的配置:alloc::allocate();
2.对象的构造:::construct();
3.对象的析构:::destroy();
4.内存的释放:alloc::deallocate();
考虑到小型区块可能造成内存破碎问题(即形成内存碎片),SGI STL 设计了双层级配置器。第一层配置器直接使用malloc() 和 free().第二层配置器则视情况采用不同的策略:但配置区块超过 128 bytes时,调用第一级配置器。当配置区块小于 128 bytes时,采用复杂的 memory pool 方式。
SGI 第二层配置器定义了一个 free-lists,这个free-list是一个数组,数组的元素都是指针,用来指向16个链表的表头(16个链表上分别挂着大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块)。
SGI 第二层配置器两个重要函数:ROUND_UP():申请的内存字节数上调为8的倍数;refill():没有可用的free list时调用。
PS:malloc的原理
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
https://www.cnblogs.com/zhuwbox/p/3699977.html
26、内存泄漏
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的分类:
1. 堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
2. 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
3. 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
27、new 和 malloc的区别
1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
3、new不仅分配一段内存,而且会调用构造函数,malloc不会。
4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5、new是一个操作符可以重载,malloc是一个库函数。
6、malloc分配的内存不够的时候,可以用realloc扩容。new没用这样操作。
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
28、模板及可变参数模板
- 函数模板:针对仅参数类型不同的函数;
//例子
template<typename T> void swap(T& a,T& b){};
template<classT> T swap(T& a,T& b){};
- 类模板:仅数据成员和成员函数类型不同的类;
//例子
template<class T> class A {
public:
T a;
T b;
T hy(T c, T &d);
};
使用模板类创建对象:A<int> m
c++11新特征:可变参数模板
//例子 Template<class ... T> void func(T ... args) { cout<<"num is"<<sizeof ...(args)<<endl; }
通过递归来实现真正的可变长:
#include<iostream> using namespace std; double Sum2() { // 边界条件 return 0; } template<typename T1, typename... T2> double Sum2(T1 p,T2...arg) { double res = p + Sum2(arg...); return res; } int main() { cout<<Sum2(1.0,2.0,3.0,4.0)<<endl; cout<<Sum2()<<endl; return 0; }
29、右值引用
c++中引入了右值引用
和移动语义
,可以避免无谓的复制,提高程序性能。
- 如何区分左值和右值:看能不能对表达式取地址,如果能,则为左值,否则为右值。
- 右值引用使用的符号是
&&。
//例子:
int&& a = 1;
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a
的生命期一样,只要a
还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。
注意:这里a
的类型是右值引用类型(int &&
),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。
https://www.jianshu.com/p/d19fc8447eaa
30、lambda
#include <algorithm>
#include <cmath>
using namespace std;
//例子1
void abssort(float *x, unsigned N)
{
std::sort(x,
x + N,
[](float a, float b) { return abs(a) < abs(b); });
}
//例子2:自动推断返回类型
std::cout << [](float f) { return std::abs(f); } (-3.5);
//例子3:指定返回类型
std::cout << [](float f) -> int { return std::abs(f); } (-3.5);
//例子4:以传值的形式捕获同范围内的变量
float f0 = 1.0;
std::cout << [=](float f) { return f0 + std::abs(f); } (-3.5);
//例子5:以传引用的形式捕获同范围内的变量
std::cout << [&](float f) { return f0 + std::abs(f); } (-3.5);
//例子6:以混合的形式捕获同范围内的变量
float f0 = 1.0f;
float f1 = 10.0f;
std::cout << [=, &f0](float a) { return f0 += f1 + std::abs(a); } (-3.5);
std::cout << '\n' << f0 << '\n';
https://www.cnblogs.com/langzou/p/5962033.html