C++_Primer06.function
函数
自动对象(automatic object):只存在于块执行期间的对象;当块执行结束后,块中创建的自动对象的值就变成未定义的了
局部静态对象(local static object):在程序执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁;其生命周期贯穿函数调用及之后的时间
分离式编译(seperate compilation):允许我们把程序分割到几个文件中去,每个文件独立编译
对象代码(object code):编译每个文件生成各自的对象代码
CC -c factMain.cc # 生成 factMain.o 文件
参数传递
形参的类型决定了形参和实参的交互方式,如果形参是引用类型,则它将被绑定到对应的实参上;否则实参的值拷贝后赋给形参
当形参是引用类型时,我们说它对应的实参被 引用传递(passed by reference)或函数被 传引用调用(called by reference)
引用形参是它绑定的对象的别名,即引用形参是它对应的实参的别名
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。这样的实参是被 值传递(passed by value)或者函数被 传值调用(called by value)
传值参数
当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。
指针形参
当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。这样可以间接地访问它所指的对象。
引用形参
引用类型的形参,形参是直接绑定到实参上的,修改形参等于修改了实参。
void reset(int &i){
i = 0;
}
int main(int argc, char** argv){
int j = 50;
reset(j);
cout << j << endl; // 0
return 0;
}
使用引用避免拷贝
使用 const 限定符防止实参被修改
void reset1(int& i){
i = 0;
}
void reset2(const int& i){
cout << "i in reset2: " << i << endl;
}
int main(int argc, char** argv){
int i = 50;
int j = 60;
reset1(i);
cout << i << endl;
reset2(j);
cout << j << endl;
return 0;
}
编译运行:
$ g++ --std=c++11 -o ref ref.cpp
$ ./ref
0
i in reset2: 60
60
使用形参返回额外信息
// 返回字符串中某个指定字符第一次出现的位置,并计算该字符出现的总次数
string::size_type find_char(const string& s, char c, string::size_type& occurs){
auto ret = s.size();
occurs = 0;
for (decltype(ret) i = 0; i != s.size(); ++i){
if (s[i] == c){
if (ret == s.size()){
ret = i;
}
++occurs;
}
}
return ret;
}
尽量使用常量引用
使用引用而非常量引用会极大地限制函数所能接受的实参类型:
string::size_type find_char1(string& s, char c, string::size_type& occurs);
string::size_type find_char2(const string& s, char c, string::size_type& occurs);
find_char1("Hello World", '0', ctr); // 错误,只能接受 string类型,不能接受字面量(const string) 类型
find_char2("Hello World", '0', ctr); // 正确
另外,如果主调函数中的字符串 s 是个常量,则只能调用 find_char2 函数:
bool is_sentence(const string& s){
string::size_type ctr = 0;
return find_char2(s, '.', ctr) == s.size() - 1 && ctr == 1;
}
数组形参
传递数组时,实际上传递的是指向数组首元素的指针
// 这三种函数定义方式等价
void print(const int*);
void print(const int[]);
void print(const int[10]); // 这里的维度表示我们期待数组含有多少个元素,实际上不一定
因为数组是以指针的形式传递给函数,所以一开始函数并不知道数组的确切大小,调用者应该为此提供一些额外的信息,一般有三种常用技术:
- 使用标记指定数组长度
void print(const char* cp){
if (cp){
while (*cp){
cout << * cp++ << endl;
}
}
}
- 使用标准库规范
void print(const int* beg, const int* end){
while (beg != end){
cout << * beg++ << endl;
}
}
- 显式传递一个表示数组大小的形参
void print(const int ia[], size_t size){
for (size_t i = 0; i < size; ++i){
cout << ia[i] << endl;
}
}
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向 const 指针
只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针
数组引用形参
对数组引用时要注意不要写成引用数组
void print(int (& arr)[10]){
for (auto i : arr){
cout << i << endl;
}
}
含有可变形参的函数
C++11 提供了两种方法:
- 如果所有的实参类型相同,可以传递一个名为 initializer_list 的标准库类型
- 如果实参类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板
另外,C++ 还有一种特殊的形参类型,即省略符,可以用它传递可变数量的实参,不过这种功能一般只用于与 C 函数交互的接口程序
initializer_list
类似 vector,initializer_list 也是一种模板类型,定义 initializer_list 对象时,必须说明列表中所含元素的类型:
initializer_list<string> ls;
initializer_list<int> li;
initiliazer_list 提供的操作:
| 操作 | 说明 |
|---|---|
| initializer_list |
默认初始化;T类型元素的空列表 |
| initializer_list |
列表初始化 |
| lst2(lst) | 拷贝或赋值,但不会拷贝列表中的元素;拷贝后原始列表和副本共享元素 |
| lst2 = lst; | 同上 |
| lst.size() | 元素数量 |
| lst.begin() | 指向首元素 |
| lst.end() | 指向尾元素下一个位置的指针 |
定义不定数量形参的函数:
void error_msg(initializer_list<string> il){
for (auto beg = il.begin; beg != il.end(); ++beg){
cout << * beg << " ";
}
cout << endl;
}
使用该函数:
if (expected != actual){
error_msg({"functionX", expected, actual});
} else {
error_msg({"functionX", "okay"});
}
initializer_list 形参也可以和其他参数同时使用。比如调试系统可能有个名为 ErrCode 的类用来表示不同类型的错误:
void error_msg(ErrCode e, initializer_list<string> il){
cout << e.msg() << ": ";
for (auto& ele : il){
cout << ele << " ";
}
cout << endl;
}
调用:
if (expected != actual){
error_msg(ErrCode(42), {"functionX", expected, actual});
} else {
error_msg(ErrCode(42), {"functionX", "okay"});
}
省略符形参
省略符形参是为了便于 C++ 调用 C 代码而设置的,这些代码使用了名为 varargs 的 C 标准库功能。
省略符形参应当仅仅用于 C++ 和 C 代码的通用类型
大多数类型的对象在传递给省略符形参时都无法正确拷贝
省略符形参只能出现在形参列表的最后一个位置,它有如下两种形式:
// 省略符形参对应的实参无须类型检查
void foo(param_list, ...); // 逗号可选
void foo(...);
返回类型和 return 语句
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量
// 给定计数值,单词和结束符,根据计数值大于1则返回复数形式,否则返回单词圆形
string make_plural(size_t ctr, string& word, string& ending){
return (ctr > 1) ? word + ending : word;
}
该函数的返回类型是 string,意味着返回值将被拷贝到调用点。因此,该函数将返回 word 的副本或者 word 与 ending 和组成的临时 string 对象。
如果函数返回引用,则该引用仅是它所引对象的一个别名:
const string& shorterString(const string& s1, const string& s2){
return s1.size() > s2.size() ? s2 : s1;
}
形参和返回类型都是 const string 的引用,所以不管是调用函数还是返回结构都不会真正拷贝 string 对象
不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间也会被释放掉。因此函数终止意味着局部变量的引用将指向不再有效的内存区域:
// 错误示范
const string& manip(){
string ret;
if (!ret.empty()){
return ret; // 错误:ret是局部对象
} else {
return "Empty"; // 错误:"Empty" 是一个局部临时量
}
}
类似地,返回局部对象的指针也是错误的,一旦函数完成,局部对象被释放,指针将指向一个不存在的对象
引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。我们可以像使用其他左值那样来使用返回引用的函数的调用。特别的,我们能为返回类型是非常量引用的函数的结果赋值:
char& get_val(string& str, string::size_type idx){
return str[idx]
}
int main(int argc, char** argv){
string s("a value");
cout << s << endl; // a value
get_val(s, 0) = 'A';
cout << s << endl; // A value
return 0;
}
列表初始化返回值
C++11 规定,函数可以返回列表。如果列表为空,临时量执行值初始化,否则返回的值由返回类型决定:
// 根据不同的错误,返回不同数量的字符串
vector<string> process(){
// some code
if (expected.empty()){
return {}; // 返回一个空的 vector 对象
} else if (expected == actual){
return {"functionX", "okay"}; // 返回列表初始化的 vector 对象
} else {
return {"functionX", expected, actual};
}
}
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间;
如果函数返回的是类类型,由类本身定义初始值如何使用
主函数 main 的返回值
main 函数的返回值是个例外,它可以没有 return 语句而直接结束,此时编译器将隐式地插入一条返回0的 return 语句。
main 函数的返回值可以看做是状态指示器,返回0表示成功,返回其他值表示执行失败。
返回数组指针
数组不能被拷贝,所以函数不能返回数组。但是函数可以返回数组的指针或引用
类型别名
最直接的方法是使用类型别名:
typedef int arrT[10];
// 等价于
using arrT = int[10];
// 使用:
arrT* func(int i); // func 返回一个指向含有10个整数的数组的指针
声明一个返回数组指针的函数
想要在声明 func 时不使用类型别名,必须要牢记数组的维度,并且数组的维度必须跟在函数名之后:
Type (*function(parameter_list))[dimension];
如果没有外层的括号把 * 和函数名括起来,则函数的返回值将是指针的数组
int (* func(int i))[10];
使用尾置返回类型
trailing return type. C++11 新标准,简化上述声明的方法,任何函数的定义都能使用尾置返回,但这种形式对于返回类型比较复杂的函数最有效:
auto func(int i) -> int(* )[10];
func 接受一个int类型的参数,返回一个指针,指向一个含有10个整数的数组
使用 decltype
int odd[] = {1,2,3};
int even[] = {4,5,6};
// 返回一个指针,指向含有3个整数的数组
decltype(odd)* arrPtr(int i){
return (i%2) ? &odd : &even
}
函数重载
overload. 同一个作用域内,函数名相同,而形参列表不同
main 函数不能重载
const 形参的重载
顶层 const 不影响传入函数的对象,所以带顶层 const 的形参和不带的形参不属于重载:
// 以下两个等价:
Record lookup(Phone);
Record lookup(const Phone);
// 以下两个等价:
Record lookup(Phone*);
Record lookup(Phone* const);
对于指针或引用形参,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,因为它们的 const 是底层的:
// 以下两个属于重载:
Record lookup(Phone&);
Record lookup(const Phone&); // 常量引用
// 以下两个属于重载:
Record lookup(Phone*);
Record lookup(const Phone*); // 指向常量的指针
const_cast 和重载
const string& shorterString(const string& s1, const string& s2){
return s1.size() <= s2.size() ? s1 : s2;
}
上述函数的参数和返回类型都是 const string 的引用,我们想要对两个非常量的 string 实参调用这个函数,但返回的结果仍然是 const string 的引用,此时我们需要重载,当它的实参不是常量时,得到的结果是一个普通引用:
string& shorterString(string& s1, string& s2){
auto& r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
完整代码:
#include <iostream>
#include <string>
using namespace std;
const string& shorterString(const string& s1, const string& s2);
string& shorterString(string& s1, string& s2);
int main(int argc, char** argv){
string s1 = "abc";
string s2 = "abcd";
cout << shorterString(s1, s2) << endl;
return 0;
}
const string& shorterString(const string& s1, const string& s2){
return s1.size() <= s2.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<string&>(r);
}
调用重载的函数
函数匹配(function matching),或重载确定(overload resolution):编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。
当调用重载函数时,有三种可能的结果:
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误
- 有多于一个函数可以匹配,但每一个都不是明显的最佳选择,此时也将发生错误,称为二义性调用(ambiguous call)
特殊用途语言特性
- 默认实参
- 内联函数
- constexpr 函数
默认实参
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char background=' ');
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
当设计含有默认实参的函数时,应当尽量让不怎么使用默认值的形参出现在前面,让经常使用默认值的形参出现在后面
默认实参声明
函数的声明通常是将其放在头文件中,并且一个函数只声明一次,但多次声明同一个函数也是合法的。
函数的后续声明只能为那些没有默认值的形参添加默认实参,而且该形参的右侧所有形参都必须有默认值:
// 已有函数声明
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);
用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时:
void f2(){
def = '*'; // 改变了实参的默认值
sz wd = 100; // 隐藏了外层定义的 wd,但没有改变默认值
window = screen(); // screen(ht(), 80, '*')
}
内联函数和 constexpr 函数
在大多数机器上,一次函数调用其实包含着一系列工作:
- 调用前要先保存寄存器,并在返回时恢复;
- 可能需要拷贝实参;
- 程序转向一个新的位置继续执行
内联函数可以避免函数调用的开销。如果把 shorterString 函数定义成内联函数,则以下调用在编译时会展开:
cout << shorterString(s1, s2) << endl;
// 在编译时展开成:
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
在 shorterString 函数的返回类型前面加上关键字 inline,就可以将它声明成内联函数了:
inline const string&
shorterString(const string& s1, const string& s2){
return s1.size() <= s2.size() ? s1 : s2;
}
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数。
constexpr 函数
constexpr 函数是指能用于常量表达式的函数。
定义的方法与其他函数类似,不过要遵循几个约定:
- 函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句
constexpr int new_sz(){return 42;}
constexpr int foo = new_sz();
编译器在编译阶段把对 constexpr 函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数
constexpr 函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。比如空语句、类型别名以及 using 声明
我们允许 constexpr 函数的返回值并非一个常量:
constexpr size_t scale(size_t cnt){return new_sz() * cnt;}
// 如果实参是常量表达式,则它的返回值也是常量表达式;反之则不然:
int arr[scale(2)]; // 正确,scale(2) 是常量表达式
int i = 2;
int a2[scale(i)]; // 错误,scale(i) 不是常量表达式
当把 scale 函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求。constexpr 函数不一定返回常量表达式。
const 与 constexpr:
const并未区分出编译期常量和运行期常量
constexpr限定在了编译期常量
constexpr修饰的函数,返回值不一定是编译期常量
constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。
和其他函数不一样的是,内联函数和 constexpr 函数可以在程序中多次定义。不过对于某个给定的内联函数或 constexpr 函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和 constexpr 函数通常定义在头文件中。
调试帮助
assert 预处理宏
所谓预处理宏其实是一个预处理变量,其行为类似内联函数。它的用法:
assert(expr);
首先对 expr 求值,如果表达式为假,assert 输出信息并终止程序的执行;如果为真则什么也不做。
assert 宏定义在 cassert 头文件中,预处理名字由预处理器而非编译器管理,所以我们可以直接使用而不需要提供 using 声明。宏名字在程序内必须唯一,含有 cassert 头文件的程序不能再定义名为 assert 的变量、函数或其他实体。
NDEBUG 预处理变量
assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。如果定义了 NDEBUG,则 assert 什么也不做。默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。
关闭调试状态的方法有两种:
- 使用
#define语句定义 NDEBUG - 使用编译器提供的命令行选项:
$ CC -D NDEBUG main.C # 对于 windows,使用 /D
这等价于在 main.C 文件的一开始写 #define NDEBUG
assert 应该仅用于验证那些确实不可能发生的事情。我们可以把它当做调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。
也可以利用 NDEBUG 编写自己的条件测试代码:
void print(const int ia[], size_t size){
#ifndef NDEBUG
// __func__ 是编译器定义的一个局部静态变量,用于存放函数的名字
cerr << __func__ << ": array size is " << size << endl;
#endif
// ...
}
除了 __func__ 外,预处理器还定义了另外4个用于程序调试的很有用的变量:
| 变量名 | 说明 |
|---|---|
__FILE__ |
存放文件名的字符串字面量 |
__LINE__ |
存放当前行号的整型字面值 |
__TIME__ |
存放文件编译时间的字符串字面值 |
__DATE__ |
存放文件编译日期的字符串字面值 |
函数匹配
函数匹配顺序
以如下函数组为例:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // 调用 void f(double, doube)
确定候选函数和可行函数
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为 候选函数(candidate function)。候选函数具备两个特征:函数名相同;其声明点在调用点可见。此时有4个名为 f 的候选函数。
第二步是考察实参,然后从候选函数集中选出这组实参调用的函数,这些新选出的函数称为 可行函数(viable function)。可行函数也有两个特征个:形参数量相同;每个实参与形参类型相同或能转换成形参的类型。此时根据参数数量排除掉两个,剩下第2个和第4个。
如果没找到可行函数,编译器将报告无匹配函数的错误
寻找最佳匹配
第三步是从可行函数中选择与本次调用最匹配的函数。逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。如果调用 f(int),实参不得不从 double 转换成 int。而另一个 f(double, double) 则与实参精确匹配。因此编译器把 f(5.6) 解析成对含有两个 double 形参的函数的调用。
精确匹配需要的条件是有且只有一个函数满足下列条件,则匹配成功:
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
- 至少有一个实参的匹配优于其他可行函数提供的匹配
如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报二义性调用的信息。
实参类型转换
编译器将实参到形参的转换划分为几个等级,具体顺序:
- 精确匹配
- 类型相同
- 实参从数组类型或函数类型转换成对应的指针类型
- 向实参添加顶层 const 或从实参中删除顶层 const
- 通过 const 转换实现的匹配
- 通过类型提升实现的匹配
- 通过算数类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
需要类型提升和算数类型转换的匹配
内置类型的提升和转换可能在函数匹配时产生意想不到的结果
在设计良好的系统中,函数很少出现类似下面例子的形参
void ff(int);
void ff(short);
ff('a'); // char 提升为 int,调用 ff(int)
void manip(long);
void manip(float);
manip(3.14); // 错误:二义性调用
函数指针
声明一个可以指向该函数的指针,只需要用指针替换函数名即可:
// 函数原型
bool lengthCompare(const string&, const string&);
// 定义 pf
bool (* pf)(const string&, const string&);
// 给 pf 赋值,使 pf 是指向 lengthCompare 的指针;以下两种写法等价:
pf = lengthCompare;
pf = &lengthCompare;
pf = 0; // 使 pf 不指向任何函数
调用函数指针,以下三种写法等价:
bool b1 = pf("hello", "goodbye");
bool b2 = (* pf)("hello", "goodbye");
bool b3 = lengthCompare("hello", "goodbye");
函数指针形参
不能定义函数类型的形参,但形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:
// 两种定义方式等价
void useBigger(const string&, const string&, bool pf(const string&, const string&));
void useBigger(const string&, const string&, bool (*pf)(const string&, const string&));
我们可以直接把函数作为实参使用,此时它会自动转换成指针:
useBigger(s1, s2, lengthCompare);
直接使用函数名显得冗长,可以使用类型别名和 decltype 来简化:
// 函数类型别名
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; // 与上一行等价
// 函数指针的类型别名
typedef bool (* FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; // 与上一行等价
// 用函数指针当做形参
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, FuncP2); // 与上等价
返回指向函数的指针
要想声明一个返回函数指针的函数,最简单的方法是使用类型别名:
using F = int(int*, int); // F 是函数类型,不是指针
using PF = int(* )(int*, int); // PF 是指针类型
与函数类型的形参不一样的是,返回类型不会自动地转换成指针,必须显式地将返回类型指定为指针:
PF f1(int); // 正确
F f1(int); // 错误,F不是指针
F* f1(int); // 正确
或者,直接把函数指针写在返回类型上:
int (* f1(int))(int*, int);
从内向外,f1 是个函数,需要一个 int 参数
返回的是一个指针,指向一个函数,这个函数有两个参数
第一个参数是 int 指针,第二个参数是 int 值,返回值是 int
使用 auto 和 decltype 用于函数指针
如果我们明确知道返回的函数是哪一个,就能使用 decltype 简化书写
string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
// 根据形参的取值,getFunc 函数返回指向 sumLength 或 largerLength 的指针
decltype(sumLength)* getFunc(const string&);
练习 6.54
- 编写函数的声明,令其接受两个int,返回int,然后声明一个vector对象,令其元素指向该函数的指针。
- 编写四个函数,分别是加减乘除,将它们保存在上述 vector 中
- 调用这四个函数
#include <iostream>
#include <vector>
using namespace std;
int add(int, int);
int subtract(int, int);
int multiply(int, int);
int divide(int, int);
typedef int(* pf)(int, int);
using pf1 = int(* )(int, int);
int main(int argc, char** argv){
vector<int(* )(int, int)> v1;
v1.push_back(&add);
v1.push_back(&subtract);
v1.push_back(&multiply);
v1.push_back(÷);
cout << v1[0](5, 2) << endl;
cout << v1[1](5, 2) << endl;
cout << v1[2](5, 2) << endl;
cout << v1[3](5, 2) << endl;
return 0;
}
int add(int a, int b){
return a + b;
}
int subtract(int a, int b){
return a - b;
}
int multiply(int a, int b){
return a * b;
}
int divide(int a, int b){
return a / b;
}
output:
7
3
10
2

浙公网安备 33010602011771号