百度校招C++一面
公众号:阿Q技术站
来源:https://www.nowcoder.com/feed/main/detail/494256d167d344e1b3764d39740c83a1
1、Linux用过哪些命令?
-
ls
(List):列出目录内容。例如:
ls -l
显示详细信息,ls -a
显示所有文件(包括隐藏文件)。 -
cd
(Change Directory):切换目录。例如:
cd /path/to/directory
进入指定目录,cd ..
返回上一级目录。 -
pwd
(Print Working Directory):显示当前工作目录的路径。 -
mkdir
(Make Directory):创建新目录。例如:
mkdir new_directory
创建名为"new_directory"的新目录。 -
touch
:创建新文件。例如:
touch new_file.txt
创建名为"new_file.txt"的新文件。 -
rm
(Remove):删除文件或目录。例如:
rm file.txt
删除文件,rm -r directory
递归删除目录。 -
cp
(Copy):复制文件或目录。例如:
cp file.txt new_location/
复制文件,cp -r directory/ new_location/
复制目录。 -
mv
(Move):移动文件或目录,也可用于重命名。例如:
mv file.txt new_location/
移动文件,mv old_name.txt new_name.txt
重命名文件。 -
cat
(Concatenate):查看文件内容。例如:
cat file.txt
显示文件内容。 -
more
和less
:分页显示文件内容。例如:
more file.txt
分页显示文件内容。 -
head
和tail
:显示文件开头或结尾的内容。例如:
head -n 10 file.txt
显示文件前10行,tail -n 5 file.txt
显示文件后5行。 -
grep
:搜索文件中的文本。例如:
grep "search_text" file.txt
查找文件中包含"search_text"的行。 -
find
:在文件系统中查找文件或目录。例如:
find /path/to/search -name "filename"
查找指定文件。 -
ps
(Process Status):显示当前运行的进程。例如:
ps aux
显示所有进程的详细信息。 -
kill
:终止进程。例如:
kill process_id
终止指定进程。 -
top
:实时监控系统性能和进程活动。 -
df
:显示磁盘空间使用情况。例如:
df -h
以人类可读的方式显示磁盘使用情况。 -
du
:显示目录的磁盘使用情况。例如:
du -h directory
以人类可读的方式显示目录的磁盘使用情况。 -
wget
:从网络下载文件。例如:
wget http://example.com/file.zip
下载文件。 -
scp
:通过SSH协议进行安全文件传输。例如:
scp local_file.txt user@remote_server:/path/to/destination
将文件从本地传输到远程服务器。
2、grep有哪些参数?
-
-i
:忽略大小写例如:
grep -i "pattern" file.txt
会不区分大小写地搜索模式。 -
-r
或-R
:递归搜索例如:
grep -r "pattern" /path/to/directory
会递归搜索指定目录下的所有文件。 -
-l
:只显示包含匹配模式的文件名例如:
grep -l "pattern" *
会列出包含模式的文件名。 -
-v
:反向搜索,显示不匹配的行例如:
grep -v "pattern" file.txt
会显示文件中不包含模式的所有行。 -
-n
:显示匹配行的行号例如:
grep -n "pattern" file.txt
会显示每行匹配的行号。 -
-c
:只显示匹配的行数例如:
grep -c "pattern" file.txt
会显示匹配模式的行数。 -
-w
:只匹配整个单词例如:
grep -w "word" file.txt
会匹配整个单词 “word”,而不是包含该单词的字符串。 -
-A
:显示匹配行及其后面的若干行例如:
grep -A 2 "pattern" file.txt
会显示匹配模式的行以及后面的 2 行。 -
-B
:显示匹配行及其前面的若干行例如:
grep -B 2 "pattern" file.txt
会显示匹配模式的行以及前面的 2 行。 -
-E
:启用扩展正则表达式例如:
grep -E "(pattern1|pattern2)" file.txt
可以使用正则表达式匹配多个模式。 -
-f
:从文件中读取模式例如:
grep -f patterns.txt file.txt
会从文件 “patterns.txt” 中读取模式并搜索。 -
--include
:仅搜索指定文件类型例如:
grep "pattern" --include="*.txt" /path/to/directory
会搜索指定类型的文件。 -
--exclude
:排除指定文件类型例如:
grep "pattern" --exclude="*.log" /path/to/directory
会排除指定类型的文件。 -
--color
:高亮显示匹配文本例如:
grep --color=auto "pattern" file.txt
会用颜色高亮显示匹配的文本。
3、sed和awk用过吗?
sed
一种用于文本处理的流编辑器。
- 替换文本: 可以使用正则表达式在文本中查找并替换匹配的内容。
sed 's/old_pattern/new_pattern/g' filename
- 删除行或文本块: 可以删除包含特定文本的行或文本块。
sed '/pattern/d' filename
- 插入、追加和编辑文本: 可以插入、追加或编辑文本行。
sed '2i\This is a new line' filename
- 打印文本: 可以选择性地打印文本。
sed -n '/pattern/p' filename
awk
一种文本处理工具,它基于一种处理文本文件的编程语言,awk
适合处理结构化的文本数据。
- 文本分割和字段提取:
awk
可以将每行文本分割成字段,然后你可以访问和操作这些字段。
awk -F',' '{print $1, $3}' filename
- 数据过滤和处理: 你可以使用
awk
来筛选、转换和操作文本数据。
awk '$2 > 50 {print $1, $2}' filename
- 自定义输出格式:
awk
允许你以自定义的格式输出文本。
awk '{printf "Name: %s, Age: %d\n", $1, $3}' filename
- 循环和条件语句: 你可以在
awk
脚本中使用循环和条件语句来进行更复杂的数据处理。
awk '{if ($2 > 50) print $1, $2}' filename
4、死锁解决?
死锁发生在不同进程或线程之间,其中每个进程或线程都在等待其他进程或线程所持有的资源,导致所有进程或线程都无法继续执行。
- 预防死锁:
破坏死锁的四个条件中的一个或多个来预防死锁。但不能破坏互斥条件,其他三个都可。
- 资源分配顺序: 一种预防死锁的方法是规定资源的分配顺序,确保每个进程或线程按照相同的顺序请求资源。这样,可以避免循环等待的情况。
- 资源申请前知道资源需求: 进程在请求资源前,可以预先知道它将需要哪些资源,并一次性申请所有资源。这有助于减少死锁的可能性。
- 避免死锁:
和预防死锁的区别就是,在资源动态分配过程中,用某种方式阻止系统进入不安全状态。比如银行家算法。
- 银行家算法: 银行家算法是一种资源分配算法,用于避免死锁。它通过检查资源分配状态和资源需求,确保分配资源后系统不会陷入死锁。
- 检测和恢复:
允许系统在运行过程中发生死锁,但可已设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
- 死锁检测: 可以定期检测系统中是否存在死锁。如果检测到死锁,可以采取措施来解除死锁。
- 死锁解除: 一旦检测到死锁,可以采取以下方法之一来解除死锁:
- 终止某些进程或线程,以释放它们所占用的资源。
- 通过资源的抢占,暂时剥夺某些进程或线程的资源,直到死锁解除为止。
- 超时和重试:
进程或线程可以设置超时机制,如果它在一定时间内未能获取所需的资源,就放弃并重试。这有助于避免进程无限期地等待资源。
- 资源释放:
如果一个进程或线程在等待资源时,发现它持有的资源被其他进程或线程请求,它可以释放这些资源,然后再次请求它们。
- 资源分配策略:
采用合适的资源分配策略,如动态资源分配,以根据系统的资源状况来分配资源。这可以减少死锁的发生。
5、内核态和用户态切换?
- 内核态
内核态是操作系统的高特权级别,用于执行操作系统内核代码。在内核态中,操作系统内核可以访问系统的所有硬件资源、执行特权指令以及对系统资源进行管理和控制。
- 用户态
用户态是操作系统中的一种较低特权级别,通常用于运行普通应用程序。在用户态中,应用程序只能访问自己的内存空间和受操作系统允许的系统资源。应用程序无法直接访问硬件设备或执行特权指令。
- 切换步骤
- 中断和异常: 当应用程序需要访问特权资源或执行系统调用时,它会触发一个中断或异常。这可以是应用程序主动请求的,也可以是由硬件事件(例如时钟中断)引起的。
- 保存上下文: 当发生中断或异常时,操作系统内核会保存应用程序的当前上下文,包括寄存器、程序计数器和堆栈指针等信息。这是为了在将来再次执行应用程序时能够恢复到相同的状态。
- 切换到内核态: 操作系统内核会将处理器的特权级别从用户态切换到内核态,这通常涉及到修改处理器的状态标志位,以允许执行特权指令。
- 执行内核代码: 一旦进入内核态,操作系统内核可以执行所需的操作,如访问硬件设备、管理内存或执行系统调用。
- 恢复用户态: 当内核完成操作后,它会恢复应用程序的上下文,并将处理器的特权级别切换回用户态。
6、HTTP1.0、1.1、2.0、3.0?
HTTP是一种用于传输超文本的协议,它在不同版本中不断演化和改进,以满足不断增长的网络需求。
HTTP 1.0:
HTTP 1.0是最早的HTTP版本,定义于1996年。它的特点如下:
- 短连接: 每次请求/响应都需要建立和关闭TCP连接,导致了多次握手和释放的开销,降低了性能。
- 无状态: HTTP 1.0是无状态的,每个请求/响应都是独立的,服务器不会保存连接状态,需要在每次请求中包含所有信息。
- 明文传输: HTTP 1.0数据传输通常以明文方式进行,缺乏加密和安全性。
- HTTP头部: 头部信息较简单,只包含必要的元数据。
HTTP 1.1:
HTTP 1.1是HTTP 1.0的后续版本,于1999年发布。它引入了以下改进:
- 长连接: HTTP 1.1引入了持久连接,允许多个请求/响应共享一个TCP连接,减少了握手和释放的开销,提高了性能。
- 管道化: HTTP 1.1支持请求/响应的管道化,允许客户端发送多个请求而不等待响应,进一步提高了性能。
- Host头部: 引入了Host头部字段,允许在同一台服务器上托管多个域名,从而支持虚拟主机。
- 缓存控制: 引入了更精细的缓存控制机制,使缓存更有效。
HTTP/2.0:
HTTP/2.0是HTTP 1.1的进一步改进,于2015年发布。它引入了以下特性:
- 多路复用: HTTP/2.0允许多个请求/响应流共享一个TCP连接,通过二进制帧实现多路复用,提高了并发性能。
- 头部压缩: 引入了头部字段压缩,减小了每个请求的开销,减少了带宽占用。
- 服务器推送: 服务器可以主动将与请求相关的资源推送给客户端,减少了客户端请求的延迟。
- 优化流: HTTP/2.0通过优化请求和响应流的传输,提高了性能。
HTTP/3.0:
HTTP/3.0是最新的HTTP版本,于2020年发布。它采用了一种名为QUIC(Quick UDP Internet Connections)的新的传输协议,以进一步提高性能和安全性:
- 基于UDP: HTTP/3.0不再依赖于TCP,而是基于UDP传输,减少了连接建立的时延,提高了网络性能。
- 多路复用: 类似HTTP/2.0,HTTP/3.0支持多路复用,允许多个请求/响应流在一个连接上并发传输。
- 头部压缩: HTTP/3.0继续采用头部字段压缩,降低了请求开销。
- 连接迁移: HTTP/3.0支持在不同网络条件下迁移连接,以适应移动设备的切换。
总的来说,HTTP 1.0到HTTP/3.0的演化过程主要关注了性能、效率和安全性的提升。HTTP/2.0和HTTP/3.0引入了多路复用和头部压缩等重要特性,以改进传输效率和降低延迟。HTTP/3.0更进一步通过使用QUIC协议提供了更佳的性能和网络适应性。
7、HTTPS具体原理?
HTTPS是一种用于安全传输数据的协议,它在HTTP的基础上通过加密通信来保护数据的机密性和完整性。
- 加密通信:
HTTPS使用加密来保护数据的隐私。它通过使用加密算法来对数据进行加密和解密。主要的加密协议包括:
- SSL: SSL是HTTPS早期采用的加密协议,目前已被TLS所取代。SSL协议使用了对称加密和非对称加密。客户端和服务器之间协商密钥,然后使用对称加密来加密和解密数据。
- TLS: TLS是SSL的继任者,被广泛采用。TLS提供了更强大的加密和身份验证机制,包括支持各种密码套件,公钥基础设施,以及数字证书的使用。
- 加密算法: HTTPS使用对称加密和非对称加密。对称加密使用相同的密钥来加密和解密数据,而非对称加密使用一对公钥和私钥。常见的对称加密算法包括AES,而常见的非对称加密算法包括RSA和ECC。
- 数字证书:
HTTPS的另一个关键组成部分是数字证书,它用于验证服务器的身份。
数字证书包括以下信息:
- 服务器的公钥
- 服务器的域名
- 数字签名,由证书颁发机构(CA)签署
数字证书是通过证书颁发机构(CA)签署的,以证明服务器的身份。客户端在与服务器建立连接时会验证数字证书的有效性。如果证书有效,客户端将使用其中的公钥来加密通信数据,确保数据只能由服务器解密。
- 握手过程
- 客户端向服务端发起第一次握手请求,告诉服务端客户端所支持的SSL的指定版本、加密算法及密钥长度等信息。
- 服务端将自己的公钥发给数字证书认证机构,数字证书认证机构利用自己的私钥对服务器的公钥进行数字签名,并给服务器颁发公钥证书。
- 服务端将证书发给客户端。
- 客服端利用数字认证机构的公钥,向数字证书认证机构验证公钥证书上的数字签名,确认服务器公开密钥的真实性。
- 客户端使用服务端的公开密钥加密自己生成的对称密钥,发给服务端。
- 服务端收到后利用私钥解密信息,获得客户端发来的对称密钥。
- 通信双方可用对称密钥来加密解密信息。
8、为什么握手是三次、挥手是四次?
为什么握手是三次?
TCP建立连接时之所以只需要"三次握手",是因为在第二次"握手"过程中,服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的。SYN是请求连接标志,表示服务器端同意建立连接;ACK是确认报文,表示告诉客户端,服务器端收到了它的请求报文。
即SYN建立连接报文与ACK确认接收报文是在同一次"握手"当中传输的,所以"三次握手"不多也不少,正好让双方明确彼此信息互通。
为什么挥手是四次?
TCP释放连接时之所以需要“四次挥手”,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次"挥手"传输的。为何建立连接时一起传输,释放连接时却要分开传输?
- 建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。
- 释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。
9、输入一个地址每一层做了啥?
- 物理层:
- 物理层负责将数字数据转换为模拟信号或数字信号,以便通过网络传输。
- 在这一层,你的计算机将 IP 地址转换成电压、光信号或者其他物理信号,以便将数据传输到物理介质上,比如以太网电缆、Wi-Fi 信号等。
- 数据链路层:
- 数据链路层负责将物理层传输的数据分割成帧(Frames),并添加帧首部和帧尾部,以便将数据在本地网络中传输。
- 本地网络设备(如交换机)将数据帧从一个设备传输到另一个设备,保证数据在局域网内的传输。
- 网络层:
- 网络层将数据帧封装成数据包(Packets)并添加源和目标 IP 地址。
- 路由器会根据目标 IP 地址来选择最佳路径将数据包从本地网络传输到目标网络。
- 传输层:
- 传输层负责将数据包封装成段(Segments),并添加源和目标端口号。
- 传输层协议(如 TCP 或 UDP)负责控制数据流、错误检测和纠正、流量控制以及数据传输的可靠性。
- 会话层:
- 会话层建立、管理和终止网络会话。
- 在网络通信中,会话层可能会涉及到建立到目标服务器的连接,并在连接上进行数据传输。
- 表示层:
- 表示层负责数据的编码、加密和压缩,以确保数据在通信过程中的完整性和安全性。
- 这一层可能会涉及到数据格式的转换,以适应不同系统之间的差异。
- 应用层:
- 应用层包括用户应用程序,它们使用网络协议与网络进行通信。
- 在这个层次上,你的 Web 浏览器使用 HTTP 协议请求指定 IP 地址的网页,服务器接收请求并响应,提供页面内容。
10、python生成器?
生成器是 Python 中一种特殊的迭代器(Iterator)对象,它允许你按需生成值,而不是一次性生成所有值并将它们存储在内存中。生成器通常用于处理大数据集或无限序列,以避免占用大量内存。生成器的核心思想是延迟生成,它允许你在需要时逐个生成值。
在 Python 中,有两种创建生成器的方式:使用生成器表达式和使用函数定义生成器。
- 生成器表达式:
生成器表达式类似于列表推导,但是使用圆括号而不是方括号,并返回一个生成器对象。生成器表达式的语法如下:
(generator_expression)
示例:
# 生成一个包含1到10之间所有偶数的生成器
even_numbers = (x for x in range(1, 11) if x % 2 == 0)
# 使用生成器表达式生成值
for num in even_numbers:
print(num)
- 使用函数定义生成器:
使用函数来创建生成器,这种方式更加灵活,因为它允许你在生成值的过程中编写更多的逻辑。生成器函数使用 yield
语句来生成值,当遇到 yield
语句时,函数的状态被保存,允许你在下一次迭代中继续执行函数。
示例:
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
# 创建生成器对象
counter = count_up_to(5)
# 使用生成器函数生成值
for num in counter:
print(num)
需要注意的是,生成器在每次迭代后会保持其状态,这使得它们非常适合处理大数据集或需要分阶段生成值的情况。 11、python协程和进程?
协程(Coroutines):
协程是一种轻量级的、用户级的线程,也称为微线程。它允许你在一个单一的线程中实现多个执行上下文,从而实现并发。在 Python 中,协程通常使用 async
和 await
关键字来实现,这是异步编程的一部分。协程的主要特点包括:
- 非抢占式:协程是协作式多任务处理的一种形式,它不是操作系统进行的线程或进程抢占式多任务处理。在协程中,程序员需要显式地控制何时让出 CPU 控制权。
- 低开销:协程比线程和进程更轻量级,因为它们不需要操作系统级的线程切换,而是在应用程序级别进行切换。
- 单线程内多协程:协程可以在单个线程内创建多个执行上下文,从而在一个线程中实现并发。
- 异步编程:协程通常与异步编程框架(如 asyncio)一起使用,用于处理非阻塞的 I/O 操作,例如网络请求和文件读写。
示例:
import asyncio
async def foo():
await asyncio.sleep(1)
print("Foo")
async def bar():
await asyncio.sleep(2)
print("Bar")
async def main():
await asyncio.gather(foo(), bar())
asyncio.run(main())
进程(Process):
进程是操作系统级别的并发单位,每个进程都有独立的内存空间和系统资源。进程之间的通信通常需要使用特殊的机制,如进程间通信(IPC)。在 Python 中,可以使用 multiprocessing
模块来创建和管理多个进程。进程的主要特点包括:
- 独立内存空间:每个进程有自己的独立内存空间,不同进程之间的数据不会相互干扰。
- 抢占式:操作系统会自动分配 CPU 时间片给不同的进程,实现抢占式多任务处理。
- 多核利用:进程可以同时利用多个 CPU 核心,因此适用于 CPU 密集型任务。
- 独立性:进程之间的错误不会相互影响,一个进程崩溃不会导致其他进程崩溃。
示例:
from multiprocessing import Process
def foo():
print("Foo")
def bar():
print("Bar")
if __name__ == "__main__":
p1 = Process(target=foo)
p2 = Process(target=bar)
p1.start()
p2.start()
p1.join()
p2.join()
区别和应用场景:
- 性能和资源开销:协程通常比进程具有更低的性能开销,因为它们在同一个进程内运行,而不需要操作系统级的进程切换。进程具有更高的性能开销,但适用于需要充分利用多核 CPU 的 CPU 密集型任务。
- 通信和同步:协程通常使用异步编程框架来进行通信和同步,而进程需要使用 IPC 机制来实现通信。协程更适用于处理 I/O 密集型任务和异步编程。
- 独立性:进程是完全独立的,一个进程崩溃不会影响其他进程,而协程在同一个进程内共享相同的内存空间,因此一个协程的错误可能会影响其他协程。
- 多核利用:进程可以利用多核 CPU,而协程通常在单个核心上运行,除非使用多线程或多进程来充分利用多核。
12、流式输出?
流式输出(Stream Output)是指将数据流按特定格式输出到屏幕、文件或其他目标设备的过程。在C++中,流式输出通常是使用<<
运算符结合流对象(如cout
、ofstream
等)来实现的。
-
流对象: 在C++中,流对象用于连接到不同的输出目标。常见的流对象包括:
-
cout
:用于标准输出,通常是终端或控制台窗口。 -
cerr
:用于标准错误输出,通常也是终端或控制台窗口。 -
ofstream
:用于写入文件。 -
ostringstream
:用于字符串流,将输出文本写入字符串变量。
-
-
流操作符
<<
: 流操作符是<<
,用于将数据插入到输出流中。这个运算符可以连接多个数据项,并按照格式化规则输出到流对象。 -
输出数据类型: 流式输出可以处理多种数据类型,包括整数、浮点数、字符、字符串和自定义类型等。
-
格式化输出: 可以使用流操作符的参数来指定输出的格式,例如设置输出的小数位数、对齐方式、宽度等。例如:
cout << "Value: " << 42 << " Float: " << fixed << setprecision(2) << 3.14159 << endl;
-
控制换行: 使用
endl
流操作符或'\n'
控制字符来换行输出。 -
自定义输出运算符: 对于自定义的数据类型,可以通过重载
<<
运算符来指定如何输出该类型的对象。这允许你以自定义的方式输出数据。
示例:
#include <iostream>
#include <string>
int main() {
int number = 42;
double pi = 3.14159;
std::string text = "Hello, World!";
std::cout << "Number: " << number << std::endl;
std::cout << "Pi: " << pi << std::endl;
std::cout << "Text: " << text << std::endl;
return 0;
}
13、适配器模式?
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许你将一个类的接口转换成客户端所期望的另一个接口。这种模式通常用于使已存在的类与其他类一起工作,而这些类的接口不兼容或不匹配。适配器模式是软件开发中常见的模式,它有助于解决不同类之间接口不一致的问题。
适配器模式涉及以下几个角色:
- 目标接口(Target):这是客户端所期望的接口,也是客户端代码直接调用的接口。适配器模式的目标是将适配器类适配成这个接口。
- 适配器(Adapter):适配器是一个类,它实现了目标接口,并包装了需要适配的类。它充当目标接口与需要适配的类之间的桥梁,使它们能够协同工作。适配器通常包含一个对需要适配的类的引用。
- 需要适配的类(Adaptee):这是需要适配的类,它有一个与目标接口不兼容的接口。
给个例子:
// 目标接口
class Target {
public:
virtual void request() = 0;
};
// 需要适配的类
class Adaptee {
public:
void specificRequest() {
std::cout << "Adaptee's specific request." << std::endl;
}
};
// 适配器
class Adapter : public Target {
private:
Adaptee* adaptee;
public:
Adapter(Adaptee* a) : adaptee(a) {}
void request() override {
adaptee->specificRequest();
}
};
int main() {
Adaptee* adaptee = new Adaptee();
Target* adapter = new Adapter(adaptee);
adapter->request();
delete adaptee;
delete adapter;
return 0;
}
14、new和malloc差异?
- 语法差异:
new
是 C++ 中的运算符,而malloc
是 C 和 C++ 中的库函数。new
在分配内存时还会调用对象的构造函数,因此适用于动态对象的分配。malloc
仅分配一块原始内存块,不会调用对象的构造函数。你需要显式调用构造函数,适用于分配原始数据块的情况。
- 类型安全性:
new
在分配内存时会考虑到对象的类型,因此它是类型安全的。如果你尝试分配一个类的对象,new
会自动调用构造函数,并返回正确类型的指针。malloc
返回一个void*
指针,不考虑对象类型。这可能导致类型不匹配或未初始化的对象。
- 内存块的大小:
new
需要知道对象的大小以正确分配内存,因此不需要指定要分配的字节数。这意味着它将分配足够大小的内存以容纳对象。malloc
需要你明确指定要分配的字节数,它不考虑对象的大小。这可能导致分配的内存块过大或过小。
- 异常处理:
new
可以抛出std::bad_alloc
异常,以处理内存不足的情况。malloc
仅返回NULL
指针,需要手动检查分配是否成功。
15、为啥要有析构函数?
用于释放对象占用的资源,清理对象的状态,并执行必要的清理工作。
- 释放资源: 在对象的生命周期结束时,特别是在动态分配内存、打开文件、建立网络连接等需要手动释放的资源时,析构函数用于释放这些资源,避免内存泄漏和资源泄漏。
- 清理状态: 析构函数用于清理对象的内部状态。这可以包括将对象恢复到初始状态、释放分配的缓冲区、断开连接、关闭文件等。这有助于确保对象在销毁时不会留下不一致或不合理的状态。
- 回收资源: 在某些情况下,析构函数可以被用于实现自动资源管理,例如使用智能指针。智能指针在析构函数中释放资源,从而确保资源的正确释放。
- 自定义清理逻辑: 对于用户自定义类,析构函数允许程序员定义自己的清理逻辑。这样,你可以确保对象销毁时执行特定的操作,比如写日志、通知其他对象等。
- 异常安全性: 析构函数在处理异常时起到重要作用。如果在析构函数中正确处理异常,可以确保资源的释放和状态的清理不会导致程序异常崩溃。
在C++中,析构函数的名称与类名称相同,前面加上波浪号(~)。它没有返回值,也不接受参数。当对象超出其作用域或通过 delete
运算符释放时,析构函数会自动调用。
16、智能指针内存泄漏的情况?
- 循环引用(Circular References): 这是最常见的智能指针内存泄漏情况之一。当两个或多个智能指针互相引用,形成一个循环引用时,这些对象将永远不会被销毁,因为它们的引用计数永远不会达到零。为了避免这种情况,可以使用
std::weak_ptr
来打破循环引用。
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b = b; // a和b互相引用
b->a = a;
解决方法:
struct A {
std::shared_ptr<B> b;
};
struct B {
std::weak_ptr<A> a; // 使用weak_ptr打破引用循环
};
- 裸指针(Raw Pointers): 如果你将智能指针中的裸指针传递给其他函数或存储为全局变量,可能导致内存泄漏。这是因为这些裸指针超出了智能指针的管理范围,而无法正确释放资源。确保在传递智能指针时不要泄露裸指针,并避免存储智能指针的裸指针。
std::shared_ptr<int> sp = std::make_shared<int>(42);
int* rawPtr = sp.get(); // 不应该将sp.get()泄露到其他函数或全局变量中
解决方法是,尽量避免使用裸指针,如果必须使用,确保在智能指针生存期内使用它。
- 循环依赖(Circular Dependencies): 这种情况发生在多个对象相互依赖的场景中。每个对象都持有其他对象的智能指针,可能导致内存泄漏。
解决方法是使用std::weak_ptr
来打破循环依赖,就像处理循环引用一样。
- 手动
reset
或release
: 如果你手动调用reset
或release
方法,可能会导致智能指针失去对资源的控制,从而导致内存泄漏。
std::shared_ptr<int> sp = std::make_shared<int>(42);
sp.reset(); // 此时sp不再管理资源,可能导致内存泄漏
要避免这种情况,不要手动重置智能指针,让其自然析构即可。
17、手撕:链表删除节点 给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。 返回删除后的链表的头节点。
思路:
- 首先,要检查链表是否为空。如果链表为空,就没有节点可删除。
- 遍历链表,找到要删除节点的前一个节点,以便将其 “跳过”。
- 修改前一个节点的
next
指针,将其指向删除节点的下一个节点,从而跳过要删除的节点。 - 释放删除节点的内存,以防止内存泄漏。
- 最后,返回链表的头节点,因为头节点可能在删除时发生变化。
示例代码:
#include <iostream>
// 定义链表节点
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* deleteNode(ListNode* head, int val) {
// 处理空链表的情况
if (head == nullptr) {
return nullptr;
}
// 创建一个虚拟头节点,以便处理头节点可能被删除的情况
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* prev = dummy;
// 遍历链表查找要删除的节点
while (head != nullptr) {
if (head->val == val) {
prev->next = head->next;
delete head;
break;
}
prev = head;
head = head->next;
}
// 释放虚拟头节点,返回实际的头节点
ListNode* newHead = dummy->next;
delete dummy;
return newHead;
}
// 打印链表
void printList(ListNode* head) {
while (head != nullptr) {
std::cout << head->val;
head = head->next;
if (head) {
std::cout << " -> ";
}
}
std::cout << std::endl;
}
int main() {
// 创建一个链表: 1 -> 2 -> 3 -> 4
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
std::cout << "Original List: ";
printList(head);
int targetValue = 0;
std::cin >> targetValue;
head = deleteNode(head, targetValue);
std::cout << "List after deleting " << targetValue << ": ";
printList(head);
// 释放链表内存
while (head != nullptr) {
ListNode* temp = head;
head = head->next;
delete temp;
}
return 0;
}