C++开发面经

C++开发面经

基础语法:

进程,线程,协程的区别
  1. 进程(Process):

    • 进程是操作系统进行资源分配的基本单位,每个进程都有独立的内存空间和系统资源,包括代码、数据、打开的文件、网络连接等。

    • 进程之间相互独立,通过进程间通信(IPC,Inter-Process Communication)进行数据交换。

    • 创建和销毁进程都需要操作系统的调度和管理,进程切换的开销比较大。

  2. 线程(Thread):

    • 线程是进程的一个执行流,一个进程可以包含多个线程,它们共享进程的内存空间和资源。

    • 线程之间可以直接访问同一进程中的数据,因此线程之间的通信更加方便快捷。

    • 线程的创建、销毁和切换比进程更加轻量级,因为它们共享相同的地址空间。

  3. 协程(Coroutine):

    • 协程是一种用户态的轻量级线程,它由用户程序来控制,而不是由操作系统来调度。

    • 协程的调度是协作式的,它由程序员显式地控制调度时机,可以在任意时刻进行挂起和恢复。

    • 协程通常运行在单个线程中,因此不涉及线程切换的开销,但也意味着无法利用多核处理器的优势。

右值和右值的作用

右值(rvalue)和左值(lvalue)是C++中用于表示表达式的值分类的两种概念。右值和左值的主要区别在于其可以出现的位置和用途。

右值(rvalue):
  1. 出现位置: 右值是指在表达式中仅出现在赋值号(=)右边的值,或者说不能被取地址的临时对象。

  2. 临时对象: 右值可以是临时对象(Temporary objects),例如函数返回的临时对象、字面量等。

  3. 移动语义: 右值在C++11中引入了移动语义(Move semantics),可以通过移动而不是复制来处理临时对象,提高效率。

右值的作用:
  1. 移动语义: 右值的引入使得C++可以进行资源管理的优化,例如移动语义可以避免不必要的对象复制,提高性能。

  2. 完美转发: 右值引用(Rvalue reference)允许程序员编写更加灵活的函数模板,实现完美转发(perfect forwarding),可以在不产生额外的复制和移动操作的情况下将参数传递给其他函数。

右值的引入使得C++在资源管理和性能优化方面更加灵活和高效,通过适当地利用右值和右值引用,可以编写出更加高效和可维护的代码。

如果没有右值引用,怎么延长右值的生存期
在没有右值引用的情况下,要延长右值的生存期通常会使用拷贝构造函数或移动构造函数。这意味着将右值复制到一个持久性的对象中,以延长其生存期。具体方式取决于对象是否可移动和是否定义了相应的移动构造函数。
使用拷贝构造函数:

如果对象不可移动,可以使用拷贝构造函数将右值复制到一个新的对象中。这样做会在内存中创建一个新的对象,原始右值和新对象都有持久的生命周期。

使用移动构造函数:

如果对象可移动,可以使用移动构造函数将右值的资源转移给新对象,而不进行拷贝操作。这样做可以避免不必要的对象复制,提高性能。

vector和list的区别

std::vectorstd::list 都是C++标准库提供的容器,它们在内部实现和性能特征上有一些显著的区别,适用于不同的使用场景。

std::vector:
  1. 基于数组实现: std::vector 内部使用动态数组来存储元素,因此元素在内存中是连续存储的,可以通过索引进行快速访问。

  2. 随机访问: 由于元素连续存储,因此支持高效的随机访问,时间复杂度为 O(1)。

  3. 动态扩容: 当元素数量超出当前容量时,std::vector 会重新分配更大的内存空间,并将原有元素拷贝到新的内存空间中,这可能会导致重新分配的开销。

  4. 插入和删除操作: 在中间位置插入或删除元素时,需要将后续元素进行移动,因此时间复杂度为 O(n)。

std::list:
  1. 基于双向链表实现: std::list 内部使用双向链表来存储元素,因此元素在内存中不是连续存储的,而是通过指针相互连接。

  2. 顺序访问: 由于不支持随机访问,std::list 只能通过迭代器进行顺序访问,时间复杂度为 O(n)。

  3. 动态插入和删除: 在链表中插入或删除元素的时间复杂度为 O(1),因为只需要调整相邻节点的指针即可,不需要移动其他元素。

  4. 空间开销: 由于链表节点需要额外的指针来维护连接关系,因此 std::list 的空间开销通常比 std::vector 大。

选择使用场景:
  • 如果需要频繁地进行随机访问,或者需要高效的尾部插入和删除操作,推荐使用 std::vector

  • 如果需要频繁地进行插入和删除操作,且不关心随机访问性能,推荐使用 std::list

  • 在一些特殊情况下,也可以考虑使用 std::deque,它既提供了连续存储的特性,又提供了类似于 std::list 的动态插入和删除操作。

如果vector要加入的内容很多应该怎么做

如果 std::vector 预期要添加大量的内容,可以考虑以下几种优化策略:

  1. 预分配空间: 在添加大量元素之前,使用 reserve() 函数预分配足够的内存空间,以减少动态扩容的次数。这样可以避免重新分配和拷贝元素的开销。

    cppCopy codestd::vector<int> myVector;
    myVector.reserve(10000); // 预分配足够的内存空间
  2. 使用移动语义: 如果添加的内容是临时对象或者可以被移动的对象,可以考虑使用移动语义来避免不必要的复制操作,提高性能。

    cppCopy codestd::vector<std::string> myVector;
    std::string tempString = "example";
    // 使用移动语义将临时对象移动到 vector 中
    myVector.push_back(std::move(tempString)); 
  3. 批量添加元素: 如果要添加的内容是一批数据,可以考虑使用 insert() 函数一次性添加多个元素,而不是逐个添加。

    cppCopy codestd::vector<int> myVector;
    std::vector<int> newData = {1, 2, 3, 4, 5};
    // 一次性添加多个元素
    myVector.insert(myVector.end(), newData.begin(), newData.end()); 
  4. 分批添加: 如果要添加的内容非常大,可以考虑分批添加,每次添加一部分数据,以避免一次性分配过多内存导致内存不足。

    cppCopy codestd::vector<int> myVector;
    const int batchSize = 1000;
    for (int i = 0; i < totalSize; i += batchSize) {
        // 每次添加 batchSize 个元素
        myVector.insert(myVector.end(), data.begin() + i, data.begin() + std::min(i + batchSize, totalSize));
    }

通过以上优化策略,可以在 std::vector 添加大量内容时提高性能和效率,并减少不必要的资源浪费。

push_back和emplace_back的区别

push_back()emplace_back() 都是 std::vector 的成员函数,用于在向向量尾部添加元素。它们的主要区别在于添加元素的方式和性能特征。

push_back():
  • push_back() 用于将给定元素添加到向量的末尾。

  • 当使用 push_back() 添加元素时,需要创建一个临时对象,并通过复制或移动该临时对象将其添加到向量中。

  • 如果添加的元素类型没有移动构造函数,push_back() 将执行元素的拷贝构造函数来创建临时对象,并通过拷贝将其添加到向量中。

  • 如果添加的元素类型有移动构造函数,push_back() 将尝试通过移动语义来将临时对象添加到向量中,以减少复制操作。

cppCopy codestd::vector<int> myVector;
int value = 10;
myVector.push_back(value); // 添加 value 的拷贝到向量中
emplace_back():
  • emplace_back() 允许在向量尾部直接构造元素,而不需要创建临时对象。

  • 当使用 emplace_back() 添加元素时,可以直接将参数传递给元素的构造函数,以在向量中就地构造新元素。

  • emplace_back() 可以避免临时对象的创建和复制操作,因此通常比 push_back() 更加高效。

cppCopy codestd::vector<std::string> myVector;
myVector.emplace_back("example"); // 在向量尾部直接构造一个新的 std::string 元素
总结:
  • 如果需要添加一个已经存在的对象,或者只能通过复制或移动现有对象来添加元素,应该使用 push_back()

  • 如果可以直接在向量中构造新元素,并且希望避免创建临时对象和复制操作,应该使用 emplace_back()

volatile 关键字的作用

volatile 是C和C++中的一个关键字,用于告诉编译器对于标记的变量或对象不能进行优化,因为它们的值可能在程序执行期间被外部因素更改。volatile 的作用包括以下几个方面:

  1. 禁止编译器优化: 编译器在编译过程中会对变量进行优化,例如对变量的读取操作可能会被优化为只读取一次。但是对于被 volatile 关键字修饰的变量,编译器会禁止这种优化,确保每次访问都会从内存中读取变量的值。

  2. 与多线程相关: 在多线程编程中,volatile 可以用于告诉编译器某个变量是在多个线程之间共享的,因此不应该进行优化。虽然 volatile 不能保证原子性或顺序性,但它可以确保每次访问都会从内存中读取最新的值。

  3. 与中断处理相关: 在嵌入式系统和驱动程序开发中,volatile 可以用于告诉编译器某个变量可能被中断处理程序修改,因此不能进行优化。这样可以确保在中断发生时,相关变量的值能够及时更新。

  4. 与内存映射相关: 在一些嵌入式系统中,volatile 可以用于告诉编译器某个变量是通过内存映射方式访问的外部设备寄存器,因此不能进行优化。这样可以确保对这些寄存器的读写操作能够被及时执行。

总的来说,volatile 关键字用于告诉编译器对于标记的变量或对象不能进行优化,因为它们的值可能会在程序执行期间被外部因素更改。在多线程、中断处理、内存映射等场景中,volatile 可以确保程序的正确性和可靠性。但需要注意的是,volatile 不能保证原子性,也不能代替互斥锁或其他同步机制。

redis

AOF持久化和重写,AOF的同步方式

AOF(Append-Only File)持久化和重写是Redis用于持久化数据的两种主要方式之一。AOF持久化通过将Redis的写命令追加到一个文件中来记录数据变化,这个文件可以通过重放命令来恢复数据。

AOF持久化
  1. 记录写命令: Redis将每个写命令(例如SET、INCR等)追加到AOF文件末尾,以记录数据的变化。

  2. 数据重放: 当Redis重新启动时,可以通过重新执行AOF文件中的写命令来恢复数据,以将数据状态恢复到最后一次保存时的状态。

  3. AOF重写: 为了减小AOF文件的体积和避免文件过大导致恢复速度慢,Redis会周期性地执行AOF重写操作。AOF重写是通过遍历内存中的数据来生成一个新的AOF文件,新文件中的命令会代替旧文件中的一部分命令,从而减小AOF文件的大小。

AOF的同步方式

在AOF持久化过程中,Redis支持多种同步方式来确保数据的安全性和持久性,包括:

  1. 总是同步(always): 每次写命令都会立即同步到磁盘,这种方式可以保证数据的完整性,但会降低性能。

  2. 每秒同步(everysec): Redis每秒钟会将写命令同步到磁盘一次,这种方式在一定程度上保证了数据的持久性,并且性能相对较好。

  3. 不同步(no): Redis不主动同步数据到磁盘,而是依赖操作系统的缓存机制来处理数据的持久化。这种方式性能最好,但是在发生意外宕机时可能会导致数据丢失。

  4. 自动同步(always、everysec): Redis可以根据配置自动选择总是同步或每秒同步的方式,具体取决于数据安全和性能之间的平衡。

Redis为什么要用单线程,单线程+IO多路复用和多线程的区别

Redis选择单线程模型的主要原因是为了避免多线程下的复杂性和线程安全问题,以及提高CPU的利用率。单线程模型结合了非阻塞IO和IO多路复用技术,可以达到很高的性能。

单线程模型:
  1. 简单性: 单线程模型避免了多线程带来的复杂性,减少了线程之间的竞争和同步开销,使得代码更加简单清晰。

  2. 线程安全: 由于Redis采用单线程模型,无需考虑线程安全问题,避免了因为多线程导致的死锁、竞争条件等问题。

  3. 事件循环: Redis采用事件循环机制,通过非阻塞IO和IO多路复用技术,可以在单线程下同时处理多个客户端连接的请求,实现高并发。

  4. 高性能: 单线程模型虽然只有一个线程,但通过事件循环和非阻塞IO技术,可以充分利用CPU资源,实现高性能的数据处理。

多线程模型:
  1. 并行处理: 多线程模型可以利用多核CPU的优势,实现并行处理,可以提高系统的吞吐量和响应速度。

  2. 复杂性: 多线程模型中需要考虑线程间的同步和通信问题,包括锁机制、条件变量等,增加了代码的复杂性和开发的难度。

  3. 线程安全: 多线程模型需要保证数据的线程安全,因此需要考虑锁的粒度和锁的性能,以及可能出现的死锁、饥饿等问题。

  4. 资源消耗: 多线程模型中每个线程都需要一定的内存和CPU资源,当线程数量增多时,会增加系统的资源消耗和管理开销。

总的来说,单线程模型适用于高并发、IO密集型的场景,可以实现简单、高性能的数据处理;而多线程模型适用于CPU密集型的场景,可以实现并行处理,提高系统的吞吐量。选择哪种模型取决于具体的应用场景和性能需求。

计网部分:

域名输入到浏览器后发生的过程
  1. URL解析: 浏览器首先解析你输入的 URL(Uniform Resource Locator),提取出其中的域名部分。

  2. DNS解析: 浏览器向本地 DNS(Domain Name System)服务器发送一个 DNS 查询请求,以解析域名对应的 IP 地址。本地 DNS 服务器通常会缓存之前的 DNS 查询结果,以加快解析速度。如果本地 DNS 服务器缓存中没有找到对应的 IP 地址,则会向根域名服务器发送请求,根域名服务器会返回负责该顶级域名(例如 .com, .org, .net 等)的权威 DNS 服务器的地址。然后本地 DNS 服务器再向权威 DNS 服务器发送请求,获取域名对应的 IP 地址。这个过程可能会经过多次迭代查询。

  3. 建立TCP连接: 一旦浏览器获取到了目标域名对应的 IP 地址,它会通过 TCP(Transmission Control Protocol)协议与目标服务器建立连接。TCP是一种可靠的传输协议,用于在网络上建立可靠的连接。

  4. 发送HTTP请求: 一旦TCP连接建立成功,浏览器会向目标服务器发送一个 HTTP(HyperText Transfer Protocol)请求,请求对应域名下的具体资源,比如网页的HTML文件、图片、CSS文件等。

  5. 服务器响应: 服务器收到HTTP请求后,会处理请求,并将相应的资源通过HTTP协议返回给浏览器。

  6. 接收并渲染页面: 浏览器接收到服务器返回的资源后,会解析HTML和其他资源,构建DOM树(Document Object Model)并渲染页面,最终将页面呈现给用户。

  7. 关闭连接: 当浏览器完成页面加载后,它会关闭与服务器的连接。这样,整个页面加载过程就结束了。

常见http状态码

HTTP(Hypertext Transfer Protocol)状态码是服务器对客户端请求的响应状态的一种标识,它告诉客户端发生了什么情况。以下是一些常见的HTTP状态码及其含义:

  1. 1xx(信息性状态码):指示请求已被接收,继续处理。

    • 100 Continue:服务器已收到请求头,并且客户端应继续发送请求的主体部分。

    • 101 Switching Protocols:服务器已经理解了客户端的请求,但是需要切换协议以完成处理。

  2. 2xx(成功状态码):表示请求被成功接收、理解、接受或处理。

    • 200 OK:请求已成功。一般用于GET和POST请求。

    • 201 Created:请求已经被实现,并且一个新的资源已经被创建。

    • 204 No Content:服务器成功处理了请求,但未返回任何内容。

  3. 3xx(重定向状态码):表示客户端需要执行某些额外的操作才能完成请求。

    • 301 Moved Permanently:请求的资源已被永久移动到新位置。

    • 302 Found:请求的资源临时移动到新的位置。

    • 304 Not Modified:客户端的缓存是最新的,不需要重新传输。

  4. 4xx(客户端错误状态码):表示客户端请求存在错误,服务器无法处理。

    • 400 Bad Request:请求无效,服务器无法理解。

    • 401 Unauthorized:请求需要用户身份验证。

    • 404 Not Found:请求的资源不存在。

  5. 5xx(服务器错误状态码):表示服务器在尝试处理请求时发生错误。

    • 500 Internal Server Error:服务器遇到了一个未曾预料的状况,导致无法完成请求。

    • 503 Service Unavailable:服务器当前无法处理请求,一般用于临时维护或过载情况。

以上是一些常见的HTTP状态码及其含义,HTTP状态码有很多种,每种状态码都有特定的含义,客户端可以根据状态码来了解请求的处理情况。

osi模型

OSI(Open Systems Interconnection)模型是国际标准化组织(ISO)定义的一种网络通信参考模型,它将计算机网络通信划分为七个不同的层次,每个层次都有特定的功能和责任。以下是对每个层次的简要描述:

  1. 物理层(Physical Layer):

    • 物理层是最底层的层次,它负责传输原始比特流,处理数据的物理传输介质和信号特性,如电压、电流、光信号等。

  2. 数据链路层(Data Link Layer):

    • 数据链路层负责将原始比特流转换为逻辑帧,并在相邻节点之间传输数据帧,以便进行数据传输的可靠性和错误检测。

  3. 网络层(Network Layer):

    • 网络层负责在不同网络之间进行数据包的路由和转发,实现数据的网络互联和路径选择。

  4. 传输层(Transport Layer):

    • 传输层负责在网络之间建立端到端的通信连接,并提供数据传输的可靠性和流量控制,通常包括TCP和UDP两种协议。

  5. 会话层(Session Layer):

    • 会话层负责建立、管理和终止会话连接,实现数据传输的起始点和终点之间的逻辑连接。

  6. 表示层(Presentation Layer):

    • 表示层负责数据的格式化、编码和解码,以确保不同计算机系统之间的数据交换和解释的一致性。

  7. 应用层(Application Layer):

    • 应用层是最高层的层次,负责为用户提供网络服务和应用程序,如电子邮件、文件传输、远程登录等。

OSI模型将网络通信划分为七个层次,每个层次都有特定的功能和责任,层与层之间通过接口进行通信和协作,实现了网络通信的标准化和分层设计。

操作系统:

操作系统进程切换过程

操作系统中进程切换是指从一个进程的执行上下文(包括寄存器状态、内存映射、打开的文件等)切换到另一个进程的执行上下文的过程。进程切换通常发生在操作系统的调度器中,以便让多个进程共享CPU资源。

下面是典型的操作系统进程切换过程:

  1. 保存当前进程上下文: 当操作系统决定要切换到另一个进程时,首先会保存当前进程的执行上下文。这包括将当前进程的寄存器状态保存到该进程的控制块(PCB,Process Control Block)中,保存当前进程的栈指针、程序计数器、以及其他相关状态。

  2. 选择下一个要执行的进程: 在调度器中,操作系统会根据特定的调度算法选择下一个要执行的进程。常见的调度算法包括先来先服务(FCFS,First-Come-First-Served)、轮转调度(Round Robin)、优先级调度等。

  3. 恢复下一个进程的上下文: 一旦选择了下一个要执行的进程,操作系统会从该进程的控制块中恢复其执行上下文。这包括将寄存器状态加载到CPU寄存器中,恢复栈指针、程序计数器等。

  4. 执行新进程: 一旦新进程的上下文被恢复,CPU开始执行该进程的指令。进程可以是从头开始执行,也可以是从上次被中断的地方继续执行。

在多任务操作系统中,这个进程切换过程会持续发生,以便实现多个进程之间的时间共享。优化进程切换过程可以提高系统的性能和响应速度,常见的优化包括减小进程切换的开销、采用更高效的调度算法、以及减少上下文切换的次数等。

用户态和内核态的区别
什么时候在用户态,什么时候在内核态,用户态什么时候切换到内核态,怎么返回用户态
用户态(User Mode):
  1. 权限限制: 在用户态下,程序只能访问受限的系统资源,不能直接操作底层硬件设备或关键系统资源。

  2. 操作自身进程空间: 程序运行在用户态时,可以操作自身的进程空间,包括读写自己的内存空间。

  3. 异常处理: 用户态下的程序只能处理常规的异常,例如访问非法内存、除零等。无法直接处理涉及系统资源的异常。

内核态(Kernel Mode):
  1. 完全控制系统资源: 在内核态下,操作系统具有对系统资源的完全控制权,可以直接访问和操作底层硬件设备和所有系统资源。

  2. 特权指令: 内核态下的程序可以执行特权指令,如访问特殊的机器指令、修改控制寄存器等。

  3. 处理所有异常: 内核态下的程序可以处理所有的异常,包括涉及系统资源的异常。

切换时机:
  1. 用户态切换到内核态: 当用户程序需要访问受限资源或执行特权操作时,会触发系统调用(Syscall)或发生异常(如缺页异常、设备中断等),此时会从用户态切换到内核态。另外,中断也会导致用户态切换到内核态。

  2. 内核态返回用户态: 当内核态执行完相应的系统调用或异常处理后,会将控制返回给用户态程序,从而完成内核态返回用户态的过程。

返回用户态:

内核态返回用户态的方式通常包括以下几个步骤:

  1. 恢复用户态上下文: 内核在执行完相应的系统调用或异常处理后,会将用户态的程序状态(如寄存器内容、栈指针等)从内核栈中恢复回来。

  2. 切换到用户态的代码段: 内核会将程序计数器设置为用户态代码的地址,以便在返回时执行用户态程序的代码。

  3. 切换堆栈: 如果内核使用了自己的栈空间来执行系统调用或异常处理,它会将栈指针切换回用户态程序的栈,以便程序可以继续执行。

数据库

MySQL、LevelDB 和 MongoDB 是三种常见的数据库,它们各有优点和缺点,适用于不同的场景和需求。

MySQL:
优点:
  1. 成熟稳定: MySQL 是一种成熟的关系型数据库管理系统(RDBMS),经过长时间的发展和优化,稳定性较高。

  2. 支持SQL: MySQL 支持标准的 SQL 查询语言,具有强大的查询和分析功能。

  3. 广泛应用: MySQL 在Web应用开发中被广泛应用,有大量的开发者和社区支持,拥有丰富的生态系统和资源。

  4. 事务支持: MySQL 支持事务处理,具有较好的事务一致性和可靠性。

缺点:
  1. 性能限制: MySQL 在大规模数据处理和高并发访问时性能可能受限,需要进行优化和调整。

  2. 扩展性差: MySQL 的扩展性较差,难以在大规模集群环境中实现高性能和高可用性。

  3. 数据模型固定: MySQL 是关系型数据库,数据模型相对固定,难以适应半结构化数据或非结构化数据的存储需求。

LevelDB:
优点:
  1. 高性能: LevelDB 是一种高性能的键值存储引擎,适用于大规模数据的读写操作。

  2. 内存效率高: LevelDB 使用内存映射技术来管理数据,具有较高的内存利用率和效率。

  3. 轻量级: LevelDB 是一个轻量级的数据库引擎,代码简洁清晰,易于集成和部署。

缺点:
  1. 单进程: LevelDB 是一个单进程的数据库引擎,无法充分利用多核处理器的优势。

  2. 数据模型简单: LevelDB 的数据模型相对简单,不支持复杂的查询和分析功能,适用于简单的键值存储场景。

  3. 可靠性差: LevelDB 在面对硬件故障或异常情况时,可靠性较差,容易导致数据丢失或损坏。

MongoDB:
优点:
  1. 灵活性高: MongoDB 是一种文档型数据库,支持灵活的数据模型和半结构化数据存储,适用于多样化的数据需求。

  2. 高性能: MongoDB 使用内存映射技术和索引优化等手段来提高数据访问性能,适用于高并发、大规模数据的存储和查询。

  3. 可扩展性好: MongoDB 支持分布式部署和数据分片技术,具有良好的可扩展性,能够满足不同规模的数据存储需求。

  4. 支持丰富的查询语言: MongoDB 支持丰富的查询语言和聚合操作,能够进行复杂的数据分析和查询。

缺点:
  1. 内存消耗大: MongoDB 在大规模数据存储和高并发访问时消耗大量内存,需要较高的硬件配置。

  2. 复制和故障恢复复杂: MongoDB 的复制和故障恢复机制相对复杂,需要进行配置和管理。

  3. 性能不稳定: MongoDB 在一些场景下性能不稳定,容易受到硬件、网络等因素的影响,需要进行优化和调整。

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值