title: C++常见面试题之基本语言
categories: [C++]
tags: [面试题]
date: 2021/05/11
<div align = 'right'>作者:hackett</div>
<div align = 'right'>微信公众号:加班猿</div>
一、基本语言
1、基本知识
1 说下C++和C的区别
C++是面向对象的语言(有重载、继承和多态三种特性,增加了类型安全功能,比如强制类型转换,支持范式编程,比如模板类、函数模板等)
C是面向过程的结构化编程语言
2 说一下C++中static关键字的作用
- 静态全局变量:
- 全局数据区分配内存,程序运行期间一直存在
- 未初始化自动初始化为0
- 声明它的整个文件可见,文件之外不可见
- 静态局部变量:
- 该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化
- 作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束
- 静态函数:
- 只在声明的文件中可见,不可被其他文件调用
- 其它文件中可以定义相同名字的函数,不会发生冲突
- 静态数据成员:
- 静态数据成语在程序中只有一个拷贝,由该类型所有对象共享访问,只分配一次内存
- 存储在全局数据区,静态数据成员定义时要分配空间,所以不能在类声明中定义
- 静态成员函数:
- 它为类的全部服务而不是为某一个类的具体对象服务
- 它不具有this指针,无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数
3 说一说C++中四种cast转换
-
const_cast
用于将const变量转为非const
-
static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
-
dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
-
reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
4 说一下C/C++中指针和引用的区别
1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
5.可以有const指针,但是没有const引用;
6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
7.指针可以有多级指针(**p),而引用只有一级;
8.指针和引用使用++运算符的意义不一样;
9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
5 说一下指针和数组的主要区别
指针和数组的主要区别如下:
指针 | 数组 |
---|---|
保存数据的地址 | 保存数据 |
间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据 | 直接访问数据, |
通常用于动态的数据结构 | 通常用于固定数目且数据类型相同的元素 |
通过Malloc分配内存,free释放内存 | 隐式的分配和删除 |
通常指向匿名数据,操作匿名函数 | 自身即为数据名 |
6 说一下野指针是什么?
野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针。
7 说一下C++中的智能指针
share_ptr:申请堆内存初始化为1,使用时+1,释放时-1,为0时堆内存释放
unique_ptr:指向的堆内存空间的引用计数,都只能为 1,放弃所指空间,堆内存空间释放回收
weak_ptr:需要搭配share_ptr使用,指针被释放所指堆内存的引用计数不会-1
8 说一下智能指针有没有内存泄露的情况
当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏
9 说一下智能指针内存泄露如何解决
为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。
10 说一下为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数
1、将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
2、C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
11 说一下函数指针
定义:函数指针是指向函数的指针变量。
用途:调用函数和做函数的参数,比如回调函数。
12 说一下fork函数
fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样
13 说一下C++析构函数的作用
1、析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数
2、使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏
14 说一下静态函数和虚函数的区别
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
15 写个函数在main函数执行前先运行
__attribute((constructor)) void before()
{ printf("before main\n"
);}
16 C++怎么定义常量?常量 存放在内存中的那个位置?
1、常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化。
2、对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
17 const修饰成员函数的目的?
const修饰的成员函数表明函数调用不会对对象做出任何更改
18 如果同时定义了两个函数,一个带const,一个不带,会有问题吗?
不会,这相当于函数的重载。
19 请你来说一说C++中的隐式类型转换?
首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象
20 请你来说一说C++函数栈空间的最大值?
默认是1M,不过可以调整
21 请你来说一说extern“C” ?
C++调用C函数需要extern C,因为C语言没有函数重载。
22 请你说说你了解的RTTI?
运行时类型检查,在C++层面主要体现在dynamic_cast和typeid,VS中虚函数表的-1位置存放了指向type_info的指针。对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info
23 请你说说虚函数表具体是怎样实现运行时多态的?
子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。
24 请你说说C语言是怎么进行函数调用的?
每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。
25 请你说说C语言参数压栈顺序?
从右到左
26 请你说说C++如何处理返回值?
生成一个临时变量,把它的引用作为函数参数传入函数内
27 请你回答一下C++中拷贝赋值函数的形参能否进行值传递?
不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。
28 请你回答一下malloc与new区别?
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。
29 请你说一说select ?
select在使用前,先将需要监控的描述符对应的bit位置1,然后将其传给select,当有任何一个事件发生时,select将会返回所有的描述符,需要在应用程序自己遍历去检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大
30 请你说说fork,wait,exec函数 ?
父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0.调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1
2.类和数据抽象
1 请你来说一下C++中类成员的访问权限?
public:共有的
protected:受保护的
private:私有的
类内三者都可以访问,类外只能访问共有public的
2 请你来说一下C++中struct和class的区别?
C++中,可以用struct和class定义类,都可以继承
struct的默认继承权限和默认访问权限是public
class 的默认继承权限和默认访问权限是private
class还可以定义模板类形参,比如template <class T, int i>
3 请你回答一下C++类内可以定义引用数据成员吗?
可以,必须通过成员函数初始化列表初始化。
4 请你回答一下什么是右值引用,跟左值又有什么区别?
右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
能够更简洁明确地定义泛型函数。
左值和右值的概念:
左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
右值引用和左值引用的区别:
左值可以寻址,而右值不可以。
左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。
3.编译与底层
1 请你来说一下一个C++源文件从文本到可执行文件经历的过程?
对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
2 请你回答一下malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?
Malloc函数用于动态分配内存。
malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。
Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;
malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址
Malloc在申请内存时 < 128K -- 使用系统函数brk在堆区中分配
Malloc在申请内存时 > 128K -- 使用系统函数mmap在映射区分配
3 请你说一说C++的内存管理是怎样的?
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
4 请你回答一下如何判断内存泄漏?
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete
1、内存泄漏检查工具Valgrind,mtrace
2、写代码时添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,来判断内存是否泄露
5 请你来说一下什么时候会发生段错误?
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
1、使用野指针
2、试图修改字符串常量的内容
6 请你来说一下reactor模型组成 ?
reactor模型要求主线程只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作,读写数据、接受新的连接以及处理客户请求均在工作线程中完成。
7 请自己设计一下如何采用单线程的方式处理高并发?
在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来
4.C++11
1 请问C++11有哪些新特性?
C++11 最常用的新特性如下:
auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
初始化列表:使用初始化列表来对类进行初始化
右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
atomic原子操作用于多线程资源互斥操作
新增STL容器array以及tuple
利用Lambda表达式,可以方便的定义和创建匿名函数
2 请你详细介绍一下C++11中的可变参数模板、右值引用和lambda这几个新特性
可变参数模板:
C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面带上省略号”。
右值引用:
C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,没有名字的就是右值。
利用Lambda表达式,可以方便的定义和创建匿名函数
如果你觉得文章还不错,记得"点赞关注"
关注我的微信公众号【 加班猿 】可以获取更多内容