C++面试题

286 阅读13分钟

一, C/C++语法基础

C和C++有什么区别?

  • C++是面向对象的语言,而C是面向过程的语言;
  • C++引入new/delete运算符,取代了C中的malloc/free库函数;
  • C++引入引用的概念,而C中没有;
  • C++引入类的概念,而C中没有;
  • C++引入函数重载的特性,而C中没有。

new/deletemalloc/free之间有什么关系?

  • 相同点:

    • 对于内部数据类型来说,没有构造与析构的过程,所以两者是等价的,都可以用于申请动态内存和释放内存;
  • 不同点:

    • new/delete可以调用对象的构造函数和析构函数,属于运算符,在编译器权限之内;
    • malloc/free仅用于内存分配和释放,属于库函数,不在编译器权限之内;
    • new是类型安全的,而malloc返回的数据类型是void *,所以要显式地进行类型转换;
    • new可以自动计算所需字节数,而malloc需要手动计算;
    • new申请内存失败时抛出bad_malloc异常,而malloc返回空指针。

如果在申请动态内存时找不到足够大的内存块,即mallocnew返回空指针,那么应该如何处理这种情况?

  • 对于malloc来说,需要判断其是否返回空指针,如果是则马上用return语句终止该函数或者exit终止该程序;
  • 对于new来说,默认抛出异常,所以可以使用try...catch...代码块的方式:

内存泄漏的场景:

  • mallocfree未成对出现;new/new []delete/delete []未成对出现;
    • 在堆中创建对象分配内存,但未显式释放内存;比如,通过局部分配的内存,未在调用者函数体内释放:

      char* getMemory() {
          char *p = (char *)malloc(30);
          return p;
      }
      int main() {
          char *p = getMemory();
          return 0;
      }
      
    • 在构造函数中动态分配内存,但未在析构函数中正确释放内存;

  • 未定义拷贝构造函数或未重载赋值运算符,从而造成两次释放相同内存的做法;比如,类中包含指针成员变量,在未定义拷贝构造函数或未重载赋值运算符的情况下,编译器会调用默认的拷贝构造函数或赋值运算符,以逐个成员拷贝的方式来复制指针成员变量,使得两个对象包含指向同一内存空间的指针,那么在释放第一个对象时,析构函数释放该指针指向的内存空间,在释放第二个对象时,析构函数就会释放同一内存空间,这样的行为是错误的;
  • 没有将基类的析构函数定义为虚函数。
  • 在Linux系统下,可以使用valgrind、mtrace等内存泄漏检测工具。

structclass有什么区别?

  • 成员的默认访问权限:

    struct的成员默认为public权限,class的成员默认为private权限;

  • 默认继承权限:

    struct的继承按照public处理,class的继承按照private处理。

指针和引用有什么区别?

  • 指针是一种对象,用来存放某个对象的地址,占用内存空间,而引用是一种别名,不占用内存空间;
  • 指针可以声明为空,之后进行初始化,普通指针可以随时更换所指对象,而引用必须在声明的时候初始化,而且初始化后不可改变;
  • 指针包含指向常量的指针和常量指针,而引用不包含常量引用,但包含对常量的引用。

堆和栈有什么区别?

  • 分配和管理方式不同:

    • 堆是动态分配的,其空间的分配和释放都由程序员控制;
    • 栈是由编译器自动管理的,其分配方式有两种:静态分配由编译器完成,比如局部变量的分配;动态分配由alloca()函数进行分配,但是会由编译器释放;
  • 产生碎片不同:

    • 对堆来说,频繁使用new/delete或者malloc/free会造成内存空间的不连续,产生大量碎片,是程序效率降低;
    • 对栈来说,不存在碎片问题,因为栈具有先进后出的特性;
  • 生长方向不同:

    • 堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长;
    • 栈是向着内存地址减小的方向增长的,从内存的高地址向低地址方向增长;
  • 申请大小限制不同:

    • 栈顶和栈底是预设好的,大小固定;
    • 堆是不连续的内存区域,其大小可以灵活调整。

浅拷贝和深拷贝有什么区别?

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存;而深拷贝会创造一个相同的对象,新对象与原对象不共享内存,修改新对象不会影响原对象。

动态绑定是如何实现的?

当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在对象中增加一个指针vptr,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。

构造函数和析构函数能抛出异常吗?

  • 从语法的角度来说,构造函数可以抛出异常,但从逻辑和风险控制的角度来说,尽量不要抛出异常,否则可能导致内存泄漏。
  • 析构函数不可以抛出异常,如果析构函数抛出异常,则异常点之后的程序,比如释放内存等操作,就不会被执行,从而造成内存泄露的问题;而且当异常发生时,C++通常会调用对象的析构函数来释放资源,如果此时析构函数也抛出异常,即前一个异常未处理又出现了新的异常,从而造成程序崩溃的问题。

 什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?

智能指针是一个RAII类模型,用于动态分配内存,其设计思想是将基本类型指针封装为(模板)类对象指针,并在离开作用域时调用析构函数,使用delete删除指针所指向的内存空间。

智能指针的作用是,能够处理内存泄漏问题和空悬指针问题。

分为auto_ptrunique_ptrshared_ptrweak_ptr四种,各自的特点:

  • 对于auto_ptr,实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象;但auto_ptr在C++11中被摒弃,其主要问题在于:

    • 对象所有权的转移,比如在函数传参过程中,对象所有权不会返还,从而存在潜在的内存崩溃问题;
    • 不能指向数组,也不能作为STL容器的成员。
  • 对于unique_ptr,实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象,因为无法进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值;

  • 对于shared_ptr,实现共享式拥有的概念,即多个智能指针可以指向相同的对象,该对象及相关资源会在其所指对象不再使用之后,自动释放与对象相关的资源;

  • 对于weak_ptr,解决shared_ptr相互引用时,两个指针的引用计数永远不会下降为0,从而导致死锁问题。而weak_ptr是对对象的一种弱引用,可以绑定到shared_ptr,但不会增加对象的引用计数。

shared_ptr是如何实现的?

  1. 构造函数中计数初始化为1;
  2. 拷贝构造函数中计数值加1;
  3. 赋值运算符中,左边的对象引用计数减1,右边的对象引用计数加1;
  4. 析构函数中引用计数减1;
  5. 在赋值运算符和析构函数中,如果减1后为0,则调用delete释放对象。

vectorreserve()resize()方法之间有什么区别?

首先,vector的容量capacity()是指在不分配更多内存的情况下可以保存的最多元素个数,而vector的大小size()是指实际包含的元素个数;

其次,vectorreserve(n)方法只改变vector的容量,如果当前容量小于n,则重新分配内存空间,调整容量为n;如果当前容量大于等于n,则无操作;

最后,vectorresize(n)方法改变vector的大小,如果当前容量小于n,则调整容量为n,同时将其全部元素填充为初始值;如果当前容量大于等于n,则不调整容量,只将其前n个元素填充为初始值。

Tcp和udp区别描述清楚。但是udp在现实哪些场景下用不知道。

第二节 进程和线程

#00 进程和线程有什么区别?

  • 进程是资源分配的最小单位,拥有独立的地址空间,每启动一个进程,系统都会建立数据表来维护其代码段、堆栈段和数据段;创建和切换进程的开销较大;进程间通信较复杂;多进程程序更加安全,进程间互不影响。
  • 线程是程序执行的最小单位,没有独立的地址空间;创建和切换线程的开销较小;线程间通信较方便;多线程程序不易维护,线程间相互影响。

#01 多进程和多线程有什么区别?

  • 多进程中数据是分离的,这样共享复杂,同步简单;而多线程中数据是共享的,这样共享简单,同步复杂;
  • 进程创建、销毁和切换比较复杂,速度较慢;线程创建、销毁和切换比较简单,速度较快;
  • 进程占用内存多,CPU利用率低;线程占用内存少,CPU利用率高;
  • 多进程的编程和调试比较简单,多线程的编程和调试比较复杂;
  • 进程间不会相互影响;而一个线程挂掉将导致整个进程挂掉;
  • 多进程适用于多核、多机分布;多线程适用于多核分布。

进程间的通信方式有哪些?

  • 匿名管道pipe:半双工的通信方式,数据只能单向流动,且只能在有亲缘关系的进程间使用;
  • 有名管道named pipe:与pipe相似,但允许在无亲缘关系的进程间使用;
  • 消息队列message queue:消息链表,存放在内核中并由消息队列标识符进行标识,克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点;
  • 共享存储shared memory:映射一段能够被其他进程访问的内存,一般与信号量配合使用;
  • 信号量semophore:计数器,用来控制多个进程对共享资源的访问,常常作为锁机制,用于不同进程间或同一进程内不同线程间的同步;
  • 套接字socket:可用于不同机器间的进程通信;
  • 信号signal:比较复杂,用于通知进程某个事件已经发生。

什么是进程切换/上下文切换?

进程切换即上下文切换,是指处理器从一个进程切换到另一个进程,内核在处理器上对于进程进行以下操作:

  1. 挂起一个进程,将这个进程在处理器中的状态(即上下文)存储于内存中;
  2. 在内存中检索下一个进程的上下文,并将其在CPU的寄存器中恢复;
  3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。

#00 同步和异步有什么区别?

  • 同步是指在发起一个调用之后,调用者需要一直等待调用结果的通知,才能进行后续的操作;
  • 异步是指在发起一个调用之后,调用者不能立即得到调用结果的返回,需要被调用者通过状态、通知和回调来通知调用者。

需要注意的是,同步/异步强调的是消息通信机制。

#01 阻塞和非阻塞有什么区别?

  • 阻塞是指在发起一个调用之后,在消息返回之前,当前进程/线程会被挂起,直到有消息返回,当前进程/线程才会被激活;
  • 非阻塞是指在发起一个调用之后,不会阻塞当前进程/线程,而会立即返回。

需要注意的是,阻塞/非阻塞强调的是等待消息时的状态。

selectpollepoll之间有什么区别?

  • select本质上是通过设置和轮询fd_set来检查是否有就绪的文件描述符,其缺点在于:

    • 单个进程可监视的文件描述符数量较少,在32位机器上默认为1024个,在64位机器上默认为2048个;
    • 每次调用select都需要把fd_set从用户空间拷贝到内核空间,文件描述符较多时开销较大;
    • 每次调用select都需要线性扫描fd_set,文件描述符较多时开销较大。
  • pollselect相似,不同之处在于poll使用pollfd链表结构保存文件描述符,因此与select相比,没有文件描述符数量的限制。

  • epoll提供了三个函数:

    • epoll_create用于创建一个epoll句柄;

    • epoll_ctl用于注册要监听的事件类型,其特点是:

      • 每次注册新的事件到epoll句柄中时,会把所有的文件描述符拷贝进内核空间,保证了每个文件描述符在整个过程中只拷贝一次,不会出现重复拷贝;
      • 为每个文件描述符指定一个回调函数,当事件发生时,就会调用这个回调函数,把就绪的文件描述符加入到就绪链表中;
    • epoll_wait用于等待事件的发生,唤醒等待中的进程;

    epoll对文件描述符的操作有两种模式:

    • 水平触发:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,下次调用epoll_wait时,将会再次响应应用程序并通知此事件;
    • 边缘触发:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件,如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件;

需要注意的是,表面上看epoll的性能最好,但是连接数量较少并且都十分活跃的情况下,selectpoll的性能可能较好,因为epoll的通知机制需要使用回调函数。

#01 TCP/IP的四层网络模型包含哪些层?

应用层(对应OSI的应用层、表示层、会话层)、运输层、网际层(对应OSI的网络层)、网络接口层(对应OSI的数据链路层、物理层)。 github.com/zouxiaobo/i…

TCP/IP的四层网络模型包含哪些层?