《面中率最高》
1.new、delete、malloc、free关系
delete会调用对象的析构函数,和new对应free只会释放内存,new调用构造函数。malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
2.delete与 delete []区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套
MemTest *mTest1=new MemTest[10];
MemTest *mTest2=new MemTest;
Int *pInt1=new int [10];
Int *pInt2=new int;
delete[]pInt1; //-1-
delete[]pInt2; //-2-
delete[]mTest1;//-3-
delete[]mTest2;//-4-
在-4-处报错。
这就说明:对于内建简单数据类型,delete和delete[]功能是相同的。对于自定义的复杂数据类型,delete和delete[]不能互用。delete[]删除一个数组,delete删除一个指针。简单来说,用new分配的内存用delete删除;用new[]分配的内存用delete[]删除。delete[]会调用数组元素的析构函数。内部数据类型没有析构函数,所以问题不大。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。
3.C++有哪些性质(面向对象特点)
封装,继承和多态。
4.子类析构时要调用父类的析构函数吗?
析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。
5.多态,虚函数,纯虚函数
多态:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;
在程序编译时多态性体现在函数和运算符的重载上;
虚函数:在基类中冠以关键字 virtual 的成员函数。 它提供了一种接口界面。允许在派生类中对基类的虚函数重新定义。
纯虚函数的作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。作为接口而存在 纯虚函数不具备函数的功能,一般不能直接被调用。
从基类继承来的纯虚函数,在派生类中仍是虚函数。如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)。
抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类必须用作派生其他类的基类,而不能用于直接创建对象实例。但仍可使用指向抽象类的指针支持运行时多态性。
6.求下面函数的返回值(微软)
int func(x)
{
int countx = 0;
while(x)
{
countx ++;
x = x&(x-1);
}
return countx;
}
假定x = 9999。 答案:8
思路:将x转化为2进制,看含有的1的个数。
7.什么是“引用”?申明和使用“引用”要注意哪些问题?
答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
8.将“引用”作为函数参数有哪些特点?
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用”*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
9.在什么时候需要使用“常引用”?
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;
例1
int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确
例2
string foo( );
void bar(string & s);
那么下面的表达式将是非法的:
bar(foo( ));
bar(“hello world”);
原因在于foo( )和”hello world”串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。引用型参数应该在能被定义为const的情况下,尽量定义为const 。
10.将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }
好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!
注意事项:
(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了”无所指”的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)流操作符重载返回值申明为“引用”的作用:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << “hello” << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。
赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
#include<iostream.h>
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20;
cout<<vals[0];
cout<<vals[9];
}
int &put(int n)
{
if (n>=0 && n<=9 ) return vals[n];
else { cout<<“subscript error”; return error; }
}
(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。
11、结构与联合有和区别?
(1). 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。
12、试写出程序结果:
int a=4;
int &f(int x)
{ a=a+x;
return a;
}
int main(void)
{ int t=5;
cout<<f(t)<<endl; a = 9
f(t)=20; a = 20
cout<<f(t)<<endl; t = 5,a = 20 a = 25
t=f(t); a = 30 t = 30
cout<<f(t)<<endl; } t = 60
}
13.重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?
常考的题目。从定义上来说:
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重写:是指子类重新定义父类虚函数的方法。
从实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。
14.有哪几种情况只能用intialization list 而不能用assignment?
答案:当类中含有const、reference 成员变量;基类的构造函数都需要初始化表。
15. C++是不是类型安全的?
答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
16. main 函数执行以前,还会执行什么代码?
答案:全局对象的构造函数会在main 函数之前执行。
17. 描述内存分配方式以及它们的区别?
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
18.分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。
答案:
BOOL : if ( !a ) or if(a)
int : if ( a == 0)
float : const EXPRESSION EXP = 0.000001
if ( a < EXP && a >-EXP)
pointer : if ( a != NULL) or if(a == NULL)
19.请说出const与#define 相比,有何优点?
答案:
const作用:定义常量、修饰函数参数、修饰函数返回值三个作用。被Const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
20.简述数组与指针的区别?
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(1)修改内容上的差别
char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
(2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
char a[] = “hello world”;
char *p = a;
cout<< sizeof(a) << endl; // 12 字节
cout<< sizeof(p) << endl; // 4 字节
计算数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4 字节而不是100 字节
}
21题: int (*s[10])(int) 表示的是什么?
int (*s[10])(int) 函数指针数组,每个指针指向一个int func(int param)的函数。
22题:栈内存与文字常量区
char str1[] = “abc”;
char str2[] = “abc”;
const char str3[] = “abc”;
const char str4[] = “abc”;
const char *str5 = “abc”;
const char *str6 = “abc”;
char *str7 = “abc”;
char *str8 = “abc”;
cout << ( str1 == str2 ) << endl;//0 分别指向各自的栈内存
cout << ( str3 == str4 ) << endl;//0 分别指向各自的栈内存
cout << ( str5 == str6 ) << endl;//1指向文字常量区地址相同
cout << ( str7 == str8 ) << endl;//1指向文字常量区地址相同
结果是:0 0 1 1
解答:str1,str2,str3,str4是数组变量,它们有各自的内存空间;而str5,str6,str7,str8是指针,它们指向相同的常量区域。
23题:将程序跳转到指定内存地址
要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?
*((void (*)( ))0x100000 ) ( );
首先要将0x100000强制转换成函数指针,即:
(void (*)())0x100000
然后再调用它:
*((void (*)())0x100000)();
用typedef可以看得更直观些:
typedef void(*)() voidFuncPtr;
*((voidFuncPtr)0x100000)();
24题:int id[sizeof(unsigned long)];这个对吗?为什么?
答案:正确 这个 sizeof是编译时运算符,编译时就确定了 ,可以看成和机器有关的常量。
25题:引用与指针有什么区别?
【参考答案】
1) 引用必须被初始化,指针不必。
2) 引用初始化以后不能被改变,指针可以改变所指的对象。
3) 不存在指向空值的引用,但是存在指向空值的指针。
26题:const 与 #define 的比较 ,const有什么优点?
【参考答案】
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。
(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
27题:复杂声明
void * ( * (*fp1)(int))[10];
float (*(* fp2)(int,int,int))(int);
int (* ( * fp3)())[10]();
分别表示什么意思?
【标准答案】
1.void * ( * (*fp1)(int))[10]; fp1是一个指针,指向一个函数,这个函数的参数为int型,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个void*型指针。
2.float (*(* fp2)(int,int,int))(int); fp2是一个指针,指向一个函数,这个函数的参数为3个int型,函数的返回值是一个指针,这个指针指向一个函数,这个函数的参数为int型,函数的返回值是float型。
3.int (* ( * fp3)())[10](); fp3是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是int型。
28题:内存的分配方式有几种?
【参考答案】
一、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量。
二、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
三、从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
29题:基类的析构函数不是虚函数,会带来什么问题?
【参考答案】派生类的析构函数用不上,会造成资源的泄漏。
30题:全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
【参考答案】
生命周期不同:
全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
使用方式不同:通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用;分配在栈区。
操作系统和编译器通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。
《C++开发工程师面试题》
(一)2018.4 拼多多实习服务端
1、 一个C++源文件从文本到可执行文件经历的过程
对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:
1).预处理,产生.ii文件
2).编译,产生汇编文件(.s文件)
3).汇编,产生目标文件(.o或.obj文件)
4).链接,产生可执行文件(.out或.exe文件)
2、#include 的顺序以及尖叫括号和双引号的区别
- #include的顺序的区别:
头文件的引用顺序对于程序的编译还是有一定影响的。如果要在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误,也就是常见的某行少个“;”符号。
- #include尖括号和双引号的区别:
1)#include <> ,认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方式因编译器的不同而差别迥异。
2)#include “”,认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。查找范围大于<>。
3、进程和线程,为什么要有线程
1、和进程相比,它是一种非常”节俭”的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。(资源)
2、运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。(切换效率)
3、线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。(通信)
除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:
1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。(CPU设计保证)
2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。(代码易维护)
4、C++11有哪些新特性
1)关键字及新语法:auto、nullptr、for
2)STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set
3)多线程:std::thread、std::atomic、std::condition_variable
4)智能指针内存管理:std::shared_ptr、std::weak_ptr
5)其他:std::function、std::bind和lamda表达式
5、为什么可变参数模板至关重要,右值引用,完美转发,lambda
6、malloc的原理,brk系统调用干什么的,mmap呢
malloc的实现方案:
1)malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。
2)调用 malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。
3)调用 free 函数时,它将用户释放的内存块连接到空闲链表上。
4)到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。
brk和mmap:
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。
7、C++的内存管理方式,STL的allocator,最新版本默认使用的分配器
C++的内存管理方式:
在c++中内存主要分为5个存储区:
栈(Stack):局部变量,函数参数等存储在该区,由编译器自动分配和释放.栈属于计算机系统的数据结构,进栈出栈有相应的计算机指令支持,而且分配专门的寄存器存储栈的地址,效率分高,内存空间是连续的,但栈的内存空间有限。
堆(Heap):需要程序员手动分配和释放(new,delete),属于动态分配方式。内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。操作系统有一个记录空间内存的链表,当收到内存申请时遍历链表,找到第一个空间大于申请空间的堆节点,将该节点分配给程序,并将该节点从链表中删除。一般,系统会在该内存空间的首地址处记录本次分配的内存大小,用于delete释放该内存空间。
全局/静态存储区:全局变量,静态变量分配到该区,到程序结束时自动释放,包括DATA段(全局初始化区)与BSS段(全局未初始化段)。其中,初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和静态变量存放在BSS段。BSS段特点:在程序执行前BSS段自动清零,所以未初始化的全局变量和静态变量在程序执行前已经成为0.
文字常量区:存放常量,而且不允许修改。程序结束后由系统释放。
程序代码区:存放程序的二进制代码
SGI 版本STL的默认配置器std::alloc
参见:《STL源码剖析》
1)考虑到小型区块所可能造成的内存碎片问题,SGI设计了双层配置器。第一级配置器直接使用malloc()和free();第二级则视情况采取不同的策略:当配置区块超过128bytes时,视为“足够大”,便调用第一级配置器;当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用memory pool(内存池)整理方式,而不在求助于第一级配置器。
2)内存池的核心:内存池和16个自由链表(各自管理8,16,…,128bytes的小额区块)。在分配一个小区块时,首先在所属自由链表中寻找,如果找到,直接抽出分配;若所属自由链表为空,则请求内存池为所属自由链表分配空间;默认情况下,为该自由链表分配20个区块,若内存池剩余容量不足,则分配可分配的最大容量;若内存池连一个区块都无法分配,则调用chunk_alloc为内存池分配一大块区块;若内存不足,则尝试调用malloc分配,否则返回bad_alloc异常。
8、hash表的实现,包括STL中的哈希桶长度常数。
hash表的实现主要涉及两个问题:散列函数和碰撞处理。
1)hash function (散列函数)。最常见的散列函数:f(x) = x % TableSize .
2)碰撞问题(不同元素的散列值相同)。解决碰撞问题的方法有许多种,包括线性探测、二次探测、开链等做法。SGL版本使用开链法,使用一个链表保持相同散列值的元素。
虽然开链法并不要求表格大小必须为质数,但SGI STL仍然以质数来设计表格大小,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。
9、hash表如何rehash,怎么处理其中保存的资源
先想想为什么需要rehash:
因为,当loadFactor(负载因子)<=1时,hash表查找的期望复杂度为O(1). 因此,每次往hash表中添加元素时,我们必须保证是在loadFactor <1的情况下,才能够添加。
模仿C++的vector扩容方式,Hash表中每次发现loadFactor==1时,就开辟一个原来桶数组的两倍空间(称为新桶数组),然后把原来的桶数组中元素全部转移过来到新的桶数组中。注意这里转移是需要元素一个个重新哈希到新桶中的。
10、Redis的rehash怎么做的,为什么要渐进rehash,渐进rehash怎么实现的
为了避免rehash对服务器造成影响,服务器不是一次将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1].
以下是哈希表渐进式 rehash 的详细步骤:
为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
11、Redis的定时机制怎么实现的,有哪些弊端,你将如何改进这个弊端
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:文件事件(服务器对套接字操作的抽象)和时间事件(服务器对定时操作的抽象)。Redis的定时机制就是借助时间事件实现的。
一个时间事件主要由以下三个属性组成:id:时间事件标识号;when:记录时间事件的到达时间;timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理时间。一个时间事件根据时间事件处理器的返回值来判断是定时事件还是周期性事件。
弊端:Redis对时间事件的实际处理时间并不准时,通常会比时间事件设定的到达事件稍晚一些。
改进:多线程?一个处理文件事件,一个处理时间事件? (不确定)。
12、Redis是单线程的,为什么这么高效
虽然Redis文件事件处理器以单线程方式运行,但是通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程运行的模块进行对接,这保持了Redis内部单线程设计的简单性。
13、Redis的数据类型有哪些,底层怎么实现
1)字符串:整数值、embstr编码的简单动态字符串、简单动态字符串(SDS)
2)列表:压缩列表、双端链表
3)哈希:压缩列表、字典
4)集合:整数集合、字典
5)有序集合:压缩列表、跳跃表和字典
14、Redis和memcached的区别
Redis和memcached的区别:
1)数据类型 :redis数据类型丰富,支持set liset等类型;memcache支持简单数据类型,需要客户端自己处理复杂对象
2)持久性:redis支持数据落地持久化存储;memcache不支持数据持久存储。)
3)分布式存储:redis支持master-slave复制模式;memcache可以使用一致性hash做分布式。
4)value大小不同:memcache是一个内存缓存,key的长度小于250字符,单个item存储要小于1M,不适合虚拟机使用
5)数据一致性不同:redis使用的是单线程模型,保证了数据按顺序提交;memcache需要使用cas保证数据一致性。CAS(Check and Set)是一个确保并发一致性的机制,属于“乐观锁”范畴;原理很简单:拿版本号,操作,对比版本号,如果一致就操作,不一致就放弃任何操作
6)cpu利用:redis单线程模型只能使用一个cpu,可以开启多个redis进程
15、TCP的模型,状态转移
TCP四层模型:
状态转移:
熟悉三次握手 和 四次释放的TCP状态转移。
16、用过哪些设计模式,单例模式,观察者模式的多线程安全问题
设计模式
1)Template Method模式:《effective c++》 条款35 :借助Non-virtual Interface手法实现Template Method模式
2)Strategy模式:《effective c++》 条款35:借助Function Pointers 实现Strategy模式、借助std::function完成Strategy模式、古典Strategy模式
17、用过多线程吗,以前的多线程代码还能怎么优化,线程池的实现
线程的创建
include int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void (start_routine)(void*), void *restrict arg);
线程终止:
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。 线程可以调用pthread_exit终止自己。
线程池的实现:
18、epoll怎么实现的,reactor模型组成
epoll实现:
第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄(eventpoll的对象)来标识。
struct eventpoll{ …. /红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件/ struct rb_root rbr; /双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件/ struct list_head rdlist; ….};
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
Reactor模型:
1)Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接。
2)Synchronous Event Demultiplexer(同步事件复用器):阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。
3)Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。
4)Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。
5)Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。
20、手撕代码:1)给定一个数字数组,返回哈夫曼树的头指针。2)最长公共连续子序列。
21、随便挑一个自己收获最多比赛或者项目介绍,收获了什么
22、单核机器上写多线程程序,是否需要考虑加锁,为什么?
23、线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的
24、HTTP和HTTPS的区别,HTTPS有什么特点,带来的好处和坏处,怎么实现的
25、线程间的同步方式,最好说出具体的系统调用
1)互斥量(mutex)
include int pthread_mutex_destroy(pthread_mutex_t *mutex); //销毁int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); //初始化pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int pthread_mutex_lock(pthread_mutex_t *mutex); //上锁int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试上锁 int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
2)条件变量(Condition Variable)
include int pthread_cond_destroy(pthread_cond_t *cond); //销毁int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); //初始化pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);int pthread_cond_broadcast(pthread_cond_t *cond);int pthread_cond_signal(pthread_cond_t *cond);
3)信号量(Semaphore)
include int sem_init(sem_t *sem, int pshared, unsigned int value);int sem_wait(sem_t *sem);int sem_trywait(sem_t *sem);int sem_post(sem_t * sem);int sem_destroy(sem_t * sem);
调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()。调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。
26、哈希表的桶个数为什么是质数,合数有何不妥?
质数比合数更容易避免冲撞,也就是说使用质数时,哈希效果更好,原始数据经哈希后分布更均匀。
其余时间聊项目,聊拼多多使用的技术。比较重要的一点是大家的项目经历,项目经历并不仅仅是摆在那里证明自己做过项目,要首先对项目有全局上的了解,再对自己负责的部分了如指掌,最好用到了什么组件和技术都去了解他们的原理,那么在面试的时候就有很多很多聊的了。
(二)腾讯二面面经
1、redis的主从复制怎么做的
Redis旧版复制功能只有同步和命令传播。新版复制功能加入了部分同步的功能。
1)同步:
2)命令传播:
当主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。
3)部分同步:(断线后重复制)
复制偏移量:通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态。
复制积压缓冲区:主服务保存最近的写命令到复制积压缓冲区,是一个先进先出队列
服务器运行ID:从服务器记录上次同步的主服务器的Id。
2、写代码,去掉字符串中的空格空格
include using namespace std;int main(){ char str[40] = ” abc 123 456 “; int num = 0; int i; for(i = 0; str[i] != ‘\0’; ++i) { if(str[i] == ‘ ‘) ++num; else str[i-num] = str[i]; } str[i-num] = ‘\0’; printf(“%s\n”,str);}
3、如何把一个文件快速下发到100w个服务器
gossip算法?Gossip有众多的别名“闲话算法”、“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。
4、如何判断一个图是否连同?
DFS、BFS、并查集
5、ubuntu开机的时候系统做了什么
1)加载BIOS
BIOS程序首先检查,计算机硬件能否满足运行的基本条件,这叫做”硬件自检”。硬件自检完成后,BIOS把控制权转交给下一阶段的启动程序。
2)读取MBR
计算机读取该设备的第一个扇区,也就是读取最前面的512个字节。如果这512个字节的最后两个字节是0x55和0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动,控制权于是被转交给”启动顺序”中的下一个设备。
3)Bootloader
在这种情况下,计算机读取”主引导记录”前面446字节的机器码之后,不再把控制权转交给某一个分区,而是运行事先安装的”启动管理器”(boot loader),由用户选择启动哪一个操作系统。
Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好一切准备。
Boot Loader有若干种,其中Grub、Lilo和spfdisk是常见的Loader。Linux环境中,目前最流行的启动管理器是Grub。
4)加载内核
内核的加载,内核加载后,接开始操作系统初始化,根据进程的优先级启动进程。
《2019面试题》
可以说个人秋招就要结束了,就等两个offer通知,然后签完搞定,这里提供一下自己复习的东西吧,我也就把这个东西给搞了一遍,然后面试基本没啥问题了,如果问的很深的话,那就只能只求多福了兄弟!其中可能有一些错误或者由于编译环境有差异请大家自动忽略这些错误【由于个人是搞ACM的,所以关于算法方面的东西就没有怎么提供了,不过大家把数据结构刷一遍是必要的】
信号的生命周期?
信号产生-》信号在进程中注册-》信号在进程中的注销-》执行信号处理函数
信号的产生方式?
(1)当用户按某些终端键时产生信号(2)硬件异常产生信号【内存非法访问】(3)软件异常产生信号【某一个条件达到时】(4)调用kill函数产生信号【接受和发送的所有者必须相同,或者发送的进程所有者必须为超级用户】(5)运行kill命令产生信号
信号处理方式?
(1)执行默认处理方式(2)忽略处理(3)执行用户自定义的函数
如何消除隐式转换?
使用explicit关键字进行修饰
重载,重写和隐藏的区别?
重载:即函数重载
重写【覆盖】:即用于虚函数
隐藏:只要派生类的函数名与基类相同就会隐藏
volatile表示什么?有什么作用?
易变的,不会被编译器进行优化,让程序取数据直接去内存中的。
Static_cast<>,dynamic_cast<>,const_cast<>,reinterpret_cast<>的各自作用和使用环境?
Static_cast:能完成大部分转换功能,但是并不确保安全
Const_cast:无法从根本上转变类型,如果是const,它就依旧是const,只是如果原对象不是const,可以通过此转换来处理,针对指针和引用而言。
Dynamic_cast:针对基类和派生类指针和引用转换,基类和派生类之间必须要继承关系,是安全的
Reinterpret_cast:允许将任何指针类型转为其他指针类型,是安全的
Malloc和new的区别?
New:
内存分配错误时,抛出bad_alloc异常,可以定义set_new_handler函数来在产生异常时进行处理;本身是一个运算符;分配内存的地方为自由存储区【为一个抽象概念】;对于对象而言,会先申请内存空间然后调用构造函数;无需指定大小
Malloc:
内存分配错误时,返回NULL;本身是一个库函数;分配内存的地方为堆;只申请内存空间;需要指定申请多大的内存;
free和delete的区别?
Delete:
本身是一个运算符
Free:
本身是一个库函数
free一个数组时如何知道要释放多大的内存呢?
一般在数组前面几个字节中存在某一个结构体来保存当前申请的数组大小。
__stdcall和__cdecl的区别?
__stdcall:
从右往左压栈,堆栈参数数据由函数本身清除,一般是通过汇编指令ret x,x表示弹出x个字节,参数必须是确定,必须为函数本身知晓,所以此关键字不能用于有可变参数应用的函数声明。
__cdecl:
从右往左压栈,由调用者来对堆栈数据进行清除,步骤:调用方调用函数-》函数执行-》函数结果返回-》调用方清除堆栈参数,主要针对可变参数
linux内部提供了那些调试宏?
__FILE__:表示在哪个文件
__LINE__:表示在当前多少行
__FUNCTION__:表示在执行在哪个函数
手写线程安全的单例模式?
引用和指针的区别?
指针:是一个变量类型;指针可以不进行初始化;指针初始化后可以改变,在写代码时需要大量的检测
引用:是一个别名;引用必须要初始化;引用初始化后不可改变,无需检测
出现异常时,try和catch做了什么?
Catch(Ep a)发生异常-》建立一个异常对象-》拷贝一个异常对象-》catch处理
Catch(Ep &a)发生异常-》建立一个异常对象-》引用异常对象-》catch处理
异常对象通常建立在全局或者堆中【需要在函数外进行捕捉】
Catch捕捉异常的转换:异常处理时,如果用基类的处理派生类的对象会导致派生类完全当做基类来使用,即便有虚函数也没用,所以派生类必须放在基类前处理。
C++如何处理多个异常的?
多次catch处理
常对象的成员变量一定不可以修改吗?为什么?
可以修改,用mutable来修饰,可以突破const的限制。
虚函数的调用过程?
找到对象内存中vfptr所指向虚函数表的地址-》找到虚函数表相应的虚函数地址
汇编层面:
Mov ecx, dword ptr[ebp-0ch]将this指针放进ecx
Mov edx, dword ptr[ecx]将虚表的地址放进edx
Call dword ptr[edx+4]:调用虚表中函数
虚函数放置顺序与声明顺序一样,成员变量也是
虚表中放的不是函数的入口地址,而是一个jmp跳转指令的地址
单继承,多继承,菱形继承,虚继承时,对象内存中的差异区别?如果存在虚函数呢?
单继承:
多继承:
菱形继承:
实现一个vector?是1.5还是2倍,各有什么优缺点?
1.5倍优势:可以重用之前分配但是释放的内存
2倍劣势:每次申请的内存都不可以重用
map底层用了什么?
红黑树
如果用map删除了一个元素,迭代器还能用吗?为什么?怎样做可以接着用?
能用,a.erase(it ++);因为是直接申请的内存,所以可以直接通过获取后续节点来处理
红黑树的特征是什么?
(1)根节点为黑色(2)一个节点为红色,子节点必定为黑色(3)从任意一点触发到达每一个叶子节点的黑色节点个数相同(4)每一个节点不是红色就是黑色(5)每一个叶子节点都是黑色
红黑树如何插入和删除的?
插入:
(1)如果父节点为黑色,直接插入不处理
(2)如果父节点为红色,叔叔节点为红色,则父节点和叔叔节点变为黑色,祖先节点变为红色,将节点操作转换为祖先节点
(3)如果当前节点为父亲节点的右节点,则以父亲结点为中心左旋操作
(4)如果当前节点为父亲节点的左节点,则父亲节点变为黑色,祖先节点变为红色,以祖先节点为中心右旋操作
删除:
(1)先按照排序二叉树的方法,删除当前节点,如果需要转移即转移到下一个节点
(2)当前节点,必定为这样的情况:没有左子树。
(3)删除为红色节点,不需要处理,直接按照删除二叉树节点一样
(4)如果兄弟节点为黑色,兄弟节点的两个子节点为黑色,则将兄弟节点变为红色,将着色转移到父亲节点
(5)如果兄弟节点为红色,将兄弟节点设为黑色,父亲结点设为红色节点,对父亲结点进行左旋操作
(6)如果兄弟节点为黑色,左孩子为红色,右孩子为黑色,对兄弟节点进行右旋操作
(7)如果兄弟节点为黑色,右孩子为红色,则将父亲节点的颜色赋值给兄弟节点,将父亲节点设置为黑色,将兄弟节点的右孩子设为黑色,对父亲节点进行左旋
红黑树和B+,B-的区别?
红黑树的深度比较大,而B+和B-的深度则相对要小一些,而B+较B-则将数据都保存在叶子节点,同时通过链表的形式将他们连接在一起。
线程同步几种方式?
互斥锁,信号量,临界区
手写strcpy,memcpy,memmove函数?
需要注意内存重叠问题
Do{}while(0)的用法有哪些?
(1)可以将语句当做一个独立的域(2)对于多语句可以正常的运行(3)可以有效的消除goto语句,达到跳转语句的效果
手写快排?时间复杂度?空间复杂度?能进行优化吗?还有吗?能进行尾递归优化吗?
最优时间复杂度:nlogn
最差时间复杂度:n^2
平均时间复杂度:nlogn
空间复杂度:logn -> n
优化:
(1)随机(2)三数取中(3)当排序达到一定长度时用插入排序(4)分隔一次后,将相同数据不处理(5)使用并行或者多线程(6)进行尾递归优化【即将logn降解为更低的复杂度】
线程池的作用是什么?
处理线程多并发,用一个数组保存线程,然后一直放着,如果没用就用条件变量让它休眠,如果加入一个新的任务就唤醒其中一个去执行这个任务。
Pthread_cond_signal和pthread_cond_broadcast的区别
Pthread_cond_signal表示唤醒睡眠线程中的一个【单播,可能按照优先级或者先来后到的原则】
Pthread_cond_boardcast表示唤醒所有睡眠线程【广播】
线程有几种状态?进程又有几种状态?
线程:
进程:
TCP三次握手和四次挥手及各自的状态?
三次握手:
CLOSE LISTEN
SYN_SENT
SYN_RCVD
ESTABLISHED
ESTABLISHED
四次挥手:
FIN_WAIT1
CLOSE_WAIT
FIN_WAIT2
LAST_ACK
TIME_WAIT
CLOSE
CLOSE
TCP如果两次握手会出什么问题?那三次握手又会造成什么问题?有什么好的解决方法没?
两次握手:客户端发送的连接请求可能在网络中滞留了,如果没有三次握手,可能会再次创建一个连接。
三次握手:引起SYN flood
不断发送同步报文段会因为传输控制模块TCB【处于半连接状态】从而消耗服务器资源
(1)【处理连接和半连接】定时释放监控系中无效的连接
(2)Syn cache技术【处理半连接状态】,接受到的SYN先不创建TCB,而是用一个hash表来表示,当前连接,如果接收到ACK然后再创建TCB
(3)Syn cookie技术【处理连接】通过一个cookie值来确定当前连接是否合法,合法就连接,一般的验证方法是,服务器接受到一个syn包,服务器通过syn产生一个cookie数据作为初始化序列,接收到ACK包时,序列-1就是得到的cookie,然后进行相应的验证。
TCP四次挥手为什么要有TIME_WAIT状态?为什么?
有两个原因:
(1)保证TCP协议全双工连接能够可靠关闭,直接关闭的话,如果服务器没有收到ACK,会重复发FIN。
(2)保证这次连接的重复数据从网络中消失,如果上次的socket和这次的socket处理的程序一样,就会导致这次连接把上次的数据加进来了。
死锁的原因?条件?如何预防?又如何避免?如何解除?
原因:系统资源不足;进程运行推进顺序不合适;资源分配不当
条件:互斥;不剥夺;循环等待;请求与保持
预防:破坏任意一个条件
避免:银行家算法
检测:资源分配图简化法
排序稳定的算法,你知道那些?
冒泡排序;插入排序;归并排序;基数排序
解决hash冲突的方法?
线性探测法;开链法;再哈希法;
C++分为内存分为哪几部分?
堆;栈;静态全局;常量;自由存储区
如果new申请内存失败了,如何去解决?如果让你实现一个new,你会怎么实现?
如果申请失败可以通过set_new_handler来进行处理。
实现:需要注意申请失败,如果相应的处理函数则调用,否则抛出bad_alloc异常
如何得到一个结构体内成员的偏移量?
进程与线程的区别?
(1)进程又自己的独立地址空间,线程没有
(2)进程是资源分配的最小单位,线程是CPU调度的最小单位
(3)进程和线程通信方式不同
(4)进程切换上下文开销大,线程开销小
(5)一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程
(6)对进程进程操作一般开销都比较大,对线程开销就小了
逐层打印二叉树?
用BFS队列
构造函数能不能虚函数?为什么?那拷贝构造函数能不能为虚函数?为什么?
不可以为虚函数,因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针。
拷贝构造函数是构造函数所以理由同上。
析构函数能不能虚函数?为什么?
可以为析构函数,没有为什么
模板和实现可不可以不写在一个文件里面?为什么?
只能写在一个一个头文件中。
原因:多文件处理变为一个文件其实是通过链接器来实现的,所以如果用源文件来处理模板实现,会导致链接失效,最主要的原因还是在编译,编译器会暂时不处理模板类只有在实例化对象时才去处理,但是这就需要实现的代码了,如果放在其他文件的话,就会无法形成相应的类。
什么是RAII资源管理?
即资源获取就是初始化,利用对象生命周期来控制程序资源,简单来说就是通过局部对象来处理一些资源问题
为什么要字节对齐?
(1)有些特殊的CPU只能处理4倍开始的内存地址
(2)如果不是整倍数读取会导致读取多次
(3)数据总线为读取数据提供了基础
在成员函数中调用delete this会出现什么问题?对象还可以使用吗?
如果当前内存空间真正被释放了再次调用成员函数会报错,调用成员变量好像没有问题。
如果在构造函数中调用memset(this, 0, sizeof(*this))来初始化内存空间,有什么问题吗?
对于有虚函数和虚表存在的类,在进行memset后不能调用虚函数和虚基表继承而来的数据和函数
对一个数组而言,delete a和delete[] a有什么区别?为什么?
对于基础数据类型没有什么区别,对于对象delete值调用一次析构函数,delete[]才会析构所有的东西。
Dynamic_cast是如何实现运行时类型转换的?
如果有些虚函数的话,会到对应的虚表中的RTTI去查找对应的类型来判断可不可以进行相应的转换。
C语言调用C++语法函数怎么做?那C++调用C语法的函数怎么做?
使用extern “C”来产生C语言环境编译的程序供外部使用。
Extern “C”是什么意思?他有什么作用?
表示当前声明需要用C语言环境进行编译。
进程间的通信方式有哪些?线程间的通信方式呢?
进程:共享内存,消息队列传递,无名管道,有名管道,信号,套接字
线程:锁机制,信号量,信号
IO模型主要有哪些?
阻塞,非阻塞,IO多路复用,异步
阻塞和非阻塞?同步与异步的区别?
自己领悟
Select,poll和epoll的区别?为什么?
Select和poll缺点:(1)每次调用select都需要将fd集合从用户态拷贝到内核态(2)每一次调用select都需要在内核中遍历所有的fd(3)select支持的文件描述符太小,默认1024,poll没有限制
Epoll:使用红黑树来存储fd,同时每一次通过epoll__ctl来将fd加入内核中,同时通过双向列表来返回已经出发某一个事件的fd
手写如何通过一个结构体的成员变量得到一个结构体的地址?
Struct{char a[0];}的作用?有什么好处?
充当可变缓冲区的作用,同时char a[0]不占用内存空间。
如何判断两个浮点数相等?
需要考虑浮点误差
浮点数为什么会有误差?
因为二进制无法精准的表示十进制小数,0.3和0.2都无法完整的用二进制表示。
TCP的nagle算法和延迟ack,还有CORK呢?他们有什么好处?一起用会有什么效果?你觉得可以有什么改进?
nagle算法:防止网络中存在太多小包而造成网络拥塞
延迟ack:减少ACK包的频繁发送
CORK:将多个包变成一个包发送,提高网络利用率,使载荷率更大
不可一起使用
栈上分配内存和堆上分配内存有什么区别?
栈上:分配简单,只需要移动栈顶指针,不需要其他的处理
堆上:分配复杂,需要进行一定程度清理工作,同时是调用函数处理的。
变量的存储方式有哪些?
Auto,extern,register,static
线程私有和共享那些资源?进程私有和共享那些资源?
线程私有:线程栈,寄存器,程序寄存器
共享:堆,地址空间,全局变量,静态变量
进程私有:地址空间,堆,全局变量,栈,寄存器
共享:代码段,公共数据,进程目录,进程ID
什么是守护进程?如何查看守护进程?什么是僵尸进程?如何查看僵尸进程?
守护进程:一个生命周期长,并且控制终端,然后周期性执行某种任务的进程
查看守护进程:ps a敏感词>
僵尸进程:进程退出,但是占用资源没有被回收
查看僵尸进程:ps -ef|grep defunct
进程同步机制?
信号量;管程;
什么是信号?
进程间通信机制中唯一的异步通信机制
kill函数的每一个参数的作用?
Pid>0:发给ID为pid的进程
Pid=0:发给进程组所有的进程
Pid=-1:发给所有的进程
Pid<-1:发给指定进程组的进程
什么是协程?
用户态的轻量级线程,有自己的寄存器和栈
虚拟内存实现有哪几种方式?有什么意义?
三种:请求分页存储管理;请求分段存储管理;请求段页式存储管理
什么是类型安全?能举例吗?
两个类型直接进行转换,必须是显式的,string和STL模板是类型安全的
确保线程安全的几种方式?
(1)原子操作(2)同步与锁(3)可重入(4)阻止过度优化volatile
OSI七层模型?
应用层;表示层;会话层;传输层;网络层;数据链路层;物理层;
TCP/IP五层模型?
应用层;传输层;网络层【路由器】;数据链路层【交换机、网桥、网卡】;物理层【中继器、集线器】;
DHCP协议是什么?使用什么端口?他的优劣?
DHCP协议:动态主机配置协议
客户端端口:68;服务端端口:67
说说DHCP协议执行的过程?
DHCP discover广播-》
《-DHCP offer广播
DHCP request-》
《-DHCP ack
在0.5T和0.875T会尝试新的租用,服务器不同意则返回nack否则是ack;
如果发生IP冲突则返回DHCP decline。
网络序是大端还是小端?为什么要这样?
大端,历史遗留问题
ping命令使用的是什么协议?
ICMP协议
路由表一般包含什么?
(1)网络地址(2)网络掩码(3)网关【下一跳服务器】(4)跃点数【距离】
停止等待协议的缺点?为什么?
信道利用率太低,每次都需要等上一次ACK包接收到了才能再次发送
拥塞控制的方式?具体怎么做的?快重传的时机是什么?
(1)慢开始(2)拥塞避免(3)快重传【收到3个失序分组确认】(4)快恢复
DNS协议如何实现将域名解析为IP地址的?
(1)客户机的应用程序调用解析程序将域名已UDP数据报的形式发给本地DNS服务器
(2)本地DNS服务器找到对应IP以UDP形式放松回来
(3)弱本地DNS服务器找不到,则需要将域名发送到根域名服务器,根域名服务器返回下一个要访问的域名服务器,则访问下一个域名服务器。
创建进程的步骤?
(1)申请空的PCB(2)为新进程分配资源(3)初始化PCB(4)将新进程插入就绪队列中
进程切换发生的原因?处理进程切换的步骤?
原因:中断发生;更高优先级进程唤醒;进程消耗完了时间片;资源阻塞;
步骤:(1)保存处理器的上下文(2)用新状态和其它相关信息更新正在运行进程的PCB(3)将原来的进程移到合适的队列中【就绪,阻塞】(4)选择另外一个执行的进程,更新被选中进程的PCB,将它加载进CPU
虚函数表是在什么时候确定的?那虚表指针呢?
编译时确定虚函数表,虚表指针则是运行时
如何检查内存泄露?如果不通过printf,debug等调试方式和编译器报错提示呢?
使用GDB调试器
Int(*f(int,void(*)()))(int,int)是什么意思?
一个函数,参数为int和指向返回值为void的无参数的函数指针,返回值为一个指向返回值为int,参数为int和int的函数指针
STL空间配置器如何处理内存的?能说一下它的大概实现方案吗?为什么是8bytes的倍数?
分为两部分:大于128bytes用malloc直接申请,小于128bytes则使用一个8bytes倍数的数组来进行申请。
为8bytes的原因是为了提高效率,同时对于64位的机器而言,地址大小为8bytes
HTTP 403表示什么?
权限不够
静态函数能定义为虚函数吗?为什么?
不可以,因为虚函数属于对象,不属于类
静态函数能定义为常函数吗?为什么?
不可以,因为常函数是操作成员变量的,而静态函数没有成员变量可说
知道什么是幂等性吗?举个例子?
其任意多次执行所产生的影响均与一次执行的影响相同。
当接受方的接受窗口为0时还能接受数据吗?为什么?还能接受什么数据?那怎么处理这些数据呢?
可以接受。
数据:零窗口探测报文;确认报文段;携带紧急数据的报文段
可能会被抛弃
当接受方的返回的接受窗口为0时,发送方会进行什么操作?
开启计时器,发送零窗口探测报文
请求页面置换策略有哪些方式?他们的区别是什么?各自有什么算法解决?
全局和局部;
全局:在整个内存空间置换
局部:在本进程中进行置换
全局:(1)工作集算法(2)缺页率置换算法
局部:(1)最优算法(2)FIFO先进先出算法(3)LRU最近最久未使用(4)时钟算法
系统调用与函数调用的区别?
(1)一个在用户地址空间执行;一个在内核空间执行
(2)一个是过程调用,开销小;一个需要切换用户空间和内核上下文,开销大
(3)一般相同;不同系统不同
对于默认处理的结构体,能用memcmp来进行比较吗?为什么?如果不能,该如何比较?
不能,因为字节对齐多出来的内存是随机的,必须要一个个成员比较
C++中有哪些机制可以取代宏?
Inline,typedef,const
手写一个有可变参数的函数?
使用va_list,va_start,va_arg,va_end。
也可以用宏定义##__VA_ARGS__,可以针对空参数消除逗号
可靠信号与不可靠信号的区别?
一个会丢失,另外一个则会用队列来保存相应的事件
this指针调用成员变量时,堆栈会发生什么变化?
将相应的参数从右往左压栈,然后将this指针放到寄存器中
实现一个shared_ptr类和auto_ptr类?
Shared_ptr则是引用计数处理,auto_ptr则是权限转移机制
下面这两个函数在执行过程中有什么区别?
Int f(string&a); f(“abc”);//报错
Int f(const string&a); f(“abc”);//正常
C++中可以继承string类吗?为什么?
不可以,因为string不是类
Char * const *(*next)()是什么?
next是一个指针,指向一个函数,这个函数返回一个指针,这个指针指向char类型的常量指针
访问一个网页的过程,计算机发生了什么?
(1)先找DNS
(2)建立TCP连接
(3)发送HTTP报文
(4)接受HTTP报文
(5)浏览器解析显示
如何判断const所修饰的对象?
const只修饰其后的【变量】,至于const放在类型前还是类型后并没有区别
作者:牛客网
链接:https://www.jianshu.com/p/2de5b739178a
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。