+++ author = "Wxn" title = "2024-04-19面试复盘" date = "2024-04-19" description = "Please read me first." tags = [ "Dilay", ] categories = [ "面试复盘", ] +++ This article offers a sample of basic Markdown. # 正文开始 1.struct和class的区别 2.值传递,地址传递,引用传递(拷贝)的区别 4.指针是几个字节? 5.static关键字 6.虚函数指针,虚函数表 7.多态 8.编译器如何知道你是进行的函数重载 答:函数重载,它允许在同一个作用域中定义多个同名函数,但这些函数的参数列表或类型必须不同。在调用这些函数时,编译器会根据传递给函数的参数的数量、类型或顺序来确定要调用的具体函数版本。 简单来讲,就是函数重载可以是参数类型不同,参数顺序不同,也可以是参数个数不同 编译器会在内部,把函数名称和参数列表一起存储在符号表中,这样编译器就知道了你用的是哪个重载之后的函数 就是,编译器在处理函数重载时,会将函数名称和参数列表一起存储在符号表(或类似符号表的数据结构)中。这样编译器就能够确定每个函数的唯一签名,并且在需要进行函数调用匹配时可以快速检索并确定调用哪个函数版本。 9.构造,析构的顺序 在C++中,对象的构造和析构顺序 最主要是 和对象的创建和销毁顺序相关。 一般情况下,对象的构造顺序与对象的创建顺序相同,对象的析构顺序与对象的销毁顺序相反。 比如说,如果一个类 `A` (包含类 `B` 和类 `C` 的对象作为成员),则在创建 `A` 的对象时,首先会调用 `B` 的构造函数,然后调用 `C` 的构造函数。 然后析构顺序就与构造顺序相反。也就说当销毁对象A时,析构函数会按照对象在类成员声明中的逆序依次调用。继续上面的例子,也就是说,他首先会调用 `C` 的析构函数,然后调用 `B` 的析构函数。 还有一种情况:就是父类和子类的析构与构造关系: 比如说现在有一个子类b,他继承了父类a,那么在创建子类b的对象时, 就会先调用父类a的构造函数, 再调用自己子类b的构造函数, 在析构的时候,顺序就反过来了, 就是先析构子类b, 最后再析构父类a ```cpp Base::fun() Child::fun() ~Child::fun() ~Base::fun() ``` 10.stl:vector、list、Map的区别 11.智能指针 智能指针有auto_ptr,share_ptr,unique_ptr还有shared_ptr auto_ptr是C++98中引入的第一个智能指针,但是由于他的不安全性已被C++11弃用。他其实被unique_ptr给取代了,可以理解为auto_ptr他其实是一个浅拷贝,非常容易造成多个指针指向同一块内存区域的现象,这就引发非常多的潜在的问题:比如一些常见的段错误,多次析构,访问一段未定义的空间等,比如说你在文件中访问这个指针,但是其实你已经在其他地方释放这段内存了. 然后,unique_ptr 他提供了独占所有权的智能指针,允许有且仅有一个 `unique_ptr` 拥有资源。与 `auto_ptr` 不同,`unique_ptr` 实现了更安全和更完整的独占所有权语义 然后,shared_ptr,他允许多个指针共享同一块内存,并且会在最后一个 shared_ptr 被销毁时释放这段内存。它使用的是一种引用计数的技术来追踪资源的所有者数量,并在所有者数量为零时自动释放资源。 然后最后一个,是weak_ptr: 他是 `shared_ptr` 的伴侣类,它允许你共享资源但不拥有资源。weak_ptr 不会增加引用计数,它通常用于解决 `shared_ptr` 的循环引用问题。 循环引用的问题: 然后循环引用通常是shared_ptr互相引用造成的,两个或多个对象相互持有对方的 shared_ptr,导致它们的引用计数永远不会降为零,然后导致资源泄漏。 我想到的一个例子是,比如说有一个双向列表,他们的每个节点对象都有两个指针,一个是前指针prior pointer,一个是next指针, 然后两个相邻的节点对象,节点1的next指针指向节点2自身,节点2的前指针prior pointer指向节点1自身,然后这两个节点都用share_ptr进行创建,在创建时,节点自身这段内存引用计数+1,然后被相邻指针指向时,引用计数再+1,这样每个节点的引用计数就都是2, 然后当我们销毁节点这个share_ptr后,前后节点的引用计数都降为了1,但是前节点在等着后节点的prior pointer放手,后节点在等着前节点的next pointer放手,这就形成了死锁, 造成的现象就是这两个节点的指针已经被销毁了,但是节点的内存迟迟无法析构,就造成了"内存泄漏",解决的办法就是把这个节点的prior指针和next指针都变成weak_ptr 12.项目中如何保证线程安全? 13.死锁 14.多线程中多个信号与主线程 主线程一般是负责控制和协调其他线程的任务。比如说我这个共享单车项目中的服务器,他的主线程主要是负责监听客户端的连接,一旦有客户端来进行连接,我们就会把这个客户端通过libevent变成一个事件,并把他加入到事件集合中去,然后由其他线程来监听该客户端的读写事件,并通过轮询的方式拿到线程池中的一个线程,然后由这个线程通过调用它自己的回调函数来处理这个读写事件.相当于主线程就是是负责统筹规划的,他只复制监听客户端,剩下的交给其他线程. 然后我的毕设的服务器是只用到了libevent,没有用到线程池,毕设主要的项目亮点是硬件的uboot系统移植以及arm架构下的库的交叉编译,还有qt的自定义部件、界面以及qt打包等,最终形成一个完善的项目. 然后硬件是用到的一个多进程通信,使用的是shm共享内存 在多线程编程中,主线程可以通过发送信号来与其他线程进行通信。 常见模式有: 1. **条件变量(Condition Variables)**:相当于是一种基于互斥锁的线程间同步机制,通常情况下是和锁一起使用的。主线程可以使用条件变量来等待特定的条件达成,而其他线程则可以通过发送信号来通知主线程条件的改变。一旦条件满足,主线程就会被唤醒并继续执行。 2. **事件(Events)**:事件也是一种用于线程间同步的通信机制,通常用于向其他线程发出信号以触发特定事件的发生,然后执行事件的回调函数。主线程可以等待某个事件的发生,其他线程可以通过发送信号来触发该事件。 3. **信号量(Semaphores)**: 信号量维护一个计数器,表示可用资源的数量。线程可以通过请求(等待)和释放(发信号)操作来获取和释放资源。当计数器为正时,线程可以继续执行,否则线程会被阻塞直到资源可用。就是我们常说的那个操作系统pv操作,经常用来解决“生产者消费者的问腿” 伪代码: ```cpp 待补充。。。 ``` 4. **消息队列(Message Queues)**:主线程可以通过发送消息到队列中来通知其他线程,而其他线程则可以从队列中接收消息并执行相应的操作。 # 补充: ## 1)C语言链表 ```c #include #include typedef struct Node{ int data; struct Node * prior; struct Node * next; } Node; int main () { /*** * c语言像数组的链表 */ /* Node* list = (Node *)malloc(sizeof(Node) *5); if (list == NULL) { // 内存分配失败的处理 fprintf(stderr, "Memory allocation failed.\n"); return 1; } // 初始化链表节点 for (int i = 0; i < 5; ++i) { list[i].data = i; // 数据域初始化为索引值或其他你选择的值 if (i == 0) { // 第一个节点的前驱指向 NULL list[i].prior = NULL; } else { // 其他节点的前驱指向前一个节点 list[i].prior = &list[i - 1]; } if (i == 4) { // 最后一个节点的后继指向 NULL list[i].next = NULL; } else { // 其他节点的后继指向下一个节点 list[i].next = &list[i + 1]; } } // ... 使用链表 for(int i = 0; i< 5;i++) { int num = list[i].data; printf("%d\n",num); } // 释放分配的内存 free(list); */ /******c语言链表********/ Node *n1 = (Node *)malloc(sizeof(Node)); Node *n2 = (Node *)malloc(sizeof(Node)); Node *n3 = (Node *)malloc(sizeof(Node)); n1->data = 1; n1->prior = NULL; n1->next = n2; n2->data = 2; n2->prior = n1; n2->next = n3; n3->data = 3; n3->prior = n2; n3->next = NULL; for(Node * node = n1; node != NULL;node = node->next) { int num = node->data; printf("[%d]\n",num); } return 0; } 输出: [1] [2] [3] ``` ## 2)链表-智能指针版 ```cpp #include #include //智能指针 using namespace std; struct Node{ int data; weak_ptr prior; weak_ptr next; }; int main() { shared_ptr n1 = make_shared(); shared_ptr n2 = make_shared(); shared_ptr n3 = make_shared(); n1->data = 1; n1->prior.reset();//智能指针设置为空应该用reset函数,而不是n1->prior = nullptr; n1->next = n2; n2->data = 2; n2->prior = n1; n2->next = n3; n3->data = 3; n3->prior = n2; n3->next.reset();//n3->next = nullptr; for(shared_ptr node = n1 ; node != nullptr ; node = node -> next.lock()){//你需要先用lock将weak_ptr提升为shared_ptr才能够进行赋值 int num = node ->data; printf("[%d]\n",num); } //销毁智能指针(你也可以不销毁,因为智能指针自己非常懂事) n1.reset(); n2.reset(); n3.reset(); return 0; } 输出: [1] [2] [3] ``` ## 3)链表-普通cpp版 ```cpp #include using namespace std; struct Node{ int data; Node* prior; Node* next; }; int main() { Node * n1 = new Node; Node * n2 = new Node; Node * n3 = new Node; n1->data = 1; n1->prior = nullptr; n1->next = n2; n2->data = 2; n2->prior = n1; n2->next = n3; n3->data = 3; n3->prior = n2; n3->next = nullptr; for(Node * node = n1 ; node != nullptr ; node = node->next){ int num = node ->data; printf("[%d]\n",num); } //销毁智能指针 delete n1; delete n2; delete n3; return 0; } 输出: [1] [2] [3] ```