C++ | 对比inline内联函数和宏的不同点

本文详细探讨了宏的优缺点,强调了宏可能导致的运算符优先级问题和调试困难。接着,介绍了C++的内联函数,作为宏的一种改进,保留了宏的优点,如无栈帧开销,同时修复了宏的缺点,提供更安全和调试友好的方式。内联函数通过在调用处展开代码来提高效率,但也可能导致程序体积增大。文章还讨论了内联函数的实现机制和使用注意事项,包括inline的声明与定义应保持一致。最后,提供了一道笔试题来检验读者的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

在这里插入图片描述

一、前言

本文我们聊一聊一下宏和内联函数之间的关系。

  • 预处理章节,我们重点介绍了什么是宏,以及宏和函数之间的区别,还记得下面这张图吗,我们对宏和函数之间做了一个很细致的分析🔍
  • 那在本文中,我们来说的就不是普通的函数了,它叫做【内联函数】,是C++里面独有的,由inline关键字进行修饰,具体怎样,敬请看下去吧👇

在这里插入图片描述

二、宏的优缺点分析

在将内联函数之间前,我们再来聊聊有关宏的一些内容

1、概念回顾

下面是宏的申明方式:

#define name( parament-list ) stuff
//其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中

注:① 参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

  • 在学习了C语言后,如果我不带你去回顾这一些,那你是否能立马写出一个宏来呢,例如:写一个用于求和的宏函数
  • 那我相信对于很多同学来说都会措手不及,答案会像是下面这样千奇百怪🤣
//1.
#define Add(int x, int y)	return x + y;
//2.
#define ADD(x, y) x + y
//3.
#define ADD(x, y) (x + y)
//4.
#define ADD(x, y) ((x) + (y));
//5.
#define ADD(x, y) ((x) + (y))
  • 那究竟哪一个对的呢?首先第一个肯定是错误的,因为我说了这是【宏】,而不是函数,这样子是函数的写法。

后面四个的主要区别就在于后面的stuff,我们一一来分析一下

  • 首先若是写成下面这样,传入1给到x,传入2给到y,此时去我们去Linux环境下看看预处理之后会发生什么
int ret = ADD(1, 2);
  • 可以看到很明显进行了一个【宏替换】

在这里插入图片描述

  • 若此时我将调用的函数后面再乘上一个3呢,这会发生什么?
int ret = ADD(1, 2) * 3;
  • 可以观察到此时在预处理阶段也是直接进行了一个替换,不过仔细观察就可以发现,由于*的优先级来得高,所以2会和后面的3先进行一个运算,这也就造成了最后结果的不同

在这里插入图片描述


所以我们应该要像下面这样,在外层加上一个大括号,防止出现优先级的问题

#define ADD(x, y) (x + y)
  • 可是这样真的就可以了吗?若此时我向ADD宏函数传入下面这样的参数呢?会发生什么?
int a = 10;  
int b = 20;
int ret = ADD(a | b, a & b);
  • 编译器还是一样会去做傻傻的替换,但是这个时候我们又得注意优先级的问题了,对于+号来说,它的优先级高于&按位与和|按位或的,如果不清楚优先的话可以看看操作符汇总大全
  • 所以中间的【b】和【a】会先进行结合,然后再去算&|

在这里插入图片描述

  • 如果要防止这种表达式的传入而造成的优先级问题,可以对内部的形参也加上一个括号(),这样就不会出现问题了

#define ADD(x, y) ((x) + (y));
  • 可是呢,有的同学虽然想到了这一点,但是却在最后面加了一个;号,对于分号来说是我们在写代码的时候作为一条语句结束的标志,但是对于宏来说也可以写分号吗?
  • 将原先的传参继续替换成下面这样来试试
int ret = ADD(1, 2) * 3;
  • 继续通过预处理之后的结果去进行观察,就可以发现在进行宏替换之后原先的语句中出现了;号,但是分号后面还有3要乘,这个时候其实就不对了,所以宏定义后是不可以加分号的

在这里插入图片描述

  • 最后这一种才是最正确的写法
#define ADD(x, y) ((x) + (y))		//✔

2、宏的缺点

看了上面有关【宏】概念的一些回顾,我们来聊聊它的缺点所在

① 宏可能会带来运算符优先级的问题,导致程容易出现错。【加括号太麻烦了!!!】

  • 这一点相信你在看了上一小节的叙述之后一定是感同身受,只是完成一个两数相加的功能,就需要加上这么多括号了,若是再复杂一些的功能,那岂不是要加很多了🙄

② 宏是不能调试的【这点比较致命👈】

  • 可以观察到,无论是在哪个平台下,对于宏来说都是无法调试的

Windows环境下VS

在这里插入图片描述

Linux环境下gdb

在这里插入图片描述

③ 没有类型安全的检查【直接替换】

  • 可以看到,无论我传入何种类型的参数,都不会出现问题。这点其实说明了宏对于类型的检查是不严谨的

在这里插入图片描述

④ 有些场景下非常复杂,容易出错,不容易掌握

#define SWAP(n) num = (((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1))
  • 乍眼一看,就感觉非常复杂,都是一些二进制的位运算,若是没有对这一块很熟悉的话,就很容易写错,因此说对于一个宏来说其实要实现一些功能的时候会非常繁琐

3、宏的优点

了解了宏的缺点之后,我们再来瞧瞧它的优点,既然在一些场景被广泛使用,那一定也具有它的优点💡

宏常量提高复用性和可维护性

  • 这一点很好理解,也就是我们平常用得最多的,一般会将频繁使用到的一些数字定义成一个宏,这样就不需要每次写数字了,直接写这个宏所定义的变量即可
  • 而且在修改的时候也会很方便,只需要修改一下宏定义处即可,实现一改多改【这一点在项目里面还是比较常用的
#define n 500

宏函数类型不固定,int、double、float都可以用

  • 这一点其实我们在讲缺点的时候也有提及,虽然类型检查得不严谨,但真正用起来还是很香的,这很像是我们之前说到过的函数重载,虽然函数名相同,但是可以传入不同的参数实现不同的运算

宏函数不用建立栈帧,因此不会产生消耗

  • 刚才我们有观察过,对于【宏】来说是不能调试的,因为它根本就不是一个真正意义上的函数,只是具备函数的功能罢了。既然不是函数的话也就不会在栈上建立函数栈帧,那也就不会有过多的消耗

好,以上就是本文所要讲的有关【宏】相关的所有内容,希望能唤起读者对这个知识点的回忆

三、inline内联函数

接下去我们就正式来讲讲C++中的独有的【内联函数】

1、概念

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率

  • C++的祖师爷呢,认为【宏】存在一定的缺陷,但是呢也有它的好处,也就是直接做替换,不需要开辟函数栈帧,所以在C++中就出现了内敛函数这么一个东西
inline int Add(int x, int y)
{
	int z = x + y;
	return z;
}
  • 但是它真不会去进行函数调用吗?我们到VS中来看看

在这里插入图片描述

  • 那此时就又同学疑惑了🤨这不还是会有call指令吗,哪来的不调用一说呢?
  • 因为我们还需要去做一些配置💻

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 然后我们再去观察一下汇编就可以发现不存在call指令了,编译器直接将内敛函数Add做了展开

在这里插入图片描述

  • 所以C++中我们是不推荐用宏的,因为有内联函数这个特性,即保留了宏的优点,无需调用函数建立栈帧,而且还修复了宏的缺陷,不再需要将内容写得那么复杂,写成日常的函数形式即可,只需要在前面加上一个inline关键字,就可以起到这种效果。非但如此,它还可以调试💻

接下去我们再来介绍一下有关内敛函数的一些特性

2、特性①:空间换时间

有同学可能不是很理解空间换时间是什么意思,可以看看时空复杂度章节。不过要说明的一点是这里的空间不是内存,是编译后的程序,而【空间换时间】就会使得编译出来的可执行程序会变大

  • 对于这种空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用
    • 【缺陷】:可能会使目标文件变大;
    • 【优势】:少了调用开销,提高程序运行效率;
  • 可以通过下面这幅图来看看:此时程序中有一个swap()函数,内部有10行代码。在程序的1000个地方都会对其进行调用,那请问此时去使用内联函数和不使用内联函数的区别是什么?
    • 当不使用内联函数时,就是将这个swap()函数当做普通的函数的话,我们知道在底层会进行一个call指令再通过jump指令跳转到这个函数所在地址然后执行这个函数,那每一个调用的地方都要跳转的话,就会跳转1000次,所以swap + 调用swap指令,合计是1000 + 10条指令
    • 当使用内联函数时,通过上面的学习可以知道,它不会去进行一个函数的调用,而是直接将相关的指令拷贝到程序中调用这个swap()函数的每一块地方,所以swap + 调用swap指令,合计是1000 * 10条指令

在这里插入图片描述

  • 可以看出,在使用内联函数减少函数调用的同时也会增加程序的负担,使得目标文件会变得很大

🎁趣味杂谈:庞大的游戏更新包

在理解了一个函数设不设置内联函数的区别的时候,顺带来谈一谈这个目标文件体积变大的缘故

  • 要知道,干我们这一行基本一年到头都是在公司,也有逢年过节才能回家一趟,而且平常工作忙得也没有时间玩游戏,那此刻当你过年回家的时候,就看到家里的一些弟弟妹妹拿着手机📱在打《王者荣耀》,这相信是最正常不过了
  • 那此时呢,你也心里痒痒,打开了你尘封已久的王者荣耀,然后看到要更新,而且一看就是4、5个G,突然之间就不想玩了😅更新的过程中只能眼巴巴地看着别人玩,
  • 不过呢,你有个同事,也是很久都没有玩过游戏了,也是一样进去更新,但是呢她却3、5分钟更新好了,因为更新包只有3、500M。这是为什么呢?原来她是苹果手机,系统是IOS系统,其实对于更新的这些内容都是指令,只是你们的指令数目不同而已。那我们都知道【苹果】这家公司系统是自己设计的,而且对于像芯片、硬件、软件各方面都领先于同行
  • 所以说这个性能不一样就会手机系统上使用的软件也存在差异。别人这个性能好,在优化各种方面都齐全,所以就不会导致这个更新包越变越大,相反对比我们平常使用的几千元的安卓手机,却越用越卡,这也是很多人选择使用苹果手机的原因👈

在这里插入图片描述

3、特性②:inline实现机制

好,小插曲,我们回归正题,继续来讲讲内联函数的第二大特性 —— 【inline实现机制】

  • inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同
  • 一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性

《C++prime》第五版关于inline的建议:

在这里插入图片描述
我们可以到VS中来观察一下

  • 可以观察到当内联函数的内容增多时,程序中去调用这个内联函数依旧会执行call指令,而不是将其函数中的内容直接拷贝到程序调用处,即使是加了这个inline关键字,似乎还是起不到内联函数的作用,这是为什么呢?
  • 因为编译器在对于这个内联请求的时候发觉不对劲,所以选择忽略了这个请求,而不是像宏那样无论怎样都会傻傻地进行替换

在这里插入图片描述

  • 这其实就是内联函数在替代宏之后很优秀的一个特性,假设说现在你这个设置的内联函数有1000多行代码,在一个大项目中又有1000个地方调用了这个内联函数。
    • 如果不采用将其展开去调用的话消耗的顶多也就是1000 + 1000条指令
    • 如果采用内联将其展开的话消耗的就是1000 * 1000条指令,这就很恐怖了😱
  • 那上面这个还是个普通的大一点的函数,但你再想如果是**··**呢?层层地往下调用再一层层地返回来,那需要调用的指令就更多了,如果全部站开的话,就会造成一个灾难性的后果⚠

在这里插入图片描述

所以呢,加不加内敛是你的事,最终要不要把它真的展开变成内敛编译器说了算,所以我们在使用的时候一般是比较短小、调用频繁的函数(10几行以内)加上内联,其他就不加了

4、特性③:inline的声明与定义

inline内联函数的第三个特性,就是我们要注意内联函数的定义和声明不可以分开,导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到

// F.h
#include <iostream>
using namespace std;

inline void f(int i);
//--------------------------
// F.cpp
#include "F.h"

void f(int i)
{
	cout << i << endl;
}
//--------------------------
// main.cpp
#include "F.h"

int main()
{
	f(10);
	return 0;
}
  • 通过去VS中执行这段分文件代码,可以发现它不是一个【编译时错误】,而是一个【运行时错误

在这里插入图片描述
👉 这种错误一般都是最后链接目标文件的时候除了问题,不是很清楚的可以看看程序的编译和链接
👉 对于这些很复杂的符号func@@YAXH@Z,是C++中的函数名修饰,可以看看探究函数重载的原理:函数名修饰

在这里插入图片描述
那我们现在就要去思考,为什么会出现这种【运行时错误】呢?

  • 最主要的一点首先你要知道:内联函数的不进符号表的,因为对于内联函数来说它在其调用的地方都展开了,所以不需要产生一串指令把它放到符号表中,再通过一条一条指令去调用

反汇编观察函数调用的流程

对于内联函数来说是这样,但是普通函数来说是怎么一步步地进行调用的呢?我们可以通过汇编来看看💻

  • 对于函数的地址来说,在指针章节我们有说过,它值得就是【函数指针】,但是从汇编角度来看,它call的到底是谁的地址呢?

在这里插入图片描述

  • 通过按下F11跳转到了一条叫做【jmp】的指令,此时你去观察这个地址,其实就是这条【jmp】的地址,在英语中jump是跳的意思,那你可以将这条指令理解为一个中转站,call指令会暂时跳到这条指令所在的地址,然后再通过这条指令去找需要调用函数的地址

在这里插入图片描述

  • 此刻,再按下F10的话就会通过这个函数的地址找到这个函数,此时就开始执行函数内部的逻辑,

在这里插入图片描述

  • 然后,当执行完函数内部的逻辑后,便回到了call指令的下一条指令,这就算是一次函数调用的过程

在这里插入图片描述

可以将它们放到一起来观察,就可以发现它们之间的逻辑非常紧密

在这里插入图片描述
如果对上面过程还是不太理解的可以看看反汇编深挖【函数栈帧】的创建和销毁

内联函数的生成机制【⭐】

上面是对于普通函数的调用机制,那对于内联函数呢?也是这么去跳转吗?

  • 一定要注意!内联内联,那么这个函数的内容就直接放到调用的地方
  • 它便说:也就是我不需要你再通过这么繁琐的步骤一步步地跳转过来了,我会将内部的东西做一些优化,直接放到你那里,你执行这些指令即可

可是呢,为什么会出现链接错误❌

  • 因为在【预编译】的时候就要展开func.h这个头文件,但是在主调用接口中包含的头文件中只有函数的声明没有实现,此时只能在【链接】的时候展开了,但是在链接的时候因为只有声明所以只得到了函数名修饰后的地址。编译器便需要通过这个地址找到函数所在的位置,对于普通函数而言在这个时候就可以通过call找过去了,但是对于内联函数而言,却无法做到,因为它并没有【call】和【jmp】这些指令,因此就造成了链接错误的现象

那要如何去解决呢?

  • 若是这个函数要定义成内联函数的话,就不要将定义和声明分开了,在头文件中定义出来后就直接对其进行声明,便不会造成这样的问题了
// F.h
#include <iostream>
using namespace std;

inline void f(int i)
{
	cout << i << endl;
}
//--------------------------
// main.cpp
#include "F.h"

int main()
{
	f(10);
	return 0;
}

✍一道经典笔试题

关于c++的inline关键字,以下说法正确的是( )

A.使用inline关键字的函数会被编译器在调用处展开
B.头文件中可以包含inline函数的声明
C.可以在同一个项目的不同源文件内定义函数名相同但实现不同的inline函数
D.递归函数也都可以成为inline函数

【答案】:C
【解析】:

  • [A] 不一定,因为inline只是一种建议,需要看此函数是否能够成为内联函数
  • [B] inline函数不支持声明和定义分离开,因为编译器一旦将一个函数作为内联函数处理,就会在调用位置展开,即该函数是没有地址的,也不能在其他源文件中调用,故一般都是直接在源文件中定义内联函数的
  • [C] inline函数会在调用的地方展开,所以符号表中不会有inline函数的符号名,不存在链接冲突
  • [D] 比较长的函数,递归函数就算定义为inline,也会被编译器忽略,故错误

四、总结与提炼

好,最后来总结一下本文所学习的内容📖

  • 首先我们回顾来一下C语言中所学习的【宏】,经过了对宏的优缺点分析以及一些同学的错误案例对照,在面试的时候让你写一个宏,可不要写错了哦!
  • 接下去,我们就正式地开始介绍内联函数,对于内联函数来说,它不仅保留了宏的优点,没有函数调用建立栈帧的开销,而且还修复了宏的缺点,将其做成一个函数的形式,简洁直观,而且便于调试观察,提升程序运行的效率
  • 但是对于内联函数来说,也是存在要注意的地方,因为它也是会和宏一样在调用的地方展开,不过会进行一定程度的优化,可这种空间换时间的思想只适用于小型的函数,对于大型的函数不建议定义成【内联函数】,会造成程序的过多臃肿
  • 但是在看了内联函数的生成机制后,其实我们也不用担心在误用内联函数后使得程序变大,它会有一个自动判断的机制,若是你程序的行数过多的话,编译器就会忽略你的这请求,对于我们要将一个函数声明为内联函数其实是在向编译器发起一个申请,它可以选择接收也可以选择拒绝🙅‍

以上就是本文要介绍的所有内容,感谢您的阅读🌹

在这里插入图片描述

<think>嗯,用户想了解C语言中的内联函数。首先我得回忆一下内联函数的基本概念。内联函数,顾名思义,应该是用inline关键字声明的函数,编译器在处理时会尝试将函数体直接插入到调用处,而不是通过常规的函数调用机制。这样做的目的是为了减少函数调用的开销,提高程序的运行效率,特别是在频繁调用小函数的时候。 不过,我之前学过,内联函数只是给编译器的一个建议,编译器可以选择忽略这个建议。比如,如果函数体太大或者有递归调用,编译器可能不会进行内联展开。这点需要明确告诉用户,不能让他们误解所有声明为inline的函数都会被内联。 接下来,我得考虑内联函数的优缺点。优点很明显,减少调用开销,提升性能。但缺点呢?比如可能增加代码体积,尤其是如果内联函数被多处调用,每个调用点都展开的话,代码会膨胀。这可能导致缓存命中率降低,反而影响性能。所以需要权衡使用场景,适合小且频繁调用的函数。 另外,内联函数有什么区别?这点用户可能也会感兴趣。是在预处理阶段展开,而内联函数是在编译阶段展开。内联函数有类型检查,更安全,调试也方便,而容易出错,尤其是涉及复杂表达式时。比如的参数可能被多次求值,导致副作用,而内联函数不会有这个问题。 还有,C99标准对内联函数的规定是什么?在C89/C90中,inline不是标准关键字,可能依赖编译器扩展。而C99开始支持inline关键字。需要注意不同编译器的实现差异,比如GCCMSVC可能有不同的行为。 使用内联函数的步骤,可能需要说明如何声明定义。通常是在函数声明前加上inline关键字,或者在头文件中定义static inline函数,以便在多个源文件中使用。但要注意,如果在头文件中定义非static的内联函数,可能会导致链接错误,因为多个编译单元可能会有相同的符号。 另外,内联函数普通函数在链接时的处理不同。普通函数在链接时处理重复定义,而内联函数可能需要特殊处理,比如使用extern inline或者static inline来避免链接问题。 用户可能的疑问点:什么时候应该使用内联函数?如何确保编译器真的内联了函数?这时候可能需要建议查看编译器文档,或者使用编译器特定的选项来强制内联或查看内联结果。 还有,内联函数C++中的内联函数有什么区别?C++中的内联函数处理可能更复杂,但用户现在问的是C语言,所以不需要展开,但可能需要提醒CC++在这方面的差异。 总结结构的话,可能需要分点:定义、工作原理、优点、缺点、与的区别、使用注意事项、示例。这样用户看起来清晰。 需要确保回答中用中文,并且数学公式用正确的LaTeX格式,比如变量用$...$,独立公式用$$...$$。不过用户的问题是关于C语言的,可能数学公式不多,但如果有涉及到比如时间复杂度的话,可能需要用。 最后检查是否有错误,比如是否混淆了CC++内联函数特性,或者是否遗漏了关键点。比如在C中,内联函数的定义通常放在头文件中,而普通函数一般不在头文件中定义,否则会导致重复定义错误。而使用static inline可以避免这个问题,因为每个编译单元会有自己的副本,虽然可能增加代码体积,但避免了链接问题。</think>### C语言中的内联函数介绍 #### 1. **定义与基本概念** **内联函数Inline Function)** 是C语言中通过关键字 `inline` 声明的函数。其核心目的是通过**将函数代码直接嵌入调用处**来减少函数调用的开销(如栈帧创建、参数传递等),从而提升程序运行效率。 --- #### 2. **工作原理** - **编译阶段处理**:当函数被声明为 `inline` 时,编译器会尝试将函数体代码替换到每一个调用点,而不是生成独立的函数调用指令。 - **示例**: ```c inline int max(int a, int b) { return (a > b) ? a : b; } ``` 调用 `max(x, y)` 时,编译器可能直接替换为 `(x > y) ? x : y`。 --- #### 3. **优点** - **减少开销**:适用于频繁调用的小函数(如简单计算、条件判断),避免函数调用产生的性能损失。 - **优化代码**:允许编译器对嵌入后的代码进行更深入的优化(如常量传播、循环展开)。 --- #### 4. **缺点与限制** - **代码膨胀**:若函数体较大或调用次数过多,会导致生成的可执行文件体积增大。 - **编译器自主性**:`inline` 仅是对编译器的建议,实际是否内联由编译器决定(如GCC的 `-O2` 优化级别会主动内联小函数)。 - **调试困难**:内联后的代码无法通过函数名直接跟踪。 --- #### 5. **与(Macro)的区别** | 特性 | 内联函数 | (`#define`) | |---------------------|-----------------------------|------------------------------| | **展开阶段** | 编译阶段 | 预处理阶段 | | **类型安全** | 支持参数类型检查 | 无类型检查,易出错 | | **副作用** | 参数只求值一次 | 参数可能被多次求值(如 `MAX(a++, b++)`) | | **调试支持** | 可调试 | 难以调试 | --- #### 6. **使用注意事项** - **适用场景**:适合函数体简单(如1-3行)、调用频繁的代码。 - **头文件定义**:通常将 `inline` 函数定义在头文件中,并配合 `static` 避免链接错误: ```c // 在头文件中定义 static inline int add(int a, int b) { return a + b; } ``` - **C99标准支持**:C99正式引入 `inline` 关键字,需确保编译器兼容。 --- #### 7. **示例代码** ```c #include <stdio.h> // 声明内联函数 static inline int square(int x) { return x * x; } int main() { int a = 5; printf("Square of %d is %d\n", a, square(a)); return 0; } ``` **输出**: ``` Square of 5 is 25 ``` --- #### 8. **总结** 内联函数通过消除函数调用开销优化性能,但需权衡代码体积与执行效率。它比更安全,但实际效果依赖编译器实现。合理使用内联函数是C语言高效编程的重要技巧之一。
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烽起黎明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值