适合具备 C 语言基础的 C++ 教程之二

开发 后端
在上一则教程中,通过与 C 语言相比较引出了 C++ 的相关特性,其中就包括函数重载,引用,this 指针,以及在脱离 IDE 编写 C++ 程序时,所要用到的 Makefile的相关语法。

 [[381641]]

前言

在上一则教程中,通过与 C 语言相比较引出了 C++ 的相关特性,其中就包括函数重载,引用,this 指针,以及在脱离 IDE 编写 C++ 程序时,所要用到的 Makefile的相关语法。本节所要叙述的是 C++的另外两个重要的特性,也就是构造函数和析构函数的相关内容,这两部分内容也是有别于 c语言而存在的,也是 c++的一个重要特性。

构造函数

类的构造函数是类的一种特殊的成员函数,它会在每次创建新的对象的时候执行,构造函数的名称和类的名称是完全相同的,并不会返回任何的类型,也不会返回 void。构造函数可以用于为某些成员变量设置初始值。

比方说,我们现在有如下所示的一段代码:

  1. #include <iostream> 
  2. using namespace std; 
  3.  
  4. class Person{ 
  5. private: 
  6.     char *name
  7.     int age; 
  8.     char *work
  9.  
  10. public
  11.     Person() {cout << "Person()" << endl;} 
  12. }; 
  13.  
  14. int main(int argc, char **argv) 
  15.     Person per; 
  16.  
  17.     return 0; 

在主函数中,定义 Person per 的同时,就会自动地调用 Person() 函数,那么不难猜出,执行 test 文件的时候,输出结果如下:

image-20210113124209248

上述的构造函数并没有参数,实际上在构造函数是可以具有参数的,具体的看如下所示的代码:

  1. #include <iostream> 
  2. using namespace std; 
  3.  
  4. class Person 
  5. private: 
  6.     char *name
  7.     int age; 
  8. public
  9.     Person(char *nameint age) 
  10.     { 
  11.         cout << "Person(char *,int)" << endl; 
  12.         this->name = name
  13.         this->age = age; 
  14.     } 
  15.  
  16.     Person(){cout << "Person()" << endl;} 
  17. }; 
  18.  
  19. int main(int argc, char **argv) 
  20.     Person per; 
  21.     Person per2("zhangsan",18); 
  22.  
  23.     return 0; 

上述代码中,定义第一个 Person 实例的时候,就会自动地调用无形参地构造函数,当实例化第二个 Person 类的时候,就会自动地调用有形参地构造函数。

这个时候,运行函数的输出结果如下所示:

image-20210113125016221

可以看到调用构造函数的顺序是和实例化对象的顺序是一致的。

构造函数除了可以有形参,也可以有默认的形参,比如说下面这段代码:

  1. #include <iostream> 
  2. using namespace std; 
  3.  
  4. class Person 
  5. private: 
  6.     char *name
  7.     int age; 
  8. public
  9.     Person(char *nameint age, char *work = "none"
  10.     { 
  11.         cout << "Person(char *,int)" << endl; 
  12.         this->name = name
  13.         this->age = age; 
  14.         this->work = work
  15.     } 
  16.  
  17.     Person(){cout << "Person()" << endl;} 
  18.  
  19.     void printInfo(void) 
  20.     { 
  21.         cout << "name =" << name << ",age = "<< age << ",work ="<< work << endl; 
  22.     } 
  23. }; 
  24.  
  25. int main(int argc, char **argv) 
  26.     Person per; 
  27.     Person per2("zhangsan",18); 
  28.     Person per3(); 
  29.  
  30.     per2.printInfo(); 
  31.  
  32.     return 0; 

上述代码中,第一条代码和第二条代码创建了两个 Person 实例,在创建时依次调用构造函数,这里需要注意的是,第三条语句,这条语句看起来像是实例化了一个 per3 对象,但是 per3 括号里并没有实参,这其实是定义了一个函数,函数的形参为void,返回值为 Person ,并非是一个对象。这里还需要注意的一点是 per2 对象,它在调用构造函数时,形参有一个默认值,所以最终,程序输出的结果如下所示:

image-20210113131653000

在实例化对象的时候,我们也可以通过定义指针的形式实现,下面代码是上述代码的一个改进,并且以指针的形式实例化了对象,代码如下所示:

  1. #include <iostream> 
  2. #include <string.h> 
  3.  
  4. using namespace std; 
  5.  
  6. class Person 
  7. private: 
  8.     char *name
  9.     int age; 
  10.     char *work
  11.  
  12. public
  13.     Person(){cout << "person()" << endl;} 
  14.     Person(char *name,int age, char *work
  15.     { 
  16.         cout << "Person(char *,int, char *)" << endl; 
  17.         this->name = new char[strlen(name) + 1]; 
  18.         strcpy(this->name,name); 
  19.         this->age = age; 
  20.         this->work = new char[strlen(work) + 1]; 
  21.         strcpy(this->work,work); 
  22.     } 
  23.  
  24.     void printInfo(void) 
  25.     { 
  26.         cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl; 
  27.     } 
  28. }; 
  29.  
  30. int main(int argc,char *argv) 
  31.     Person per("zhangsan",18,"teacher"); 
  32.     Person per2; 
  33.  
  34.     Person *per4 = new Person; 
  35.     Person *per5 = new Person(); /* 这两种方式定义的效果是一样的 */ 
  36.  
  37.     Person *per6 = new Person[2]; 
  38.  
  39.     Person *per7 = new Person("lisi", 18,"doctor"); 
  40.     per.printInfo(); 
  41.     per7.printInfo(); 
  42.  
  43.     delete per4; 
  44.     delete per5; 
  45.     delete []per6; 
  46.     delete per7; 

上述代码中,使用了new 来分配给对象空间,再分配完之后,系统会自动的进行释放,或者说是使用手动的方式进行释放内存,在手动释放内存的时候,我们采用 delete 的方式来进行释放,当创建了两个指针数组的时候,在手动释放的时候,要在指针变量前面加上 [],在实例化指针对象的时候,也可以带上参数或者说是不带参数。下面是上述代码的运行结果:

image-20210114125841211

析构函数

析构函数的引出

上述我们知道,在函数运行完之后,用 new 分配到的空间才会被释放掉,那么如果是在函数调用里用 new 获取到的空间会随着函数调用的结束而释放么,我们现在来做这样一个实验,把上述中的代码中的主函数写成 test()函数,然后在 main() 函数里调用。

代码如下所示:

  1. #include <iostream> 
  2. #include <string.h> 
  3. #include <unistd.h> 
  4.  
  5. using namespace std; 
  6.  
  7. class Person 
  8. private: 
  9.     char *name
  10.     int age; 
  11.     char *work
  12.  
  13. public
  14.     Person(){cout << "person()" << endl;} 
  15.     Person(char *name,int age, char *work
  16.     { 
  17.         cout << "Person(char *,int, char *)" << endl; 
  18.         this->name = new char[strlen(name) + 1]; 
  19.         strcpy(this->name,name); 
  20.         this->age = age; 
  21.         this->work = new char[strlen(work) + 1]; 
  22.         strcpy(this->work,work); 
  23.     } 
  24.  
  25.     void printInfo(void) 
  26.     { 
  27.         //cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl; 
  28.     } 
  29. }; 
  30.  
  31. void test(void) 
  32.     Person per("zhangsan",18,"teacher"); 
  33.     Person per2; 
  34.  
  35.     Person *per4 = new Person; 
  36.     Person *per5 = new Person(); /* 这两种方式定义的效果是一样的 */ 
  37.  
  38.     Person *per6 = new Person[2]; 
  39.  
  40.     Person *per7 = new Person("lisi", 18,"doctor"); 
  41.     per.printInfo(); 
  42.     per7->printInfo(); 
  43.  
  44.     delete per4; 
  45.     delete per5; 
  46.     delete []per6; 
  47.     delete per7; 
  48.  
  49. int main(int argc, char **argv) 
  50.     for (int i = 0; i < 1000000; i++) 
  51.         test(); 
  52.     cout << "run test end" << endl; 
  53.     sleep(10); 
  54.     return 0; 

这是运行前的空闲内存的大小:

image-20210114133025365

紧接着是函数运行完 100 0000 次的 test 函数之后的空闲内存大小:

image-20210114133140216

然后,是主函数运行完之后,推出主函数之后,空闲的内存剩余量:

image-20210114133241325

总结一下就是,在子函数里用 new 分配给局部变量的空间,具体来说在上述代码中的体现就是用 new给 this->name分配的空间。也就是在主函数没有运行完是不会被释放掉的,也就是说只有在主函数运行完之后,子函数里用 new 分配的空间才会被释放掉,因此,如果想要在子函数调用完之后就释放掉用 new 分配的空间,就需要编写代码来实现。而这个操作, C++ 提供了析构函数来完成,下面是使用析构函数来进行释放内存的代码:

  1. #include <iostream> 
  2. #include <string.h> 
  3. #include <unistd.h> 
  4.  
  5. using namespace std; 
  6.  
  7. class Person 
  8. private: 
  9.     char *name
  10.     int age; 
  11.     char *work
  12.  
  13. public
  14.     Person(){cout << "person()" << endl;} 
  15.     Person(char *name,int age, char *work
  16.     { 
  17.         cout << "Person(char *,int, char *)" << endl; 
  18.         this->name = new char[strlen(name) + 1]; 
  19.         strcpy(this->name,name); 
  20.         this->age = age; 
  21.         this->work = new char[strlen(work) + 1]; 
  22.         strcpy(this->work,work); 
  23.     } 
  24.  
  25.     ~Person() 
  26.     { 
  27.         if (this->name
  28.             delete this->name
  29.         if (this->work
  30.             delete this->work
  31.     } 
  32.  
  33.     void printInfo(void) 
  34.     { 
  35.         //cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl; 
  36.     } 
  37. }; 
  38.  
  39. void test(void) 
  40.     Person per("zhangsan",18,"teacher"); 
  41.     Person per2; 
  42.  
  43.     Person *per4 = new Person; 
  44.     Person *per5 = new Person(); /* 这两种方式定义的效果是一样的 */ 
  45.  
  46.     Person *per6 = new Person[2]; 
  47.  
  48.     Person *per7 = new Person("lisi", 18,"doctor"); 
  49.     per.printInfo(); 
  50.     per7->printInfo(); 
  51.  
  52.     delete per4; 
  53.     delete per5; 
  54.     delete []per6; 
  55.     delete per7; 
  56.  
  57. int main(int argc, char **argv) 
  58.     for (int i = 0; i < 1000000; i++) 
  59.         test(); 
  60.     cout << "run test end" << endl; 
  61.     sleep(10); 
  62.     return 0; 

下述就是代码运行之前,和主函数在休眠的时候的剩余内存的容量,可以看出,剩余内存的容量是一样的,换句话说,也就是在 test()函数运行完成之后,用 new 分配的空间就已经被释放掉了,就算执行了 1000000 次也没有造成内存泄漏。这也说明了我们的析构函数是有作用的。

image-20210115130212394

析构函数在什么地方被调用

上述析构函数的存在避免了内存泄漏,那么析构函数是在什么时候被调用的呢,用一句话描述就是:在实例化对象被销毁的前一瞬间被调用的,另外还要注意的是构造函数可以有很多个,有参的,无参的构造函数,但是对于析构函数来讲,它只有一个,并且它是无参的。具体的来看如下所示的代码,在刚才那段代码的基础上,我们添加一些打印信息,从而推断我们析构函数调用的位置:

  1. #include <iostream> 
  2. #include <string.h> 
  3. #include <unistd.h> 
  4.  
  5. using namespace std; 
  6.  
  7. class Person 
  8. private: 
  9.     char *name
  10.     int age; 
  11.     char *work
  12.  
  13. public
  14.     Person() 
  15.     { 
  16.         name = NULL
  17.         work = NULL
  18.     } 
  19.     Person(char *name,int age, char *work
  20.     { 
  21.         this->name = new char[strlen(name) + 1]; 
  22.         strcpy(this->name,name); 
  23.         this->age = age; 
  24.         this->work = new char[strlen(work) + 1]; 
  25.         strcpy(this->work,work); 
  26.     } 
  27.  
  28.     ~Person() 
  29.     { 
  30.         cout << "~Person()" << endl; 
  31.         if (this->name
  32.         { 
  33.             delete this->name
  34.             cout << "The name is:" << name << endl;    
  35.         } 
  36.         if (this->work
  37.         { 
  38.             delete this->work
  39.             cout << "The work is:" << work << endl; 
  40.         } 
  41.     } 
  42.  
  43.     void printInfo(void) 
  44.     { 
  45.         //cout << "name is:" << name << ",age is:" << age << ",work is:" << work << endl; 
  46.     } 
  47. }; 
  48.  
  49. void test(void) 
  50.     Person per("zhangsan",18,"teacher"); 
  51.  
  52.     Person *per7 = new Person("lisi", 18,"doctor"); 
  53.     delete per7; 
  54.  
  55. int main(int argc, char **argv) 
  56.     test(); 
  57.     return 0; 

我们来看输出的结果:

image-20210115132418481

通过上面的输出结果可以知道,先输出的是lisi,后输出的是 zhangsan,而在实例化对象的时候,是先创建的 per 对象,并初始化为 zhangsan,后创建的 per7 对象,并初始化为 lisi,再调用析构函数的时候顺序却是颠倒过来的。因此,总结一下就是:

per 这个实例化对象是在 test()函数执行完之后,再调用的析构函数,而对于 per7对象来说,是在执行 delete per7这条语句之后调用的析构函数,所以也就有了上述的输出结果。

另外,引出一点,如果我们在上述的代码中把delete per7这条语句给注释掉,那么会怎么样呢,下图是去掉该语句之后的结果:

image-20210115133215468

我们看到,上述就只执行了 zhangsan的析构函数,并没有执行lisi的析构函数,这也告诉我们,在使用 new 创建的实例化对象,必须使用 delete 将其释放掉,如果没有使用 delete 来将其释放,那么在系统退出之后,会自动地释放掉它地内存,但是这个时候是不会调用它地析构函数的。

最后,关于构造函数和析构函数,如果类里没有实现任何构造函数和析构函数,那么其系统本身会调用一个默认的构造函数和析构函数。那么,除了默认的构造函数和默认的析构函数,还存在一个默认的拷贝构造函数,接下来,来叙述这个拷贝构造函数。

拷贝构造函数

默认拷贝构造函数

我们直接来看这样一段代码:

  1. #include <iostream> 
  2. #include <string.h> 
  3. #include <unistd.h> 
  4.  
  5. using namespace std; 
  6.  
  7. class Person { 
  8. private: 
  9.     char *name
  10.     int age; 
  11.     char *work
  12.  
  13. public
  14.  
  15.     Person() {//cout <<"Pserson()"<<endl; 
  16.         name = NULL
  17.         work = NULL
  18.     } 
  19.     Person(char *name)  
  20.     { 
  21.         //cout <<"Pserson(char *)"<<endl; 
  22.         this->name = new char[strlen(name) + 1]; 
  23.         strcpy(this->namename); 
  24.         this->work = NULL
  25.     } 
  26.  
  27.     Person(char *nameint age, char *work = "none")  
  28.     { 
  29.         //cout <<"Pserson(char*, int)"<<endl; 
  30.         this->age = age; 
  31.  
  32.         this->name = new char[strlen(name) + 1]; 
  33.         strcpy(this->namename); 
  34.  
  35.         this->work = new char[strlen(work) + 1]; 
  36.         strcpy(this->workwork); 
  37.     } 
  38.  
  39.     ~Person() 
  40.     { 
  41.         cout << "~Person()"<<endl; 
  42.         if (this->name) { 
  43.             cout << "name = "<<name<<endl; 
  44.             delete this->name
  45.         } 
  46.         if (this->work) { 
  47.             cout << "work = "<<work<<endl; 
  48.             delete this->work
  49.         } 
  50.     } 
  51.  
  52.     void printInfo(void) 
  53.     { 
  54.         //printf("name = %s, age = %d, work = %s\n"name, age, work);  
  55.         cout<<"name = "<<name<<", age = "<<age<<", work = "<<work<<endl; 
  56.     } 
  57. }; 
  58.  
  59. int main(int argc, char **argv) 
  60.     Person per("zhangsan", 18); 
  61.     Person per2(per); 
  62.  
  63.     per2.printInfo(); 
  64.  
  65.     return 0; 

在主函数的第二行代码中,我们可以看到我们创建了一个实例,并且传入的参数是 per,但是我们看类里面的代码实现,并没有发现有一个构造函数的形参为 Person ,那这个时候,会发生什么函数调用呢,实际上是会调用一个系统的默认构造函数,这个默认的构造函数会进行值拷贝,会将 per中的内容拷贝到 per2中去,下图是这个过程的一个示意图:

image-20210117015212259.png

通过上图可以看到,在执行默认的拷贝构造函数的时候,执行的是值拷贝,那么相应的,per 的 name 也就指向了 address1,per2 的 name 同样也指向了 adress,从而完成了值拷贝的过程,下面是代码运行的结果:

image-20210117015527675

可以看到,在输出 per2 的内容的时候,输出的是 per 的初始化内容,在主函数运行完之后,就要执行析构函数来释放使用 new 分配的空间,首先是释放 per 的内容,然后紧接着是释放 per2的内容,但是在刚刚的叙述中,使用默认构造函数进行拷贝的时候,使用的是值拷贝,从而造成的效果是 per2 的 name 和 work 指向的地址是 per 中的同一块地址,这样,在执行析构函数的时候,同一块内存空间就会被释放两次,从而导致错误。因此,使用默认的拷贝构造函数存在一定的问题,也就需要我们自己来定义拷贝构造函数,下面介绍自定义的拷贝构造函数。

自定义拷贝构造函数

我们根据在上述代码的基础上,修改得到我们自定义的拷贝构造函数如下:

  1. #include <iostream> 
  2. #include <string.h> 
  3. #include <unistd.h> 
  4.  
  5. using namespace std; 
  6.  
  7. class Person { 
  8. private: 
  9.     char *name
  10.     int age; 
  11.     char *work
  12.  
  13. public
  14.  
  15.     Person() {//cout <<"Pserson()"<<endl; 
  16.         name = NULL
  17.         work = NULL
  18.     } 
  19.     Person(char *name)  
  20.     { 
  21.         //cout <<"Pserson(char *)"<<endl; 
  22.         this->name = new char[strlen(name) + 1]; 
  23.         strcpy(this->namename); 
  24.         this->work = NULL
  25.     } 
  26.  
  27.     Person(char *nameint age, char *work = "none")  
  28.     { 
  29.         cout <<"Pserson(char*, int)"<<endl; 
  30.         this->age = age; 
  31.  
  32.         this->name = new char[strlen(name) + 1]; 
  33.         strcpy(this->namename); 
  34.  
  35.         this->work = new char[strlen(work) + 1]; 
  36.         strcpy(this->workwork); 
  37.     } 
  38.  
  39.     Person(Person &per)  
  40.     { 
  41.         cout <<"Pserson(Person &per)"<<endl; 
  42.         this->age = per.age; 
  43.  
  44.         this->name = new char[strlen(per.name) + 1]; 
  45.         strcpy(this->name, per.name); 
  46.  
  47.         this->work = new char[strlen(per.work) + 1]; 
  48.         strcpy(this->work, per.work); 
  49.     } 
  50.  
  51.     ~Person() 
  52.     { 
  53.         cout << "~Person()"<<endl; 
  54.         if (this->name) { 
  55.             cout << "name = "<<name<<endl; 
  56.             delete this->name
  57.         } 
  58.         if (this->work) { 
  59.             cout << "work = "<<work<<endl; 
  60.             delete this->work
  61.         } 
  62.     } 
  63.  
  64.     void printInfo(void) 
  65.     { 
  66.         //printf("name = %s, age = %d, work = %s\n"name, age, work);  
  67.         cout<<"name = "<<name<<", age = "<<age<<", work = "<<work<<endl; 
  68.     } 
  69. }; 
  70.  
  71. int main(int argc, char **argv) 
  72.     Person per("zhangsan", 18); 
  73.     Person per2(per); 
  74.  
  75.     per2.printInfo(); 
  76.  
  77.     return 0; 

上述中,我们编写了一个拷贝构造函数,函数的形参是 Person 类的引用,然后我们在主函数中传入 per 实参,程序执行的结果如下图所示:

image-20210117234707175

通过图片代码的运行结果我们也可以知道,在执行主函数的第二行代码的时候,调用了默认的拷贝构造函数。

对象的构造顺序

在上述代码的基础上,比如说我们存在如下几个实例化对象。

  1. Person per_g("per_g", 10); 
  2.  
  3. void func(void) 
  4.     Person per_func("per_func",11); 
  5.     static Person per_func_s("per_func_s",11); 
  6.  
  7. int main(int argc,char **argv) 
  8.     Person per_main("per_main",11); 
  9.     static Person person_main_s("person_main_s",11); 
  10.  
  11.     for (int i = 0; i < 2; i++) 
  12.     { 
  13.         func(); 
  14.         Person per_for("per_for",i); 
  15.     } 
  16.  
  17.     return 0; 

紧接着,我们来看上述代码的执行结果,结果如下图所示:

image-20210118000045599

通过上述的结果,我们可以得出:

实例化类的构造顺序是按照定义的顺序进行构造的,全局的实例化对象会在主函数执行前被构造,然后紧接着构造的是在主函数定义的实例化对象 per_main 和 per_main_s,构造的顺序不会因为其实例化对象是 static 而发生改变,紧接着就是函数 func里面的 per_func和 per_func_s。在退出 func的时候,会释放掉 func中的局部变量,这个时候会调用 per_func的析构函数,但是这时是不会释放掉 func中的 per_func_s,因为它是 static 的,紧接着会构造 per_for对象,当一个 for循析构函数。环执行完毕之后,就会将刚刚那个构造的 per_for对象释放掉,也就是会调用析构函数。紧接着,我们继续调用 func函数,在 func函数里面,会执行 per_fun的构造函数,但是不会执行 per_fun_s的构造函数,因为已经构造过了,在最后,主函数运行完毕之后,以此释放实例化的空间,首先会释放掉 per_main,然后释放 per_main_s,紧接着释放全局变量的空间per_g。

在类里初始化类对象

在刚刚说到的类里面,我们继续添加新的代码,同样的,我们有如下所示的这样一个类:

  1. class 
  2. private: 
  3.     Person father; 
  4.     Person mother; 
  5.     int student_id; 
  6. public
  7.     Student(int id, char *father, char *mother, int father_age = 49, int mother_age = 39) : mother(mother,mother_age),father(father,father_age) 
  8.     { 
  9.         cout << "Student(int id, char *father, char *mother, int father_age, int mother_age)" << endl; 
  10.     } 
  11. }; 
  12.  
  13. int main(int argc, char **argv) 
  14.     Student s(100,"Bill","Lisa"
  15.  
  16.     return 0; 

上述代码运行就会输出如下所示的信息:

image-20210119131136755

这样的操作,就会首先调用的是 father的构造函数,然后,紧接着再调用的是 mother的构造函数,然后,才是调用的 Student的构造函数,在主函数执行完毕之后,执行析构函数的顺序又和刚刚的相反。

小结

上述便是关于 C++比较核心的两个概念,构造函数以及析构函数两大特性,除了讲述了两大特性的基本概念之外,也叙述了为什么要适用析构函数,以及析构函数调用的位置,同时也叙述了拷贝构造函数的相关内容。在本节的末尾也讲述了构造的顺序以及析构的顺序,最后,给出了一种在类里面初始化类对象的一种方法。

本文转载自微信公众号「 wenzi嵌入式软件」,可以通过以下二维码关注。转载本文请联系 wenzi嵌入式软件公众号。

 

责任编辑:武晓燕 来源: wenzi嵌入式软件
相关推荐

2021-02-21 12:09:32

C 语言基础语法

2021-02-20 06:13:18

C 语言C++

2021-02-16 10:57:34

C++ C 语言windows

2021-02-08 20:25:12

C 语言C++Linux

2021-07-16 07:21:45

C++可调用对象std::functi

2010-01-15 17:38:37

C++语言

2011-07-14 16:56:21

2021-04-25 08:11:57

C语言常量与变量标识符命名规范

2010-01-19 14:45:35

C++语言

2020-08-21 13:20:36

C++If ElseLinux

2011-07-14 17:45:06

CC++

2021-02-06 07:49:48

C语言编程开发技术

2011-07-14 17:17:21

C++指针

2011-07-15 00:47:13

C++多态

2011-01-05 11:12:34

C++

2022-01-14 09:10:56

C++文件Linux

2011-07-13 18:24:18

C++

2022-07-01 11:56:54

C语言C++编程语言

2010-01-22 15:30:36

C++语言

2020-07-30 12:40:35

CC++编程语言
点赞
收藏

51CTO技术栈公众号