校招后端面经——C++

1. 内联函数和宏
  1. 内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。
  2. 宏不是函数,只是在预处理阶段将程序中有关字符串替换成宏体。 而内联函数则在编译时直接被镶嵌到目标代码中
  3. 编译器会对内联函数做安全检查或自动类型转换
  4. 内联函数可以访问类的成员变量,宏定义则不能
  5. 在类声明同时定义的成员函数,自动转化为内联函数
2. class和struct的区别
1. 区别
  1. class默认类内成员为私有的,struct默认成员为公有的。
  2. class默认类继承为私有的,struct默认继承为公有的。
2. C++的struct与C的struct

在C++中对struct的功能进行了扩展,struct可以被继承,可以包含成员函数,也可以实现多态,当用大括号对其进行初始化需要注意:

当struct和class中都定义了构造函数,就不能使用大括号对其进行初始化
若没有定义构造函数,struct可以使用{ }进行初始化,而只有当class的所有数据成员及函数为public时,可以使用{ }进行初始化

所以struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

3. 合成析构函数
  1. 编译器总会为我们合成一个析构函数,合成析构函数按对象创建时的逆序撤销每一个非static成员,因此,它按成员在类中声明次序的逆序撤销成员,对于class类型的成员,合成析构函数会调用该成员的析构函数来撤销对象,即使我们编写了自己的析构函数,合成析构函数依旧运行
  2. 合成析构函数不管你建不建析构函数他都有
  3. 合成析构函数负责销毁对象本身,例如我们对象含有一个int成员时,合成析构函数可以回收这个int成员占用的空间,这也是合成析构函数真正做的事情
  4. 我们的析构函数是对合成析构函数的扩展,所以我们常常在合成析构函数中析构堆中的资源
  5. 按照合成析构函数和析构函数的分工,可以理解为必然先运行手动建立的析构函数,再运行合成的析构函数
4. 类模板
  1. template.在模板定义语法中关键字class与typename的作用完全一样

  2. 一个类模板(类生成类)允许用户为类定义一种模式,使得类中的某些数据成员、默认成员函数的参数,某些成员函数的返回值,能够取任意类型。

  3. 例子:

    template<class T>
    
    class Test
    {
    private:
        T n;
        const T i;
    public:
        Test():i(0) {}
        Test(T k);
        ~Test(){}
    
        void print();
        T operator+(T x);
    };
    
  4. 模板类是类模板实例化后的一个产物

5. 虚继承
1. 目的

虚继承是为了解决多重继承带来的二义性问题

会产生虚基表指针

6. 类的大小

https://blog.csdn.net/fengxinlinux/article/details/72836199

1. 计算原则
  1. 类的大小的计算遵循结构体对齐原则
  2. 类的大小与普通数据成员有关,与成员函数和静态成员无关。
  3. 虚函数对类的大小有影响,是因为虚函数表指针带来的影响
  4. 虚继承对类的大小有影响,是因为虚基表指针带来的影响
  5. 空类的大小为1
2. 空类
空类继承

当派生类继承空类后,派生类如果有自己的数据成员,而空基类的一个字节并不会加到派生类中去。例如

class Empty {};
class D : public Empty { int a;};

sizeof(D)为4

类中包含空类对象数据成员

在这种情况下,空类的1字节是会被计算进去的。而又由于字节对齐的原则,所以结果为4+4=8

class Empty {};
class HoldsAnInt {
    int x;
    Empty e;
};

sizeof(HoldsAnInt)为8。

3. 虚函数表指针

指向虚函数表的指针在对象的最前面

vptr指针的大小为4或8

  1. 派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数

    (1)虚函数按照声明顺序放于表中

    (2)基类的虚函数在派生类的虚函数前面

  2. 在派生类中对基类的虚函数进行覆盖

    (1)覆盖的虚函数被放到虚表原来基类虚函数的位置,基类的虚函数被替换掉

    (2)没有被覆盖的函数依旧

  3. 多继承:无虚函数覆盖

    (1)每个基类都有自己的虚函数表

    (2)派生类的虚函数被放到第一个基类的表中

  4. 多继承:含虚函数覆盖

    基类的虚表指针中的虚函数都被覆盖

注意:GCC子类共享父类虚函数表指针,不占用额外的空间;VC在虚继承情况下,不共享父类虚函数表指针

4. 虚继承

虚基类指针大小为4或8字节

在虚继承中,由最底层派生类的构造函数初始化虚基类

每个派生类都有一个虚基类指针,所以最后派生类要叠加子类的虚基类指针

不同编译器对间接存取的方法不同,以下以GCC和VC为例,均采用以下代码进行实验:

1. class SuperBase  
2. {  
3. public:  
4.     int m_nValue;  
5.     void Fun(){}  
6.     virtual ~SuperBase(){}  
7. };  
8. class Base1:  virtual public SuperBase  
9. {  
10. public:  
11. virtual ~ Base1(){}  
12. };  
13. class Base2:  virtual public SuperBase  
14. {  
15. public:  
16. virtual ~ Base2(){}  
17. };  
18. class Der:public Base1, public Base2  
19. {  
20. public:  
21. virtual ~ Der(){}  
22. };  
  1. GCC中结果为8, 12, 12, 16

解析:sizeof(SuperBase) = sizeof(int) + 虚函数表指针

sizeof(Base1) = sizeof(Base2) = sizeof(int) + 虚函数指针 + 虚基类指针

sizeof(Der) = sizeof(int) + Base1中虚基类指针 + Base2虚基类指针 + 虚函数指针

GCC共享虚函数表指针,也就是说父类如果已经有虚函数表指针,那么子类中共享父类的虚函数表指针空间,不在占用额外的空间,这一点与VC不同,VC在虚继承情况下,不共享父类虚函数表指针,详见如下。

2)VC中结果为:8, 16, 16, 24

解析:sizeof(SuperBase) = sizeof(int) + 虚函数表指针

sizeof(Base1) = sizeof(Base2) = sizeof(int) + SuperBase虚函数指针 + 虚基类指针 + 自身虚函数指针

sizeof(Der) = sizeof(int) + Base1中虚基类指针 + Base2中虚基类指针 + Base1虚函数指针 + Base2虚函数指针 + 自身虚函数指针

7. 几种cast
1. reinterpret_cast

能够将一种对象类型转换为另一种,不管它们是否相关。

在任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。

【注意】reinterpret_cast不能用于内置类型之间的转换,只能用于不同指针之间的转换。

CBase* pBase = new CBase( ) ;  
CDerived* pDerived = reinterpret_cast<CDerived*>(pBase) ;  

这种类型转换实际上是强制编译器接受static_cast通常不允许的类型转换,它并没有改变指针值的二进制表示,只是改变了编译器对源对象的解释方式。

2. static_cast

允许执行任意的隐式转换和相反转换动作。(即使它是不允许隐式的) 应用到类的指针上,意思是说它允许子类类型的指针转换为父类类型的指针(这是一个有效的隐式转换),同时,也能够执行相反动作:转换父类为它的子类。

class Base {}; 
class Derived : public Base {};  
Base *a    = new Base; 
Derived *b = static_cast<Derived *>(a); 

除了操作类型指针,也能用于执行类型定义的显式的转换,以及基础类型之间的标准转换

double d = 3.14159265; 
int i = static_cast<int>(d);  
3. dynamic_cast

只用于对象的指针和引用。当用于多态类型时,它允许任意的隐式类型转换以及相反过程。不过,与static_cast不同,在后一种情况里(注:即隐式转换的相反过程),dynamic_cast会检查操作是否有效。也就是说,它会检查转换是否会返回一个被请求的有效的完整对象。 检测在运行时进行。如果被转换的指针不是一个被请求的有效完整的对象指针,返回值为NULL.

class Base { virtual dummy() {} };
class Derived : public Base {};

Base* b1 = new Derived;
Base* b2 = new Base;

Derived* d1 = dynamic_cast<Derived *>(b1);          // succeeds
Derived* d2 = dynamic_cast<Derived *>(b2);          // fails: returns 'NULL'

如果一个引用类型执行了类型转换并且这个转换是不可能的,一个bad_cast的异常类型被抛出:

class Base { virtual dummy() {} };
class Derived : public Base { };

Base* b1 = new Derived;
Base* b2 = new Base;

Derived d1 = dynamic_cast<Derived &*>(b1);          // succeeds
Derived d2 = dynamic_cast<Derived &*>(b2);          // fails: exception thrown
4. const_cast

一般用于强制消除对象的常量性

5. 比较

dynamic_cast 主要用于执行“安全的向下转型(safe downcasting)”,也就是说,要确定一个对象是否是一个继承体系中的一个特定类型。它是唯一不能用旧风格语法执行的强制转型,也是唯一可能有重大运行时代价的强制转型。

static_cast 可以被用于强制隐型转换(例如,non-const 对象转型为 const 对象,int 转型为 double,等等),它还可以用于很多这样的转换的反向转换(例如,void* 指针转型为有类型指针,基类指针转型为派生类指针),但是它不能将一个 const 对象转型为 non-const 对象(只有 const_cast 能做到),它最接近于C-style的转换

const_cast 一般用于强制消除对象的常量性。它是唯一能做到这一点的 C++ 风格的强制转型。

reinterpret_cast 是特意用于底层的强制转型,导致实现依赖(implementation-dependent)(就是说,不可移植)的结果,例如,将一个指针转型为一个整数。这样的强制转型在底层代码以外应该极为罕见。

8. const
1. const修饰成员变量

不能被修改,只能在初始化列表中赋值

2. const修饰成员函数

不能改变类中任何成员的变量,也不能调用任何非常成员函数

3. const修饰类对象/对象指针/对象引用

该对象的任何成员都不能被修改,不能调用任何非const的成员函数

9. static
1. 隐藏

全局变量加上static后,外部文件无法再通过extern访问到该变量

2. 保持变量内容的持久

存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。

如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。

3. 修饰类成员

类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。

不能将静态成员函数定义为虚函数。

静态数据成员是静态存储的,所以必须对它进行初始化。

初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;

初始化时不加该成员的访问权限控制符private,public

初始化时使用作用域运算符来标明它所属类

静态数据成员初始化的格式: <数据类型><类名>::<静态数据成员名>=<值>

静态成员为父类和子类共享

4. 修饰内部类对象

内部静态类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。

非静态内部类能够访问外部类的静态和非静态成员。

静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。

一个非静态内部类不能脱离外部类实体被创建,一个非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面

10. 友元函数

友元(frend)机制允许一个类将对其非公有成员的访问权授予指定的函数或者类,友元的声明以friend开始,它只能出现在类定义的内部,友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响。

1. 友元函数

友元函数是指某些虽然不是类成员函数却能够访问类的所有成员的函数。类授予它的友元特别的访问权,这样该友元函数就能访问到类中的所有成员。

2. 友元类

友元关系不能被继承

友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明

友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

3. 友元成员函数

使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员了。

11. vector内存增长

vector所有的内存相关问题都可以归结于它的内存增长策略。vector有一个特点就是:内存空间只会增长不会减少。vector有两个函数,一个是capacity(),返回对象缓冲区(vector维护的内存空间)实际申请的空间大小,另一个size(),返回当前对象缓冲区存储数据的个数。对于vector来说,capacity是永远大于等于size的,当capacity和size相等时,vector就会扩容,capacity变大。

1. push_back的过程

这个问题其实很简单,在调用push_back时,若当前容量已经不能够放入新的元素(capacity=size),那么vector会重新申请一块内存,把之前的内存里的元素拷贝到新的内存当中,然后把push_back的元素拷贝到新的内存中,最后要析构原有的vector并释放原有的内存。所以说这个过程的效率是极低的,为了避免频繁的分配内存,C++每次申请内存都会成倍的增长,例如之前是4,那么重新申请后就是8,以此类推。当然不一定是成倍增长,比如在我的编译器环境下实测是0.5倍增长,之前是4,重新申请后就是6。

2. 内存释放

就像前面所说的,vector的内存空间是只增加不减少的,我们常用的操作clear()和erase(),实际上只是减少了size(),清除了数据,并不会减少capacity,所以内存空间没有减少。那么如何释放内存空间呢,正确的做法是swap()操作。

也可以简单使用以下操作

vector<Point>().swap(pointVec); //或者pointVec.swap(vector<Point> ())  

swap交换技巧实现内存释放思想:vector()使用vector的默认构造函数建立临时vector对象,再在该临时对象上调用swap成员,swap调用之后原来vector占用的空间就等于一个默认构造的对象的大小,临时对象就具有原来对象v的大小,而该临时对象随即就会被析构,从而其占用的空间也被释放。交换之后,temp会被析构。

3. vector循环删除一个元素应该注意什么
for(it=iVec.begin();it!=iVec.end();)
     {
         if(*it % 3 ==0)
             it=iVec.erase(it);    //删除元素,返回值指向已删除元素的下一个位置 
             或者iVec.erase(it++);
         else
             ++it;    //指向下一个位置
     }

删除后it迭代器失效,变为空指针

其他stl,如map也一样

该方法中利用了后缀++的特点,这个时候执行iVec.erase(it++);这条语句分为三个过程
1、先把it的值赋值给一个临时变量做为传递给erase的参数变量

2、因为参数处理优先于函数调用,所以接下来执行了it++操作,也就是it现在已经指向了下一个地址。

3、再调用erase函数,释放掉第一步中保存的要删除的it的值的临时变量所指的位置。

12. 虚函数
9. 虚函数使用规则

virtual在函数中的使用限制

普通函数不能是虚函数,也就是说这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。

静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。

内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。

构造函数不能是虚函数,否则会出现编译错误。

13. 拷贝构造函数和赋值构造函数
1. 区别
  1. 拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
  2. 一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象
  3. 实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数对象初始化产生一个新的对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。
2. 深拷贝和浅拷贝
浅拷贝

如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除空间存在)

深拷贝

如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。

14. 字符串复制
1. strcpy

将由source指针指示的C 字符串(包括结尾字符)复制到destination指针指示的区域中。该函数不允许source和destination的区域有重叠,同时,为了避免溢出,destination区域应该至少和source区域一样大。

遇到被复制字符的串结束符"\0"才结束,所以容易溢出。

2. strcnpy

复制source的前num字符到destination。如果遇到null字符(’\0’),且还没有到num个字符时,就用(num - n)(n是遇到null字符前已经有的非null字符个数)个null字符附加到destination。注意:并不是添加到destination的最后,而是紧跟着由source中复制而来的字符后面。下面举例说明:

char des[] = “Hello,i am!”;

char source[] = “abc\0def”;

strncpy(des,source,5);

此时,des区域是这样的:a,b,c,\0,\0,i,空格,a,m,!

\0,\0并不是添加在!的后面。

3. memcpy

将source区域的前num个字符复制到destination中。该函数不检查null字符(即将null字符当作普通字符处理),意味着将复制num个字符才结束。该函数不会额外地引入null字符,即如果num个字符中没有null字符,那么destination中相应字符序列中也没有null字符。

memcpy可以复制任意内容,例如字符数组、整型、结构体、类等

4. memmove

同memcpy完成同样的功能,区别是,memmove允许destination和source的区域有重叠。而其他三个函数不允许。

区域重叠:memmove函数用于从src拷贝count个字符到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。但复制后src内容会被更改。

区域重叠时采用逆向复制

15. sizeof
  1. 与strlen()比较 strlen计算字符数组的字符数,以"\0"为结束判断,不计算为’\0’的数组元素。 sizeof计算数据(包括数组、变量、类型、结构体等)所占内存空间,用字节数表示(当然用在字符数组计算"\0"的大小)。

  2. 指针与静态数组的sizeof操作 指针均可看为变量类型的一种。所有指针变量的sizeof 操作结果均为4。

    void  fun(char p[])
    {
     sizeof(p);         //等于4,数组做型参时,数组名称当作指针使用!!
    }
    
    实例3(经典考题):
    double* (*a)[3][6];
    cout<<sizeof(a)<<endl; // 4 a为指针
    cout<<sizeof(*a)<<endl; // 72 *a为一个有3*6个指针元素的数组
    cout<<sizeof(**a)<<endl; // 24 **a为数组一维的6个指针
    cout<<sizeof(***a)<<endl; // 4 ***a为一维的第一个指针
    cout<<sizeof(****a)<<endl; // 8 ****a为一个double变量
    
  3. 使用sizeof时string的注意事项

    string s=“hello”; sizeof(s)等于string类的大小(32)

    sizeof(s.c_str())得到的是与字符串指针(char*)大小(4)

  4. sizeof(函数) 若求函数的sizeof,相当于求函数返回值的sizeof,注意函数并不执行。

  5. 结构体陷阱

    实例5:
    struct s1
    {
     char a[8];
    };
    
    struct s2
    {
     double d;
    };
    
    struct s3
    {
     s1 s;
     char a;
    };
    
    struct s4
    {
     s2 s;
     char a;
    };
    cout<<sizeof(s1)<<endl; // 8
    cout<<sizeof(s2)<<endl; // 8
    cout<<sizeof(s3)<<endl; // 9
    cout<<sizeof(s4)<<endl; // 16;
    s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。
    
16. 指针数组和数组指针

指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。

int *p[4];

数组指针:首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。

int (*p)[4]; //不严谨看,p是一个二维数组

17. 智能指针

根据该博客整理

1. 定义
  1. 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  2. 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
2. shared_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

3. unique_ptr

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。

unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

4. weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

在循环引用中,如果出现相互嵌套的情况,使用share_ptr会导致内存泄漏的问题,而用weak_ptr则不会

18. offsetof宏
1. 形式

#define offset(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER)

2. 解析

此类复杂表达式的解析应该采用从内向外、逐层理解的方式。

  1. (TYPE *)0表示将数字0强制类型转换为TYPE类型(TYPE为结构体类型)的指针。因此这里的0代表内存地址0,即我们认为内存地址0开始的sizeof(TYPE)个字节内存储的是一个TYPE类型的变量。
  2. ((TYPE *)0)->MEMBER 得到该结构体变量中的MEMBER成员变量
  3. &(((TYPE*)0)->MEMBER) 使用取地址符&取得了MEMBER成员变量的地址,(size_t)加在前面表示将MEMBER成员变量的地址强制类型转换为size_t(即unsigned int),并将结果作为宏的返回值。

offsetof宏的运算是在编译器编译时完成的,因此内存的0地址在机器指令中根本未被操作。

19. 指针和引用的区别
  1. 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来

    的变量实质上是同一个东西,只不过是原变量的一个别名而已。

  2. 引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

  3. 可以有const指针,但是没有const引用;

  4. 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

  5. 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

  6. ”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值