关于面试,我投的大部分岗位是图像处理相关的,其中涉及到不少C++的知识点,这是前段时间总结的 C++ 面试常考题,大部分来自于牛客网和各种博客,在我实际面试中出现过的问题已经高亮表示了。
1 C 和 C++ 的区别?
1) C++是面向对象的语言,而C是面向过程的结构化编程语言
2) C++具有封装、继承和多态三种特性
3) C++支持范式编程,比如模板类、函数模板等
2 C++ 中指针和引用的区别?
1) 指针有自己的一块空间,而引用只是一个别名;
2) 指针可以初始化为空,引用必须被初始化且必须是一个已有对象的引用;
3) 指针在使用时可以改变所指的对象,引用初始化的时候就固定了,不能被改变;
4) 指针++表示指向下一个对象,引用++表示所指对象加1.
3 智能指针
C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
为什么要使用智能指针:
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。
4 指针和数组的主要区别
1) 数组保存的是数据,而指针保存的是数据的地址
2) 数组直接访问数据,而指针间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据
5 为什么析构函数必须是虚函数?为什么 C++ 默认的析构函数不是虚函数
1) 将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
2) C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
6 函数指针
函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。(可以把函数当作参数进行传递,可以实现多态:指针指向不同的函数)
7 C++ 中的析构函数
1) 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数;
2) 析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
3) 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
4) 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
8 重载和覆盖
重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
9 static 关键字
1) 加了static关键字的全局变量只能在本文件中使用。例如在a.c中定义了static int a=10;那么在b.c中用extern int a是拿不到a的值得,a的作用域只在a.c中。
2) static定义的静态局部变量分配在数据段上,普通的局部变量分配在栈上,会因为函数栈帧的释放而被释放掉。
3) 对一个类中成员变量和成员函数来说,加了static关键字,则此变量/函数就没有了this指针了,必须通过类名才能访问
10 虚函数和多态
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
11 ++i 和 i++ 的实现
++i先自增1,再返回,i++先返回i,再自增1
1)++i实现:
int& int::operator++() { *this +=1; return *this; }
2)i++实现:
const int int::operator(int)
{
int oldValue = *this;
++(*this);
return oldValue;
}
12 C++ 函数栈空间的最大值
默认是1M,不过可以调整
13 new/delete 与 malloc/free 的区别是什么
1) new/delete是C++的关键字,而malloc/free是C语言的库函数
2) 后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数
14 C语言参数压栈顺序
从右到左
15 map 和 set 有什么区别
1) map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。
2) map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
3) set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。
4) map支持下标操作,set不支持下标操作。map可以用key做下标,
16 vector 和 list 的区别
1) vector, 连续存储的容器,动态数组,在堆上分配空间 ;
底层实现:数组。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素。
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
2)list,动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
底层:双向链表
访问:随机访问性能很差,只能快速访问头尾节点。
适用场景:经常插入删除大量数据
2) vector在中间节点进行插入删除会导致内存拷贝,list不会。
3) vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
4) vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
17 STL 中迭代器的作用,有指针为何还要迭代器
1) Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
2) 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、--等,相当于一种智能指针。
3) 迭代器产生原因:
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
18 STL 迭代器是怎么删除元素的呢
1) 对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
2) 对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3) 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,
19 C++ 中类成员的访问权限
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
20 C++中 struct 和 class的区别
在C++中,可以用struct和class定义类,都可以继承。区别在于:struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private.
21 左值和右值
1) 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
2) 左值可以寻址,而右值不可以。 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。左值可变,右值不可变。
22 C++11 有哪些新特性
1) auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
2) nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
3) 智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
23 C++ 源文件从文本到可执行文件经历的过程
1)预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
2)编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
3)汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
4)链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
24 include 头文件的顺序以及双引号””和尖括号<>的区别
1) 双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
2) 对于使用双引号包含的头文件,查找头文件路径的顺序为,先查找当前头文件目录
24 C++ 的内存管理是怎样的
1) 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
2) 数据段:存储程序中已初始化的全局变量和静态变量
3) bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
4) 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
5) 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
25 内存泄漏,段错误
1) 内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。判断内存是否泄露:1 linux环境下的内存泄漏检查工具Valgrind, 2 写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致.
2) 段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
使用野指针
试图修改字符串常量的内容
26 new 和 malloc 的区别
1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
3、new不仅分配一段内存,而且会调用构造函数,malloc不会。
4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5、new是一个操作符可以重载,malloc是一个库函数。
27 软件开发流程
1) 需求分析
2) 概要设计
3) 详细设计
4) 编码
5) 测试
6) 软件交付、验收
7) 维护
28 c/s 和 b/s 架构
1) C/S架构是第一种比较早的软件架构,主要用于局域网内。也叫客户机/服务器模式
分为客户机和服务器两层:
第一层: 在客户机系统上结合了界面显示与业务逻辑;
第二层: 通过网络结合了数据库服务器。
如果用户要用的话需要下载一个客户端,安装后可以使用,比如QQ,微信等。
优点:1)C/S架构的界面和操作可以很丰富。2)安全性能可以很容易保证。3)由于只有一层交互,因此响应速度较快。
缺点:1)用户群固定。由于程序需要安装才可使用,因此不适合面向一些不可知的用户。2)维护成本高,发生一次升级,则所有客户端的程序都需要改变。
2) B/S架构的全称为Browser/Server,即浏览器/服务器结构。
B/S架构有三层,分别为:
第一层表现层:主要完成用户和后台的交互及最终查询结果的输出功能。
第二层逻辑层:主要是利用服务器完成客户端的应用逻辑功能。
第三层数据层:主要是接受客户端请求后独立进行各种运算。
优点:1)客户端无需安装,有Web浏览器即可。2)BS架构可以直接放在广域网上,交互性强。3)BS架构无需升级多个客户端,升级服务器即可。
缺点:1)在速度和安全性上需要花费巨大的设计成本,这是BS架构的最大问题。2)客户端服务器端的交互是请求-响应模式,通常需要刷新页面。
3)C/S和B/S各有优势,C/S在图形的表现能力上以及运行的速度上肯定是强于B/S模式的,不过缺点就是他需要运行专门的客户端,而且更重要的是它不能跨平台
29 数据库事务
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。事务是DBMS中最基础的单位,事务不可分割。
事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID。
1) 原子性:事务被视为不可分割的最小单元,事物的所有操作要不成功,要不失败回滚。
2) 一致性:数据库在事务执行前后都保持一致性状态。
3) 隔离性:一个事务所做的修改在最终提交以前,对其他事务是可不见的
4) 持久性:一旦事务提交,则其所做的修改将会永远保存到数据库中
30 数据库索引
数据库索引是为了增加查询速度而对表字段附加的一种标识,是对数据库表中一列或多列的值进行排序的一种结构。
DB在执行一条Sql语句的时候,默认的方式是根据搜索条件进行全表扫描,遇到匹配条件的就加入搜索结果集合。如果我们对某一字段增加索引,查询时就会先去索引列表中一次定位到特定值的行数,大大减少遍历匹配的行数,所以能明显增加查询的速度。
优点:
1)通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
2)可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
缺点:
1) 索引需要占物理空间
2) 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护
31 数据库的三大范式
1) 第一范式:当关系模式R的所有属性都不能再分解为更基本的数据单位时,称R是满足第一范式,即属性不可分
2) 第二范式:如果关系模式R满足第一范式,并且R的所有非主属性都完全依赖于R的每一个候选关键属性。
3) 第三范式:即非主属性不传递依赖于键码
31 SQL 优化方法有哪些
1) 通过建立索引对查询进行优化
2) 对查询进行优化,应尽量避免全表扫描
32 平衡二叉树(AVL树)和红黑树
1)平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。
2)红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑),红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。
3)所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。
33 哈夫曼编码
哈夫曼编码是哈夫曼树的一种应用,广泛用于数据文件压缩。哈夫曼编码算法用字符在文件中出现的频率来建立使用0,1表示字符的最优表示方式。
34 map 和 unordered_map的底层实现
map底层是基于红黑树实现的,因此map内部元素排列是有序的。而unordered_map底层则是基于哈希表实现的,因此其元素的排列顺序是杂乱无序的。
35 栈溢出的原因
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数。
1) 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出,局部变量是存储在栈中的。
2) 递归调用层次太多。
3) 指针或数组越界。例如进行字符串拷贝,或处理用户输入等等。
36 堆和栈的区别
C语言的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区。
1) 栈区:存放函数的参数值、局部变量等,由编译器自动分配和释放。栈由系统自动分配,速度快,但是程序员无法控制。
2) 堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作。一般是由程序员分配释放,未被释放可能引起内存泄漏。堆是有程序员自己分配,速度较慢,容易产生碎片,不过用起来方便。
3) 全局变量和静态变量的存储是放在一块的。
4) 常量区:常量存储在这里,不允许修改。
5) 代码区:存放函数体的二进制代码。
37 数组和链表
1) 数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素,因此数组的随机读取效率很高。数组的插入数据和删除数据效率低。
数组随机访问性强,查找速度快,插入删除效率低,必须要有连续的内存空间。
2) 链表中的元素在内存中不是顺序存储的,而是通过元素中的指针联系到一起,链表中增加删除元素很简单,只需要修改元素的指针就行了。插入删除速度快,内存利用率高,不会浪费内存,不能随机查找。
38 快速排序 C++ 实现
nt once_quick_sort(vector<int> &data, int left, int right)
{
int key = data[left];
while (left < right)
{
while (left < right && key <= data[right])
{
right--;
}
if (left < right)
{
data[left++] = data[right];
}
while (left < right && key > data[left])
{
left++;
}
if (left < right)
{
data[right--] = data[left];
}
}
data[left] = key;
return left;
}
int quick_sort(vector<int> & data, int left, int right)
{
if (left >= right )
{
return 1;
}
int middle = 0;
middle = once_quick_sort(data, left, right);
quick_sort(data, left, middle-1);
quick_sort(data, middle + 1, right);
}
39 哈希表(hash表)
哈希表的实现主要包括构造哈希和处理哈希冲突:构造哈希,主要包括直接地址法,除留余数法。
处理哈希冲突:当哈希表关键字集合很大时,关键字值不同的元素可能会映射到哈希表的同一地址上,这样的现象称为哈希冲突。常用的解决方法有:
1) 开放定址法,冲突时,用某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。(如,线性探测,平方探测)
2) 再哈希法:当发生冲突时,用另一个哈希函数计算地址值,直到冲突不再发生。
3) 链地址法:将所有哈希值相同的key通过链表存储,key按顺序插入链表中。
40 合并两个有序链表
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1 == NULL)
{
return l2;
}
if(l2 == NULL)
{
return l1;
}
if(l1->val < l2->val)
{
l1->next=mergeTwoLists(l1->next,l2);
return l1;
}
else
{
l2->next=mergeTwoLists(l1,l2->next);
return l2;
} } };
41 指针反转
/*
struct ListNode {
int val;
struct ListNode *next;
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead)
{
// 反转指针
ListNode* pNode=pHead; // 当前节点
ListNode* pPrev=nullptr;// 当前节点的上一个节点
ListNode* pNext=nullptr;// 当前节点的下一个节点
ListNode* pReverseHead=nullptr;//新链表的头指针
// 反转链表
while(pNode!=nullptr)
{
pNext=pNode->next; // 建立链接
if(pNext==NULL) // 判断pNode是否是最后一个节点
pReverseHead=pNode;
pNode->next=pPrev; // 指针反转
pPrev=pNode;
pNode=pNext;
}
return pReverseHead;
}
};
42 C++ 中的 const 关键字
1) const可以修饰变量,引用,指针等,修饰指针时,在*左边表示指针所指数据是常量,不可改变,但指针可以指向其他内存单元。在*右边时,表示指针本身是常量,不能指向其他内存地址,但指针所指的数据可以改变。
2) const可以修饰函数参数,传递过来的参数在函数内不可以改变。可以修饰成员变量,把变量变成常量,只可以读不可以写。
3) const修饰函数和成员函数的时候将const放在函数名后面,它修饰的成员函数不能修改任何的成员变量,const成员函数不能调用任何非const成员函数。
4) const可以修饰对象,const对象只能调用const成员函数,必须要提供一个const版本的成员函数。
43 C++中 static 关键字的作用
1) 最重要的一条:隐藏
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。如果在变量和函数前面加了static,那么它就会对其他源文件隐藏。利用这个特性,可以在不同的文件中定义同名函数和变量,而不必担心命名冲突。
2) 第二个作用是保持变量内容的持久(static变量中的记忆功能和全局生命周期)
存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。此外,如果作为static局部变量定义在函数内,它的生命周期是整个源程序,但是作用域与自动变量相同。只能在该函数内使用这个静态变量。退出函数后,尽管变量仍然存在,但不能使用它。
3)static的第三个作用是默认初始化为0(static变量)
4)第四个作用:c++中的类成员声明static
在类中声明static变量或函数时,初始化时使用作用域运算符来标明它所属类。
1 类的静态成员函数是属于整个类而非对象,所以它没有this指针。2 不能将静态成员函数定义为虚函数。3 静态数据成员是静态存储的,所以必须对它进行初始化。
44 C++ 中的 new 和 malloc
1) New和malloc都是在堆上申请内存的。Malloc和free是库函数,c/c++都可以用,new和delete是c++中的关键字。
2) New可以调用对象的构造函数,delete调用相应的析构函数。Malloc仅仅分配内存,free
仅仅释放内存。
3) new/delete的底层调用了malloc/free。
4) c++中允许重载new/delete操作符,malloc不允许重载。
5) char *p; p=(char*)malloc(100)。int *p=new int; int *a = new int[100];
6) new返回的是对象类型的指针,如上面的p,返回的是一个int型指针。而malloc返回的是*void,即无类型指针,需要做强制类型转换。