C/C++ 八股文(二)

共 21329字,需浏览 43分钟

 ·

2021-09-14 02:58


星标/置顶 公众号👇硬核文章第一时间送达

说一下static关键字的作用

参考回答:

1 全局静态变量

在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量。

静态存储区,在整个程序运行期间一直存在。

初始化:未经初始化的全局静态变量会被自动初始化为 0(自动对象的值是任意的,除非他被显式初始化);

作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。

2 局部静态变量

在局部变量之前加上关键字 static,局部变量就成为一个局部静态变量。

内存中的位置:静态存储区

初始化:未经初始化的全局静态变量会被自动初始化为 0(自动对象的值是任意的,除非他被显式初始化);

作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

3 静态函数

在函数返回类型前加 static,函数就定义为静态函数。函数的定义和声明在默认情况下都是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。

函数的实现使用 static 修饰,那么这个函数只可在本 cpp 内使用,不会同其他 cpp 中的同名函数引起冲突;

warning:不要再头文件中声明 static 的全局函数,不要在 cpp 内声明非static 的全局函数,如果你要在多个 cpp 中复用该函数,就把它的声明提到头文件里去,否则 cpp 内部声明需加上 static 修饰;

4 类的静态成员

在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用

5 类的静态函数

静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。

在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

说一下C++和C的区别

参考回答:

设计思想上:

C++ 是面向对象的语言,而C是面向过程的结构化编程语言

语法上:

  • C++ 具有封装、继承和多态三种特性
  • C++ 相比 C,增加多许多类型安全的功能,比如强制类型转换、
  • C++ 支持范式编程,比如模板类、函数模板等

说一说c++中四种cast转换

参考回答:C++ 中四种类型转换是:static_cast, dynamic_cast,const_cast, reinterpret_cast

1、const_cast

用于将 const 变量转为非 const

2、static_cast

用于各种隐式转换,比如非 const 转 const,void* 转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

3、dynamic_cast

用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。

  • 向上转换:指的是子类向基类的转换
  • 向下转换:指的是基类向子类的转换

它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

4、reinterpret_cast

几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

5、为什么不使用 C 的强制转换?

C 的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

请说一下C/C++ 中指针和引用的区别?

参考回答

  1. 指针有自己的一块空间,而引用只是一个别名;
  2. 使用 sizeof 看一个指针的大小是4,而引用则是被引用对象的大小;
  3. 指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象 的引用;
  4. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
  5. 可以有 const 指针,但是没有 const 引用;
  6. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
  7. 指针可以有多级指针(**p),而引用止于一级;
  8. 指针和引用使用++运算符的意义不一样;
  9. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

给定三角形ABC和一点P(x,y,z),判断点P是否在ABC内,给出思路并手写代码

参考回答:

根据面积法,如果 P 在三角形 ABC 内,那么三角形 ABP 的面积+三角形 BCP 的面积+三角形 ACP 的面积应该等于三角形 ABC 的面积。算法如下:

#include <iostream>
#include <math.h>
using namespace std;
#define ABS_FLOAT_0 0.0001
struct point_float
{
float x;
float y;
};
/**

* @brief 计算三角形面积

*/

float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2)
{
    point_float AB,   BC;
    AB.x = pt1.x - pt0.x;
    AB.y = pt1.y - pt0.y;
    BC.x = pt2.x - pt1.x;
    BC.y = pt2.y - pt1.y;
    return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f;
}
/**

* @brief 判断给定一点是否在三角形内或边上

*/
bool IsInTriangle(const point_float A, const point_float B, const point_float C, const point_float D)
{
    float SABC, SADB, SBDC, SADC;
    SABC = GetTriangleSquar(A, B, C);
    SADB = GetTriangleSquar(A, D, B);
    SBDC = GetTriangleSquar(B, D, C);
    SADC = GetTriangleSquar(A, D, C);
    float SumSuqar = SADB + SBDC + SADC;
    if ((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < 
        ABS_FLOAT_0))
    {
        return true;
    }
    else
    {
        return false;
    }
}

怎么判断一个数是二的倍数,怎么求一个数中有几个1,说一下你的思路并手写代码

参考回答:

  1. 判断一个数是不是二的倍数,即判断该数二进制末位是不是 0:a % 2 == 0 或者a & 0x0001 == 0
  2. 求一个数中 1 的位数,可以直接逐位除十取余判断:
int fun(long x)
{
int _count = 0;
while(x)
{
    if(x % 10 == 1)
         ++_count;
    x /= 10;
}
return _count;
}
int main()
{
    cout << fun(123321) << endl;
    return 0;
}

请你回答一下野指针是什么?

参考回答:

野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针

请你介绍一下 C++ 中的智能指针

参考回答:

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11 中最常用的智能指针类型为 shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为 0 时,智能指针才会自动释放引用的内存资源。对 shared_ptr 进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过 make_shared 函数或者通过构造函数传入普通指针。并可以通过 get 函数获得普通指针。

请你回答一下智能指针有没有内存泄露的情况

参考回答:

当两个对象相互使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。例如:

请你来说一下智能指针的内存泄漏如何解决

参考回答:

为了解决循环引用导致的内存泄漏,引入了 weak_ptr 弱指针,weak_ptr 的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

请你回答一下为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数 考点:虚函数 析构函数

参考回答:

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

C++ 默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此 C++ 默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

请你来说一下 C++ 中析构函数的作用

参考回答:

析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。

析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如 ~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括 void 类型)。只能有一个析构函数,不能重载。

如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

类析构顺序:

  • 派生类本身的析构函数;
  • 对象成员析构函数;
  • 基类析构函数。

请你来说一下静态函数和虚函数的区别

参考回答:

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

请你来说一说重载和覆盖

参考回答:

重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中

重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

请你说一说 strcpy 和 strlen

参考回答:

strcpy 是字符串拷贝函数,原型:

char strcpy(char dest, const char *src);

从 src 逐字节拷贝到 dest,直到遇到 '\0' 结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是 strncpy 函数。

strlen 函数是计算字符串长度的函数,返回从开始到 '\0' 之间的字符个数。

请你说一说你理解的虚函数和多态

参考回答:

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了 virtual 关键字的函数,在子类中重写时候不需要加 virtual 也是虚函数。

虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

请你来回答一下 ++i 和 i++ 的区别

参考回答:

++i 先自增 1,再返回,i++ 先返回 i,再自增 1

请你来写个函数在 main 函数执行前先运行

参考回答:

__attribute((constructor))void before()
{
    printf("before main\n");
}

以下四行代码的区别是什么?

const char * arr = "123";
char * brr = "123"
const char crr[] = "123"
char drr[] = "123";

参考回答:

const char * arr = "123";
//字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
char * brr = "123";
//字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值
const char crr[] = "123";
//这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
char drr[] = "123";
//字符串123保存在栈区,可以通过drr去修改

请你来说一下 C++ 里是怎么定义常量的?常量存放在内存的哪个位置?

参考回答:

常量在 C++ 里的定义就是一个 top-level const 加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。

请你来回答一下 const 修饰成员函数的目的是什么?

参考回答:

const 修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上 const 限定,这样无论 const 对象还是普通对象都可以调用该函数。

如果同时定义了两个函数,一个带 const,一个不带,会有问题吗?

参考回答:

不会,这相当于函数的重载。

请你来说一说隐式类型转换

参考回答:

首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。

请你来说一说 C++ 函数栈空间的最大值

参考回答:

默认是 1M,不过可以调整

请你回答一下 new/delete 与 malloc/free 的区别是什么

参考回答:

首先,new/delete 是 C++ 的关键字,而 malloc/free 是 C语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数

请你说说你了解的RTTI

参考回答:

运行时类型检查,在 C++ 层面主要体现在 dynamic_cast 和typeid,VS 中虚函数表的 -1 位置存放了指向 type_info 的指针。对于存在虚函数的类型,typeid 和 dynamic_cast 都会去查询 type_info

请你说说虚函数表具体是怎样实现运行时多态的?

参考回答:

子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在 VS 中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。

请你说说C语言是怎么进行函数调用的?

参考回答:

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的 esp 指针压栈。

请你说说 C++ 如何处理返回值?

参考回答:

生成一个临时变量,把它的引用作为函数参数传入函数内。

请你回答一下 C+ +中拷贝赋值函数的形参能否进行值传递?

参考回答:

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。

请你说一说 select

参考回答:

select 在使用前,先将需要监控的描述符对应的 bit 位置 1,然后将其传给 select,当有任何一个事件发生时,select 将会返回所有的描述符,需要在应用程序自己遍历去检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大

请你说说 fork,wait,exec 函数

参考回答:

父进程产生子进程使用 fork 拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec 函数可以加载一个 elf 文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork 从父进程返回子进程的 pid,从子进程返回 0.调用了 wait 的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回 0,错误返回 -1。exec 执行成功则子进程从新的程序开始运行,无返回值,执行失败返回 -1

请你回答一下静态函数和虚函数的区别

参考回答:

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

请你说一说重载和覆盖

参考回答:

  • 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
  • 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

请你来说一下map和set有什么区别,分别又是怎么实现的?

参考回答:

map 和 set 都是 C++ 的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。

map 和 set 区别在于:

  1. map 中的元素是 key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set 与之相对就是关键字的简单集合,set 中每个元素只包含一个关键字。

  2. set 的迭代器是 const 的,不允许修改元素的值;map 允许修改 value,但不允许修改 key。其原因是因为 map 和 set 是根据关键字排序来保证其有序性的,如果允许修改 key 的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了 map 和 set 的结构,导致 iterator 失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以 STL 中将 set 的迭代器设置成 const,不允许修改迭代器的值;而 map 的迭代器则不允许修改 key 值,允许修改 value 值。

  3. map 支持下标操作,set 不支持下标操作。map 可以用 key 做下标,map 的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和 mapped_type 类型默认值的元素至 map 中,因此下标运算符[ ]在 map 应用中需要慎用,const_map 不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type 类型没有默认值也不应该使用。如果 find 能解决需要,尽可能用 find。

请你来介绍一下 STL 的 allocaotr

参考回答:

STL 的分配器用于封装 STL 容器在内存管理上的底层细节。在 C++ 中,其内存配置和释放如下:

  • new 运算分两个阶段:
  1. 调用 ::operator new 配置内存;
  2. 调用对象构造函数构造对象内容
  • delete 运算分两个阶段:
  1. 调用对象希构函数;
  2. 掉员工::operator delete释放内存

为了精密分工,STL allocator 将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。

同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL 采用了两级配置器,当分配的空间大小超过 128B 时,会使用第一级空间配置器;当分配的空间大小小于 128B 时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

请你来说一说 STL 迭代器删除元素

参考回答:

这个主要考察的是迭代器失效的问题。

  1. 对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
  2. 对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
  3. 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。

请你说一说 STL 中 MAP 数据存放形式

参考回答:红黑树。unordered map底层结构是哈希表

请你讲讲 STL 有什么基本组成

参考回答:

STL 主要由:以下几部分组成:

容器迭代器仿函数算法分配器配接器

他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数

请你说说 STL 中 map 与 unordered_map

参考回答:

  1. Map映射,map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。

底层实现:红黑树

适用场景:有序键值对不重复映射

  1. Multimap 多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。

底层实现:红黑树

适用场景:有序键值对可重复映射

请你说一说 vector 和 list 的区别,应用,越详细越好

  1. Vector 连续存储的容器,动态数组,在堆上分配空间

底层实现:数组

两倍容量增长:

vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。

性能:

访问:O(1)

插入:在最后插入(空间够):很快

  • 在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
  • 在中间插入(空间够):内存拷贝
  • 在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。删除:
  • 在最后删除:很快
  • 在中间删除:内存拷贝

适用场景:经常随机访问,且不经常对非尾节点进行插入删除。2. List 动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。

底层:双向链表

性能:

访问:随机访问性能很差,只能快速访问头尾节点。

插入:很快,一般是常数开销

删除:很快,一般是常数开销

适用场景:经常插入删除大量数据

区别

  • vector 底层实现是数组;list 是双向 链表。
  • vector 支持随机访问,list 不支持。
  • vector 是顺序内存,list 不是。
  • vector 在中间节点进行插入删除会导致内存拷贝,list 不会。
  • vector 一次性分配好内存,不够时才进行 2 倍扩容;list 每次插入新节点都会进行内存申请。
  • vector 随机访问性能好,插入删除性能差;list 随机访问性能差,插入删除性能好。应用vector 拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用 vector。

list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用 list。

请你来说一下 STL 中迭代器的作用,有指针为何还要迭代器

参考回答:

  1. 迭代器

Iterator(迭代器)模式又称 Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator 模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由 iterator 提供的方法)访问聚合对象中的各个元素。

由于 Iterator 模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如 STL 的 list、vector、stack 等容器类及 ostream_iterator 等扩展 iterator。

  1. 迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、--等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的 ++,--等操作。

迭代器返回的是对象引用而不是对象的值,所以 cout 只能输出迭代器使用 * 取值后的值而不能直接输出其自身。

  1. 迭代器产生原因

Iterator 类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

请你说一说epoll原理

参考回答:

调用顺序:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

首先创建一个 epoll 对象,然后使用 epoll_ctl 对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以 epoll_event 结构体的形式组成一颗红黑树,接着阻塞在 epoll_wait,进入大循环,当某个 fd 上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。

n 个整数的无序数组,找到每个元素后面比它大的第一个数,要求时间复杂度为 O(N)

参考回答:

vector<int> findMax(vector<int>num)
{
if(num.size()==0)return num;
vector<int>res(num.size());
int i=0;
stack<int>s;
while(i<num.size())
{
if(s.empty()||num[s.top()]>=num[i])
{
s.push(i++);
}
else
{
res[s.top()]=num[i];
s.pop();
}
}
while(!s.empty())
{
res[s.top()]=INT_MAX;
s.pop();
}
for(int i=0; i<res.size(); i++)
cout<<res[i]<<endl;
return res;
}

请你回答一下 STL 里 resize 和 reserve 的区别

参考回答:

resize():改变当前容器内含有元素的数量(size()),eg: vectorv; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;

reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;

测试代码如下:

#include <iostream>
#include <vector>
using namespace std;
int main() {
    vector<int> a;
    a.reserve(100);
    a.resize(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //50  100
    a.resize(150);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //150  200
    a.reserve(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //150  200
    a.resize(50);
    cout<<a.size()<<"  "<<a.capacity()<<endl;
        //50  200    
}

请你来说一下 C++ 中类成员的访问权限

参考回答:

C++ 通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

请你来说一下 C++ 中 struct 和 class 的区别

参考回答:

在 C++ 中,可以用 struct 和 class 定义类,都可以继承。区别在于:structural 的默认继承权限和默认访问权限是 public,而 class 的默认继承权限和默认访问权限是 private。另外,class 还可以定义模板类形参,比如 template。

请你回答一下C++类内可以定义引用数据成员吗?

参考回答:

可以,必须通过成员函数初始化列表初始化。

请你回答一下 malloc 的原理,另外 brk 系统调用和 mmap 系统调用的作用分别是什么?

参考回答:

Malloc 函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc 其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc 采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时 malloc 采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。

当进行内存分配时,Malloc 会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc 采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。

Malloc 在申请内存时,一般会通过 brk 或者 mmap 系统调用进行申请。其中当申请内存小于 128K 时,会使用系统函数 brk 在堆区中分配;而当申请内存大于 128K 时,会使用系统函数 mmap 在映射区分配。

请你说一说C++的内存管理是怎样的?

参考回答:

在 C++ 中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

  • 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
  • 数据段:存储程序中已初始化的全局变量和静态变量
  • bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
  • 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
  • 映射区:存储动态链接库以及调用mmap函数进行的文件映射
  • 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

请你来说一下 C++/C 的内存分配

参考回答:

32bitCPU 可寻址 4G 线性空间,每个进程都有各自独立的 4G 逻辑地址,其中 0~3G 是用户态空间,3~4G 是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:

各个段说明如下:

3G 用户空间和 1G 内核空间

静态区域:

  • text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

  • data segment(数据段):存储程序中已初始化的全局变量和静态变量

  • bss segment:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为 0 动态区域:

  • heap(堆):当进程未调用 malloc 时是没有堆段的,只有调用 malloc 时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动 break 指针),从低地址向高地址增长。分配小内存时使用该区域。堆的起始地址由 mm_struct 结构体中的 start_brk 标识,结束地址由 brk 标识。

  • memory mapping segment(映射区):存储动态链接库等文件映射、申请大内存( malloc 时调用 mmap 函数)

  • stack(栈):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux 可以通过 ulimi t命令指定。

请你回答一下如何判断内存泄漏?

参考回答:

内存泄漏通常是由于调用了 malloc/new 等内存申请的操作,但是缺少了对应的 free/delete。为了判断内存是否泄露,我们一方面可以使用 linux 环境下的内存泄漏检查工具 Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。

请你来说一下什么时候会发生段错误

参考回答:

段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:

  • 使用野指针
  • 试图修改字符串常量的内容

请你来回答一下什么是 memory leak,也就是内存泄漏

参考回答:

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的分类:

  • 堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过 malloc,realloc new 等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
  • 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET 等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
  • 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是 virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

请你来说一下 reactor 模型组成

参考回答:

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 Demultiplexerselect 方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler 处理,即回调 EventHandler中的 handle_event() 方法。4.Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。
  4. Concrete Event Handler:事件 EventHandler 接口,实现特定事件处理逻辑。

请自己设计一下如何采用单线程的方式处理高并发

参考回答:

在单线程模型中,可以采用 I/O 复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来

请你说说 select,epoll 的区别,原理,性能,限制都说一说

参考回答:

  1. IO 多路复用

IO 复用模型在阻塞 IO 模型上多了一个 select 函数,select 函数有一个参数是文件描述符集合,意思就是对这些的文件描述符进行循环监听,当某个文件描述符就绪的时候,就对这个文件描述符进行处理。

这种 IO 模型是属于阻塞的 IO。但是由于它可以对多个文件描述符进行阻塞监听,所以它的效率比阻塞 IO 模型高效。

IO 多路复用就是我们说的select,poll,epoll。select/epoll的好处就在于单个 process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select,poll,epoll 这个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。

当用户进程调用了 select,那么整个进程会被 block,而同时,kernel 会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select() 函数就可以返回。

I/O 多路复用和阻塞I/O其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection

所以,如果处理的连接数不是很高的话,使用select/epollweb server不一定比使用multi-threading + blocking IOweb server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IOblock

2、select

select:是最初解决 IO 阻塞问题的方法。用结构体 fd_set 来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理。存在的问题:

  • 内置数组的形式使得 select 的最大文件数受限与 FD_SIZE;
  • 每次调用 select 前都要重新初始化描述符集,将 fd 从用户态拷贝到内核态,每次调用 select 后,都需要将 fd 从内核态拷贝到用户态;
  • 轮寻排查当文件描述符个数很多时,效率很低;

3、poll

poll:通过一个可变长度的数组解决了select 文件描述符受限的问题。数组中元素是结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态。poll 解决了 select 重复初始化的问题。轮寻排查的问题未解决。

4、epoll

epoll:轮寻排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll 采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。

epoll 对文件描述符的操作有两种模式:LT(level trigger)ET(edge trigger)。LT 模式是默认模式

LT模式

LT(level triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。

ET模式

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比LT模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

3、LT 模式与 ET 模式的区别如下:

LT 模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET 模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

请问 C++11 有哪些新特性?

参考回答:

C++11 最常用的新特性如下:

  • auto 关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
  • nullptr 关键字:nullptr 是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而 NULL 一般被宏定义为 0,在遇到重载时可能会出现问题。
  • 智能指针:C++11 新增了std::shared_ptrstd::weak_ptr等类型的智能指针,用于解决内存管理的问题。
  • 初始化列表:使用初始化列表来对类进行初始化
  • 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
  • atomic 原子操作用于多线程资源互斥操作
  • 新增 STL 容器 arra y以及 tuple

往期推荐




☞ 专辑 | 趣味设计模式
☞ 专辑 | 音视频开发
☞ 专辑 | C++ 进阶
☞ 专辑 | 超硬核 Qt
☞ 专辑 | 玩转 Linux
☞ 专辑 | GitHub 开源推荐
☞ 专辑 | 程序人生


关注公众「高效程序员」👇一起优秀!

回复 “入群” 进技术交流群,回复 “1024” 获取海量学习资源。
浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报