C++ primer plus读书笔记——第17章 输入、输出和文件
第17章 输入、输出和文件
1. 对键盘进行输入缓冲可以让用户在将输入传输给程序之前返回并更正。C++程序通常在用户按下回车键时刷新输入缓冲区。
2. 一些I/O类
streambuf类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区、刷新缓冲区和管理缓冲区内存的类方法。
ios_base类表示流的一般特征,如是否可读取、是二进制流还是文本流等。
ios类基于ios_base类,其中包括了一个指向streambuf对象的指针成员。
ostream类从ios类派生而来,提供了输出方法。
istream类从ios类派生而来,提供了输入方法。
iostream类是基于istream类和ostream类的,因此继承了输入方法和输出方法。
3. C++11提供了I/O的char和wchar_t具体化。例如,istream和ostream都是char具体化的typedef。同样,wistream和wostream都是wchar_t具体化。例如,wcout对象用于输出宽字符流。头文件ostream中包含了这些定义。
4. 在程序中包含iostream文件将自动创建8个流对象(4个用于窄字符流,4个用于宽字符流)。
cin对象对应于标准输入流。wcin对象与此类似,但处理的是wchar_t类型。
cout对象与标准输出流相对应。wcout对象与此类似,但处理的是wchar_t类型。
cerr对象与标准错误流相对应,可用于显示错误信息。这个流没有被缓冲,这意味着信息将被直接发送给屏幕,而不会等到缓冲区填满或新的换行符。wcerr与此类似。
5. 对象代表流——这意味着什么呢?当iostream文件为程序声明一个cout对象时,该对象将包含存储了与输出有关的信息的数据成员,如显示数据是使用的字符宽度、小数位数、显示整数时使用的计数方法以及描述用来处理输出流缓冲区的streambuf对象的地址。
6. C++将输出看作是字节流(根据实现和平台的不同,可能是8位、16位或32位的字节,但都是字节)。因此,ostream类最重要的任务之一就是将数值类型(如int或float)转换为以文本表示的字符流。
7. cout字符串指针将显示字符串。对于其它类型的指针,C++打印地址的数值表示。如果要获得字符串的地址,则必须将其强制转换为其它类型。
int eggs = 12;
char *amount = “dozen”;
cout << &eggs; //打印eggs变量的地址
cout << amount; //打印字符串“dozen”
cout << (void *)amount;//打印字符串“dozen”的地址
8. 其它ostream方法
除了各种operator<<()函数外,ostream类还提供了put()方法和write()方法,前者用于显示字符,后者用于显示字符串。
cout.put(‘w’).put(‘t’);
write()方法显示整个字符串,其模板原型如下:
basic_ostream<chart, traits> & write(const char_type *s, streamsize n);
write的第一个参数提供了要显示的字符串的地址,第二个参数指出了要显示多少个字符串。
还需要注意的是,write()方法并不会在遇到空字符时自动停止打印字符,而只是打印指定数目的字符,即使超出了字符串的边界。
但write()确实为将数据存储在文件中提供了一种简洁、准确的方式。
9. ostream类对cout对象处理的输出进行缓冲,所以输出不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满。通常,缓冲区为512字节或其整数倍。在屏幕输出时,程序不必等到缓冲区被填满。例如,将换行符发送到缓冲区后,将刷新缓冲区。另外,正如前面指出的,多数C++实现都会在输入即将发生时刷新缓冲区。
假设有如下代码:
cout << “Enter a number: ”;
float num;
cin >> num;
它会立刻显示cout信息,即使输出字符串中没有换行符。
10. 有两个控制符可以强行刷新缓冲区。控制符flush刷新缓冲区,而控制符endl刷新缓冲区,并插入一个换行符。
cout << “Hello, good-looking!” << flush;
cout << “wait a moment, please” << endl;
事实上,控制符也是函数。可以直接使用flush来刷新缓冲区:
flush(cout);
11. cout一个浮点数时,默认情况是,当指数大于等于6或小于等于-5时,将使用科学计数法来表示。
12. ostream类是从ios类派生而来的,而后者是从ios_base类派生而来的。ios_base类存储了描述格式状态的信息。由于ios_base类是ostream类的间接基类,因此可以将其方法用于ostream对象,如cout。
13. 修改计数系统
要控制整数以十进制、十六进制还是八进制显示,可以使用dec、hex和oct控制符。
hex(cout);
虽然控制符实际上是函数,但它们更通用的使用方式是:
cout << hex;
14. 调整字段宽度
cout.width(); //返回字段宽带的当前设置
cout.width(int i); //将字段宽度设置为i,并返回以前的字段宽度值
width()方法只影响接下来显示的一个项目,然后字段宽度将恢复默认值。
默认的字段宽带为0,适用于所有的数据。因为C++永远不会截短数据,因此如果试图在宽度为2的字段中打印一个7位值,C++将增宽字段,以容纳该数据。C/C++的原则是:显示所有数据比保持列的整洁性更加重要。C++是内容终于形式。
15. 填充字符
在默认情况下,cout使用空格填充字段中未被使用的部分。
cout.fill(‘*’); //将填充字符改为星号
这对于检查打印结果,防止接收方添加数据很有用。
与字段宽度不同的是,新的填充字符一直有效,知道更改它为止。
16. 设置浮点数的显示精度
浮点数的精度含义取决于输出模式。在默认模式下,它指的是显示的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。C++的默认精度是6位。
cout.precision(2);
新的精度设置会一直有效,知道被重新设置为止。
17. 打印末尾的0和小数点
cout.setf(ios_base::showpoint);
将显示末尾的小数点,还将导致末尾的0被显示出来。例如2,将会显示为2.000000。
showpoint是ios_base类声明中定义的类级静态常量。
18. setf()函数有两个原型。第一个为:
fmtflags setf(fmtflags);
ios_base定义了格式常量:
ios_base::boolalpha 输入和输出bool值,可以为true或false
ios_base::showbase 对于输出,使用C++基数前缀(0,0x)
ios_base::showpoint 显示末尾的小数点
ios_base::uppercase 对于16进制输出,使用大写字母
ios_base::showpos 在整数前面加上+
注意,仅当基数为10时,才使用加号或符号。C++将十六进制和八进制都视为无符号。
19. 第二个setf()原型接受两个参数,并返回以前的设置:
fmtflags setf(fmtflags, fmtflags);
第一个参数表示所需设置的fmtflags值,第二个参数表示要清除第一个参数中的哪些位。例如,将第3位设置为1表示以10为基数,将第4位设置为1表示以8为基数,将第5位设置为1表示以16为基数。假设输出是以10为基数的,而要将它设置为以16为基数,则不仅要将第5位置为1,还需要将第3为置为0。
cout.setf(ios_base::hex, ios_base::basefield);
第二个参数 |
第一个参数 |
含义 |
ios_base::basefield |
ios_base::dec |
使用基数10 |
ios_base::oct |
使用基数8 |
|
ios_base::hex |
使用基数16 |
|
ios_base::floatfield |
ios_base::fixed |
定点计数法 |
ios_base::scientific |
科学计数法 |
|
ios_base::adjustfield |
ios_base::left |
左对齐 |
ios_base::right |
右对齐 |
|
ios_base::internal |
符号或基数前缀左对齐,值右对齐 |
20. 如果您熟悉C语言的printf()说明符,则可能知道,默认的C++模式对应于%g说明符,定点表示法对应于%f说明符,而科学表示法对应于%e说明符。
21. 在C++标准中,默认表示法精度是总位数,不显示末尾的0。而定点表示法和科学表示法都有下面的两个特征:
精度指的是小数位数,而不是总位数;
显示末尾的0。
22. 调用setf()的效果可以通过unsetf()消除,后者原型如下:
void unsetf(fmtflags mask);
cout.unsetf(ios_base::boolalpha);
您可能注意到了,没有专门指示浮点数默认显示模式的标记。系统的工作原理如下:仅当只有定点为被设置时使用定点表示法;仅当只有科学位被设置时使用科学表示法;如没有被设置或两位都被设置时,使用默认表示法。因此,启用默认模式的方法如下:
cout.setf(0, ios_base::floatfield);
cout.unsetf(ios_base::floatfield);
23. 使用setf()不是进行格式化、对用户最为友好的方法,C++提供了多个控制符,能够调用setf(),并自动提供正确的参数。P751-P752
24. 头文件iomanip
使用iostream工具来设置一些格式值不太方便。为简化工作,C++在头文件iomanip中提供了一些控制符,它们能够提供前面讨论过的服务,但表示起来更方便。3个最常用的控制符分别是setprecision()、setfill()、setw(),与前面讨论的控制符不同的是,这三个控制符都带参数。
#include <iostream>
#include <iomanip>
#include <math>
int main()
{
using namespace std;
int n = 10;
cout << fixed << right;
cout << setw(6) << setfill(.) << n << setfill(‘ ’)
<< setw(12) << setprecision(3) << sqrt(root) << endl;
return 0;
}
25. 典型的运算符函数的原型如下:
istream & operator>>(int &);
参数和返回值都是引用。由于参数是引用,因此cin能够修改用作参数的变量的值。返回调用对象的引用,使得输入能够拼接起来。
26. 顺便说一句,可以将hex、oct和dec控制符与cin一起使用,来指定将整数输入解释为十六进制、八进制还是十进制格式。cin >> hex;
27. istream还为字符指针重载了>>抽取运算符。
抽取运算符将读取输入中的下一个单词,将它放置到指定的地址,并加上一个空值字符,使之称为一个字符串。
char name[20];
cin >> name;
28. while(cin >> input)如果istream对象的错误状态被设置,if或while语句将判定该对象为false。
29. cin或cout包含一个描述溜状态的数据成员,从ios_base类那里继承的。流状态(被定义为iostate类型,而iostate是一种bitmask类型)由3个ios_base元素组成:eofbit、badbit或failbit,其中每个元素都是1位。
30. 当cin操作到达文件尾时,它将设置eofbit;当cin操作未能读取到预期的输入时,它将设置failbit。I/O失败(如试图读取不可访问的文件或试图写入写保护的磁盘),也可能将failbit设置为1。在一些无法诊断的失败破坏流时,badbit元素将被设置。P756很多流状态相关的函数
31. 流状态位被设置后,流将对后面的输入或输出关闭,直到位被清除。
while(cin >> input)
{
sum += input;
}
if(cin.fail() && !cin.eof())
{
cin.clear(); //reset stream state
while(!isspace(cin.get()))//get rid of bad input
continue;
}
else
{
cout << “I can not go on!\n”;
exit(1);
}
cout << “Now enter a new number: ”;
cin >> input;
32. 单字符输入
在使用char参数或没有参数的情况下,get()方法读取下一个输入字符,即使该字符是空格、制表符或换行符。get(char &)版本将输入字符赋给其参数,并返回istream对象的引用,到达文件尾时,返回值判定为false;而get(void)版本将输入字符转换为整形(通常是int),并将其返回,到达文件尾时,返回值EOF。
while(cin.get(ch))
{
}
while((ch = cin.get()) != EOF)
{
}
在get()方法中,get(char &)的接口更佳。get(void)的主要优点是,它与标准C语言中的getchar()函数极其相似,这意味着可以通过包含iostream(而不是stdio.h),并用cin.get()替换所有的getchar(),用cout.put(ch)替换所有的putchar(ch),来将C程序转换为C++。
33. 通过键盘可以仿真文件尾,对于DOS和Windows命令提示符模式,为按下Ctrl+Z;对于Unix,是在行首按下Ctrl+D。
34. 字符串输入
istream & get(char *, int, char);
istream & get(char *, int);
istream & getline(char *, int, char);
istream & getline(char *, int);
第一个参数是用于放置输入字符串的内存单元地址。第二个参数比要读取的最大字符数大1。第3个参数指定用作分界符的字符,只有两个参数的版本将换行符用作分界符。
get()和getline()之间的区别在于,get()将换行符留在输入流中,这样接下来的输入操作首先看到的将是换行符;而getline()抽取并丢弃输入流中的换行符。
istream & ignore(int n = 1, int ch = EOF);
cin.ignore(255, ‘\n’);丢弃接下来的255个字符或直到到达第一个换行符。
方法 |
行为 |
getline(char *, int) |
遇到文件尾,则设置failbit; 如果读取了最大数目的字符,且下一个字符不是换行符,则设置failbit; |
get(char *, int) |
遇到文件尾,则设置failbit; 遇到空行,则设置failbit; |
35. 其它istream方法read()、peek()、gcount()、putback()
read()读取指定数目的字节,并将它们存储在指定的位置中。
char gross[144];
cin.read(gross, 144);
与getline()和get()不同的是,read()不会在输入后加上空值字符。
peek()返回输入中的下一个字符,但不抽取输入流中的字符。
gcount()方法返回最后一个非格式化抽取方法读取的字符数。这意味着字符是由get()、getline()、ignore()或read()方法读取的,不是由抽取运算符读取的(>>),抽取运算符对输入进行格式化,使之与特定的数据类型匹配。
putback()函数将一个字符插入到输入字符串中,被插入的字符将是下一条输入语句的第一个字符。putback()方法接受一个char参数,返回istream &。
36. 用于文件输入的ifstream类和用于文件输出的ofstream类都是从头文件iostream中的类派生而来的,因此这些新类的对象可以使用前面介绍过的方法。
37. 文件I/O要包含头文件fstream。对于大多数实现来说,包含该文件便自动包含iostream头文件,因此不必显式包含iostream。
38. 要将文件输出流对象与特定的文件关联,可以使用open()方法:
ofstream fout;
fout.open(“jar.txt”);
可以使用另一个构造函数将这两步合并成一个语句:
ofstream(“jar.txt”);
类似地,声明一个ifstream对象,将它与文件名关联起来,可以这么做:
ifstream fin;
fin.open(“jellyjar.txt”);
或
ifstream fin(“jellyjar.txt”);
39. 当输入或输出流对象过期时,文件的连接将自动关闭,另外,也可以使用close()方法来显示地关闭文件的连接:
fout.close();
fin.close();
关闭这样的连接并不会删除流,而只是断开流到文件的连接。例如,fin对象与它管理的输入缓冲区仍然存在。可以将流重新连接到同一个文件或另一个文件。
40. 小技巧:
string filename;
cin >> filename;
ofstream fout(filename.c_str());
41. C++文件流类从ios_base类那里继承了一个流状态成员。以前,检查文件是否打开的常见方式如下:
if(fin.fail())…
if(!fin.good())…
if(!fin)…
上面三种方式等价,然而,这些测试无法检测这样一种情形:试图以不合适的文件模式打开文件失败时。方法is_open()能够检测到这种错误以及good()能够检测到的错误。然而,老式C++没有实现is_open()。
42. 有些C++实现要求在重新关联文件时使用fin.clear(),有些则不要求,这取决于将文件与ifstream对象关联起来时,是否自动重置流状态。使用fin.clear()是无害的,即使在不必使用它的时候。
43. 将流与文件关联时,都可以提供第二个参数指定文件模式。
ifstream fin(“banjo”, mode1);
- ofstream fout;
fout.open();
fout.open(“banjo”, mode2);
常量 |
含义 |
ios_base::in |
打开文件,以便读取 |
ios_base::out |
打开文件,以便写入 |
ios_base::ate |
打开文件,并移到文件尾 |
ios_base::app |
追加到文件尾 |
ios_base::trunc |
如果文件存在,则截断文件 |
ios_base::binary |
二进制文件 |
注意,ios_base::ate和ios_base::app都将文件指针指向打开的文件尾。二者的区别在于,ios_base::app模式只允许将数据添加到文件尾,而ios_base::ate模式将指针放到文件尾。
44. 文本文件和二进制文件。对于字符来说,二进制表示和文本表示是一样的,即字符的ASCII码表示。对数字来说,二进制文件与文本文件有很大区别。
45. 文本格式便于读取,可以使用编辑器或字处理器来读取和编辑文本文件,可以很方便地将文本文件从一个计算机系统传输到另一个计算机系统。二进制格式对于数字来说比较精确,不会有转换误差或舍入误差,而且保存数据的速度快,占用空间小。
46. 将数据以文本文件或二进制文件保存的方法:
文本文件:
cons tint LIM = 20;
struct planet
{
char name[LIM];
double population;
double g;
};
planet pl;
ofstream fout(“planet.dat”, ios_base::out | ios_base::app);
fout << pl.name << “ “ << pl.population << “ “ << pl.g << endl;
必须使用成员运算符显式的提供每个结构成员,还必须将相邻的数据分隔开,以便区分。
要用二进制格式存储相同的信息,可以这样做:
ofstream fout(“planet.dat”, ios_base::out | ios_base::app | ios_base::binary);
fout.write((char *)&pl, sizeof pl);
与文本文件相比,信息的保存更加紧凑,精确。
47. 将struct数据以二进制保存,这种方法适用于不使用虚函数的类。在这种情况下,只有数据成员被保存,而方法不会被保存。如果类有虚方法,则也将复制隐藏指针(该指针指向虚函数的指针表)。由于下一次运行程序时,虚函数表可能在不同位置,因此将文件中的旧指针信息复制到对象中,将可能造成混乱。
48. string对象本身实际上并没有包含字符串,而是包含了一个指向其中存储了字符串内存单元的指针。
49. 随机存取常用于数据库文件,程序跳到数据在文件中的位置,读取其中的数据。为了能够读取和修改记录,可以创建一个fstream对象,以读写模式打开文件。fstream类是从iostream类派生而来的,而后者基于istream和ostream两个类。因此,它继承了它们的方法。还继承了两个缓冲区,一个用于输入,一个用于输出,并能同步化这两个缓冲区的处理。
fstream finout;
finout.open(file, ios_base::in | ios_base::out | ios_base::binary);
50. fstream类为此继承了两个方法:seekg()将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置(实际上,由于fstream使用缓冲区来存储中间数据,因此指针指向的是缓冲区的位置,而不是实际的文件)。也可以将seekg()用于ifstream对象,将seekp()用于ofstream对象。
seekg()的原型如下:
basic_istream<charT, traits>& seekg(off_type, ios_base::seekdir);
basic_istream<charT, traits>& seekg(pos_type);
由于char具体化,上面两个原型等同于下面的代码:
istream & seekg(streamoff, ios_base::seekdir);
istream & seekg(streampos);
第一个原型定位到离第二个参数指定的文件位置特定距离的位置;第二个原型定位到离文件头特定距离的位置。
fin.seekg(30, ios_base::beg);
fin.seekg(-1, ios_base::cur);
fin.seekg(0, ios_base::end);
fin.seekg(123);
P785-P787完整程序。
tellg()和tellp()方法报告当前的文件位置。
51. 生成临时文件名char * tmpnam(char *pszName);P787-P788
52. iostream族支持程序与终端之间的I/O,而fstream族提供程序和文件之间的I/O。C++还提供了sstream族,它们使用相同的接口提供程序和string对象之间的I/O。
53. 读取string对象中的格式化信息或将格式化信息写入string对象中被称为内核格式化(incore formatting)。
54. 头文件sstream定义了一个从ostream类派生而来的ostringstream类(还有一个基于wostream的wostringstream类,用于宽字符集)。如果创建了一个ostringstream对象,则可以将信息写入其中,它将存储这些信息。
55. ostringstream类有一个名为str()的成员函数,该函数返回一个被初始化为缓冲区内容的字符串对象:
string mesg = outstr.str();
使用str()方法可以“冻结”该对象,这样便不能将信息写入该对象中。
56. istringstream类允许使用istream方法族读取istringstream对象中的数据,istringstream对象可以使用string对象进行初始化。
57. istringstream和ostringstream类使得能够使用istream和ostream类的方法来管理存储在字符串中的字符数据。
C++ primer plus读书笔记——第17章 输入、输出和文件的更多相关文章
- A Byte of Python 笔记(10)输入/输出:文件和储存器
第12章 输入/输出 大多数情况下,我们需要程序与用户交互.从用户得到输入,然后打印一些结果. 可以分别使用 raw_input 和 print 语句来完成这些功能.对于输出,可以使用多种多样的 s ...
- C primer plus 读书笔记第四章
本章的标题是字符串的格式化输入/输出,重点介绍输入和输出. 本章的第一段示例代码和上一张示例代码很相近,代码就不贴了,新出现的特性是使用了一个数组来存放字符串,C预处理命令和strlen()函数. 下 ...
- C++ primer plus读书笔记——第16章 string类和标准模板库
第16章 string类和标准模板库 1. string容易被忽略的构造函数: string(size_type n, char c)长度为n,每个字母都为c string(const string ...
- C++ primer plus读书笔记——第15章 友元、异常和其他
第15章 友元.异常和其他 1. 友元类的所有方法都可以访问原有类的私有成员和保护成员.另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元.哪些函数.成员函数.或类为友元是由类定义的, ...
- C++ primer plus读书笔记——第14章 C++中的代码重用
第14章 C++中的代码重用 1. 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现).获得接口是is-a关系的组成部分.而使用组合,类可以获得实现,但不能获得接口. ...
- C++ primer plus读书笔记——第13章 类继承
第13章 类继承 1. 如果购买厂商的C库,除非厂商提供库函数的源代码,否则您将无法根据自己的需求,对函数进行扩展或修改.但如果是类库,只要其提供了类方法的头文件和编译后的代码,仍可以使用库中的类派生 ...
- C++ primer plus读书笔记——第12章 类和动态内存分配
第12章 类和动态内存分配 1. 静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域运算符来指出静态成员所属的类.但如果静态成员是整形或枚举型const,则可以在类声明中初始化 ...
- C++ primer plus读书笔记——第11章 使用类
第11章 使用类 1. 运算符重载是一种形式的C++多态. 2. 不要返回指向局部变量或临时对象的引用.函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据. 3. 运算符重载的格式如下: ...
- C++ primer plus读书笔记——第9章 内存模型和名称空间
第9章 内存模型和名称空间 1. 头文件常包含的内容: 函数原型. 使用#define或const定义的符号常量. 结构声明. 类声明. 模板声明. 内联函数. 2. 如果文件名被包含在尖括号中,则C ...
随机推荐
- 手把手教你如何使用Charles抓包
一.为什么使用charles 前几天因为需要通过抓包定位问题,打开了尘封已久的fiddler,结果打开软件后什么也干不了,别说手机抓包了,打开软件什么请求也抓不到. 很多时候都是如此,如果一个方案不行 ...
- 力扣 - 剑指 Offer 09. 用两个栈实现队列
目录 题目 思路 代码 复杂度分析 题目 剑指 Offer 09. 用两个栈实现队列 思路 刚开始想的是用stack1作为数据存储的地方,stack2用来作为辅助栈,如果添加元素直接push入stac ...
- 前端富文本编辑器vue + tinymce
之前有项目需要用到富文本编辑器,在网上找了好几个后,最终选择了这个功能强大,扩展性强的tinymce tinymce中文文档地址(不全):http://tinymce.ax-z.cn/ tinymce ...
- TypeError: 'list' object cannot be interpreted as an integer Python常见错误
想要通过索引来迭代一个list或者string的元素, 这需要调用 range() 函数.要记得返回len 值而不是返回这个列表.
- .NET Core3.1 Dotnetty实战系列视频
一.概要 由于在.net的环境当中对dotnetty相关资料相对较少,所以这里主要分享一个dotnetty使用教程希望能帮助到正在使用这套框架的开发者们.虽然这套框架已微软官方已经不在维护,但是这套框 ...
- MySQL提升笔记(4)InnoDB存储结构
这一节本来计划开始索引的学习,但是在InnoDB存储引擎的索引里,存在一些数据存储结构的概念,这一节先了解一下InnodDB的逻辑存储结构,为索引的学习打好基础. 从InnoDB存储引擎的存储结构看, ...
- Java 获取资源文件路径
1 问题描述 通过源码运行时,一般使用如下方式读取资源文件: String str = "1.jpg"; 资源文件与源码文件放在同一目录下,或者拥有同一父级目录: String s ...
- Go-30-main包
main包 package main import ( "fmt" "kubeflow-tool/main/cmd" "os" ) func ...
- Ambassador-05-自动重试
自动重试定义: retry_policy: retry_on: <string> num_retries: <integer> per_try_timeout: <str ...
- Flutter 状态管理- 使用 MobX
文 / Paul Halliday, developer.school 创始人 众所周知,状态管理是每个软件项目都需要持续迭代更新的方向.它并不是一个「一次性」的工作, 而需要不断确保你遵循的最佳实践 ...