c++面试八股文

c++面试八股文

1.虚函数是什么?怎么实现?在内存中什么位置?

1.被virtual修饰的成员函数。

2.在成员函数前加virtual关键字。

3.C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

2.什么情况下要使用多态?为什么不直接在类里写函数?

1.如果需要其他类对一个类进行公有继承,则应该使用多态。

2.太过于复杂,代码冗余,多态可实现接口重用

3.vector插入元素和动态扩展的原理?

1.vector底层含有三个指针,first、last和end;size = last - first,capacity = end - start。当插入元素时,指针last向后移动一位。

2.如果发现last == end, 则进行扩容,一般是扩容一倍。

4.vector迭代器什么时候会失效?失效后如何删除元素?

1.push_back导致迭代器失效。vector在push_back的时候当容量不足时会触发扩容,导致整个vector重新申请内存,并且将原有的数据复制到新的内存中,并将原有内存释放,这自然是会导致迭代器失效的,因为迭代器所指的内存都已经被释放。

2.insert导致迭代器失效。一种是因为insert后导致扩容,原因与push_back一样;另一种时insert导致vector内元素移动,则部分迭代器失效。

3.erase导致迭代器失效。erase导致vector内元素移动,则部分迭代器失效。

失效后删除元素:1.使用返回值,erase()函数会返回一个指向被删除元素之后的元素的迭代器。因此,在循环中调用 erase()后,可以将迭代器设置为该返回值。

std::vector<int> vec = {1, 2, 3, 4};
for (auto it = vec.begin(); it != vec.end();) {
    if (*it == 2) {
        it = vec.erase(it);
    } else {
        ++it;
    }
}

2.不使用迭代器:另一种方法是不使用迭代器,在循环中使用下标访问容器元素。这种方法虽然没有使用迭代器方便,但可能会导致性能问题。

5.map和unordered_map的区别?时间复杂度是多少?

区别:1.头文件不同

​ 2.实现不同。map使用红黑树,而unordered_map使用哈希表。

​ 3.性质不同。map有序、unordered_map无序

map查找的时间复杂度为O(N),unordered_map为O(1)。

6.快排了解吗?时间复杂度多少?
#include<iostream>
 
using namespace std;
 
void quickSort(int arr[], int begin, int end) {
	if (begin >= end) return;
	int left = begin;
	int right = end;
	int temp = arr[left];
 
	while (left < right) {
		//从后往前找比他小的放前面,从前往后找比它大的放后面
		//以第一个数为基准,必须先从后往前走,再从前往后走
		while (left < right && arr[right] >= temp) {
			right--;
		}  //跳出此循环,代表right找到了比temp小的数字,所以此时arr[left]=arr[right]
		if (left < right) {
			arr[left] = arr[right];
		}
		while (left < right && arr[left] <= temp) {
			left++;
		}//同理
		if (left < right) {
			arr[right] = arr[left];
		}
		if (left == right) {
			arr[left] = temp;
		}
	}
	quickSort(arr, begin, left - 1);
	quickSort(arr, left + 1, end);
}
int main() {
 
	int arr[11] = { 5,6,3,2,7,8,9,1,4,0,0 };
	quickSort(arr, 0, 10);
	for (auto x : arr) {
		cout << x << " ";
	}
	return 0;
}

时间复杂度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhajjNPi-1688703325248)(C:\Users\柴\Desktop\笔记\面试八股文.assets\1682836747136.png)]

7.HTTP1.1和HTTP2.0区别?

HTTP/1.1 和 HTTP/2.0 是 Web 中最常用的两个协议版本,它们有以下主要区别:

  1. 多路复用:HTTP/2.0 支持多路复用,可以通过单一的连接发送和接收多个请求和响应。这种方式可以提高网络负载性能,并且在高延迟的链接中可以减少加载时间。
  2. 二进制分帧:HTTP/2.0 将所有数据分成更小的二进制帧(frame),每个帧都是相互独立的,可以根据需要交错发送,然后再在另一端重新组装。这样做的结果是可以更好地控制流量,避免了 HTTP/1.x 的“队头阻塞”问题。
  3. 头部压缩:HTTP/2.0 采用 HPACK 算法对消息头进行压缩,从而减少消息头的大小,并降低了网络传输的开销。
  4. 服务器推送:HTTP/2.0 允许服务器向客户端推送额外的内容,即在一个资源被客户端请求之前,服务器可以预先将其它相关资源推送到客户端,从而提高了页面的渲染速度。
  5. 连接建立优化:HTTP/2.0 在连接建立时采用了 TCP 的“快速打开”(TCP Fast Open)技术,从而减少了握手次数,提高了连接建立的速度。

综上所述,HTTP/2.0 的多路复用、二进制分帧、头部压缩以及服务器推送等特性使它在网络传输中的效率和性能方面都优于 HTTP/1.1。

8.c++如何避免死锁

死锁是多线程编程中常见的问题之一,它指的是两个或多个线程无法继续执行,因为它们互相持有对方需要的资源。在 C++ 中,可以通过以下方法避免死锁:

  1. 避免嵌套锁:如果一个线程在持有锁时又尝试获得同样的锁,就会出现死锁。在代码中应该避免嵌套加锁。
  2. 使用智能指针:使用智能指针(如 std::shared_ptr)来管理对象的生命周期,可以避免手动释放资源时出现的死锁问题。
  3. 避免锁顺序死锁:当一个线程按照不同的顺序获取多个锁时,可能会出现锁顺序死锁。为了避免这种情况,应该按照相同的顺序获取锁。
  4. 使用 RAII 技术:使用 RAII(Resource Acquisition Is Initialization)技术,在构造函数中获取锁,在析构函数中释放锁,可以保证锁的正确获取和释放,从而避免死锁问题。
  5. 使用超时机制:在等待锁时可以设置超时时间,如果超过一定时间仍然无法获取到锁,就放弃当前操作,以避免长时间占用锁资源而导致死锁。
9.hash表实现原理?冲突怎么解决?

hash表也叫散列表,就是以 键-值(key-indexed) 的形式存储的数据结构。可以根据key来快速的查找到value。也就是说,它通过把key值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

冲突解决方式:拉链法,开放定制法。

10.模板什么时候被实例化?

1.声明一个类模板的指针和引用,不会引起类模板的实例化,因为没有必要知道该类的定义。

2.定义一个类类型的对象时需要该类的定义,因此类模板会被实例化。

3.在使用sizeof()时,它是计算对象的大小,编译器必须根据类型将其实例化出来,所以类模板被实例化.

4.new表达式要求类模板被实例化。

5.引用类模板的成员会导致类模板被编译器实例化。

11.模板实例化的顺序

在 C++ 中,模板可以被显式或隐式地实例化。如果同时存在多个可行的实例化版本,则编译器会根据以下优先级规则选择最佳的实例化版本:

1.非模板函数:非模板函数具有最高优先级,因为它们不涉及模板类型参数的选择和匹配。

2.显式实例化:如果程序中存在与模板参数匹配的显式实例化版本,则优先选择显式实例化。

template void func<int>(int);  // 显式实例化

3.模板特化:如果程序中存在与模板参数匹配的特化版本,则选择特化版本。

template<>
void func<float>(float) { /* ... */ }  // 特化版本

4.模板函数重载:对于多个模板函数重载版本,编译器会根据函数调用时实参的类型进行匹配,选择最佳的版本。

5.普通函数重载:对于多个普通函数重载版本,编译器同样会根据函数调用时实参的类型进行匹配,选择最佳的版本。如果没有找到匹配的函数,则会尝试进行隐式类型转换或模板实例化等操作,尝试寻找匹配的函数。

需要注意的是,在使用模板时应该避免出现二义性,即同一模板参数既可以匹配多个重载版本,又可以匹配模板特化或显式实例化版本。如果出现二义性,则编译时会报错。

12.c++的内存结构?

C++ 内存结构主要包括以下几个部分:

  1. 栈(Stack):栈是一种后进先出(LIFO)的数据结构,用于存储局部变量、函数参数和返回值等。每当程序调用一个函数时,都会在栈上创建一个新的帧(Frame),并将函数的参数、局部变量和返回地址等信息压入栈中。当函数返回时,这些信息会被弹出栈。
  2. 堆(Heap):堆是一种动态内存分配区域,用于存储变量或对象等动态申请的内存。程序可以通过 newmalloc 等操作来在堆上动态地分配内存,在使用完毕后需要手动释放返回给系统。
  3. 全局数据区(Data segment):全局数据区用于存储程序中的静态变量、全局变量和常量等数据。其中,静态变量和全局变量在程序启动时就被初始化,而常量则通常存储在只读数据段中
  4. 代码区(Code segment):代码区用于存储程序的机器码指令,即可执行的程序代码。它通常包括程序的函数、全局变量和静态变量等。
  5. 栈顶指针(Stack Pointer):栈顶指针是一个特殊的寄存器,用于指向当前栈顶的位置。当程序调用函数或执行其他操作时,栈顶指针会相应地移动。

C++ 的内存结构是非常重要的,因为正确地使用和管理内存可以避免许多常见的错误和安全问题。例如,在使用栈上的变量时,需要注意其作用域和生命周期,以避免访问已经失效的内存;在使用堆上的内存时,则需要确保及时释放它,以避免内存泄漏等问题。

13.编译c++程序有哪些方法

C++ 程序可以通过以下几种方式进行编译:

  1. 命令行编译:在命令行终端中使用 C++ 编译器(如 g++、clang++、MSVC 等)手动编译源代码文件,生成可执行文件。例如,在 Linux 系统中,可以使用以下命令进行编译:
g++ main.cpp -o myprogram

这将会将 main.cpp 编译成一个名为 myprogram 的可执行文件。

  1. 集成开发环境(IDE):集成开发环境是一种集成了编辑器、编译器、调试器等多个工具的软件,可以方便地进行程序开发和调试。常见的 C++ IDE 包括 Visual Studio、Code::Blocks、Xcode 等。
  2. 构建工具(Build Tools):构建工具是一种自动化编译和构建系统,可以根据项目描述文件(如 Makefile、CMakeLists.txt 等)自动编译和生成可执行文件。常见的 C++ 构建工具包括 Make、CMake、Bazel 等。
  3. 在线编译器:在线编译器是一种基于 Web 的编程工具,可以在互联网上直接编写和运行 C++ 代码。常见的在线编译器包括 CodePen、OnlineGDB、repl.it 等。
14.linux编译c++的几种方式

四种:使用g++, cmake,使用库,使用ide。具体见下面博客。

linux编译的四种方法

cmake使用教程

15.linux查看磁盘空间 命令
df -h:查看每个根路径的分区大小
df -hl:查看磁盘剩余空间
du -sh [目录名]:返回该目录的大小
16.LINUX查看进程的4种方法
ps aux #查看进程使用情况
top  #top命令提供了运行中系统的动态实时视图。
17.linux查看日志命令

第一种:查看实时变化的日志(比较吃内存)

最常用的:tail -f app.log  (默认最后10行,相当于增加参数 -n 10)

​ tail -200f app.log (最后200行,某一时刻往前推)

Ctrl+c 是退出tail命令

其他情况:tail -n 20 app.log  (显示app.log最后20行)

tail -n +5 app.log  (从第5行开始显示文件)
第二种:搜索关键字附近的日志

最常用的:cat -n filename |grep “关键字”

其他情况:cat app.log | grep -C 5 ‘关键字’   (显示日志里匹配字串那行以及前后5行)

cat app.log | grep -B 5 ‘关键字’   (显示匹配字串及前5行)

cat app.log | grep -A 5 ‘关键字’   (显示匹配字串及后5行)

18.linux查看ip地址

查看当前登录的服务器ip地址的四种方法:

hostname -i #主机ip
ifconcig #最常用,查看所有在用的网络接口,查看所有的 ip,找到 ens 开头的网卡,即可找到对应的 ip
ifconfig -a #查看所有网络接口
ip addr 或者 ip add # 网卡的方式查看 ip,找到 ens 开头的网卡,即可找到对应的 ip
ip a | more
19.linux查看端口命令

第一种:lsof命令

lsof(list open files)是一个列出当前系统打开文件的工具。

lsof 可查看端口占用情况

lsof -i:端口号

第二种:netstat -tunlp命令

netstat -tunlp用于显示 tcp,udp 的端口和进程等相关情况。

netstat -tunlp | grep 端口号
  • -t (tcp) 仅显示tcp相关选项
  • -u (udp)仅显示udp相关选项
  • -n 拒绝显示别名,能显示数字的全部转化为数字
  • -l 仅列出在Listen(监听)的服务状态
  • -p 显示建立相关链接的程序名
20.linux查看网络接口

要查看Linux系统中的网络接口,可以使用命令行工具ifconfig或ip。

  1. 使用ifconfig命令

ifconfig是一个用于配置和显示网络接口的工具。要查看所有网络接口的信息,可以在终端窗口中键入以下命令:

ifconfig -a

这将列出系统上所有的网络接口,包括以太网、无线网络、回环接口等,并显示每个接口的IP地址、MAC地址、子网掩码等详细信息。

  1. 使用ip命令

ip命令是在Linux系统中管理网络接口的更先进的工具。要列出当前系统上的所有网络接口(包括未启动的接口),可以在终端窗口中键入以下命令:

ip link show

这将列出系统上所有的网络接口,包括每个接口的名称、状态、MAC地址等详细信息。要显示某个特定接口的详细信息,可以使用以下命令:

ip addr show <interface>

其中,是要查看的网络接口的名称。例如,要查看eth0接口的详细信息,可以输入以下命令:

ip addr show eth0

这将列出该接口的IP地址、MAC地址、子网掩码等详细信息。

21.linux查看网络状态

要查看Linux系统中的网络状态,可以使用命令行工具netstat或ss。

  1. 使用netstat命令

netstat是一个用于显示网络连接状态的工具。要查看当前系统上的所有网络连接和监听端口,可以在终端窗口中键入以下命令:

netstat -a

这将列出所有的网络连接和监听端口,并显示每个连接的协议、本地地址、外部地址、状态等详细信息。

如果只想查看某个特定协议(如TCP或UDP)的连接,可以使用以下命令:

netstat -at   # 查看TCP连接
netstat -au   # 查看UDP连接

此外,还可以使用以下参数来进一步定制netstat的输出:

  • -l:只显示监听端口
  • -n:以数字格式显示IP地址和端口号
  • -p:显示每个连接对应的进程ID和名称
  1. 使用ss命令

ss命令与netstat类似,也是一个用于显示网络连接状态的工具。要查看当前系统上的所有网络连接和监听端口,可以在终端窗口中键入以下命令:

ss -a

这将列出所有的网络连接和监听端口,并显示每个连接的协议、本地地址、外部地址、状态等详细信息。

与netstat不同的是,ss命令使用更少的系统资源,并且提供了更多的过滤选项和输出格式。例如,要显示所有TCP连接的详细信息,可以使用以下命令:

ss -t

这将列出所有TCP连接,包括每个连接的状态、本地地址、外部地址、PID等详细信息。

此外,还可以使用以下参数来进一步定制ss的输出:

  • -l:只显示监听端口
  • -n:以数字格式显示IP地址和端口号
  • -p:显示每个连接对应的进程ID和名称
  • -o:显示更多的连接信息,如定时器、窗口大小等。
22.内存管理机制有哪些

操作系统中的内存管理机制主要包括以下几种:

1.分区存储管理

分区存储管理是指将系统内存划分为若干个固定大小的分区,每个分区只能分配给一个进程使用。这种方法可以避免内存碎片问题,并且可以方便地实现多进程并发。

分区存储管理又可分为等长分区和不等长分区两种方式。等长分区是将内存按照相同大小的块进行划分,而不等长分区则是根据进程的需求动态地调整分区大小。

2.请求分页存储管理

请求分页存储管理是指将进程的地址空间划分为若干个大小相等的页面,每个页面可以独立分配和释放。当一个进程需要内存时,会向操作系统请求分配一页或多页空间。

请求分页存储管理可以有效地解决内存碎片问题,并且可以更加灵活地管理内存空间。但是,它也会带来一定的开销,如页面映射表、TLB缓存等。

3.请求分段存储管理

请求分段存储管理是将进程的地址空间划分为若干个不同大小的段,每个段可以独立分配和释放。当一个进程需要内存时,会向操作系统请求分配一个或多个段。

请求分段存储管理可以更好地满足进程对内存空间大小的需求,但是也会产生一定的开销,如段表、段页表等。

4.交换存储管理

交换存储管理是指将进程占用的部分内存暂时保存到磁盘中,以释放出物理内存供其他进程使用。当进程需要恢复被交换出去的内存时,操作系统会从磁盘中读取相应数据并将其还原到内存中。

交换存储管理可以有效地扩展可用内存空间,但是也会带来一定的性能开销,因为磁盘I/O速度较慢。

23.进程间通信方式

管道,消息队列,共享内存,信号,信号量,socket。

24.IO多路复用

select/poll/epoll多路复用系统调用,进程通过一个系统调用从内核获取多个事件

select:将已连接的socket放到一个文件描述符集合,select函数将文件描述符集合拷贝到内核,通过内核遍历集合来寻找是否有事件产生,将该socket标记,再将整个文件描述符集合拷贝回用户态里,再进行处理。select需要两次遍历和两次拷贝。poll和select本质没有太大区别。

epoll:1.在内核中使用红黑树来跟踪进程所有待检测的文件描数字,把准备好需要监控的socket通过epoll_ctl()函数加入到内核的红黑树中,红黑树增删改的时间复杂度是O(logn)。在内核维护红黑树,每次用户空间只需传入一个socke,避免了大量的数据拷贝。2.epoll使用事件驱动机制,内核里维护了一个链表来记录就绪事件,当某个socke有事件发生是,通过回调函数将其加入就绪列表,调用epoll_wait()时,只返回有事件发生的文件描述符的个数,提高了检测效率。

25.什么是边缘触发和水平触发?

边缘触发:当有事件发生时,服务器端只会从epoll_wait()中苏醒一次,即使数据没有读完,也不会再苏醒。

水平触发:当有事件发生时,服务器端不断地从epoll_wait()中苏醒,直到内核缓冲区数据被读取完才结束。

26.基于条件变量写一个多线程模型
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

const int THREAD_COUNT = 10;
const int MAX_COUNT = 100;

std::mutex g_mutex;
std::condition_variable g_conditionVariable;
int g_count = 0;

void threadFunc(int index) {
    for (int i = 0; i < MAX_COUNT; ++i) {
        std::unique_lock<std::mutex> lock(g_mutex);
        while (g_count % THREAD_COUNT != index) {
            g_conditionVariable.wait(lock);
        }
        std::cout << "Thread " << index << ": " << g_count++ << std::endl;
        g_conditionVariable.notify_all();
    }
}

int main() {
    std::thread threads[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads[i] = std::thread(threadFunc, i);
    }
    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads[i].join();
    }
    return 0;
}

27.基于semaphore写一个多线程模型
#include <iostream>
#include <thread>
#include <mutex>
#include <semaphore.h>

const int THREAD_COUNT = 10;
const int MAX_COUNT = 100;

std::mutex g_mutex;
sem_t g_semaphore;
int g_count = 0;

void threadFunc(int index) {
    for (int i = 0; i < MAX_COUNT; ++i) {
        sem_wait(&g_semaphore);
        std::lock_guard<std::mutex> lock(g_mutex);
        std::cout << "Thread " << index << ": " << g_count++ << std::endl;
        sem_post(&g_semaphore);
    }
}

int main() {
    sem_init(&g_semaphore, 0, THREAD_COUNT);
    std::thread threads[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads[i] = std::thread(threadFunc, i);
    }
    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads[i].join();
    }
    sem_destroy(&g_semaphore);
    return 0;
}

28.设计模式的几大原则
  • 开闭原则:对扩展开放,对修改关闭。
  • 单一职责原则:一个类只做一件事,一个类应该只有一个引起它修改的原因。
  • 里氏替换原则:子类完全可以替换父类。也就是说在使用继承时,只扩展新功能,而不要破坏父类原有的功能。
  • 依赖倒置原则:细节依赖于抽象,抽象不应依赖于细节。把抽象层放在程序设计的高层,并保持稳定,程序的细节实现由低层的实现层完成。
  • 迪米特法则(最少知道法则):一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。
  • 接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
29.手写一个单例模式(饿汉模式)
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

private:
    Singleton() {}
    ~Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

30.手写一个懒汉模式
#include <mutex>

class Singleton {
public:
    static Singleton& getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return *instance;
    }

private:
    Singleton() {}
    ~Singleton() {}

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::mutex mutex_;
    static Singleton* instance;
};

std::mutex Singleton::mutex_;
Singleton* Singleton::instance = nullptr;

在上面的代码中,我们使用了 std::mutex 类型的互斥量来保证线程安全,同时使用了双重检查锁定机制(Double-Checked Locking Pattern),以避免每次获取实例时都加锁的性能开销。

具体来说,当第一个线程访问 getInstance() 函数时,发现 instance 变量是空的,于是它会尝试获取互斥锁,并再次检查 instance 的值是否为空。如果为空,那么它就创建一个新的 Singleton 对象,并将其赋值给 instance。之后该线程释放互斥锁并返回 instance 的引用。

当其他线程访问 getInstance() 函数时,由于此时 instance 已经不为空,所以它们可以直接返回 instance 的引用,而不用重复创建新的对象。

这种实现方式可以保证懒加载(Lazy Initialization)、线程安全和单例性。同时,使用双重检查锁定也能避免加锁的性能开销,提高程序的效率。

31.unique_lock和lock_guard区别

std::lock_guardstd::unique_lock 都是 C++11 提供的互斥量(mutex)保护机制,它们的作用都是在程序中加锁和解锁互斥量。它们之间的主要区别如下:

  1. 拥有权:std::lock_guard 在构造时会立即获取锁,并在析构时自动释放锁,所以不能手动释放锁。而 std::unique_lock 可以控制锁的拥有权,可以随时手动获取和释放锁。
  2. 灵活性:由于无法手动获取和释放锁,因此 std::lock_guard 不太灵活,只能适用于一些简单的场景。如果需要更多的灵活性,比如可以暂时释放锁,等待某个条件变量等,那么应该使用 std::unique_lock
  3. 性能:由于 std::lock_guard 主要用于简单的场景,其实现相对简单,因此通常比 std::unique_lock 更轻量级,性能也更好。但这并不意味着 std::lock_guard 总是更好,具体取决于应用场景。

综上所述,如果只需要一个简单的锁保护机制,并且不需要手动获取和释放锁,可以优先考虑使用 std::lock_guard;如果需要更多的灵活性,可以考虑使用 std::unique_lock

32.DNS服务器用的什么协议?ping呢?

dns协议是应用层协议,将主机名转换成ip地址,使用udp传输。

ping使用的是ICMP协议,基于IP协议的,在网络层传递错误和控制信息。

33.TCP/IP五层模型

TCP/IP 协议族是互联网中最常用的协议族,它是由美国国防部研究计划署(DARPA)在 1970 年代末期开发的。TCP/IP 协议族采用了分层的设计思想,将整个协议栈划分为五个层次,分别是:

  1. 应用层(Application Layer):应用层负责处理特定的网络应用程序,如 HTTP、SMTP、FTP 等。应用层协议定义了应用程序之间通信的规则和格式。常见的应用层协议有 HTTP、DNS、SMTP、POP3、IMAP 等。
  2. 传输层(Transport Layer):传输层负责提供端到端的可靠数据传输服务,包括 TCP 和 UDP 两种协议。TCP 提供面向连接的可靠数据传输服务,保证数据的完整性和顺序性;UDP 则提供无连接的不可靠数据传输服务,适用于需要快速传输数据而不关心数据是否丢失或顺序的场景。
  3. 网络层(Network Layer):网络层负责将数据包从源主机传输到目标主机,实现了跨网络的数据传输。其中 IP 协议是网络层协议的代表,它定义了数据包的结构和交换方式,并通过路由选择算法和地址分配机制实现了主机之间的逻辑连接。
  4. 数据链路层(Data Link Layer):数据链路层负责将网络层传输过来的数据包封装成帧,并发送到物理层。同时也会从物理层接收帧并进行解封装。数据链路层协议包括 Ethernet、PPP、HDLC 等。
  5. 物理层(Physical Layer):物理层负责实现计算机与计算机之间的原始比特流(bit stream)的传输,包括电缆、光纤、无线电波等。物理层规定了比特流的传输速率、编码方式、电平等物理特性,确保传输的可靠性和稳定性。

总体来说,TCP/IP 五层模型是一个完整的网络传输模型,每一层都有自己的协议和功能,负责不同的任务,共同构建出了互联网这个复杂的计算机网络。

34.为什么使用线程池

线程池是一种常见的并发编程技术,它可以管理多个线程,提供了一种复用线程资源的方式,从而避免了线程频繁创建和销毁的开销。线程池通常由一个工作队列和若干个工作线程组成,当有新的任务需要执行时,线程池会将任务添加到工作队列中,空闲的工作线程会自动从队列中取出任务并执行,执行完毕后再返回到线程池中等待下一次任务。

线程池的主要优点包括:

  1. 提高性能:线程池可以避免频繁创建和销毁线程的开销,同时还可以控制并发线程的数量,以避免过度竞争和资源抢占的问题,从而提高程序的性能和响应速度。
  2. 提高可靠性:线程池可以通过合理配置参数来控制线程的数量、队列大小等属性,从而避免系统崩溃或者异常退出的风险。
  3. 简化编程:线程池可以隐藏底层线程管理的细节,让程序员更专注于业务逻辑的实现,从而简化编程难度。
  4. 支持任务排队和优先级调度:线程池通常具备任务排队和优先级调度的功能,可以根据任务类型和优先级来决定任务的执行顺序,从而更好地满足业务需求。

总之,线程池是一种非常实用的并发编程工具,可以帮助程序员提高性能、可靠性和代码复用率,并且能够有效地控制多线程的数量和并发度,避免出现过载和资源抢占等问题。

35.什么是RAII技术

RAII,全称为 Resource Acquisition Is Initialization,中文名为“资源获取即初始化”,是 C++ 中常用的一种管理资源、避免资源泄漏的编程技术。

RAII 技术基于 C++ 语言的对象生命周期原理,它通过在对象构造函数中获取资源,并在对象析构函数中释放资源的方式,来确保资源的正确管理。使用 RAII 技术,可以有效地避免因程序异常退出或者忘记释放资源等问题导致的内存泄漏、文件句柄泄漏、锁死等问题。

RAII 技术可以用于多种资源的管理,包括动态内存、文件句柄、网络连接、锁等等。下面是一个使用 RAII 技术管理动态内存资源的示例代码:

class MyResource {
public:
    MyResource() {
        m_data = new int[100];
        // 获取动态内存资源
    }
    
    ~MyResource() {
        delete[] m_data;
        // 释放动态内存资源
    }

private:
    int* m_data;
};

int main() {
    MyResource res; // 创建 RAII 对象
    // 使用资源
    return 0;
} // RAII 对象在作用域结束时自动销毁,释放资源

在上述代码中,MyResource 类就是一个利用 RAII 技术来管理动态内存资源的例子。当 MyResource 类的对象被创建时,它会自动获取一个大小为 100 的 int 数组的内存,并在对象析构时自动释放该内存。

总之,RAII 技术是 C++ 中一种非常实用的编程技术,可以帮助程序员正确地管理资源,避免出现资源泄漏等问题。在使用 RAII 技术时,可以利用 C++ 的对象生命周期原理,通过在对象构造函数中获取资源,并在对象析构函数中释放资源的方式来实现。

36.c++内存分布

C++ 是一种编译型语言,它的内存分配和布局是由编译器和操作系统共同管理的。在 C++ 中,变量和对象的内存布局通常分为以下四个部分:

  1. 文字段(text segment):也称为代码段,存放程序的指令代码,不能被修改。
  2. 数据段(data segment):也称为静态数据区,存放全局变量、静态变量和常量,包括初始化的和未初始化的数据。
  3. 堆(heap):存放动态分配的内存,由 newdelete 等运算符进行管理,通常需要程序员手动管理内存生命周期。
  4. 栈(stack):存放函数调用时的临时变量、函数参数、返回值等信息,由编译器自动管理分配和释放。

下面是一个简单的代码示例,演示了 C++ 中变量和对象的内存分布:

#include <iostream>

int g_data = 10; // 全局变量,存放于数据段
const int g_const_data = 20; // 全局常量,存放于数据段

int main() {
    int a = 1; // 自动变量,存放于栈中
    static int b = 2; // 静态变量,存放于数据段
    const int c = 3; // 常量,存放于文字段

    int* p = new int(4); // 动态分配内存,存放于堆中
    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;
    std::cout << "c = " << c << std::endl;
    std::cout << "*p = " << *p << std::endl;

    delete p; // 释放动态分配的内存

    return 0;
}

在上述代码中,全局变量 g_data 和全局常量 g_const_data 存放在数据段中,自动变量 a 存放在栈中,静态变量 b 存放在数据段中,常量 c 存放在文字段中。而动态分配的内存 p 存放在堆中。

总之,C++ 中的内存分布和布局由编译器和操作系统自动管理,程序员可以通过不同的方式来进行内存分配和释放,并且需要特别注意内存泄漏、越界访问等问题。

37.怎么debug和看内存是否泄露

在 C++ 中,调试和内存泄漏检测是非常重要的任务。下面介绍一些常用的调试技术和内存泄漏检测工具:

调试技术

  1. 使用断言:在程序中插入断言语句可以帮助检测程序中的错误和异常,例如数组越界、空指针等问题。可以使用 <cassert> 头文件中的 assert() 宏来实现。
  2. 打印日志信息:在程序中加入一些调试输出语句,可以在程序运行时输出一些有用的信息,例如变量值、函数调用顺序等等。可以使用 std::cout 或者 fprintf 等函数来实现。
  3. 使用调试器:调试器是一种可以让程序停在某个断点处,并允许程序员逐步执行程序的工具。可以使用 GDB、LLDB、Visual Studio 等调试器进行调试。

内存泄露检测工具

  1. Valgrind:Valgrind 是一种非常流行的内存泄漏检测工具,它可以检测出一些常见的内存错误,例如内存泄漏、非法内存访问等。可以使用 valgrind 命令来运行程序。
  2. AddressSanitizer:AddressSanitizer 是 GCC 和 Clang 中的一个内存错误检测工具,它可以检测出内存泄漏、缓冲区溢出等问题。可以在编译时使用 -fsanitize=address 选项启用。
  3. LeakSanitizer:LeakSanitizer 是 Clang 中的一个内存泄漏检测工具,它可以检测出动态分配的内存泄漏问题,并且提供了详细的错误信息和堆栈跟踪。可以在编译时使用 -fsanitize=leak 选项启用。

在进行调试和内存泄漏检测时,需要注意以下几点:

  1. 在编写代码时,应遵循 RAII 原则,自动化地管理资源的生命周期。
  2. 在进行内存泄漏检测之前,应先确保程序的逻辑正确性。
  3. 在进行内存泄漏检测时,应尽可能地模拟真实的使用场景,并覆盖所有可能的情况。
  4. 内存泄漏检测工具可能会影响程序的执行效率,因此应该根据实际情况选择合适的工具和配置选项。

总之,在 C++ 中进行调试和内存泄漏检测是一项非常重要的任务,通过合理使用调试技术和内存泄漏检测工具,可以有效地提高代码质量和性能。

38.c++智能指针什么情况下会产生循环引用

C++ 智能指针是一种自动化内存管理工具,可以避免常见的内存泄漏问题。智能指针的主要作用是在对象不再被使用时,自动释放所占用的内存空间,并且可以避免多个指针同时指向同一个对象时产生的问题。

然而,在使用 C++ 智能指针时,如果存在循环引用的情况,就可能导致内存泄漏问题。循环引用(也称为循环依赖)是指两个或多个对象之间相互引用,形成了一个闭环的依赖关系,导致它们无法被垃圾回收器正常地回收。

下面是一个简单的循环引用示例,其中 A 类和 B 类互相持有对方的指针:

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> bptr_;
};

class B {
public:
    std::shared_ptr<A> aptr_;
};

int main() {
    std::shared_ptr<A> aptr = std::make_shared<A>();
    std::shared_ptr<B> bptr = std::make_shared<B>();
    aptr->bptr_ = bptr;
    bptr->aptr_ = aptr;

    return 0;
}

在上述代码中,类 A 和类 B 分别持有对方的智能指针,导致它们无法被正常地释放。当程序结束时,智能指针的引用计数会一直保持为 1,因此无法自动释放内存空间,从而产生了内存泄漏问题。

为了避免循环引用导致的内存泄漏问题,可以使用弱引用(std::weak_ptr)来替代智能指针的强引用(std::shared_ptr)。弱引用是一种特殊的智能指针,它不会改变所指对象的引用计数,并且不会阻止所指对象被销毁。可以通过 std::weak_ptr 来创建弱引用:

c++复制代码class B;

class A {
public:
    std::weak_ptr<B> bptr_;
};

class B {
public:
    std::weak_ptr<A> aptr_;
};

在上述代码中,使用 std::weak_ptr 来替代类 A 和类 B 中的智能指针。由于弱引用不会增加引用计数,因此即使两个对象之间存在循环引用,也不会导致内存泄漏问题。

39.RAII基于什么实现的

RAII的实现依赖于c++两个重要的特性,构造函数与析构函数,构造函数用于初始化对象,获取对象资源;析构函数用于销毁对象,释放资源。

40.什么是虚函数表

虚函数表(Virtual Function Table,简称 vtable)是一种用于实现多态的技术,常用于 C++ 等面向对象编程语言中。它是一个包含一组指向虚函数的指针的数据结构,每个对象都有自己的虚函数表,用于动态绑定和调用虚函数。

在 C++ 中,如果类定义了一个或多个虚函数,就会隐式地为该类生成一个虚函数表,其中存储了各个虚函数的地址。当一个对象被创建时,该对象会分配一块内存用于存储其成员变量和虚函数表的指针。这些指针通常位于对象的首部或尾部,并且可以通过偏移量访问到。

当需要调用一个虚函数时,C++ 编译器会根据对象的类型查找对应的虚函数表,并从中取出相应的函数指针进行调用。由于每个对象都有自己的虚函数表,因此可以通过基类的指针或引用来调用派生类的虚函数,从而实现多态

41.什么是路由表

路由表是用于存储和管理网络路由信息的数据结构。

在一个复杂的网络中,数据包需要经过多个路由器才能到达目的地。每个路由器都有自己的路由表,通过查询路由表来确定接下来应该将数据包发送到哪个方向或者下一个路由器。路由表通常包含以下几个关键信息:

  1. 目标地址:数据包要到达的目的地,可以是单个 IP 地址或一个 IP 地址段。
  2. 子网掩码:用于判断目标地址是否属于本地网络,以便决定数据包的转发方式。
  3. 网关地址:如果目标地址不属于本地网络,则需要将数据包发送到下一个路由器,这时需要指定网关地址。
  4. 接口名称:用于标识数据包应该从哪个物理接口发送出去。

路由表的内容可以手动配置,也可以自动获取。自动获取路由表的方式通常有两种,一种是根据静态路由协议(如 RIP、OSPF、BGP 等)来生成路由表,另一种是使用动态主机配置协议(DHCP)来分配 IP 地址和路由信息。

42.路由表为空怎么找到下一跳

当路由表为空时,网络中的数据包无法直接确定下一跳,因此需要通过其他手段来寻找目标节点。下面介绍几种常见的解决方案:

  1. 静态路由:如果网络结构相对稳定,可以手动配置静态路由。通过手动添加路由规则,指定目标地址和下一跳地址,让数据包能够按照预先设定的路径进行转发。
  2. 动态路由:如果网络结构较为复杂或者存在频繁变化,建议使用动态路由协议。常用的动态路由协议有 OSPF、BGP 等,这些协议可以自动检测网络拓扑结构,计算出最佳路径,并记录在路由表中供数据包参考。
  3. ARP:如果目标节点位于本地网络中,可以使用 ARP 协议(Address Resolution Protocol)来获取下一跳的 MAC 地址。ARP 协议通常用于将 IP 地址转换为 MAC 地址,以便数据包能够准确地传输到目标节点。
  4. DNS:如果目标节点是一个域名,可以使用 DNS(Domain Name System)来解析域名并获取相应的 IP 地址。DNS 服务通常提供了缓存机制,可以加快域名解析的速度并减少网络负载。

总之,当路由表为空时,可以使用静态路由、动态路由、ARP 或 DNS 等手段来获取下一跳信息。根据网络的具体情况和需求,选择合适的解决方案可以提高数据传输效率和网络安全性。

43.什么是Innodb

InnoDB 是 MySQL 数据库管理系统的一种存储引擎,是MySQL官方推荐使用的默认存储引擎。它提供了事务处理、行级锁定和外键约束等一系列高级数据库功能,适用于大型在线应用程序和高并发负载环境下的数据管理。以下是InnoDB的几个主要特点:

  1. 支持事务:InnoDB 存储引擎支持 ACID(原子性、一致性、隔离性和持久性)事务,可以确保数据操作的完整性和一致性。
  2. 行级锁定:InnoDB 使用行级锁定机制,可以更好地支持多个用户同时访问同一张表的不同记录,从而提高并发性能。
  3. 外键约束:InnoDB 支持外键约束,可以在关联表之间建立引用完整性约束,从而确保数据在插入或更新时始终保持一致性。
  4. 支持热备份:InnoDB 支持在线备份和恢复,可以在不停机的情况下对数据库进行备份和恢复,提高数据库的可用性和可靠性。
  5. 支持全文索引:InnoDB 支持全文索引功能,可以更快地搜索和查询大量文本数据。

总之,InnoDB 是一个功能强大、稳定可靠的存储引擎,被广泛应用于大型在线应用程序和高并发负载环境下的数据管理。

44.事务有哪些特性?

数据库事务具有 ACID 四个特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

  1. 原子性(Atomicity):原子性是指一个事务中的所有操作要么全部完成,要么全部不完成,不会出现部分完成的情况。如果一个事务执行过程中发生了错误,那么整个事务将被回滚到最初状态,以确保数据的一致性和完整性。
  2. 一致性(Consistency):一致性是指一个事务执行前后,数据库从一个一致性状态转换到另一个一致性状态。在事务执行过程中,数据库会对数据进行各种约束检查,以确保数据的有效性和正确性。
  3. 隔离性(Isolation):隔离性是指在多用户并发访问数据库时,每个用户只能看到它自己的修改结果,与其他用户并发操作所产生的结果相互独立。隔离级别越高,意味着允许的并发量就越小,但数据的安全性和一致性也会得到更好的保障。
  4. 持久性(Durability):持久性是指事务提交后,数据库需要将其所做的变更永久保存到磁盘或其他介质中。即使系统发生故障或重启,数据也不会丢失,以确保数据的持久性和可靠性。

总之,ACID 四个特性是数据库事务的核心特征,能够保证数据的一致性、可靠性和完整性。在设计和开发数据库应用程序时,必须注重 ACID 特性的实现,才能保证数据操作的正确性和可维护性。

44.索引分类以及什么时候需要索引,什么时候不需要?

分类:主键索引,二级索引,B+索引,hash索引,唯一索引,普通索引。

适用于索引:

  • 有唯一性字段的字段
  • 经常用where查询的字段,这样可以提高整个表的查询效率,如果是多个字段,则可以建立联合索引。
  • 经常用于 GROUP BYORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。

不适用于索引:

  • 表中数据太少时,不需要创建索引。
  • 需要经常更新的字段,因为b+树有序,更新会破坏b+树结构,重新排序需要时间。
  • 字段中存在大量重复数据的,比如性别。
  • 不用where,GROUP BY和ORDER BY的字段,因为创建索引需要大量物理空间。
45.MySQL的索引怎么设计的?
  • 主键索引:主键索引是唯一性索引。用于标识表中每一行的唯一标识符。确保每个记录都有一个唯一标识。
  • 唯一索引:确保列值是唯一的,且可以为null,但只能有一个null。
  • 聚簇索引:按照表中的物理顺序存储数据,可在查询中减少查询I/O次数并提高范围查询性能。
  • 非聚簇索引:单独存储的索引,可提高查询性能但会增加I/O次数。
  • 复合索引:在多个列上创建索引,提高多个列的查询性能。
  • 前缀索引:在列的一部分上创建索引,可以减小索引的大小,从而提高查询性能。
46.索引失效的情况?
  • 多索引列进行函数操作或者计算。
  • 对索引列进行了类型转换。
  • 使用通配符开头的模糊查询。
  • or条件查询,or前可使用索引。or后的无法使用。
  • 数据量过小时,使用索引性能不会有很大提升,反而浪费存储空间
  • 更新频繁的表,使用索引每次更新都会导致大量索引需要进行更新,得不偿失。
47.c++写一个懒汉式的单例模式
#include <iostream>

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
using namespace std;
int main()
{
    Singleton* s1 = Singleton::getInstance();
    Singleton* s2 = Singleton::getInstance();
    if (s1 == s2) cout << "111" << endl; //两实例相同
    else cout << "000" << endl; //两实例不同
    return 0;
}

输出:111
48.写一个线程安全的版本
#include <iostream>
#include <mutex>
using namespace std;
class Singleton {
private:
    static Singleton* instance;
    static mutex mtx;
    Singleton() {}
public:
    static Singleton* getInstance() {
        lock_guard<mutex> lock(mtx);
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
mutex Singleton::mtx;
//using namespace std;
int main()
{
    Singleton* s1 = Singleton::getInstance();
    Singleton* s2 = Singleton::getInstance();
    if (s1 == s2) cout << "111" << endl; //两实例相同
    else cout << "000" << endl; //两实例不同
    return 0;
}

输出:111
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值