感觉测试相关的岗位HC较少,现在趁着之前投递过的研发岗位的流程还没走完,赶紧补一下研发相关的内容,我从B站找到一位老师的视频,从他那边获取到了一份C++面试题总结,接下来的几天,我将针对这个份面试题进行学习。
相关信息来自豆包或网络中可查询的资料,该文章用于个人学习记录,不对正确性负责,建议自行查询相关内容。
目录
1. 智能指针实现原理
2. 智能指针,里面的计数器何时会改变
3. 智能指针和管理的对象分别在哪个区
4. 面向对象的特性:多态原理
5. 介绍一下虚函数,虚函数怎么实现的
6. 多态和继承在什么情况下使用
7. 除了多态和继承还有什么面向对象方法
8. C++内存分布。什么样的数据在栈区,什么样的在堆区
9. C++内存管理(RAII啥的)
10. C++从源程序到可执行程序的过程
11. 一个对象=另一个对象会发生什么(赋值构造函数)
12. 如果new了之后出了问题直接return。会导致内存泄漏。怎么办
13. c++11的智能指针有哪些。weak_ptr的使用场景。什么情况下会产生循环引用
14. 多进程fork后不同进程会共享哪些资源
15. 多线程里线程的同步方式有哪些
16. size_of是在编译期还是在运行期确定
17. 函数重载的机制。重载是在编译期还是在运行期确定
18. 指针常量和常量指针
19. vector的原理,怎么扩容
20. 介绍一下const
21. 引用和指针的区别
22. Cpp新特性知道哪些
23. 类型转换
23. RAII基于什么实现的(生命周期、作用域、构造析构
25. 手撕:Unique_ptr、控制权转移(移动语义)、实现多态
27. 右值引用
28. 函数参数可不可以传右值
29. 参考c/c++堆栈实现自己的堆栈。要求:不能用stl容器。
30. stl容器了解吗?底层如何实现:vector数组,map红黑树,红黑
树的实现
31. 完美转发介绍一下 去掉std::forward会怎样?
32. 介绍一下unique_lock和lock_guard区别?
33. C代码中引用C++代码有时候会报错为什么?
34. 静态多态有什么?虚函数原理:虚表是什么时候建立的?为什么
要把析构函数设置成虚函数?
35. map为啥用红黑树不用avl树?(几乎所有面试都问了map和
unordered_map区别)
36. inline 失效场景
37. C++ 中 struct 和 class 区别
38. 如何防止一个头文件 include 多次
39. lambda表达式的理解,它可以捕获哪些类型40. 友元friend介绍
41. move函数
42. 模版类的作用
43. 模版和泛型的区别
44. 内存管理:C++的new和malloc的区别
45. new可以重载吗,可以改写new函数吗
46. C++中的map和unordered_map的区别和使用场景
47. 他们是线程安全的吗
48. c++标准库里优先队列是怎么实现的?
49. gcc编译的过程
50. C++ Coroutine
51. extern C有什么作用
52. c++ memoryorder/elf文件格式/中断对于操作系统的作
53. C++的符号表
54. C++的单元测试
首先C++ 的智能指针的源代码是包含在标准库中的,并且在
头文件中定义,位于 std
命名空间。然后智能指针是为保护内存而设计的,主要是为了管理动态分配的内存,防止内存泄漏。
无论是具体哪种智能指针都会在指针销毁时自动调用析构函数从而释放对应对象所占的空间,从而保护内存。常见的智能指针有三种,分别是std::shared_ptr
(引用计数型)、std::unique_ptr
(独占型)和 std::weak_ptr
(弱引用型)。我们来关注一下智能指针是如何初始化的:
#include
#include
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called." << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called." << std::endl;
}
void printMessage() {
std::cout << "This is a message from MyClass." << std::endl;
}
};
int main() {
// 创建一个std::shared_ptr对象,引用计数初始化为1
// 另一个std::shared_ptr对象也指向相同的对象,引用计数增加到2
std::shared_ptr ptr1 = std::make_shared();
std::shared_ptr ptr2 = ptr1;
// 创建一个std::unique_ptr对象,独占对象所有权
std::unique_ptr ptr1 = std::make_unique();
std::unique_ptr ptr3 = std::move(ptr1);
// 不能进行拷贝操作,以下代码会编译错误
// std::unique_ptr ptr2 = ptr1;
// 可以进行移动操作
// ptr1现在为空,ptr3拥有对象所有权并可以调用成员函数
// 创建一个std::weak_ptr对象,指向ptr1所管理的对象
std::shared_ptr ptr1 = std::make_shared();
std::weak_ptr ptr2 = ptr1;
// 通过lock方法获取一个std::shared_ptr,如果对象未释放则可以访问
if (std::shared_ptr ptr3 = ptr2.lock()) {
ptr3->printMessage();
}
// 当ptr1超出作用域,MyClass对象被释放,再次通过lock获取将得到空指针
return 0;
}
三种智能指针中除std::weak_ptr
外都是按照以下格式初始化智能指针
类型_ptr<类名> 智能指针名称 = make_类型<类名>();
指针类型 | std::shared_ptr |
std::unique_ptr |
std::weak_ptr |
概念 | 引入计数机制记录指向同一对象的不同智能指针数量,根据计数来决定释放内存与否 | 强调独占资源的所有权,不能被拷贝 | 主要用于解决std::shared_ptr 循环引用的问题。通过lock 方法可以尝试获取一个有效的std::shared_ptr |
优点 | 灵活、共享所有权、自动内存管理 | 独占所有权:避免内存释放责任划分问题、高效 | 解决循环引用、安全的弱引用 |
缺点 | 性能开销、循环引用 | 不能共享 | 间接访问、功能受限 |
解释:
shared_ptr的共享所有权为什么是优点:在一个复杂的数据结构中,多个节点可能需要共享对某个公共资源(如一个配置对象)的访问,使用std::shared_ptr
可以方便地实现这种共享。
循环引用是什么:当存在循环引用时,资源可能永远不会被释放。例如,有两个类A
和B
,A
中有一个std::shared_ptr
成员,B
中也有一个std::shared_ptr
成员。如果没有其他对象引用A
和B
,它们会因为相互引用而使得引用计数永远不会为 0,导致内存泄漏。
std::weak_ptr
是如何解决循环引用的:将类中的shared_ptr修改为weak_ptr即可。
我认为这是针对计数型智能指针share_ptr的问题:
创建新的共享指针指向同一对象时增加计数
共享指针超出作用域或者被重置时减少计数:return 0 或者显示重置 ptr1.reset();
std::weak_ptr
对引用计数的影响(间接改变):std::weak_ptr
使用lock方法获取到std::weak_ptr
后计数器增加
智能指针本身在栈区,托管的资源在堆区,利用了栈对象超出生命周期后自动析构的特征,所以无需手动delete释放资源
多态的概念
多态是面向对象编程中的一个重要特性,它允许不同的对象对同一消息(方法调用)做出不同的响应。简单来说,就是使用基类的指针或引用调用虚函数时,实际执行的函数版本是根据指针或引用所指向的对象的实际类型来确定的,而不是基类中定义的函数版本。
多态的实现原理(以 C++ 为例)
虚函数表(v - table)和虚函数表指针(v - pointer)当一个类包含虚函数时,编译器会为这个类创建一个虚函数表(v - table)。虚函数表是一个存储类中虚函数地址的表,这个表中的每一项都是该类的一个虚函数的地址。同时,每个包含虚函数的类的对象都会有一个虚函数表指针(v - pointer),这个指针通常位于对象的开头部分(编译器实现相关),它指向所属类的虚函数表。例如,有一个基类Base
和一个派生类Derived
,它们都有虚函数:
override
关键字。它用于显式地表明一个函数是对基类中虚函数的重写。class Base {
public:
virtual void func() {
std::cout << "Base::func()" << std::endl;
}
};
class Derived : public Base {
public:
void func() override {
std::cout << "Derived::func()" << std::endl;
}
};
封装、继承、多态
抽象类
定义:抽象类是不能被实例化的类,它通常包含一个或多个纯虚函数。纯虚函数是在基类中声明但没有定义的虚函数,它的作用是为派生类提供一个接口规范,要求派生类必须实现这些函数。
示例:在一个图形绘制系统中,Shape
类可以是一个抽象类,它有一个纯虚函数draw
。这样,任何从Shape
派生出来的具体图形类(如Circle
、Rectangle
)都必须实现draw
函数,保证了图形绘制的一致性。
接口
定义:接口是一种特殊的抽象类,它只包含纯虚函数,没有数据成员(在一些语言如 Java 中有严格的接口定义,在 C++ 中可以通过抽象类来模拟接口的概念)。接口定义了一组行为规范,类通过实现接口来表明它具有这些行为。
示例:在一个车辆管理系统中,可以有一个Vehicle
接口,包含start
(启动)、stop
(停止)和drive
(驾驶)等纯虚函数。汽车类(Car
)、摩托车类(Motorcycle
)等实现这个接口,以保证它们都具有这些基本的车辆操作行为。
C++内存分布包括栈区、堆区、全局 / 静态存储区、常量存储区、代码区
栈先进后出,存储局部变量、函数参数
堆区,存储动态分配的变量
RAII(Resource Acquisition Is Initialization)的本质
- RAII 是一种 C++ 编程技巧,它基于 C++ 的对象生命周期管理机制,将资源的获取和初始化与对象的生命周期绑定在一起。
A[源文件.cpp/.cxx等] -->|预处理指令处理| B[预处理后文件.i/.ii]; B -->|编译| C[汇编语言文件.s]; C -->|汇编| D[目标文件.o/.obj]; D -->|链接| E[可执行文件.exe(Windows)/无扩展名(Linux)]; A -->|程序员编写| A; B -->|预处理器| B; C -->|编译器| C; D -->|汇编器| D; E -->|链接器| E;
预处理:当编译器对源文件进行预处理时,#include
指令会将头文件内容插入到源文件中相应位置
编译:编译器会进行词法分析,把代码中的字符流分解成单词。然后进行语法分析,根据 C++ 语法规则构建语法树。接着进行语义分析,检查类型等语义信息。最终生成的.s
文件包含了针对特定计算机体系结构(如 x86、ARM 等)的汇编指令。
汇编:汇编指令转换为机器码。
链接:链接器将多个目标文件以及可能需要的库文件组合在一起,生成最终的可执行文件。
这个问题其实就是在问浅拷贝和深拷贝的区别:
简单来说,浅拷贝不特殊处理指针成员,深拷贝会在复制对象时,为指针成员分配新的内存,并将原始对象指针所指向的数据复制到新的内存中。
class MyClass {
public:
int* ptr;
MyClass() {
ptr = new int(10);
}
MyClass& operator=(const MyClass& other) {
if (this!= &other) {
// 释放当前对象的指针所指向的内存
delete ptr;
// 为当前对象的指针分配新的内存
ptr = new int(*other.ptr);
}
return *this;
}
};
首先内存泄漏、内存保护相关先想到智能指针和RAII,然后题中提到出了问题,那可以考虑以下异常机制,其实也是体现了RAII的思想
父子进程共享这些资源,但它们有各自独立的地址空间、进程 ID、父进程 ID 等。并且,对于共享的资源,如文件描述符和内存映射区域,父子进程的操作可能会相互影响,需要谨慎处理。
文件描述符:父进程和子进程通过相同的文件描述符来访问同一个文件或者 I/O 设备,它们共享文件的当前偏移量、文件状态标志(如读写模式等)。
内存映射区域:当父进程通过mmap
系统调用创建了内存映射区域后,子进程在fork
之后也会继承这个内存映射区域。这个区域可以是将文件映射到内存,或者是匿名内存映射(用于进程间共享内存等情况)。
信号处理函数
互斥锁
条件变量:条件变量用于在线程之间传递信号,通常与互斥锁一起使用。一个线程可以等待某个条件变量满足特定的条件,而其他线程在满足条件时可以通过条件变量来通知等待的线程。
信号量
读写锁
编译期,对于基本类型、自定类型、数组或者指针都是如此