Skip to content

科大讯飞C++面经

来源:https://www.nowcoder.com/discuss/547456005882298368

1、谈谈你对面向对象的理解?

  1. 对象:对象是OOP的核心概念,它代表了现实世界中的实体或抽象概念。每个对象具有属性(数据)和方法(函数),这些方法定义了对象可以执行的操作。
  2. 类:类是对象的模板或蓝图。它定义了对象的属性和方法。类可以看作是对象的设计规范,它描述了对象应该如何创建和操作。
  3. 封装:封装是一种将数据和方法组合到一个单元中的机制。对象将数据隐藏在内部,只允许通过公共接口(方法)来访问和修改数据。这提供了数据的安全性和灵活性。
  4. 继承:继承是OOP的一个关键概念,它允许创建新类(子类)基于已存在的类(父类)。子类可以继承父类的属性和方法,并可以添加新属性和方法,或覆盖继承的方法。
  5. 多态:多态性允许不同类的对象对相同的消息做出不同的响应。它允许在不知道对象的确切类型的情况下编写通用代码。
  6. 抽象类:抽象类是不能被实例化的类,它定义了一组方法的接口,但这些方法的具体实现留给了子类。它用于描述通用行为。
  7. 接口:接口是一种特殊类型的抽象类,它只包含方法的声明,没有具体实现。类可以实现一个或多个接口来定义其行为。

2、指针和引用的区别?

  1. 定义:
    • 指针:指针是一个变量,它存储了另一个变量的内存地址。定义指针时使用*符号,例如int* ptr;定义了一个整数指针。
    • 引用:引用是一个别名,它是一个已存在变量的别名。定义引用时使用&符号,例如int& ref = x;定义了一个整数引用。
  2. 内存地址:
    • 指针:指针存储了变量的内存地址,可以指向不同的对象,可以为nullptr(空指针)。
    • 引用:引用是已存在变量的别名,它没有自己的内存地址,它直接绑定到另一个变量。
  3. 指向:
    • 指针:指针可以在不同的时间指向不同的对象,通过改变指针的值来实现。
    • 引用:引用在创建时被绑定到一个对象,它不能在后续指向其他对象。
  4. 语法:
    • 指针:通过解引用运算符*来访问指针指向的对象,例如*ptr
    • 引用:引用使用原始的变量名来访问对象,例如ref
  5. 空引用:
    • 指针:指针可以为nullptr,表示不指向任何对象。
    • 引用:引用不能为空,它在创建时必须绑定到一个对象。
  6. 传递给函数:
    • 指针:指针可以用于传递对象的地址给函数,实现函数参数的传递。
    • 引用:引用也可用于传递对象给函数,可以实现更加直观的函数调用。
  7. 用途:
    • 指针:通常用于动态内存分配、数组、数据结构、以及需要指向多个对象的情况。
    • 引用:通常用于函数参数传递、操作大型数据结构以避免复制、以及实现运算符重载。
  8. 安全性:
    • 指针:指针需要谨慎使用,因为它们可以导致空指针异常和内存泄漏等问题。
    • 引用:引用通常更加安全,因为它们总是指向一个有效的对象,但需要注意生命周期管理。

3、声明和定义的区别?

  1. 声明:

    • 声明是指在程序中告诉编译器一个标识符的存在,但不为它分配内存或分配空间。这就像是一个承诺,告诉编译器在程序的某处会有一个具有特定名称和类型的标识符。

    • 声明通常包括变量、函数、类或其他用户定义的类型的名称和类型信息,但不包括初始化或分配内存的过程。

示例:

C++
extern int x; // x被声明为一个int类型的变量
void myFunction(); // myFunction被声明为一个函数
class MyClass; // MyClass被声明为一个类
  1. 定义:

    • 定义是指在程序中为标识符分配内存或空间,也可以为它们分配初始值。定义实际上创建了标识符的实体,使其可以在程序中使用。

    • 通常,只有一个声明,而有一个定义,但在某些情况下,一个声明也可能是一个定义(例如,在变量定义中初始化时)。

示例:

C++
int x = 10; // x被定义为一个int类型的变量并初始化为10
void myFunction() {
    // 函数定义
}
class MyClass {
    // 类定义
};

声明是指向编译器声明某个标识符的类型和名称,而定义是在程序中为这个标识符分配内存或空间,或为其分配初始值。

4、C和C++中struct的区别?

在C中:

  1. 在C中,struct可以包含数据成员,但不能包含成员函数。
  2. 默认情况下,struct的成员都是公有的,可以从结构体外部访问。
  3. C中的struct通常用于将相关数据字段组合在一起,而不支持封装和面向对象编程的特性。

示例:

C++
struct Point {
    int x;
    int y;
};

int main() {
    struct Point p1;
    p1.x = 10;
    p1.y = 20;
    return 0;
}

在C++中:

  1. 在C++中,struct可以包含数据成员,也可以包含成员函数。这使得C++的struct具备类似于类的特性,包括封装、继承和多态。
  2. 默认情况下,C++中的struct的成员是公有的,但可以通过关键字"private"、"protected"和"public"进行访问控制。
  3. C++中的struct用于定义类似于类的结构,可以支持面向对象编程的特性。

示例:

C++
struct Point {
    int x;
    int y;
    
    void printCoordinates() {
        std::cout << "X: " << x << ", Y: " << y << std::endl;
    }
};

int main() {
    Point p1;
    p1.x = 10;
    p1.y = 20;
    p1.printCoordinates();
    return 0;
}

C中的struct主要用于数据聚合,而C++中的struct在某种程度上类似于类,支持更多的面向对象特性。

5、覆盖和重载的区别?

覆盖(Override):

  1. 覆盖是指派生类(子类)中的函数重新实现(重写)基类(父类)中的虚函数。
  2. 覆盖通常涉及继承和多态。在派生类中的虚函数使用与基类中的虚函数具有相同的名称、参数列表和返回类型,以便在运行时动态绑定到派生类的实现。
  3. 覆盖允许派生类根据其特定需求自定义虚函数的行为,而不改变函数的接口。

示例:

C++
class Shape {
public:
    virtual void draw() {
        // 基类的虚函数实现
    }
};

class Circle : public Shape {
public:
    void draw() override {
        // 派生类覆盖基类的虚函数
        // 自定义绘制圆的实现
    }
};

重载(Overload):

  1. 重载是指在同一类中或在相同的作用域内,使用相同的名称定义多个函数,但它们的参数列表不同。
  2. 重载与函数签名(函数的名称和参数列表)有关。编译器根据调用时提供的参数来选择正确的函数重载版本。
  3. 重载允许在相同的类或命名空间内定义具有相同名称但不同参数的多个函数。

示例:

C++
class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
    
    double add(double a, double b) {
        return a + b;
    }
};

总结:

  • 覆盖用于在派生类中重写基类的虚函数,实现多态性。
  • 重载用于在相同作用域内创建具有相同名称但不同参数列表的多个函数,使函数可以根据提供的参数进行调用重载版本。

6、线程生命周期中可能出现的状态?

  1. 新建状态:当创建一个线程时,它进入新建状态。在这个阶段,操作系统分配资源,但线程还没有开始执行。
  2. 就绪状态:线程已经准备好运行,但尚未获得CPU时间片。它在等待操作系统的调度,以便在CPU上运行。
  3. 运行状态:线程获得CPU时间片并正在执行。在多任务系统中,多个线程可能会在运行状态之间切换。
  4. 阻塞状态:线程可能由于等待某些条件或资源而被阻塞,例如等待I/O操作完成、等待互斥锁、等待信号等。当条件满足时,线程会从阻塞状态切换到就绪状态。
  5. 死亡状态:线程完成了它的任务或被强制终止后,它进入死亡状态。在这个状态下,线程释放了它占用的资源,包括分配的内存和句柄。

7、线程同步互斥方式?

  1. 互斥锁(Mutex):互斥锁是最常见的线程同步机制之一。它允许一个线程进入临界区(一段需要同步的代码),而其他线程必须等待锁被释放后才能进入。这确保了在某一时刻只有一个线程可以执行临界区的代码。
  2. 信号量(Semaphore):信号量是一个计数器,用于控制对资源的访问。它可以用于限制多个线程对资源的并发访问,例如,可以设置信号量为1来实现互斥。
  3. 条件变量(Condition Variable):条件变量用于线程之间的通信和等待。一个线程可以等待某个条件为真,而其他线程可以在满足条件时通知等待线程。
  4. 读写锁(Read-Write Lock):读写锁允许多个线程同时读取共享数据,但只有一个线程可以写入。这可以提高多线程读取性能,但在写入时需要互斥。
  5. 自旋锁(Spin Lock):自旋锁是一种互斥机制,它不会将线程阻塞,而是会一直尝试获取锁。这适用于短时间的临界区,可以减少上下文切换的开销。
  6. 原子操作(Atomic Operations):原子操作是不可中断的操作,可以确保在多线程环境中对共享数据的操作是原子的。这包括原子整数操作和CAS(Compare-and-Swap)等。

8、i++、++i是线程安全的吗?

  1. i++(后缀递增):i++ 首先返回 i 的当前值,然后将 i 的值加一。在多线程环境下,如果两个线程同时尝试执行 i++ 操作,可能会导致竞态条件,因为它们都可以读取相同的 i 值,然后同时增加它。这可能导致结果不一致。
  2. ++i(前缀递增):++i 首先将 i 的值加一,然后返回增加后的值。与后缀递增不同,前缀递增通常是线程安全的,因为它在递增和返回新值之间没有中断点。在大多数编程语言中,前缀递增通常会使用原子操作,从而保证线程安全性。

9、C++中如何创建线程?

通过一个例子来解释:

C++
#include <iostream>
#include <thread>

// 线程函数,将在新线程中执行
void threadFunction() {
    std::cout << "This is a new thread!" << std::endl;
}

int main() {
    // 创建一个新线程并指定要执行的函数
    std::thread myThread(threadFunction);

    // 等待新线程执行完成
    myThread.join();

    std::cout << "Main thread is done!" << std::endl;

    return 0;
}

C++中,在头文件<thread>中包含,然后定义了一个名为 threadFunction 的函数,它将在新线程中执行。

10、stl中vector和map是线程安全的吗?

C++标准库中的std::vectorstd::map并不是线程安全的。这意味着如果多个线程同时访问和修改这些容器,没有任何保护机制,可能会导致竞态条件和数据损坏。

如果在线程环境中使用这些容器,你需要采取额外的措施来确保线程安全。有几种方法可以实现线程安全的操作:

  1. 使用互斥锁(Mutex):你可以在访问这些容器之前使用互斥锁来保护它们,从而一次只允许一个线程访问容器。这可以通过std::mutex来实现。
  2. 使用线程安全的数据结构:C++标准库的std::shared_mutex(C++17及更高版本支持)和第三方库(例如std::shared_mutexfolly::ConcurrentHashMap等)提供了线程安全的容器和数据结构,可用于多线程环境。
  3. 使用并行算法:C++标准库提供了一些并行算法,如std::for_eachstd::transform,它们可以帮助你在多线程环境中安全地遍历容器。
  4. 使用线程局部存储:每个线程都有自己的独立存储,因此你可以在每个线程中使用非线程安全的容器,而不必担心竞态条件。

11、C++中如何捕获信号?

一般步骤:

  1. 包含必要的头文件:首先,你需要包含一些头文件来使用信号处理函数。在C++中,通常会包含 <csignal> 头文件。
C++
#include <csignal>
  1. 定义信号处理函数:你需要编写一个信号处理函数,该函数的返回类型应该是 void,接受一个整数参数,该参数代表接收到的信号。
C++
void signalHandler(int signal) {
    // 处理接收到的信号
}
  1. 设置信号处理函数:你需要将信号处理函数与特定的信号关联。这可以通过 signal 函数来完成。该函数接受两参数,第一个参数是信号的值,第二个参数是信号处理函数的指针。
C++
signal(SIGINT, signalHandler); // 设置处理Ctrl+C信号

SIGINT 信号(通常由 Ctrl+C 触发)与 signalHandler 函数关联。

  1. 处理信号:当程序运行时,如果接收到与你设置的信号相关的事件,将调用信号处理函数。你可以在信号处理函数中定义信号触发时要执行的操作。

  2. 可选:取消信号处理:如果你想取消信号处理并恢复信号的默认行为,可以使用 signal 函数将信号处理函数设置为 SIG_DFL

C++
signal(SIGINT, SIG_DFL); // 恢复Ctrl+C信号的默认处理

12、socket中如何指定TCP还是UDP传输?

使用socket()函数来创建套接字,然后使用bind()函数来绑定地址和端口。

  1. 创建TCP套接字:
C++
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

SOCK_STREAM 参数用于创建一个TCP套接字,AF_INET 表示使用IPv4地址族。

  1. 创建UDP套接字:
C++
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

SOCK_DGRAM 参数用于创建一个UDP套接字,同样使用了IPv4地址族。

  1. 套接字类型参数为0

在某些情况下,你可以将套接字类型参数设置为0,这样系统会根据地址族参数来选择默认的套接字类型。通常,如果你指定地址族为AF_INET,系统将选择TCP套接字;如果你指定地址族为AF_INET6,则系统将选择IPv6的TCP套接字。

13、如果服务端接入了很多客户端,如何做会话的亲和性?

  1. IP地址和端口号: 最简单的方法是为每个客户端分配唯一的IP地址和端口号组合。这样,特定客户端的请求将总是被路由到同一个服务器端进程,确保会话亲和性。
  2. Cookie: 在Web应用程序中,可以使用会话Cookie来维护会话亲和性。服务器在客户端的第一次请求中发送一个唯一的会话标识符(通常是Cookie),客户端在随后的请求中将这个标识符包含在请求中。服务器使用这个标识符将请求路由到正确的会话。
  3. URL重写: 在某些情况下,会话标识符可以通过URL参数进行传递。服务器生成带有会话标识符的URL,并将其返回给客户端。客户端在随后的请求中使用这个URL,确保请求被路由到正确的会话。
  4. SSL会话标识符: 在使用SSL/TLS进行安全通信的情况下,SSL会话标识符可以用于维护会话亲和性。
  5. 分布式会话存储: 如果应用程序在多个服务器上运行,可以使用分布式缓存或数据库来存储会话数据,确保在任何服务器上都可以访问相同的会话信息。
  6. 负载均衡器配置: 负载均衡器可以配置为使用一些均衡算法,如IP哈希或源IP地址绑定,以确保特定客户端的请求被路由到相同的服务器。

14、用户将文件上传到服务端的文件系统,服务器如何做流量控制?

  1. 带宽限制: 服务器可以限制每个客户端或连接的带宽。这可以通过配置服务器或使用专门的带宽管理工具来实现。服务器可以设置每个连接的最大传输速率,以确保资源分配合理。
  2. 队列管理: 服务器可以为文件上传请求维护一个队列,以限制同时处理的请求数。这可以防止太多的客户端同时上传文件导致资源不足。
  3. 请求配额: 为了确保公平性,服务器可以为每个客户端设置请求配额。这意味着每个客户端在一段时间内只能上传一定数量的文件或数据。
  4. 等待时间: 如果服务器资源有限,可以要求客户端在上传文件之前等待一段时间。这种等待时间可以根据服务器负载情况进行调整。
  5. 资源监控: 服务器可以监控系统资源的使用情况,如CPU、内存和磁盘空间。如果资源接近枯竭,服务器可以暂停或拒绝新的上传请求,直到资源恢复正常。
  6. 压缩和分片: 服务器可以要求客户端在上传文件之前对文件进行压缩,以减少传输的数据量。另外,可以要求客户端将大文件分成小块进行上传,以减轻服务器的负担。
  7. 错误处理: 如果上传中断或失败,服务器应该有适当的错误处理机制,以便及时释放资源,防止资源泄漏。
  8. 记录和监控: 服务器应该记录上传请求的信息,以便进行后续分析和监控,以便及时检测并解决潜在的流量控制问题。

15、如何分别对普通用户和VIP用户做流量管控?

  1. 带宽限制: 普通用户可以被限制在较低的带宽上,以确保他们不会占用过多的网络资源,而VIP用户可以获得更高的带宽。这可以通过流量调度器或带宽管理设备来实现。
  2. 配额控制: 可以为VIP用户分配更多的资源配额,如下载量、上传量或请求次数。这样,VIP用户可以享受更多的服务资源。
  3. 并发连接限制: 对于某些服务,可以限制普通用户的并发连接数,以防止滥用。VIP用户可以享有更高的并发连接限制。
  4. 优先级队列: 在高负载情况下,可以为VIP用户的请求设置更高的优先级,以确保他们的请求在普通用户之前获得服务。
  5. 高级服务: VIP用户可以获得更多的高级服务功能,如无广告浏览、高清视频流或更快的下载速度。
  6. 访问控制列表(ACL): 使用ACL来限制普通用户对某些内容的访问,或允许VIP用户访问特定的资源。
  7. 计费策略: VIP用户通常需要支付额外的费用来享受更多的服务。这可以通过不同的计费策略来实现,如包月、包年或按使用量计费。
  8. 用户身份验证: 通过用户身份验证来识别VIP用户,以确保只有已认证的用户才能享受VIP服务。

16、硬链接和软链接的区别?

  1. 硬链接:
    • 硬链接是文件系统中的多个目录项(文件名)指向相同的物理文件数据块。这意味着所有硬链接都共享相同的inode和数据块。
    • 如果你删除一个硬链接,文件的数据块不会被删除,只有当所有硬链接都被删除后,文件的数据块才会被释放。
    • 硬链接不能跨越文件系统的边界,因为不同文件系统有不同的inode表。
    • 硬链接不支持目录。
  2. 软链接:
    • 软链接是一个特殊的文件,它包含指向另一个文件或目录的路径。软链接的目标可以是文件或目录,甚至可以是不在同一文件系统中的路径。
    • 删除软链接不会影响目标文件,目标文件被删除后软链接将成为"坏链接",也称为"悬挂链接"。
    • 软链接支持目录,可以创建指向其他目录的链接。

区别:

  • 硬链接是多个目录项指向相同的数据块,删除硬链接不会删除数据块。
  • 软链接是指向另一个文件或目录的路径,删除软链接不会影响目标文件,如果目标文件被删除,软链接将成为坏链接。
  • 硬链接不支持目录。
  • 软链接支持目录,可以跨越文件系统边界。

17、Linux中如何设置文件属性为当前用户组内其他成员只读不能写?

  1. 使用chmod命令设置文件权限:

    首先,你可以设置文件的权限,确保当前用户组内其他成员只能读取文件。你可以使用以下命令:

C++
chmod 644 文件名

这会将文件的权限设置为 -rw-r--r--,意味着文件所有者有读写权限,但其他用户只有读权限。

  1. 使用chown命令设置文件的属主和属组:

    确保文件的属主是你的用户名,文件的属组是你的用户组,可以使用以下命令:

C++
chown 用户名:用户组 文件名

这样,文件的权限被设置为只读,只有文件所有者(你)可以进行写操作,其他当前用户组内的成员只能读取文件。

18、如何查看所有的TCP连接?

  1. 使用netstat命令:
C++
netstat -tuln

这会列出所有的TCP连接,包括已经建立的连接和正在监听的连接。

C++
-t:显示TCP连接。
-u:显示UDP连接。
-l:显示正在监听的连接。
-n:显示数值格式的IP地址和端口号,而不是域名。
  1. 使用ss命令:
C++
ss -tuln

ss命令提供了与netstat类似的功能,可以列出TCP和UDP连接,以及监听的连接。

C++
-t:显示TCP连接。
-u:显示UDP连接。
-l:显示正在监听的连接。
-n:显示数值格式的IP地址和端口号,而不是域名。

19、python中的垃圾回收机制?

Python使用垃圾回收机制来自动管理内存。Python的垃圾回收机制主要依赖于引用计数和循环引用检测。Python中的垃圾回收机制的工作原理:

  1. 引用计数:Python中的每个对象都有一个引用计数,用于跟踪对象的引用情况。当一个对象的引用计数为0时,表示没有任何引用指向它,这时可以释放对象所占用的内存。
  2. 循环引用检测:引用计数虽然能够处理绝大多数的内存管理问题,但当存在循环引用时,引用计数会出现问题。循环引用是指多个对象相互引用,导致它们的引用计数永远不会降为0。为了解决这个问题,Python引入了循环引用检测机制。Python的垃圾回收器会周期性地检测循环引用,并清理不再访问的循环引用对象。
  3. 垃圾回收算法:Python的垃圾回收器采用了分代垃圾回收算法,将对象分为不同的代。新创建的对象被放入第0代,经过一定时间后,如果仍然存活,就会升级到下一代。只有第0代会频繁进行垃圾回收,而其他代的垃圾回收相对较少。这个分代策略有效地减少了垃圾回收的开销。
  4. 自动内存管理:Python的垃圾回收器通常是自动执行的,程序员不需要手动释放内存。当一个对象的引用计数变为0,或者它被循环引用检测标记为垃圾时,Python会自动回收这些对象所占用的内存。

虽然Python的垃圾回收机制能够有效管理内存,但在某些情况下,仍然需要注意避免循环引用,以免内存泄漏。可以使用gc模块来手动进行垃圾回收的控制和调试。

20、python中class的init方法和new方法有什么区别?

  1. new 方法:
    • __new__ 是在一个对象实例被创建之前调用的,通常用于控制对象的创建过程。
    • __new__ 方法通常用于创建不可变的对象,如intstrtuple等。
    • __new__ 方法的第一个参数是类本身(通常为cls),它用于创建实例对象。
    • __new__ 方法返回一个新创建的对象实例,如果不返回对象,则__init__ 方法不会被调用。
    • 通常情况下,不需要自定义__new__ 方法,因为大多数对象可以通过直接调用类的构造函数来创建。
  2. init 方法:
    • __init__ 是在对象实例创建后被调用的,用于初始化对象的属性。
    • __init__ 方法通常用于可变对象(如自定义类)的初始化,以设置对象的属性。
    • __init__ 方法的第一个参数是对象实例本身(通常为self),用于引用该对象。
    • __init__ 方法不返回任何值,它只用于初始化对象的状态。

简单例子作理解:

C++
class MyClass:
    def __new__(cls):
        print("Creating a new instance")
        instance = super(MyClass, cls).__new__(cls)
        return instance

    def __init__(self):
        print("Initializing the instance")

obj = MyClass()

__new__ 方法在对象创建之前被调用,而__init__ 方法在对象创建后被调用。__new__ 方法负责创建对象,而__init__ 方法负责初始化对象。

21、外键、left_join、游标?

  1. 外键(Foreign Key):
    • 外键是数据库表中的一列或多列,用于建立与另一个表的关联。外键在一个表中引用了另一个表的主键,从而创建了表之间的关联关系。
    • 外键的作用是确保数据的完整性,它可以防止在引用表中插入无效的数据。如果在包含外键的表中插入数据,该数据必须存在于被引用表的主键列中,否则将会触发外键约束错误。
  2. LEFT JOIN:
    • LEFT JOIN 是一种 SQL 查询操作,用于从两个或多个表中检索数据,同时返回左表中的所有行,以及与右表中匹配的行。
    • 如果左表中的某一行在右表中没有匹配项,仍然会返回该行,但是右表中的列将包含 NULL 值。
    • LEFT JOIN 通常用于获取包括匹配和非匹配数据的结果集,以便进行数据分析和关联查询。
  3. 游标(Cursor):
    • 游标是数据库中一种用于在结果集中导航和检索数据的机制。它通常用于编程中,允许程序对数据库中的查询结果进行逐行处理。
    • 游标可以被用于检索查询结果中的一行或多行,进行数据的读取、更新、插入或删除等操作。
    • 在编程中,使用游标可以逐行处理大型查询结果,从而减少内存开销。

22、如果一堆数据中,很多数据都局部有序,选择哪个排序方式更好?

通常情况下,使用**归并排序(Merge Sort)**可能是更好的选择。

归并排序的一个主要优点是它在合并局部有序的数据时表现良好,因为归并排序的核心思想是合并两个已排序的子数组,而这些局部有序的子数组正是归并排序的强项。

与归并排序不同,快速排序(Quick Sort)在处理局部有序的数据时性能可能不如归并排序。快速排序的性能高度依赖于划分的质量,如果数据已经大致有序,那么快速排序可能需要多次划分才能达到平均情况下的性能。

22、二叉树层序遍历具体实现过程?

描述:给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

思路:

  1. 创建一个队列 queue 用于存储将要访问的节点。
  2. 将根节点 root 入队。
  3. 进入循环,直到队列为空。
    • 在每次循环中,先计算队列的当前大小 levelSize,这表示当前层的节点数。
    • 创建一个空列表 currentLevel,用于存储当前层的节点值。
    • 从队列中弹出 levelSize 个节点,并将它们的值加入 currentLevel
    • 将这些节点的非空子节点入队,继续下一层的遍历。
    • currentLevel 加入结果列表 result
  4. 返回 result,其中包含了按层次顺序遍历的节点值。

示例代码:

C++
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

// Definition for a binary tree node.
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> result;
    if (!root) {
        return result; // 如果根节点为空,返回空的结果
    }

    queue<TreeNode*> queue;
    queue.push(root);

    while (!queue.empty()) {
        int levelSize = queue.size();
        vector<int> currentLevel;

        for (int i = 0; i < levelSize; i++) {
            TreeNode* node = queue.front();
            queue.pop();
            currentLevel.push_back(node->val);

            if (node->left) {
                queue.push(node->left);
            }
            if (node->right) {
                queue.push(node->right);
            }
        }

        result.push_back(currentLevel);
    }

    return result;
}

int main() {
    // 创建一个二叉树示例
    TreeNode* root = new TreeNode(3);
    root->left = new TreeNode(9);
    root->right = new TreeNode(20);
    root->right->left = new TreeNode(15);
    root->right->right = new TreeNode(7);

    vector<vector<int>> result = levelOrder(root);

    // 打印层序遍历结果
    for (const vector<int>& level : result) {
        for (int val : level) {
            cout << val << " ";
        }
        cout << endl;
    }

    return 0;
}

23、接触过哪些设计模式,C++中如何实现单例模式?

懒汉模式

懒汉模式是在首次请求单例实例时才进行初始化,以延迟对象的创建。这种方式在需要时才创建单例,节省了资源,但需要考虑线程安全。

C++
class LazySingleton {
public:
    // 获取单例实例的静态成员函数
    static LazySingleton& getInstance() {
        // 使用静态局部变量来延迟初始化
        static LazySingleton instance;
        return instance;
    }

    // 其他成员函数和数据成员
private:
    LazySingleton() {
        // 构造函数的实现
    }

    // 禁用拷贝构造函数和赋值操作符
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;
};

懒汉模式的优点是在需要时才创建单例,但需要注意多线程环境下的线程安全问题。

饿汉模式

饿汉模式是在类加载时就创建单例实例,无论是否需要。这种方式在程序启动时就创建单例,无需考虑线程安全问题。

C++
class EagerSingleton {
public:
    // 获取单例实例的静态成员函数
    static EagerSingleton& getInstance() {
        return instance;
    }

    // 其他成员函数和数据成员
private:
    EagerSingleton() {
        // 构造函数的实现
    }

    // 禁用拷贝构造函数和赋值操作符
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;

    // 在类加载时创建单例对象
    static EagerSingleton instance;
};

// 在类外部初始化静态成员变量
EagerSingleton EagerSingleton::instance;

饿汉模式的优点是不需要考虑线程安全问题,但缺点是在程序启动时创建单例可能占用资源,甚至在不需要单例时浪费了资源。