C++命名空间 缺省参数 const总结 引用总结 内联函数 auto关键字详解
一只少年AAA 人气:0命名空间
概述
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
举个例子:
#include <stdio.h> #include <stdlib.h> int rand = 10; int main() { printf("%d", rand); return 0; }
程序编译的结果显示rand重定义了,为什么会这样呢?因为在stdlib.h这个头文件中已经定义了rand这样一个函数,这样就导致了编译器不知道这是一个函数还是一个变量,C语言中无法应对这种冲突,只能通过改名字来避免。
而C++为了解决这个问题,引入了命名空间的概念。
命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
//命名空间 //A就是命名空间的名字 namespace A{ int a; void func() {} }
注意事项:
- 命名空间只能写在全局
- 命名空间可以嵌套命名空间
- 命名空间是开放的,随时可以加入新的成员,但是新加入的成员只能在加入后使用
- 匿名命名空间就类似于static
- 命名空间可以取别名
- 分文件编写的时候,如果头文件有两个命名空间,但是里面的成员函数或者成员变量同名的时候,在cpp重实现函数需要加上命名空间
接下来给一个完整的代码块来展示命名空间的注意事项和使用:
#include<iostream> //引入头文件 #include<string>//C++中的字符串 using namespace std; //标准命名空间 //A就是命名空间的名字 namespace A { //命名空间内既可以定义变量也可以定义函数 int rand = 10; int Sub(int x, int y) { return x + y; } struct ListNode { int data; struct ListNode* next; }; } //命名空间的嵌套定义 namespace B { int rand; namespace C { int rand; } } //命名空间是开放的,随时可以加入新成员,但是新成员只能在加入后使用 namespace B { int c;//此时c会合并到命名空间B中,实际上就是个合并的过程 } //匿名命名空间 namespace { int d = 5;//命名空间没有名字,就类似于static int d = 50,是个静态全局变量,别的文件无法使用 } int main() { //命名空间的使用 //1.::作用域限定符 //访问A空间的Sub函数 cout << A::Sub(10, 20) << endl; //2.访问嵌套空间 //访问B空间的C空间的rand变量 B::C::rand = 5; cout << B::C::rand << endl; system("pause"); return EXIT_SUCCESS; }
using关键字
引入using关键字之后,命名空间的使用又变得不一样
- 用using将命名空间的成员引入
namespace A { int a = 10; } void test01() { //using声明的意思就是让命名空间中某个标识符可以直接使用 using A::a; cout<<a<<endl; }
注意:
1.使用using声明,就不会每次写A::a了,直接用a就可以
2.using A::a声明的意思就是把变量a又在test函数中定义了一次,此时如果在test内部再定义一个int a;就会出错
- 用using namespace 命名空间名称引入
namespace A { int a = 10; } using namespace A; void test01() { cout<<a<<endl; }
使用using关键字修饰namespace整个命名空间,实际上就是脱去了这个命名空间的外衣,就等价于你定义了一个int a在全局
思考一个问题:下面代码有错吗?
在test01函数体内又定义了一个int a,会报错么?如果不报错,那么输出的是全局的 a = 10 还是局部的a = 20?
namespace A { int a = 10; } using namespace A; void test01() { int a = 20; cout<<a<<endl; }
答案是不会报错,输出的是局部的20,因为命名空间A内部的变量a在使用using关键字后相当于在全局定义了一个int a ;而在函数体内定义一个局部的 int a;两个变量的作用域不同,是可以定义同名变量的,输出的是局部变量的值,小伙伴的要注意区分~
C++输入和输出
- C语言用的是printf和scanf进行输入和输出的。那么C++是用什么来进行输入输出的呢?
- C++用到的是cout(控制台)和cin(键盘)两个函数进行操作,使用是必须包含iostream的头文件及 std标准命名空间。
- C++头文件不带.h,将std标准命名空间进行展开。
#include <iostream> using namespace std;// 将std标准命名空间进行展开 int main() { cout << "hello world" << endl; // std::cout << "hello world" << endl; 也可以这样写就不展开std标准命名空间 return 0; }
使用C++输入输出更方便,不需增加数据格式控制,比如:整形–%d,字符–%c
int main() { int a = 0; double b = 0.0; cin >> a; cin >> b; cout << "a = " << a << " b = " << b << endl; system("pause"); return EXIT_SUCCESS; }
运行结果如下:
缺省参数
概念:
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
void PrintNum(int n = 0) { cout << n << endl; } int main() { PrintNum();// 无参数时,使用参数的默认值 PrintNum(10);// 有参数时,使用指定的实参 system("pause"); return EXIT_SUCCESS; }
全缺省参数
参数都要一个默认值,给定的实参依次从左向右给形参赋值
注意:我们在调用函数时,只能缺省最右边的若干个参数,形如:Fun(4, , 6);
这种调用是错误的调用方法。
void Func(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { // 实参从左向右一次给形参赋值 Func(); Func(1); Func(1, 2); Func(1, 2, 3); system("pause"); return EXIT_SUCCESS; }
运行结果如下:
半缺省参数
只有部分形参给定了默认值,半缺省参数必须从右往左依次来给出,不能间隔着给。
值得注意的是,缺省参数只能为最右边的若干个。
形如:void Fun(int a=10, int b, int c = 30) { }
这样的语句是错误的用法。
形如:Fun(1, ,3)
这种调用也是错误的。
void Func(int a, int b = 10, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func(1); Func(1, 2); Func(1, 2, 3); system("pause"); return EXIT_SUCCESS; }
运行结果如下:
注意:
- 半缺省参数必须从右向左依次给出,不能间隔着给
- 缺省参数不能在声明中和定义中同时出现(推荐写在声明中)
- 缺省参数必须是全局变量和常量
- C语言中不支持缺省参数
const限定符
const修饰符的作用
- const类型定义: 指明变量或对象的值是不能被更新,引入目的是为了取代预编译指令
- 可以保护被修饰的东西,防止意外的修改,增强程序的健壮性
- 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高
- 可以节省空间,避免不必要的内存分配
规则
- const离谁近,谁就不能被修改
- const修饰一个变量时,一定要给这个变量初始化,若不初始化,在后面也不能初始化
分类
常变量: const 类型说明符 变量名 常引用: const 类型说明符 &引用名 常对象: 类名 const 对象名 常成员函数: 类名::fun(形参) const 常数组: 类型说明符 const 数组名[大小] 常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名
const全局/局部变量
C
在C语言中const修改全局变量是存储在全局区(即静态存储区),修饰局部变量时存储在栈区
//const修饰的常量 const int a = 10;//全局const常量,放在常量区,受到常量区的保护 void test01() { //直接修改失败 a = 100; //间接修改失败 int *p = &a; *p = 100; }
- 全局的const修饰的变量本质就是常量,全局const修饰的变量放在常量区中,不能通过变量名直接修改也不可以通过地址来间接修改
//局部conts修饰常量 void test02() { conts int b = 10;//数据放在栈区,是个伪常量 //直接修改失败 b = 100; //间接修改成功 int *p = &b; *p = 100; }
- 局部const修饰的变量是个伪常量,不是真正意义上的常量,数据存放在栈区而不是常量区,可以间接修改但是不能直接修改。
总结:
- C语言的const修饰的全局变量和局部变量都有空间
- C语言的const修饰的全局变量具有外部链接属性,可以采用extern声明在别的文件中使用
C++
在C++中编译器会自动优化,会将常量的数值直接替换(类似于宏定义),这导致了const局部变量与真实值产生了不一致。(常量折叠现象),而C语言会先去内存中寻找,然后替换
举个例子:
const int aa = 10;//没有内存 void test01() { cout << aa << endl;//在编译阶段,编译器会自动优化,将aa直接替换成常量10 const int bb = 20;//栈区 int *p = (int *)&bb; *p = 200; cout << bb << endl;//输出的还是20,还是那句话,在编译阶段代码中的bb就已经全部被替换成20,此时其实输出的是这样的cout << 20 << endl;但是变量bb此时的值已经被改变了,变成了200,但是由于编译器优化,造成了常量折叠现象 }
总结:
- C++语言的const修饰的变量有时有空间,有时没有空间(发生常量折叠,且没有对变量进行取地址操作)
- C++中,const修饰的全局变量具有内部链接属性,也就是说,无法使用别的文件的const修饰的变量,但是这种规则依旧可以打破
- const修饰的全局变量永远都没有内存,永远无法修改它的值,但是const修饰的局部变量可以有空间,可以修改它的值
编译器不能优化的情况
- 不能优化自定义数据类型
- 如果用变量给const修饰的局部变量赋值,那么编译器也不能优化
- 使用extern和voaltile关键字来阻止优化
例子一:用变量给const修饰的局部变量赋值
void test03() { int a =10; const int b = a; int *p = (int *)&b; *p = 100; cout << b << endl;//输出100 }
例子二:利用关键字阻止优化
void test04() { const volatile int a = 7; int *p = (int *)(&a); *p = 8; cout << "a=" << a << endl;//输出8 cout << "*p=" << *p; system("pause"); return 0; }
例子三:自定义数据类型不能优化
struct Maker { Maker() { a = 100; } int a; }; void test05() { const Maker ma; cout << ma.a <<endl; Maker *p = (Maker*)&ma; p->a = 200;//可以修改ma中的值 cout << ma.a << endl; }
const修饰指针和引用
const修饰指针
涉及到两个很重要的概念,顶层const和底层const
从 const 指针开始说起。const int* pInt;
和 int *const pInt = &someInt;
,前者是 *pInt
不能改变,而后者是 pInt
不能改变。因此指针本身是不是常量和指针所指向的对象是不是常量就是两个互相独立的问题。用顶层表示指针本身是个常量,底层表示指针所指向的对象是个常量。更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用;底层 const 则与指针和引用等复合类型有关,比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const 或者二者兼备。
int a = 1; int b = 2; const int* p1 = &a;//指针常量(顶层const) int* const p2 = &a;//常量指针(底层const) 1.指针常量(指针不可改,指针指向的对象可改) int a = 10; int b = 5; int * const p1 = &a; p1 = &b; //指针不可改,不合法 *p1 = b; //指针指向的对象可改,合法 2.常量指针(指针可改,指针指向的对象不可改) int a = 10; int b = 5; const int* p2 = &a; p2 = &b; //指针可改, 合法 *p2 = b; //不合法
拷贝与顶层和底层 const
int i = 0; int *const p1 = &i; // 不能改变 p1 的值,这是一个顶层 const int ci = 42; // 不能改变 ci 的值,这是一个顶层 const int *p2 = &ci; // 允许改变 p2 的值,这是一个底层 const int *const p3 = p2; // 靠右的 const 是顶层 const,靠左的是底层 const const int &r = ci; // 所有的引用本身都是顶层 const,因为引用一旦初始化就不能再改为其他对象的引用,这里用于声明引用的 const 都是底层 const
const修饰引用
常引用所引用的对象不能更新,使用方法为:const 类型说明符 &引用名
非const引用只能绑定非const对象,const引用可以绑定任意对象,并且都当做常对象
常引用经常用作形参,防止函数内对象被意外修改。对于在函数中不会修改其值的参数,最好都声明为常引用。复制构造函数的参数一般均为常引用
class Example{ public: Example(int x, int y):a(x),b(y){} Example(const Example &e):a(e.a),b(e.b){} //复制构造函数 void print(); void print() const; private: const int a,b; static const int c = 10; }; void Example::print() {cout<<"print():"<<a<<ends<<b<<endl;} void Example::print() const {cout<<"print() const:"<<a<<ends<<b<<endl;}
const修饰函数参数
const修饰参数是为了防止函数体内可能会修改参数原始对象。因此,有三种情况可讨论:
- 1、函数参数为值传递:值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原始对象,这种情况不需要将参数声明为const。
- 2、函数参数为指针:指针传递(pass-by-pointer)只会进行浅拷贝,拷贝一份指针给函数,而不会拷贝一份原始对象。因此,给指针参数加上顶层const可以防止指针指向被篡改,加上底层const可以防止指向对象被篡改。
- 3、函数参数为引用:引用传递(pass-by-reference)有一个很重要的作用,由于引用就是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时也导致可以通过修改引用直接修改原始对象(毕竟引用和原始对象其实是同一个东西),因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const。给引用加上底层const,既可以减小拷贝开销,又可以防止修改底层所引用的对象。
void Fun(const A *in); //修饰指针型传入参数 void Fun(const A &in); //修饰引用型传入参数 void func (const int& n) { n = 10; // 编译错误 }
const修饰函数返回值
const修饰函数返回值的含义和用const修饰普通变量以及指针的含义基本相同。这样可以防止外部对 object 的内部成员进行修改。
const int* func() // 返回的指针所指向的内容不能修改 { // return p; }
const成员函数和数据成员
类的常成员函数
由于C++会保护const对象不被更新,为了防止类的对象出现意外更新,禁止const对象调用类的非常成员函数。因此,常成员函数为常对象的唯一对外接口。
常成员函数的声明方式:类型说明符 函数名(参数表) const
- const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数
- const对象的成员是不能修改的,而通过指针维护的对象却是可以修改的
- const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查
class A { public: //返回值的类型是int &类型 int& getValue() const { // a = 10; // 错误 return a; } private: int a; // 非const成员变量 };
注意事项:
- 常成员函数的定义和声明都要含有const关键字
- 一个函数是否含有const关键字可以作为重载函数,const对象默认调用const函数,非const对象默认调用非const函数,如果没有非const函数,也可以调用const函数
- const函数中不能更新目的对象的任何成员(mutable修饰的变量除外,这里不展开阐述),以此方法来保证const对象不被修改
- 如果const成员函数想修改成员变量值,可以用mutable修饰目标成员变量
类的常数据成员
类的数据成员不能在任何函数中被赋值或修改,但必须在构造函数中使用初始化列表的方式赋初值
,因为const修饰的对象必须初始化。
举个例子,刚才的类如果a, b为常数据成员,则应该改写为如下形式:
class Example{ public: Example(int x, int y):a(x),b(y){} //初始化列表方式赋初值 void print(); void print() const; private: const int a,b; }; void Example::print() {cout<<"print():"<<a<<ends<<b<<endl;} void Example::print() const {cout<<"print() const:"<<a<<ends<<b<<endl;}
如果为静态常数据成员,由于不属于具体对象,所以不能在构造函数里赋值,仍然应该在类外赋值。特别地,如果静态常量为整数或枚举类型,C++允许在类内定义时指定常量值。
比如以下两种方式均合法:
class Example{ public: Example(int x, int y):a(x),b(y){} void print(); void print() const; private: const int a,b; static const int c = 10; //静态常量 };
class Example{ public: Example(int x, int y):a(x),b(y){} void print(); void print() const; private: const int a,b; static const int c; //静态常量 }; const int Example::c = 10;
const修饰类对象
用const修饰的类对象,该对象内的任何成员变量都不能被修改。
因此不能调用该对象的任何非const成员函数,因为对非const成员函数的调用会有修改成员变量的企图。
class A { public: void funcA() {} void funcB() const {} }; int main { const A a; a.funcB(); // 可以 a.funcA(); // 错误 const A* b = new A(); b->funcB(); // 可以 b->funcA(); // 错误 }
const与宏定义的区别
(1) 编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
(2) 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
const常量会在内存中分配(可以是堆中也可以是栈中)。
(4)作用范围不同
const有作用域,而define不重视作用域,默认定义处到文件结束,如果定义在指定作用域下有效的常量,那么define不能用。
(5)const 可以节省空间,避免不必要的内存分配。 例如:
#define PI 3.14159 //常量宏 const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ...... double i=Pi; //此时为Pi分配内存,以后不再分配! double I=PI; //编译期间进行宏替换,分配内存 double j=Pi; //没有内存分配 double J=PI; //再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而 #define定义的常量在内存中有若干个拷贝。
const与static的区别
static
1、static局部变量 将一个变量声明为函数的局部变量,那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中
2、static 全局变量 表示一个变量在当前文件的全局内可访问
3、static 函数 表示一个函数只能在当前文件中被访问
4、static 类成员变量 表示这个成员为全类所共有
5、static 类成员函数 表示这个函数为全类所共有,而且只能访问静态成员变量
static关键字的作用
(1)函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值
(2)在模块内的static全局变量和函数可以被模块内的函数访问,但不能被模块外其它函数访问
(3)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝
(4)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量
const关键字的作用
(1)阻止一个变量被改变
(2)声明常量指针和指针常量
(3)const修饰形参,表明它是一个输入参数,在函数内部不能改变其值
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”
引用
概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。也就好比我们给同学取了一个外号一样。
用法: 类型&引用变量名(对象名)= 引用实体;
int main() { int a = 10; int& ra = a;// ra是a的引用 cout << a << endl; cout << ra << endl; system("pause"); return EXIT_SUCESS; }
特性及注意事项
- 引用在定义的时候必须初始化
- 一个变量可有多个引用
- 引用一个引用一个实体,无法再引用其他实体
Type& ref = val;
注意事项:
- &在此不是取地址运算符,而是起到一个标识的作用
- 类型标识符指的是目标变量的类型
- 引用初始化之后不能改变
- 不能有NULL引用,必须确保引用是和一块合法的存储单元关联
int a = 10; int b = 20; //int& ra;//引用必须初始化 int& ra = a; int& rra = a; //int& ra = b;//引用一旦初始化,不能改变它的指向 //int& raaa = NULL;//NULL本身就是不合法的,不能绑定不合法的空间
数组的引用
int arr[] = {1, 2, 3, 4, 5}; //三种方式 //1.定义数组类型 typedef int(MY_ARR)[5]; MY_ARR &arref = arr;//建立引用,int &b = a //2.直接定义引用 int(&arref2)[5] = arr; //3.建立引用数组类型 typedef int(&MY_ARR3)[5]; MY_ARR3 arref3 = arr;
引用的使用场景
//1.作为函数的参数 void func(int &a, int &b) { cout << a+b << endl; } //2.引用作为函数的返回值 int& func2() { int b = 10;//不能返回局部变量的引用 int &p = b; return p;//错误的 } int& func3() { static int b = 10; return b; } void test01() { int& q = func2(); cout << q << endl; q = 100; cout << q << endl; func2() = 200; cout << q << endl; cout << func2() << endl; //--------------上面的代码都是错误的,这里解释一下 //int& q = func2();实际上就是int&q = p,但是fun2函数执行完了之后,局部变量全部被销毁了,所以int&q = 就指向了一个非法的区域,但是编译器没有检测出来,具体原因是什么不清楚 func3() = 100; cout << func3() << endl; }
引用做返回值时,一般分两种情况:返回的对象未归还系统和返回的对象归还系统。如果返回对象不归还系统,我们可以引用返回,否则就需要传值返回
想要返回局部变量:1.把局部变量写在堆区 2.把局部变量写在静态区
常引用
注意:
- 字面不能赋给引用,但是可以赋给const引用
- const修饰的引用,不能修改
//普通引用 int a = 10; int &ref = a; ref = 20; //int &ref2 = 10不能给字面量取别名,因为10这个字面量在内存中没有空间,存储在寄存器中 //常引用 const int &ref3 = 10;//可以给const修饰的引用赋予字面量 //编译器会把上面的代码变为:int tmp = 10;const int &ref3 = tmp;
内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
inline int Add(int x, int y) { return x + y; } int main() { int z = Add(1, 2); return 0; }
如果加了inline关键字,编译器在编译期间会用函数体替换函数的调用(类似于宏定义,在编译阶段就替换函数调用,将函数调用直接展开,减少了调用的时间,但是空间消耗巨大)。
下面是内敛函数的几个特性:
1.inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
2.inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联(是否称为内联函数由编译器决定)。
3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
例子:一个相同的函数,一个加了inline 关键字,一个没加,加上一个函数要执行10条指令,文两个函数分别调用1000次要执行多少条指令?
普通函数:1000+10(一次调用1次指令,加起来就是1000条,每次调用都是call函数,函数不展开就是10条)
内联函数:1000*10条指令(展开就是每次调用都是10条指令)
所以说,内联函数展开,会让程序变大,所以代码很短的函数可以考虑有内联,长函数和递归函数不适合用内联。
auto关键字
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int a = 10; auto b = a;// 自动推导b的类型为int auto c = 'c';// 自动推导类型为char cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; //auto d;必须初始化
有一下几种用法:
- auto与指针和引用结合起来使用(auto和auto*无区别)
int a = 10; // auto和auto*无区别 auto pa1 = &a; auto* pa2 = &a; auto& ra = a;// ==> int& ra = a; cout << typeid(a).name() << endl; cout << typeid(pa1).name() << endl; cout << typeid(pa2).name() << endl; cout << typeid(ra).name() << endl;
运行结果如下:
- 在同一行定义多个变量(这些变量类型必须相同,编译器只对第一个类型进行推导)
auto a = 3, b = 4; auto c = 3.4, d = 5.5; auto i =0, *p = &i;//正确,i是整型,p是整型指针 cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; cout << typeid(i).name() << endl; cout << typeid(p).name() << endl;
auto不能推导的两个常见
- auto不能作为函数的参数
- auto不能直接用来声明数组
加载全部内容