文章目录
类和对象
结构体和类
🌸结构体
//结构体
typedef struct People {
int age;
string name;
string tele;
int high;
}Peo;
- 类可以看成结构体的升级版,C 结构体中只能放成员变量,不能放成员函数,而类里面可以
- 上面 People 结构体描述了人的静态属性,如果要让 People 吃喝睡,就需要类中的函数描述该行为(即函数)
🌸什么是类
- 类可以比作蓝图,通过类可以造出实物(实例出对象)
- 我们常用的 cout 就是一个 ostream 类创建的对象
class People {
public:
void People_Eat() {
cout << "people is eating" << endl;
}
string People_InitNmae(string str) {
name = str;
cout << "people's name is " << name << endl;
}
void People_Drink(); //内外定义
private:
string name;
int age;
int high;
string tele;
}*p1, p2; //在创建类的同时建立一个类指针 p1 和一个 p2 对象
//在类外定义函数,需要加上作用域 ::
void People1::People_Drink() {
cout << "people is drinking" << endl;
}
//注意:成员函数如果在类里面定义,可能会被当成内联函数
//建议在 xx.cpp 文件中定义类,在 xx.h 文件中声明类
🌸struct 和 class 区别
C++ 把 struct 看成类,所以从类的角度看,struct 声明变量等于在类里面声明变量,如下
struct Animal {
private:
string catogory;
int age;
public:
//由于 C++ 把 struct 看成类,所以 C++ 中 struct 中也可以声明成员函数
void animal_eat() {
cout << "animal is eating" << endl;
}
};
//C++ 中的一个小细节
struct ListNode { //C 风格
int value;
struct ListNode* next;
};
struct ListNode l; //C 的结构体类型为 “struct + 结构体名字”
struct ListNode { //C++ 风格
int value;
ListNode* next;
};
//因为 struct 升级成类,ListNode 就是类名,类的类型就是类名
ListNode l;
//C++ 中 struct 和 class 都可以创建类,建议用 class
🌸类域:类的 {} 里面的就是类域的范围
- 类里的变量叫成员变量,类里的函数叫成员函数
- 类里的变量只是声明而不是定义,本质上看待声明和定义的区别就是有没有为变量创建空间
- 在实例化为对象时会创建空间,说明对象里的变量就被定义了
访问限定符
🌸public、protected、private
- public:类内外都可以访问类成员变量或函数
- protected 和 private:只能在类内访问,protected 和 private 的具体区别以后细谈
🌸访问权限(public、protected、private)的作用域从该访问权限符出现到下一个访问限定符出现为止
🌸建议将成员变量设为私有,好处:
- 可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
🌸class 默认访问权限是 private,struct 默认是 public(为了兼容 C)
struct man {
int id = 0;
string name;
void set_name(string s) {
name = s;
}
};
class woman {
int id = 1;
string name;
public:
void set_name(string s) {
name = s;
}
};
int main() {
man m;
woman w;
cout << m.id; //0
cout << w.id; //error,class 默认访问权限为 private,private 下的成员类外不能访问
//可以通过成员函数访问 private 成员变量
w.set_name("girl");
}
🌸类的大小
类的大小只计算非静态成员变量,存在内存对齐(和结构体一样)
class People {
public:
void People_Eat();
string People_InitNmae(string str);
void People_Drink();
private:
string name;
int age;
int high;
}*p1, p2:
cout << sizeof(p2) << endl; //12
class A {
static int a;
int aa;
} AA;
cout << sizeof(AA) << endl; //4,说明 static 的成员变量不计入类的大小
class B {
static int b;
int bb;
void fun(){}
} BB;
//类里面的函数放在公共代码区,不计算其占用内存
cout << sizeof(BB) << endl; //4
class C {
static int c;
int cc;
void fun1() {}
static void fun2() {}
} CC;
cout << sizeof(CC) << endl; //4,说明 static 修饰的函数也不属于类成员
❗️注意:空类的大小为1,起占位作用,标识一下有这个类,这1个字节不存有效数据
C++ 会对每个空对象分配一个字节内存,用来区分不同的空对象(不同空间地址对应不同空对象)
class empty{};
sizeof(empty); //1
empty e1, e2;
cout << sizeof(e1) << ' ' << sizeof(e2); //1 1
this 指针
🌸this 指针:指向当前对象的一个指针,可以访问对象里面的所有成员
🌸引例
- 我们知道在 C++ 中成员变量和成员函数是分开存储的,每个非静态成员函数只会延生一份函数实例(这里先不考虑静态成员函数,下面同理),也就是说多个同类型的对象会共用一块代码
- 那么:这一块代码是如何确认是那个对象调用的自己呢
- C++ 通过提供特殊的对象指针 this 解决上述问题,this 指针指向被调用的成员函数所属的对象
🌸this 指针的特点
- this 指针是隐含在每一个非静态成员函数内的一个形参,对象调用成员函数时,将对象地址作为实参传递给成员函数的 this 形参
- 所以对象中不存储 this 指针,this 指针一般存放在栈中
- this 指针可以为空
- this 指针的类型为 classname* const, 说明指针的指向无法改变
🌸this 指针存放的地方
this 可以简单理解存在栈中,因为编译器把 this 指针作为一个隐含参数传给函数,所以本质上还是参数,形参在哪开辟空间,自然是栈
❗️注意:
- this 指针不是对象的一部分
- 静态函数没有 this 指针,因为静态函数不属于某个对象
🌸this 指针的应用
class A {
public:
int a;
//用途1:当形参和成员变量同名时,可用 this 指针来区分
void fun(int a) {
a = a; //error,编译器会就近原则找 a,此时两个 a 都是形参 a
//使用 this 指针
this->a = a;
//用实例化对象调用这个函数时,会向函数自动传参的指向这个对象的指针
//前一个 a 是成员变量里面的 a,后面的 a 是参数 a
}
//用途2:在类的非静态成员函数中返回对象本身,可使用 return *this
A& fun1(A& p) {
//this = nullptr; //error,this 一旦指向就不能更改
this->a += p.a;
return *this;
//这里函数的返回类型是引用,如果不用引用的方式返回,相当于返回时创建了一个临时对象
//这个对象和 p 是不同的另一个对象(匿名对象),那么后续的操作与p就没有关系了
}
};
void test(){
A AA;
AA.fun(10);
cout << AA.a << endl;
AA.fun1(AA).fun1(AA).fun1(AA).fun1(AA); //fun1() 返回的是引用
cout << AA.a << endl; //50
//如果是传值返回,虽然不会报错,但是意思就不一样了(输出为 20)
//这里和 std 输出流 cout 很像(事实上 cout 返回的就是自己)
}
🌸空指针访问成员函数
C++ 中空指针也是可以调用成员函数的,但是要注意有没有用到 this 指针
class A {
public:
int a;
void fun1() {
cout << "this is fun1" << endl;
}
void fun1() {
cout << "this is fun2" << a << endl;
//相当于 cout << "this is fun2" << this->a << endl;
//这里编译器也会看成 this->a,以确保指向的是当前对象
}
};
void test() {
A* AA = nullptr; //this 指针为 nullptr
AA->fun1(); //可以运行,因为函数并没有使用 this 指针
AA->fun2(); //不可以运行
//因为 fun2 函数中存在对成员变量的使用,而 AA 为空指针,即非法访问
}
🌸总结:定义空指针合法,但是使用空指针不合法
疑问:对于 AA,AA->fun1() 不也是调用空指针吗,为什么不会报错
-
因为函数放在了公共代码区,编译器看到这种会直接去公共代码区 call 函数地址,而不会通过空指针去找,即代码存在空指针,但是没有使用
-
-> 本质是给一个地址去找,但是 func1() 在公共代码区,不用找,直接调用
🌸成员变量在类里面虽然只是声明,但是也可以给定默认值,类似缺省参数
class tmp {
public:
void display() {
cout << a << endl;
cout << b << endl;
}
private:
int a = 1; //注:这不是成员变量的初始化,而是给一个缺省值
char b = 'b';
};
类的成员函数
- 类的六个默认成员:构造和析构函数,拷贝构造和赋值重载,取地址重载(普通和 const)
- 类的常见成员函数:const 修饰的成员函数、static 静态成员函数
一、构造函数
- 构造函数是一个特殊的成员函数,名字与类名相同
- 创建类对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次
❗️注意
构造函数的虽然名称叫构造,但是其主要任务并不是开空间创建对象,而是初始化对象
🌸其特征如下:
- 构造函数语法:classname() {}
- 构造函数,没有返回值也不写 void
- 函数名称与类名相同
- 构造函数可以有参数,可以发生重载
- 在实例化对象时候会自动调用构造,无须手动调用,而且只会调用一次
🌸默认构造函数和默认生成的构造函数
如果我们没有写构造函数,编译器会默认生成一个无参的构造函数(默认生成的构造函数);如果我们自己定义了默认构造函数,那编译器就不会生成构造函数。即我们写了编译器就不生成了
- 默认构造函数:无参的构造函数或全缺省的构造函数,默认构造函数只能有一个
- 默认构造函数 != 默认生成的构造函数
- 默认构造函数是我们自己写的,默认生成的构造函数时编译器提供的
class tmp{
public:
tmp(){ //无参构造
cout << "无参构造" << endl;
name = "张三";
ID = 0;
}
tmp(string a = "张三", int b = 10){ //全缺省构造
name = a;
ID = b;
} //无参和全缺省构造函数只能存在一个,否则会有歧义
tmp(string a, int b){ //有参构造
name = a;
this->ID = b;
cout << "有参构造";
} //有参和全缺省构造函数可以同时存在,但是要注意满足函数重载的要求
private:
string name;
int ID;
double salary;
string email;
};
当我们不定义构造函数时系统会默认生成一个无参构造函数,但该函数是空实现,不会对成员变量初始化
class test1{
public:
test1(){
cout << "无参构造函数" << endl;
}
test1(int a, int b){
cout << "有参构造函数" << endl;
}
};
class test2 {
public:
//没有定义构造函数,相当于编译器定义了 test2() {}
test1 tmp; //对象成员变量
int a;
int b;
double c;
};
void test() {
test2 t; //打印 无参构造函数,说明调用了 test1 的无参构造函数
cout << t.a << t.b << t.c; //随机数,说明没有初始化
}
❗️注意:构造函数一般写在 public 限定符里面
class test{
a(){
cout << "无参构造" << endl;
}
};
void test(){
test t; //error,private 限定符下的构造函数不可以访问
}
二、拷贝构造
拷贝构造:创建一个与已有对象一样的新对象
🌸拷贝构造特点:
- 拷贝构造函数只有单个形参,该形参是对拷贝对象的引用(一般常用 const 修饰)
- 拷贝构造是构造函数的一个重载
- 拷贝构造函数的参数有且只有一个而且必须是引用(或者指针),如果是传值会引发递归
- 类里面的构造函数不能只有拷贝构造,因为有了拷贝构造函数系统就不会生成默认的构造函数,此时拷贝构造函数也不能创建对象
class example {
public:
example(){ //构造函数
age = -1;
}
example(const example& p) { //拷贝构造
cout << "拷贝构造\n" << endl;
age = p.age; //小细节:这里的 p 可以使用 private 限定符里面的 age
}
//如果使用传值拷贝
example(const example p) {} //error,创建形参时也会调用拷贝构造函数,造成递归
private:
int age;
};
void test() {
example x1;
example x2(x1); //拷贝构造
}
//一个类里面不能只有拷贝构造函数
class test {
public:
test(const test& t) {}
private:
int a;
};
int main(){
test t; //error
return 0;
}
🌸深拷贝和浅拷贝
- 若自己没有定义拷贝构造函数,系统会生成默认的拷贝构造函数
- 默认的拷贝构造函数按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
- 这就造成了深浅拷贝问题
class per {
public:
int age;
int* high;
public:
per(int x, int y){
high = new int(y);
age = x;
}
per(const per& p){ //浅拷贝
high = p.high; //编译器默认拷贝函数实现的代码,只拷贝了地址的值
age = p.age;
} //如果浅拷贝,那么当释放 high 的内存时,因为两个指针指向的是同一块空间,所以会多次释放内存导致程序出错
per(const per& p){ //深拷贝
high = new int(*p.high); //在堆创建重新开辟一块地址,存放 p 中的数据
age = p.age;
}
~per(){ //析构函数中自动释放栈区的内存,但是堆区的内存需要手动释放
assert(high);
delete high;
high = nullptr;
}
};
🌸构造函数的调用
三种调用方式:括号法、显示法、隐式转换法
class example{
public:
example(){
cout << "无参构造" << endl;
}
example(int a){
cout << "有参构造" << endl;
}
example(const example& exam){
cout << "拷贝构造" << endl;
}
public:
int a;
int b;
};
void test(){
//1. 括号法
example p1; //调用无参、系统默认生成、全省的构造函数
example p2(10); //调用有参构造函数
//example p2{10} //也可以这样写
//PS:int a(1) 这种写法类似括号法调用构造函数,不会报错
example p3(p1); //调用拷贝构造函数
//注意:调用系统默认生成的构造函数或无参构造函数时,不要()
example p4(); //不会报错,但编译器会将其看成函数的声明,并不会创建对象
//2. 显示法
example p5; //调用系统默认生成的构造函数或无参构造函数
example p6 = example(10); //有参构造函数的调用
example p7 = example(p6); //拷贝构造函数的调用
//3 .隐式转换法
example p8 = 10; //有参构造,相当于 example p8(10)
example p9 = p8; //拷贝构造
//当有多个成员时,可以用 {}
example p10 = {10, 20}; //括号里面的参数应与构造函数一致
}
🌸匿名对象
class example {
public:
int a = 10;
string name = "zhangsan";
};
void test(){
cout << example().a << endl;
example(10); //这种对象叫做匿名对象
//特点:当匿名对象该行的代码执行完后,会立刻销毁该匿名对象
//注意:不要用拷贝构造函数创建一个匿名对象
example(p2); //编译器会认为创建了一个 p2 对象,example(p2) == example p2;
//对于example(10),编译器会认为 example 是调用构造函数,10 是函数参数
//对于example(p3),编译器会认为 example 是一个数据类型,即创建一个 p3 对象
}
//其他关于匿名对象的一些细节
class A{
public:
A(){
cout << "构造函数";
}
A(A& a){
cout << "拷贝构造";
}
};
void test(example A);
test(example()); //匿名对象作为函数参数,打印 构造函数
//本来是要打印 拷贝构造 的,但是这里不会,函数参数 A 就是传入的匿名对象,这是编译器对匿名对象的优化
🌸拷贝构造函数调用时机
//1. 使用一个已经创建完毕的对象来初始化一个新对象
example t1;
example t2(t1); //拷贝构造函数
//2. 值传递的方式给函数参数传值
void test(example t);
test(t1); //由于形参是临时拷贝,所以会调用拷贝构造函数
//3. 以值方式返回局部对象
example test1(){
example p1;
return p1;
//p1 是局部对象,当函数运行完后就会被销毁
//返回 p1 时会创建一个临时对象,其特征和 p1 一样,所以也是拷贝构造
}
example t3 = test1(); //这里是拷贝构造的隐式调用
//注意:上面一共发生了两次拷贝构造(函数返回时和创建t3对象时)
🌸总结
- 如果定义了有参构造函数就不在提供默认无参构造,但是会提供默认拷贝构造
- 如果定义拷贝构造函数,就不会再提供其他构造函数
三、析构函数
析构函数:在对象生命周期将要结束前系统自动调用, 执行一些清理工作(主要是堆区内存)
🌸析构函数的特点:
- 语法: ~classname() {}
- 析构函数没有返回值也不写 void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,不可以发生重载
- 在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
- 析构函数和构造函数一样,都需要写在 public 限定符后,其本质也是成员函数
class person {
public:
person(string str = "jam", int age = -1){ //构造函数
cout << "构造函数的调用" << endl;
name = str;
m_age = age;
} //当我们不写构造函数时,编译器也会自己写一个构造函数,其函数为 person() {};
~person(){ //析构函数
cout << "析构函数的调用" << endl;
} //当我们不写析构函数时,编译器也会自己写一个析构函数,其函数为 ~person(){};
public:
string name;
string lover;
private:
int m_age;
int IDcard;
};
int main(){
person s1;
cout << s1.name << endl;
return 0; //打印 析构函数的调用
}
🌸上述代码分析:
- s1 是创建在栈区的一个对象,main 函数执行完毕后会自动释放内存,所以在 main 函数执行完之前就会调用析构函数
- 如果在类中没有自己定义析构函数,那么析构函数就是一个空实现,对象中的数据(主要是堆区)会在 main 函数执行完后销毁
- 如果在 main 函数 return 0; 前面加个 system(“pause”); 是不能打印出析构函数的调用的,因为此时 main 函数还没有结束
🌸对象成员变量的构造和析构顺序
对象成员:类对象作为另一个类的成员变量
class A {
public:
A(){
cout << "A的构造函数" << endl;
}
~A(){
cout << "A的析构函数" << endl;
}
};
class B{
public:
B(){
cout << "B的构造函数" << endl;
}
~B(){
cout << "B的析构函数" << endl;
}
private:
A a;
};
//测试其构造和析构顺序
void test(){
B b; //A B B A
}
四、静态(staic)成员和函数
静态成员和函数:用 static 关键字修饰的成员变量或函数
🌸静态成员的特点
- 静态成员不能给缺省值,必须在类外初始化
- 因为在类外初始化时才分配空间,所以不在类外初始化就不能用
- 所有对象都共享同一份静态成员变量,在编译之前就创建了内存,存放在静态区
- 静态函数没有 this 指针,不能访问非静态成员变量(可以理解为静态函数或者静态变量都只会存在一份,当静态成员函数中使用非静态成员变量时,编译器不知道是修改的哪个对象的成员)
class C {
public:
C(){
++a;
}
C(const C& c1){
++a;
}
static int get_a(){ //静态成员函数
b = 4; //error,不能修改非静态成员变量
return a;
}
static int c;
private:
static int a;
int b;
};
🌸静态成员变量的初始化
int main() {
int C::c = 10; //error,因为 static 是一个全局的变量,不能在 main 函数中初始化
}
//静态成员变量必须在全局作用域内初始化
int C::a = 10; //静态成员变量的初始化不受限定符(private)的限制(特例)
int C::c = 10; //初始化时不用加 static(加了反而报错)
🌸静态成员的访问
因为所有对象共享静态成员,所有静态成员有两种访问方式
class C {
public:
static int c;
static int get_a() {
return a;
}
private:
static int a;
};
int C::c = 10;
int C::a = 10;
int main() {
C test;
//1. 通过对象进行访问
cout << test.c << endl;
//2. 通过类名进行访问
cout << C::c << endl;
//虽然静态成员初始化不受限定符限制,但是访问时和普通成员一样有三个级别
cout << C::a; //error,private 下的成员变量不能在类外访问
cout << C::get_a(); //可以通过静态函数访问
//静态成员函数的调用
//1.通过对象进行访问
int a = test.get_a();
//2.通过类名
int b = C::get_a();
}
🌸总结
- 所有类对象所共享静态成员,其不属于某个具体的实例对象
- 静态成员变量必须在类外定义,定义时不添加 static 关键字
- 类静态成员可用 classname::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有 public、protected、private 3种访问级别
- 静态成员函数不能调用非静态成员函数(相当于在静态成员函数里面不可以使用非静态成员)
- 非静态成员函数可以使用静态成员变量和函数
五、const 修饰的成员函数
const 成员函数:成员函数后加 const
🌸特点:
- const 修饰的成员函数,本质上修饰的是其隐含参数 this 指针
- const 成员函数内不可以修改成员变量,因为 this 指针变成了:const classname* const this(常指针)
- 类的成员变量在声明时加上关键字 mutable,就可以在 const 修饰的成员函数中修改
- 建议如果一个成员函数不需要修改成员变量,就加上 const
class A {
public:
int a;
mutable int b;
void fun1(){
this->a = 10;
a = 20;
}
void fun2() const{ //const 修饰的成员函数
a = 10; //error,不能更改成员变量
this->a = 10; //error
this->b = 10; //加上 mutable 后就可以修改
} //该函数相当于void fun2(const A* this)
};
🌸常对象:
特点:
- 对象声明前加 const 称该对象为常对象
- 常对象里面的成员变量不能修改(mutable 修饰的可以)
- 常对象只能调用 const 修饰的函数(若调用普通成员函数,如果该函数修改了成员变量,那么相当于侧面修改了成员变量)
void test(){
const A tmp; //常对象
tmp.a = 10; //error,常对象里面的成员变量不能修改
tmp.b = 10; //mutable 修饰的成员变量可以修改
tmp.fun1(); //error,常对象只能调用 const 修饰的函数
tmp.fun2();
}
六、取地址操作符重载函数
🌸有普通和 const 修饰两种,这两个函数一般不用定义,编译器默认生成
class tmp {
public:
tmp* operator&(){ //普通
cout << "普通" << endl;
return nullptr; //让我们无法获得对象地址
}
const tmp* operator&() const { //const修饰
cout << "const修饰" << endl;
return this;
}
//注意:这两个函数构成函数重载,因为 this 指针的类型不同
};
int main() {
tmp a;
tmp* pa = &a; //普通
const tmp b;
const tmp* pb = &b; //const修饰
}
初始化列表
- 我们知道通过构造函数,可以给成员变量一个初始值,但是这并不是初始化,只能叫赋初值
- 对于一些特殊类型的变量,例如引用、const 修饰和自定义类型(没有默认构造函数),在构造函数体内赋初值的方法是不能实现的
- 真正的初始化发生在构造函数语句前,即初始化列表处,所以构造函数在赋初值时需要一次初始化和一次拷贝(赋值)的开销
🌸初始化列表:对成员变量进行初始化操作
语法:构造函数():成员变量1(值1), 成员变量2(值2), …… {}
class init {
public:
int m_age;
string name;
//原始的构造函数初始化对象
init(int age, string str) {
m_age = age;
name = str;
}
//使用初始化列表
init() :m_age(-1), name("null") {}
//还可写成如下,使得更加灵活
init(int a, string str) :m_age(a), name(str) {}
};
❗注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含引用、const 修饰、自定义类型(该类没有默认构造函数)的成员变量,必须在初始化列表位置进行初始化
- 尽量使用初始化列表初始化,因为对于自定义类型成员变量,一定会先使用初始化列表初始化,内置类型没有区别
- 成员变量在类中声明的次序就是其在初始化列表中初始化的顺序,与其在初始化列表中的先后次序无关
//对第三点举个例子
class Time{
public:
Time(int day = 0){
this->day = day;
}
public:
int year;
int month;
int day;
};
class date{
public:
date(int hour, int day){
//这里没有使用初始化列表,那么 time 里面的 day 就不能设置成我们希望的值
//time.day 只能用其默认构造函数初始化为0
this->hour = hour;
//time.day = day; //也可以在构造函数类对其赋值
}
public:
int hour;
int min;
int second;
Time time;
};
void test(){
date d(10, 20);
cout << d.time.day; //结果为0,与我们想要的结果不符
}
//使用初始化列表初始化自定义类型
class A{
public:
string mobile_name;
A(string str){
mobile_name = str;
}
};
class B{
public:
A a; //对象成员
string name;
B(string str1, string str2) :name(str1), a(str2){}
//a(str2)等价于A a = str2; 属于构造函数的隐式转换调用
};
//关于第四点的举例
class tmp{
public:
tmp(int a) :a2(a), a1(a2){}
void print(){
cout << "a1=" << a1 << endl << "a2=" << a2 << endl;
}
int a1;
int a2;
};
void test(){
tmp t(1);
t.print(); //a1=-88768854 a2=1
//说明先初始化a1,后初始化a2
}
explicit 关键字
explicit:用该关键字修饰的构造函数,会禁止构造函数的隐式类型转换
class tmp {
public:
tmp(int){ //占位参数
cout << "tmp(int)" << endl;
}
tmp(int, int){
cout << "tmp(int, int)" << endl;
}
};
void test(){
tmp A = 1; //隐式类型转换
tmp B = { 1,2 }; //c++11支持多参数转换,c++98 不支持
}
//使用explicit
class tmp1 {
public:
explicit tmp1(int a) {}
explicit tmp1(int a, int b) {}
};
void test(){
tmp1 A = 1; //error
tmp1 B = { 1,2 }; //error
}
本篇文章到这里就结束了,欢迎批评指正!