C++学习笔记——零散知识点记录

C++学习笔记——零散知识点记录

环境:ubuntu系统
IDE:visual studio code
语言:C++,CMake

只是个人的学习笔记,用于记录一些零散的知识点,如有错误,欢迎大佬指出纠正。
参考书籍:《视觉SLAM十四讲-从理论到实践》——高翔

1. CMakeLists.txt

编译与执行由VS进行,编译的build与运行的三角在最下面的蓝色横条上这也太不起眼了找了好久啊,VS终端的默认字体太丑了,需要参照网上教程修改一下 。

# 最低 c++ 版本要求
cmake_minimum_required( VERSION 2.8 )

# 创建项目
project( Hello )

# 添加一个运行程序
add_executable( hello hello.cpp )

# 添加一个库
add_library( hello libhello.cpp )

# 将执行程序与库链接
add_executable( useHello useHello.cpp )
target_link_libraries( useHello hello )

# 添加额外库的路径
# eigen作为只有头文件没有二进制文件的库不需要链接
include_directories("/usr/include/eigen3")

第三方库安装步骤:
1、cmake [path]
path为CMakeLists.txt所在的路径,中间文件将会生成在当前路径。
2、make
编译项目。
3、sudo make install
将编译好的第三方库安装到/usr/local//usr/local/include/路径下。

大型项目中使用的写法

# cmake最低版本
cmake_minimum_required(VERSION 3.10.2)
project(项目名)

# 添加.cpp文件路径
add_library(生成的库名 STATIC
    src/第1个CPP.cpp
    src/第2个CPP.cpp
)

# 添加.h文件路径
target_include_directories(生成的库名
    PUBLIC
    $<BUILD_INTERFACE: ${CMAKE_CURRENT_LIST_DIR}/include>
)

# 生成时需要导入的其它库
target_link_libraries(生成的库名

    # 第三方库
    yaml-cpp
    Eigen3::Eigen
    glog
    ${OpenCV_LIBS}

    # 自己的库
    自己的库1
)

# 其它cmake文件的导入路径
add_subdirectory(ThirdParty/xxx)

2. 输出函数cout, cerr

#include <iostream>
using namespace std;

int main(int argc, char **argv) {
    cout << "测试一下……" << "连接输出!" << endl;
    cout << "换行输出!" << endl;
    cout << "测试一下不换行" ;
    cout << "看看在哪里!" << endl;
    cout << "测试一下换3行"  << endl << endl << endl;
    cout << "看看在哪里!" << endl;

    // 控制cout输出数据精度,设置N位有效数字
    double i = 2.4321515151341;
    cout.precision(3);
    cout << i << endl;
    cout << i << endl;
    cout.precision(7);
    cout << i << endl;

    return 0;
}

输出为:

测试一下……连接输出!
换行输出!
测试一下不换行看看在哪里!
测试一下换3行


看看在哪里!
2.43
2.43
2.432152

结论:cout的换行取决于endl,作为换行符使用。
将cout换成cerr则输出错误信息,不经过缓冲直接输出到屏幕,用于快速报错输出,而且只能输出到显示器。

3. cmath模块(数学)

3.1 圆周率π

#include <iostream>
#include <cmath>
using namespace std;

int main(int argc, char **argv) {
    cout << M_PI << endl;
    return 0;
}

得到:

3.14159

虽然输出短,但是资料中表示实际精度为:

#define M_PI 3.14159265358979323846 

3.2 自然对数E

#define M_E  2.71828182845904523536

4. time, clock与sleep(休眠与计时)

<unistd.h>在这里类似python的time模块,能够用于计算程序的运行时间。

#include <iostream>
using namespace std;

// sleep, time的头文件
#include <unistd.h>
// clock的头文件
#include <ctime>

#define MATRIX_SIZE 50

int main(int argc, char **argv) {

    // 基于 <unistd.h>的计时,类似python的time模块
    // 通过两个时间戳的相减来获得持续时间
    // 持续时间受到sleep的影响

    // usleep,微秒级休眠
    double start_time = time(NULL);
    usleep(1E6);
    cout <<  "<unistd.h> time of usleep is " << (double)difftime(time(NULL), start_time)  << "s" << endl;


    // sleep,秒级休眠
    start_time = time(NULL);
    sleep(3);
    cout <<  "<unistd.h> time of sleep is " << (double)difftime(time(NULL), start_time) << "s" << endl;

    // 基于ctime的时钟周期计时,被用于检测算法的运行时间从而对比算法效率
     // 除CLOCKS_PER_SEC,即用硬件的时钟周期数除以每秒的时钟周期数,最大精度为毫秒
     // 计算时间不会受到sleep影响,原因未知

    // usleep,微秒级休眠
     clock_t time_stt = clock();
    usleep(1E6);
    cout <<  "<ctime> time of usleep is " << 1000 * (clock() - time_stt) / (double) CLOCKS_PER_SEC << "ms" << endl;

    // sleep,秒级休眠
    time_stt = clock();
    sleep(3);
    cout <<  "<ctime> time of sleep is " << 1000 * (clock() - time_stt) / (double) CLOCKS_PER_SEC << "ms" << endl;

    return 0;
}

5. 语法笔记

5.1 size_t

一种无符号整数类型,为了增加可移植性而设计。
能够自动扩展到当前运行环境下最大可能出现的对象大小。
例子:

for (size_t y = 0; y < image.rows; y++) {}

5.2 std::vectors

优点:连续存储可以用偏移访问,高效访问元素,高效从末尾添加或者删除
缺点:不适合在末尾以外的位置添加或者删除元素

5.4 std::minmax_element

返回由第一个最小值和最后一个最大值的迭代器构成的一个pair
输入开头与结尾,然后输入比较规则。

auto min_max = minmax_element(matches.begin(), matches.end(), [](const DMatch &m1, const DMatch &m2) { return m1.distance < m2.distance; });
double min_dist = min_max.first->distance;
double max_dist = min_max.second->distance;

5.5 void*

一个能够用于接受任意类型指针的万能指针,但是不确定数据类型无法使用,接收该类型参数后,必须按照正确的指针类型转换后,才可以被用于功能模块运算。

6. C++中的lambda匿名函数[](){}

此处引用来源:C++ [](){}

总体结构分为三部分:
1、捕获说明[]

[]不截取任何变量
[&} 截取所有外部作用域变量,作为引用使用
[=] 截取所有外部作用域变量,拷贝之后使用
[=, &a] 截取所有外部作用域变量,拷贝之后使用,而&标记的变量使用引用
[a] 截取变量a拷贝之后使用,不截取其他变量
[a, &b] x按值传递,y按引用传递
[this] 截取当前类中的this指针,&或者=默认添加此选项

2、参数列表()
函数的输入参数。
3、函数体{}
函数结构与返回部分。

7. 工程实现框架

根目录为工程文件夹路径,小型算法库的普遍分类法:

|-c_plus_project
    |-include
        |-myproject
            |-存放所有.h头文件,引用时写include "myproject/xxx.h"
    |-app
    |-src
        |-存放.cpp的源代码文件
    |-bin
        |-存放编译的二进制文件
    |-config
        |-存放配置文件
    |-test
        |-存放测试文件
    |-cmake_modules
        |-存放第三方库的cmake文件
    |-CMakeLists.txt

8. public,protected,friendly,private

可被访问的范围:

public:当前类 当前包 子孙类 其他包
protected:当前类 当前包 子孙类
friendly(不写时默认):当前类 当前包
private:当前类

9. 运算符重载operator

使用operator为特殊类型自定义运算符的计算方式。
两个类之间的运算符重载就太多了,网上有很多的写法,这里记录一下基本数据类型的重载运算。

直接重载基本类型会报错,提示需要是一个类类型或者枚举类型,可能是出于安全性或者一些未知的考虑不允许这么做,因此需要把左侧的参数转换为一个类的类型再进行运算符重载。

自己写的样例非常丑 :重载char与int的加减法,使得char在加减int时操作在ascii码上进行并返回一个char类型的返回值。

// 运算符重载
#include <iostream>
#include <string>

using namespace std;

class Char
{
public:
    char operator+(const int& i){
        return (char)((int)this->char_value + i);
    }

    char operator-(const int& i){
        return (char)((int)this->char_value - i);
    }

    char char_value;
};

int main(int argc, char **argv) {\

    string n_input;
    getline(cin, n_input);
    int n = atoi(n_input.c_str());
    string str;
    getline(cin, str);

    cout << n <<endl;
    cout << str << endl;

    Char *p=new Char(); 

    p->char_value = 's';

    cout << *p + 2 << endl;
    cout << *p - 2 << endl;


    return 0;
}

输入

5
abcde

运行得到

bzfzj

向导师请教后,得知应该确实没法直接重载基本类型之间的运算符,需要转换为类来进行运算,但是这又涉及到一个运行效率的问题。

's'+2='u'的重载加法运算符为例,导师演示了两种写法,核心区别在于类当中的数据是直接存储char c_;还是引用const char& c_;,两种写法各有优劣。

写法1:将字符变量复制到类的内存空间进行运算
优点:可以使用临时字符变量’s’直接创建类的实例
缺点:需要复制字符变量内存,占用了额外存储空间开销

// 运算符重载,'s'可使用临时变量
#include <iostream>
#include <string>

using namespace std;

class Char
{
    public:
    Char(const char& c) :c_(c) {};
    const char& Data() const { return c_;}
    private:
    // 类使用赋值过来的内存在自身内存空间中储存数据
    char c_;
};

char operator+(const Char& j,  const int& i)
{
        return (char)((int)(j.Data()) + i);
}

int main(int argc, char **argv) {

    Char p= 's';

    cout << p + 2 << endl;

    return 0;
}

写法2:将字符变量引用到类中进行运算
优点:直接引用原本数据的内存地址,类本身几乎没有额外运行开销
缺点:必须用一个变量储存临时字符变量’s’,否则类进行运算时,临时变量已被销毁,会产生未知的结果

// 运算符重载,'s'无法使用临时变量
#include <iostream>
#include <string>

using namespace std;

class Char
{
    public:
    Char(const char& c) :c_(c) {};
    const char& Data() const { return c_;}
    private:
    // 类自身不储存变量,只使用引用
    const char& c_;
};

char operator+(const Char& j,  const int& i)
{
        return (char)((int)(j.Data()) + i);
}

int main(int argc, char **argv) {\
    // 这里必须用一个变量作为存储空间储存's'!
    // 否则会出现未知的结果!
    char s = 's';
    Char p= s;

    cout << p + 2 << endl;

    return 0;
}

10. 标准输入cin与getline()

cin输入一行中多个元素,元素间以空格分开,不会记录换行符
样例:

#include <iostream>
#include <string>
#include <list>

using namespace std;

int main(int argc, char **argv) {
    int n, m;
    cin >> n >> m;
    return 0;
}

getline()一次获取一行作为string,不会记录换行符
样例:

#include <iostream>
#include <string>

using namespace std;

int main(int argc, char **argv) {\

    // 获取输入
    string n_input;
    // getline没有读换行符
    getline(cin, n_input);
    int n = atoi(n_input.c_str());
    string str;
    getline(cin, str);
    return 0;
}

11. 格式化输出setw()

参数表示宽度,用数字表示。
setw() 函数只对紧接着的输出产生作用。
当后面紧跟着的输出字段长度小于 n 的时候,在该字段前面用空格补齐,当输出字段长度大于 n 时,全部整体输出。
此处转自:菜鸟教程C++ setw() 函数

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    // 开头设置宽度为 4,后面的 runoob 字符长度大于 4,所以不起作用
    cout << setw(4) << "runoob" << endl;
    // 中间位置设置宽度为 4,后面的 runoob 字符长度大于 4,所以不起作用
    cout << "runoob" << setw(4) << "runoob" << endl;
    // 开头设置间距为 14,后面 runoob 字符数为6,前面补充 8 个空格
    cout << setw(14) << "runoob" << endl;
    // 中间位置设置间距为 14 ,后面 runoob 字符数为6,前面补充 8 个空格
    cout << "runoob" << setw(14) << "runoob" << endl;
    return 0;
}

12. auto与decltype

详见:c++ auto关键字使用
auto:根据值的类型自动为变量分配类型

decltype:根据一个变量的类型来为另一个变量分配类型

#include <iostream>

using namespace std;

int main ()
{
    auto a = 5;
    cout << a << endl;

    decltype(a) b = 7;
    cout << b << endl;

   return 0;

}

运行得到:

5
7

13. 将数据存储到磁盘和从磁盘读取

将std::vector<cv::Point2f>类型的数据存储到本地:

// 保存特征点
ostringstream out1;
// 将vector中的<cv::Point2f>转换为字符串储存
for (auto p = cur_pts.begin(); p != cur_pts.end(); p++) {
     out1 << to_string((*p).x) << " " <<  to_string((*p).y) << " " << endl;
 }
// 写入文件
ofstream fout1(save_path + "cur_pts.txt");
if (fout1) {
     //将out流转换为string类型,写入到文件流中
     fout1 << out1.str() << endl;
     fout1.close();
}

反向操作,从磁盘读取并还原为原本的数据类型:

#include <iostream>
#include <string>
#include <fstream> 
#include <vector>

#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char **argv) {
    string save_path = "存储目录";
    // 读特征点
    string temp; 
    int pos;
    float x;
    float y;

    ifstream cur_pts_file(save_path + "cur_pts.txt"); 
    vector<cv::Point2f > cur_pts;
    if (!cur_pts_file.is_open()) 
    { 
        cout << "未成功打开文件cur_pts.txt" << endl; 
    } 
    while(getline(cur_pts_file, temp)) 
    { 
        if (temp.length() != 0) {
            // 获得字符串
            // 找到分割的空格位置
            pos = temp.find(" "); 
            // x坐标
            x = stof(temp.substr (0, pos));
            // y坐标
            y = stof(temp.substr (pos+1, temp.length()-pos-1));
            cur_pts.push_back(Point2f(x, y));
        }
    } 
    cur_pts_file.close(); 

    // 打印行数进行验证
    cout << cur_pts.size() << endl;

    return 0; 
} 

14. ++i与i++

++i:先对i+1再返回当前值
i++:先返回当前值再对i+1

在不影响功能的场合,尽量使用前置自增++i,因为i++会对i进行一次复制创建临时副本,如果i是迭代器之类的复杂数据,可能会对效率有很大的影响。

15. 临时变量

在【int i;与之后的程序块】的外面再套一个大括号,可以让i成为临时变量。

16. continue,for,i++

在for循环中,使用continue中止循环,会执行for中的i++
使用list的p=list.erase(p);也相当于执行了一次p++
会导致直接跳过了两个
break后不会执行i++

17. 以类似数组方式传递数据头部指针

适用于vector,Matrix等
传入a.data()

18. for (auto &a : vec)

使用

 for (auto &a : vec)

for (auto a = vec.begin(); a != vec.end(); a++)
#endif

效率更高

19. std::string转数字

float a = std::atof(std_string.c_str());
int b = std::atoi(std_string.c_str());

20. 从路径中分隔出末尾的文件名

int pos=path.find_last_of('/');
std::string img_name(path.substr(pos+1));

21. 将double转换为保留3位小数的字符串

std::string doubleToString(const double &val)
{
    char* chCode;
    chCode = new char[20];
    // 保留多少位小数
    sprintf(chCode, "%.3lf", val);
    std::string str(chCode);
    delete[]chCode;
    return str;
}

22. 从磁盘读取文本文件和bin二进制文件中的数字矩阵

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <Eigen/Core>
#include <Eigen/Dense>

ifstream logfile;
std::string timestamp;
// 打开记录文件名的txt
logfile.open("log.txt");
// 获取不带换行符的每一个文件名内容
while (getline(logfile, filename))
{
    // 读取磁盘bin数字矩阵文件的流
    ifstream datafile;
    // 打开其中一帧的点云文件
    datafile.open(filename + ".bin");
    // 点云
    std::vector<Vector3d> point_cloud;

    // 读取不确定长度的bin点云文件
    while (!datafile.eof())
    {
        // 单个3d点
        Vector3d point;
        // 从bin文件中读取单个3d点
        datafile.read((char *)(&point), sizeof(point));
        // 存入点云列表
        point_cloud.push_back(point);
    }
    // 关闭bin数据文件
    datafile.close();

}
// 关闭记录文件
logfile.close();

23. 头文件顶部和底部的宏定义

假设A.h为:

#ifndef A
#define A
...
#endif

假设B.h导入A.h为:

#ifndef B
#define B

#include "A.h"
...
#endif

假设C.h同时导入A和B

#include "A.h"
#include "B.h"

此时B中A的导入复制部分会因为宏定义不运行,从而避免A中的内容被重复运行2次后出现重复定义错误。

24. 使用共享指针创建新的类

std::shared_ptr<ClassA> new_class(new ClassA());

25. 程序休眠

#include <thread>

using namespace std::chrono;

std::this_thread::sleep_for(10s);
std::this_thread::sleep_for(10ms);
std::this_thread::sleep_for(10us);

26. cmake的debug模式

编译命令

rm -rf build
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make -j4
cd ..

vscode的launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(gdb) Launch",
            "type": "cppdbg",
            "request": "launch",
            "program": "执行程序路径",
            "args": [
                "参数1",
                "参数2",
            ],
            "stopAtEntry": false,
            "cwd": "${workspaceRoot}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "Set Disassembly Flavor to Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ]
        }
    ]
}

然后在VScode中按F5启动调试模式运行,查看左下角堆栈中的崩溃点。

27. 动态库编译与静态库编译

Cmake语法

# 静态库
add_library(xxx STATIC 
...
# 动态库
add_library(xxx SHARED
...

28. 遍历文件夹中的文件和文件夹

#include <iostream>
#include <filesystem>


std::string path = "test/";
// 如果目录不存在
if (!filesystem::exists(path))
{
    return -1;
}
// 获取开头和结尾标记
auto begin = filesystem::recursive_directory_iterator(path);
auto end = filesystem::recursive_directory_iterator();

for (auto it = begin; it != end; it++)
{
    auto &entry = *it;
    // 文件
    if (filesystem::is_regular_file(entry))
    {
        cout << entry.path() << endl;
    }
    // 目录
    else if (filesystem::is_directory(entry))
    {
        cout << entry.path() << endl << endl;
    }
}

29. 运算计时

 double dbStart = (double)cv::getTickCount();
Run();
double dbEnd = (double)cv::getTickCount();
std::cout << "Time Cost: " << (dbEnd - dbStart) * 1000.0 / cv::getTickFrequency() << std::endl;

30. 清理与写日志

// 用于清理运行日志
void clear_log() { remove("log.txt"); }

// 将数据写入日志文件
void write_log(string text)
{
    std::ofstream file;
    file.open("log.txt", ios::app);
    file << text;
    file.close();
}

31. C++ 内存中在堆和栈上创建对象

C++ 内存中的堆和栈,在堆和栈上创建对象,new、delete

32. 内存检测

linux内存检测工具valgrind
使用参考:
linux下内存泄露检测工具介绍
Linux下几款C++程序中的内存泄露检查工具

安装

sudo apt-get install valgrind

使用

valgrind --tool=memcheck --leak-check=full --show-reachable=yes --trace-children=yes --log-file=memory_leak.log [执行程序名] [命令行参数]

注:使用该工具进行内存检测时需要编译为debug版本,即使用:

cmake -DCMAKE_BUILD_TYPE=Debug ..

对于release版本编译的库,在检测结果中会显示为???,例如调用的第三方库检测到错误。

33. emplace_back比push_back更加高效

使用emplace_backvector对象添加元素到末尾,比push_back更加高效,而效果完全相同。
缺点是emplace_back适用于高版本,低版本缺少支持,因此代码向下兼容可能会出问题。

34. 宏定义配置需要编译的代码

在执行build命令时:

cmake .. -DTEST_DEBUG=1

在cmakelist文件末尾加上:

# 设置为ON时,在cmake命令后加入-DTEST_DEBUG=1启用debug模式
option(TEST_DEBUG "option for debug" ON)
if (TEST_DEBUG) 
	add_definitions(-DTEST_DEBUG)
endif(TEST_DEBUG)

注:add_definitions()中宏定义必须以-D开头,实际上变量名没有这个D

实际c++代码里面:

#ifdef TEST_DEBUG
    // 代码
#endif

35. 直接用输入参数为成员变量赋值

Class1::Class1(int a, double b, int c)
    :  a_(a), b_(b), c_(c)

36. 为成员初始化有参数的构造函数

假设类A中有个成员b_是类B,而类B没有无参数构造函数
所以需要在A的构造函数中

A::A() : b_(input) 
{}

而不能是

A::A() 
{b_ = B(input);}

37. 在C/C++混合代码中出现内存越界错误

在C/C++混合代码中出现内存越界错误,但是报错的代码段又看不出问题。
错误笔记:在更之前的地方,C代码写内存越界导致整个程序已经崩溃了,而且C没有报错。

38. C数据结构的字节对齐

用于跨设备传输时设置好内存对齐

#pragma pack(push) //保存对齐状态 
#pragma pack(1)//设定为1字节对齐 
struct test 
{ 
char m1; 
double m4; 
int m3; 
}; 

struct test2 
{ 
char m1; 
double m4; 
int m3; 
}; 
#pragma pack(pop)//恢复对齐状态

39. API头文件设置为纯C结构

#ifdef __cplusplus
extern "C"
{
#endif
// 代码内容
#ifdef __cplusplus
}
#endif

40. 智能指针

无需自行delete释放的指针

#include <memory>

std::shared_ptr<TypeName> point = std::make_shared<TypeName>();

41. 枚举类型赋值时转换

为枚举类型赋值时,先强制转换为该变量的类型,而不是直接用int赋值。

a = static_cast<枚举类型名>(变量名);

42. std::any匹配任何类型

使用std::any可以匹配任何参数类型,类似于void指针。

43. 读取raw文件

// 测试读取用的数据,类型为CV_32FC3
std::string file_path = "xxx.raw";

std::ifstream fin;
// 指定binary读取模式
fin.open(file_path, std::ios::binary);
if (!fin)
{
    std::cerr << "open failed: " << file_path << std::endl;
}
// seek函数会把标记移动到输入流的结尾
fin.seekg(0, fin.end);
// tell会告知整个输入流(从开头到标记)的字节数量
int length = fin.tellg();
// 再把标记移动到流的开始位置
fin.seekg(0, fin.beg);
std::cout << "file length: " << length << std::endl;
// load buffer
char *buffer = new char[length];
// read函数读取(拷贝)流中的length各字节到buffer
fin.read(buffer, length);

// 重新转换为float32类型
float *img_buffer = (float *)buffer;

// 图像的数据
int width = 320;
int height = 320;
int channel = 1;

// 查看图像
cv::Mat image(cv::Size(width, height), CV_32FC1, img_buffer);
cv::imshow("test", image);
cv::waitKey();

44. 遍历文件夹(基于opencv)

// c++遍历文件夹
std::vector<cv::String> vcPath;
vcPath.clear();
cv::glob(input_data_path, vcPath, false);

45. 读取yaml(基于opencv)

#include <opencv2/core.hpp>
// opencv读取yaml配置文件
cv::FileStorage fs_setting(config_path, cv::FileStorage::READ);
std::string data = static_cast<std::string>(fs_setting["node_name"]);

46. c++创建文件夹

if (!std::filesystem::exists(fpath))
    {
        std::filesystem::create_directories(fpath);
    }
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值