C++初阶:针对C语言的优化

什么是C++

        C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的
程序,需要高度的抽象和建模时, C 语言则不合适。为了解决软件危机, 20 世纪 80 年代, 计算机
界提出了 OOP(object oriented programming :面向对象 ) 思想 ,支持面向对象的程序设计语言
应运而生。
        1982年, Bjarne Stroustrup 博士在 C 语言的基础上引入并扩充了面向对象的概念,发明了一
种新的程序语言。为了表达该语言与 C 语言的渊源关系,命名为 C++ 。因此: C++ 是基于 C 语言而
产生的,它既可以进行 C 语言的过程化程序设计,又可以进行 以抽象数据类型为特点的基于对象的
程序设计,还可以进行面向对象的程序设计

背景

        1979年,贝尔实验室的本贾尼等人试图分析 unix 内核的时候,试图将内核模块化,于是 C
语言的基础上进行扩展,增加了类的机制 ,完成了一个可以运行的预处理程序,称之为 C with
classes
        C++是在C 的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式 等。熟悉 C 语言之后,对 C++ 学习有一定的帮助,本篇主要目标:
1. 补充 C 语言语法的不足,以及 C++ 是如何对 C 语言设计不合理的地方进行优化的,比如:作用
域方面、 IO 方面、函数方面、指针方面、宏方面等。
2. 为后续类和对象学习打基础。

命名空间

C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存
在于全局作用域中,可能会导致很多冲突。
使用命名空间的目的是 对标识符的名称进行本地化 , 以避免命名冲突或名字污染 namespace 关键字的出现就是针对这种问题的。(解决程序员跟库的问题、程序员跟程序员的问题)
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
 printf("%d\n", rand);
return 0;
}
编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”。
扩展:
     ::域作用限定符,::左边不写,代表全局域
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
// ::域作用限定符
// 全局域
// 局部域
// 命名空间域(在全局中)
// 类域
int x = 0;
int main()
{
	int x = 1;

	printf("%d\n", x);
	printf("%d\n", ::x);

	return 0;
}

命名空间定义和使用

定义命名空间,需要使用到 namespace 关键字 ,后面跟 命名空间的名字 ,然 后接一对 {} 即可, {}
中即为命名空间的成员。
// bit是命名空间的名字,一般开发中是用项目名字做命名空间名。
// 1. 正常的命名空间定义
namespace bit
{
	// 命名空间中可以定义变量/函数/类型
	int rand = 10;
	int Add(int left, int right)
	{
		return left + right;
	}
		struct Node
	{
		struct Node* next;
		int val;
	};
}


//编译器搜索原则:
//不指定域:1、当前局部域   2、全局域
//指定域   3、如果指定了,直接去指定域搜索
int main()
{
	printf("%d\n", bit::rand);

	printf("%d\n", bit::Add(1,2)); 

	struct bit::Node phead;

	return 0;
}
//2. 命名空间可以嵌套
// test.cpp
//例1
namespace N1
{
int a;
int b;
int Add(int left, int right)
 {
     return left + right;
 }
namespace N2
 {
     int c;
     int d;
     int Sub(int left, int right)
     {
         return left - right;
     }
 }
}

//例2
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using std::cout;
using std::endl;

namespace bit
{
	namespace zs
	{
		void Push()
		{
			cout << "zs" << endl;
		}
	}

	namespace ls
	{
		void Push()
		{
			cout << "ls" << endl;
		}
	}
}

int main()
{
	bit::zs::Push();
	bit::ls::Push();

	return 0;
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
int Mul(int left, int right)
 {
     return left * right;
 }
}

注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

命名空间使用的三种方式:

加命名空间名称及作用域限定符
int main()
{
    printf("%d\n", N::a);
    return 0;    
}
使用 using 将命名空间中某个成员引入(部分展开)
using N::b;
int main()
{
    printf("%d\n", N::a);
    printf("%d\n", b);
    return 0;    
}
使用 using namespace 命名空间名称 引入(全部展开)
即:展开命名空间,访问权限打开(大型项目中不会这么用,容易冲突;小项目、日常练习可以,方便)
using namespce N;
int main()
{
    printf("%d\n", a);
    printf("%d\n", b);
    Add(10, 20);
    return 0;    
}

std是所有c++库的命名空间:

#include<iostream>
using namespace std;

int main()
{
	std::cout << "hello world" << std::endl;//写不写using namespace std;都行

	cout << "hello world" << endl;//写using namespace std;



	return 0;
}

理解:

C++输入&输出

#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;

int main()
{
	// 1、左移
	int i = 100;
	i = i << 1;
	const char* str = "hello world";
	char ch = '\n';

	// 2、流插入 自动识别类型
	cout << str << i << ch << endl;
	printf("%s%d%c", str,i,ch);
	
	// 右移

	// 流提取
	cin >> i >> ch;
	cout << str << i << ch << endl;

	scanf("%d%c", &i, &ch);
	printf("%s%d%c", str, i, ch);

	double d = 1.11111111;
	printf("%.2lf\n", d);
	cout << d << endl;

	return 0;
}
// ps:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等
等。因为C++兼容C语言的用法,这些又用得不是很多,这里就不展开学习了。
说明:
1. 使用 cout 标准输出对象 ( 控制台 ) cin 标准输入对象 ( 键盘 ) 时,必须 包含 < iostream > 头文件
以及按命名空间使用方法使用 std
2. cout cin 是全局的流对象, endl 是特殊的 C++ 符号,表示换行输出,他们都包含在包含 <
iostream > 头文件中。
3. << 是流插入运算符, >> 是流提取运算符
4. 使用 C++ 输入输出更方便,不需要像 printf/scanf 输入输出时那样,需要手动控制格式。
C++ 的输入输出可以自动识别变量类型。
5. 实际上 cout cin 分别是 ostream istream类型的对象。>> << 也涉及运算符重载等知识。
注意:早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应
头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,
规定 C++ 头文件不带 .h ;旧编译器 (vc 6.0) 中还支持 <iostream.h> 格式,后续编译器已不支持,因
推荐 使用 <iostream>+std 的方式。

std命名空间的使用惯例:

std C++ 标准库的命名空间,如何展开 std 使用更合理呢?
1. 在日常练习中,建议直接 using namespace std 即可,这样就很方便。
2. using namespace std 展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型 /
/ 函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模
大,就很容易出现。所以建议在项目开发中使用,像 std::cout 这样使用时指定命名空间 +
using std::cout 展开常用的库对象 / 类型等方式。

缺省/默认参数

缺省参数概念

缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值 。在调用该函数时,如果没有指定实
参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a = 0)
{
 cout<<a<<endl;
}
int main()
{
 Func();     // 没有传参时,使用参数的默认值
 Func(10);   // 传参时,使用指定的实参
return 0;
}

缺省参数分类

全缺省参数

void Func(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

int main()
{
	Func(1, 2, 3);
	Func(1, 2);
	Func(1);
	Func();

	return 0;
}

半缺省参数

 //半缺省 从右往左连续给
void Func(int a, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

int main()
{
	Func(1, 2, 3);
	Func(1, 2);
	Func(1);

	return 0;
}
注意:
1. 半缺省参数必须 从右往左依次 来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现;当声明和定义分离时,缺省参数放到声明里
 //a.h
  void Func(int a = 10);
  
  // a.cpp
  void Func(int a = 20)
 {}
  
  // 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该
用那个缺省值。
3. 缺省值必须是常量或者全局变量(全局变量一般不用)
4. C 语言不支持(编译器不支持)

应用场景

举例:

#include"Stack.h"

struct Stack
{
	int* a;
	int size;
	int capacity;
	//...
};

void StackInit(struct Stack* ps, int n = 4);
void StackPush(struct Stack* ps, int x);

int main()
{
	struct Stack st1;

	// 1、确定要插入100个数据
	StackInit(&st1, 100);  // call StackInit(?)

	// 2、只插入10个数据
	struct Stack st2;
	StackInit(&st2, 10);   // call StackInit(?)

	// 3、不知道要插入多少个
	struct Stack st3;
	StackInit(&st3);

	return 0;
}

函数重载

函数重载概念

函数重载: 是函数的一种特殊情况, C++ 允许在 同一作用域中 声明几个功能类似 的同名函数 ,这
些同名函数的 形参列表 ( 参数个数 或 类型 或 类型顺序 ) 不同 ,常用来处理实现功能类似数据类型
不同的问题。
// C语言不允许同名函数
// CPP语言允许同名函数,要求:函数名相同,参数不同,构成函数重载
// 1、参数类型的不同
// 2、参数个数不同
// 3、参数顺序不同(本质还是类型不同)
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
 cout << "int Add(int left, int right)" << endl;
 return left + right;
}
double Add(double left, double right)
{
 cout << "double Add(double left, double right)" << endl;
 return left + right;
}
// 2、参数个数不同
void f()
{
 cout << "f()" << endl;
}
void f(int a)
{
 cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
 cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
 cout << "f(char b, int a)" << endl;
}
int main()
{
 Add(10, 20);
 Add(10.1, 20.2);
 f();
 f(10);
 f(10, 'a');
 f('a', 10);
 return 0;
}

C++支持函数重载的原理--名字修饰

 C语言不支持重载   链接时,直接用函数名去找地址,有同名函数,区分不开
 CPP如何支持的呢? 函数名修饰规则,名字中引入参数类型,各个编译器自己实现了一套

为什么 C++ 支持函数重载,而 C 语言不支持函数重载呢?
C/C++ 中,一个程序要运行起来,需要经历以下几个阶段: 预处理、编译、汇编、链接
 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载
注意:如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
法区分。

引用

引用概念

引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空
间,它和它引用的变量 共用同一块内存空间。
比如: 李逵 ,在家称为 " 铁牛 " ,江湖上人称 " 黑旋风 "
类型 & 引用变量名 ( 对象名 ) = 引用实体;
void TestRef()
{
	int a = 10;
	int& ra = a;//<====定义引用类型,ra就是a的别名
	printf("%d\n", a);
	printf("%d\n", ra);
}
注意: 引用类型 必须和引用 实体 同种类型

引用特性

1. 引用在 定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
int main()
{
	int a = 0;

	// 1、引用必须初始化
	//int& b;
	// b = c;

	// 2、引用定义后,不能改变指向
	int& b = a;
	int c = 2;
	b = c;  // 不是改变指向,而是赋值

	// 3、一个变量可以有多个引用,多个别名
	int& d = b;
    int& e=d;

	return 0;
}

常引用

int main()
{
	int a = 0;

	// 权限的缩小 --可以的
	const int& c = a;

	const int x = 10;
	int j = x;//因为这里是拷贝,j的改变不影响x,也不存在所谓的权限方法

	// 权限的放大 --不可以的      --指针和引用赋值才存在权限放大
	//int& y = x; //y的改变要影响x
	const int& y = x;

	const int& z = 10;//给常量取别名
	const int& m = a + x;//a+x表达式的返回值是临时对象(变量)。临时对象具有常性
	// 权限的放大 
	//int& n = a + x;

	double d = 1.1;
	int i = d;

	const int& ri = d;//临时变量具有常性
	//int& rd = d; // 该语句编译时会出错,类型不同
	return 0;
}

使用场景

 引用: 1、做参数(a、输出型参数 b、对象比较大,减少拷贝,提高效率)
 这些效果,指针也可以,但是引用更方便

void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}

 2、做返回值 (a、修改返回对象 b、减少拷贝提高效率)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int& Count()
{
	static int n = 0;
	n++;
	// ...
	return n;
}

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	cout << Count()<<endl;
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}

总结:

注意: 如果函数返回时,出了函数作用域,如果返回对象还在 ( 还没还给系统 ) ,则可以使用
引用返回,如果已经还给系统了,则必须使用(传值返回)(<--空间销毁前,用寄存器存储值)

传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直

接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

值和引用的作为传参类型的性能比较

值和引用的作为返回值类型的性能比较

通过上述代码的比较,发现 传值和引用在作为传参以及返回值类型上效率相差很大

引用和指针的区别

语法概念上 引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
底层实现上 实际是有空间的,因为 引用是按照指针方式来实现 的。
int main()
{
	int a = 10;
	int& ra = a;  // 语法不开空间,
	ra = 20;

	int* pa = &a; // 语法上开空间
	*pa = 20;

	return 0;
}

引用和指针的汇编代码对比:

引用和指针的不同点 :
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用 在定义时 必须初始化 ,指针没有要求
3. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何
一个同类型实体
4. 没有 NULL 引用 ,但有 NULL 指针
5. sizeof 中含义不同 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32
位平台下占 4 个字节 )
6. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全

内联函数

扩展:


// 频繁调用100w次,建立100w个栈帧
// c语言如何解决这个问题的?宏函数
//int Add(int a, int b)
//{
//	return a + b;
//}

// 容易犯错注意点:
// 1、不是函数
// 2、分号
// 3、括号控制优先级
// 核心点:宏是预处理阶段进行替换

#define ADD(a, b) ((a)+(b))

// 为什么要加里面的括号?
//因为a,b是表达式的话,表达式要整体替换,所以要加括号!!
int main()
{
	if (ADD(1, 2))
	{}

	ADD(1, 2) * 3;

	int x = 1, y = 2;
	ADD(x | y, x & y);  // (x|y + x&y)

	return 0;
}

// 宏的缺点:
// 1、语法复杂,坑很多,不容易控制
// 2、不能调试
// 3、没有类型安全的检查

概念

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

特性

1. inline 是一种 以空间换时间 的做法,如果编译器将函数当成内联函数处理,在 编译阶段,会
用函数体替换函数调用 ,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。
2. inline 对于编译器而言只是一个建议,不同编译器关于 inline 实现机制可能不同 ,一般建
议:将 函数规模较小 ( 即函数不是很长,具体没有准确的说法,取决于编译器内部实现 )
是递归、且频繁调用 的函数采用 inline 修饰,否则编译器会忽略 inline特性。
下图为《C++prime 》第五版关于 inline 的建议:
3. 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;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl 
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

auto关键字(C++11)

auto简介

在C++11中,auto关键字可以用来自动推断变量的类型,它在编译时会根据初始化表达式的类型来确定变量的类型。

使用auto的主要好处是可以简化代码并提高可读性。它可以减少手动指定变量类型的工作,并且可以防止类型错误。相比于显式指定变量类型,使用auto可以让代码更加灵活和易于维护。

int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
【注意】
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto
的实际类型 。因此 auto 并非是一种 类型 的声明,而是一个类型声明时的 占位符 ,编译器在编
译期会将 auto 替换为变量实际的类型

auto的使用细则

1.auto 与指针和引用结合起来使用
auto 声明指针类型时,用 auto auto* 没有任何区别,但用 auto 声明引用类型时则必须 &
int main()
{
    int x = 10;
    auto a = &x;
    auto* b = &x;
    auto& c = x;
    cout << typeid(a).name() << endl;
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    *a = 20;
    *b = 30;
     c = 40;
    return 0;
}
2.在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
void TestAuto()
{
    auto a = 1, b = 2; 
    auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

auto不能推导的场景

1. auto 不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2. auto 不能直接用来声明数组
void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};
}
3. 为了避免与 C++98 中的 auto 发生混淆, C++11 只保留了 auto 作为类型指示符的用法
4. auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有
lambda 表达式等进行配合使用。

基于范围的for循环(C++11)

范围for的语法

C++98 中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
     array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
     cout << *p << endl;
}
对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因
C++11 中引入了基于范围的 for 循环。 for 循环后的括号由冒号 分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
     e *= 2;
for(auto e : array)
     cout << e << " ";
return 0;
}
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环

范围for的使用条件

1. for 循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供
begin end 的方法, begin end 就是 for 循环迭代的范围。
注意:以下代码就有问题,因为 for 的范围不确定
void TestFor(int array[])
{
    for(auto& e : array)
        cout<< e <<endl;
}
2. 迭代的对象要实现 ++ == 的操作

指针空值nullptr(C++11)

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何

种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
 cout<<"f(int)"<<endl;
}
void f(int*)
{
 cout<<"f(int*)"<<endl;
}
int main()
{
 f(0);
 f(NULL);
 f((int*)NULL);
 return 0;
}

程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0 ,因此与程序的
初衷相悖。
C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器
默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void
*)0
注意:
1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr C++11 作为新关键字引入
2. C++11 中, sizeof(nullptr) sizeof((void*)0) 所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr
  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值