详解C++中单继承与多继承的使用

目录
  • 前言
  • 1.继承的概念和定义
    • (1)继承的概念
    • (2)继承的定义方法
    • (2)继承后子类的成员类型
  • 2.基类与派生类的赋值转换
    • (1)派生类赋值给基类
    • (2)基类给派生类
  • 3.继承中的作用域
    • (1)隐藏的概念
    • (2)例题
  • 4.派生类的默认成员函数
    • (1)默认生成的成员函数
    • (2)自己写
  • 5.友元与静态成员
  • 6.多继承
    • (1)概念
    • (2)复杂的菱形继承
    • (3)虚继承解决菱形继承问题
    • (4)虚继承的原理
  • 7.继承与组合
    • (1)两者区别
    • (2)继承与组合的区别
    • (3)使用情况
  • 8.总结

前言

C++的继承机制相对其他语言是比较复杂的一种,不同于java只支持单继承,C++不仅支持单继承,也支持多继承,对于多继承中的菱形问题会引发一系列的麻烦,C++的两个重要缺陷,一个是多继承,一个是垃圾回收器。本文将详细讲解C++的单继承和多继承,以及菱形继承的解决方法及原理。

1.继承的概念和定义

(1)继承的概念

继承是面向对象设计使代码可以复用的重要手段,它允许程序员在保持原有类的基础上进行扩展。被扩展的类称为基类或者父类,扩展生成的类叫做子类或者派生类,继承是类设计层次的复用。

继承的作用是使得子类中既包含父类的成员,也可以包含自己的成员。

(2)继承的定义方法

class Person
{
private:
	string _name;
	int _age;
};
class Student :public Person
{
private:
	int _id;
};

看这一段代码,其中子类Student继承了父类Person,Student后的public表示的是继承方式。

(2)继承后子类的成员类型

继承方式和父类的成员属性共同决定了子类中的成员属性。我们用一张表来表示三者之间的关系。

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 派生类中不可见 派生类中不可见 派生类中不可见

我们只需要两点来记忆这个表格:

1.基类的private成员在派生类中无论以什么方式继承都是不可见的。

2.子类中的成员属性取继承方式和父类成员属性中权限小的那个: public>protected>private

表格的说明:

1.不可见的意思不是没有被继承,而是不能使用,在底层继承下来比没有继承下来更方便。

2.在父类中private和protected没有区别,但是在子类中,protected成员可以在类内访问,而private不能,因此可以说protected是为了继承而存在的。

3.如果不写继承方式,如果子类是class定义的,那么默认为private继承,是struct定义的,默认是public继承。

4.不可见与private成员区别:不可见指的是在类内与类外都不能使用,private成员在类内可以使用,在类外不可以使用。

5.不想给子类访问的成员我们设成private。

2.基类与派生类的赋值转换

(1)派生类赋值给基类

我们定义了一个父类person和它的派生类student,以上是它们各自的成员。

当我们将一个派生类的对象赋值给基类的对象时,发生的过程我们称之为切片。即只将子类中父类成员赋值过去。当父类中有private成员时,同样会进行切片,只是不显示而已,因此继承中尽量不要定义私有成员。

注意,这种赋值兼容方式仅限于公有继承。

私有继承不支持切片,这是因为对于父类中的public成员,私有或保护继承之后会转变成private/protected类型,而赋值时会发生将派生类对象中的private/protected成员赋值给父类对象中的public成员的现象,但是private/protected成员在类外是不能被访问的,因此不支持私有继承。

	Person b;
	Student a;
	b = a;
	Person* ptr = &a;
	Person& ref = a;

注意一个细节,我们可以使用引用赋值,说明这里并不存在类型转换的行为,因为类型转换中间会产生临时变量,需要使用const引用。

double d;
const int& r=d;//发生了类型转换。

(2)基类给派生类

先说结论:

父类对象不可以直接赋值给子类对象。

这是因为子类对象中有父类不存在的类型,无法进行赋值。也不能通过所谓的强制类型转换进行赋值。

但是C++支持指针和引用的赋值:

	Person b;
	Student a;
	a = (Student)b;//不正确
	Student* ptr = (Student*)&b;//支持
	Student& ref = (Student&)b;//支持

虽然指针和引用可以,但是当指针向下访问的时候超过父类对象的时候会出现问题。

会出现指向空的情况。

3.继承中的作用域

(1)隐藏的概念

基类和派生类都有各自独立的作用域。

如果不同的域内有同名的成员,我们根据就近原则或者指定作用域的方式来指定成员的位置。

隐藏:子类与父类中出现同名成员,子类成员将屏蔽父类成员对同名成员进行直接访问,这种情况叫隐藏,也叫重定向

注意如果是成员函数的隐藏,只要函数名相同就会构成隐藏,与参数无关。

举一个例子:

class Person
{
protected:
	string _name = "小六子";
	int _num = 111;
};
class Student :public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "身份证号:" << Person::_num << endl;
		cout << "学号:" << _num << endl;
	}
protected:
	int _num = 999;
};
int main()
{
	Student s1;
	s1.Print();
}

在这段代码中,Person和Student分别定义了_num,当子类对象中的成员函数直接访问_num时,根据的是就近原则,访问的是子类中的_num,当要访问父类中的_num时,需要使用::来指定类域,就可以进行访问。父类中的_num与子类中的_num构成隐藏。

这段代码打印的结果是:

(2)例题

这里有一道小小的题目,是关于函数隐藏的:

class A
{
public:
	void func()
	{
		cout << "func" << endl;
	}
};
class B :public A
{
public:
	void func(int i)
	{
		A::func();
		cout << "func(int i)->" << i << endl;
	}
};
void Test()
{
	B b;
	b.func(10);
	b.func();
}

提问在Test中的两个函数能否调用成功?

b.func(10)可以调用成功,因为构成了隐藏。
b.func()不能调用成功,会发生变异报错,因为隐藏了调不动。

4.派生类的默认成员函数

对于六大默认成员函数我们这里暂时先讨论4种重要的,即:构造函数,析构函数,拷贝构造,赋值运算符重载。

(1)默认生成的成员函数

当我们不在子类中书写时,编译器会默认生成。这里只需要记住一句话:

继承下来的成员调用父类的来处理,自己的按基本规则来处理。

以构造函数举例:派生类中的父类成员调用父类中的构造函数,自己的成员按照构造函数自动生成的规则来。

(2)自己写

自己写的情况

1.父类没有默认构造函数,需要我们自己写构造函数。

2.子类有资源需要释放,需要我们自己写析构函数。

3.如果子类涉及浅拷贝问题,需要自己写拷贝构造和赋值重载。

构造函数

父类成员调用对应的父类构造函数处理。子类成员按普通类处理。

举一个例子:

class Person
{
public:
	Person(string name , int num=2)
		:_name(name)
		,_num(num)
	{}
protected:
	string _name ;
	int _num ;
};
class Student :public Person
{
public:
	Student(int num,string _name,int _num)
		:_num(num)
		,Person(_name,_num)
	{}
protected:
	int _num;
};
int main()
{
	Student s1(2,"zhangsan",2);
}

看这一段代码,父类中没有默认构造函数(注意与默认成员函数区分),因此要初始化父类中的对象需要我们自己书写子类中的构造函数。在书写构造函数时,父类对象成员初始化使用父类中的构造函数,子类成员的初始化按正常方式书写即可。

拷贝构造和运算符重载函数

	Student(const Student& s)
		:Person(s)
		,_num(s._num)
	{}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);//不指明类域的话会发生自己调自己的情况
			_num = s._num;
			return *this;
		}
	}
	int main()
{
	Student s1(2,"zhangsan",2);
	Student s2(s1);
	Student s3 = s2;
}

我们可以通过调试来查看结果:

析构函数

析构函数比较特殊,对于父类中的析构函数,我们不需要指定去书写,就像下面这种情况:

//父类中的析构
	~Person()
	{
		cout << "~Person" << endl;
	}
//子类中的析构
		~Student()
	{
		Person::~Person();
	}

注意,析构函数的名字在最后会被统一处理成destructor(),如果不指定类域的话,父类析构函数和子类析构函数会构成隐藏,因此需要指定类域。
对于上述int中的代码,需要析构三个子类对象,打印出的结果是:

我们发现调用了六次父类中的析构函数。这说明每个对象的父类成员都被析构了两次。如果需要释放空间,则一定会报错。

先说结论:我们自己实现子类构造函数时,不需要显示调用父类析构函数,我们显示调用一次,它还会自动调用一次。

下面简单说明一下,为什么程序需要自动调用:

我们知道变量的定义是发生在栈中的,因此就存在构造和析构的顺序问题,栈满足先入后出原则,因此先构造的需要后析构。

在构造的过程中,我们会先初始化父类成员,再初始化子类成员。因此我们需要先析构子类成员,再析构父类成员。

如果先析构父类会打乱栈的顺序,因此编译器会自动调用父类的析构函数。

5.友元与静态成员

这个只需要记住两点:

1.友元关系不能继承。

2.静态成员会被继承下来,无论继承多少,静态成员只有一个。

6.多继承

(1)概念

一个类有两个及以上父类时称这个继承关系为多继承。

class Student
{
public:
protected:
	int _id;
};
class Teacher
{
public:
protected:
	int _course;
};
class Assistant:public Student,public Teacher
{
public:
protected:
protected:
};

我们使用逗号表示分隔,即继承多个父类。可以通过调试来观察子类Assitant的内容:

(2)复杂的菱形继承

菱形继承是多继承的一种情况:

具有这样的继承关系的称为菱形继承。

菱形继承出现的问题:从对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。

数据冗余指的是类Assistant中会有两份Person的成员,二义性指的是这两份成员每一次调用不知道调用的的是哪一个,需要指定类域。

这段代码表示的就是菱形继承的关系:

class Person
{
public:
	string _name;
};
class Student:public Person
{
public:
protected:
	int _num;
};
class Teacher:public Person
{
public:
protected:
	int _id;
};
class Assistant:public Student,public Teacher
{
public:
protected:
protected:
	int _course;
};
int main()
{
	Assistant a;
}

我们通过调试可以观测a中的内容,发现会存在两份Person中的成员:

如果要对这两个Person成员赋值时,需要指定类域。

	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

这就是所谓的二义性,在实际中一个人不能有两个名字,对于冗余性来说,如果Person中有一个很大的数组浪费的空间会很多。

(3)虚继承解决菱形继承问题

虚继承可以解决菱形继承的二义性和数据冗余问题。如上面的继承关系,在Student和Teacher的继承Person时使用的虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

class Student:virtual public Person
{
public:
protected:
	int _num;
};
class Teacher:virtual public Person
{
public:
protected:
	int _id;
};

只需要在菱形的腰部两个父类加入virtual关键词即可。

注意要在菱形的腰部。

当加完之后,在Assistant的对象中,Person类的_name成员就只有一个了。无论是否指定类域,更改的变量都只有一个:

(4)虚继承的原理

内存演示

要研究虚继承的原理,我们给出一个简化的菱形继承结构,再借助内存窗口窗口观察对象成员的模型。

class A
{
public:
	int _a;
};
class B:public A
{
public:
	int _b;
};
class C:public A
{
public:
	int _c;
};
class D :public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

当没使用虚继承(即没有使用virtual时)

我们使用内存窗口来观察内容:

通过观察内存中的布局,我们发现d中的B父类对象和C父类对象中的内容分别是连续存放的,B中有父类A中成员_a的值是1,其自己成员_b的值是3,两者的内存是挨着的,C同理,对于D类中自己的成员_d,放在了内存的最后。

确定d中B类对象和C类对象的存储顺序是根据继承顺序决定的。由于上述代码是class D :public B, public C,因此B类的对象会存在C类的前面。
而当我们给腰部加上virtual构成虚继承之后:

class B:virtual public A
{
public:
	int _b;
};
class C:virtual public A
{
public:
	int _c;
};

使用virtual之后,我们发现已经将A中对象_a放入在了最后,因此无论指定不指定类域,改变的都是同一个_a的值。

但同时我们发现内存中多了两行,那么这两行是干什么的呢?

虚基表

从格式来看,这两行显然是都是地址。

我们再开辟一个内存2,向其中输入上面地址,我们发现地址中存储的内容是00 00 00 00,C类对象中同理,这里就不演示了。

这里00 00 00 00的意义在后面多态中会学习到,注意看它的下一个位置存放的是00 00 00 14

这里是十六进制,因此表示的是20这个数字。

再来看内存1:

两者的地址之差刚刚好是20个字节。

因此我们可以知道:在虚继承中,B类对象和C类对象的内存中新加入的是一个地址,分别用于寻找两者与A类型变量的偏移量。B类对象与A类对象的偏移量是20,同理可验证C类对象的偏移量是12。而内存2也有一个专有名词:虚基表

总结:A一般叫做虚基类,在D里面,A类成员放在一个公共的位置,有时B要找A,C要找A,就要通过虚基表中的偏移量进行计算。

比如,当我们再用B类和C类建立两个变量:

	B b = d;
	C c = d;

此时会发生切片处理,需要将d中的A类对象赋值到b和c中,此时就需要使用到虚基表来寻找。

再比如:

	B* pb = &d;
	pb->_a = 10;

pb指向了d的首地址,要更改d中的_a的值,指针pb也需要使用虚基表来进行寻找。

7.继承与组合

(1)两者区别

首先我们要对继承和组合进行区分:

继承表示的是子类继承父类,组合表示的是在一个类中定义了另一个类的成员变量。

//继承
class A
{
public:
	int _a;
};
class B:public A
{
public:
	int _b;
};
//组合
class C
{
public:
	int _c;
};
class D
{
public:
	int _d;
	C _obj;
};

(2)继承与组合的区别

我们需要明确一点:类之间,模块之间最好是低耦合,高内聚的,因为方便维护。

低耦合:类之间依赖关系越弱越好。

高内聚:内部成员关系紧密。

1.继承对应于白盒:B可以直接使用A中的公有和保护成员,破坏了封装性。

2.组合对应于黑盒:D只能使用C的公有,不能直接使用保护成员。

举一个例子:

如果A中有5个public,5个protected

对于组合来说,非基类只能使用这5个public,基类中的其他成员随便修改都不会影响该非基类。

对于继承来说,基类中一切的改变都会影响子类。

那可以抛弃继承的语法吗?当然是不行的。

多态是建立在继承的基础上的。

(3)使用情况

1.如果B就是一个A,比如Student是一个Person,我们称这种关系为is-a关系,此时适合使用继承。

2.如果D被包含于C,比如head包含eyes,我们称这种关系为has-a关系,此时适合使用组合。

3.当遇到特殊情况,is-a和has-a都可以讲通时,优先使用组合

8.总结

C++的语法复杂在于C++是第一个吃螃蟹的人,很多地方会考虑太多,拿多继承举例,有了多继承就有了菱形继承,有了菱形继承,就有了菱形虚拟继承,底层实现就更为复杂了,所以一般不建议设计多继承,设计了多继承也不建议设计菱形继承。

到此这篇关于详解C++中单继承与多继承的使用的文章就介绍到这了,更多相关C++单继承 多继承内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++ 超详细梳理继承的概念与使用

    目录 继承的概念及定义 继承的概念 继承定义 定义格式 继承关系和访问限定符 继承基类成员访问方式的变化 基类和派生类对象赋值转换 继承中的作用域 派生类的默认成员函数 继承与友元 继承与静态成员 复杂的菱形继承及菱形虚拟继承 菱形继承 虚拟继承解决数据冗余和二义性的原理 继承的总结和反思 继承的概念及定义 继承的概念 继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类.继承呈现了面向对象程序设计的层次结构,体

  • C++多继承多态的实例详解

    C++多继承多态的实现 如果一个类中存在虚函数,在声明类的对象时,编译器就会给该对象生成一个虚函数指针,该虚函数指针指向该类对应的虚函数表. 多态的实现是因为使用了一种动态绑定的机制,在编译期间不确定调用函数的地址,在调用虚函数的时候,去查询虚函数指针所指向的虚函数表. 派生类生成的对象中的虚函数指针指向的是派生类的虚函数表,因此无论是基类还是派生来调用,都是查询的是派生类的表,调用的是派生类的函数. 如果发生了多继承,多个基类中都有虚函数,那么该是怎样的呢?虚函数指针如何排列,多个基类的指针为

  • C++类继承时的构造函数

    前言: 子类需要编写自己的构造函数和析构函数,需要注意的是,子类只负责对新增的成员进行初始化和扫尾编写构造和析构函数,父类成员的初始化和扫尾工作由父类的构造函数和析构函数完成. 无论何种类型的继承方式,子类都无权访问父类的所有成员,所以子类对父类的初始化需要父类的构造函数完成.此时,子类的构造函数必须提供父类构造函数所需的参数. 子类构造函数的语法如下: 子类::子类(全部参数表):父类1(父类1参数表),父类2(父类2参数表)      ...对象成员1(对象成员1参数表),对象成员2(对象成

  • C++ 多继承详情介绍

    C++支持多继承,即允许一个类同时继承多个类. 关于多继承,一直以来争议不断,有一部分人认为多继承会带来大量的问题,为了解决这些问题会使得语言本身变得非常复杂,因此应当避免.另外一派认为多继承在某些场景下可以起到非常关键的作用,应当予以支持. 关于多重继承是好是坏,这是一个非常复杂的问题,网上历来争议不断.因此不过多阐述,感兴趣的同学可以查阅一下相关资料.仅仅从实际支持来看,目前市面上大部分的语言包括Java仅支持单继承,只有C++等少数语言支持多继承.这和C++的理念也有关,即认定不能通过减少

  • c++ 虚继承,多继承相关总结

    看这一篇文章之前强烈建议先看以下我之前发布的 虚指针,虚函数剖析 例1: 以下代码输出什么? #include <iostream> using namespace std; class A { protected: int m_data; public: A(int data = 0) {m_data=data;} int GetData() { return doGetData(); } virtual int doGetData() { return m_data; } }; class

  • C++详细讲解继承与虚继承实现

    目录 继承的概念及定义 概念: 定义: 继承关系和访问限定符 总结 基类和派生类对象赋值转换 继承中的作用域 派生类的默认成员函数 继承与友元 继承与静态成员 复杂的菱形继承及菱形虚拟继承 虚继承原理 继承的总结 继承的概念及定义 概念: 继承机制是面向对象程序设计为了提高代码复用率的一种手段,它可以保持原类特性的基础上进行拓展,简单来说继承是类层次的复用. 接下来我们来看一个简单的继承 class Person { public: void Print() { cout<<"nam

  • 详解C++中单继承与多继承的使用

    目录 前言 1.继承的概念和定义 (1)继承的概念 (2)继承的定义方法 (2)继承后子类的成员类型 2.基类与派生类的赋值转换 (1)派生类赋值给基类 (2)基类给派生类 3.继承中的作用域 (1)隐藏的概念 (2)例题 4.派生类的默认成员函数 (1)默认生成的成员函数 (2)自己写 5.友元与静态成员 6.多继承 (1)概念 (2)复杂的菱形继承 (3)虚继承解决菱形继承问题 (4)虚继承的原理 7.继承与组合 (1)两者区别 (2)继承与组合的区别 (3)使用情况 8.总结 前言 C++

  • 详解C++ 中的三种继承方式

    public 方式继承 基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态;基类的私有成员不可见,基类的私有成员仍然是私有的,派生类不可访问基类中的私有成员. 基类成员对派生类对象的可见性对派生类对象来说,基类的公有成员是可见的,其他成员是不可见的. 所以,在公有继承时,派生类的对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有成员和保护成员. 简单来说,派生类能访问基类的public, prote

  • 详解angular中的作用域及继承

    在一些使用angular框架的大型项目中,似乎有很多个controller,而每个controller都有自己的$scope. 1.$rootscope $rootScope顶级作用域,也叫根作用域,类似于window,window的属性在任何子作用域都可以访问.$rootScope则是所有controller进行数据沟通的中间域,即在$rootScope中的数据,在每个controller中都能通过$rootScope.xxx获取到. 2.$scope (1)作用 $scope 就在视图和控制

  • 详解C++编程中的私有继承和公有继承

    C++类的私有继承 在声明一个派生类时将基类的继承方式指定为private的,称为私有继承,用私有继承方式建立的派生类称为私有派生类(private derived class ), 其基类称为私有基类(private base class ). 私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,即派生类的成员函数能访问它们,而在派生类外不能访问它们.私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们.一个基类成员在基类中的访问属性和在派生类中

  • 详解Java中的封装、继承、多态

    封装 在如何理解面向对象这篇文章中,提到所谓的封装就是"功能都给你做好了,你不必去理解它是怎么写出来的,直接使用即可.".但你得清楚一点,那就是这句话是相对于使用者来说的,而作为开发者,封装就得我们自己来干. 那么作为开发者,我们应该如何去封装呢?其实你应该反过来问,他们应该如何去使用,这样一想会简单很多,作为使用者,自然是希望越简单越好,也就是说,一些复杂的东西,我们不应该让使用者去操作,那也就是说我们应该把复杂的,以及不必要的参数给它封死,不让使用者去操作. 为什么不让使用者去操作

  • 详解Java中使用externds关键字继承类的用法

    理解继承是理解面向对象程序设计的关键.在Java中,通过关键字extends继承一个已有的类,被继承的类称为父类(超类,基类),新的类称为子类(派生类).在Java中不允许多继承. (1)继承 class Animal{ void eat(){ System.out.println("Animal eat"); } void sleep(){ System.out.println("Animal sleep"); } void breathe(){ System.o

  • 详解Django模板层过滤器和继承的问题

    过滤器 模板层对变量的操作实际还有很多,过滤器就是其中一种.学过Linux系统的一定知道管道操作符,其可以将上一步输出直接作为下一步输入进行处理,这里的过滤器就是类似管道符,其写法也是以管道符|为标志,允许我们对模板层获得的变量进行改变,例如大小写转换.增减大小等等 语法 {{ 变量|过滤器1|过滤器2:'值' |...}} 这里的过滤器可以分为两类,一类是不带参数的,例如lower,upper,safe:一类是带参数的add:'10',冒号分隔,后面跟上值 过滤器 示例 说明 lower {{

  • Android style的继承方式 点(.)和parent详解及实例

    Android style的继承方式 点(.)和parent详解及实例 一.概述 通过继承机制,可以利用已有的style来定义新的style.所定义的新的style型不仅拥有新定义的item,而且还同时拥有旧的item.我们称已存在的用来派生新的style为父style.由新定义的style,又称为子style.    比如: <style name="pickprof_guide_text"> <item name="android:textSize&qu

  • Java编程复用类代码详解

    本文研究的主要是Java编程中的复用类,那么到底复用类是什么东西,又有什么用法,下面具体介绍. 看了老罗罗升阳的专访,情不自禁地佩服,很年轻,我之前以为和罗永浩一个级别的年龄,也是见过的不是初高中编程的一位大牛之一,专访之后,发现老罗也是一步一个脚印的人.别说什么难做,做不了,你根本就没去尝试,也没有去坚持. If you can't fly then run,if you can't run then walk, if you can't walk then crawl,but whateve

  • JS 面向对象之继承---多种组合继承详解

    这一次要讲 组合.原型式.寄生式.寄生组合式继承方式. 1. 组合继承:又叫伪经典继承,是指将原型链和借用构造函数技术组合在一块的一种继承方式. 下面来看一个例子: function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { alert(this.n

随机推荐