目录
C和C++的区别
1、语言特性
2、内存管理
3、C++的库更加丰富
4、对异常的处理
什么是封装继承多态?
封装
继承
多态
new和malloc的区别
STL容器有哪些?容器对应的使用场景?(挑一个你认为最熟悉的容器)
vector、list
应用场景
迭代器失效问题
stack、queue、priority_queue
map、set
map和set的区别?
map作为一个迭代有序的容器,当键值key是一个自定义类时,如何保证它是有序的?
map已经有了一个键值key,如果重复的插入这个键值key但是设置不同的value,是插入失败还是覆盖原来的value?
unordered_map、unordered_set
什么是哈希冲突?如何解决哈希冲突?
C和C++的区别
C语言是一门面向过程的语言,而C++是一门面向对象的语言。这就完了?那么这就完了!!
1、语言特性
从语言特性上来讲C++完全继承了C语言。C语言是一门面向过程的语言,更加注重执行的过程,C++在继承了C语言语法的同时引入了类,封装,继承,多态。使得C++在使用上变得更加灵活和可扩展,是一门强大的面向对象语言。
问题延展:什么是封装继承和多态?如何实现多态?C++有几种继承方式?多继承会有哪些问题?如何解决?虚继承原理是什么?…
2、内存管理
在内存管理方面,C++有更加丰富的内存管理机制,可以通过析构函数和运算符重载自动释放来管理动态内存,C语言则需要手动的释放。同时在内存的申请和访问上也有不同,C++倾向于使用new关键字申请动态内存,C语言则使用malloc等函数来申请动态内存。
问题延展:new和malloc的区别?动态内存在哪里申请?什么是内存碎片?C和C++中的内存泄漏是什么?如何避免内存泄漏?什么是RAII技术?…
3、C++的库更加丰富
C++标准库引入了标准模板库STL,STL将很多可复用的数据结构和算法,整合成为容器提供给开发人员使用,以便开发人员能更高效轻松的编写高质量代码。而C语言在这方面支持相对较少,更多的使用使用第三方库。
问题延展:举例STL容器,你对哪个容器最熟悉?它的应用场景是什么?什么是深浅拷贝?默认构造什么时候生成?…
4、对异常的处理
在错误的处理中,C++还使用了抛异常的机制,允许开发人员通在程序执行的过程中遇到错误时引发异常,并在适当的地方捕获异常和处理,C语言通常使用返回错误值表示函数在执行时出现错误的错误信息,对比一下,C++的异常处理机制可以提供更好的安全性,可以利用异常处理机制保证资源的正确释放和状态的一致性。
问题延展:如何处理异常?什么是异常的捕获顺序?C++中可以抛出哪些类型的异常?…
什么是封装继承多态?
对于我们新手程序员,这个基本是必问的了,逃不掉的。没啥好说的就学吧,学不完的……
封装
封装就是将成员和函数用类包裹起来,提供C++提供的访问限定符,来限定哪些是可以被外部使用的函数和成员,哪些是不能被直接访问的函数和成员。规范了使用者对类使用的方式,只通过调用函数提供的接口对类进行操作,同时提高了成员访问的安全性。
问题延展:访问限定符有哪些?说一下static关键字、如何访问静态的成员和成员函数?静态函数如何使用类内的成员?什么是构造函数?构造函数什么时候生成?…
class A{
public:
void func1(){ std::cout << "public" << std::endl; } // 外部可以访问
protected:
void func2() { std::cout << "protected" << std::endl; } // 继承的子类可以访问
private:
void func3() {std::cout << "private" << std::endl; } // 只有自己可以访问
}
继承
继承是指一个类可以从另一个类继承属性,继承的一方我们称为子类,被继承的一方我们称为父类,子类通过继承父类的属性可以实现代码的重用,同时子类也可以通过覆盖的方式修改其行为,最重要的是继承可以实现多态。
class A{
protected:
void func(){ std::cout << "i'm A::func() << std::endl; "}
}
class B : public A{
void print()
{
A::func();
}
}
问题延展:C++有几种继承方式?什么是多继承?、菱形继承是什么?有什么问题?如何解决?…
多态
多态就是同一个函数可以实现不同的效果,分为静态多态和动态多态。
静态多态也叫做重载,他是在相同作用域下通过函数参数不同,或者参数位置不同实现的两个函数名相同,但可以通过传入不同参数实现不同的函数效果。其本质是通过对汇编代码的函数名用参数修饰,而我们在表面上看两个函数名相同的函数,在链接上是通过查找不同的函数名链接,实现重载。
// 重载
int max(int a,int b) { return a > b : a : b };
int max(double a,double b) { return a > b : a : b; }
动态多态是通过子类继承父类,并对父类的虚函数重写来实现的,然后通过父类的指针或引用,来实现多态调用。实现无论是哪个对象传递给给父类指针(引用)都可以实现对自己重写父类虚函数的调用,是两个不同作用域下实现的,并且重写函数的参数和函数名返回值都必须与父类的要重写的虚函数一致。
class A{
protected:
virtual void func() { std::cout << "i'm A::func() << std::endl; "}
}
class B : public A{
virtual vodi func() { std::cout << "i'm B::func()" << std::endl; }
}
// 多态调用
void f(A* p)
{
p->func();
}
int main()
{
A a;
B b;
f(&a);
f(&b);
return 0;
}
问题延展:虚函数如何实现多态?什么是静态绑定和动态绑定?虚表什么时候生成?父类的对象可以实现多态吗?为什么?析构函数可以声明为虚函数,为什么构造函数不行?…
new和malloc的区别
你以为这就是一个简单的问题吗?那你就错了,这个问题可坑了。只是作为引出,面试官后续的问题,一定会把你往内存的问题上带。
1、new是C++上的一个操作符,作为操作符new可以使用运算符对new进行运算符重载,malloc则是C语言的一个用来申请动态内存的函数,new和malloc都是在虚拟内存的堆(heap)分配空间,堆区的空间由程序员自己管理和手动释放,new对应的释放为delete,malloc释放空间使用free函数。 (关键词:操作符和函数,堆区释放空间)
2、new会自动计算需要分配的内存大小并且new在分配空间时会调用对象的够着函数来构造对象,malloc开辟空间需要手动计算,并且malloc就只负责向堆区申请空间不会调用构造函数(关键词:自动分配和手动计算,调用和不调用)
3、new申请完成会返回一个申请的对象指针,malloc申请完成会返回一个void*的指针,并且需要我们对这个指针进行强转。new在申请失败后会抛出异常,而malloc失败则会返回一个空指针。(关键词:返回值,抛异常,空指针)
加分项:在申请空间时开始只能获取到虚拟内存的空间,只有在初始化我们分配的空间后,虚拟内存才会通过页表和缺页中断方式才能与物理内存建立映射关系。所以new
操作符在分配内存空间后会调用构造函数来初始化对象,并与物理内存建立映射关系。而malloc因为不会对对象进行初始化,刚开始并不会与内存建立映射关系,需要对空间初始化后,才能建立映射关系。
【其实到这里就可以看出面试官问出这个问题,无非就是想考察面试者对内存的理解和掌握】
问题延展:什么是虚拟内存?虚拟内存地址空间发布?堆区和栈区的区别?什么是内存碎片?什么是内存对齐?什么是内存泄漏?如何避免(必扯到RAII,智能指针)?缺页中断……
STL容器有哪些?容器对应的使用场景?(挑一个你认为最熟悉的容器)
刚开始可以把STL常见的容器都讲一遍,什么vector,list啦...巴拉巴拉的一大堆想起来的全部讲一遍。然后要熟悉容器的对应场景和容器的特征。最后再一一介绍。
vector、list
vector和list都是线性表,vector本质是一个动态开辟的数组,它的每个元素在地址上都是连续的,list则对应数据结构的带头双向循环链表,通过前驱指针和后继指针指针来连接每一个元素节点。
应用场景
vector得益于地址的连续,适合做查找,排序操作,同时支持通过下标访问元素,在尾插尾删的时间复杂度为O(1)。
list依靠指针连接节点,所以在添加和删除节点上,list可以动态的调整自己的长度。并且由于list作为链表在链表的任意节点插入元素都是O(1)的时间复杂度,适合需要频繁进行插入和删除数据的场景。
迭代器失效问题
当遍历vector容器时,对vector中的元素进行了插入或删除操作,导致迭代器无法正确的访问到vector中的元素,也就是迭代器指向了一个错误的地方。
#include <vector>
int main()
{
std::vector<int> nums = {1,2,3,4,6};
for(auto it = nums.begin();it != nums.end();++it)
{
if(*it == 4)
{
// nums.insert(it,5);// 迭代器失效
it = nums.insert(it,5); // 让迭代器指向新的位置
}
}
return 0;
}
list使用了指针连接方式,所以当在遍历list时,插入或删除list中元素节点,迭代器会指向新插入的节点或者是删除节点的后一个节点,不会导致迭代器失效。
问题延展:什么是深浅 拷贝?push_back和empty_back区别?左值引用右值引用?(暂时没想出来还有什么问题,可以提醒一下,接下来可能会直接转排序问题)……
stack、queue、priority_queue
stack栈,是一个后进先出的数据结构,不是在虚拟内存地址空间上的那个栈,它适用于一些需要后进先出的场景,例如括号匹配、逆波兰表达式、循环模拟树的前中后序(深度优先)等
queue队列,是一个先进先出的数据结构,同样的适用于一些需要先进先出的场景,例如层序遍历(广度优先)、线程池任务队列、生产者消费者模型等。
priority_queue,使用的是数据结构中的堆(heap),同样也区别于虚拟内存地址空间上的堆,在思想上是使用数组模拟一个二叉树,通过插入元素向下调整,删除元素向上调整。保证堆顶的数据始终为整个堆中最大或者最小的,保持堆顶为最大数据的叫做大堆,最小数据的为小堆。向上调整和向下调整的策略可以用来做堆排序,还可以解决topk问题,以及一些需要考虑优先级的场景。
问题延展:堆排序的时间复杂度?构建堆的时间复杂度?(暂时没想到)……
map、set
map和set的区别?
map和set的底层都同样使用了红黑树结构,map和set是封装了红黑树。
1、map使用的是键值对的方式存储数据,set只存储唯一元素没有键值对的概念。(关键词:键值对)
2、map内部重载了方括号([]),可以使用方括号通过键值查找或添加元素。set只能使用insert插入元素。(关键词:重载运算符)
3、红黑树的前序是有序的,所以遍历map和set也都是有序的。但区别在于map按照键值来排列顺序,set使用存储的元素值排序。同时map的key存储只能是唯一的,set的存储的每一个元素也是唯一的(关键词:key值排序,元素值排序)
map作为一个迭代有序的容器,当键值key是一个自定义类时,如何保证它是有序的?
可以通过使用仿函数的方式,仿函数重载括号来实现元素的比较方式,例如string可以使用首元素字典序的方式来排序。除了仿函数,还可以使用lambda表达式来自定义比较方法。(关键词:仿函数,重载括号,lambda)
map已经有了一个键值key,如果重复的插入这个键值key但是设置不同的value,是插入失败还是覆盖原来的value?
如果map中已经有了一个key值,那么重新插入一个key值相同的元素会覆盖掉原有的value。也就是map对于重复的键值会覆盖原来的值,不会导致插入失败。(关键词:覆盖)
问题延展:什么是红黑树?红黑树的时间复杂度是多少?为什么选择红黑树而不是其他数据结构?如何插入和删除节点以保持红黑树的平衡?……
unordered_map、unordered_set
unordered_map和unordered_set的底层使用的数据结构是哈希表(散列表)。哈希表是一个插入和查找操作都是O(1)时间复杂度,而哈希表的底层是一个数组,插入元素是对元素重新计算,然后映射到特定的数组下标来存储。但与map、set不同的是unordered_map和unordered_set存储的数据是无序的。
什么是哈希冲突?如何解决哈希冲突?
哈希冲突是当对插入元素进行映射时时,映射到的相应数组下标已经存在其他元素,这就是哈希冲突。
解决方法:
1、线性探测
线性探测其实就是,当发生哈希冲突时,向映射位置的下一个位置查看是否为空,如果下一个地址没有存储元素,直接存储到该位置,否则继续找空位置。
2、二次探测
二次探测通过计算一个增量序列来寻找下一个可用的位置,具体来说就是,当发生哈希冲突时,二次探测会计算一系列的增量值,通常使用二次方程来计算增量。假设哈希函数计算得到的哈希值为h,那么二次探测会依次检查位置h+1,h+4,h+9,h+16,以此类推,直到找到一个空闲位置或遍历完整个哈希表。
3、链地址法
这个很好理解,就是哈希表的每个地址不再只能存储一个元素了,我的每个地址存储的是一个链表,当你发生冲突时,直接将这个元素尾插到链表最后一个节点位置,从而解决哈希冲突。当然,存储的不一定就是链表,也可以存储红黑树来加快查找效率,这也是库里使用的解决方法。
问题延展:哈希表的扩容和缩容是如何实现的?哈希表的负载因子是什么?如何影响哈希表的性能?应用场景…
就先写到这了,这篇文章其实也算是我自己的复习总结吧,但希望这篇文章对看到的人能有所帮助吧,最后,今天是2023年10月1日,也祝大家节日快乐,祖国万岁!