• C++——对象和类


      最重要的OOP特性:

        *抽象;

        *封装和数据隐藏;

        *多态;

        *继承;

        *代码的可重用性;

    一、抽象和类

      1、类型

        指定基本类型完成了三项工作:1)、决定数据对象需要的内存数量;2)、决定如何解释内存中的位(long和float在内存中占用的位数相同,但将他们转化为数值的方法不同);3)、决定可使用数据对象执行的操作或方法;

        对于内置类型来说,有关操作的信息被内置到编译器中。但在C++中定义用户自定义类型的时候,必须自己提供这些信息。

      2、C++中的类

        类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。

        一般来说,类规范由两部份组成。1)、类声明:以数据成员的方式描述数据部份,以成员函数(被称为方法)的方式描述共有接口;2)、类方法定义:描述如何实现类成员函数。

        简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。

        

        接口:接口是一个共享框架,供两个系统(如计算机和打印机之间或用户和计算机程序之间)交互时使用。对于类,我们说公共接口。在这里,公众是使用类的程序,交互系统由类对象组成,而接口有编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。

        通常,将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。

          

      类的定义:

        C++关键字class指出其后面的代码定义了一个类设计。

      (1)访问控制

        关键字private和public描述了对类成员的访问控制。使用类对象的程序可以直接访问公有部份,但是只能通过公有成员函数(或友元函数)来访问对象的私有成员。因此,公有成员函数是程序和对象私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。

        类设计尽可能将公有接口和实现细节分开。公有接口表示设计的抽象组建。将实现细节放在一起并将他们与抽象分开被称为封装。数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,也是一种封装。

      (2)控制对成员的访问:是公有还是私有

        无论类成员是数据成员还是成员函数,都可以在类的公有部分和私有部分中声明。但由于隐藏数据是OOP的主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分。

        类的默认访问类型是private,而结构的默认访问类型是public。

      

      3、实现类成员函数

        成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数。但是它们有两个特殊的特征:

          *定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类,确定方法定义对应的类的身份;

          *类方法可以访问类的private组件。

        首先,成员函数的函数头使用作用域运算符解析(::)来指出函数所属的类。例如:void Peson:: showAge();表明定义的show()函数是Person类的成员。      

        第二,方法可以访问类的私有成员。因此,可以在成员函数中直接访问类的私有部分,而其他函数则不能。

    Person.h文件:

    #ifndef __Demo__Person__

    #define __Demo__Person__

    #include <iostream>

    #include <string>

    class Person{

    private:

        std::string name;

        int age;

        float height;

        float weight;

        void introduce(){//定义于类声明中的函数都会自动成为内联函数

           std::cout << "name:" << name << ", age:" << age << ", height:" << height << "CM, weight:" << weight << "KG ";

        }

    public:

        void setAge(int myAge);

        void setName(std::string myName);

        void setHeight(float myHeight);

        void setWeight(float myWeight);

        void show();

    };

    #endif /* defined(__Demo__Person__) */

    Person.cpp文件:

    #include "Person.h"
    #include <iostream>
    
    void Person::setName(std::string myName){
        name = myName;
    }
    void Person::setAge(int myAge){
        age = myAge;
    }
    void Person::setHeight(float myHeight){
        height = myHeight;
    }
    void Person::setWeight(float myWeight){
        weight = myWeight;
    }
    void Person::show(){
        introduce();
    }

    main()函数:

    #include <iostream>
    #include "Person.h"
    
    using namespace std;
    int main(int argc, const char * argv[]) {
        Person per;
        per.setAge(24);
        per.setHeight(169);
        per.setWeight(100);
        per.setName("小虎");
        per.show();
      
        return 0;
    }

    输出结果:

    name:小虎, age:24, height:169CM, weight:100KG

      说明:定义位于类声明中的函数都将自动成为内联函数;和使用结构成员一样,类对象也通过成员运算符来访问其成员;创建的每一个新对象都有自己的存储空间,用于存储其内部变量和类成员,同一个类的所有对象都共享同一组类方法,即每个方法都只有一个副本。在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法。

      

      4、类小结

        指定类设计的第一步是提供类声明。类声明类似结构声明,包括数据成员和函数成员。声明有私有部分,在其中声明的成员只能通过成员函数进行访问;声明还有共有部分,在其中声明的成员可以被使用类对象的程序直接访问。通常,数据成员被放在私有部分中,成员函数被放在公有部分中,因此典型的类声明格式如下:

        class className{

        private:

           data member declarations

        public:

           member function prototypes

       };

      公有部分的内容构成了设计的抽象部分——公有接口。将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。

      类设计的第二步是实现类成员函数。可以在类声明中提供完整的函数定义,而不是函数原型,但通常的做法是提供单独的函数定义(除非函数很小;同时,在类声明中定义的成员函数会被自动转换成内联函数)。在这种情况下,需要使用作用域解析运算符来指出成员函数属于哪个类。

      要创建对象(类的实例),只需要将类名视为类型名即可。

      类成员函数(方法)可通过类对象来调用。方法是使用成员运算符句点。

    二、构造函数和析构函数

      构造函数和析构函数是在类体中说明的两种特殊的成员函数。

       构造函数是在创建对象时,使用给定的值来初始化对象;析构函数的功能恰好相反,是在系统释放对象前,对对象做一些善后工作。

      1、构造函数

        之所以需要构造函数,因为我们不能像初始化常规变量和结构那样来初始化类对象。通常,类的数据成员是私有的,这意味着程序不能直接访问数据成员;程序只能通过成员函数来访问类的数据成员,因此需要设计合适的成员函数,才能成功地将对象初始化。

        (1)声明和定义构造函数

        类构造函数:专门用于构造新对象、将值赋给它们的数据成员;构造函数可以带参数、可以重载,同时没有返回值(但是不能使用void关键字来指示构造函数没有返回值)。构造函数是类的成员函数,系统约定构造函数名必须和类名相同。

        对于构造函数说明以下几点:

          *构造函数的函数名必须和类名相同。构造函数的主要作用是完成初始化对象的数据成员以及其他的初始化工作;

          *在定义构造函数时,不能指定函数的返回值类型,也不能指定为void类型;

          *一个类可以定义若干个构造函数。当定义多个构造函数时,必须满足函数重载的原则;

          *构造函数可以指定参数的缺省值;

          *若定义的类要说明该类的对象时,构造函数必须是公有的成员函数。如果定义的类仅用于派生其他类时,则可将构造函数定义成保护的成员函数。

          *每个对象必须有相应的构造函数。如果没有显式地定义构造函数,系统默认缺省的构造函数。

         由于构造函数属于成员函数,它对私有数据成员、保护的数据成员和公有数据成员均能进行访问。

     1 #include <iostream>
    2
    #include <string> 3 4 class Person{ 5 private: 6 std::string m_name; 7 int m_age; 8 float m_height; 9 float m_weight; 10 public: 11 Person(std::string name , int age , float height, float weight){//构造函数,可以直接在类定义文件中定义,也可以在原文件中定义;可以设定参数默认值 12 m_name = name; 13 m_age = age; 14 m_height = height; 15 m_weight = weight; 16 } 17 };

       (2)使用构造函数    

         C++提供了两种使用构造函数来初始化对象的方式。第一种方式是显式地调用构造函数:

            Person per = Person("name",12,165,50);

        另一种是隐式地调用构造函数:

            Person per1("name1",15,160,45);

            这与下面的方式是等价的:

            Person per1 = Person("name1",15,160,45);

        每次创建对象(甚至使用new动态分配内存)时,C++都使用类构造函数。将构造函数与new一起使用的方式:  

            Person *per2 = new Person("name2",16,175,60);//这种情况下,类对象没有名称,但是可以使用指针管理该对象。

        注意:不能使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数使用来创建对象,而不能通过对象来调用。

        (3)默认构造函数

         默认构造函数是在没有提供显式初值时,用来创建对象的构造函数。

         如果没有提供任何构造函数,则C++将自动提供默认构造函数。

         默认构造函数没有参数,因为声明中不饱含值。

         注意:当且仅当没有定义任何构造函数时,编译器才提供默认构造函数(缺省的构造函数),编译器提供的默认构造函数并不对所产生对象的数据成员赋初值,即新产生的对象的数据成员的值是不确定的。为类定义了构造函数后,程序员就必须为他提供默认构造函数。如果定义了非默认构造函数,但是没有提供默认构造函数,则如下面的声明将会出错:

            Person per;//这样做的目的可能是想禁止创建未初始化的对象

         如果要创建对象,而不显式地初始化,则必须提定义不接受任何参数的默认构造函数。定义默认构造函数有两种方式,一种是给已有的构造函数的参数提供默认值,例如:

            Person(string name = "无名氏", int age = 0, float height = 0, float weight = 0){...};  

          另一种方式是通过函数重载来定义另一个构造函数-----没有参数的构造函数,例如:

            Person();//实际上是对非默认构造函数的重载

        定义了默认构造函数后,便可以声明对象,而不对它初始化,例如:Person onePer;

        注意:只能有一种默认构造函数,不能同时采用这两种方式来定义默认构造函数。

        注意区别下面这几种形式,不要混淆:

          Person per("XiaoHong", 12, 160,45);//隐式调用非默认构造函数(接受参数的构造函数)

          Person thePer();//声明一个返回值类型为Person的函数

          Person onePerson;//隐式调用默认构造函数,注意隐式调用默认构造函数不能带小括号,不然就是在声明一个函数。    

       (4)对局部对象、静态对象和全局对象的初始化

           对于局部对象:每次定义对象时,都要调用构造函数;

          对于静态对象:在首次定义对象时,调用构造函数,且由于对象一直存在,只调用一次构造函数。

          对于全局对象:在main()函数执行之前调用构造函数。

      

        关于构造函数的说明:

          1)、在定义类时,只要显式地定义了构造函数(不管是非默认构造函数还是默认构造函数),则编译器就不会产生缺省的构造函数;

          2)、所有的对象在定义时,必须调用构造函数。不存在没有构造函数的对象。

        

      2、析构函数 

         析构函数有如下特点:

           (1)析构函数是成员函数,函数体可以写在类体内,也可以写在类体外;

           (2)析构函数是一个特殊的成员函数,其名称必须是类名加上前置"~",以便和构造函数区别;

           (3)析构函数不能带任何参数,不能有返回值,不指定函数类型。

           (4)在类中,只能定义一个析构函数,析构函数不允许重载;

           (5)析构函数是在撤销对象时,系统自动调用的。

         何时调用析构函数有编译器决定,通常不应该在代码中显示地调用析构函数(有关例外情况后面会讲到):

            *如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。

            *如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。

            *如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。

            *程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。

       由于在类对象过期时其析构函数将被自动调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。

      3、不同存储类型的对象调用构造函数和析构函数

        (1)对于全局定义的对象(在函数外定义的对象):在程序开始执行时,调用构造函数;在程序结束时调用析构函数;

        (2)对于局部定义的对象(在代码块中定义的对象):在程序执行到定义对象的地方时,调用构造函数;在退出对象的作用域时,调用析构含糊;

        (3)对于静态局部对象:在首次到达对象的定义时,调用构造函数;到程序结束时调用析构函数。

        (4)对于new运算符动态生成的对象:在产生对象时,调用构造函数;只有使用delete释放对象时,才调用析构函数。如果不用delete来撤销动态生成的对象,程序结束时,对象仍然存在,并占用相应的存储空间,即系统不能自动调用析构函数来撤销动态生成的对象。

     

    Person.h文件:

     1 #include <iostream>
     2 #include <string>
     3 
     4 class Person{
     5 private:
     6     std::string m_name;
     7 public:
     8     Person(std::string);
     9     Person();
    10     std::string getName();
    11     ~Person();
    12 };

    Person.cpp源文件:

     1 #include "Person.h"
     2 #include <iostream>
     3 
     4 std::string Person::getName(){
     5     return m_name;
     6 }
     7 Person::Person(std::string name ){
     8     std::cout << """ << name << ""出生了
    ";
     9     m_name = name;
    10 }
    11 Person::~Person(){
    12    std::cout << m_name << "死了,欧耶!
    ";
    13 }
    14 Person::Person(){
    15     std::cout <<""无名氏"出生了
    ";
    16     m_name = "无名氏";
    17 }

    main.cpp:

     1 #include <iostream>
     2 #include "Person.h"
     3 
     4 using namespace std;
     5 Person GlobPer;//对象无名氏
     6 void creatPerson(Person );
     7 int main(int argc, const char * argv[]) {
     8     std::cout << "
    ------------------代码块开始------------------
    ";
     9    
    10     {
    11         Person per1("小红");//对象小红
    12         Person *per2 = new Person("小蓝");//对象小蓝
    13         Person per3 = Person("小可");//属于初始化
    14         per3 = Person("小宇");//创建了一个没有名字的临时对象,然后把对象赋给了per3
    15         std::cout << "+++++creatPerson()函数开始+++++
    ";
    16         creatPerson(per1);
    17         std::cout << "+++++creatPerson()函数结束+++++" << endl;
    18         delete per2;
    19        
    20     }
    21     cout << "------------------代码块结束------------------
    
    " ;
    22     return 0;
    23 }
    24 void creatPerson(Person one){
    25 
    26     Person myPer(one.getName());
    27     
    28 }

    输出结果:

     1 "无名氏"出生了
     2 
     3 ------------------代码块开始------------------
     4 "小红"出生了
     5 "小蓝"出生了
     6 "小可"出生了
     7 "小宇"出生了
     8 小宇死了,欧耶!
     9 +++++creatPerson()函数开始+++++
    10 "小红"出生了
    11 小红死了,欧耶!
    12 小红死了,欧耶!
    13 +++++creatPerson()函数结束+++++
    14 小蓝死了,欧耶!
    15 小宇死了,欧耶!
    16 小红死了,欧耶!
    17 ------------------代码块结束------------------
    18 
    19 无名氏死了,欧耶!

      注意:在默认情况下,将一个对象赋给同类型的另一个对象时,C++将原对象的每个数据成员的内容复制到目标对象相应的数据成员中。

      提示,如果既可以通过初始化,也可以通过赋值创建对象,则应采用初始化方式。通常初始化效率更高。

      4、C++11列表初始化

      在C++11中,可以将列表初始化语法用于类,只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们扩起来。例如有如下的类定义:

        class Person{

        private:

           string name_;

           int age_;

        public:

           Person(string name, int age);

           Person();

        }

        那么对Person对象的列表初始化语法如下:

          Person per{"name", 14};//这种格式与非默认构造函数的参数列表匹配,那么将调用非默认构造函数初始化对象

          Person thePer = {"xiaohong", 15};

          Person onePer{};//这种格式和默认构造函数相匹配,那么将调用默认构造函数初始化对象

      

      5、const成员函数

       如果把对象声明成为const,那么该对象就只能调用调用const成员函数,以确保方法不会修改对象。

       声明和定义const成员函数的方法:1)、声明成员函数时,在函数的括号后面加上const关键字;2)、定义const成员函数的时候,也应该在函数的括号后面加上const关键字。

       例如,假设Person有一个show()的成员函数,如果想要让Person的const对象能够调用show()成员函数,声明show()函数的方式应该如下:

              void show() const;

       定义show()成员函数的方式:

              void Person::show() const{....};

       一般来说,只要类方法只要不修改调用对象,就应该声明为const。注意,非const对象可以调用const成员函数;但是,const对象只能调用const成员函数。

      

      6、构造函数与析构函数小结:

        构造函数是一种特殊的成员函数,在创建对象时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标(参数列表)都不同。另外构造函数没有声明类型。通常,构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表相同。

        默认构造函数没有参数,因此如果创建对象时没有进行显式地初始化,则将调用默认构造函数。如果程序中没有提供任何构造函数,则编译器会为程序定义一个默认构造函数;否则,必须自己提供默认构造函数。默认构造函数可以没有任何参数;如果有,则必须给每个参数提供默认值。

        当对象被删除时,程序将调用析构函数。每个类只能有一个析构函数。析构函数没有返回类型(连void都没有),也没有参数。其名称为类名称前加上~。

        如果构造函数使用了new,则必须提供使用delete的析构函数。例如,假设类Card的数据成员和构造函数如下:

           class Card{

            private:

                int no_;

                Person *person_;//person为一个Person对象,假设Person的构造函数为Person(string name = "...");

            public:

                Card(string name, int no = 0){

                  person_ = new Person(name);

                  no_ = no;

                }

          }

          那么Card的析构函数应该如下:

            ~Card(){

              delete person_;

            }

    三、this指针

      this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)。一般来说,所有的类方法都将this指针设置为调用他的对象的地址。

      注意:每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this,可以将*this作为调用对象的别名。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。

    Man.h:

     1 #include <iostream>
     2 #include <string>
     3 using namespace std;
     4 class Man{
     5 private:
     6     int age_;
     7     string name_;
     8 public:
     9     Man();
    10     Man(string name, int age);
    11     const Man & whoIsOlder(const Man &) const;
    12     void introduce() const;
    13     int getAge() const;
    14     string getName() const;
    15 };

    Man.cpp:

     1 #include "Man.h"
     2 #include <iostream>
     3 
     4 Man::Man(string name, int age){
     5     name_ = name;
     6     age_ = age;
     7 }
     8 Man::Man(){
     9     name_ = "无名氏";
    10     age_ = 0;
    11 }
    12 void Man::introduce()const{
    13     std::cout << "我的名字:" << name_ << ", 我的年龄:" << age_ << endl;
    14 }
    15 string Man::getName()const{
    16     return name_;
    17 }
    18 int Man::getAge()const{
    19     return age_;
    20 }
    21 const Man& Man::whoIsOlder(const Man & aMan) const{
    22     if (age_ > aMan.getAge()) {
    23         return *this;
    24     }
    25     return aMan;
    26 }

    main.cpp:

    #include <iostream>
    #include "Man.h"
    
    using namespace std;
    
    int main(int argc, const char * argv[]) {
        Man manA{"小红", 45};
        Man manB{"冬瓜",25};
        manA.introduce();
        manB.introduce();
        cout << manA.whoIsOlder(manB).getName() << "年龄比较大" << endl;
        return 0;
    }

    输出结果:

    1 我的名字:小红, 我的年龄:45
    2 我的名字:冬瓜, 我的年龄:25
    3 小红年龄比较大

    四、对象数组

      声明对象数组的方法和声明标准类型数组的方法相同。假设Person类,那么声明Person对象数组的方法:

          Person pers[3];

      声明对象数组的要求这个类必须有默认构造函数(不论是编译器提供的还是自定义的)。因为声明数组的时候,对数组中的元素进行初始化的时候将调用默认构造函数。

      可以用构造函数来初始化数组元素。早这种情况下必须为每个元素调用构造函数:

          Person pers[3] = {

              Person{"小明",12},

              Person{"小红",13},

              Person{"小虎",11}

          };

       初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后降临时对象的内容复制到相应的元素中。因此,要创建类对象数组,这个类必须有默认构造函数。

    五、类作用域

        在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。另外,类作用域意味着不能从外部直接访问类的成员,公有成员函数也是如此。也就是说,要调用公有成员函数,必须通过类对象。同样,在定义成员函数时,必须使用作用域解析运算符。

        在类声明或成员函数定义中,可以使用为修饰的成员名称(未限定的名称)。构造函数名称被调用时,能被识别,是因为它的名称与类名相同。在其他情况下,使用类成员名时,必须根据上下文使用直接成员运算符(.)、间接成员运算符(->)或作用域解析运算符(::)。

        (1)作用域为类的常量

        使符号常量的作用域为类的方法有两种:

          1)、在类中声明一个枚举。在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。例如:

                class Year{

                private:

                   enum {Months = 12};//这只是声明了一个新的数据类型,因此声明枚举并不会创建新的数据成员。

                   int index;

                  ......

                }

            注意,用这种方式声明枚举并不会创建类数据成员,即所有对象中都不包含枚举。因为枚举声明只是创建了一个新的数据类型,而枚举的枚举值在整个类中相当于符号名称。

          2)、另一种在类中定义常量的方式是使用关键字static。在类声明中用static创建的const常量将与其他静态变量存储在一起,而不是存储在对象中,由此可以创建作用域为类的常量。例如:

                class Year{

                private:

                   static const int Months = 12;

                   int manth[Months];

                   .....

                }

       (2)作用域内枚举

          传统枚举中,作用域相同的两个枚举,如果两个枚举中有重名的枚举量,那么将会发生冲突。例如下面两个作用域相同的枚举:

            enum Egg{Small, Medium, Large,Jumbo};

            enum T_shirt{Small, Medium,Large,Xlarge};

          这样将无法通过编译,因为Egg Small和T_shirt Small位于同一个作用域,这将会发生冲突。为避免这种情况,C++提供了一种新枚举,其枚举的作用域为类。可以将上面的两个枚举进行改进如下:

            enum class Egg{Small, Medium, Large, Jumbo};

            enum class T_shirt {Small. Medium, Large,Xlarge};

          也可以使用关键字struct代替class。无论使用哪种方式,都需要使用枚举名来限定枚举量:

            Egg chioce = Egg::Large;

            T_shirt shirt = T_shirt::Large;

         同时,C++11还提高了作用域内枚举的安全性。在有些情况下,常规枚举将自动转换为整型,但在作用域内枚举不能隐式地转换为整型,即不能将枚举直接赋值给整型变量,需要进行显式转换。

         默认情况下,C++11作用域内枚举的底层类型为int,但也可以通过声明来改变底层类型,但是底层类型必须为整型:

          enum class : short Egg{Small, Medium, Large, Jumbo};// :short 将底层类型指定为short

        在常规枚举中,也可以通过上面的方式改变常规枚举的底层。

    六、抽象数据类型

         栈的特征,首先,栈存储了多个数据项(该特征使得栈成为了一个容器————一种更为通用的抽象);其次,栈由可对它执行的操作来描述。

            *可创建空栈

            *可将数据项添加到栈顶(压入)

            *可从栈顶删除数据项(弹出)

            *可查看栈是否填满

            *可查看栈是否为空

       

  • 相关阅读:
    Java 反射
    类中静态/普通/构造初始化顺序
    计算机世界中的0和1
    Java并发练习
    HashMap底层
    HashMap 与 Hashtable 的区别
    为什么重写了equals() 就要重写hashcode()
    干货型up主
    JSP页面元素
    重定向与请求转发的区别
  • 原文地址:https://www.cnblogs.com/mupiaomiao/p/4630409.html
Copyright © 2020-2023  润新知