C++ Primer中小细节 章节三:C++标准库

IO类型:

头文件:
流读取写入:iostream
文件读取写入:fstream
string读取写入:sstream

io类型不可拷贝和赋值,因此无法用于形参或返回类型,而通常以引用方式传递。读写一个io对象会改变其状态,因此不能用const。

文件io:
ifstream in(infile);
ofstream out;
out.open(outfile, ofstream::app);   //默认以out方式,可指定app以追加方式打开
out.is_open();   //  或者 if(out) 检测状态
out.close();  // out对象被销毁时会自动调用close
string io:
istringstream record("name phone");
record >> name;   //读取
while(record >> phone){   }

ostringstream outformat;
outformat << " " << name;  //写入
outformat.str()  //返回保存字符串的拷贝

io状态:

io的状态由多个位来表示
检测:

while( cin >> word ) {}
s.eof()    // 结束
s.fail()   // fail(IO操作失败) 或 bad位被置位
s.bad()    // bad位(流崩溃)被置位
s.good()   //完好
cin.clear(cin.rdstate() & ~cin.failbit)       //  无参对指定位复位,有参更新状态(复位failbit,其他位不变)
s.setstate(iostate)    // 对指定位置位
s.rdstate()            // 返回当前状态

刷新缓冲区:

endl  换行+刷新
flush 直接刷新
ends  空字符+刷新

cout<<unitbuf;  //所有输出操作都会刷新
cout<<nounitbuf;  //恢复到正常情况

关联流:

标准库将cin和cout关联一起,任何读取操作都会先刷新关联的输出流。

cin.tie(&cout)  //输入关联输出
cin.tie()  //返回关联的输出流指针,无则返回空指针

顺序容器:

vector  可变大小数组,随机访问,尾部插入删除快,其他很慢
deque  双端队列,随机访问,头尾插入删除很快,中间较慢
list  双向链表,双向顺序访问,任何位置插入删除很快
forward_list  单向链表,单向顺序访问,任何位置插入删除很快
array  固定大小数组,随机访问,不能添加删除元素
string  类似于vector,随机访问,尾部插入删除快,其他很慢

1、优先选择vector。除了array固定长度,其他容器都可以扩展大小。
2、string、vector存在连续空间,插入删除都要移动后续元素,适合随机访问,中部操作不频繁;list、forward_list访问元素是只能遍历整个容器,适合中部操作频繁。
3、forward_list没有size,在保存和计算大小时消耗大。
4、如果既需要中部插入,又需要随机访问,则需测试性能考虑占据主导的操作。

如果程序在输入时频繁操作中部,随后只是随机访问,那么:
1、是否真的需要中间位置操作,可以先尾部插入,然后sort排序;
2、如果必须在中间插入,先使用list,然后拷贝到vector中。

vector<vector<int>> nums;
vector<nodefault> v1(10, init);   //初始化时nodefault没有默认构造函数,必须传入init。

容器通用操作:

在这里插入图片描述
类型别名允许在不关注具体类型的情况下使用它。list<string>::iterator iter;
在这里插入图片描述

vector<const char*> ar = {"A", "B"};
vector<string> wo(ar);  //错误:拷贝必须类型相同
forward_list<string> wo(ar.begin(), ar.end());  //正确:传递范围进行拷贝。wo和ar类型可以不同,元素类型也可不同,只需拷贝时进行类型转化

在这里插入图片描述

// array中大小是类型的一部分
array<string, 2> a;  //2个默认初始化的string
array<string, 2> a = {"A", "B"};
a = {"C"}; //错误:不能将一个花括号列表赋予array
array<string, 2> a = {"A"}; //a[0]为A,其余空字符串

在这里插入图片描述

list<string> names;
vector<const *char> old;
names = old;  //错误:必须类型相同
names.assign(old.begin(), old.end());  //正确:类似于范围初始化

swap:
1、除array外,swap不会对任何元素使用修改,插入,删除,因此保证在常数时间内完成。而array则会真正的交换元素。
2、不会交换元素意味着元素的迭代器、引用、指针仍然指向之前的元素,不过所属容器改变了(除了string以外,string会使其失效)。而array在swap后迭代器、引用、指针绑定元素保持不变,但是元素值已经改变。
3、统一使用非成员swap,在泛型编程中很重要。

在这里插入图片描述
在这里插入图片描述
1、比较运算类似于string的比较,因为是逐元素比较,因此需要元素定义相应的比较运算。
在这里插入图片描述

//显式定义
list<string>::iterator it = a.begin();
list<string>::const_iterator it = a.begin();
//以c开头是为了集合auto使用begin和end
auto t = a.begin();  //是否是const与a的类型有关,当a为const,t才为const
auto t = a.cbegin(); //肯定是const的 

在这里插入图片描述

顺序容器特有操作:

在这里插入图片描述
当使用一个对象来初始化或者将一个对象插入容器,其实质上是放入了对象值得一个拷贝,而不是元素本身。

while(cin >> word){
	iter = names.insert(iter, word);
}

emplace和push区别:
emplace对应于push的相关操作,不过emplace是直接利用参数在容器的内存位置构造元素,而push是将构造好的元素拷贝到容器的内存位置,多了一个拷贝的操作。

c.emplace_back("131", 25);   //正确,emplace会使用这些参数构造Sales_Data对象
c.push_back("131", 25);  //错误:push操作不支持
c.push_back(Sales_Data("131", 25));  //正确:push操作会将这个对象拷贝到容器里

//emplace的参数类型和元素类型的构造函数相同
c.emplace_back();  //默认构造函数
c.emplace(iter, "131");  //Sales_Data("131")构造函数
std::string s = "abc";
std::vector<std::string> vec;
vec.push_back(s);            // copied
vec.push_back(std::move(s)); // good, moved in
vec.emplace_back("abc");     // good, constructed in place
vec.emplace_back(s);         // copied
vec.emplace_back(10, 'a');   // constructed as "aaaaaaaaaa"

在这里插入图片描述

front、back、下标、at返回的都是引用。如果容器为const,返回的也是const
引用。
c.front() = 24;
下标的index不会检查,而at超出范围会出现out of range错误。
vec[0];
vec.at(0);

在这里插入图片描述
1、删除函数并不检查参数,在删除之前需要检查元素是否存在。

slist.clear();
slist.erase(slist.begin(), slist.end());

在这里插入图片描述

forward_list:

forward_list删除当前元素时需要知道前驱以能够将前驱链接起来。为此提供的操作为:
在这里插入图片描述

// 删除奇数
auto prev = flist.before_begin();
auto curr= flist.begin();
while(curr != flist.end()){
	if(*curr % 2){
		curr = flist.erase_after(prev);
	}else{
		prev = curr;
		++curr;
	}
}

迭代器失效:

向容器添加、删除元素时都有可能导致容器的指针、引用、迭代器失效。
在这里插入图片描述
在这里插入图片描述
添加、删除vector、string或deque元素都需要考虑迭代器、引用、指针的失效问题。因为end返回的迭代器在添加或删除后总是会失效,因此不要在循环前保存end返回的迭代器,且C++中end操作很快。

auto iter = vi.begin();
while(iter != vi.end()){
	if(*iter % 2){
		iter = vi.insert(iter, *iter);  //跳过插入元素和当前元素
		iter += 2;
	}else{
		iter = vi.erase(iter);  //删除直接移动到下一元素
	}
}

vectore对象的增长:

在这里插入图片描述
resize只改变容器中元素的数量,而不会改变容器的容量;
reserve只有需要容量超过当前容量的时候,才会改变容器的容量;

在这里插入图片描述

size指的是容器中已保存元素的数量;
capacity指的是不分配新内存空间的情况下,容器能够容纳的元素数量;
只要操作没有超过容器的容量,容器就不会出现分配内存空间。
只有在执行insert操作时size和capacity相等、或者resize、reserve时给定大小超过当前capacity时,vector才能重新分配内存。

额外string操作:

初始化:
在这里插入图片描述
在这里插入图片描述
裁剪:
在这里插入图片描述
其他:
1、新字符可以来自于另一个string、字符指针、花括号包围的字符列表、一个字符+一个计数值。当字符来自于一个string或字符指针,可以传入额外参数控制拷贝所有还是部分。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
搜索:
1、搜索返回string::size_type类型(unsigned),搜索失败返回static的string::npos,npos类型为const size_type(unsigned),并且初始化值为-1。因此使用int或者其他带符号类型保存返回值是不好的。
在这里插入图片描述
在这里插入图片描述
比较:
在这里插入图片描述
数值转换:
在这里插入图片描述

// 第一个非空白符需为+-或数字,浮点数可以包含.Ee,整型根据基数可以包含字母
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));

容器适配器:stack、queue、priority_queue

容器适配器根据已有容器使其表现出另一种类型。
在这里插入图片描述
默认stack和queue基于deque实现;priority_queue基于vector实现

stack<string, vector<string>> stk;
stack<string, vector<string>> stk(vec);

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

关联容器:

关联容器是以关键字来保存和访问的。
在这里插入图片描述
在这里插入图片描述
1、value_type为pair,可以改变pair的值,但不能改变关键字;
2、auto it = map.begin(), *it为pair的引用,it->first打印key;
3、set的iterator和const_iterator都是修改关键字;
4、有序容器迭代器按照关键字升序进行遍历;
5、通常不能对关联容器使用泛型算法,因为关键字的const使得不能将关联容器传给修改或重排容器元素的算法,因为这类算法需要向元素写入值。关联容器能够使用只读取元素的算法,但大多数算法都需要搜索序列,而关联容器不能通过关键字进行快速查找,因此使用泛型搜索算法几乎是个坏主意。如果需要对关联容器使用算法:将关联容器当作源序列,使用copy泛型算法拷贝到另一个序列;可以调用inserter将一个插入器绑入关联容器,通过使用inserter将关联容器当作一个目的位置来调用另一个算法。
初始化:

set<string> ss = {"A", "B"};
map<string, string> sm = { {1, "A"}, {2, "B"} };

关键字类型:
1、有序容器需要关键字严格弱序;
2、如果元素没有定义比较函数,可以传入比较函数来比较;

multiset<Sales_Data, decltype(compare)*>  bookstore(compare);

在这里插入图片描述

set2.insert(vec.cbegin(), vec.cend());
set2.insert({1, 2, 3});
map2.insert({key, 1});
map2.insert(make_pair(key, 1));
map2.insert(pair<string, size_t>(key, 1));
auto ret = map2.insert(map<string, size_t>::value_type(key, 1));
// ret类型:pair<map<string, size_t>::iterator, bool>

1、insert返回值依赖于容器类型和参数。对于不包含重叠关键字的容器,insert和emplace返回一个pair,first为一个迭代器,指向给定关键字元素,second输出是否插入成功,如果容器已存在该关键字则为false。对于允许重叠关键字的容器,直接返回指向新元素的迭代器。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
1、对于map,使用下标索引如果关键字未在map之中,会自动插入,但如果我们不想插入,可以使用find。
2、对于multimap的查找,有多个相同关键字时则会相邻存储。

// 统计数量查找
auto be = find();
auto num = count();
while(num){
	++be;
	--num;
}

// 使用bound
// lower_bound指向第一个key位置,如果不存在,将会指向第一个关键字大于key的位置,有可能是尾后迭代器。
// upper_bound指向最后一个key元素之后的元素位置,如果不存在将会指向关键字的插入位置。
for(auto be = lower_bound(), end = upper_bound(); beg != end; ++beg){
	beg->second;
}

//使用equal_range返回迭代器pair
for (auto pos = equal_range(key); pos.first != pos.end; ++pos.first){
	pos.first->second;
}

无序容器:

1、使用哈希函数和关键字类型==运算符来组织元素。
2、无序容器在内存组织上为一组桶,使用哈希函数将一个元素映射到对应的桶。如果允许重复关键字,那么所有相同元素将会保存在同一个桶。因此无序容器性能依赖于哈希函数的质量和桶的数量、大小。
在这里插入图片描述
如果使用自定义类作为关键字,那么需要实现hash和等于算法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Pair类型:

在这里插入图片描述

泛型算法:服务于容器

1、大多数定义在头文件algorithm中,部分在numeric里;
2、通常情况下这些算法并不是直接应用于容器,而是在两个迭代器范围内遍历。由此导致算法不会执行容器的操作(添加、删除),而只是运行在迭代器之上(移动、改变值)。
3、迭代器导致这些算法不依赖容器,但是算法依赖于元素类型的操作,比如等于;

只读算法:
1、只读最好使用const迭代器;
2、用一个单一迭代器访问容器,都表示算法假设第二个序列长度至少比第一个序列一样长,而这需要程序员来保证。如equal函数;

查找函数 find
find_if(w.begin(), w.end(), func);   //根据条件查找

计数函数 count

累计函数 accumulate
string sum = accumulate(v.cbegin(), v.cend(), string(""));  //注意显式的构造空字符串,因为此类型决定了使用哪个+运算符。如果使用字面""就会出错,因为const char*没有+运算符。

相等函数 equal
equal(v1.cbegin(), v1.cend(), v2.cbegin()); //equal假设二者一样长,不要求容器类型相同(基于迭代器)、元素类型相同(基于==)

写算法:

填充函数 fill

填充函数 fill_n
fill_n(vec.begin(), 10, 0);  //需要保证vec至少含有10个元素,否则结果未定义。
fill_n(back_inserter(vec), 10, 0);

back_inserter:插入迭代器,一种向容器中添加元素的迭代器。头文件iterator中
auto it = back_inserter(vec);  //通过向迭代器赋值,迭代器在vec上调用push_back
*it = 42;

拷贝函数 copy
auto ret = copy(begin(arr), end(arr), arr2);  //将arr内容拷贝到arr2,arr为数组类型,ret指向拷贝到arr2尾元素之后的位置

替换函数 replace
replace(ilist.begin(), ilist.end(), 0, 42);  //将0替换为42
replace_copy(ilist.cbegin(), ilist.cend(), back_inserter(vec), 0, 42);  //保留原序列不变,

重排算法:

排序函数 sort

消重函数 unique
sort(w.begin(), w.end()); //先排序,方便查找重复
auto end_unique = unique(w.begin(), w.end());  //相邻重复消除,返回指向不重复范围末尾的迭代器
w.erase(end_unique, w.end());  //泛型算法运行于迭代器上并不能删除元素。即使w没有重复,该操作也是安全的。

定制操作:
sort函数的第三参数即称为谓词。接受一个参数为一元谓词(unary predicate),接受两参数称为二元谓词(binary predicate)。

bool shorter(const string &s1, const string &s2){
	return s1.size() < s2.size();
}
sort(w.begin(), w.end(), shorter);

lambda表达式:

1、可调用对象:函数、函数指针、类重载运算符、lambda表达式。
2、当定义一个lambda时,编译器生成了一个与lambda对应的新的类类型。在向函数传递lambda时,同时定义了新类型和该类型的一个对象,传递的参数就是生成的未命名对象。当使用auto定义一个lambda初始化的变量时定义了从lambda类型生成的对象。
3、默认情况下从lambda生成的类都包含了该lambda所捕获的变量的数据成员,类似于普通类的数据成员。这些数据成员在lambda对象被创建时被初始化。
4、可以函数返回lambda,但是该lambda不能包含引用捕获(函数内局部变量会被销毁)。

[capture list] (parameter list) -> return type {function body}
capture list:捕获列表,lambda所在函数中定义的局部变量的列表
return tpye:返回类型,lambda必须使用尾置返回
可以忽略参数列表和返回类型,但必须包含捕获列表和函数体
auto f = [] {return 42};  //如果只有一个return,则返回类型可以推断出,如果有多个则返回类型为void
cout<<f();

传递参数
[] (const string &s1) {return s1.size();}    //lambda不能有默认参数
for_each(w.begin(), w.end(), [](const string &s1){cout<<s<<" ";});

捕获所在函数中的局部变量,捕获列表可以使用局部非static变量,
lambda可以直接使用局部static变量和在它所在函数之外声明的名字。
[sz] (const string &s1) {return s1.size()<sz;}  //lambda不能使用未在捕获列表中的局部变量,但是可以使用处于当前函数体之外的名字,如cout。

捕获普通变量(int、string)可以采用值捕获,捕获指针或迭代器或引用捕获需保证对象值符合期望。避免捕获指针或引用。

值捕获:前提是对象可拷贝,在lambda创建时拷贝
[v1]

引用捕获:需确保在lambda执行时引用对象一直存在。如ostream对象不可拷贝。
[&os]

隐式捕获:
[&]  //内部变量引用捕获
[=]  //内部变量值捕获
[&, c]  //混合捕获,必须&或=在前,且隐式捕获和显式捕获方式必须不同,&之后变量只能值捕获
[=, &os]  //混合捕获,必须&或=在前,且隐式捕获和显式捕获方式必须不同,=之后变量只能引用捕获

默认情况下,lambda不能修改其捕获的拷贝变量,如果修改,可使用mutable

size_t v1 = 42;
auto f = [v1] () mutable {return ++v1;} 
v1 = 0;
auto j = f();  // j=43,与lambda外v1无关。

引用捕获变量能否修改依赖于该引用指向变量是否const
size_t v1 = 42;
auto f = [&v1] () mutable {return ++v1;} 
v1 = 0;
auto j = f();  // j=1,在调用之前v1值变了。
指定返回类型
[](int i) -> int {if(i<0) return -i else return i;} 

参数绑定:
lambda用于简单函数的替代,当重复调用较多时,函数仍是较佳选择。不过存在的问题时lambda可以通过捕获的方式获得当前所处函数变量。而函数需要采用绑定的方式才能获得。

// bind在functional头文件中
auto newCall = bind(call, args);

auto check6 = bind(check_size, _1, 6);  //占位符_1表示check6只接受一个参数,check6使用_1位置的变量和6来调用check_size(s, 6)

using std::placeholders::_1;  //或者using namespace std::placeholders;包含所有 
find_if(w.begin(), w.end(), bind(check_size, _1, sz)); //解决了find_if的一元谓词和check_size两个参数的矛盾
新可调用对象有两个参数,分别作为第三个(g的第二个参数)和第五个(g的第一个参数)传递给f,f的第一、二、四参数分别被绑定到a、b、c上
auto g = bind(f, a, b, _2, c, _1) 
g(_1, _2) //调用g时参数按照_1,_2位置绑定

重排参数
sort(w.begin(), w.end(), shorter())  //升序
sort(w.begin(), w.end(), bind(shorter, _2, _1)) //降序
bind非占位符参数采用拷贝的方式,不过有些变量无法拷贝时
函数ref返回一个对象,包含给定的引用,该对象是可以拷贝的。functional中还有个cref函数
for_each(w.begin(), w.end(), bind(print, ref(os), _1, ' '));

再探迭代器:

插入迭代器(insert):用于向容器插入元素

back_inserter  使用push_back的迭代器
front_inserter  使用push_front的迭代器
inserter  总是使用第二参数插入容器迭代器所表示元素的位置之前

inserter等价于:
it = c.insert(it, val);
++it;  //递增it使得指向原来的元素

在这里插入图片描述

流迭代器(stream):用于遍历所关联的IO流
1、流迭代器并不是立即读取流数据,而是在使用迭代器时才真正读取。这在创建流迭代器没有使用就销毁了;或者两个不同对象同步读取同一个流时,何时读取就重要了
2、流迭代器不支持递减,因为在流上不能反向移动
在这里插入图片描述

// istream_iterator使用>>读取流,因此要读取的类型必须定义了输入运算符
istream_iterator<int> int_it(cin);  //从cin读取int
istream_iterator<int> int_eof; //尾后迭代器

ifstream in("afile");
istream_iterator<string> str_it(in);
读取示例:
istream_iterator<int> int_it(cin);  //从cin读取int
istream_iterator<int> int_eof; //尾后迭代器
while(int_it != int_eof){
	vec.push_back(*int_it++);  //++返回旧值,*解引用
}
可简写成:
istream_iterator<int> int_it(cin), eof; 
vector<int> vec(int_it, eof);
使用算法操作流迭代器:
cout << accumulate(int_it, eof, 0) << endl;

在这里插入图片描述

// ostream_iterator使用<<输出流,因此要输出的类型必须定义了输出运算符,不允许空的或者尾后的ostream_iterator
ostream_iterator<int> out_it(cout, " ");
for(auto e : vec){
	*out_it++ = e;
}
cout<<endl;
或:
for(auto e : vec){
	out_it = e;  //可忽略*和++,但不建议,建议与其他流操作保持一致
}
cout<<endl;
或:
copy(vec.begin(), vec.end(), out_it);
cout<<endl;

反向迭代器(reverse):迭代器向后移动
在这里插入图片描述
在这里插入图片描述

sort(v.begin(), v.end()); //正常序
sort(v.rbegin(), v.rend());  //逆序

// 注意crbegin和cend、rcomma和rcomma.base()生成的是相邻但是不相同的位置
cout << string(s.crbegin(),  rcomma) <<endl;  //rc为最后一个,位置,结果为TSAL
cout << string(rcomma,  s.cend()) <<endl;  //将反向迭代器rc转为普通迭代器,结果为LAST

移动迭代器(move):非拷贝而是移动元素

动态内存shared_ptr&new:

1、new和delete分别用于在堆上新建、删除对象。不过容易出现内存泄漏的问题。新标准提供了智能指针来处理。
shared_ptr:
1、shared_ptr类似于关联一个引用计数器,当初始化另一个shared_ptr、作为参数传递给函数、作为函数返回值时都会增加计数。当shared_ptr被赋予新值、离开局部作用域(被销毁),计数器就会递减。
2、当最后一个shared_ptr被销毁时,会自动销毁对象,并释放内存。注意只要还存在一个shared_ptr,就不会销毁对象、释放内存。
3、不能使用相同的内置指针来初始化多个智能指针;不delete get返回的内置指针;如果使用get返回的内置指针,当最后一个智能指针被销毁后,内置指针就会无效了;如果使用智能指针管理资源而不是new分配的内存,记得传递一个删除器。

shared_ptr<std::string> p1;  //默认情况下初始化为空指针

//最安全的分配和使用动态内存方式。在动态内存中分配对象并初始化它。
//其初始化方式是将括号内参数构建对象,类似于顺序容器的emplace方法。无参数使用值初始化
shared_ptr<std::string> p1 = make_shared<std::string>("ok"); 
auto p1 = make_shared<std::string>("ok");  

if (p1 && p1->empty()) {
	*p1 = "a";
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
new和delete:

int* pi = new int;  //new无法为分配的对象命名,而是返回指向该对象的指针。默认情况下对象会默认初始化,因此基础类型将是未定义,而类按照默认构造函数初始化。

初始化:
int *pi = new int(1024);  //圆括号
vector<int> *vecp = new vector<int>{1,2,3};  //列表
int* pi = new int;  //默认初始化,值未定义
int* pi = new int(); //值初始化为0。对于类类型(string),默认初始化和值初始化都会调用默认构造函数,但对于内置类型则有不同表现。

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

delete pi; //销毁指针并释放内存,对同一指针释放两次可能导致自由空间的内容被破坏。

在这里插入图片描述
在这里插入图片描述
shared_ptr和new联用:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
不可混用智能指针和普通指针,因为智能指针只能析构自身的拷贝。当将一个智能指针绑定到普通指针上后就将内存管理交到了智能指针,此时不应该再使用普通指针,因为不知道何时被销毁。

在这里插入图片描述
shared ptr的get返回一个内置指针,但不能用该内置指针delete此指针。
在这里插入图片描述
智能指针与异常:
在这里插入图片描述
智能指针与哑类:
在这里插入图片描述
unique_ptr:
1、某时刻只能有一个unique ptr指向一个给定对象

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值