Printf 函数使用参考与研究

内容

  1. C的数据类型

    1. C 的基本数据类型
    2. C 中数值常量的数据类型
  2. Printf 及其函数族

  3. Fmt 格式控制字串

    1. 基本类型 specifier
    2. 类型 specifier 的 length modifier
    3. 宽度修饰符
    4. 精度修饰符
    5. 可变域宽与精度
    6. Flag 标识
  4. Printf 参数类型提升

    1. 表达式情况
    2. 函数参数情况
  5. Printf 实际中使用一些注意点

  6. 参考文献

一些说明


  • 测试环境 CPU : Intel Core i5-2400 OS : Win7 编译器 : GCC 3.4.2 MinGW
  • 输出说明 输出以 '$' 作为开始及结束标志,这样输出的空格可以很方便的看出。

C的数据类型


考虑到格式控制字串中需要 Type 的 length modifier 与数据类型对应; 又因为printf函数为可变长,会发生参数类型隐式转换,因此需要对传入printf的参数的类型有清晰的了解,所以这里回顾一下C中的数值类型:

C的基本数据类型

有以下4种:

char a single byte, 1Byte

int integer, 4 Byte

float single-precision floating point, 4Byte

double double-precision floating point, 8Byte

在基本数据类型基础上,可以加入一下 qualifier ,与基础数据类型结合成为新的类型:

short / long : 仅用于修饰 int , 新类型中 int 可省略 : short , long; 容量大小关系: short <= int <= long; short 至少为16bit, long 至少为32bit; 还可以有类型 long long int;

unsigned / signed : 仅用于修饰 char 及 int(任何 int 类型,包括 short , long); 默认为 signed (注意 char 也是 signed ) ,扩展时高位填充符号位。见如下代码, 输出见代码最下的注释,下同。

1 char c1 = 0xff;
 2 unsigned char c2 = 0xff;
 3 int n1;
 4 unsigned int n2;
 5 n1 = c1;
 6 n2 = c1;
 7 printf("$%#x, %#x$\n", n1, n2);
 8 n1 = c2;
 9 n2 = c2;
10 printf("$%#x, %#x$\n", n1, n2);
11 n1 = (unsigned char)c1;
12 n2 = (unsigned char)c2;
13 printf("$%#x, %#x$\n", n1, n2);
14 /* $0xffffffff, 0xffffffff$
15  * $0xff, 0xff$
16  * $0xff, 0xff$ */

long : 修饰 double 形成新类型 long double ; float , double , long double 的 size 是 implementation-defined 。

1 printf("$short %d, int %d, long %d, long long %d$\n",
 2         sizeof(short),
 3         sizeof(int),
 4         sizeof(long),
 5         sizeof(long long));
 6 printf("$float %d, double %d, long double %d$\n",
 7         sizeof(float),
 8         sizeof(double),
 9         sizeof(long double));
10 /* $short 2, int 4, long 4, long long 8$
11  * $float 4, double 8, long double 12$ */

更多关于数据位宽的信息请参见 .

C 中数值常量的数据类型

数值常量可用 prefix 和 suffix 来修饰.

prefix 仅用于对各种 int 的修饰,用于指定数值的进制. 不加表示 decimal, leading zero 表格 octal ,如 012 表示十进制的 10; 0x 或 0X 表示 hexadecimal. C 中无法表示2进制数值常量.

suffix 用来指定数值类型,对整数和小数都有效。不加 suffix 整数默认为 int, 小数默认为 double. suffix大小写无关,以下以大写为例.

  • 对于整数 : 数值常量至少也是 int(没有 char 和 short 常量);其余类型都有相应后缀对应,规律是 unsigned 写作U, long 写作 L;如 2U, 3UL, 4L, 0x8fffffffffffffffLL. 总之有 unsigned int , long, unsigned long, long long 类型的后缀.
  • 对于小数 : 可用 F 或 L 修饰. F 表示 float; L 表示 long double. 可见小数的3中类型都有对应的 suffix. 不能用 F 修饰整数常量, 如 2F, 编译报错.

Printf及其函数族


printf 函数原型 : int printf ( const char * format, ... ); 作用是向 stdout 提供 output format conversion. 返回值 : 若成功返回输出的字符数, 失败则会设置 ferror 并返回一个负数.

printf函数族含有2个相似的函数:

  • int fprintf ( FILE * stream, const char * format, ... );
  • int sprintf ( char * str, const char * format, ... );

fprintf 是将格式化后的字串输出到 FILE * 指定的 stream 中. sprintf 将格式化后的字串输出到 由 str 指向的缓冲中,会自动在字串结尾输出 NULL 字符.若成功,两函数均返回输出的字符数(不包括 NULL 字符).若出错,则返回一个负值.

1  printf("$%s$\n", str1);
2  fprintf(stdout, "$%s$\n", str1);
3  /* $test$
4   * $test$ */

可见两者的意义是相同的.

printf , fprintf, sprintf 3个函数都是可变长的(注意到原型中的 "..."). 这三个函数还有对应的用 va_list 作为参数的版本:

  • int vprintf(const char *format, va_list arg)
  • int vfprintf(FILE stream, const char format, va_list arg)
  • int vsprintf(char s, const char format, va_list arg)

这3个函数除了用 va_list 取代可变长参数外全部一样.可参见

Fmt 格式控制字串


printf 中的 char * format 不仅仅是一个寻常字串, 起到控制输出的作用. 包含2中 objects : ordinary characters 和 conversion specification. 其中 ordinary chars 会被原封不动的复制至 stdout. 而 conversion specification 将 printf 的输入

参数转变为字串并输出. 每个 conversion specification 由 '%' 开始, 以1个 type specifier 字符结尾(见下面), 这2个字符是必须有的,若结尾 type specifier 不被识别,则结果是 undefined. 两者中间还可以加入些可选的格式调整选项. 整个 format 字

串的结构是这样的 :

%[flags][width][.precision][length]specifier

各自作用:

  • specifier : 指明传入 printf 的参数类型
  • width : 指明 converted argument 输出时至少的宽度, wider if necessary. 对输出的对齐控制至关重要
  • 对 整数, 小数, 字串的输出起到控制作用 : 精确控制输出多少字符.
  • length modifier : 与基本类型结合,从而能让 printf 正确解析传入的参数. 原则就是 type specifier 必须要与传入参数类型兼容.参见 "Printf 参数类型提升及入栈出栈研究" 小节.
  • flags : 对输出做一些 "微调", 如对齐,显示符号,填充,备选的显示方式.

基本类型 specifier

conversion specification中最重要的就是 type specifier ,因为 printf 几乎对传入的参数一无所知,基本上只能靠它来分辨栈中的数据类型. 见下表,表中黄色的行表示由 C99 引入, 下同.

*specifier* Argument type/Output Example
d*or*i int Signed decimal integer 392
u int Unsigned decimal integer 7235
o int Unsigned octal(without a leading zero) 610
x unsigned int Unsigned hexadecimal integer(without a leading 0x) 7fa
X unsigned intUnsigned hexadecimal integer (uppercase,without a leading 0X) 7FA
f double Decimal floating point, lowercase 392.65
F doubleDecimal floating point, uppercase 392.65
e double Scientific notation (mantissa/exponent), lowercase 3.9265e+2
E doubleScientific notation (mantissa/exponent), uppercase 3.9265E+2
g double Use the shortest representation:%eor%f 392.65
G double Use the shortest representation:%Eor%F 392.65
a Hexadecimal floating point, lowercase -0xc.90fep-2
A Hexadecimal floating point, uppercase -0XC.90FEP-2
c int single character, after conversion to unsigned char a
s char * String of characters sample
p void * Pointer address b8000000
n Nothing printed.

The corresponding argument must be a pointer to asigned int.

The number of characters written so far is stored in the pointed location.
% A%followed by another%character will write a single%to the stream. %



说明:

  • 若传入 printf 的参数类型与表格中指定的 type specifer 一致(包括之后由 length modifier 合成的新类型,见 "类型 specifier 的 length modifier" 一节)则肯定不会出错,否则会发生类型隐式转换,容易引入错误.
  • 并不是大小写无关的. 存在对应大小写的 type specifier ,2者的输出一般也是大小写的区别,有: x/X, f/F, e/E, g/G, a/A; 没有提到的大小写并不能混用: 如不能写 "%D", 不会被识别.
  • f/F : 以十进制输出格式 [-] mmm.ddd ; d 的个数由precision 指定; 默认为 6; precision 为 0 时不会输出 小数点 '.'
  • e/E : 以十进制输出格式 [-] m.dddddd e+/-xx or [-] m.dddddd E+/-xx; 科学计数(1≤|m|<10); d 的个数由 precision 指定; 默认为 6; precision 为 0 时不会输出 小数点 '.'
  • g/G : 很智能的一种输出法,对于打印不需要按列对齐的浮点数特别有用.思想就是: 精度控制最多输出的有效数字个数, 以下以默认精度 6 来讨论.

    • 若碰到很小的数呢? 比如 1e-10, 若写成写成 0.0000000001 就太丑了,虽然有效数字是 1,但太长了.这时也会转而用 %e 输出.注意到,当 e == -4时, %e 和 %f 格式化后的字串一样长,如: 3.14159e-04 和 0.000314159 长度一样,都是 11 个字符.

    • 因此我们有结论: 当小数转化为科学计数法后,幂 >= 6 (或者说指定的精度) 或 幂 < -4 时,用 %e 输出, 否则用 %f.同时我们注意到: %g其实就是: 在输出 6 位有效数字且在保证正确(就比如 1,000,000 就不能为了有效数字是 6 的限制,而截断最高或最低位,而应该必须输出为 1e+06 的形式)的前提下, 采用 %f %e 生成的字串较短的那一个. 助记: "%g 是用 %f 和 %e 输出较短的那个"
  • p 专门用来输出指针地址.

  • % 是特殊的 类型 specifier, 它表示输出字符 '%', 不需对应的传入参数. 但其同样受 flag, width 控制
  • n 给定的演示代码没能在我的环境下成功,不知为什么,演示代码 :
1 int i = 0;
2 printf("$hello %n world!$\n",&i);
3 printf("$%d$\n",i);
4 /* $hello $0$ */

类型 specifier 的 length modifier

为什么要用 length modifier ? 正如前面提到的, 进入 printf 后, 函数基本上对传入的参数一无所知,需要 类型 specifier 来解释存在栈中的参数.而之前的类型 specifier 仅对应了 int , double,及指针等类型.对于 C 中的其他类型并没有涉及. 就想 C 中

通过qualifier 来在基础类型上扩展一样,这里也通过 length modifier 来支持传入的“复合类型"参数.

比如 :

long size;
...
printf("%d\n", size);

若在一台 long 的位宽度 > int 的机器上, 输出时只会弹出 int 的长度, 造成栈数据混乱. 因此输入/解析数据类型匹配非常重要.

length modifier 仅有3个(仅考虑 C99 标准之前),分别是 : h, l, L; 见下表,表中

specifiers
*length* d i u o x X f F e E g G a A c s p n
*(none)* int unsigned int double int char* void* int*
hh signed char unsigned char signed char*
h short int unsigned short int short int*
l long int unsigned long int wint_t wchar_t* long int*
ll long long int unsigned long long int long long int*
j intmax_t uintmax_t intmax_t*
z size_t size_t size_t*
t ptrdiff_t ptrdiff_t ptrdiff_t*
L long double

整数用 h, l, ll 分别表示相对于前缀的 short, long, long long已经足够. 小数的话用 L 来匹配 long double 数据即可. 再次提示 : c 对应的参数应该是 int.之所以 传入 char 型数据还能用,是因

为发生了类型提升.

宽度修饰符

  • 指定其所修饰的格式项应该打印的字符数的 minimum width,对所有的 type specifier 都有作用.
  • 若打印的字符不能填满 width 指定的宽度, 会填充 ' '(对齐方式和数值的填充字符可通过 flags 微调).借助宽度修饰符我们可以轻松做到在指定宽度的区域打印数值.
  • 若实际打印的字符超出 width ,并不会被截断, 而是会挤占右侧接下来的空间. width 规定了输出有效字符的 "下界"(因为不会截断输出).

可参考表格 :

*width* description
*(number)* Minimum number of characters to be printed. If the value to be printed is shorter than this number, the result is padded with blank spaces. The value is not truncated even if the result is larger.
* The *width* is not specified in the *format* string, but as an additional integer value argument preceding the argument that has to be formatted.



精度修饰符

用来限制输出的字符的数量,即体现"精度" 对整数, 小数, 字串都有控制作用.

  • 对所有整数来说 : "%.n" 指定了输出的最小位数是 n.若输出的数字不够 n 位,则前面补 0 . 与 width, flag 的配合: width 仍然生效, 但 flag 的 0 会因 precision 而失效. 若precision == 0,且 待输出的整数也为0, 则什么都不会输出.整数的 width 和 precision 都不会截断输出.
1 printf("$%03.2d$\n", 2);
2 printf("$%02.3d$\n", 2);
3 printf("$%2.0d$\n", 2);
4 printf("$%2.0d$\n", 0);
5 /* $ 02$
6  * $002$
7  * $ 2$
8  * $  $ */
  • 对所有小数格式 e/E,f/F来说 : precision 指定了小数点后显示的位数.且仅当 precision > 0 时才会出现小数点.
  • 对于 g/G 格式来说 : 指定了输出的有效数字位数.详见 "基本类型 specifier" 小节.
  • 对于 s 来说 : 精确指定输出的字符数,超过会截断.不指定的话默认会碰到 NULL 才停止输出.
  • 对于 %c 和 %% ,精度不起作用.
*.precision* description
.*number* For integer specifiers (d,i,o,u,x,X): *precision* specifies the minimum number of digits to be written. If the value to be written is shorter than this number, the result is padded with leading zeros. The value is not truncated even if the result is longer. A *precision* of0means that no character is written for the value0.

Fora,A,e,E,fandFspecifiers: this is the number of digits to be printed **after** the decimal point (by default, this is 6).

ForgandGspecifiers: This is the maximum number of significant digits to be printed.

Fors: this is the maximum number of characters to be printed. By default all characters are printed until the ending null character is encountered.

If the period is specified without an explicit value for *precision*,0is assumed.
.* The *precision* is not specified in the *format* string, but as an additional integer value argument preceding the argument that has to be formatted.



可变域宽与精度

之前说的域宽和精度都是定死的(magic number),这在实际中可能会带来不便, 能根据宏或者变量动态指定这2者会让程序更容易修改和移植. 比如

1 #define NAMESIZE 14
2 char name[NAMESIZE];
3 ...
4 printf("...%.14s...", ..., name, ...);

如果想让精度跟着 NAMESIZE 变, 直接写成 "...%.NAMESIZE..." 是不行的, 宏无法到达字串内. 正确的做法就是用字符 '*' 间接指定宽度或精度,通过向 printf 参数传入所需参数来实现动态指定.如

1 printf("%*.*s\n", 12, 5, str);
2 printf("%12.5\n", str);

两者完全等价.

Flags 标识

Flags 会在之前各个 field 控制形成的 specification 基础上进行调整,有如下选项 : '-', '+', ' ', '0', '#'

  • '-' : 输出改为右对齐(默认是左对齐),即填充的字符出现在输出的左侧(默认为右侧). 当然, 对齐控制仅当 width 存在是才有意义(即显示宽度大于被显示位数时). 在固定区域打印字串时,一般右对齐会比左对齐好看些( excel 单元格内就是右对齐). 需要写成类似这样 "%-14s".
  • '+' : 强制数字输出时必须带符号(默认仅负数会输出符号 '-'). (因此 0 也会被输出为 +0) 与 '-' 无任何联系. 仅作用于数值, %s, %c 无效,且不能影响 %e 方式下的 幂 e 的符号.啥时候用呢?
1 printf("%+s\n", "sd");
2 printf("%+c\n", 'a');
3 printf("%e\n", 2.3);
4 /* sd
5  * a
6  * 2.300000e+000 */

当数值有正有负,且需要左对齐时,强制输出符号防止正数和负数错位,在科学计数法中, "%+e" 就比 "%e" 要整齐:

1 for(i = -1; i <= 3; i++)
2     printf("$%+d$\n", i);
3 /* $-1$
4  * $+0$
5  * $+1$
6  * $+2$
7  * $+3$ */
  • ' ' : 作用于 '+' 类似,非负数在前面插入1个空格 ' '; 若 '+' 与 ' ' 同时存在, 以 '+' 为准.
1 double x;
2 for(x = -1; x <= 3; x++){
3     printf("% e %+e %e\n", x, x, x);
4 }
5 /* -1.000000e+000 -1.000000e+000 -1.000000e+000
6  *  0.000000e+000 +0.000000e+000 0.000000e+000
7  * 1.000000e+000 +1.000000e+000 1.000000e+000
8  * 2.000000e+000 +2.000000e+000 2.000000e+000
9  * 3.000000e+000 +3.000000e+000 3.000000e+000 */
  • '#' : 对输出进行 "微调"

    • 对于整数来说 : 影响 %o, %x ,使得输出与程序员的习惯一致: %#o 强制令输出的8进制第一个 digit 必须是 0 (0 仍然输出 0,而非 00). %#x(或%#X) 使得输出加上 0x(0X) 前缀.
    • 对于全部浮点数来说, 必须打印小数点 '.'; 且对 g/G 来说, trailing zero 会被打印,同时仍受 precision 控制.
      1 printf("$%.0f %#.0f, %g, %#g, %#g$\n", 3.0, 3.0, 3.0, 3.0, 300.0);
      2 /* $3 3., 3, 3.00000, 300.000$ *
    • 可见 '#' flag 提供了对输出控制的一些有限的可选项.
  • 除了 '+' 会覆盖 '-' 之外, 其余 flag 都是相互独立的.

Printf 参数类型提升


使用 Printf 过程中通常会遇到一些很奇怪的问题 : 如 "%f" 不能传入 3 ,必须是3.0为什么? 为什么 Printf 输出小数时 "%f" 可以支持 float 和 double, 即使两者 size 不同? 但相似的情况 scanf 就必须用 "%f" 来输入 float,用 "%lf" 来输入 double?

为什么 "%d" 可以同时用于输出 char, short int, int 甚至 long ? 但 long long int 就必须用 "%lld" ? 本节内容讨论这些和类型转换相关的问题.

C 中的很多类型转换是 implicit 情况完成的, 主要体现在 2 处 : 表达式 和 函数调用中. 这种机制很多时候并不需要程序员干预, 但特定情况下, 非常有必要弄清类型隐式转换的结果,才能写出正确的代码. Printf 函数参数传递正是其中之一.这里仅讨论数值

型变量的类型提升.

C中的类型提升 : 表达式情况

Integer Promotion: char, short int, int bit-field, 包括他们的 signed 或者 unsigned 变型, 及枚举类型. 可以使用在需要 int 或 unsigned int 的表达式中. 若 int 可以完整表示源类型的所有值, 那么该源类型的值就转换为 int, 否则转换为 unsigned int. (char 提升后为 signed 或 unsigned 是 )

1     char c1 = 0xff;
 2     unsigned char c2 = 0xff;
 3     int n1;
 4     unsigned int n2;
 5 
 6     n1 = c1;
 7     n2 = c1;
 8     printf("c1 : %#x, c2 : %#x\n", n1, n2);
 9     
10     n1 = c2;
11     n2 = c2;
12     printf("c1 : %#x, c2 : %#x\n", n1, n2);
13     /* c1 : 0xffffffff, c2 : 0xffffffff
14      * c1 : 0xff, c2 : 0xff */

"The integer promotions preserve value including sign." char 型被看做 signed 或 unsigned 是 implemention defined. 这里被看作是 signed.

Usual Arithmetic Conversions :

以操作数类型为数值类型的双目运算符为例, 运算前会发生数据类型转换,并以类似的方式产生结果类型.它的目的是进行必要的类型 promotion 之后,将表达式的类型统一为一种类型,即结果的类型. 因为built-in operators 必须作用于相同类型的变量.

转换规则如下 :

  • 整数常数转为 int, 小数常数转为 double (没有后缀指明的情况)
  • 若其中一个操作数的类型是 long double, 则全部转为 long double , 否则
  • 若其中一个操作数的类型是 double, 则全部转为 double, 否则
  • 若其中一个操作数的类型是 float, 则全部转为 float, 否则
  • 两个操作数先进行 Integer Promotion ,再执行下面的规则 :

    • 若2操作数类型相同, 则不必进行转换
    • 若有一个操作数的类型是 unsigned long int, 则全转换为 unsigned long int, 否则若其中一个操作数类型是 long int, 另一个是 unsigned int (最复杂情况): 若 long int 能够完整表示 unsigned int 的所有值 (如 long 4 Byte, int 是 2 Byte),则转为long int. 否则转为 unsigned long int. ( C标准 : if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, then the operand with unsigned integer type is converted to the type of the operand with signed integer type. Otherwise, both operands are converted to the unsigned integer type corresponding to the type of the operand with signed integer type. ) 否则
    • 若有一个操作数的类型是 unsigned int, 则全部转为 unsigned int. 否则
    • 2者全部转为 int

C 中的类型提升 : 函数参数情况

由于 C 不支持函数重载, 且ANSI 的函数声明必须包含形参的类型,所以统一函数声明和定义的参数类型必须精确的一致,才能保证实参和形参传递参数时类型匹配. 因此 ANSI C 传递参数时一般不会发生隐式参数类型提升,因为声明中已精确指定参数类型.

比如声明 : void foo(char); 调用时 char c1; foo(c1); c1 并不会先升级为 int, 压入 foo 的堆栈内. 传入的参数是作为声明的类型压入堆栈的.

有一个例外,就是可变长参数,就像 Printf 的 ... 部分.由于并没有精确指定待传入的参数类型,因此向 Printf 函数传入参数时就会发生类型提升, 整数执行 integer promotion(若本来就是 long long int 变量,或通过常数后缀 ll 指明为 long long int 则视

作 long long int ); 小数转为 double (若本来就是 long double 变量,或通过常数后缀 L指明为long double 则视作 long double).这样 char, short int 都被转为了 int.float 都转为 double.而 Printf 函数内部也很清楚会发生这种转换,因此每次能根

据 format string 中的 type specifier 来从栈中弹出适当的长度作为待输出的变量.

Printf 实际中使用一些注意点


实际中使用 Printf ,一些容易引发错误的地方 :

  • Printf 作为系统库函数,中断处理程序中调用时不合适的.
  • Printf 函数常常用作产生调试输出情况,如看代码执行到哪一步,尤其是看代码挂在哪儿了,这时看输出就可以知道了.但其实有一个隐含前提 : Printf 输出到缓存的已经全部显示出来.什么情况下 Printf 会输出缓存的内容(当然前提是设置为缓存输出)? 1.缓存满. 2. 程序员调用 fflush. 3. main 函数结束后库作清理工作的时候会把缓存中剩余的字符输出. 当出现异常时,程序退出,可能会导致缓存中尚有未打印的字符,这时根据 printf 输出来调试就会有误导作用. 因此可以考虑禁止输出缓存 : setbuf(stdout, NULL);
  • MinGW : 若在 Windows 平台用 GCC, 即 MinGW compiler(MinGW :Minimalist GNU for Windows),如很流行的 Dev C++. 原理就是编译器用 GCC,但 Runtime Library 却是用 Windows 的msvcrt.dll. 但 msvcrt.dll 对 C99 引入的类型如 long double, long long 却不支持(GCC 认为long double 的 size 是 12 byte,可参见本文开始的例程 sizeof 输出; 而 msvcrt.dll 却认为 long double 完全等同于 double,即 8 byte). 而在纯 MSVC 环境就不会有问题. 因为 MSVC编译器已经知道 long double 等同于 double 了. 结论就是 MinGW 对 C99 引入的新标准支持不好, 别用了.
1 long double db = 1.234;
2 printf("$%Lf$\n", db);
3 /* $-0.000000$ */

参见 http://stackoverflow.com/questions/7134547/gcc-printf-and-long-double-leads-to-wrong-output-c-type-conversion-messes-u

参考文献


  1. The C Programming Laguage 2nd edition
  2. C 专家编程
  3. C 陷阱和缺陷
  4. ISO-C-FDIS.1999-04.pdf
  5. http://www.cplusplus.com/reference/cstdio/printf/?kw=printf

原文链接: https://www.cnblogs.com/elitegoblin/archive/2013/06/07/3123964.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/91589

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月10日 上午1:15
下一篇 2023年2月10日 上午1:15

相关推荐