C++ Primer 06 函数

函数

1 函数基础

形参和实参

实参时形参的初始值。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。

函数的形参列表

函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与 C 语言兼容,也可以使用关键字 void 表示函数没有形参

void f1() { }  // 隐式地定义空形参列表
void f2(void) { }  // 显式地定义空形参列表
函数返回类型

大多数类型都能用作函数的返回类型。一种特殊的返回类型是 void,它表示函数不返回任何值。函数返回类型不能是数组类型或函数类型,但是可以是指向数组或函数的指针。

1.1 局部对象

在C++语言中,名字有作用域,对象有生命周期。

  • 名字的作用域是程序文本的一部分,名字在其中可见。
  • 对象的生命周期是程序执行过程中该对象存在的一段时间。

在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。局部变量的生命周期依赖于定义的方式。

自动对象

对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当达到定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为 自动对象。当快的执行结束后,块中创建的自动对象的值就变成未定义的了。

形参就是一种自动对象。

局部静态对象

有时候,需要让局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成 static 类型从而获得这样的对象。 局部静态对象 在程序执行路径第一次经过对象定义语句时初始化,并指导程序终止才销毁,在此期间即使对象所在的程序结束执行也不会对它有影响。

size_t count() {
	static size_t ctr = 0;  // 调用结束后,这个值依然有效
	return ++ctr;
}
int main() {
	for (size_t i = 0; i != 10; i++)
		cout << count() << endl;
	return 0;
}

这段程序将输出从1到10的数字。

注: 如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。

1.2 函数声明

函数在使用之前需要先进行声明,函数只能定义一次,但可以声明多次。如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。

函数声明和函数定义非常类似,唯一的区别时函数声明无需函数体,用一个分号替代即可。因为函数的声明不包含函数体,所以也无须形参名。但是协商形参的名字可以帮助使用者更好的理解函数的功能。

void print(int, char);

函数的三要数 (返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作 函数原型

在头文件中进行函数声明

尽管函数的声明放在该函数的源文件中是合法的,但如果把函数声明放在头文件中,就能确保同一函数的所有生命保持一致。而且一旦我们想改变函数的接口,只需改变一条声明即可。

注: 含有函数声明的头文件应该包含到定义函数的源文件中,由编译器负责验证函数的定义和声明是否匹配。

1.3 分离式编译

C++ 支持分离式编译。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。


2 参数传递

形参的类型决定了形参和实参的交互方式,如果形参是引用类型,它将绑定到对应的实参上(实参被引用传递 或 函数被传引用调用);否则,将实参的值拷贝后赋给形参(实参被值传递 或 函数被传值调用)。

2.1 传值参数

指针形参

指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针,但是指向的地址是同一个地址。

注: 熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。C++语言中建议使用引用类型的形参替代指针。

2.2 传引用参数

使用引用避免拷贝

拷贝大的类类型对象或者容器对象,会比较低效,甚至有的类类型(包括 IO 类型在内) 根本不支持拷贝操作。当遇到这些情况的适合,函数只能通过引用形参访问该类型对象。
注1: 如果函数无需改变引用形参的值,最好将其声明为常量引用。
注2: 可以通过使用引用形参返回额外信息。

2.3 const 形参和实参

当形参是 const 时,必须要注意关于顶层 const 的讨论,当实参初始化形参时会忽略顶层 const。当形参有顶层 const 时,传给它常量对象或非常量对象都是可以的。

void fcn(const int i) { /* fcn能够读取i,但是不能向i写值 */ }
void fcn(int i) { /* .. */ }  // 错误 重复定义了 fcn(int)

虽然C++允许定义若干具有相同名字的函数,不过前提是不同的形参列表,因为顶层 const 被忽略了,所以上述两个 fcn 函数的参数完全一样。

注: 形参的初始化方式和变量的初始化方式是一样的。

2.4 数组形参

数组有两个特殊性质: 1. 不允许拷贝数组 2. 使用数组时会将其装换成指针。所以我们不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:

void print (const int*);
void print (const int[]);
void print (const int[10]);  // 这里的维度表示我们期望数组含有多少元素,实际不一定

尽管标新啊形式不同,但上面上个函数是等价的:每个函数的唯一形参都是 const int * 类型的。当编译器处理对 print 函数的调用时,只检查传入的参数是否是 const int * 类型。

int i = 0, j[2] = {0, 1};
print(&i);  // 正确,&i 的类型是int*
print(j);  // 正确,j 转换成int* 并指向 j[0]

以数组作为形参的函数也必须确保使用数组是不会越界,由于数组是以指针的形式传递给函数的,所以函数并不知道数组的确切尺寸,所以调用者需要提供一些额外的信息,管理指针形参有三种常用的技术:

  • 使用标记指定数组长度: 要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串,这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。
  • 使用标准库规范: 使用这种方法我们需要传入两个指针,指向数组首元素的指针 begin 和 尾元素下一个位置的指针 end。
  • 显式传递一个表示数组大小的形参: 专门定义一个表示数组大小的形参,这种方法比较常用。
数组引用形参

C++ 允许将变量定义成数组的引用,所以形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上

f(int &arr[10])  // 错误: 将 arr 声明称了引用的数组
f(int (&arr)[10])  // 正确:arr 是具有10个整数的整型数组的引用
传递多维数组

将多维数组传递给函数时,真正传递的是指向数组首元素的指针,因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维的大小都是数组类型的一部分,不能省略。

// matrix 指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) {/* ... */}

上述语句将 matrix 声明成指向含有10个整数的数组的指针。

// 等价定义
void print(int matrix[][10], int rowSize) {/* ... */}

matrix 的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。

2.5 main:处理命令行选项

一般情况下,我们定义的 main 函数只有空形参列表

int main() { ... }

然而,有时我们确实需要给 main 传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作,这些命令行选项通过两个(可选的)形参传递给 main 函数

int main(int argc,char *argv[]) { ... }

第一个形参 argc 表示十足中字符串的数量,第二个形参 argv 是数组。因此 main 函数也可以定义成:
注: 如果未标明的话就是只有一个代表本函数,命令行参数的输入要从命令行输入。

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

当实参传给 main 函数之后,argv 的第一个元素指向程序的名字或者一个空字符串,接下来的元素一次传递命令行提供的实参,最后一个指针之后的元素值保证为0。
注: 当使用 argv 中的实参时,一定要记得可选的实参从 argv[1]开始;argv[0]保存程序的名字,而非用户的输入。

2.6 含有可变形参的函数

解决问题:为了编写能处理不同数量实参的函数,C++11 新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为 initializer_list 的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板。

initializer_list 形参

如果所有的实参类型相同,可以传递一个名为 initializer_list 类型的形参,initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。其定义在同名的头文件中,操作如下:

类型含义
initializer_list<T> lst默认初始化,T类型元素的空列表
initializer_list<T> lst{a, b}lst元素和初始值一样多,是对应初始值的副本
las2(lst1)拷贝或赋值 一个 initializer_list 对象
las2 = lst1拷贝或赋值 一个 initializer_list 对象
lst.size()列表中的元素数量
lst.begin()返回lst首元素的指针
lst.end()返回lst尾元素下一位置的指针

注: 拷贝一个 initializer_list 对象并不会拷贝里面的元素。其实只是引用而已。而且里面的元素全部都是const的。

initializer_list只是一种模板类型,所以定义 initializer_list 对象时,必须说明列表中所含元素的类型。 但是 initializer_list 对象中的元素永远是常量值,我们无法改变initializer_list 对象中元素的值。

如果想向 initializer_list 形参中传递一个值的序列,则必须把序列放在一对花括号内

// expected 和 actual 是 string 对象
if (expected != actual)
	 error_mag({"functionX", expected, actual});
else
	error_mag({"functionX", "okay"});
省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为 varargs 的C标准库功能。
注: 省略符形参应该仅仅用于C和C++通用的类型。特别注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

省略符形参只能出现在形参列表的最后一个位置,它的形式有以下两种

void foo(parm_list, ...);
void foo(...);

3 返回类型和return语句

3.1 有返回值的函数

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

函数完成后,它所占用的存储空间也随之被释放掉。因此函数终止意味着局部变量的引用将指向不再有效的内存区域,所以说不要返回局部对象的引用或指针。

引用返回左值

函数的返回类型决定函数调用是否为左值。调用一个返回引用的函数得到左值,其他返回类型得到的右值。可以向使用其他左值那样来使用返回引用的函数的调用,特别是,能为返回类型是非常量引用的函数的结果赋值:

char &get_val(string &str, string::size_type ix){
	return str[ix];  // 假定索引值是有效的
}
int main() {
	string s("a value");
	get_val(s, 0) = 'A';  // 将s[0]的值改为A
	renturn 0;
}

如果返回类型是常量引用,我们不能给调用的结果赋值。
注: 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(内存中的位置)。

列表初始化返回值

C++11 新标准规定,函数可以返回花括号包围的值的列表。

vector<string> process() {
	// expected 和 actual 是 string 对象
	if (expected.empty())
		return {};  // 返回一个空 vector 对象
	else if (expected == actual)
		return {"functionX", "okay"};
	else
		return {"functionX", expected, actual};
}

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。

主函数main的返回值

如果函数的返回类型不是 void,那么它必须返回一个值,但是有个例外:main 函数可以没有 return 语句直接结束,此时编译器将隐式地插入一条返回 0 的 return 语句

3.2 返回数组指针

因为数组不能被拷贝,所以函数不能返回数组,不过函数可以返回数组的指针或引用。要想返回数组指针可以使用以下几种方法。

类型别名

要想定义一个返回数组的指针或引用的函数比较繁琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名

typedef int arr[10];  // arr是一个类型别名,它表示含有10个整数的数组
using arr = int[10];  // arr的等价声明
arr* func(int i);  // func 返回一个指向含有10个整数的数组的指针
声明一个返回数组指针的函数
int arr[10];  // 含有10个整数的数组
int *p1[10];  // 含有10个指针的数组
int (*p2)[10] = &arr;  // 指向含有10个整数的数组

和这些声明一样,如果我们要定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后,函数的形参列表也跟在函数名字后面且形参列表应该先与数组的维度。因此,返回数组指针的函数形式如下:

Type (*function(parameter_list)[dimension])
int (*func(int i))[10]

Type 表示元素的类型, dimension 表示数组的大小。(*function(parameter_list)) 两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。

使用尾置返回类型

在C++11新标准中还有一种可以简化上述 func 声明的方法,就是使用 尾置返回类型。任何函数的定义都能使用位置返回,这种形式对于返回类型比较复杂的函数最为有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个 auto:

// func 接受一个 int 类型的实参,返回一个指针,指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
使用 decltype

如果知道函数返回的指针将指向哪个数组,就可以使用 decltype 关键字声明返回类型。

int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i) {
	return (i % 2) ? &odd : &enev;  // 返回一个指向数组的指针
}

attPtr 使用关键字 decltype 表示它返回类型是个指针,并且该指针所指的对象与 odd 类型一致。

decltype 并不负责把数组类型转换成对应的指针,所以 decltype 的结果是个数组,想要表示 arrPtr 返回指针还必须在函数生命是加一个 * 符号。


4 函数重载

如果同意作用域内的几个函数名字相同但形参列表不同,我们称之为 重载函数

注1: main 函数不能重载。

Record lookup(Phone);
Record lookup(const Phone);  // 重复定义了Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);  // 重复定义了Record lookup(Phone*)

注2: 顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开。

const_cast 和 重载

const_cast 在重载函数中十分有用,const_cast 可以改变运算对象的底层 const

// 比较 string 对象的长度,返回较短的那个的引用
const string &shorterString(const string &s1,const string &s2) {
	return r1.size() <= r2.size() ? s1 : s2;
}

string &shorterString(string &s1, string &s2) {
	auto &r = shorterString(const_cast<const string&>)(s1),
							const_cast<const string&>(s2));
	return const_cast<const string&>(r);
}
调用重载函数三种可能的结果
  • 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用

5 特殊用途语言特性

5.1 默认实参

某些参数有这样一种形参,在函数的很多次调用中它们都被赋予了一个相同的值,此时,我们把这个反复出现的值称为函数的 默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

默认实参声明

多次声明同一个函数是合法的,不过需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参都必须有默认值。

// 表示高度和宽度的形参没有默认值
string screen(sz, sz, char = ' ');
// 错误:重复声明  不能修改一个已经存在的默认值
string screen(sz, sz, char = '*');
// 正确:添加默认实参
string screen(sz = 24, sz = 80, char);

注: 通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。

默认实参初始值
// wd、def 和 ht 的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);

注: 局部变量不能作为默认实参,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。

5.2 内联函数

将函数指定成 内联函数,通常就是将它在每个调用点上“内联地”展开。内联机制用于优化规模较小、流程直接、频繁调用的函数,但很多编译器不支持内联递归函数。

在函数返回类型前面加上关键字 inline,就可以将它声明称内联函数

inline Type func(parameter_list) { /* ... */}

5.3 constexpr 函数

constexpr 函数是指能用于常量表达式的函数,定义 constexpr 函数的方法与其他函数类似,不过需要遵循几项约定:

  • 函数的返回类型及所有形参的类型都得是字面值类型。
  • 函数体中必须有且只有一条 return 语句。

注: constexpr 函数不一定返回常量表达式。

把内联函数和 constexpr 函数放在头文件内

内联函数和 constexpr 函数可以在程序中多次定义,不过对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此,内联函数和 constexpr 函数 通常定义在头文件中。

5.4调试帮助

C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。程序可以包含一些用于调试的代码,但这些代码 只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两种预处理功能:assertNDEBUG

assert 预处理宏

assert 是一种 预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert 宏定义在 cassert 头文件中,预处理名字由预处理器而非编译器管理,因此我们可以直接使用预处理名字。也就是说我们应该使用 assert 而不是 std::assert。assert 宏使用一个表达式作为它的条件:

assert(expr);

首先对 expr 求值,如果表达式为假(0),assert 输出信息并终止程序的执行。如果表达式为真(非0),assert 什么也做。

和预处理变量一样,宏名字在程序内必须唯一。含有 cassert 头文件的程序不能再定义名为 assert 的变量、函数或者其他实体。

assert 宏常用于检查“不能发生”的条件。如:一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阀值,

assert(word.size() > threshold);
NDEBUG 预处理变量

assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。如果定义了 NEDBUG,则 assert 什么也不做。默认状态下没有定 NDEBUG ,此时 assert 将执行运行时检查。

我们可以使用一个 #define 语句定义 NDEBUG,从而关闭调试状态。定义 NDEBUG 能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此, assert 应该仅用于验证那些确实不可能发生的事情。我们可以把 assert 当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

使用 NDEBUG 编写自己的条件调试代码
void print(const int ia[],size_t size)
{
    #ifndef NDEBUG
        //__func__是编译器定义的一个局部静态变量,用于存放函数的名字
        cerr << __func__<< ":array size is " << size <<endl;
    #endif // NDEBUG
    //...
}

在这段代码中,使用变量 __func__ 输出当前调试的函数名字,编译器为每个函数都定义了 __func__ ,它是 const char 的一个静态数组,用于存放函数的名字。

预处理器还定义了另外4个对于程序调试很有用的名字:

  • __FILE__ :存放文件名的字符串字面值;
  • __LINE__ :存放当前行号的整型字面值;
  • __TIME__ :存放文件编译时间的字符串字面值;
  • __DATE__ :存放文件编译日期的字符串字面值。

可以使用这些常量在错误消息中提供更多信息,例如:

if(str.size() < threshold)
    cerr<<"Error: "<<__FILE__
        << " : in function " << __func__
        << " at line " << __LINE__ << endl
        << "      Compile on " << __DATE__
        << " at " << __TIME__ << endl
        << "      Word read was \""<<str
        << "\":Length too short"<<endl;

在这里插入图片描述


6 函数匹配

6.1 函数匹配的步骤

  1. 选定本次调用对应的重载函数集,集合中的函数称为 候选函数候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
  2. 考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为 可行函数可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
    注1: 如果函数有默认实参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。
    注2: 如果没找到可行函数,编译器将报告误匹配函数的错误。
  3. (如果有匹配函数的话)从可行函数中选择与本次调用最匹配的函数(最佳匹配)。在这一过程中,逐检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。基本思想是:实参类型与形参类型越接近,它们匹配得越好。

如果有多个可行函数,但是没有任何一个函数脱颖而出,则该调用时错误的,编译器将报告二义性调用的信息。
注: 调用重载函数时,应尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。

6.2 实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:

  1. 精确匹配,包括以下情况:
    • 实参类型和形参类型相同。
    • 实参从数组类型或函数类型转换成对应的指针类型。
    • 向实参添加顶层const或者从实参中删除顶层const。
  2. 通过const转换实现的匹配。
  3. 通过类型提升实现的匹配。
  4. 通过算术类型转换或指针转换实现的匹配。
  5. 通过类类型转换实现的匹配。

7 函数指针

函数指针指向的时函数而非对象,函数的类型由它返回类型和形参类型共同决定,与函数名无关。

bool lengthCompare(const string&, const string&);

声明一个可以指向该函数的指针,只需要用指针替换函数名即可

// pf指向一个函数,该函数的参数时两个 const string 的引用,返回值是 bool
bool (*pf)(const string &, const string &);

*pf 两端的括号必不可少的。如果不写这对括号,则 pf 是一个返回值为 bool 指针的函数。

// 声明一个名为pf的函数,该函数返回bool*
bool *pf(const string &, const string &);
使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针。

pf = lengthCompare;  // pf 指向名为 lengthCompare 的函数
pf = &lengthCompare;  // 等价的赋值语句,取地址符是可选的

直接使用指向函数的指针调用该函数,无需提前解引用指针

bool b1 = pf("hello", "goodbye");  // 调用 lengthCompare 函数
bool b2 = (*pf)("hello", "goodbye");  // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye");	// 另一个等价的调用

在指向不同函数类型的指针间不存在转换规则。

可以为函数指针赋一个 nullptr 或者值为 0 的整形常量表达式,表示该指针没有指向任何一个函数。

重载函数的指针

如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个 精确匹配

函数指针作为形参

不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针来用

// 第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2,
          bool pf(const string&, const string&));
// 等价的声明:显式地将形参定义成指向函数的指针
void useBigger(const string &s1, const string &s2,
          bool (*pf)(const string&, const string&));

也可以直接把函数作为实参使用,此时它会自动地转换成指针

// 自动将函数 lengthCompare 转换成指向该函数的指针
useBigger(s1, s2, lengthCompare);
返回指向函数的指针

与返回数组指针方法一致。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值