OPPO C++面试题及参考答案

news2025/2/24 18:12:32

五层协议每层包含的协议

在计算机网络的五层协议体系结构(自下而上为物理层、数据链路层、网络层、传输层和应用层)中,各层包含多种协议。

物理层主要负责在物理介质上传输原始的比特流,包括像 RJ - 45 接口标准等物理接口规范,协议有 EIA/TIA - 232、EIA/TIA - 449 等。这些协议规定了物理设备之间的机械、电气、功能和过程特性,使得数据能够以物理信号的形式在通信线路上传输。

数据链路层主要功能是将物理层接收到的原始比特流进行封装成帧,并进行差错检测和纠正。以太网(Ethernet)协议是数据链路层非常重要的协议,它规定了数据帧的格式,包括目的地址、源地址、类型字段和数据字段等部分。还有 PPP(点到点协议),它主要用于在串行链路上建立、配置和测试数据链路连接,常用于拨号上网等场景。

网络层负责将分组从源主机传输到目标主机,主要协议是 IP(互联网协议)。IP 协议规定了网络层分组(也叫 IP 数据报)的格式,包括版本、首部长度、服务类型、总长度、标识符、标志位、片偏移、生存时间、协议、首部校验和、源 IP 地址和目的 IP 地址等字段。还有 ICMP(互联网控制报文协议),用于在 IP 主机、路由器之间传递控制消息,比如用于报告错误或者提供有关 IP 数据包处理情况的信息,ping 命令就是利用 ICMP 协议来测试网络连接是否可达。

传输层提供端到端的通信服务,主要协议有 TCP(传输控制协议)和 UDP(用户数据报协议)。TCP 是一种面向连接的、可靠的传输协议,它通过序列号、确认应答、超时重传等机制保证数据的可靠传输。UDP 是一种无连接的、不可靠的传输协议,它在发送数据报之前不需要建立连接,简单高效,常用于对实时性要求较高但对数据准确性要求不是特别严格的应用场景,如实时视频流、音频流等。

应用层是五层协议的最高层,直接为用户的应用进程提供服务。HTTP(超文本传输协议)用于在 Web 浏览器和 Web 服务器之间传输超文本数据,它是万维网数据通信的基础。SMTP(简单邮件传输协议)用于发送电子邮件,它定义了邮件传输的过程和格式。DNS(域名系统)协议用于将域名解析为对应的 IP 地址,使得用户可以通过方便记忆的域名来访问互联网资源。

TCP/IP 中如何解决粘包问题?如果一直传输数据怎么拆包?

在 TCP/IP 协议中,粘包是由于 TCP 协议的特性导致的。TCP 是面向字节流的协议,它会把应用层交付的数据看作一连串无结构的字节流,在发送端可能会把多个小的数据包拼接成一个大的数据包发送,在接收端就会出现粘包现象。

解决粘包问题主要有以下几种方法。

一是使用固定长度的数据包。发送方和接收方事先约定好每个数据包的长度,接收方按照固定长度来接收数据。例如,规定每个数据包长度为 100 字节,那么接收方每次就接收 100 字节的数据,这样就可以很容易地将不同的数据包区分开来。不过这种方法的缺点是不够灵活,如果数据长度不固定,就需要对数据进行填充,会造成一定的带宽浪费。

二是在数据包头部添加长度字段。发送方在每个数据包头部添加一个表示该数据包长度的字段,接收方先读取这个长度字段,然后根据这个长度来接收相应长度的数据。例如,数据包头部的前 4 个字节用来表示该数据包的长度,接收方先读取这 4 个字节,得到长度值 n,然后再接收 n 字节的数据。这种方法相对灵活,可以适应不同长度的数据传输。

三是使用特殊的分隔符来区分不同的数据包。发送方在每个数据包的结尾添加一个特殊的分隔符,接收方通过查找这个分隔符来区分不同的数据包。比如,使用回车换行符(\r\n)作为分隔符,接收方在接收到的数据中查找 \r\n,一旦找到就认为一个数据包接收完毕。不过这种方法的难点在于如果数据本身包含分隔符,就需要进行特殊处理,比如对分隔符进行转义。

如果一直传输数据进行拆包,当采用固定长度数据包方法时,就按照固定长度依次截取数据即可。如果是头部添加长度字段的方法,接收方在接收数据时,先接收头部的长度字段,解析出长度后,再按照这个长度接收后续的数据。如果是使用分隔符的方法,接收方不断接收数据,然后在接收的数据中查找分隔符,当找到分隔符时,将分隔符之前的数据作为一个完整的数据包进行处理,然后继续在剩余的数据中查找下一个分隔符,以此类推。同时,为了避免因为网络延迟等原因导致数据接收不完整,接收方通常会设置一个缓冲区,将接收到的数据先存入缓冲区,然后在缓冲区中进行上述的拆包操作。

为什么项目中选择 TCP 传输而不选择 UDP?

在项目中选择 TCP 传输而不是 UDP 主要有以下几个原因。

首先是可靠性方面。TCP 是一种面向连接的、可靠的传输协议。它通过序列号、确认应答、超时重传等机制保证数据的可靠传输。例如,发送方发送一个数据包后会等待接收方的确认应答,如果在一定时间内没有收到确认应答,就会重新发送该数据包。这种机制对于数据准确性要求很高的应用场景非常重要,如文件传输、数据库备份等。而 UDP 是无连接的、不可靠的传输协议,它不保证数据的可靠到达,数据报可能会丢失、重复或者乱序,所以对于不能接受数据丢失的项目来说,TCP 是更好的选择。

其次是流量控制。TCP 有流量控制机制,它可以根据接收方的接收能力来调整发送方的发送速度。发送方的发送窗口大小是根据接收方的接收窗口大小来动态调整的。这样可以避免发送方发送数据过快,导致接收方缓冲区溢出。例如,接收方的接收缓冲区只剩下很小的空间时,它会通过 TCP 头部的窗口字段告知发送方减小发送窗口,从而控制发送速度。UDP 没有这种流量控制机制,可能会导致接收方无法处理大量涌入的数据。

再者是连接管理方面。TCP 在传输数据之前需要建立连接,在数据传输结束后需要断开连接。这种连接管理机制使得 TCP 传输更加有序和可控。通过三次握手建立连接可以确保双方都已经准备好进行数据传输,通过四次握手断开连接可以确保数据都已经正确传输完毕并且双方资源都已经释放。UDP 不需要建立连接,它直接将数据报发送出去,这种方式虽然简单快速,但是对于一些需要有序管理数据传输的项目来说就不太合适。

另外,对于复杂的网络环境,TCP 的拥塞控制机制可以有效地避免网络拥塞。当网络出现拥塞时,TCP 会自动降低发送速度,以减轻网络的负担。它通过慢启动、拥塞避免、快速重传和快速恢复等算法来实现拥塞控制。例如,在慢启动阶段,TCP 会以指数增长的方式增加发送窗口大小,当发现网络可能出现拥塞时(通过超时或者收到重复的确认应答),就会进入拥塞避免阶段,以线性增长的方式增加发送窗口大小。UDP 没有这种拥塞控制机制,可能会加剧网络拥塞。

如果使用 TCP 希望传输一个复杂的对象应该怎么传输?

如果要使用 TCP 传输一个复杂的对象,以下是一些常见的步骤和方法。

首先,需要对复杂对象进行序列化。序列化是将对象的状态转换为可以存储或传输的格式的过程。在 C++ 中,可以使用多种方式进行序列化。例如,如果对象是自定义的结构体或者类,可以将其成员变量按照一定的顺序转换为字节流。如果对象包含指针,需要特别注意指针所指向的数据的处理,因为在传输过程中,接收方并不知道指针所指向的数据的位置,可能需要将指针指向的数据也一起序列化。

一种常见的序列化方式是使用二进制格式。比如,可以将对象的各个成员变量的二进制表示依次写入一个字节流中。假设我们有一个包含姓名、年龄和成绩的学生结构体,我们可以先将姓名的字符串长度写入字节流,然后是姓名的字符数组,接着是年龄和成绩的二进制表示。这样就可以将整个结构体转换为一个字节流。

在发送端,将序列化后的字节流通过 TCP 套接字发送出去。可以使用 send 函数将字节流发送到指定的 TCP 连接的另一端。在发送过程中,要注意处理可能出现的发送错误,比如网络中断或者对方接收缓冲区已满等情况。

在接收端,首先通过 TCP 套接字接收字节流。可以使用 recv 函数接收数据,并且要考虑到可能会出现粘包问题,所以要按照之前提到的解决粘包问题的方法来正确接收完整的字节流。

然后,对接收的字节流进行反序列化。反序列化是序列化的逆过程,将字节流转换回原来的对象结构。根据发送端的序列化规则,依次读取字节流中的数据,恢复对象的各个成员变量。例如,先读取姓名的字符串长度,然后读取相应长度的字符数组作为姓名,接着读取年龄和成绩的值,从而重建学生结构体对象。

为了确保传输的正确性和完整性,还可以在传输的数据中添加一些校验信息。比如,可以计算字节流的校验和,在接收端对接收到的数据进行校验和验证,如果校验和不一致,就说明数据在传输过程中可能出现了错误,需要重新发送数据。

TCP 和 UDP 的区别,引申到可靠性,流量控制,拥塞控制,拥塞窗口,慢启动,丢包重传的一些问题,最后到 BBR 优化

TCP 和 UDP 是传输层的两种主要协议,它们有很多区别。

TCP 是面向连接的协议,在数据传输之前需要通过三次握手建立连接,确保双方都能正常通信。而 UDP 是无连接的协议,发送方可以直接向接收方发送数据报,不需要建立连接。例如,在进行网络通话时,如果使用 TCP,通话前会先建立一个连接,像拨通电话一样;而 UDP 就像是对讲机,直接就可以发送消息。

从可靠性方面来说,TCP 是可靠的传输协议。它通过序列号来标识每个数据包,接收方收到数据包后会发送确认应答。如果发送方没有收到确认应答,就会进行超时重传。UDP 是不可靠的传输协议,它不保证数据包能够准确无误地到达接收方,数据包可能会丢失、重复或者顺序错乱。比如,在文件传输场景下,使用 TCP 可以确保文件完整准确地传输,而 UDP 可能会导致文件部分内容丢失。

在流量控制方面,TCP 有流量控制机制。接收方会通过窗口字段告知发送方自己的接收缓冲区剩余空间大小,发送方根据这个信息来调整发送速度,避免接收方缓冲区溢出。UDP 没有这种流量控制机制,它以固定的速率发送数据,可能会导致接收方来不及处理大量的数据。

关于拥塞控制,TCP 有一套完整的拥塞控制策略。其中拥塞窗口是 TCP 拥塞控制的一个重要概念。在 TCP 的慢启动阶段,拥塞窗口大小以指数增长的方式快速增加,使得 TCP 能够快速利用网络带宽。当检测到可能出现网络拥塞(如超时或者收到重复确认应答)时,就会进入拥塞避免阶段,拥塞窗口以线性增长的方式增加。丢包重传也是 TCP 拥塞控制的一部分,当发现数据包丢失时,TCP 会根据具体情况进行快速重传或者超时重传。UDP 没有拥塞控制机制,在网络拥塞时,UDP 可能会继续以原来的速率发送数据,从而加剧网络拥塞。

BBR(Bottleneck Bandwidth and RTT)是一种 TCP 拥塞控制算法优化方案。它的主要目标是在不产生网络拥塞的情况下,尽可能地利用网络带宽。BBR 通过不断地测量网络的瓶颈带宽和往返时间(RTT),来调整发送速率和拥塞窗口大小。与传统的 TCP 拥塞控制算法相比,BBR 能够更快地适应网络状况的变化,减少网络拥塞的发生。例如,在高带宽、高延迟的网络环境下,BBR 可以更有效地利用网络资源,提高数据传输效率。

UDP/TCP 区别、优缺点及应用场景

UDP 和 TCP 都是传输层协议,它们之间有诸多区别。

从连接方式上看,TCP 是面向连接的协议,数据传输前要经过三次握手建立连接,传输结束后还需要四次握手来断开连接。而 UDP 是无连接的协议,发送端可以直接向接收端发送数据,无需建立连接的过程。

在可靠性方面,TCP 是可靠的。它通过序列号、确认应答和超时重传等机制保证数据的准确传输。每个数据包都有唯一的序列号,接收方收到数据包后会发送确认应答,若发送方未收到应答,就会重传数据包。UDP 则是不可靠的,它不保证数据能完整无误地到达接收端,数据报可能会丢失、重复或者乱序。

就传输速度而言,UDP 因为不需要建立连接和复杂的确认机制,通常传输速度比 TCP 快。TCP 由于要保证数据的可靠性,在数据传输过程中有较多的开销。

从数据顺序角度来说,TCP 会保证数据按照发送的顺序到达接收端,而 UDP 不保证数据的顺序。

TCP 的优点在于可靠、能保证数据顺序和完整性,适用于对数据准确性要求极高的场景,如文件传输、电子邮件传输、网页浏览(HTTP 协议底层使用 TCP)等。因为这些场景不能接受数据丢失或者错误,需要完整准确地获取信息。TCP 的缺点是开销大、传输速度相对较慢,建立和断开连接也需要一定的时间。

UDP 的优点是简单、高效、传输速度快,适用于对实时性要求高但对数据准确性要求相对较低的场景,如实时视频流、音频流、在线游戏中的位置更新等。这些场景中,少量的数据丢失不会对整体体验产生太大影响,更注重数据的实时传输。UDP 的缺点是不可靠,可能出现数据丢失、重复或乱序的情况。

讲讲虚函数?什么情况会使用虚函数?虚函数的底层原理清楚吗?构造函数可以是虚函数吗?

虚函数是 C++ 中实现多态性的重要机制。当在基类中声明一个虚函数时,它可以在派生类中被重新定义。这样,通过基类指针或者引用调用这个函数时,实际调用的是派生类中重新定义的函数版本。

在以下情况会使用虚函数。当有一个基类和多个派生类,并且希望根据对象的实际类型来调用相应的函数实现时,就需要虚函数。例如,有一个图形类作为基类,它有圆形、矩形等派生类。每个图形都有一个计算面积的函数,由于不同图形计算面积的方式不同,所以在基类中将计算面积函数声明为虚函数,然后在各个派生类中根据自身的形状来重新定义这个函数。这样,当通过基类指针或引用操作这些派生类对象时,就可以正确地调用对应的面积计算函数。

虚函数的底层原理主要涉及到虚函数表(v - table)和虚函数指针(v - ptr)。在含有虚函数的类中,编译器会为这个类创建一个虚函数表,这个表存储了该类所有虚函数的地址。每个含有虚函数的类的对象都会有一个虚函数指针,这个指针指向所属类的虚函数表。当通过基类指针或者引用调用虚函数时,实际上是通过对象的虚函数指针找到虚函数表,然后在表中查找对应的虚函数地址来调用函数。

构造函数不能是虚函数。因为在对象构造过程中,首先要调用构造函数来创建对象,而虚函数的调用依赖于对象已经构造完成并且虚函数指针已经正确初始化。如果构造函数是虚函数,在调用构造函数时,对象还没有完全构造好,虚函数指针也没有正确初始化,这会导致程序运行出错。

讲一讲 C++ 的多态?

C++ 的多态是面向对象编程的一个重要特性,它允许使用单一的实体(如函数或者对象)来表示多种形式。多态主要分为静态多态和动态多态。

静态多态也叫编译时多态,主要是通过函数重载和模板来实现。函数重载是指在同一个作用域内,有多个同名函数,但是它们的参数列表(参数个数、类型或者顺序)不同。在编译阶段,编译器根据函数调用时提供的参数来决定调用哪个重载函数。例如,有两个名为 add 的函数,一个接受两个整数参数并返回它们的和,另一个接受两个浮点数参数并返回它们的和。当调用 add 函数时,编译器根据传入的参数是整数还是浮点数来确定调用哪个具体的 add 函数。

模板也是实现静态多态的一种方式。它允许定义一个通用的代码模板,这个模板可以根据不同的类型参数生成不同的具体代码。例如,定义一个模板函数来交换两个变量的值,这个模板函数可以用于交换整数、浮点数、甚至是自定义类型的变量的值,在编译阶段,编译器根据实际使用的类型来生成相应的代码。

动态多态主要是通过虚函数和继承来实现。在基类中声明虚函数,派生类可以重新定义这些虚函数。当通过基类指针或者引用调用虚函数时,实际调用的是派生类中重新定义的函数。例如,有一个动物类作为基类,它有一个发声的虚函数。猫类和狗类是动物类的派生类,它们分别重新定义了发声函数来发出猫叫和狗叫的声音。当通过动物类的指针或者引用指向猫或者狗对象并调用发声函数时,就会根据对象的实际类型(猫或者狗)来发出相应的声音。这种动态多态性使得程序能够在运行时根据对象的实际类型来选择正确的函数执行,增加了程序的灵活性和可扩展性。

说下 C++ 的多态,多态有哪些形式?

C++ 的多态有两种主要形式,即静态多态和动态多态。

静态多态主要基于编译时的类型检查和模板机制。函数重载是静态多态的一种常见形式。例如,假设我们有一个名为 print 的函数。可以定义一个 print 函数用于打印整数,其参数为一个整数类型;还可以定义另一个 print 函数用于打印字符串,其参数为一个字符数组类型。当在代码中调用 print 函数时,编译器会根据传入的参数类型来确定到底调用哪一个 print 函数。这就是在编译阶段确定函数调用的具体版本,是一种静态的多态。

模板也是静态多态的重要体现。以函数模板为例,我们可以定义一个函数模板来计算两个数的最大值。这个函数模板可以适用于不同类型的数,如整数、浮点数等。在编译过程中,当使用这个函数模板来处理具体类型的参数时,编译器会根据实际的类型生成对应的函数代码。比如,当传入两个整数时,编译器会生成一个处理整数类型的最大值函数;当传入两个浮点数时,编译器会生成一个处理浮点数类型的最大值函数。

动态多态主要是通过虚函数和继承来实现。在一个类层次结构中,基类定义了虚函数,派生类可以对这些虚函数进行重定义。当通过基类的指针或者引用调用虚函数时,实际调用的是派生类中重定义后的函数。例如,有一个交通工具类作为基类,它有一个行驶速度的虚函数。汽车类和自行车类是交通工具类的派生类,它们分别重定义了行驶速度函数来返回汽车和自行车的实际行驶速度。当通过交通工具类的指针或者引用指向汽车或者自行车对象并调用行驶速度函数时,就会根据对象的实际类型(汽车或者自行车)来返回相应的速度。这种在运行时根据对象的实际类型来决定函数调用的方式就是动态多态,它为程序提供了更大的灵活性,能够更好地处理复杂的对象层次结构和变化的对象行为。

静态和动态多态

静态多态和动态多态在 C++ 中是实现多态性的两种不同方式。

静态多态主要通过函数重载和模板来实现,在编译时期就确定了具体的函数调用。函数重载方面,以简单的数学运算函数为例。假设有一个名为 add 的函数,在同一作用域内,可以定义一个 add 函数用于两个整数相加,参数是两个整数;还可以定义另一个 add 函数用于两个浮点数相加,参数是两个浮点数。当在代码中调用 add 函数并传入参数时,编译器根据参数的类型在编译阶段就确定调用哪一个 add 函数。这种方式利用了编译器在编译时期对函数签名(包括函数名、参数类型、参数个数等)的检查来实现多态性。

模板也是实现静态多态的有效手段。例如,有一个模板函数用来计算一个数组中元素的平均值。这个模板函数可以接受不同类型的数组,如整数数组、浮点数数组等。在编译阶段,当使用这个模板函数处理具体类型的数组时,编译器会根据数组元素的类型生成相应的函数代码来计算平均值。所以,从本质上讲,静态多态是一种编译时的机制,它的优点是效率高,因为在编译阶段就已经确定了具体的执行路径,没有运行时的额外开销,如虚函数调用的开销。

动态多态主要依靠虚函数和继承来实现,它是在运行时根据对象的实际类型来确定函数调用。例如,有一个基类 Shape(代表形状),有成员函数 draw 用于绘制形状。派生类有 Circle(圆形)和 Rectangle(矩形)。在 Shape 类中将 draw 函数定义为虚函数,在 Circle 和 Rectangle 类中分别重新定义 draw 函数来绘制圆形和矩形。当通过 Shape 类的指针或引用指向 Circle 或 Rectangle 对象并调用 draw 函数时,实际调用的是 Circle 或 Rectangle 类中重新定义的 draw 函数。这种方式的优点是可以实现更灵活的对象行为,能够适应复杂的对象层次结构和动态变化的情况。不过,由于需要在运行时查找虚函数表来确定函数调用,会有一定的性能开销。

说说虚函数表

虚函数表(v - table)是 C++ 实现动态多态的关键机制。在一个包含虚函数的类中,编译器会为这个类创建一个虚函数表。这个表本质上是一个函数指针数组,数组中的每个元素指向一个虚函数的地址。

当一个类继承了另一个包含虚函数的类时,它会继承虚函数表。如果派生类重写了基类的虚函数,那么在派生类的虚函数表中,对应的虚函数指针会指向派生类重写后的函数。例如,假设有一个基类 Animal,它有一个虚函数 speak。还有两个派生类 Cat 和 Dog,它们分别重写了 speak 函数。在 Animal 类的虚函数表中,speak 函数指针指向 Animal 类定义的 speak 函数。在 Cat 类的虚函数表中,speak 函数指针指向 Cat 类重写后的 speak 函数,Dog 类同理。

每个包含虚函数的类的对象在内存中会包含一个虚函数指针(v - ptr),这个指针指向所属类的虚函数表。当通过基类指针或者引用调用虚函数时,程序会根据对象的虚函数指针找到对应的虚函数表,然后在表中查找并调用正确的虚函数。这种机制使得程序能够在运行时根据对象的实际类型(基类还是派生类)来决定调用哪个虚函数,从而实现动态多态。

虚函数表的存在使得类的布局稍微复杂一些。除了类的普通成员变量外,还需要额外的空间来存储虚函数指针。这个指针通常位于对象的开头部分,这样在访问虚函数时可以快速地通过指针找到虚函数表。而且,虚函数表在整个程序运行期间是只读的,因为它存储的是函数指针,这些指针在程序编译链接后就已经确定,不需要在运行时修改。

多态时候基类的 sizeof,子类的 sizeof

在涉及多态的情况下,基类和子类的 sizeof 大小会受到多种因素的影响。

对于基类,如果它包含虚函数,那么会有一个额外的虚函数指针(v - ptr)。这个指针的大小通常取决于编译器和系统架构。在 32 位系统中,指针大小一般是 4 字节,在 64 位系统中,指针大小一般是 8 字节。除了虚函数指针,基类的 sizeof 还包括普通成员变量的大小。例如,基类有一个 int 类型的成员变量,在 32 位系统下,如果没有虚函数,sizeof(基类)可能是 4 字节;如果有虚函数,就会加上虚函数指针的大小,可能是 8 字节。

当考虑子类时,情况会更复杂一些。子类会继承基类的所有成员,包括虚函数指针(如果基类有虚函数)。如果子类没有新增成员变量,那么它的 sizeof 至少和基类一样大,因为它包含了基类的所有内容。如果子类新增了成员变量,那么它的 sizeof 会在基类大小的基础上再加上新增成员变量的大小。例如,基类大小为 8 字节(包含虚函数指针和一个 int 成员变量),子类新增了一个 double 成员变量,在 64 位系统下,double 大小为 8 字节,那么子类的 sizeof 可能是 16 字节。

另外,如果子类重写了基类的虚函数,这不会影响子类的 sizeof 大小,因为虚函数的实现是通过虚函数表来管理的,重写虚函数只是改变了虚函数表中的指针指向,而不是增加或减少对象的实际存储空间。但是,如果子类定义了新的虚函数,那么它可能会有自己独立的虚函数表(取决于编译器的实现方式),这可能会对 sizeof 产生影响,可能会增加一个虚函数指针的大小。

为什么成员函数不占用类的空间

在 C++ 中,成员函数不占用类对象的空间主要是因为类的成员函数是所有该类对象共享的代码段。

从存储角度看,成员函数的代码在内存中只有一份副本。当创建多个类对象时,这些对象共享同一段成员函数的代码。例如,假设有一个简单的类 MyClass,它有一个成员函数 print。当创建多个 MyClass 对象时,这些对象在内存中的存储主要是它们自己的成员变量,而 print 函数的代码存储在代码区,不会在每个对象中都复制一份 print 函数的代码。

从调用机制来讲,成员函数通过一个隐式的 this 指针来访问对象的成员变量。当调用一个成员函数时,编译器会将对象的地址作为 this 指针传递给成员函数。这样,成员函数就可以根据 this 指针来访问和操作对应的对象的成员变量。例如,在 MyClass 的 print 函数中,通过 this 指针来访问 MyClass 对象的成员变量,即使多个对象调用 print 函数,由于 this 指针的不同,每个对象的成员变量都能被正确访问。

另外,这种设计也提高了内存的利用率。如果成员函数占用每个对象的空间,那么对于大型的类或者有大量对象的情况,会浪费大量的内存。而将成员函数存储在共享的代码区,只需要存储一份函数代码,大大节省了内存空间,同时也使得程序的运行更加高效,因为函数代码的加载和执行更加集中,不需要为每个对象单独处理成员函数的加载和执行。

运算符重载,几种构造函数(有参,无参,拷贝,还有一个构造函数)以及具体里面的形参是什么样?以及一些怎么用,加不加 const,用不用引用 &

运算符重载是 C++ 的一个强大特性,它允许自定义运算符的行为。例如,可以重载加法运算符 “+”,使得它能够用于自定义的类对象。

对于运算符重载,以加法运算符重载为例。如果有一个类 MyClass,想要重载 “+” 运算符,可以在类定义中定义一个名为 operator + 的函数。它的参数通常是一个 const 引用类型的该类对象,例如 operator +(const MyClass& other)。这个函数返回一个新的 MyClass 对象,表示两个对象相加的结果。在函数内部,可以根据类的成员变量来定义相加的具体逻辑。使用时,就可以像使用普通加法运算符一样,如 MyClass a,b;MyClass c = a + b。

有参构造函数用于在创建对象时初始化对象的成员变量。它的参数根据需要初始化的成员变量来确定。例如,有一个类 Point,有两个成员变量 x 和 y,有参构造函数可以是 Point(int newX,int newY),在函数体内可以通过 this - > x = newX;this - > y = newY;来初始化成员变量。使用时,可以直接创建对象并初始化,如 Point p(1,2)。

无参构造函数也叫默认构造函数,它没有参数。当没有为类定义任何构造函数时,编译器会自动生成一个默认构造函数。如果自己定义无参构造函数,例如 class MyClass {public:MyClass(){};},它主要用于创建对象时不需要传递初始参数的情况,如 MyClass m。

拷贝构造函数用于通过一个已有的对象来创建一个新的对象。它的参数通常是一个 const 引用类型的同类型对象,例如 MyClass(const MyClass& other)。在函数体内,可以通过成员变量的逐个赋值或者其他方式来复制对象。例如,在有一个类 MyClass 有成员变量 int value 的情况下,拷贝构造函数可以是 MyClass(const MyClass& other){this -> value = other.value;}。使用时,当需要复制一个对象时就会调用拷贝构造函数,如 MyClass a;MyClass b = a。

在构造函数中,加 const 可以用于修饰参数,如拷贝构造函数中的 const MyClass& other,表示在函数内部不能修改被引用的对象。使用引用 & 主要是为了避免对象的复制,提高效率,特别是在参数是大型对象的情况下。

如果构造函数加 private 会怎样

如果构造函数被定义为 private,会产生一些特殊的效果。

从对象创建角度看,在类的外部将无法直接创建该类的对象。因为构造函数是 private 的,类外部的代码没有权限调用构造函数来初始化一个新的对象。例如,有一个类 MyClass,它的构造函数是 private 的,在 main 函数或者其他外部函数中,不能使用 MyClass a;这样的方式来创建对象。

这种限制可以用于实现一些特殊的设计模式,比如单例模式。在单例模式中,只希望整个程序中存在一个该类的实例。通过将构造函数设为 private,可以防止外部随意创建对象。然后可以在类内部定义一个静态的成员函数来获取这个唯一的实例。例如,在 MyClass 类中,可以定义一个静态的 MyClass* getInstance()函数,在这个函数内部,如果还没有创建实例,就创建一个实例并返回,后续每次调用这个函数都返回这个唯一的实例。

从继承角度看,对于派生类,如果基类的构造函数是 private 的,那么派生类无法直接调用基类的构造函数来初始化基类部分。这就限制了继承的正常使用,除非在基类内部提供了一些特殊的接口或者友元函数来帮助派生类完成基类部分的初始化。例如,派生类在构造函数中默认会先调用基类的构造函数,如果基类构造函数是 private 的,编译器会报错,因为派生类没有权限调用这个构造函数。这种机制可以用于控制类的继承层次和对象的创建方式,使得类的使用更加符合特定的设计意图。

C 和 C++ 的区别

C 语言是一种过程式编程语言,而 C++ 是在 C 语言基础上发展而来的面向对象编程语言,它们有许多区别。

在编程范式方面,C 语言主要侧重于过程式编程。程序通常由一系列函数组成,通过函数调用和数据传递来完成任务。例如,编写一个简单的文件读取程序,在 C 语言中会重点关注如何打开文件、读取字节、处理错误等一系列步骤的函数实现。而 C++ 支持面向对象编程,它有类和对象的概念。可以将相关的数据和操作封装在一个类中,通过创建对象来使用这些数据和操作。比如创建一个学生类,包含学生的姓名、年龄等数据成员,以及获取姓名、设置年龄等成员函数。

在数据类型和变量方面,C 语言的基本数据类型如整型、浮点型等比较简单直接。变量的定义通常在代码块的开头部分。C++ 除了基本数据类型外,还引入了许多复杂的数据类型,像类、引用等。并且 C++ 允许变量在代码块的任何位置定义,这使得代码更加灵活。

从函数重载来看,C 语言不支持函数重载。每个函数必须有唯一的名称和参数列表。而 C++ 支持函数重载,在同一个作用域内,可以有多个同名函数,只要它们的参数列表(参数个数、类型或者顺序)不同。例如,在 C++ 中可以有两个名为 print 的函数,一个接受整数参数,一个接受字符数组参数,编译器会根据调用时的参数类型来选择正确的函数。

在内存管理上,C 语言主要通过函数如 malloc、free 来手动分配和释放内存。程序员需要精确地控制内存的使用,否则很容易出现内存泄漏、悬空指针等问题。C++ 也支持这种手动内存管理方式,但还引入了一些自动内存管理机制,如对象的构造函数和析构函数可以自动进行资源的初始化和清理。另外,C++ 还有智能指针等机制来帮助管理内存,减少内存错误。

在标准库方面,C 语言的标准库提供了一些基本的输入输出、字符串处理、数学运算等函数。C++ 标准库在 C 语言标准库的基础上进行了扩充,不仅包含了 C 语言的标准库功能,还新增了如容器类(vector、list 等)、算法(排序、查找等算法)、迭代器等功能强大的组件,方便程序员进行更高效的编程。

讲一讲 vector 的底层实现?是如何实现不定长数组的?拷贝复制的时候为什么是开辟两倍大小的内存空间?

vector 是 C++ 标准模板库中的一个容器,用于存储和管理一组同类型的元素,其底层实现主要基于动态数组。

vector 内部有三个重要的指针。一个是指向存储元素的内存块的起始位置的指针,用于访问数组元素;一个是指向已使用的内存块末尾的指针,用于确定元素的数量;还有一个是指向整个内存块末尾的指针,用于确定容量。当创建一个 vector 时,它会分配一定大小的内存空间来存储元素。例如,初始时可能分配一个较小的空间,如能容纳几个元素。

它实现不定长数组的方式是通过动态内存分配。当向 vector 中添加元素时,如果当前的内存空间足够,就直接将元素存储到已分配的内存中。但如果内存空间不足,vector 会重新分配一块更大的内存空间,将原来的元素复制到新的内存空间中,然后再添加新元素。这个重新分配内存的过程对用户是透明的。例如,当向一个初始容量为 5 的 vector 中添加第 6 个元素时,vector 会自动分配一块更大的内存(通常是按照一定的增长策略),比如新的容量可能是 10,然后将原来的 5 个元素复制过去,再添加新元素。

在拷贝复制 vector 时,有时候会开辟两倍大小的内存空间。这主要是为了减少频繁的内存重新分配操作。因为如果每次添加一个元素都重新分配内存,会带来很大的性能开销。通过开辟两倍大小的内存空间,当继续添加元素时,可以在新分配的较大空间内存储更多的元素,从而降低重新分配内存的频率。例如,一个 vector 当前容量为 10,当需要拷贝复制它时,新的 vector 可能会开辟 20 的容量,这样在后续添加元素时,就有更多的空间可以利用,而不是很快又要重新分配内存。这种策略在一定程度上平衡了内存使用效率和性能开销。

对 C++ 的内存回收有了解吗?

C++ 的内存回收主要涉及堆内存的释放。在 C++ 中,有手动内存管理和自动内存管理两种方式。

手动内存管理主要通过 new 和 delete 运算符来实现。当使用 new 运算符时,会在堆上分配一块内存,例如,通过 int* p = new int;就会在堆上分配一个能存储整数的内存空间,并且返回这个内存空间的指针。当这块内存不再需要时,需要使用 delete 运算符来释放,如 delete p;如果忘记释放内存,就会导致内存泄漏,即这块内存一直被占用,无法被其他程序使用。而且,如果对已经释放的内存或者无效的指针使用 delete,会导致程序出现未定义行为,比如悬空指针问题。

除了手动内存管理,C++ 还提供了自动内存管理机制来帮助减少内存错误。其中构造函数和析构函数在内存回收方面起到重要作用。当一个对象被创建时,构造函数会被调用,它可以进行资源的初始化,包括内存的分配等操作。当对象生命周期结束时,析构函数会被调用,它可以释放对象在构造过程中分配的资源。例如,一个类中有一个指针成员变量,在构造函数中使用 new 分配了内存,那么在析构函数中就应该使用 delete 来释放这块内存,这样可以保证对象占用的内存能够正确回收。

另外,C++ 中的智能指针也是内存回收的重要工具。智能指针可以自动管理所指向对象的生命周期。例如,shared_ptr 是一种智能指针,它通过引用计数来决定对象的释放。当一个 shared_ptr 指向一个对象时,引用计数为 1。当有其他 shared_ptr 也指向这个对象时,引用计数会增加。当一个 shared_ptr 不再指向这个对象(例如超出作用域或者被重新赋值),引用计数会减少。当引用计数为 0 时,就表示没有任何 shared_ptr 指向这个对象了,此时就会自动调用对象的析构函数来释放内存。

是否了解智能指针?为啥会使用弱指针?unique 指针与强指针的差别是什么?

智能指针是 C++ 中用于自动管理动态分配内存的对象。它可以有效避免手动使用 new 和 delete 带来的内存泄漏和悬空指针等问题。

shared_ptr 是一种智能指针,它采用引用计数的方式来管理内存。当创建一个 shared_ptr 指向一个对象时,引用计数为 1。如果有其他 shared_ptr 也指向这个对象,引用计数会增加。当一个 shared_ptr 超出其作用域或者被重新赋值,不再指向该对象时,引用计数会减少。当引用计数变为 0 时,就会自动调用对象的析构函数来释放内存。例如,在一个函数中创建了一个 shared_ptr 指向一个动态分配的对象,然后将这个 shared_ptr 返回给调用者。调用者可以继续使用这个 shared_ptr,并且只要还有 shared_ptr 指向这个对象,它就不会被释放。

weak_ptr 是一种辅助的智能指针。它主要用于解决 shared_ptr 可能出现的循环引用问题。当两个或多个对象通过 shared_ptr 相互引用时,它们的引用计数可能永远不会变为 0,从而导致内存无法释放。weak_ptr 可以指向一个由 shared_ptr 管理的对象,但它不会增加引用计数。例如,在一个树形结构中,节点之间通过 shared_ptr 相互指向,如果没有 weak_ptr,可能会因为循环引用而无法正确释放内存。通过使用 weak_ptr,可以观察对象是否还存在,并且在需要时可以通过 lock 函数将其转换为 shared_ptr 来访问对象。

unique_ptr 是一种独占式的智能指针。它和 shared_ptr 的主要区别在于所有权。unique_ptr 对其所指向的对象拥有独占所有权,不能被复制,只能被移动。这意味着一个对象只能有一个 unique_ptr 指向它。例如,当将一个 unique_ptr 赋值给另一个 unique_ptr 时,实际上是将所有权转移,原来的 unique_ptr 会失去对对象的所有权,不能再访问该对象。这种独占式的设计使得 unique_ptr 在资源管理上更加严格,适用于那些需要确保只有一个对象对资源进行管理的场景,比如对文件句柄等资源的管理。

了解智能指针吗?shared_ptr 内部具体怎么完成内存回收的?

shared_ptr 是 C++ 中一种非常重要的智能指针,它用于自动管理动态分配的内存。

在 shared_ptr 内部有一个引用计数机制来完成内存回收。当创建一个 shared_ptr 并指向一个对象时,会在内部维护一个引用计数,这个引用计数初始值为 1。这个引用计数的存储位置和实现方式是由编译器和标准库来决定的,一般是在一个单独的控制块中。

当通过拷贝构造函数或者赋值运算符将一个 shared_ptr 赋值给另一个 shared_ptr 时,引用计数会增加。例如,有 shared_ptr<int> p1(new int(1)),然后 shared_ptr<int> p2 = p1;此时 p1 和 p2 都指向同一个整数对象,这个对象的引用计数就会变为 2。

在 shared_ptr 的对象生命周期结束时,例如当一个 shared_ptr 超出它的作用域或者被重新赋值时,引用计数会减少。当引用计数变为 0 时,就表示没有任何 shared_ptr 指向这个对象了。

此时,shared_ptr 会自动调用所指向对象的析构函数来释放对象占用的内存。这个过程是由 shared_ptr 的析构函数来触发的。在析构函数中,会检查引用计数,如果为 0,就会执行删除操作。这个删除操作会根据对象的类型和分配方式来进行。如果对象是通过 new 分配的,就会使用 delete 来释放内存;如果是通过其他自定义的内存分配方式,也会按照相应的方式来释放内存。

此外,shared_ptr 还可以自定义删除器。默认情况下,它使用 delete 来释放内存,但如果对象的内存分配方式比较特殊,比如使用了自定义的内存池,就可以通过自定义删除器来指定正确的内存释放方式。自定义删除器是一个可调用对象,例如函数、函数对象或者 lambda 表达式,它会在引用计数为 0 时被调用,来完成对象的内存释放操作。

函数内部对象的生存域问题?

在函数内部定义的对象,其生存域主要局限于函数内部。当函数被调用时,这些对象被创建,当函数执行结束后,对象就会被销毁。

从栈内存角度来看,函数内部的自动存储对象(非静态局部变量)是在栈上分配内存的。例如,当定义一个简单的函数,在函数内部有一个整型变量,这个变量在函数被调用时,在栈上分配空间,它的生命周期从定义点开始,一直到函数返回。一旦函数返回,栈帧被弹出,这个变量所占用的内存空间就被释放,其中存储的值也随之丢失。

如果在函数内部定义了静态局部变量,情况则有所不同。静态局部变量在程序的整个生命周期内都存在,但它的作用域仍然局限于函数内部。它只在第一次进入函数时被初始化,后续再次进入函数时,不会再次初始化。例如,有一个函数,内部有一个静态的计数器变量,每次调用这个函数,计数器的值都会更新,而且这个值会在多次调用之间保持,因为静态变量存储在全局数据区,而不是栈上。

对于在函数内部通过动态内存分配(new 操作符)创建的对象,它们存储在堆内存中。这些对象的生存域从它们被创建开始,直到通过 delete 操作符手动释放内存或者程序结束。如果在函数内部没有正确释放动态分配的内存,就会导致内存泄漏。例如,在一个函数中用 new 创建了一个数组,但是在函数结束时没有用 delete [] 来释放,那么这个数组所占用的内存就一直被占用,直到程序结束。

在函数内部还可能出现对象作为参数传递或者作为返回值的情况。当对象作为参数传递时,根据参数的传递方式(值传递、引用传递或者指针传递),对象的生存域本身不受影响,但在函数内部可以通过不同的方式访问和操作对象。如果是值传递,会创建一个对象的副本,副本的生存域和函数内部其他自动变量一样;如果是引用传递或者指针传递,可以在函数内部操作原始对象,原始对象的生存域取决于它最初的定义位置。当对象作为返回值时,会根据返回值的类型和处理方式来确定对象的生存域。如果返回的是一个自动存储类型的对象,在函数返回后对象会被销毁,除非返回的是一个引用或者指针,并且在调用函数的环境中有合适的方式来管理这个对象。

全局变量的可访问情况?静态全局变量可访问情况?

全局变量是在所有函数和类的外部定义的变量,它的生命周期贯穿整个程序的运行过程。

从可访问性角度来看,在同一个源文件中,全局变量可以被文件内的任何函数访问。这是因为全局变量的作用域是从它的定义点开始,一直到文件末尾。例如,在一个源文件中定义了一个全局整型变量 globalVar,那么在这个文件中的任何函数,如函数 A 和函数 B,都可以直接访问和修改 globalVar 的值。

然而,当涉及多个源文件时,情况会稍微复杂一些。如果一个源文件中的全局变量要被其他源文件访问,需要使用 extern 关键字进行声明。在另一个源文件中,通过 extern 声明这个全局变量后,就可以访问它了。但是,这种跨文件访问全局变量可能会导致一些问题,比如命名冲突和代码的耦合性增加。

静态全局变量是一种特殊的全局变量。它的生命周期也是整个程序的运行过程,但它的作用域被限制在定义它的源文件内部。这意味着在同一个源文件中,静态全局变量可以像普通全局变量一样被文件内的函数访问。例如,在一个源文件中有一个静态全局变量 staticGlobalVar,这个文件中的函数可以访问和修改它。

但是,与普通全局变量不同的是,静态全局变量不能被其他源文件访问。即使在其他源文件中使用 extern 关键字声明,也无法访问这个静态全局变量。这种限制使得静态全局变量在一定程度上提高了程序的模块性,因为它可以防止变量被其他文件随意访问和修改,从而减少了不同文件之间的相互干扰和错误的可能性。

数组和链表的区别,他们的访问效率怎样?

数组和链表是两种常见的数据结构,它们有诸多区别,并且在访问效率方面也各有特点。

从存储结构上看,数组是一种连续的存储结构。它在内存中占用一块连续的存储空间,所有元素按照顺序依次存储。例如,一个整型数组,每个元素的大小是固定的(在 32 位系统中通常为 4 字节),如果数组的起始地址为 A,那么第 n 个元素的地址就是 A + n * sizeof(元素类型)。这种连续的存储方式使得数组可以通过下标直接访问元素,访问速度非常快。

链表则是一种非连续的存储结构。它由一系列节点组成,每个节点包含数据部分和指向下一个节点(在单向链表中)的指针。节点在内存中的位置可以是任意的,它们通过指针相互连接。例如,在一个简单的单向链表中,每个节点除了存储数据外,还有一个指针指向链表中的下一个节点。

在访问效率方面,数组的随机访问效率很高。因为可以通过计算元素的地址直接访问,时间复杂度为 O (1)。例如,对于一个长度为 n 的数组,无论要访问第 1 个元素还是第 n 个元素,时间花费基本相同。但是,数组的插入和删除操作相对复杂。如果要在数组中间插入一个元素,需要将插入位置之后的所有元素向后移动一位,时间复杂度为 O (n);删除操作类似,也可能需要移动大量元素。

链表的随机访问效率较低。因为要访问链表中的某个元素,需要从链表头开始逐个节点遍历,平均时间复杂度为 O (n)。例如,要访问链表中的第 n 个元素,可能需要遍历 n - 1 个节点才能找到。但是,链表的插入和删除操作相对简单。在合适的位置插入一个节点,只需要修改相关节点的指针,时间复杂度为 O (1),不过如果要先找到插入位置,这个过程可能需要遍历链表,时间复杂度又会变为 O (n)。

在空间利用方面,数组需要预先分配一定大小的连续空间。如果预先分配的空间过大,会造成空间浪费;如果空间不足,可能需要重新分配更大的空间并移动元素。链表则根据节点的数量动态分配空间,每个节点只需要额外的空间来存储指针,空间利用相对灵活。

vector 的 api 问题?reverse (),reserve (),size (),capbility () 都是什么?

vector 是 C++ 标准模板库中的一个重要容器,它提供了许多实用的 API。

size () 函数用于返回 vector 中当前存储的元素数量。例如,有一个 vector<int> v,通过 v.size () 可以知道这个 vector 中已经存储了几个整数。它的时间复杂度为 O (1),因为 vector 内部会维护一个变量来记录元素的数量,直接返回这个变量的值即可。

capacity () 函数用于返回 vector 当前能够容纳的元素数量,也就是 vector 在不重新分配内存的情况下最多能存储的元素数量。例如,当创建一个 vector 时,它可能会分配一定的初始容量,通过 capacity () 可以查看这个容量大小。这个值通常大于等于 size () 的值。当不断向 vector 中添加元素,当 size () 等于 capacity () 时,如果再添加元素,vector 可能会重新分配更大的内存来增加容量。

reserve () 函数用于手动设置 vector 的容量。例如,vector<int> v,通过 v.reserve (100) 可以将 vector 的容量设置为 100。这意味着 vector 会分配足够的内存来容纳 100 个整数元素。这个操作主要用于提前预留足够的空间,以避免在后续添加元素过程中频繁地重新分配内存,提高性能。需要注意的是,reserve () 只是改变容量,不会改变 vector 中元素的数量,也就是 size () 的值不会因为 reserve () 操作而改变。

reverse () 函数用于反转 vector 中的元素顺序。例如,有一个 vector<int> v = {1, 2, 3},通过 v.reverse () 操作后,v 中的元素顺序变为 {3, 2, 1}。这个操作会改变 vector 中元素的实际存储顺序,时间复杂度为 O (n),其中 n 是 vector 中元素的数量,因为需要逐个交换元素的位置来实现反转。

指针和引用的区别?

指针和引用是 C++ 中两个容易混淆但又有重要区别的概念。

从定义和语法上看,指针是一个变量,它存储的是另一个变量的地址。例如,定义一个指针 int* p;,可以通过 & 运算符获取一个变量的地址并赋值给指针,如 int a = 10; p = &a;。引用是一个别名,它在定义的时候必须初始化,并且之后不能再引用其他变量。例如,定义一个引用 int& r = a;,这里 r 就是 a 的别名,对 r 的操作就是对 a 的操作。

在内存占用方面,指针本身占用一定的内存空间,其大小取决于系统的位数。在 32 位系统中,指针大小通常是 4 字节,在 64 位系统中,指针大小通常是 8 字节。引用本身不占用额外的内存空间,它只是一个变量的别名,和被引用的变量共享同一块内存。

从使用方式和安全性上看,指针可以进行一些比较灵活的操作。比如,指针可以被重新赋值指向其他变量,也可以进行算术运算(如指针加 1、减 1 等操作),但是这种灵活性也带来了一些风险。如果指针使用不当,很容易导致悬空指针(指向已经释放的内存)或者野指针(未初始化的指针)等问题,从而引发程序错误。引用相对来说更加安全,因为它在定义后就不能再引用其他变量,而且引用必须绑定到一个已经存在的对象上,所以不会出现悬空引用的情况。

在作为函数参数传递时,指针和引用都可以用来修改函数外部的变量。但是指针传递需要解引用操作,而引用直接使用即可。例如,有一个函数用于修改一个整数变量的值,通过指针传递可以这样定义:void modifyValue (int* p) {(*p)++;},调用时需要传递变量的地址;通过引用传递可以这样定义:void modifyValue (int& r) {r++;},调用时直接传递变量本身。

另外,在多态性方面,指针可以用于实现动态多态。通过基类指针可以指向派生类对象,并且根据对象的实际类型来调用虚函数。引用也可以用于实现类似的动态多态,通过基类引用可以引用派生类对象,在调用虚函数时也会根据对象的实际类型进行调用。

可以多重指针、多重引用吗?

在 C++ 中,可以有多重指针和多重引用,但它们在概念和使用方式上有一些不同。

对于多重指针,例如双重指针。一个双重指针是一个指针,它指向的是另一个指针。可以通过多次使用取地址符和指针声明来创建。比如,有一个整数变量int a = 10;,可以定义一个指针int* p = &a;,这是一个普通的指针,它存储变量a的地址。然后可以定义一个双重指针int** pp = &p;,这里pp存储的是指针p的地址。多重指针在处理动态二维数组等情况时非常有用。假设要创建一个动态的二维数组,首先要分配一个指针数组,每个指针再指向一个一维数组。可以通过双重指针来操作这个二维数组结构。在访问元素时,需要进行多次解引用操作。例如,要访问二维数组arr的第i行第j列的元素,可以写成(*(*(arr + i)) + j)。这种多层间接访问的方式使得多重指针的使用相对复杂,但在一些特定的内存管理和复杂数据结构场景中是必不可少的。

对于多重引用,C++ 不允许像指针那样直接的多重引用。引用在定义时必须直接绑定到一个对象,不能像指针一样,一个引用再去引用另一个引用。不过,可以通过引用的引用作为函数参数来模拟一种类似的效果。例如,有一个函数,它的参数是引用的引用int&&&,这种情况比较少见,通常用于模板编程或者一些特殊的重载决议场景。而且在实际使用中,引用的引用很容易引起混淆,因为它不符合引用作为别名的直观概念。总的来说,多重指针在 C++ 中有实际的应用场景,而多重引用相对较少使用,并且使用规则比较特殊。

不同类型的指针本身大小是一样的吗?怎么确定一个指针的大小?

在同一平台下,不同类型的指针本身大小是一样的。指针的大小主要取决于计算机的寻址能力,也就是处理器的位数。

在 32 位的计算机系统中,指针的大小通常是 4 字节。这是因为 32 位系统的地址总线是 32 位,能够表示的最大地址范围是2^32个不同的地址,而每个地址在内存中需要用 4 字节来存储。例如,无论是指向int类型、char类型还是自定义结构体类型的指针,它们的大小都是 4 字节。这是因为指针存储的是内存地址,而不是它所指向的数据的内容。

在 64 位的计算机系统中,指针的大小通常是 8 字节。因为 64 位系统的地址总线是 64 位,能够表示的最大地址范围是2^64个不同的地址,需要 8 字节来存储这些地址。同样,不管指针指向的数据类型是什么,指针本身的大小是固定的,由系统的架构决定。

要确定一个指针的大小,可以使用sizeof运算符。例如,对于一个指向int类型的指针int* p;,可以通过sizeof(p)来获取指针p的大小。这个操作会返回指针在当前系统下占用的字节数。需要注意的是,sizeof是一个编译时运算符,它根据指针变量的类型在编译阶段就确定了返回值,而不是根据指针所指向的实际对象的大小。所以,即使指针可能指向不同大小的数据类型,指针本身的大小是由系统决定的,并且可以通过sizeof方便地进行测量。

函数指针与指针函数辨别

函数指针和指针函数是两个容易混淆的概念,它们在定义、用途和语法等方面都有明显的区别。

首先来看指针函数。指针函数本质上是一个函数,只不过这个函数的返回值是一个指针。例如,定义一个指针函数int* func(int a, int b),这个函数接受两个整数参数ab,然后返回一个指向整数的指针。在函数体内部,它可能会进行一些计算或者内存分配操作,最后返回一个指针。比如,这个函数可以在堆上分配一块内存来存储两个整数相加的结果,然后返回指向这个结果的指针。从调用角度看,当调用一个指针函数时,就像调用普通函数一样,只是返回的是一个指针。例如,int* result = func(1, 2);,这里func函数执行后返回一个指向整数的指针,这个指针被赋值给result变量。

而函数指针是一个指针,它指向的是一个函数。例如,定义一个函数指针int (*fp)(int, int);,这里fp是一个指针,它可以指向一个接受两个整数参数并返回一个整数的函数。可以将一个符合函数指针类型的函数的地址赋值给这个函数指针。例如,有一个函数int add(int a, int b),可以通过fp = &add;或者fp = add;(在 C++ 中函数名本身就代表函数的地址)将add函数的地址赋值给fp。一旦赋值完成,就可以通过函数指针来调用它所指向的函数,如int sum = (*fp)(3, 4);或者int sum = fp(3, 4);(这两种调用方式在 C++ 中都是合法的)。函数指针在实现回调函数、函数表等场景中有广泛的应用。比如,在一个图形库中,可以通过函数指针来实现不同的绘图函数,根据用户的选择或者程序的状态来调用不同的绘图函数。

静态成员变量、成员函数与静态成员函数区别

在 C++ 中,静态成员变量、成员函数和静态成员函数有明显的区别,它们在类的设计和使用中发挥着不同的作用。

静态成员变量是属于整个类的变量,而不是属于类的某个对象。它在类的所有对象之间共享。例如,假设有一个Student类,其中有一个静态成员变量totalStudents,这个变量用于记录总共创建了多少个学生对象。不管创建了多少个Student对象,totalStudents变量只有一份。它的定义通常在类内部声明,在类外进行定义和初始化。例如,在类内部可以这样声明static int totalStudents;,在类外可以通过int Student::totalStudents = 0;来定义和初始化。可以通过类名或者对象来访问静态成员变量,不过通过类名访问更加规范,如Student::totalStudents++;

成员函数是属于类的对象的函数,它可以访问和操作对象的成员变量。每个对象都有自己的一套成员函数,这些成员函数可以通过对象来调用。例如,Student类中有一个成员函数printInfo,它可以打印出学生对象的姓名、年龄等信息。当创建多个Student对象时,每个对象的printInfo函数在调用时操作的是各自对象的成员变量。

静态成员函数是属于整个类的函数,它和静态成员变量类似,不依赖于某个具体的对象。它主要用于操作静态成员变量或者执行一些与类相关但不依赖于对象状态的操作。例如,在Student类中可以有一个静态成员函数getTotalStudents,它的作用是返回totalStudents的值。静态成员函数不能访问非静态成员变量,因为非静态成员变量是属于对象的,而静态成员函数没有和具体的对象关联。它的调用方式可以通过类名直接调用,如Student::getTotalStudents();

静态全局变量作用域

静态全局变量是一种特殊的全局变量,它的生命周期贯穿整个程序的运行过程,但其作用域被限制在定义它的源文件内部。

从作用域角度看,在定义静态全局变量的源文件中,它可以被文件内的任何函数访问。例如,在一个名为main.cpp的源文件中有一个静态全局变量static int globalVar;,那么在main.cpp中的函数funcAfuncB都可以访问和修改globalVar的值。这和普通全局变量在源文件内的访问情况类似。

然而,与普通全局变量不同的是,静态全局变量不能被其他源文件访问。即使在其他源文件中使用extern关键字声明,也无法访问这个静态全局变量。假设在main.cpp中有一个静态全局变量,在另一个utils.cpp文件中想要通过extern声明来访问这个变量是不被允许的。这种限制使得静态全局变量在一定程度上提高了程序的模块性,因为它可以防止变量被其他文件随意访问和修改,从而减少了不同文件之间的相互干扰和错误的可能性。这样可以更好地将程序的不同功能模块进行隔离,每个源文件可以独立地管理自己的静态全局变量,而不用担心其他文件的影响。

解释一下重载(函数名相同,参数不同)

函数重载是 C++ 中一个非常重要的特性。它允许在同一个作用域内有多个函数具有相同的名字,但这些函数的参数列表必须不同。参数不同可以体现在参数的个数、参数的类型或者参数的顺序上。

从目的上看,函数重载主要是为了方便程序员使用更加自然和直观的函数命名方式。例如,对于一个处理加法运算的函数,可以有一个版本用于处理两个整数相加,另一个版本用于处理两个浮点数相加。如果没有函数重载,可能需要为这些相似功能的函数起不同的名字,如addIntaddFloat,这样会使得代码的可读性变差。而通过函数重载,可以都命名为add,编译器会根据调用时所传递的参数类型来决定调用哪个具体的add函数。

在参数个数方面,假设有一个函数print,可以有一个版本是print(int num),用于打印一个整数,还有一个版本是print(int num1, int num2),用于打印两个整数。在调用函数时,如print(1)就会调用第一个版本,print(1, 2)就会调用第二个版本。

在参数类型上,继续以print函数为例,除了上述整数版本,还可以有print(float num)用于打印一个浮点数。当调用print(1.0f)时,编译器就会选择这个浮点数版本的print函数。

关于参数顺序,假设有一个函数operation(int num1, float num2)operation(float num1, int num2),虽然它们的参数个数和类型组合相同,但顺序不同,这也构成了函数重载。当调用operation(1, 2.0f)operation(2.0f, 1)时,编译器会根据参数的顺序来选择不同的函数。

编译器在处理函数重载时,是在编译阶段进行的。它会根据函数调用时所提供的参数信息,通过一种称为重载决议的过程来确定到底调用哪个函数。这个过程涉及到对函数参数类型的匹配、类型转换等规则的应用。例如,如果有一个函数调用print(1.0)(这里是一个双精度浮点数),编译器会寻找一个能够最好地匹配这个参数类型的print函数,可能会选择print(float num)这个版本,并可能会进行一些隐式的类型转换。

知道操作系统中代码的局部性吗?三级缓冲了解吗?

在操作系统中,代码的局部性是一个重要的概念,它主要包括时间局部性和空间局部性。

时间局部性是指如果一个存储单元被访问,那么在不久的将来,这个存储单元很可能会被再次访问。例如,在一个循环中,循环变量和循环体内使用的变量会被频繁地访问。每次循环执行时,这些变量的值都会被读取或者修改。以一个简单的数组求和的循环为例,对于数组中的每个元素,都会对其进行读取操作,而且在整个求和过程中,会多次访问这些元素,这体现了时间局部性。因为程序倾向于在短时间内反复使用相同的数据。

空间局部性是指如果一个存储单元被访问,那么它附近的存储单元在不久的将来也很可能被访问。这是基于程序的顺序执行特性。在内存中,数据通常是连续存储的。例如,在访问一个数组元素时,由于数组在内存中是连续存储的,当访问了一个元素后,很可能会紧接着访问它的下一个元素。同样,在代码段中,当执行一条指令后,很可能会执行它后面的指令。比如,在一个函数中,函数的局部变量在内存中是相邻存储的,当访问了一个局部变量后,很可能会访问其他相邻的局部变量。

三级缓冲是一种在计算机存储体系中用于提高数据访问效率的机制。它包括 L1、L2 和 L3 缓存。

L1 缓存是最靠近 CPU 核心的缓存,它的容量最小,但速度最快。L1 缓存通常分为指令缓存和数据缓存,分别用于存储即将执行的指令和数据。由于它的速度非常快,能够在极短的时间内为 CPU 提供所需的数据和指令,减少 CPU 等待数据的时间。

L2 缓存的容量比 L1 缓存大,速度稍慢。它用于存储那些在 L1 缓存中没有命中的数据和指令。当 CPU 在 L1 缓存中找不到所需的数据时,会首先在 L2 缓存中查找。

L3 缓存是三级缓存中容量最大的,速度相对 L1 和 L2 缓存最慢,但比主内存快很多。它是多个 CPU 核心共享的缓存,用于存储从主内存中读取的数据和指令。当 L1 和 L2 缓存都没有命中时,就会在 L3 缓存中查找。通过这三级缓存的协同工作,可以大大提高 CPU 对数据和指令的访问速度,充分利用代码的局部性原理,减少 CPU 因为等待数据而闲置的时间,从而提高整个计算机系统的性能。

线程的五种状态(创建、就绪、阻塞、执行、销毁)

线程在其生命周期内会经历五种主要状态:创建、就绪、阻塞、执行和销毁。

创建状态是线程生命周期的起始阶段。当通过编程语言提供的线程创建机制(如在 C++ 中使用std::thread类)启动一个线程时,线程就进入了创建状态。在这个阶段,操作系统会为线程分配必要的资源,如线程控制块、栈空间等。不过此时线程还没有开始执行,它只是完成了初始化的步骤,就像一个运动员在比赛前的准备阶段,场地和装备都准备好了,但还没开始比赛。

就绪状态是指线程已经准备好执行,它等待 CPU 的调度。此时线程已经完成了初始化,并且已经具备了执行的条件,只是 CPU 资源还没有分配给它。可以把它想象成运动员已经站在起跑线上,等待发令枪响,只要 CPU 这个 “发令枪” 响起,线程就可以开始执行。在多线程操作系统中,会有多个线程处于就绪状态,操作系统的调度程序会根据一定的调度算法(如时间片轮转、优先级调度等)来选择其中一个线程分配 CPU 资源。

执行状态是线程正在 CPU 上运行的状态。此时线程正在执行它的任务,比如一个计算密集型线程可能正在进行复杂的数学运算,一个 I/O 密集型线程可能正在等待网络数据或者文件读取操作完成。线程在执行状态下会一直占用 CPU 资源,直到它主动放弃(如通过线程让步操作)或者被更高优先级的线程抢占(如果操作系统采用抢占式调度)。

阻塞状态是线程因为某些原因暂时无法继续执行而等待的状态。常见的原因包括等待 I/O 操作完成(如等待从磁盘读取文件或者从网络接收数据)、等待获取某个锁(互斥锁、信号量等)或者等待某个条件变量满足。例如,当一个线程发起一个网络请求后,它会进入阻塞状态,直到收到网络响应。在阻塞状态下,线程不会占用 CPU 资源,直到它所等待的事件发生,然后它会重新进入就绪状态,等待 CPU 的调度再次进入执行状态。

销毁状态是线程生命周期的结束阶段。当线程完成了它的任务或者因为某种异常情况(如出现不可恢复的错误)而终止时,线程就会进入销毁状态。在这个阶段,操作系统会回收线程所占用的资源,如释放栈空间、清理线程控制块等。这就好比运动员完成比赛后,清理场地和装备,结束整个比赛过程。

什么时候阻塞(I/O 占用时,释放后回到就绪或者执行)

线程阻塞主要发生在以下几种情况。

当线程进行 I/O 操作时,这是最常见的导致阻塞的情况。例如,当一个线程发起文件读取操作时,它需要等待磁盘驱动器将文件数据传输到内存中。在这个过程中,线程会进入阻塞状态。因为 I/O 设备(如磁盘、网络接口等)的速度通常比 CPU 慢很多,线程在等待 I/O 操作完成的过程中没有其他事情可做,所以将 CPU 资源让出来,等待数据准备好。一旦 I/O 操作完成,比如文件数据已经全部读取到内存中,线程会根据操作系统的调度机制,要么直接回到执行状态(如果 CPU 资源正好空闲并且调度算法允许),要么回到就绪状态等待 CPU 的调度,然后再进入执行状态继续处理读取到的数据。

另一种情况是线程等待获取锁时会阻塞。在多线程编程中,为了保护共享资源,会使用锁(如互斥锁)。当一个线程尝试获取一个已经被其他线程持有的锁时,它会进入阻塞状态。例如,有两个线程都需要访问和修改一个共享的全局变量,为了保证数据的一致性,使用互斥锁来保护这个变量。如果一个线程已经获取了锁并在修改变量,另一个线程尝试获取这个锁时就会阻塞。只有当持有锁的线程释放锁后,等待的线程才能获取锁,然后从阻塞状态回到就绪状态或者执行状态。

线程在等待条件变量满足时也会阻塞。条件变量通常和互斥锁一起使用,用于实现线程间的同步。例如,有一个生产者 - 消费者模型,生产者线程生产数据并将其放入缓冲区,消费者线程从缓冲区中获取数据。当缓冲区为空时,消费者线程会等待一个条件变量(表示缓冲区有数据),此时它会进入阻塞状态。当生产者线程生产了数据并放入缓冲区后,会通过信号通知条件变量,使消费者线程从阻塞状态中解脱出来,回到就绪状态或者执行状态。

锁了解过吗(互斥、条件变量、自旋等)

在多线程编程中,锁是用于控制多个线程对共享资源访问的重要机制。

互斥锁是一种最常见的锁。它用于保证在同一时刻只有一个线程能够访问被保护的共享资源。例如,有一个全局变量被多个线程访问和修改,为了防止数据不一致,就可以使用互斥锁。当一个线程想要访问这个变量时,它首先要获取互斥锁。如果此时锁没有被其他线程持有,那么这个线程就可以成功获取锁并访问变量;如果锁已经被其他线程持有,那么这个线程就会阻塞,直到锁被释放。互斥锁的实现原理基于原子操作,它能够确保在多个线程竞争锁的过程中,操作的原子性,即要么完整地获取锁,要么等待。在 C++ 中,可以使用std::mutex来实现互斥锁。例如,在一个函数中访问共享资源,可以先创建一个std::mutex对象,在访问资源前调用lock方法获取锁,访问结束后调用unlock方法释放锁。

条件变量通常和互斥锁一起使用,用于实现线程之间的同步和通信。它允许一个线程等待某个条件满足后再继续执行。例如,在生产者 - 消费者模型中,消费者线程等待缓冲区中有产品,生产者线程生产产品后通知消费者。可以使用std::condition_variable来实现这种同步。当消费者线程发现缓冲区为空时,它会在与缓冲区相关的条件变量上等待,同时释放互斥锁(因为等待过程中不需要占用锁)。当生产者线程生产了产品放入缓冲区后,它会通知条件变量,使等待的消费者线程被唤醒,重新获取互斥锁并检查缓冲区是否有产品。

自旋锁是另一种类型的锁。与互斥锁不同的是,当一个线程尝试获取自旋锁但锁已经被占用时,这个线程不会阻塞,而是会在一个循环中不断地检查锁是否被释放。自旋锁适用于等待时间较短的情况。因为如果一个线程长时间自旋,会浪费 CPU 资源。例如,在多核处理器环境下,当一个线程等待的锁很快就会被释放时,使用自旋锁可以避免线程进入阻塞状态和重新调度的开销,快速地获取锁并继续执行。但是如果等待时间过长,自旋锁可能会导致 CPU 利用率过高,因为线程一直在空转检查锁的状态。

锁可以锁变量、可以锁代码段,举个例子,当一个线程对某一代码段加锁,另一个线程可以修改这段代码中用到的全局变量吗?

当一个线程对某一代码段加锁后,另一个线程在该锁未释放的情况下通常不能修改这段代码中用到的全局变量。

假设我们有一个全局变量globalVariable,还有一个互斥锁mutex用于保护对这个全局变量的访问。有两个线程Thread1Thread2。在Thread1中有一段代码被加锁,如下:

std::mutex mutex;
int globalVariable = 0;
void Thread1Function() {
    mutex.lock();
    // 对globalVariable进行操作
    globalVariable++;
    mutex.unlock();
}

Thread2中如果也想对globalVariable进行修改,例如:

void Thread2Function() {
    // 尝试修改globalVariable
    globalVariable--;
}

如果Thread1已经获取了锁并且还没有释放,Thread2在执行到修改globalVariable的代码时,就会出现问题。如果这个锁是互斥锁,Thread2会被阻塞,直到Thread1释放锁。这是因为互斥锁保证了在同一时刻只有一个线程能够访问被锁保护的代码段和其中涉及的共享资源(在这里就是全局变量)。

但是,如果没有正确使用锁,比如忘记加锁或者使用了不合适的锁机制,就可能会导致数据不一致。例如,如果使用的是自旋锁,并且等待时间过长,可能会出现一些意外情况。或者如果在代码中有其他路径可以绕过锁直接访问全局变量,也会破坏数据的一致性。所以,在多线程编程中,要确保对共享资源(如全局变量)的访问都通过正确的锁机制来控制,以保证数据的完整性和一致性。

进程和线程的区别

进程和线程是操作系统中两个重要的概念,它们有许多区别。

从资源分配角度看,进程是资源分配的基本单位。一个进程拥有自己独立的地址空间,包括代码段、数据段、堆、栈等。这意味着每个进程都有自己独立的一套资源,如内存空间、文件描述符、设备资源等。例如,当启动两个不同的应用程序(如一个文本编辑器和一个浏览器),它们分别是两个不同的进程,每个进程都有自己独立的内存区域来存储程序代码、数据和运行时的栈等。而线程是进程内部的执行单元,它共享所属进程的资源。多个线程在同一个进程中共享代码段、数据段、堆等资源,它们只有自己独立的栈空间。这就好比一个工厂(进程)有多个工人(线程),工厂的设备(资源)是大家共享的,但是每个工人有自己的工作空间(栈)。

在调度方面,进程和线程也有所不同。进程之间的切换开销较大,因为操作系统需要保存和恢复整个进程的上下文,包括内存映射、文件描述符状态等众多信息。而线程之间的切换开销相对较小,因为它们共享大部分进程资源,只需要保存和恢复线程自己的栈指针、程序计数器等少量信息。例如,在一个多任务操作系统中,当从一个文本编辑进程切换到一个浏览器进程时,操作系统需要做大量的工作来切换资源;而在一个多线程的进程内部,从一个线程切换到另一个线程,操作相对简单。

从并发性和独立性角度看,进程之间相对独立,一个进程的崩溃通常不会直接影响其他进程。例如,一个游戏进程崩溃了,不会影响同时运行的办公软件进程。线程则不同,由于它们共享进程的资源,一个线程出现问题(如访问非法内存地址)可能会导致整个进程崩溃。不过,线程在并发执行方面更具优势,因为多个线程可以在同一个进程中更高效地共享数据和协同工作,能够更好地利用多核处理器的资源,提高程序的执行效率。

进程间通信方式

进程间通信(IPC)有多种方式,每种方式都有其特点和适用场景。

管道(Pipe)是一种简单的进程间通信方式,它分为无名管道和有名管道。无名管道主要用于具有亲缘关系(如父子进程)的进程之间通信。它是一个半双工的通信通道,即数据只能单向流动。例如,在父进程中创建一个管道,然后通过fork函数创建子进程,父子进程可以分别关闭管道的一端,通过剩下的一端进行数据传输。有名管道则可以用于无亲缘关系的进程之间通信,它有一个文件名,通过这个文件名,不同的进程可以打开管道进行通信。

消息队列(Message Queue)是一种消息传递机制。它允许进程将消息发送到一个队列中,其他进程可以从这个队列中接收消息。消息队列中的消息是有格式的,并且可以根据消息类型进行区分。例如,在一个客户 - 服务器模型中,客户端进程可以将请求消息发送到消息队列,服务器进程从消息队列中接收消息并进行处理,处理完后可以将结果消息再发送回消息队列,由客户端接收。

共享内存(Shared Memory)是一种高效的进程间通信方式。它允许不同的进程共享同一块物理内存区域。多个进程可以直接读写这块共享内存,就好像它是自己进程内的内存一样。但是,为了防止数据冲突,需要使用同步机制(如互斥锁)来控制对共享内存的访问。例如,在一个多进程的数据库系统中,多个进程可以通过共享内存来访问和更新数据库缓存,提高数据访问效率。

信号量(Semaphore)主要用于进程间的同步和互斥。它是一个计数器,可以用来控制对共享资源的访问数量。例如,有一个资源(如打印机),可以用一个信号量来表示它是否可用。初始时,信号量的值可以设为 1,表示资源可用。当一个进程想要使用这个资源时,它会先对信号量进行P操作(将信号量的值减 1),如果信号量的值大于等于 0,则可以使用资源;如果信号量的值小于 0,则进程会阻塞。当进程使用完资源后,会进行V操作(将信号量的值加 1),通知其他等待的进程资源已可用。

套接字(Socket)是一种用于不同主机之间进程通信的方式,也可以用于同一主机上的进程通信。它提供了一种通用的网络编程接口。例如,在网络应用程序中,客户端进程和服务器进程可以通过套接字建立连接,进行数据传输,包括发送和接收文件、进行网络聊天等多种应用场景。

信号在代码中如何实现

在操作系统中,信号是一种用于通知进程发生了某个事件的机制。在代码中实现信号处理主要涉及以下几个步骤。

首先,要定义信号处理函数。这个函数会在进程接收到特定信号时被调用。例如,在 C++ 中,可以定义一个函数来处理SIGINT信号(通常是通过 Ctrl + C 触发)。

#include <iostream>
#include <signal.h>

void signalHandler(int signum) {
    std::cout << "Received signal " << signum << std::endl;
    // 可以在这里添加具体的信号处理逻辑,如清理资源、保存数据等
}

然后,要使用signal函数(在 Unix/Linux 系统中)来注册信号处理函数。这个函数的第一个参数是信号编号,第二个参数是信号处理函数的指针。例如,要注册上述SIGINT信号的处理函数,可以这样做:

int main() {
    // 注册SIGINT信号的处理函数
    if (signal(SIGINT, signalHandler) == SIG_ERR) {
        std::cerr << "Error registering signal handler" << std::endl;
        return 1;
    }
    // 程序的其他部分,可以在这里进行正常的业务操作
    while (1) {
        // 保持程序运行,等待信号
    }
    return 0;
}

在这个例子中,当程序运行时,如果用户按下 Ctrl + C,操作系统会向进程发送SIGINT信号,进程就会调用之前注册的signalHandler函数来处理这个信号。

不同的信号有不同的用途和默认行为。例如,SIGTERM信号通常用于请求进程正常终止,SIGKILL信号是强制终止进程,它不能被进程捕获和处理。在编写信号处理函数时,要根据信号的性质和应用场景来设计合理的处理逻辑,比如在收到SIGTERM信号时,可以先进行一些资源清理和数据保存工作,然后再终止程序。

堆和栈的区别?栈多大?

堆和栈是计算机内存中的两个重要区域,它们有诸多区别。

从存储内容上看,栈主要用于存储函数调用的信息,包括函数的参数、局部变量、返回地址等。当一个函数被调用时,函数的相关信息会被压入栈中,当函数返回时,这些信息会被弹出栈。例如,在一个函数中有一个局部变量和一个参数,这些数据都会存储在栈中。而堆主要用于动态内存分配,程序员可以通过函数(如mallocnew)在堆上分配任意大小的内存块,用于存储数据结构(如链表、树等)或者大型对象。

在内存分配方式上,栈的内存分配是由编译器自动完成的。它是一种自动管理的内存区域,按照后进先出(LIFO)的原则进行操作。例如,在函数嵌套调用时,内层函数的栈帧会被压在外层函数栈帧之上,当内层函数返回时,其栈帧会被弹出。堆的内存分配则是由程序员手动控制的。程序员需要通过特定的函数来申请和释放内存,如mallocfree(在 C 语言中)或者newdelete(在 C++ 中)。如果忘记释放堆内存,就会导致内存泄漏。

关于栈的大小,它不是一个固定的值,会因操作系统、编译器和程序设置等因素而不同。在不同的操作系统中,栈的默认大小有所差异。例如,在 Linux 系统中,栈的大小通常可以通过ulimit - s命令来查看和设置,默认大小可能是 8MB 左右,但这个值可以根据系统配置和用户需求进行调整。在一些嵌入式系统中,栈的大小可能会更小,因为嵌入式设备的内存资源有限。在编译器方面,有些编译器也允许通过编译选项来设置栈的大小。例如,在某些 C++ 编译器中,可以通过特定的命令行选项来指定栈的大小。不过,在实际编程中,要注意避免栈溢出的情况,即避免在栈上分配过多的内存导致超出栈的容量。这可能会导致程序崩溃或者产生不可预测的行为。

网络七层,各有哪些协议

网络的七层模型从下到上依次为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层,各层包含不同的协议。

物理层:主要负责在物理介质上传输原始的比特流,协议多是和物理设备的接口、电气特性等相关规范。比如 EIA/TIA - 232、EIA/TIA - 449,它们规定了诸如 RS - 232 串口等物理接口的机械、电气、功能以及过程特性,确保数据能以合适的物理信号在通信线路上传输,像规定了接口的引脚定义、信号电平范围等,让不同设备间能基于统一标准进行最基础的物理连接和比特传输。

数据链路层:功能是将物理层接收到的比特流进行封装成帧,并进行差错检测和纠正等操作。以太网(Ethernet)协议是非常典型的,它定义了数据帧的格式,像目的地址、源地址、类型字段、数据字段等各部分的构成与长度等,以此来区分不同设备发出的数据以及携带的数据内容等,广泛应用于局域网环境。还有 PPP(点到点协议),常用于在串行链路上建立、配置和测试数据链路连接,像我们平时通过拨号上网时,很大程度上依赖 PPP 协议来搭建从电脑到网络服务提供商之间稳定的数据链路。

网络层:负责把分组从源主机传输到目标主机。IP(互联网协议)是核心协议,规定了 IP 数据报的格式,像版本号、首部长度、服务类型、总长度、标识符、标志位、片偏移、生存时间、协议、首部校验和、源 IP 地址、目的 IP 地址这些字段,都是用于在复杂的网络环境中准确地路由数据报,使数据能沿着合适的路径在不同网络间传输。ICMP(互联网控制报文协议)也在这一层,用于在 IP 主机、路由器之间传递控制消息,比如当网络出现拥塞或者某个 IP 地址不可达时,就会通过 ICMP 报文向源主机反馈情况,我们常用的 ping 命令就是基于 ICMP 协议来测试目标主机是否可达的。

传输层:提供端到端的通信服务。TCP(传输控制协议)是面向连接的可靠传输协议,通过序列号、确认应答、超时重传等机制保障数据准确无误地传输,像在进行文件传输时,使用 TCP 就能保证文件内容完整地从发送端到接收端,不会出现数据丢失、乱序等情况。UDP(用户数据报协议)则是无连接且不可靠的协议,不过它简单高效,适合实时性要求高但对数据准确性要求没那么严格的场景,例如实时的视频流传输、在线游戏中的部分位置更新数据发送等,少量的数据丢失或顺序错乱不会太影响整体的实时体验。

会话层:主要负责建立、维护、拆除会话连接,像在远程登录场景中,它管理着从用户发起登录请求到成功建立与服务器之间可交互的会话这一整个过程,协调双方如何开始、暂停以及结束通信等,确保通信过程有序且连贯,不同的远程登录协议中都有涉及会话层功能的实现部分,保障用户和服务器之间能顺畅地进行数据交互。

表示层:重点关注数据的表示形式,比如进行数据的加密、解密,对数据进行格式转换等操作,使得不同系统间的数据能互相理解和处理。例如在传输一些敏感数据时,会通过加密算法在表示层对数据加密,到接收端再解密还原,保证数据传输的安全性;或者在不同操作系统间传递文件,可能涉及到文本文件的换行符格式等的转换,让数据以合适的格式被对方系统识别和处理。

应用层:直接面向用户的应用进程提供服务。HTTP(超文本传输协议)用于在 Web 浏览器和 Web 服务器之间传输超文本数据,是我们浏览网页的基础,网页中的文字、图片、链接等信息都是基于 HTTP 协议来传输的。SMTP(简单邮件传输协议)专门用于发送电子邮件,规定了邮件从发件人邮箱服务器到收件人邮箱服务器的传输流程和邮件格式等。DNS(域名系统)协议非常关键,它负责把方便人们记忆的域名解析成对应的 IP 地址,我们在浏览器输入网址时,就是依靠 DNS 快速查找出对应的 IP,然后才能真正与目标服务器建立连接并获取网页内容等。

linux 命令查看内存

在 Linux 系统中,有多个命令可以用于查看内存相关的信息。

free 命令:这是一个常用的查看内存使用情况的命令,它能够显示系统总的物理内存、已使用内存、空闲内存以及缓冲和缓存使用的内存量等信息,并且可以区分出内存是被用于内核缓冲、用户进程等不同方面,通过这些数据能直观了解当前系统内存资源的整体状态,例如判断是否存在内存紧张的情况等。

top 命令:它提供了实时动态的系统资源使用情况展示,其中包含内存相关的详细信息。不仅可以看到总的内存量、已用内存、空闲内存等基础数据,还能看到各个进程占用内存的情况,按照内存使用量对进程进行排序,方便找出内存消耗较大的进程,了解其内存占用随着时间变化的趋势,有助于分析系统的性能瓶颈是不是出在内存方面,以及定位哪些程序可能存在内存泄漏等问题。

vmstat 命令:主要用于监控系统的虚拟内存、进程、CPU 活动等情况,对于内存方面,它可以输出诸如内存交换情况、内存页面调入调出的速率等信息,通过观察这些数据的变化趋势,能够知晓系统内存的动态使用状况,判断内存资源是否被高效利用,以及是否存在频繁的内存页面置换等可能影响系统性能的现象。

cat /proc/meminfo 命令:通过查看 /proc/meminfo 这个虚拟文件,可以获取非常详尽的内存信息,涵盖了从系统的物理内存总量、可用内存量、各个内存区域(如高端内存、低端内存等在一些特定系统架构下区分的区域)的大小,到内核使用的内存、内存中的文件缓存等各种细分的数据,为深入分析系统内存的具体构成和使用情况提供了丰富的素材,不过数据相对比较繁杂,需要有一定的知识基础去解读。

free 命令能观测到什么负载

free 命令能够观测到多方面和内存相关的负载情况。

首先,它能显示系统总的物理内存大小,让我们清楚系统所配备的基础内存资源有多少,以此为参照来衡量后续各项内存使用数据的占比情况。比如一台服务器总的物理内存是 16GB,通过这个数值就能直观对比已使用和空闲的部分占比是否合理。

其次,能看到已使用的内存量,这有助于判断当前系统中正在运行的进程总共消耗了多少内存资源。例如,当已使用内存接近或者超过总内存的大部分时,就可能提示我们系统存在内存紧张的情况,需要进一步排查是哪些进程占用过多内存,或者是否有内存泄漏问题等,像如果发现某个数据库应用程序占用内存不断攀升且一直不释放,就可以借助这一数据发现异常。

空闲的内存量也是可以观测到的,这反映了当前系统还有多少内存可以立即供新的进程使用或者应对突发的内存需求。如果空闲内存过少,可能会导致后续新启动的进程运行缓慢甚至无法启动,因为缺乏足够的内存空间来加载程序代码和数据。

再者,free 命令还能呈现缓冲(buffer)和缓存(cache)使用的内存量。缓冲主要是用于存放要写入磁盘的数据,像数据库的事务日志在写入磁盘前可能先暂存在缓冲中,通过观测缓冲内存的使用量,可以了解磁盘写入操作的潜在压力等情况。缓存则是存放从磁盘读出的数据,方便下次快速访问,比如经常访问的文件内容会被缓存起来,如果缓存占用内存过大,在内存紧张时可以考虑是否需要清理部分缓存来释放内存。

另外,还能看到内存的使用占比情况,通过已使用内存与总内存的比例,清晰知晓当前系统内存资源的紧张程度,以及与之前观测的数据对比,分析系统内存负载的变化趋势,例如是随着时间不断变紧张还是处于比较稳定且合理的状态,从而对系统整体的内存健康状况做出判断。

swap 区干什么用的

swap 区即交换区,在 Linux 等操作系统中有着重要的作用。

当系统的物理内存不够用的时候,swap 区就开始发挥作用了。例如,计算机同时运行了多个比较大型的应用程序,像同时打开多个设计软件、浏览器多个页面且每个页面有大量的多媒体内容等,物理内存中的空间被消耗殆尽,此时操作系统会把一部分暂时不使用的内存数据(通常是处于睡眠状态或者近期很少使用的进程所占用的数据)从物理内存移动到 swap 区,这个过程叫做换出(swap out),通过这样腾出物理内存空间,使得当前正在活跃使用的进程能够继续在物理内存中正常运行,避免因为内存不足而导致程序崩溃或者系统卡死等情况。

相反,当之前被换出到 swap 区的数据所在的进程又需要被使用时,操作系统会把这部分数据从 swap 区再移回到物理内存中,这个过程叫换入(swap in)。比如之前因为内存紧张被换出到 swap 区的某个文档编辑程序,当用户再次切换到该程序进行编辑操作时,操作系统就会把相关的数据从 swap 区重新调入物理内存,让程序可以正常响应操作继续运行。

swap 区可以看作是对物理内存的一种扩展和补充,在一定程度上缓解了物理内存资源有限的压力,使得系统能够同时运行更多的程序或者处理更复杂的任务,不过由于 swap 区的数据读写速度相较于物理内存要慢很多,过度依赖 swap 区会导致系统性能下降,所以一般来说,还是希望系统尽量依靠充足的物理内存来运行,只在必要的时候使用 swap 区来应急。

预处理阶段做什么

在 C++ 的编译过程中,预处理阶段是非常重要的第一步,它主要完成以下几方面的工作。

首先是进行文件包含处理,也就是处理那些以#include指令开头的语句。例如,当程序中有#include <iostream>这样的语句时,预处理程序会在系统指定的头文件路径中去查找 iostream 这个头文件,并把它的内容原原本本地复制到当前源文件中#include语句所在的位置,就好像是把 iostream 头文件里定义的输入输出相关的类、函数等全部粘贴过来一样,这样后续编译阶段就能基于这些完整的代码进行进一步的处理,同样对于自定义的头文件,也会按照指定的路径去查找并插入相应内容,确保源文件能使用到各种所需的声明和定义。

宏定义的替换也是预处理阶段的重要任务。像我们定义了#define PI 3.1415926这样的宏,在预处理时,程序中所有出现PI的地方都会被替换成3.1415926,无论是在表达式中、函数参数里等任何位置,这种替换是简单的文本替换,并且对于带参数的宏,也会按照相应的参数传递规则进行替换展开,比如#define MAX(a,b) ((a)>(b)?(a):(b)),当在代码中使用MAX(3,5)时,会被替换成((3)>(5)?(3):(5)),通过宏定义可以方便地定义一些常量或者简单的代码片段来简化编程和提高代码的可维护性,而预处理阶段保证了这些宏能正确地在代码中发挥作用。

条件编译也是在这个阶段处理的内容。通过#if#ifdef#ifndef#else#endif等指令,可以根据不同的条件来决定哪些代码片段被编译,哪些不被编译。例如,#ifdef DEBUG后面跟着一些用于调试的代码语句,#endif结束这个条件编译块,如果在编译时定义了DEBUG这个宏(可以通过命令行参数等方式定义),那么这些调试代码就会参与编译,反之则不会,这使得我们可以方便地在开发阶段包含一些辅助调试的代码,而在正式发布产品时轻松将其排除,不增加最终可执行程序的代码量,提高程序的执行效率和安全性。

还有一些诸如删除注释等工作也在预处理阶段完成,它会把源文件中的注释内容去除,因为注释对于程序的实际运行没有作用,只是方便程序员理解代码,预处理阶段将其去除后能让后续的编译过程更专注于处理真正有意义的代码部分,从而提高编译效率和准确性。

closewait 等待时间是多少

在 TCP 连接关闭过程中出现的 CLOSE_WAIT 状态,它并没有一个固定的、通用的等待时间标准设定呀。

当客户端主动发起关闭连接请求,发送 FIN 报文后,服务器端收到这个 FIN 并回应 ACK,此时服务器端就进入了 CLOSE_WAIT 状态。在这个状态下,意味着服务器端这边还有数据没发送完或者应用层还没处理完相关的关闭流程等情况。服务器需要继续向客户端发送剩余的数据,等数据都发送完毕,应用层也完成相应收尾工作后,服务器才会发送 FIN 报文给客户端,进而进入 LAST_ACK 状态,等待客户端回复 ACK 来彻底关闭连接。

这个 CLOSE_WAIT 状态持续的时长完全取决于服务器端应用程序的处理速度以及还有多少剩余数据要发送等因素。如果应用程序出现异常,比如存在代码漏洞导致忘记关闭相应的套接字或者没能及时处理完剩余数据,就可能会让连接长时间处于 CLOSE_WAIT 状态,甚至一直保持到程序被强制终止或者服务器重启等情况发生。所以说,它的等待时间是因具体的业务场景、应用程序的实现以及网络状况等诸多因素共同影响的,很难去确切地给出一个固定的时长数值呢。

MSL 是什么

MSL 全称为 Maximum Segment Lifetime,也就是最长报文段寿命。

它是 TCP 协议中的一个重要概念。在网络环境中,TCP 报文段在网络中是不能永远存在、无限传播的呀,因为如果那样的话,可能会导致一些过期的、无效的报文段还在网络里游荡,进而干扰正常的网络通信秩序。MSL 就是规定了一个 TCP 报文段在网络中所能存在的最长时间。

当一个 TCP 报文段被发送出去后,从它离开发送端开始计时,一旦超过了 MSL 所规定的时长,这个报文段就应该被网络中的路由器等设备丢弃掉,这样可以保证网络中不会充斥着大量陈旧、无用的报文段。

通常来说,MSL 的具体时长是由操作系统或者网络协议栈的实现来确定的,在不同的操作系统中可能会有差异,常见的取值大概在 2 分钟左右。而且在 TCP 的一些机制里,MSL 也有着关键作用呢。比如在 TCP 连接关闭时的 TIME_WAIT 状态,这个状态的持续时间通常是 2 倍的 MSL。当主动关闭连接的一方进入 TIME_WAIT 状态后,要等待 2 倍的 MSL 时长,这是为了确保本端最后发送的 ACK 报文能够被对端正确收到,以及保证本端在这个时间段内不会收到之前连接中延迟到达的报文,避免这些延迟报文被误当作新连接的报文而引发问题,从而保障网络通信的可靠性和稳定性。

SNMP 基于什么实现

SNMP 即简单网络管理协议(Simple Network Management Protocol),它主要基于以下几个方面来实现呀。

从网络模型层面来看,SNMP 是基于 UDP(用户数据报协议)来实现传输功能的。之所以选择 UDP,是因为 SNMP 在进行网络管理操作时,更注重的是简单高效地发送管理信息和获取反馈,像查询设备状态、获取网络接口的数据流量等操作,对实时性有一定要求,虽然 UDP 不可靠,存在报文丢失等可能性,但对于很多网络管理场景来说,偶尔的丢包可以通过重发等机制来弥补,而且不用像 TCP 那样建立复杂的连接等流程,能更快地将管理消息发送出去,满足网络管理对时效性的需求。

从数据组织和管理角度,SNMP 有一套完善的管理信息结构(SMI)。SMI 定义了如何描述被管理对象,包括对象的命名、数据类型、编码规则等内容。例如,它规定了网络设备中的各种资源(如路由器的端口状态、服务器的 CPU 使用率等)可以用什么样的数据类型来表示,如何给这些对象命名以便统一管理,通过这些规则将网络中各种各样复杂的设备资源进行标准化的描述,方便进行信息的传递和处理。

还有管理信息库(MIB)也是关键组成部分。MIB 可以看作是一个存放了所有被管理对象的数据库,它里面存储了各种网络设备的相关参数、状态等信息,不同的设备厂商可以按照标准的 MIB 定义去扩展和填充自己设备对应的信息,SNMP 协议就是通过对 MIB 中的对象进行操作,比如读取、修改其中的数值等,来实现对网络设备的监控和管理,管理人员可以利用 SNMP 协议发送相应的操作请求,去获取 MIB 里的内容或者更新里面的数据,以此达到管理网络设备的目的。

数据库查询很慢怎么办

当遇到数据库查询很慢的情况时,有多个方面可以去排查和优化呀。

首先从查询语句本身入手,查看是否存在不合理的查询条件或者关联操作等。比如在 SQL 语句中使用了没有索引的字段进行条件筛选或者多表连接时关联字段没有建立索引,那数据库在执行查询时可能就需要全表扫描,这会耗费大量的时间。可以通过使用 EXPLAIN 命令(不同数据库具体命令可能稍有差异)来分析查询语句的执行计划,查看数据库是如何去查找数据的,是否用到了索引,如果发现没有使用索引的情况,就可以根据实际的表结构和业务需求去添加合适的索引,提高查询的效率。

索引方面也需要进一步审视,虽然索引能加快查询速度,但如果索引过多、不合理,也可能会拖慢数据库的整体性能。例如创建了大量冗余的索引,或者索引的字段顺序不合理等,在数据更新(插入、删除、修改操作)时,数据库需要同时维护这些索引,会增加额外的开销。所以要定期评估索引的有效性,删除那些不必要的索引,优化索引的结构,确保其真正有助于提升查询效率。

数据库的配置参数也不容忽视呀。像缓存相关的参数,如果数据库的缓存设置过小,可能导致频繁地从磁盘读取数据,而磁盘 I/O 操作通常是比较慢的,这样就会使查询变慢。可以根据服务器的硬件资源和实际的业务负载情况,合理调整缓存大小等配置参数,提高数据在内存中的缓存命中率,加快查询响应速度。

另外,从数据表的设计角度来看,如果表的数据量过大,可能考虑进行数据分区或者分表操作。比如按照时间范围将一个大表的数据分成多个子表,查询时根据时间条件去对应的子表中查找,避免在海量数据的大表中逐一搜索,或者对一些经常一起查询的数据字段进行合理的垂直分区,把它们划分到不同的表结构中,优化查询时的数据读取量,以此来提升查询性能。

还有就是数据库服务器自身的硬件资源情况,要是服务器的 CPU、内存等资源已经处于高负载状态,那即使查询语句和索引等都优化得很好,也可能会出现查询缓慢的情况,这时可能就需要考虑升级硬件,比如增加内存、更换性能更好的 CPU 等,从根本上提升数据库的运行效率,进而加快查询速度。

InnoDB 和 MyISAM 区别?MySQL 如何做到 ACID?

InnoDB 和 MyISAM 区别

在 MySQL 中,InnoDB 和 MyISAM 是两种常用的存储引擎,它们有着诸多区别呢。

从事务支持方面来看,InnoDB 是支持事务的,它能够保证一组数据库操作要么全部成功执行,要么全部失败回滚,符合 ACID 特性(后面会详细说如何做到 ACID),像在进行银行转账操作这种涉及多个数据表更新的复杂场景时,InnoDB 可以通过事务机制确保整个转账流程的完整性和数据一致性。而 MyISAM 是不支持事务的,它更适合一些简单的、对事务要求不高的应用场景,比如只是单纯进行数据查询、插入等操作且不需要保证多个操作的整体性的情况。

在数据存储结构上,MyISAM 把数据和索引分开存储,它有三个文件,分别是表结构定义文件(.frm)、数据文件(.MYD)以及索引文件(.MYI),这样的存储方式在某些情况下方便对数据和索引分别进行管理,比如备份数据时可以只备份数据文件等。InnoDB 则是将数据和索引存储在一起,采用的是聚簇索引的方式,数据按照主键顺序进行存储,索引的叶子节点直接存储了对应的数据记录,这种结构使得通过主键进行查询时效率很高,并且在数据的关联性和一致性维护方面有优势。

对于锁机制而言,MyISAM 只支持表级锁,也就是当对一个 MyISAM 表进行操作时,不管是更新一行数据还是多行数据,都会锁住整个表,这样在并发访问时,如果有一个操作锁住了表,其他并发的读写操作都需要等待,可能会导致并发性能受限。InnoDB 支持行级锁,它可以更精细地对每一行数据进行锁定,在高并发场景下,不同的线程可以同时操作同一张表中不同行的数据,能极大地提高并发处理能力,减少锁等待的时间,不过行级锁的管理也相对复杂一些,会有一定的开销。

从外键约束支持来看,InnoDB 支持外键约束,能够很好地维护不同数据表之间的关联关系,保证数据的参照完整性,例如在一个电商系统中,订单表和用户表通过外键关联,InnoDB 可以确保删除用户时相关的订单数据的处理符合业务逻辑。MyISAM 则不支持外键约束,相对来说数据表之间的关联更多依靠应用层代码去把控。

MySQL 如何做到 ACID

原子性(Atomicity):以 InnoDB 存储引擎为例,它通过事务来实现原子性。在一个事务中包含的所有操作,比如一个转账事务里包含从一个账户扣钱和另一个账户加钱这两个操作,在执行过程中,如果出现任何异常情况,InnoDB 会利用 undo 日志进行回滚操作,把已经执行的部分操作撤销掉,使得整个事务就好像没有发生过一样,确保事务里的所有操作要么全部成功,要么全部失败,以此来保证原子性。

一致性(Consistency):MySQL 通过各种约束机制以及事务的隔离级别等来维护一致性。像前面提到的外键约束,能保证不同表之间数据的参照完整性;还有唯一约束、非空约束等,保证了单张表内数据的合理性。同时,不同的事务隔离级别(如读未提交、读已提交、可重复读、串行化)规定了事务之间相互影响的程度,避免出现脏读、不可重复读、幻读等情况,确保数据库从一个合法的状态转换到另一个合法的状态,始终保持数据的一致性。

隔离性(Isolation):依靠事务隔离级别来实现隔离性呀。例如在可重复读隔离级别下,一个事务在执行过程中多次读取同一数据,看到的数据是一样的,不会受到其他并发事务对该数据修改的影响,这是因为 InnoDB 存储引擎利用了 MVCC(多版本并发控制)机制,它会为每个数据行创建多个版本,不同事务根据一定规则去读取不同版本的数据,从而实现事务之间的隔离,让各个事务能相对独立地执行,互不干扰。

持久性(Durability):MySQL 通过 redo 日志和双写缓冲等机制来保障持久性。当对数据进行修改操作时,首先会把修改操作记录到 redo 日志中,即使在数据真正写入磁盘之前系统出现故障,比如突然断电等情况,在重启数据库后,可以根据 redo 日志里的记录来恢复数据,把没来得及写入磁盘的数据重新执行一遍写入操作,确保数据不会因为意外情况而丢失,双写缓冲则是进一步确保在写入数据时的完整性和准确性,防止出现数据页损坏等问题,共同保证了数据的持久性。

MySQL 主从复制原理

MySQL 主从复制是一种用于实现数据冗余、提高系统可用性以及分担读负载等目的的重要机制。

在主从复制架构中,有一个主服务器(Master)和一个或多个从服务器(Slave)。主服务器负责处理所有的写操作以及部分读操作,而从服务器主要用于处理读操作,以此来分担主服务器的压力。

其核心原理涉及到三个主要的线程以及二进制日志(Binlog)。

首先,在主服务器上,每当有数据修改操作(如 INSERT、UPDATE、DELETE 等)发生时,这些操作会被记录到二进制日志中,二进制日志是以事件的形式来记录数据库的更改内容,它包含了执行的 SQL 语句或者数据行的变化等信息。

然后,主服务器会有一个 Binlog Dump 线程,这个线程负责将二进制日志中的内容发送给从服务器。它会实时监测二进制日志的变化,一旦有新的日志事件产生,就会把这些事件发送给已经连接好的从服务器。

在从服务器这边,有一个 I/O 线程,它会连接到主服务器,接收来自主服务器 Binlog Dump 线程发送过来的二进制日志事件,并将其写入到从服务器本地的中继日志(Relay Log)中。中继日志的作用类似一个中转站,暂时存放从主服务器接收到的日志内容。

接着,从服务器还有一个 SQL 线程,这个线程会读取中继日志中的事件,然后按照顺序将这些事件在从服务器上重新执行一遍,就好像在从服务器上亲自执行了主服务器上发生的那些数据修改操作一样,从而使得从服务器的数据和主服务器的数据保持一致。

通过这样的机制,只要主服务器的数据发生变化,经过一系列的日志传递和执行操作,从服务器就能及时更新自己的数据,实现主从复制。不过在实际应用中,还需要考虑网络延迟、主从服务器配置差异等因素可能对复制的及时性和准确性产生的影响,需要进行合理的监控和优化。

Redis 知道吗?Redis 和 MySQL 如何一致

Redis 是一款开源的、高性能的键值对存储数据库,常用于缓存、消息队列等多种场景,它的数据存储在内存中,所以读写速度非常快。

要让 Redis 和 MySQL 的数据保持一致,有几种常见的做法。

一种是采用定时任务的方式。可以编写脚本或者利用一些调度框架,定期从 MySQL 中获取最新的数据,然后更新到 Redis 中。比如,设定每隔一定时间(如每 5 分钟),去查询 MySQL 中某些经常被访问的表的数据,将这些数据按照一定的规则更新到 Redis 对应的键值对中。不过这种方式存在一定的时间延迟,在两次更新的间隔期间,如果 MySQL 中的数据发生了变化,Redis 中的数据就可能是旧的,所以适用于对数据实时性要求不是特别高的场景。

另一种是基于数据更新的触发机制。在应用程序中,当对 MySQL 进行写操作(如插入、更新、删除数据)时,同时在代码里添加逻辑去操作 Redis。例如,在一个电商系统中,当用户下单成功后,数据库会更新订单状态,同时在业务逻辑代码里添加语句,去删除 Redis 中对应缓存的旧订单信息,然后再把更新后的订单状态信息存入 Redis,这样就能保证 Redis 和 MySQL 的数据同步更新,实时保持一致。但这种方式需要在每个涉及数据修改的业务代码处都添加相应的逻辑,增加了代码的复杂度和维护成本。

还可以利用一些数据库中间件来实现。部分中间件支持同时管理 MySQL 和 Redis,它们能够监听 MySQL 的事务提交等操作,一旦检测到有数据变化,就自动在 Redis 中进行相应的更新操作,这种方式相对比较自动化和高效,不过需要引入额外的中间件,对系统架构会有一定的影响,并且要选择合适、稳定的中间件产品,避免出现兼容性等问题。

无论采用哪种方式,都需要根据具体的业务场景、对数据一致性的要求以及系统的复杂度等因素综合考虑,权衡利弊来选择最合适的方法让 Redis 和 MySQL 的数据达到一致。

Redis 有哪些模式?

Redis 有多种运行模式,各有其特点和适用场景。

单机模式:这是最基础的模式,Redis 只在一台服务器上运行,所有的数据存储、读写操作等都在这一台机器上完成。它的优点是配置简单、易于部署,非常适合在开发环境或者小型项目中使用,用于快速验证一些业务逻辑或者进行简单的缓存应用等。例如在一个小型的个人博客网站开发中,使用单机模式的 Redis 来缓存文章内容、用户信息等,能够提高网站的访问速度,而且在开发阶段不需要复杂的集群配置就能满足需求。不过,单机模式存在单点故障的问题,如果这台服务器出现故障,比如硬件损坏、软件崩溃等情况,整个 Redis 服务就会停止,数据也无法正常访问,并且单机的存储容量和性能是有限的,随着业务量的增长可能无法满足需求。

主从模式:由一个主节点(Master)和多个从节点(Slave)组成。主节点负责接收写操作,从节点负责接收读操作,数据从主节点复制到从节点,实现数据的冗余和读写分离。这样可以提高系统的读性能,分担主节点的负载,比如在一个电商系统中,大量的商品信息查询可以分配到多个从节点上进行,而商品信息的更新等写操作则由主节点统一处理。同时,从节点还起到了备份的作用,如果主节点出现故障,可以将某个从节点升级为新的主节点继续提供服务。但主从模式存在一定的复制延迟问题,即从节点的数据更新可能会稍晚于主节点,在一些对实时性要求极高的场景中可能需要额外关注和优化。

哨兵模式:是在主从模式基础上的一种高可用性解决方案。它通过引入哨兵(Sentinel)进程来监控主从节点的状态。哨兵会定期检查主节点和从节点是否正常运行,当主节点出现故障时,哨兵能够自动进行故障转移,选举出一个新的主节点(从从节点中选择),并通知其他从节点和客户端,让整个系统可以继续正常运行,减少因主节点故障导致的服务中断时间。例如在一个对服务连续性要求较高的在线交易系统中,哨兵模式可以确保即使主节点意外宕机,系统也能快速切换到新的主节点,保障交易的正常进行。

集群模式:Redis 集群是为了应对大规模数据存储和高并发读写场景而设计的。它将数据分散存储在多个节点上,通过哈希槽(Hash Slot)的方式对数据进行分片,不同的节点负责不同范围的哈希槽,客户端可以连接到任意节点进行读写操作,节点之间会互相通信、协调数据的分布和迁移等。这样可以实现水平扩展,大大增加系统的存储容量和并发处理能力,适合大型互联网应用等对数据量和性能要求都很高的场景,比如大型社交平台的数据缓存、消息队列管理等,不过集群模式的配置和管理相对复杂,需要更多的运维成本和技术投入。

Redis 应用场景

Redis 有着广泛的应用场景,以下是一些常见的方面。

缓存应用:这是 Redis 最为常用的场景之一。由于 Redis 的数据存储在内存中,读写速度极快,所以常被用作缓存来存储频繁访问的数据,以减轻后端数据库(如 MySQL 等)的压力。比如在一个电商网站中,商品的详情信息、分类列表等都是经常被用户访问的数据,将这些数据缓存到 Redis 中,当用户再次请求时,直接从 Redis 中获取,大大提高了响应速度,减少了数据库的查询次数,提升了整个网站的性能。而且可以通过设置合适的缓存过期时间,保证数据的时效性,避免缓存数据陈旧导致的问题。

计数器应用:像网站的访问量统计、文章的点赞数、视频的播放次数等都可以利用 Redis 来实现。Redis 提供了原子性的操作,比如 INCR 命令可以对一个键对应的值进行自增操作,非常适合这种需要频繁更新且要求操作原子性的计数场景。例如在一个社交媒体平台上,每当用户点赞一篇文章,就可以通过 Redis 的 INCR 命令对该文章对应的点赞数键进行操作,而且不用担心并发情况下数据的准确性问题,因为 Redis 的原子操作能确保在多个用户同时点赞时也能正确计数。

消息队列:Redis 可以作为简单的消息队列来使用。通过 Redis 的列表(List)数据结构,生产者可以将消息 LPUSH(从左边插入)或者 RPUSH(从右边插入)到列表中,消费者则可以从另一端(比如使用 LPOP 或 RPOP 命令)取出消息进行处理,实现消息的传递和异步处理。例如在一个分布式系统中,不同的模块之间需要进行解耦通信,一个模块作为生产者生成任务消息放入 Redis 队列,另一个模块作为消费者从队列中取出任务并执行,提高了系统的灵活性和可扩展性。

分布式锁:在多线程或者多进程、分布式环境下,为了保证同一时间只有一个操作能够访问或修改某个共享资源,可以利用 Redis 来实现分布式锁。例如通过 SETNX 命令(SET if Not eXists),当多个客户端同时尝试获取锁时,只有一个客户端能够成功设置对应的键值对(表示获取锁成功),其他客户端则需要等待或者重试,在操作完成后,通过删除这个键来释放锁,以此来保证共享资源的互斥访问,常用于一些对并发控制要求较高的场景,如分布式系统中的资源调度、数据一致性维护等。

排行榜应用:Redis 的有序集合(Sorted Set)数据结构非常适合构建各种排行榜,比如游戏中的玩家积分排行榜、电商平台的商品销量排行榜等。有序集合中的每个元素都有一个分数,可以根据分数对元素进行排序,通过 ZADD 命令可以添加元素并设置分数,通过 ZRANGE 等命令可以获取排名前列的元素。例如在一个在线游戏中,玩家每次获得积分后,通过 ZADD 命令更新其在排行榜中的分数,玩家可以随时查看自己在所有玩家中的排名情况,方便且高效地实现了排行榜功能。

Redis 系统架构了解吗

Redis 的系统架构有多种模式,不同模式下有着不同的架构特点。

单机架构:这是最简单的架构形式,Redis 进程运行在单台服务器上,所有的数据存储、读写操作以及配置管理等都在这一台机器上完成。服务器内存用于存储数据,通过网络接口与客户端进行通信,接收客户端发来的命令并执行相应的操作,如执行 SET 命令进行键值对设置、GET 命令获取值等。它的优点是架构简单、易于部署和维护,适合开发环境、小型项目或者对可靠性要求不是特别高的场景。但缺点也很明显,存在单点故障风险,一旦服务器出现硬件故障、软件崩溃等问题,整个 Redis 服务就会中断,而且单机的存储容量和性能是有限的,随着业务量的增长,可能无法满足高并发读写和大量数据存储的需求。

主从架构:由一个主节点(Master)和多个从节点(Slave)组成。主节点负责处理写操作以及部分重要的读操作,从节点主要负责接收读操作。主节点将数据变更通过复制机制传递给从节点,保持数据的一致性。在架构层面,主节点有自己的内存空间用于存储数据,同时会开启相关的复制线程,将二进制日志等数据变更信息发送给从节点;从节点同样有自己的内存来存储复制过来的数据,并且有对应的 I/O 线程接收主节点的数据以及 SQL 线程来执行数据更新操作,就像前面介绍的主从复制原理那样。这种架构实现了读写分离,提高了系统的读性能,同时多个从节点也起到了一定的数据备份作用,增加了系统的可用性,不过存在复制延迟问题,可能会导致从节点的数据在短时间内与主节点不一致。

哨兵架构:是在主从架构基础上添加了哨兵(Sentinel)组件的一种高可用性架构。哨兵是独立运行的进程,一般会部署多个哨兵来监控主从节点的状态。哨兵会定期向主从节点发送 PING 命令等检测其是否存活,并且判断主从节点之间的连接是否正常等情况。当主节点出现故障时,哨兵们会通过选举机制选出一个从节点作为新的主节点,然后通知其他从节点和客户端更新主节点信息,使得系统能够快速恢复正常运行,减少因主节点故障导致的服务中断时间。在这个架构中,各个节点之间的通信和协作更加复杂,哨兵需要与主从节点频繁交互,同时要协调好故障转移等操作,确保整个系统的稳定性和高可用性。

集群架构:Redis 集群是为了应对大规模数据存储和高并发读写场景设计的一种分布式架构。它将整个数据空间划分为 16384 个哈希槽(Hash Slot),不同的节点负责不同范围的哈希槽,数据根据键的哈希值被分配到对应的哈希槽所在的节点上进行存储和管理。客户端可以连接到任意一个节点进行读写操作,节点之间通过一种叫做 Gossip 协议的通信方式互相交换信息,比如节点状态、数据迁移情况等。当需要进行数据迁移(比如新增或减少节点时),节点之间会协同完成哈希槽的重新分配以及数据的移动工作,确保数据分布的合理性和系统的扩展性。这种架构可以实现水平扩展,极大地增加了系统的存储容量和并发处理能力,不过其配置和管理相对复杂,需要对网络、节点协调等方面有较好的运维能力和技术把控。

讲一讲设计模式的原则?

设计模式遵循着一些重要的原则,这些原则有助于创建高质量、易维护且可扩展的软件系统。

开闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当有新的需求或者功能需要添加时,应当通过扩展现有代码的方式来实现,而不是直接去修改已有的、能正常工作的代码。例如,在一个图形绘制系统中,有各种图形类如圆形、矩形类等,如果后续要添加新的图形三角形,应该是新增一个三角形类去实现绘制等相关功能,而不应该去修改已有的圆形、矩形类的代码,这样可以保证原有功能的稳定性,减少因修改代码可能带来的错误引入。

里氏替换原则(Liskov Substitution Principle,LSP):子类型必须能够替换掉它们的父类型。也就是说,在任何使用父类对象的地方,都可以用子类对象来替代,并且程序的行为和功能不会出现错误或者不符合预期的情况。比如有一个动物类作为父类,有一个发声的方法,猫类和狗类作为子类重写了这个发声方法,在以动物类对象作为参数的函数中,如果传入猫或狗的对象,它们都能正确地调用各自的发声方法来完成合适的行为,不会出现异常,以此保证了继承体系下的多态性和代码的正确性。

依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。例如在一个电商系统中,订单处理模块(高层模块)不应该直接依赖具体的数据库操作模块(低层模块),而是都依赖于一个抽象的数据访问接口,数据库操作模块实现这个接口,这样如果后期要更换数据库,只需要更改数据库操作模块的实现,而不会影响到订单处理模块等高层模块的代码,提高了系统的可维护性和可扩展性。

单一职责原则(Single Responsibility Principle,SRP):一个类或者模块应该只有一个引起它变化的原因,也就是只负责一项职责。比如一个用户管理类,就只专注于用户的注册、登录、信息修改等和用户管理相关的功能,而不应该同时又去负责商品管理等其他不相关的功能,这样每个类的职责明确,代码的可读性和可维护性都会更好,当某一功能需求变化时,只需要在对应的负责该功能的类中进行修改,不会影响到其他无关的部分。

接口隔离原则(Interface Segregation Principle,ISP):客户端不应该被迫依赖于它不使用的接口。设计接口时,应该根据不同客户端的需求,将大的接口拆分成多个小的、更具体的接口,让客户端只依赖它们真正需要的接口部分。例如在一个图形绘制系统中,有绘制简单图形的客户端,也有绘制复杂图形且带有特效的客户端,那就不应该设计一个包含所有绘图功能的大接口让所有客户端都依赖,而是可以拆分成简单图形绘制接口和复杂图形绘制接口等,不同客户端按需依赖,避免不必要的耦合。

迪米特法则(Law of Demeter,LoD):也叫最少知识原则,一个对象应该对其他对象有最少的了解,尽量减少对象之间的交互关系。比如在一个公司组织架构系统中,员工类不应该直接去访问公司高层的决策信息等和它本身职责无关的内容,而是只和自己的直属上级、同事等有必要的信息交互,这样可以降低类之间的耦合度,使得系统各部分相对独立,易于维护和扩展。

GDB 怎么操作的?(简历上写了)

GDB 是一款功能强大的调试工具,以下是一些常见的操作方式。

首先,要使用 GDB 调试程序,需要在编译程序时加上调试信息,例如在使用 GCC 编译 C/C++ 程序时,加上 “-g” 选项,像 “gcc -g -o myprogram myprogram.c” 这样的命令,就可以在生成的可执行文件 “myprogram” 中包含调试信息了。

启动 GDB 调试,在命令行输入 “gdb [可执行文件名]”,比如 “gdb myprogram”,就进入了 GDB 的调试环境。

进入调试环境后,常用的操作有很多。“run” 命令用于启动程序的运行,就像在正常情况下直接执行可执行文件一样,它会按照程序设定的逻辑开始执行,不过此时是在调试模式下,可以随时暂停。

“break” 命令用于设置断点,比如 “break main”,就是在主函数的开头设置一个断点,程序运行到这里就会暂停,方便查看程序此时的状态。也可以根据具体的行号、函数名或者条件来设置断点,例如 “break [文件名]:[行号]” 可以在指定文件的指定行设置断点,“break func_name if [条件]” 可以在满足某个条件时在函数 “func_name” 处暂停程序。

当程序运行到断点处暂停后,“next” 命令可以让程序执行下一行代码,它会执行当前行并移动到下一行,适用于单步执行,特别是对于不进入函数内部的情况,只想查看当前函数主体代码的执行顺序很有用。而 “step” 命令则不同,它在执行当前行代码时,如果遇到函数调用,会进入函数内部,继续单步调试函数里的代码,能更细致地查看函数内部的执行情况。

“print” 命令用于查看变量的值,比如 “print variable_name” 就可以输出变量 “variable_name” 当前的值,这对于了解程序运行过程中变量的变化情况非常关键,可以判断程序是否按照预期在操作变量。

“backtrace” 或者 “bt” 命令能查看当前的函数调用栈信息,显示出从当前暂停位置开始,之前经过了哪些函数调用,每个函数的参数等情况,有助于定位问题所在的函数层次和查找函数间传递参数等是否存在问题。

“continue” 命令可以让暂停的程序继续运行,直到下一个断点或者程序结束,如果没有设置其他断点,程序就会一直运行下去。

当调试结束后,输入 “quit” 命令就可以退出 GDB 调试环境了。通过这些常用的操作以及它们的组合使用,能够有效地利用 GDB 来查找程序中的逻辑错误、内存错误等各种问题。

tcpdump 怎么操作的?(简历上写了)

tcpdump 是一款强大的网络数据包捕获工具,在网络分析等场景中应用广泛,以下是它的一些常见操作方式。

首先,在命令行中直接输入 “tcpdump”,不加其他参数时,它会捕获所有经过本地网络接口的数据包,并将其相关信息显示出来,比如源 IP 地址、目的 IP 地址、协议类型、端口号等基础信息,不过这样可能会产生大量的输出,不太方便查看特定的内容,所以通常会结合各种参数来使用。

可以通过 “-i” 参数指定要捕获数据包的网络接口,例如 “tcpdump -i eth0”,就是指定在名为 “eth0” 的网络接口上进行数据包捕获,这在服务器有多个网络接口或者需要明确从某个特定接口捕获数据时很有用。

“-c” 参数用于限制捕获的数据包数量,比如 “tcpdump -i eth0 -c 100”,表示只捕获 100 个数据包后就停止,适合只想获取一定数量的样本数据包进行分析的情况,避免长时间捕获大量数据包导致信息过多难以处理。

如果想根据特定的协议来捕获数据包,可使用相应的协议参数,如 “-p tcp” 就是只捕获 TCP 协议的数据包,“-p udp” 则是只捕获 UDP 协议的数据包,还有 “-p icmp” 用于捕获 ICMP 协议的数据包等,通过这样的筛选可以聚焦在关注的协议类型上,分析特定协议相关的网络通信情况。

对于根据 IP 地址来筛选数据包,“src” 和 “dst” 关键字很有用。例如 “tcpdump -i eth0 src 192.162.1.1” 可以捕获源 IP 地址为 “192.162.1.1” 的数据包,“tcpdump -i eth0 dst 192.162.1.2” 则能捕获目的 IP 地址为 “192.162.1.2” 的数据包,也可以结合使用,如 “tcpdump -i eth0 src 192.162.1.1 and dst 192.162.1.2”,用来捕获从 “192.162.1.1” 发往 “192.162.1.2” 的数据包,精准定位特定 IP 之间的通信数据。

按照端口号筛选数据包也很常见,使用 “port” 关键字,比如 “tcpdump -i eth0 port 80” 可以捕获经过端口 80(通常是 HTTP 服务使用的端口)的数据包,对于分析特定服务的网络流量很有帮助,若要捕获多个端口的数据包,可以用 “or” 连接不同端口号,如 “tcpdump -i eth0 port 80 or port 443”,就能捕获经过端口 80 或者 443(常用于 HTTPS 服务)的数据包了。

“-w” 参数用于将捕获到的数据包保存到文件中,例如 “tcpdump -i eth0 -w packets.pcap”,会把捕获的数据包以.pcap 的格式保存到名为 “packets.pcap” 的文件中,之后可以使用专业的网络分析工具(如 Wireshark 等)打开这个文件,进行更深入细致的数据包分析,这种方式适合先捕获数据然后后续慢慢分析,或者用于保存特定场景下的网络流量数据以便后续复现问题等情况。

“-r” 参数则相反,用于读取已经保存的数据包文件,像 “tcpdump -r packets.pcap”,可以重新显示文件中保存的数据包信息,方便回顾之前捕获的内容。

通过灵活运用这些参数以及它们之间的组合,tcpdump 能够帮助我们在复杂的网络环境中捕获和分析想要关注的网络数据包,了解网络通信的具体情况,排查网络故障等问题。

写过 shell 脚本吗,shell 第一句写什么?

Shell 脚本是用于在 Unix/Linux 等操作系统中自动化执行一系列命令的脚本文件。我有过编写 Shell 脚本的经历呀。

通常情况下,Shell 脚本的第一句会写 “#!/bin/bash” 或者 “#!/bin/sh” 等,这被称为 Shebang(也叫 Hashbang),它的作用是指定该脚本使用的解释器。

“#!/bin/bash” 表示这个脚本要使用 Bash(Bourne Again Shell)这个强大且应用广泛的 Shell 解释器来执行脚本中的命令。Bash 提供了丰富的语法特性、内置命令以及对各种编程结构(如条件判断、循环等)的支持,适合编写较为复杂、功能多样的 Shell 脚本,比如在脚本中需要进行大量的字符串处理、文件遍历、复杂的条件判断以及和系统环境有较多交互的情况,使用 Bash 解释器能更好地完成任务。

“#!/bin/sh” 则是指定使用系统默认的 Shell 解释器(在很多系统中可能是 Bourne Shell 的一个兼容版本),它相对来说语法更简洁、基础,适用于一些简单的、只需要执行常规命令组合的脚本,例如只是简单地进行文件复制、移动,或者执行一些基于系统基础命令的操作,不需要复杂的编程结构和高级功能的脚本,使用这种相对简单的解释器就可以满足需求。

选择哪一种作为第一句,取决于具体的脚本功能需求以及所在系统的环境情况等因素。不过需要注意的是,Shebang 这一行一定要放在脚本文件的第一行,而且前面不能有空格等其他字符,这样当执行这个脚本时,系统才能准确地识别并调用相应的解释器来运行脚本中的命令,开启整个脚本的执行流程。

stl 内存配置器

STL(Standard Template Library,标准模板库)中的内存配置器是一个关键组件,它在内存管理方面起着重要作用。

STL 的内存配置器负责为容器类(如 vector、list、map 等)分配和回收内存。它的设计初衷是为了实现高效、灵活且通用的内存管理机制,以适应不同的应用场景和平台环境。

在底层实现上,STL 内存配置器有不同的层次和策略。最基础的一种是基于::operator new::operator delete这两个全局的内存分配和回收操作符来进行简单的内存管理。例如,当一个vector容器需要扩充容量时,默认的内存配置器会调用::operator new来申请一块足够大的内存空间,用于存储新增加的元素等内容。

然而,这种简单的基于全局操作符的方式在一些情况下可能效率不高或者不太灵活。于是,STL 还提供了自定义内存配置器的机制。通过自定义内存配置器,可以按照特定的需求和规则来分配内存。比如,可以设计一种内存配置器,它从预先分配好的内存池中获取内存,而不是每次都向操作系统申请新的内存,这样在频繁地创建和销毁小对象(像在一些图形处理应用中频繁创建和销毁像素点对象等情况)时,可以减少向操作系统申请内存的开销,提高内存分配的效率,因为从内存池中获取内存通常比重新进行系统调用要快得多。

同时,内存配置器也考虑到了内存对齐等问题。不同的硬件平台对于数据在内存中的存储有对齐要求,例如,在某些平台上,一个int类型的变量地址可能需要是 4 的倍数,内存配置器会确保分配的内存满足这些对齐要求,使得对象在内存中能够正确存储和被高效访问,避免因为未对齐而导致的性能下降甚至程序错误。

另外,内存配置器还与 STL 容器的特性相适配。像vector容器的动态扩容策略就依赖于内存配置器,当需要扩充容量时,内存配置器要根据当前容器的使用情况以及预设的扩容规则(比如通常是按照一定倍数扩充,如扩充为原来的 2 倍)来合理地分配新的内存空间,并且将原有的元素正确地复制或移动到新的内存区域,保证容器操作的正确性和高效性。

不同的 STL 实现版本可能对内存配置器有不同的优化和细节处理,但总体来说,它都是围绕着高效、准确地为 STL 容器提供合适的内存资源,同时兼顾灵活性、可扩展性以及与硬件平台的适配性等多方面因素来进行设计和运作的。

Git 用过?讲讲有什么命令

Git 是一款非常流行的分布式版本控制系统,有着众多实用的命令。

git init:这个命令用于初始化一个新的 Git 仓库。在本地的一个项目目录下执行它后,该目录就会被 Git 管理起来,Git 会在这个目录下创建一个隐藏的.git 文件夹,用于存放版本控制相关的各种信息,比如版本历史记录、分支信息等。例如,当你开始一个全新的代码项目,想要用 Git 来记录代码的变化和版本演进,就在项目的根目录运行 “git init” 命令。

git add:主要作用是将文件添加到暂存区。可以添加单个文件,比如 “git add file.txt”,就是把名为 “file.txt” 的文件添加进去;也可以添加整个目录下的文件,使用 “git add.” 命令就能将当前目录下所有更改的文件都添加到暂存区,暂存区相当于一个过渡区域,准备好要提交的文件内容,以便后续进行版本提交操作。

git commit:用于将暂存区的内容提交到本地仓库,形成一个新的版本记录。需要添加一个提交说明,通过 “-m” 参数来指定,比如 “git commit -m 'Initial commit'”,这里 “Initial commit” 就是此次提交的注释,描述了这次提交做了哪些改动或者添加了什么功能等,方便后续查看版本历史时了解每个版本的具体情况。

git status:可以查看当前仓库的状态,比如哪些文件被修改了但还没添加到暂存区,哪些文件已经在暂存区准备好提交了等信息,能让你及时了解项目文件的变更情况,以便决定下一步的操作是添加文件到暂存区还是进行提交等。

git branch:用来管理分支。“git branch [分支名]” 可以创建一个新的分支,例如 “git branch feature-branch” 就创建了名为 “feature-branch” 的分支,分支可以让你在不影响主分支的情况下进行新功能的开发或者尝试不同的修改思路等;“git branch” 命令单独使用时,会列出当前仓库所有的分支,并且会用星号标记当前所在的分支。

git checkout:有多个用途,一是切换分支,比如 “git checkout master” 可以从当前分支切换到主分支 “master”;还可以用于恢复文件,若某个文件被修改了但你想恢复到上一次提交时的状态,可以使用 “git checkout [文件名]” 来实现。

git merge:用于将一个分支的内容合并到另一个分支上。比如在开发完一个新功能分支 “feature-branch” 后,想要把这个分支的修改合并到主分支 “master” 上,就在主分支下执行 “git merge feature-branch”,不过在合并过程中可能会出现冲突,需要手动解决冲突后才能完成合并。

git push:将本地仓库的分支和提交推送到远程仓库,例如和团队成员共享代码或者备份代码到远程服务器等情况,需要配置好远程仓库的相关信息后,通过 “git push origin master” 这样的命令,把本地的 “master” 分支推送到名为 “origin” 的远程仓库中。

git pull:和 “git push” 相反,它用于从远程仓库拉取最新的分支和提交到本地仓库,保持本地代码和远程代码同步,比如团队里其他成员更新了代码并推送到远程仓库后,你可以通过 “git pull” 命令获取这些新的内容,同样需要配置好远程仓库的相关信息才能正常操作。

说出知道的所有排序算法,分析各种排序算法过程

常见的排序算法有多种,以下是对它们的介绍及过程分析。

冒泡排序(Bubble Sort):它是一种简单的排序算法,基于比较和交换的思想。从数组的第一个元素开始,依次比较相邻的两个元素,如果顺序不对(比如从小到大排序时,前面的元素大于后面的元素),就交换它们的位置。这样一轮比较下来,最大的元素就会 “浮” 到数组的末尾。然后再对剩下的元素重复这个过程,直到整个数组都有序。例如,对于数组 [5, 3, 4, 6, 2],第一轮比较交换后变成 [3, 4, 5, 2, 6],最大的 6 到了末尾,接着继续对前面的元素进行多轮操作,直到数组有序。

选择排序(Selection Sort):先从待排序的数组中找到最小的元素,将其与数组的第一个元素交换位置,这样第一个元素就确定是最小的了。然后在剩下的元素中再找最小的,与第二个元素交换,以此类推,经过 n - 1 轮操作(n 为数组元素个数),整个数组就排序完成了。比如对于数组 [8, 3, 5, 2, 9],第一轮找到最小的 2,和 8 交换,变成 [2, 3, 5, 8, 9],后续继续在剩下元素中重复操作。

插入排序(Insertion Sort):把数组想象成两部分,一部分是已经有序的,一部分是待排序的。开始时,第一个元素默认是有序的,然后从第二个元素开始,将其插入到前面有序部分的合适位置,使得插入后这部分依然有序。例如对于数组 [4, 2, 7, 1, 5],开始 4 是有序的,插入 2 时,将 2 插入到 4 前面,变成 [2, 4, 7, 1, 5],接着依次插入后面的元素,不断调整有序部分,直到整个数组有序。

快速排序(Quick Sort):选择一个基准元素,通常是数组的第一个或最后一个元素等,然后通过一趟排序将数组分成两部分,左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素。接着对这两部分分别递归地进行快速排序,直到整个数组有序。比如对于数组 [6, 2, 8, 1, 7],选 6 作为基准,经过一趟排序可能变成 [1, 2, 6, 8, 7],然后再对左右两部分递归操作。

归并排序(Merge Sort):采用分治法的思想,先将数组不断地分成两半,直到每个子数组只有一个元素,此时这些子数组都是有序的。然后再将相邻的子数组两两合并,合并时比较元素大小,将它们有序地合并成一个更大的子数组,不断重复合并操作,直到最终合并成一个完整的有序数组。例如对于数组 [5, 3, 8, 2],先分成 [5, 3] 和 [8, 2],再继续分,然后合并时有序组合,逐步形成有序数组。

堆排序(Heap Sort):利用二叉堆这种数据结构,先将数组构建成一个最大堆(如果是从小到大排序),即每个父节点的值都大于等于子节点的值。然后将堆顶元素(最大元素)与数组末尾元素交换,此时最大元素就放到了正确位置,接着对剩下的元素重新调整为最大堆,重复这个过程,直到整个数组有序。比如对于数组 [3, 5, 1, 7, 2],先构建最大堆,然后交换堆顶和末尾元素,再调整堆,依次进行排序操作。

计数排序(Counting Sort):适用于整数且范围已知的情况。先统计数组中每个元素出现的次数,然后根据统计结果将元素按顺序输出,形成有序数组。例如数组元素取值范围是 0 到 5,统计每个数字出现的次数后,按照次数依次输出对应数字,就能得到有序数组,这种排序算法不是基于比较的,时间复杂度在合适条件下可以达到线性。

桶排序(Bucket Sort):把数组中的元素分配到不同的 “桶” 里,每个桶有一定的范围,然后对每个桶内部的元素分别进行排序(可以使用其他排序算法),最后将所有桶里的元素按顺序合并起来,得到有序数组。比如要对一些学生的考试成绩排序,可以根据分数范围划分不同桶,分别排序后合并。

基数排序(Radix Sort):从低位到高位依次对元素的每一位数字进行排序,比如先对个位数字排序,再对十位数字排序等,每次排序都基于上一次排序的结果,经过多轮排序后,最终得到有序数组,常用于整数排序,特别是对位数较多的整数排序有较好效果。

快排与归并排序区别。快排为什么被认为比归并快

快排与归并排序区别

算法思想方面
快速排序基于分治法,重点在于通过选择一个基准元素,一趟排序后把数组划分成左右两部分,左边元素小于等于基准,右边元素大于等于基准,然后对这两部分递归排序。而归并排序同样运用分治法,不过它是先不断地把数组从中间分成更小的子数组,直到每个子数组只有一个元素(天然有序),再将这些子数组两两合并成有序的大子数组,不断重复合并操作来实现整个数组的排序。

空间复杂度方面
归并排序在合并过程中需要额外的辅助空间,其空间复杂度是 O (n),因为它要开辟和原数组大小相同的临时空间来存放合并过程中的元素,以方便合并操作有序进行。快速排序在原地进行交换操作,通常只需要少量的额外空间用于存储一些临时变量等,空间复杂度平均情况下是 O (log n),最坏情况(比如每次选的基准都是最大或最小元素)下是 O (n)。

时间复杂度方面
在平均情况下,快速排序和归并排序的时间复杂度都是 O (n log n)。但在最坏情况下,快速排序的时间复杂度会退化成 O (n²),例如当数组已经有序或者逆序,且每次选择的基准元素都是最值时,划分就会很不均衡。而归并排序不管输入数组的初始顺序如何,时间复杂度始终稳定保持在 O (n log n)。

稳定性方面
归并排序是稳定的排序算法,在合并两个子数组时,如果遇到相等的元素,能够保证它们的相对顺序不变。而快速排序是不稳定的排序算法,因为在划分过程中元素的交换等操作可能会改变相等元素的相对顺序。

快排为什么被认为比归并快

虽然二者平均时间复杂度相同,但在实际应用中快速排序常被认为更快,原因有几点。

首先,快速排序是原地排序算法,不需要额外开辟像归并排序那样大小为 O (n) 的辅助空间,在内存使用上更高效,尤其是在处理大规模数据时,内存的频繁分配和使用会对性能产生影响,快速排序的空间优势就体现出来了。

其次,快速排序的常数因子相对较小,它的操作相对简单,主要就是比较、交换和递归操作,一趟排序过程中的元素交换等动作在现代计算机体系结构下执行效率较高,而归并排序在合并过程中需要多次比较和复制元素到辅助空间再回写到原数组等操作,相对来说开销更大一些。

另外,在实际的数据分布中,多数情况下数据不会是那种极端的有序或逆序情况,快速排序能发挥出较好的性能,达到接近平均时间复杂度的效果,所以综合来看,在很多场景下快速排序会比归并排序表现出更快的速度。

快速排序的原理是什么?快速排序要用到递归来实现吗?

快速排序的原理

快速排序是一种基于分治法的高效排序算法。

首先,要选择一个基准元素,这个基准元素可以是数组的第一个元素、最后一个元素或者随机选择的一个元素等。以选择数组的第一个元素作为基准为例,然后从数组的两端开始,设置两个指针,一个从左向右移动(称为左指针),一个从右向左移动(称为右指针)。

左指针向右移动,寻找大于基准元素的元素;右指针向左移动,寻找小于基准元素的元素。当左指针找到大于基准的元素,右指针找到小于基准的元素时,就交换这两个元素的位置。不断重复这个过程,直到左指针和右指针相遇,此时就完成了一趟排序,将数组分成了两部分,左边部分的所有元素都小于等于基准元素,右边部分的所有元素都大于等于基准元素。

接着,对划分出来的左右两部分数组,再分别重复上述的选择基准、划分的过程,也就是继续把这两部分当作新的待排序数组进行同样的操作,这个过程不断递归进行下去,直到每个子数组都只有一个元素或者为空,此时整个数组就实现了有序排列。

例如,对于数组 [5, 3, 8, 1, 7],选择 5 作为基准元素,左指针从左向右找到 8(大于 5),右指针从右向左找到 1(小于 5),交换 8 和 1 的位置,继续移动指针,再次交换合适的元素,最终一趟排序后可能变成 [1, 3, 5, 8, 7],然后对 [1, 3] 和 [8, 7] 这两部分继续进行同样的操作,直至整个数组有序。

快速排序要用到递归来实现吗?

快速排序通常是用递归来实现的。因为它的核心步骤是先对整个数组进行划分,分成左右两部分后,又要分别对这两部分继续按照同样的划分和排序规则来操作,这正好符合递归的特性,即把一个大问题逐步分解成相似的小问题来解决,小问题解决了,大问题也就解决了。

在代码实现中,递归函数会接收待排序的子数组范围(比如起始索引和结束索引)作为参数,先进行划分操作,然后递归调用自身来处理划分后的左右子数组,不断缩小问题规模,直到子数组的规模小到只有一个元素或者为空时,递归结束,也就完成了整个数组的排序过程。不过,也可以通过非递归的方式来实现快速排序,比如利用栈来模拟递归的调用过程,将需要处理的子数组范围压入栈中,然后通过循环不断从栈中取出范围进行划分等操作,但递归方式相对来说更加简洁直观,符合快速排序的算法思想,所以应用更为广泛。

数据特别大的时候,快速排序用递归会有什么问题?怎么解决?

当数据特别大的时候,快速排序使用递归会面临一些问题以及相应有对应的解决办法。

存在的问题

栈空间溢出风险:递归的实现依赖于系统的栈来保存每一层递归调用的相关信息,包括函数的参数、局部变量、返回地址等。当处理大规模数据时,快速排序需要进行大量的递归调用,递归深度可能会很深,而系统栈的空间是有限的,很容易就会出现栈空间被耗尽的情况,导致栈空间溢出错误,程序崩溃。例如,对一个包含上亿个元素的数组进行快速排序,如果递归深度达到了系统栈所能承受的极限,就会出现这个问题。

性能下降:每次递归调用都涉及到函数调用的开销,包括参数传递、栈帧的创建和销毁等操作。当数据量巨大,递归调用次数过多时,这些开销累积起来会对整体的排序效率产生负面影响,使得排序速度变慢,不能充分发挥快速排序原本高效的优势。

解决办法

尾递归优化:有些编程语言支持尾递归优化,尾递归是指递归调用是函数的最后一个操作,在这种情况下,编译器或者解释器可以将尾递归转化为循环的形式,避免了不断地创建新的栈帧,从而减少栈空间的占用。不过并非所有语言都默认支持尾递归优化,需要查看具体语言的特性来利用这种方式,例如在部分函数式编程语言中可以更好地应用尾递归优化来改进快速排序的递归实现。

手动模拟递归栈(非递归实现):可以使用一个栈(比如用数组来模拟栈结构)来手动记录需要处理的子数组范围信息,代替系统的递归调用机制。首先将整个数组的范围入栈,然后在循环中不断从栈中取出范围进行划分操作,将划分后的左右子数组范围再入栈,重复这个过程,直到栈为空,就相当于完成了递归的过程,但避免了系统栈深度过大的问题。这种方式通过自己控制栈的使用,能更灵活地处理大规模数据,减少栈空间溢出的风险,同时也能在一定程度上优化性能,减少函数调用的额外开销。

优化基准元素选择:在快速排序中,基准元素的选择对递归深度有很大影响。如果每次都选择不好的基准(比如总是选择最大或最小元素),会导致划分后的子数组极度不均衡,递归深度会接近数组元素个数,加剧栈空间溢出风险。可以采用随机选择基准元素或者通过一定算法选取更接近中间值的元素作为基准,比如 “三数取中”(取数组开头、中间、结尾三个元素中的中间值作为基准)的方法,这样能让划分后的子数组相对更均衡,降低递归深度,减少栈空间占用,提高排序效率。

采用混合排序策略:结合其他适合处理大规模数据的排序算法,比如当数据量达到一定程度后,先采用一种相对简单且空间复杂度低的排序算法(如堆排序等)对数据进行预处理,将数据大致有序化,然后再用快速排序,利用快速排序在接近有序数据上也能有较好表现的特点,这样可以减少快速排序的递归深度和整体的运算量,提升对大规模数据的排序效果。

给出一个整数,统计其二进制 1 的个数

可以通过多种方法来统计一个整数二进制表示中 1 的个数。

一种方法是将整数与 1 进行按位与操作,判断最低位是否为 1,然后将整数右移一位,不断重复这个过程。例如,对于整数 n,使用一个循环,在循环中执行n & 1,如果结果为 1,就说明最低位是 1,此时可以用一个计数器加 1 来记录 1 的个数,然后执行n = n >> 1,将 n 右移一位,直到 n 变为 0。这种方法的时间复杂度是整数二进制位数的数量级,在 32 位整数下,时间复杂度是 O (32),也就是 O (1)。

另一种更高效的方法是利用 n 和 n - 1 的按位与特性。当 n 与 n - 1 进行按位与操作时,n 的二进制表示中最右边的 1 会被置为 0。例如,对于二进制数 1100(十进制为 12),n - 1 是 1011(十进制为 11),n & (n - 1) 的结果是 1000(十进制为 8),可以看到最右边的 1 被去掉了。基于这个特性,可以不断执行 n & (n - 1),每次执行都会去掉一个 1,同时用一个计数器记录执行的次数,直到 n 变为 0,这个次数就是二进制 1 的个数。这种方法在二进制 1 的个数较少时效率更高,时间复杂度与二进制 1 的个数有关,平均情况下也接近 O (1)。

以下是使用第二种方法的简单代码示例:

int countOnes(int n) {
    int count = 0;
    while (n!= 0) {
        n = n & (n - 1);
        count++;
    }
    return count;
}

这个函数接收一个整数 n,在循环中不断去掉 n 二进制表示中的 1,并计数,最后返回 1 的个数。

两个元素相同顺序不同的整形数组,其中一个数组少了一个数,找出缺失的数。

可以通过多种方式来找出缺失的数。

一种简单的方法是先对两个数组进行排序,然后同时遍历两个数组,比较对应位置的元素。由于两个数组除了一个缺失的数之外其他元素都相同,当在遍历过程中发现两个数组对应位置的元素不同时,在完整数组中那个位置的元素就是缺失的数。这种方法的时间复杂度主要取决于排序算法的复杂度,比如使用快速排序,时间复杂度是 O (n log n),再加上遍历比较的 O (n) 时间,总的时间复杂度是 O (n log n)。

另一种方法是利用哈希表。可以先遍历元素完整的数组,将数组中的每个元素作为键存入哈希表中,然后遍历元素有缺失的数组,对于每个元素,在哈希表中查找,如果找不到,那么这个元素就是缺失的数。这种方法的时间复杂度是 O (n),因为哈希表的插入和查找操作平均时间复杂度接近常数,但是需要额外的空间来存储哈希表。

还有一种数学方法。如果知道数组中元素是从 1 到 n 的连续整数,那么可以先计算 1 到 n 的总和,用公式n * (n + 1) / 2来计算,其中 n 是完整数组的元素个数。然后计算两个数组的元素总和,用完整数组的总和减去有缺失数组的总和,得到的结果就是缺失的数。这种方法的时间复杂度是 O (n),因为需要遍历两个数组来计算总和,而且不需要额外的空间。

以下是使用数学方法的简单代码示例:

int findMissingNumber(int* arr1, int* arr2, int n) {
    int sum1 = 0, sum2 = 0;
    for (int i = 0; i < n; i++) {
        sum1 += arr1[i];
    }
    for (int i = 0; i < n - 1; i++) {
        sum2 += arr2[i];
    }
    return sum1 - sum2;
}

这个函数接收两个指针arr1arr2,分别指向完整数组和有缺失数组,n是完整数组的元素个数。通过计算两个数组的总和差值来找出缺失的数。

手写冒泡排序

冒泡排序是一种简单的排序算法,它的基本思想是通过相邻元素的比较和交换,将最大(或最小)的元素逐步 “冒泡” 到数组的一端。

以下是冒泡排序的 C++ 代码实现:

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

在这个函数中,bubbleSort接受一个整数数组arr和数组的元素个数n。外层循环for (int i = 0; i < n - 1; i++)控制排序的轮数,总共需要进行n - 1轮排序。

内层循环for (int j = 0; j < n - i - 1; j++)用于每一轮比较和交换相邻的元素。在每一轮中,比较arr[j]arr[j + 1],如果arr[j]大于arr[j + 1],就交换它们的位置,这样每一轮结束后,当前最大的元素就会被移动到数组的末尾。

例如,对于数组[5, 4, 3, 2, 1],第一轮排序时,比较54,交换位置得到[4, 5, 3, 2, 1],接着比较53,交换得到[4, 3, 5, 2, 1],以此类推,第一轮结束后数组变为[4, 3, 2, 1, 5],此时最大的元素5已经 “冒泡” 到了末尾。然后进行第二轮排序,将第二大的元素 “冒泡” 到倒数第二位,直到整个数组有序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2265520.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenAI 普及 ChatGPT,开通热线电话,近屿智能深耕AI培训

12月19日&#xff0c;在OpenAI直播活动的第10天&#xff0c;宣布允许用户通过电话或WhatsApp与ChatGPT进行交互。并在美国推出 ChatGPT 热线电话&#xff0c;用户拨打后可与 ChatGPT 进行语音对话。 这项服务的一个亮点在于它兼容各种类型的通信设备——不论是现代智能手机如iP…

四种电子杂志制作软件

​大家好&#xff0c;今天给大家种草四种超级实用的电子杂志制作软件。无论你是专业出版人士&#xff0c;还是业余爱好者&#xff0c;这四款软件都能帮助你轻松制作出精美的电子杂志。让我们一起来看看吧&#xff01; 1.FLBOOK FLBOOK是一款在线仿真翻页制作H5电子画册&#x…

idea配置

2024.3 idea 重装idea启动失败样式常用插件 重装idea启动失败 1、检查环境变量&#xff0c;是否已设置 2、检查安装目录下&#xff0c;或对应的环境变量对应的路径文件下 是否有javaagent&#xff0c;可先移除或者检查配置是否正确 样式 1、展示上方工具栏 2、展示内存使用…

微信小程序的轮播图学习报告

微信小程序轮播图学习报告 好久都没分享新内容了&#xff0c;实在惭愧惭愧。今天给大家做一个小程序轮播图的学习报告。 先给大家看一下我的项目状态&#xff1a; 很空昂&#xff01;像一个正在修行的老道&#xff0c;空的什么也没有。 但是我写了 4 个 view 容器&#xff0c;…

L24.【LeetCode笔记】 杨辉三角

目录 1.题目 2.分析 模拟二维数组的大致思想 杨辉三角的特点 二维数组的元素设置代码 两个参数returnSize和returnColumnSizes 理解"有效"的含义 完整代码 提交结果 1.题目 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉…

项目亮点案例

其实对我来说是日常操作&#xff0c;但是如果在面试的时候面试者能把日常的事情总结好发出来&#xff0c;其实足矣。 想让别人认同项目&#xff0c;选取的示例需要包含以下要素&#xff1a; 亮点项目四要素&#xff1a;明确的目标&#xff0c;问题点&#xff0c;解决方法和结果…

Vue.js组件(5):自定义组件

1 介绍 下面的所有组件全部基于VUE3 TS element plus编写&#xff0c;其中部分组件可能涉及到其他技术栈&#xff0c;会进行单独说明。 2 基础组件 2.1 表格操作组件TableToolButton 此组件用于对表格进行增加、编辑、删除、导出操作。 2.1.1 组件属性 addVisible&#x…

ctfhub技能树——disable_functions

LD_PRELOAD 来到首页发现有一句话直接就可以用蚁剑连接 根目录里有/flag但是不能看;命令也被ban了就需要绕过了 绕过工具在插件市场就可以下载 如果进不去的话 项目地址: #本地仓库;插件存放 antSword\antData\plugins 绕过选择 上传后我们点进去可以看到多了一个绕过的文件;…

【PCIe 总线及设备入门学习专栏 1.1 -- PCIe 基础知识 lane和link介绍】

文章目录 OverivewLane 和 LinkRC 和 RPPCIe controllerPCIE ControllerPHY模块 Inbound 和 OutboundPCIe transaction modelPIODMAP2P Overivew PCIe&#xff0c;即PCI-Express总线&#xff08;Peripheral Component Interconnect Express&#xff09;&#xff0c;是一种高速…

golang LeetCode 热题 100(动态规划)-更新中

爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a;输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1. 1 阶 1 阶 2. 2 阶 示例 2&…

分布式专题(10)之ShardingSphere分库分表实战指南

一、ShardingSphere产品介绍 Apache ShardingSphere 是一款分布式的数据库生态系统&#xff0c; 可以将任意数据库转换为分布式数据库&#xff0c;并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。Apache ShardingSphere 设计哲学为 Database Plus&#xff0c;旨在…

Vue 3.5 编写 ref 时,自动插入.Value

如果是 Vue 3.2 &#xff0c;那么可能用的是Volar

深度学习中的并行策略概述:2 Data Parallelism

深度学习中的并行策略概述&#xff1a;2 Data Parallelism 数据并行&#xff08;Data Parallelism&#xff09;的核心在于将模型的数据处理过程并行化。具体来说&#xff0c;面对大规模数据批次时&#xff0c;将其拆分为较小的子批次&#xff0c;并在多个计算设备上同时进行处…

OneCode:开启高效编程新时代——企业定制出码手册

一、概述 OneCode 的 DSM&#xff08;领域特定建模&#xff09;出码模块是一个强大的工具&#xff0c;它支持多种建模方式&#xff0c;并具有强大的模型转换与集成能力&#xff0c;能够提升开发效率和代码质量&#xff0c;同时方便团队协作与知识传承&#xff0c;还具备方便的仿…

《Web 应用项目开发:从构思到上线的全过程》

目录 一、引言 二、项目启动与需求分析 三、设计阶段 四、技术选型 五、开发阶段 六、测试阶段 七、部署与上线 八、维护与更新 九、总结 一、引言 在数字化浪潮席卷全球的当下&#xff0c;Web 应用如繁星般在互联网的苍穹中闪烁&#xff0c;它们形态各异&#xff0c…

中小学教室多媒体电脑安全登录解决方案

中小学教室多媒体电脑面临学生随意登录的问题&#xff0c;主要涉及到设备使用、网络安全、教学秩序等多个方面。以下是对这一问题的详细分析&#xff1a; 一、设备使用问题 1. 设备损坏风险 学生随意登录可能导致多媒体电脑设备过度使用&#xff0c;增加设备损坏的风险。不当…

Odoo 免费开源 ERP:通过 JavaScript 创建对话框窗口的技术实践分享

作者 | 老杨 出品 | 上海开源智造软件有限公司&#xff08;OSCG&#xff09; 概述 在本文中&#xff0c;我们将深入研讨如何于 Odoo 18 中构建 JavaScript&#xff08;JS&#xff09;对话框或弹出窗口。对话框乃是展现重要讯息、确认用户操作以及警示用户留意警告或错误的行…

OOP面向对象编程:类与类之间的关系

OOP面向对象编程&#xff1a;类与类之间的关系 三大关系&#xff1a;复合&#xff08;适配器设计模式&#xff09;、委托&#xff08;桥接设计模式&#xff09;、继承 8、1复合Composition has-a -> 适配器模式 一个类里面含有另一个类的对象 —> 复合关系 has-a 适配器设…

集成 jacoco 插件,查看单元测试覆盖率

文章目录 前言集成 jacoco 插件&#xff0c;查看单元测试覆盖率1. 添加pom2. 配置完成、执行扫描3. 执行结果4. 单元测试报告 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞…

下载运行Vue开源项目vue-pure-admin

git地址&#xff1a;GitHub - pure-admin/vue-pure-admin: 全面ESMVue3ViteElement-PlusTypeScript编写的一款后台管理系统&#xff08;兼容移动端&#xff09; 安装pnpm npm install -g pnpm # 国内 淘宝 镜像源 pnpm config set registry https://registry.npmmirror.com/…