编辑
2024-02-04
code
00
请注意,本文编写于 228 天前,最后修改于 205 天前,其中某些信息可能已经过时。

目录

谁能分享一下c++工程师面试经验?
C++基础知识
(1) 谈谈你对C和C++的编程差异理解
(2) static关键字在C语言和C++中各自有哪些不同用法?
(3)union是什么,有什么用?
(4)volatile关键字是做什么用的?
(5)函数调用过程在汇编层面如何进行?
(6)面向对象有哪些基本特性?
(7)多态是如何实现的?编程实现一下
(8)编程实现一个单例模式
(9)一个对象只有一个int型成员变量,sizeof的大小是多少
(10)一个对象有一个int型和一个char型成员变量,sizeof的大小是多少?
(11)一个对象只有一个int型成员变量和一个虚函数,sizeof的大小是多少?
(12)智能指针的原理,weak\_ptr是做什么用的?
(13)vector容量满了会发生什么?
(14)map和unordered\_map有什么区别?各自如何实现?
(15)右值引用是什么,move是为了解决什么问题?
(16)构造函数能不能抛出异常?析构函数呢?
(17)C++中哪几种类型转换,区别是什么?
(18)从源代码到可执行程序,中间的过程是什么样的?
数据结构与算法部分
(1)二叉树的四种遍历方式
(2)哈希表工作原理,如何解决哈希冲突?
(3)编程实现一个二分查找
(4)常用的排序算法,各自的的时间复杂度是什么?
(5) 1-2走台阶问题,递归和动态规划两种解题方法
(6)如何设计一个算法,快速判断一个IP地址有没有在系统中出现过?
(7)设计一个线程安全的队列
操作系统
(1)进程和线程的区别?
(2)进程地址空间里面有什么东西?
(3)线程的栈里面有哪些东西?
(4)谈一谈虚拟内存机制
(5)虚拟地址的翻译过程是怎样的?
(6)fork的原理是什么?
(7)进程间通信有哪些方式?
(8)共享内存的原理是什么?
(9)原子操作的原理是什么?
(10)I/O多路复用有哪些模型?
(11)epoll高性能的原因有哪些?
(12)谈一谈signal机制
(13)什么是系统调用,执行系统调用的过程是什么?
(14)写时拷贝是什么,底层实现原理?
计算机网络
(1)四层模型是哪四层,各自负责什么功能?
(2)ping命令是什么原理?
3、traceroute是什么原理?4、什么是ARP欺骗?5、集线器、交换机、路由器的区别?6、什么是MTU?为什么是这个大小?7、TCP三次握手和四次挥手机制
8、TCP的第三次握手可以携带数据吗?
9、TCP可靠性由哪些机制保证?
10、超时重传如何进行?11、DNS解析过程如何进行?12、HTTP中有哪些Method,POST和PUT什么区别?13、HTTP1.0和1.1有什么区别?14、HTTPS安全性的原理15、什么是反向代理?nginx负载均衡有哪些策略?16、从输入网址到网页内容展示出来,发生了哪些事?

谁能分享一下c++工程师面试经验?

Author: MrGood

Link: [https://www.zhihu.com/question/423364880/answer/3367011077]

v2-19bfdd324713254fa61e8de4f678e98e_r.jpg

C++基础知识

(1) 谈谈你对C和C++的编程差异理解

  1. 面向对象:最大的区别在于C++支持面向对象编程,而C语言不支持。在C++中,你可以使用类和对象进行编程,它也提供了封装、继承和多态等概念。而C语言是一种过程化的语言,主要关注函数和过程。
  2. 函数重载:在C++中,可以使用相同的函数名来定义函数,只要参数类型或参数个数不同就可以。这被称为函数重载。但在C语言中,不能有两个名称相同的函数。
  3. 异常处理:C++提供了异常处理,可以捕获和处理运行时的错误。而在C语言中,没有内置的异常处理机制。
  4. 命名空间:C++引入了命名空间的概念,可以避免名称冲突。C语言没有命名空间的概念。
  5. 标准模板库(STL):C++还提供了一套强大的模板类和函数,被称为标准模板库(STL),可以用来处理常见的数据结构和算法,如链表、队列、栈、排序等。C语言没有类似的库。
  6. 内存管理:C++和C都允许直接操作内存,但C++提供了更安全的方式来管理内存,如new和delete操作符,以及构造函数和析构函数。
  7. 兼容性:C++是C的一个超集,大多数C程序可以在C++编译器中编译和运行(除了一些不兼容的部分)。这意味着如果你了解C++,那么你也能理解C语言。

(2) static关键字在C语言和C++中各自有哪些不同用法?

在C和C++中,static关键字主要有三种用法,它们在两种语言中的作用是相同的。

  1. 静态局部变量:在函数内部使用static关键字声明的变量被称为静态局部变量。不同于普通的局部变量,静态局部变量不会在函数调用结束时被销毁,而会保留其值直到下次调用。这意味着,如果你在函数内部定义了一个static变量,那么在函数的连续调用之间,这个变量的值将被保留。
  2. 静态全局变量:在函数外部使用static关键字声明的变量被称为静态全局变量。这些变量在整个程序执行期间都存在,但它们只能在定义它们的文件中被访问,不能被其他文件访问。这提供了一种在文件间隐藏实现细节的方式。
  3. 静态函数:在函数前使用static关键字可以使函数变为静态函数。静态函数与静态全局变量类似,只能在定义它们的文件中被访问。这也提供了一种在文件间隐藏实现细节的方式。

另外,C++还有一种特殊的用法,那就是静态成员。在类中,可以使用static关键字来声明静态成员变量和静态成员函数。静态成员变量属于类本身,不管创建多少个类的对象,静态成员变量只有一份。静态成员函数也是属于类本身的,它们不能访问类的非静态成员变量,但可以访问静态成员变量。

(3)union是什么,有什么用?

union它允许在相同的内存位置存储不同的数据类型和长度的数据。你可以把union理解为一个可以存储不同类型数据的容器。

union的主要用途是节省内存,特别是当你有一些变量不会同时使用时。因为union的所有成员都共享同一块内存,所以它的大小等于其最大的成员。例如,如果你有一个union,包含一个int类型和一个double类型,那么这个union的大小就等于double的大小,因为double的大小大于int。

此外,union还常常和结构体一起使用,来实现一种称为"标记联合"的数据结构。在标记联合中,你可以使用一个结构体成员作为"标记",来表示union中哪个成员当前正在被使用。

cpp
typedef enum { INT, FLOAT, STRING } Type; typedef struct { Type type; // 标记,用来表示union中哪个成员正在被使用 union { int intValue; float floatValue; char \*stringValue; } value; // union,可以存储int、float或char \*类型的数据 } Variant;

在这个例子中,Variant类型的变量可以存储一个int、一个float或一个字符串。你可以通过type成员来确定union中哪个成员正在被使用,然后通过value成员来访问该数据。

(4)volatile关键字是做什么用的?

volatile关键字在C和C++语言中被用来指示编译器,被其修饰的变量可能在外部程序或者硬件,或者其他未知的因素中改变,即使在程序内部看起来没有明显的代码修改它。这样,编译器在优化代码的时候,就不会对这样的变量进行某些假设,并且每次引用这个变量的时候都会直接从它所在的内存读取,而不是使用可能已经保存在寄存器中的备份。

volatile关键字常用在多线程编程和嵌入式系统编程中。在多线程编程中,一个线程可能修改一个变量,同时另一个线程也在读取或写入这个变量,这时就需要使用volatile关键字来确保变量的值的正确性。在嵌入式系统编程中,volatile关键字常用于硬件寄存器的访问,因为硬件寄存器的值可能随时改变,与程序的运行无关。

(5)函数调用过程在汇编层面如何进行?

在汇编层面,函数调用过程大致包括以下步骤:

  1. 参数传递:一般来说,函数的参数是通过堆栈来传递的。首先,调用函数的代码将参数压入堆栈。这通常是通过“push”指令完成的。
  2. 调用函数:然后,调用函数的代码执行“call”指令。这将当前的程序计数器(也就是下一条要执行的指令的地址)压入堆栈,然后将程序计数器设置为被调用函数的起始地址。
  3. 执行函数:被调用函数开始执行。它可能会将一些寄存器的值压入堆栈,以便在函数结束时可以恢复这些寄存器的值。然后,它会执行函数的代码,这可能包括进一步的函数调用。
  4. 返回值处理:如果函数有返回值,通常会通过特定的寄存器(如EAX在x86架构中)来传递。
  5. 函数返回:函数执行完毕后,执行“ret”指令。这将从堆栈中弹出一个值,并将程序计数器设置为这个值,从而返回到调用函数的代码中。

(6)面向对象有哪些基本特性?

面向对象编程(Object-Oriented Programming, OOP)有以下几个基本特性:

  1. 封装(Encapsulation):封装是指将数据(属性)和操作数据的方法(行为)打包在一起形成一个“对象”,并对对象的内部细节进行隐藏,只对外部暴露必要的接口。这种特性可以提高代码的安全性和稳定性。
  2. 继承(Inheritance):继承是指一个类(子类)可以继承另一个类(父类)的属性和方法。这种特性可以实现代码的复用,同时可以建立类之间的层次关系。
  3. 多态(Polymorphism):多态是指一个接口可以有多种实现方式,或者一个类实例在不同情境下表现出不同的状态和行为。这种特性可以提高代码的可扩展性和灵活性。
  4. 抽象(Abstraction):抽象是指将复杂的系统抽象为更简单的模型。通过创建抽象类或接口,可以定义对象的通用结构。抽象可以简化复杂系统的设计和实现。

这四个特性是面向对象编程的基石,通过它们可以更好地组织和管理代码,提高代码的可复用性和可维护性。

(7)多态是如何实现的?编程实现一下

可以分为静态多态和动态多态两种。

静态多态:

java
public class Polymorphism { // 方法重载(Overload) public int add(int a, int b) { return a + b; } public int add(int a, int b, int c) { return a + b + c; } public static void main(String[] args) { Polymorphism polymorphism = new Polymorphism(); System.out.println(polymorphism.add(1, 2)); // 输出3 System.out.println(polymorphism.add(1, 2, 3)); // 输出6 } }

在这个例子中,add方法被重载了,它根据参数列表的不同来进行不同的操作,这就是静态多态。

动态多态:

cpp
#include<iostream> using namespace std; class Base { public: virtual void func() { cout << "Base function.\n"; } }; class Derived : public Base { public: void func() override { cout << "Derived function.\n"; } }; int main() { Base\* base = new Derived(); base->func(); // 输出:Derived function. delete base; return 0; }

以上代码中,Base类中的func函数被声明为虚函数,Derived类中重写了这个函数。在main函数中,我们创建了一个Derived类的对象,但是用一个Base类的指针来引用它,并调用func函数,此时调用的是Derived类的版本,这就实现了动态多态。

(8)编程实现一个单例模式

cpp
#include <mutex> class Singleton { private: static Singleton\* instance; static std::mutex mtx; Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; public: static Singleton\* getInstance() { if (instance == nullptr) { mtx.lock(); if (instance == nullptr) { instance = new Singleton(); } mtx.unlock(); } return instance; } }; // 初始化静态成员变量 Singleton\* Singleton::instance = nullptr; std::mutex Singleton::mtx; int main() { // 获取Singleton实例 Singleton\* s1 = Singleton::getInstance(); Singleton\* s2 = Singleton::getInstance(); // 检查两个实例是否相同 if (s1 == s2) { std::cout << "Singleton works, both instances are the same." << std::endl; } else { std::cout << "Singleton failed, instances are not the same." << std::endl; } return 0; }

具体见:

如何高效的实现 C++单例模式? - MrGood的回答 - 知乎

https://www.zhihu.com/answer/3363520861

(9)一个对象只有一个int型成员变量,sizeof的大小是多少

如果一个对象只有一个int型成员变量,那么sizeof该对象通常会返回4。但是,这只是一种常见的情况,实际的结果可能会因编译器的内存对齐规则和填充规则的不同而不同。

(10)一个对象有一个int型和一个char型成员变量,sizeof的大小是多少?

一个int型变量占用4个字节,一个char型变量占用1个字节。然而,由于编译器的内存对齐规则,sizeof一个包含一个int型和一个char型成员变量的对象通常会是8个字节。

(11)一个对象只有一个int型成员变量和一个虚函数,sizeof的大小是多少?

一个虚函数在对象中并不直接占用存储空间,但是如果一个类中有虚函数,那么编译器会为该类的对象生成一个虚函数表(vtable),并在每个对象中添加一个指向这个虚函数表的指针。因此,如果一个对象只有一个int型成员变量和一个虚函数,那么它的大小应该是int的大小加上一个指针的大小。

对于32位系统,一个指针通常占用4个字节,对于64位系统,一个指针通常占用8个字节。所以,在32位系统中,sizeof这个对象应该是8个字节(4个字节的int加上4个字节的指针),在64位系统中,sizeof这个对象应该是12个字节(4个字节的int加上8个字节的指针)。

(12)智能指针的原理,weak_ptr是做什么用的?

智能指针是通过对动态分配的对象进行引用计数来实现的。每当有一个新的智能指针指向一个对象时,这个对象的引用计数就会增加;当智能指针停止指向一个对象时,这个对象的引用计数就会减少。当对象的引用计数减到0时,就表示没有任何智能指针再指向这个对象,于是对象就会被自动删除。

C++标准库中包含了几种智能指针,比如unique_ptr、shared_ptr和weak_ptr等。unique_ptr是一种独占所有权的智能指针,一个对象只能由一个unique_ptr拥有。shared_ptr则允许多个智能指针共享所有权。weak_ptr则是一种不会增加引用计数的智能指针,主要用于解决循环引用的问题。例如,如果两个对象通过智能指针相互引用,那么它们的引用计数永远不会变为0,因此它们永远不会被删除,从而导致内存泄漏。通过使用weak_ptr,可以打破这种循环引用,从而避免内存泄漏。

(13)vector容量满了会发生什么?

当std::vector的容量满了,如果还尝试再向其添加元素,vector会自动重新分配更大的内存空间来存储更多的元素。

具体来说,当vector的容量不足以容纳新的元素时,vector会申请一个新的、更大的内存块,新的内存块的大小通常是原来容量的两倍(这个倍数可能因具体实现而不同)。然后,vector会将原来的元素复制(或移动)到新的内存块中,然后释放原来的内存块。

(14)map和unordered_map有什么区别?各自如何实现?

map和unordered_map都是C++标准库中的关联容器,用于存储键值对。但是它们的内部实现和性能特性有所不同。

map:

  • map内部实现了一个红黑树(一种自平衡二叉查找树),元素按键排序存储,因此迭代器遍历map元素时会得到一个有序序列。
  • map的查找、插入、删除操作的时间复杂度都是O(log n)。
  • map的空间复杂度是O(n)。

unordered_map:

  • unordered_map内部实现了一个哈希表,元素不按键排序,因此迭代器遍历unordered_map元素时得到的是一个无序序列。
  • 由于哈希表的特性,unordered_map的查找、插入、删除操作的平均时间复杂度都是O(1),但在最坏情况下(所有元素都哈希到同一个桶中)可能退化到O(n)。
  • unordered_map的空间复杂度也是O(n),但由于需要维护哈希表,其实际占用的内存可能会比map更多。

(15)右值引用是什么,move是为了解决什么问题?

右值引用是C++11引入的一个新特性,主要用于支持移动语义(Move Semantics)和完美转发(Perfect Forwarding)。
右值引用可以绑定到一个将要销毁的对象(也就是右值),这样我们就可以安全地获取对象的资源,而不用担心这个对象在之后会被使用。在C++中,我们可以用&&来声明一个右值引用。
例如,我们可以定义一个移动构造函数,它接受一个右值引用参数,并从这个参数中“偷”取资源。这样,我们就可以避免资源的复制,从而提高性能。这就是所谓的移动语义。
move函数就是为了支持这种移动语义。当我们调用std::move时,可以把一个左值转换为右值,从而可以被移动而不是被复制。
例如,假设我们有一个大型的vector对象,我们想把它传递给另一个vector。通常情况下,这会导致整个vector的复制,这可能很慢。但是,如果我们使用std::move,就可以避免这种复制,直接把原vector的内存移交给新vector,这通常会快得多。
总的来说,右值引用和move是为了优化性能,特别是在涉及大型对象或者“昂贵”的操作(如内存分配)时。

(16)构造函数能不能抛出异常?析构函数呢?

构造函数可以抛出异常。在创建对象的过程中,如果遇到任何问题(例如,无法分配所需的内存或无法打开所需的文件),就可以抛出异常。

然而,析构函数不应抛出异常。如果一个析构函数可能抛出异常,这可能导致两个问题:

  1. 如果在处理另一个异常时析构函数被调用(例如,在堆栈展开过程中),并且析构函数也抛出异常,那么C++会立即调用std::terminate(),导致程序突然终止。
  2. 如果在删除动态分配的对象时析构函数抛出异常,那么这个异常如果没有被捕获,就会导致内存泄漏。

因此,析构函数应设计为不抛出异常,也就是说,应在析构函数的声明后面加上noexcept关键字:

class MyClass { public: ~MyClass() noexcept { // 释放资源,但不抛出异常 } };

(17)C++中哪几种类型转换,区别是什么?

  1. static_cast:这是最常用的类型转换运算符。它可以用于基本数据类型之间的转换,也可以用于类之间的转换,但是对于类之间的转换,它们之间必须有继承关系。
  2. dynamic_cast:这个运算符用于动态类型转换。它只能用于类之间的转换,而且这些类之间必须有继承关系。不同于static_castdynamic_cast在运行时检查转换是否有效。如果转换无效,它将返回空指针。
  3. const_cast:这个运算符用于改变表达式的常量属性,比如,可以用它来去掉const修饰。
  4. reinterpret_cast:这个运算符用于进行低级别的类型转换。它可以将任何类型的指针转换为任何其他类型的指针,也可以将任何整数类型转换为任何指针类型,以及反向转换。reinterpret_cast运算符很容易滥用,一般在必要的时候才使用。

(18)从源代码到可执行程序,中间的过程是什么样的?

  1. 预处理:这是编译过程的第一步。预处理器(例如 C++ 的预处理器)接受源代码,并执行预处理指令。这些指令通常包括 #include#define 和条件编译指令。
  2. 编译:在这一步中,编译器将预处理后的源代码转换为汇编语言。
  3. 汇编:汇编器将汇编代码转换为机器语言,输出的结果是一个目标文件。这个目标文件包含了机器语言代码,但还不能被操作系统执行,因为它还没有被链接。
  4. 链接:链接器的任务是将一个或多个目标文件(以及可能的一些库文件)链接在一起,生成一个可执行文件。在这个过程中,链接器解决了目标文件之间的符号引用关系,例如一个目标文件中调用了另一个目标文件中的函数,链接器将这种引用关系链接在一起。

数据结构与算法部分

(1)二叉树的四种遍历方式

  1. 前序遍历:首先访问根节点,然后访问左子树,最后访问右子树。
  2. 中序遍历:首先访问左子树,然后访问根节点,最后访问右子树。
  3. 后序遍历:首先访问左子树,然后访问右子树,最后访问根节点。
  4. 层序遍历:也被称为广度优先遍历,先访问根节点,然后访问所有的子节点,然后再访问子节点的子节点,依次类推,直到所有节点都被访问到。
struct TreeNode { int val; TreeNode\* left; TreeNode\* right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; void preorder(TreeNode\* root) { if (root != NULL) { cout << root->val << " "; preorder(root->left); preorder(root->right); } } void inorder(TreeNode\* root) { if (root != NULL) { inorder(root->left); cout << root->val << " "; inorder(root->right); } } void postorder(TreeNode\* root) { if (root != NULL) { postorder(root->left); postorder(root->right); cout << root->val << " "; } } void levelorder(TreeNode\* root) { if (root == NULL) return; queue<TreeNode\*> q; q.push(root); while (!q.empty()) { TreeNode\* node = q.front(); q.pop(); cout << node->val << " "; if (node->left != NULL) q.push(node->left); if (node->right != NULL) q.push(node->right); } } int main() { TreeNode\* root = new TreeNode(1); root->left = new TreeNode(2); root->right = new TreeNode(3); cout << "Preorder: "; preorder(root); cout << "\nInorder: "; inorder(root); cout << "\nPostorder: "; postorder(root); cout << "\nLevel order: "; levelorder(root); return 0; }

(2)哈希表工作原理,如何解决哈希冲突?

哈希表是一种数据结构,它使用哈希函数将键(key)转换为数组的索引来访问数据。这种方法使得无论数据的大小如何,访问时间都保持不变。

哈希表的工作原理:

  1. 首先,哈希函数将键转换为一个整数(或哈希码)。
  2. 然后,这个哈希码被用来找到存储值的位置。通常,哈希码会被模除以数组的大小,以获取数组的索引。
  3. 如果两个键的哈希码相同,或者两个不同的哈希码被映射到同一个索引,就会出现哈希冲突。

解决哈希冲突的几种方法:

  1. 链接法(链地址法):在哈希表中,每个索引位置都连接一个链表。当哈希冲突发生时,冲突的元素将被添加到相应索引位置的链表中。
  2. 开放定址法:当哈希冲突发生时,寻找下一个可用的地址位置来存储冲突的值。这种方法的几种实现方式包括线性探测(寻找下一个可用的位置)、二次探测(寻找下一个可用的位置的平方)、双重哈希(使用第二个哈希函数来确定新的位置)。
  3. 再哈希法:在冲突时使用另一个哈希函数。
  4. 建立一个大的哈希表:可以减少哈希冲突,但是可能会浪费内存空间。
  5. Coalesced Hashing(合并哈希):它是链地址法和开放定址法的混合形式。它像开放定址法那样存储元素,但是当冲突发生时,它会在哈希表的末尾添加一个新的元素,并用链表连接冲突的元素。

(3)编程实现一个二分查找

int binarySearch(int arr[], int left, int right, int x) { if (right >= left) { int mid = left + (right - left) / 2; // 如果元素存在于中间 if (arr[mid] == x) return mid; // 如果元素小于 mid,那么它只能存在于左子数组中 if (arr[mid] > x) return binarySearch(arr, left, mid - 1, x); // 否则元素可以只存在于右子数组中 return binarySearch(arr, mid + 1, right, x); } // 元素不存在于数组中 return -1; } int main(void) { int arr[] = {2, 3, 4, 10, 40}; int x = 10; int n = sizeof(arr) / sizeof(arr[0]); int result = binarySearch(arr, 0, n - 1, x); (result == -1) ? cout << "元素不在数组中" : cout << "元素在数组中的索引为 " << result; return 0; }

(4)常用的排序算法,各自的的时间复杂度是什么?

  1. 冒泡排序(Bubble Sort):最坏情况下的时间复杂度为O(n²),最好情况下为O(n),平均情况下为O(n²)。
  2. 选择排序(Selection Sort):无论最好、最坏还是平均情况,时间复杂度均为O(n²)。
  3. 插入排序(Insertion Sort):最坏情况下的时间复杂度为O(n²),最好情况下为O(n),平均情况下为O(n²)。
  4. 快速排序(Quick Sort):最坏情况下的时间复杂度为O(n²),但在平均情况下为O(n log n),最好情况下也为O(n log n)。
  5. 归并排序(Merge Sort):无论最好、最坏还是平均情况,时间复杂度均为O(n log n)。
  6. 堆排序(Heap Sort):无论最好、最坏还是平均情况,时间复杂度均为O(n log n)。
  7. 希尔排序(Shell’s Sort):最坏情况下的时间复杂度为O(n²),但在平均情况下通常优于O(n²),最好情况下为O(n log n)。
  8. 计数排序(Counting Sort):时间复杂度为O(n + k),其中k是输入的范围。
  9. 桶排序(Bucket Sort):平均情况下的时间复杂度为O(n + k),最好情况下为O(n + k),最坏情况下为O(n²)。
  10. 基数排序(Radix Sort):时间复杂度为O(nk),其中k是最大数的位数。

(5) 1-2走台阶问题,递归和动态规划两种解题方法

int climbStairs(int n) { if (n <= 2) { return n; } return climbStairs(n-1) + climbStairs(n-2); } int climbStairs(int n) { if (n <= 2) { return n; } int dp[n+1]; dp[1] = 1; dp[2] = 2; for (int i = 3; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; }

(6)如何设计一个算法,快速判断一个IP地址有没有在系统中出现过?

可以使用哈希表(HashMap)或者字典(Dictionary)。哈希表是一种数据结构,它支持快速插入和查找操作。在这个问题中,我们可以将IP地址作为键,出现的次数作为值存储在哈希表中。

以下是一个简单的实现方式:

#include <unordered\_map> #include <string> class IPAddressChecker { public: bool hasSeen(std::string ip) { if (ipMap.count(ip) > 0) { return true; } else { ipMap[ip] = 1; return false; } } private: std::unordered\_map<std::string, int> ipMap; }; int main() { IPAddressChecker checker; checker.hasSeen("192.168.0.1"); // 返回 false,因为这个IP地址之前没有出现过 checker.hasSeen("192.168.0.1"); // 返回 true,因为这个IP地址已经出现过了 return 0; }

(7)设计一个线程安全的队列

#include <queue> #include <mutex> #include <condition\_variable> template <typename T> class ThreadSafeQueue { public: ThreadSafeQueue() {} void push(T value) { std::lock\_guard<std::mutex> lock(mtx); data.push(value); cv.notify\_one(); } bool try\_pop(T& value) { std::lock\_guard<std::mutex> lock(mtx); if (data.empty()) { return false; } value = data.front(); data.pop(); return true; } void wait\_and\_pop(T& value) { std::unique\_lock<std::mutex> lock(mtx); cv.wait(lock, [this]{ return !data.empty(); }); value = data.front(); data.pop(); } private: std::queue<T> data; std::mutex mtx; std::condition\_variable cv; };

这个队列的实现满足线程安全:多个线程可以同时对这个队列进行入队和出队操作,而不会出现数据竞争的问题。

push方法将一个元素添加到队列中,并通知一个正在等待的线程。

try_pop尝试从队列中移除一个元素,如果队列为空,则返回false。

wait_and_pop方法将阻塞当前线程,直到队列中有元素为止。它会移除队列中的一个元素,并将其返回。

操作系统

(1)进程和线程的区别?

进程和线程都是一个时间段的描述,是对系统进行资源分配和调度的基本单位,是多进程和多线程技术的基础。它们之间的主要差别在于:

  1. 独立性:进程是系统资源(CPU,内存等)分配的最小单位,线程是CPU调度的最小单位。进程可以拥有完全独立的资源,而线程只能拥有一些基本资源(如程序计数器,一组寄存器,栈),但必须依赖于所属进程的其他资源(如内存空间,文件等)。
  2. 通信:进程间通信(IPC)需要操作系统提供IPC机制,而线程间可以直接通通过程通信。
  3. 切换:由于同一进程的线程共享内存和文件资源,因此线程间切换的开销小于进程切换。
  4. 调度:由于进程是资源拥有的单位,频繁的调度会影响系统的性能,从一个进程到另一个进程需要很多时间,所以系统不能频繁的进行进程调度。线程是CPU调度的单位,线程切换的时间要比进程切换的时间短,所以系统可以频繁的进行线程调度。
  5. 分配:每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立的执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  6. 生命周期:进程的生命周期要比线程的生命周期长。
  7. 系统开销:由于创建或撤消进程时,系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销

(2)进程地址空间里面有什么东西?

一个进程的地址空间通常包含以下几个部分:

  1. 文本段(Text Segment):这个段包含了程序的代码。这个段通常是只读的,防止程序意外地修改其指令。
  2. 数据段(Data Segment):这个段包含了程序使用的全局变量。程序启动时,由系统根据程序的大小需求来分配空间。
  3. 堆(Heap):堆是用于动态内存分配的,如程序运行中创建的每个对象和实例。当你在程序中创建一个新的对象实例时,新的内存就会被动态地添加到堆上。
  4. 栈(Stack):这个段包含了局部变量和函数调用的信息。每次函数声明周期结束后,都会有一个栈帧被压入栈中。栈帧包含了函数的局部变量,返回地址和调用的参数。
  5. 文件映射区:用于文件映射,主要是为了做共享内存,还有动态库等。
  6. 命令行参数和环境变量:这部分存放了从shell传递给程序的命令行参数和环境变量。


转载图,侵权立刻删除

数据段中静态存储区(Static Storage Area)和常量区(Constant Area)是计算机内存中的两个特定区域,而BSS(Block Started by Symbol)则是静态存储区的一部分。

具体来说:

  1. 静态存储区(Static Storage Area):

静态存储区是在程序运行期间一直存在的存储区域,用于存储静态变量、全局变量和静态对象。这些变量和对象在程序执行期间都不会销毁,而是在程序生命周期内保持不变。静态存储区的内存空间在程序加载时被分配,并在程序退出时才会释放。

  1. 常量区(Constant Area):

常量区也被称为只读数据区或文字常量区,用于存储常量值和字符串字面量。常量区的数据在程序执行期间也是不变的,因此只能读取,而不能修改。常量区的数据在程序加载时被分配,并在程序退出时才会释放。常量区中的数据通常是只读的,无法进行修改。

  1. BSS(Block Started by Symbol):

BSS是静态存储区中的一部分,用于存储未初始化的全局变量和静态变量。在程序加载时,BSS段被设置为零或空值,不需要为每个变量分配具体的空间。只有在程序运行时实际分配内存并初始化这些变量。

需要注意的是,具体的内存布局和存储区域的划分可能会因编译器、操作系统和平台的不同而有所差异。因此,在某些情况下,BSS可能被合并到静态存储区中。

总结来说,静态存储区是存储静态变量、全局变量和静态对象的区域,其中BSS是静态存储区的一部分,用于存储未初始化的全局变量和静态变量。常量区是存储常量值和字符串字面量的只读区域。

(3)线程的栈里面有哪些东西?

  1. 局部变量:每个线程的栈都会存放其自己的局部变量。这些变量只在当前线程的上下文中存在,而其他线程无法访问。
  2. 函数调用信息:当一个函数被调用时,会在栈上创建一个栈帧来存储该函数的信息,包括返回地址、传递给函数的参数、函数内部的局部变量以及函数返回值等。
  3. 返回地址:函数调用完成后,线程需要知道接下来从哪里开始执行,这个地址就保存在栈中。
  4. 临时数据:在函数调用过程中,可能会有一些中间计算结果或者临时变量需要存储,这些也会保存在栈上。
  5. 寄存器状态:当发生上下文切换时,当前线程的CPU寄存器状态会被保存到栈上,以便在切换回来时能恢复执行环境。
  6. 异常处理信息:用于存放与异常处理相关的信息。

(4)谈一谈虚拟内存机制

虚拟内存是一种内存管理技术,主要用来使每个进程仿佛拥有连续的、完整的地址空间,从而使得程序能够更加容易地管理内存。它的工作原理是,将程序的地址空间分割成固定大小的块,称为"页面"(page),同时,将物理内存也分割成同样大小的块,称为"页框"(page frame)。

当程序需要使用一部分内存时,操作系统会将相应的"页面"映射到"页框"上。这个映射关系会被保存在一个叫做"页表"的数据结构中。当程序访问自己的地址空间时,硬件会自动查找页表,找到对应的物理内存地址,然后从那里读取或写入数据。

虚拟内存有几个主要的好处:

  1. 它让每个进程都有自己的独立地址空间,因此进程之间不会互相干扰,提高了系统的稳定性。
  2. 它使得程序不必关心物理内存的实际情况,只需要操作自己的地址空间就行,简化了程序的设计。
  3. 它可以让程序使用的内存空间超过物理内存的大小。因为不需要的页面可以被暂时地保存到磁盘上,等到需要的时候再加载到内存中。
  4. 它使得内存的利用率更高。因为不同进程的页面可以映射到同一个页框上,例如,所有进程共享的库函数就可以只在内存中保存一份。

(5)虚拟地址的翻译过程是怎样的?

虚拟地址的翻译过程是通过硬件和操作系统中的内存管理单元(MMU)完成的。以下是一个简化的虚拟地址翻译过程:

  1. CPU发出虚拟地址。这个地址被分为两部分:页号和页内偏移。
  2. MMU使用页号作为索引,查询页表。页表是操作系统维护的一个数据结构,保存了所有页到页框的映射关系。
  3. 在页表中,每一个条目都包含了页框的物理地址和一些控制位,例如是否可读、可写等。
  4. MMU从页表中读取页框地址,然后将它和页内偏移组合起来,形成物理地址。
  5. 最后,物理地址被送到内存控制器,从内存中读取或写入数据。

如果在页表中找不到对应的条目,就会产生一个页面错误(page fault)。这通常意味着对应的页面不在内存中,而在磁盘上。此时,操作系统需要将页面从磁盘中读取到内存中,然后更新页表,最后重新进行地址翻译。这个过程被称为页面置换(page replacement)。

(6)fork的原理是什么?

当一个进程调用fork()函数时,操作系统会创建一个新的进程。新进程是原进程的一个副本,包括代码、堆、栈、文件描述符、环境变量等等。

  1. fork()调用完成后,两个进程(父进程和子进程)会各自独立运行。它们有各自独立的地址空间,互不影响。但是,这两个进程会共享同样的代码段。
  2. 在子进程中,fork()返回0;在父进程中,fork()返回子进程的PID。这样,父进程和子进程可以通过检查fork()的返回值来确定自己的角色。
  3. 对于子进程,它继承了父进程的很多属性,例如打开的文件描述符、信号处理方式等。但是,有一些属性是不同的,例如PID、父进程ID、处理器使用时间等。

虽然在fork()后,父进程和子进程有各自独立的地址空间,但是大部分现代操作系统并不会立即复制所有的内存页面,而是使用一种称为**写时复制(copy-on-write)**的优化技术。具体来说,初始时,父进程和子进程共享同样的内存页面,这些页面被标记为只读。当某个进程试图修改一个页面时,操作系统会创建这个页面的一个副本,供这个进程使用。这样,只有在必要时才会复制内存页面,可以节省内存和提高性能。

(7)进程间通信有哪些方式?

  1. 管道(Pipes):这是最简单的IPC方法,允许一个进程向另一个进程传输数据。数据流是单向的,通常用于父子进程间的通信。
    优点:实现简单,适用于父子进程间的通信。
    缺点:只能在有共同祖先的进程间使用,数据流是单向的,没有同步机制。
  2. 命名管道(Named Pipes):也称为FIFO,与管道类似,但它们有一个名称,因此可以用于任何两个进程之间的通信。
    优点:可以实现任何两个进程间的通信。
    缺点:数据流仍然是单向的。
  3. 消息队列(Message Queues):消息队列允许进程将消息发送到队列中,其他进程可以从队列中读取这些消息。
    优点:可以实现任何两个进程间的通信,有较好的同步机制,数据传输形式较为灵活。
    缺点:队列的大小可能受到系统资源限制。
  4. 信号(Signals):信号是一种异步的通知机制,用于通知进程发生了某个事件。
    优点:实现简单,适用于异步事件的通知。
    缺点:信号类型有限,数据传输量小,不适合复杂的通信需求。
  5. 共享内存(Shared Memory):在内存中创建一个共享区域,多个进程可以访问这个区域。
    优点:数据传输速度快,无需内核参与,可以传输大量数据。
    缺点:需要手动解决同步和并发问题。
  6. 套接字(Sockets):套接字可以在不同的计算机之间进行通信,是Internet上最常用的IPC方法。
    优点:可以跨机器通信,使用灵活,可传输任意数据。
    缺点:实现较为复杂,网络环境不稳定可能影响通信效果。
  7. 信号量(Semaphores):主要用于同步和互斥,不是用来传输数据。
    优点:可以用于多进程、多线程之间的同步互斥问题。
    缺点:不是用于数据传输,使用不当容易导致死锁。

C++-一文了解进程间通信,从概念到实现 - MrGood的文章 - 知乎

https://zhuanlan.zhihu.com/p/672264623

(8)共享内存的原理是什么?

在内存中创建一个共享区域,多个进程可以访问这个区域。见上文。

(9)原子操作的原理是什么?

原子操作是指一个被中断的操作,即使在并发环境下也不会被其他进程或线程干扰的操作。换句话说,它是一个单独的不可分割的操作,其他操作无法看到它的中间状态,只能看到它的初始状态或最终状态。

在硬件层面,例如在多核和多处理器环境下,CPU通常提供原子指令(如交换、比较和交换等)来实现原子操作。这些原子指令可以在单个CPU周期内完成,从而确保在它执行过程中不会被其他CPU或线程干扰。

在软件层面,可以使用各种同步机制(如互斥锁、信号量、临界区等)来保证操作的原子性。例如,当一个线程在执行某个操作时,可以通过锁机制阻止其他线程访问同一资源,从而确保该操作能够原子地执行。

(10)I/O多路复用有哪些模型?

  1. select:最早的I/O复用技术,但存在一些问题,如单个进程能够监视的文件描述符数量存在最大限制,对socket进行扫描时是线性扫描,效率较低。
  2. poll:功能和select几乎相同,但是没有最大文件描述符数量的限制。
  3. epoll:只有在被监控的文件描述符发生变化时才会通知用户进程,避免了大量无用操作,因此效率较高。但是epoll是Linux特有的,不具有跨平台性。

详情见下文。

C++-一文了解I/O多路复用,从概念到实现 - MrGood的文章 - 知乎

https://zhuanlan.zhihu.com/p/672454948

(11)epoll高性能的原因有哪些?

epoll是Linux特有的IO多路复用机制,其性能优于select和poll。epoll使用一组函数来操作一个内核事件表,避免了每次调用都需要将全部文件描述符拷贝到内核态的问题。当某个文件描述符状态发生变化时,只需要操作事件表即可,效率较高。但是epoll的使用也较为复杂,需要更多的编程技巧。详情见上文。

(12)谈一谈signal机制

信号机制的工作原理可以概括为:一个进程(或者操作系统内核)可以给另一个进程发送一个信号,收到信号的进程则会根据信号的类型采取相应的动作。这个动作可以是默认的(例如终止进程、忽略信号等),也可以是进程自定义的(通过捕获信号并指定信号处理函数来实现)。

常见的信号有:

  1. SIGHUP:挂起信号。通常在终端关闭时发送给与其关联的进程。
  2. SIGINT:中断信号。通常在用户按下Ctrl+C时发送。
  3. SIGQUIT:退出信号。通常在用户按下Ctrl+\时发送。
  4. SIGKILL:杀死信号。用于立即结束进程,该信号不能被捕获、阻塞或忽略。
  5. SIGSTOP:停止信号。用于暂停进程的执行,该信号不能被捕获、阻塞或忽略。
  6. SIGCONT:继续信号。用于恢复被停止的进程。
  7. SIGTERM:终止信号。用于请求进程结束,但进程可以捕获该信号并决定如何处理。
  8. SIGSEGV:段错误信号。当进程进行非法内存访问时发送。
  9. SIGPIPE:管道破裂信号。当进程写入一个没有读端的管道时发送。
  10. SIGALRM:定时器到时信号。通常用于实现定时器。
  11. SIGUSR1和SIGUSR2:用户自定义信号。用于进程间通信。

(13)什么是系统调用,执行系统调用的过程是什么?

系统调用是操作系统提供给上层应用的接口,应用程序通过系统调用请求操作系统完成某些特殊的工作。这些工作通常包括:创建和控制进程,进行网络通信,访问硬件设备,访问文件系统等。因为这些操作涉及到系统资源的管理和保护,所以必须由操作系统来完成。

执行系统调用的过程通常如下:

  1. 用户态程序将系统调用的参数放在特定的地方,如某些寄存器或某些内存地址。
  2. 用户态程序执行一条特殊的指令,这条指令会引发一个软件中断或陷阱。这会将CPU从用户模式切换到内核模式,并跳转到一个预先设定的地址开始执行。这个地址处的代码负责处理所有的系统调用。
  3. 内核开始执行系统调用处理代码。首先,它会查看用户放置的参数,以确定用户想要执行哪个系统调用。然后,它会检查参数是否有效,如果参数无效,就返回一个错误码并结束。如果参数有效,它就执行相应的系统调用。
  4. 系统调用完成后,内核将结果放在一个地方,供用户态程序检查。然后,内核执行一条指令,将CPU从内核模式切换回用户模式,同时跳转回用户态程序的代码。
  5. 用户态程序检查系统调用的结果,然后继续执行。

(14)写时拷贝是什么,底层实现原理?

写时拷贝(Copy-On-Write,简称COW)是一种计算机程序资源管理技术,它允许同时拥有一个共享对象的多个部分在没有显式复制的情况下进行独立写操作。写时拷贝技术的主要优点是,如果没有写操作,就不会进行不必要的复制操作,从而节省了内存和CPU的使用。

写时拷贝的底层实现原理如下:

  1. 当一个进程需要复制一个对象(例如,当一个进程需要创建一个新的进程,它需要复制一份自己的地址空间)时,操作系统并不立即复制这个对象。相反,它创建了一个指向原始对象的指针,并将这个指针赋予新的进程。此时,两个进程实际上都在访问同一个对象。
  2. 当一个进程试图修改这个对象时,操作系统会捕获这个写操作,并在此时真正地创建一个新的对象副本。然后,它会更新该进程的指针,使其指向新的对象副本。至此,每个进程都有了自己的独立对象副本,可以进行独立的写操作。

计算机网络

(1)四层模型是哪四层,各自负责什么功能?

四层模型,也被称为TCP/IP模型,由下至上分别为:数据链路层、网络层、传输层和应用层。

  1. 数据链路层:也它负责处理在物理媒介上发送和接收数据包,包括如何开始和结束一个信息传输的细节,如何检测和纠正错误,如何控制网络设备和网络卡等等。
  2. 网络层:它负责数据包从源到目标的传输和网络路由,选择最合适的路径,以及处理如何在复杂的互联网上找到路径。
  3. 传输层:它负责提供端到端的通信,当你需要从一个网络节点向另一个网络节点发送数据时,传输层协议就起作用了。在TCP/IP中,这包括了诸如TCP和UDP协议。
  4. 应用层:这是最接近用户的一层,负责处理特定的应用程序细节。各种应用程序协议存在于应用层,如HTTP、FTP、SMTP、DNS等等。

(2)ping命令是什么原理?

其工作原理是基于ICMP(Internet Control Message Protocol,网络控制报文协议)。

当你在计算机上执行ping命令,向目标主机发送一个ICMP Echo请求报文,如果网络连接通畅,目标主机就会回应一个ICMP Echo回应报文。

在Ping命令执行过程中,它会记录并显示这些关于回应的信息:

  1. RTT(Round-Trip Time,往返时间):从发送ICMP Echo请求报文到收到ICMP Echo回应报文所花费的时间,通常用来衡量网络延迟。
  2. Packet Loss(数据包丢失):发送的ICMP Echo请求报文中,未能收到回应的数量,通常用百分比表示,用来衡量网络的可靠性。

通过Ping命令我们可以快速地得知两台主机之间的网络连接状况,以及网络的质量。

3、traceroute是什么原理?4、什么是ARP欺骗?5、集线器、交换机、路由器的区别?6、什么是MTU?为什么是这个大小?7、TCP三次握手和四次挥手机制

三次握手过程如下:

  1. 第一次握手:客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
  2. 第二次握手:服务器收到SYN报文段后,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1;同时,自己也发送一个SYN请求信息,将SYN位置为1,Sequence Number为y;服务器将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RCVD状态;
  3. 第三次握手:客户端收到服务器的SYN+ACK报文段后,将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕后,客户端和服务器都进入ESTABLISHED状态,完成TCP三次握手。

四次挥手过程如下:

  1. 第一次挥手:客户端发送一个FIN报文,用来关闭客户端到服务器的数据传送,客户端进入FIN_WAIT_1状态。
  2. 第二次挥手:服务器收到FIN后,会发送ACK报文,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务器进入CLOSE_WAIT状态。
  3. 第三次挥手:服务器发送FIN报文,用来关闭服务器到客户端的数据传送,服务器进入LAST_ACK状态。
  4. 第四次挥手:客户端收到FIN后,客户端进入TIME_WAIT状态,接着发送一个ACK给服务器,确认序号为收到序号+1,客户端进入TIME_WAIT状态,等待2MSL后发送此连接。

8、TCP的第三次握手可以携带数据吗?

TCP的第三次握手是可以携带数据的。在第三次握手时,客户端向服务器发送确认报文(ACK),确认自己收到了服务器的同步请求报文。

9、TCP可靠性由哪些机制保证?

  1. 三次握手:在建立连接之前,TCP使用三次握手协议来确保双方都准备好进行数据传输。这个过程也能够确保双方都有能力接收和发送数据。
  2. 序列号和确认应答:每个发送的TCP段都有一个序列号,接收方在接收到数据后会发送一个确认应答,确认应答中包含下一个期待接收的数据序列号。这样发送方就知道数据是否已经被接收,如果在一定时间内没有收到确认应答,发送方会重新发送数据。
  3. 流量控制:TCP使用滑动窗口机制进行流量控制,避免发送方发送数据的速度超过接收方的处理速度。
  4. 拥塞控制:当网络出现拥塞时,TCP会减少数据的发送速度,以防止网络拥塞的进一步恶化。
  5. 重传机制:当发送的数据包在网络中丢失或者在接收方处发生错误时,TCP会重新发送这些数据包。
  6. 校验和:TCP头部有一个校验和字段,用于检查数据在传输过程中是否发生错误。如果发现错误,TCP会请求重传。

10、超时重传如何进行?11、DNS解析过程如何进行?12、HTTP中有哪些Method,POST和PUT什么区别?13、HTTP1.0和1.1有什么区别?14、HTTPS安全性的原理15、什么是反向代理?nginx负载均衡有哪些策略?16、从输入网址到网页内容展示出来,发生了哪些事?

v2-1b1eb376e10fe9214909b810820d9f2d_r.jpg

本文作者:OhtoAi

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!