C++:继承与派生

    

        为什么会有继承这样的语法呢??试想这样一个场景:假设我们这个App需要去获取不同类型用户的数据,并进行分类,那么就需要我们去写对应不同的类,比如说学生、老师、军人、公司职工…………每个类都需要有名字、联系方式、家庭住址、年龄……,我们会发现这样每个类都要写一份,非常冗余,于是我们的祖师爷为了解决这个问题,设计出了继承的语法,比如说用户的共同点是都是用户,我们就可以写一个关于人的类,作为基类,而不同类型用户就作为基类的派生类,去继承基类的成员,从而达到我们的目的。

一、继承的概念和定义

1.1 继承的概念

      继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

从下面这个代码可以很好地体现继承的特点

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};
class Student : public Person
{
protected:
	int _stuid; // 学号
};
class Teacher : public Person
{
protected:
	int _jobid; // 工号
};
int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}


        继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。如果我们使用监视窗口查看Student和Teacher对象,可
以看到变量的复用。调用Print可以看到成员函数的复用。
       监视窗口看上去给人的感觉就是,子类把父类当成了自己的一个成员一样

1.2 继承的定义

1、格式:Person是父类,也称作基类。Student是子类,也称作派生类

2、继承关系和访问限定符

我们会发现继承方式和访问限定符都存在公有、保护、私有这三种继承模式。

 3、继承基类成员访问方式的变化

 总结:

1、基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

 2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。(所以我们不想让子类继承就写成私有,想让子类继承就写成保护。可以理解为private防外人也防儿子,但是protected防外人但是不防儿子。

3、 上面表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。

4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,我们常用的一般都是class,所以最好显示地写出继承方式

5、在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承
,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

 

二、基类和派生类对象赋值转换

1、派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。

2、基类对象不能赋值给派生类对象

 我们开看下面这段代码

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};
int main()
{
	    //以前我们所知道的强制类型转化
	     double d = 1.1;
	     int i = d;
		 //子类对象可以赋值给父类对象/指针/引用
		Student sobj;
		Person pobj = sobj;
		Person* pp = &sobj;
		Person& rp = sobj;
        //反之  基类对象不能赋值给派生类对象
	    sobj = pobj;
}

3、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。

来看看下面这段代码:

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};
int main()
{
    //基类的指针可以通过强制类型转换赋值给派生类的指针
		Student sobj;
		Person* pp = &sobj;
		Student * ps1 = (Student*)pp;   // 这种情况转换时可以的。
	    ps1->_No = 10;
        
		Person pobj = sobj;
	    pp = &pobj;
	    Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
		ps2->_No = 10;
		return 0;
}

三、继承中的作用域

1. 在继承体系中基类派生类都有独立的作用域

2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义
。(在子类成员函数中,可以使用 基类::基类成员 显示访问

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

4. 注意在实际中在继承体系里面最好不要定义同名的成员。(但是有些情况下不可避免,后面会说)

四、派生类的默认成员函数

       6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

3. 派生类的operator=必须要调用基类的operator=完成基类的复制

4、派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。

5. 派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构
 

     我们来观察下面这个Person类,再来看看如何去写Student的构造、析构、赋值重载以及拷贝构造。

class Person
	{
	public:
		//构造函数
		Person(const char* name = "peter")
			: _name(name)
		{
			cout << "Person()" << endl;
		}
	     //拷贝构造
		Person(const Person& p)
			: _name(p._name)
		{
			cout << "Person(const Person& p)" << endl;
		}
	     //赋值重载
		Person& operator=(const Person& p)
		{
			cout << "Person operator=(const Person& p)" << endl;
			if (this != &p)
				_name = p._name;
	
			return *this;
		}
        //析构函数
		~Person()
		{
			cout << "~Person()" << endl;
		}
	protected:
		string _name; // 姓名
	};

 根据上图分析,可以确定学生类的成员函数应该如下:

class Student : public Person
	{
	public:
		Student(const char* name, int num)
			:Person(name)
			,_num(num)
		{
			cout << "Student()" << endl;
		}
		Student(const Student& s)
			:Person(s)
			, _num(s._num)
		{
			cout << "Student(const Student& s)" << endl;
	
		}
		Student& operator=(const Student& s)
		{
			if (this != &s)
			{
				Person::operator=(s);
				_num = s._num;
			}
			cout << "Student& operator=(const Student& s)" << endl;
			return *this;
		}
		~Student()
		{
			cout << "~Student()" << endl;
		}
		// 子类析构函数完成时,会自定调用父类析构函数,保证先析构子再析构父
	protected:
		int _num; //学号
	};

思考:

1、既然我们知道了怎么显式写,但是真的有必要吗??我们将子类的成员函数全部注释掉看看结果:

总结:

(1)构造函数最好是显式写一下。

(2) 如果不涉及资源管理,没必要写拷贝构造和赋值构造,涉及到资源管理需要进行深拷贝,就需要我们去显式写。

(3)析构函数一般情况想不用写,但在后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(涉及到虚函数)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
 

 2、子类和父类的构造和析构顺序是怎么样的?

五、继承与友元

 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

 

 解决方案就是在学生类里也搞个友元。

六、继承与静态成员

        基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员。静态成员无法继承,但是理论上来说是可以访问的。

思考:

1、如何计算该程序中一共创建了多少个对象??

设置一个静态成员变量,然后在基类的构造函数里++

如上图,我们可以发现如果把静态成员设为公有,那么就会出现可以修改的情况,所以我们最好把静态成员变量设成私有,然后用写一个静态成员函数getcount去获取结果

2、如何实现一个不能被继承的类 ?

      将基类的构造函数藏在私有作用域里,这样继承的时候子类不可见基类构造函数,就无法创建出子类对象了!(将析构函数设成私有也可以)

 但是你可能会想,把构造函数放在私域,那A不是也创建不了对象了吗???确实是这样的!

但是也有一个解决的方法,就是利用静态成员函数去返回这个构造函数。 

3、为什么2用静态成员函数呢??

            因为这涉及到先有鸡还是先有蛋的问题,我们不创建对象就调用不了这个函数,但是要调用这个函数又需要一个对象,所以为了解决这个问题,只能将该函数变成静态成员函数,这样我们可以通过类限定符去访问他 

七、复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

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


菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。

 

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

     为了能够更好地观察虚继承的过程,我们选择一个较为简单的模型。

class A
{
public:
	int _a;
};
// class B : public A
class B :  public A
{
public:
	int _b;
};
// class C : public A
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;
}

我们先看看没有进行虚继承时,内存中是怎样的

我们看看虚继承后是怎样的,为了能更好地观察,我们用32位环境

 思考:

1、不是为了解决冗余问题吗??为什么这边的空间反而变大了??

           解决数据冗余要付出存指针的代价,但是如果冗余的对象超过指针的大小的话,那么就赚了。哪怕真的损耗了一点空间。。至少二义性解决了

2、存的地址指向的空间难道不算空间消耗么??

       理论上来说是有一定的损耗,但是如果我们创建了多个d对象,其实就是多个d类型对象在共用一个 空间,那么就可以忽略不计了,因为理论上来说他们的偏移量都是不会变的!!比如我们再写个d2.

所以如果我们创建了多个d对象,那么存地址的优势就更突出了!!因为偏移量都是一样的

3、 A有一个_a对象,所以有一个指针,如果A有多个对象是存多个指针吗?

       我们看看下面的图:

所以哪怕A对象成员变量很多,也只会存一个地址。综合2和3我们可以得到一个结论,无论是创建特别多的d对象,还是说这个a的成员变量非常多,都只会存一个地址。所以虚继承显然能够解决数据冗余的问题。

4、既然原来b中的a空间存的是一个地址,那如果用b类型或者c类型的指针存d的地址,然后再分别访问_a   或者是用d赋值给b和c类型的对象得到的会是地址吗??

      

从该图我们可以看到并不会, d赋值给b和c的时候,他们会先通过这个地址找到存放偏移量的空间,然后再回来找到_a,最后也是按照存地址的方式去展现。而B C类型的指针存放d地址的时候,也会通过这个地址找到偏移量,然后再回过头来找_a。所以在这个过程中他们自己会进行处理。从这里我们也可以看到虚继承的访问会存在一定的时间消耗,因为已经不是单纯地解引用了,而是经过了多一层的解引用和计算。

5、下面这道题的运行结果是什么

class A{
public:
	A(const char *s) { cout << s << endl; }
	~A(){}
};
class B :virtual public A
{
public:
	B(const char *s1, const char*s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(const char *s1, const  char*s2) :A(s1) { cout << s2 << endl; }
};

class D : public C, public B
{
public:
	D(const char *s1, const char *s2, const char *s3, const char *s4)
		:B(s1, s2)
		,C(s1, s3)
		,A(s1)
	{
		cout << s4 << endl;
	}
};
int main() {
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

6、为什么虚基表的前四个字节位置是空出来的?? 

       这是为了后面的虚函数表做准备的,用来存从虚基表找到虚函数表的偏移量(菱形继承多态)。在多态那一环节会去验证。

 八、继承的总结和反思

1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
计出菱形继承。否则在复杂度及性能上都有问题。

2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

3. 继承和组合

优先使用组合,而不是类继承

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

        类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见(可以直接访问被继承对象的成员变量)。

  对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口(只能通过接口去访问被组合对象的成员变量)。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

继承的缺点有以下几点:

①:父类的内部细节对子类是可见的。

②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

组合的优点:

①:当前对象只能通过所包含的那个对象去调用其方法(只能用接口不能用成员变量),所以所包含的对象的内部细节对当前对象时不可见的。

②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。 

③:对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象。

组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。

结论:优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。

  这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/447777.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python从0到100(三):Python中的变量介绍

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…

四川易点慧电子商务有限公司抖音小店安全正规

在如今网络购物日益普及的时代&#xff0c;消费者对于购物平台的选择越来越挑剔。四川易点慧电子商务有限公司抖音小店以其安全正规的经营模式&#xff0c;赢得了广大消费者的信赖和好评。本文将为您详细介绍四川易点慧电子商务有限公司抖音小店的优势和特点&#xff0c;让您在…

windows下pytorch的dataloader多进程(num_workers)问题,为何num_workers的值只能为0?

问题背景介绍 本人是windows系统&#xff0c;在使用torch.utils.data.Dataloader加载torchvision中的数据集时&#xff0c;将其中的形参num_workers设置为了大于0的数&#xff0c;然后出现以下错误。 原因 在 Windows 系统下&#xff0c;num_workers 参数在使用 PyTorch 的 t…

内部文档多维保密,如何做好内部文件的保密措施?

在企业的日常运营中&#xff0c;内部文档往往包含了公司的核心战略、财务数据、客户信息等重要内容。一旦这些文件泄露&#xff0c;可能会给企业带来无法估量的损失。 因此&#xff0c;做好内部文件的保密措施显得尤为关键。 这里有一件真实的案例可以参考一下。 某大型制造企…

网红老阳分享的蓝海赚钱项目,这三个真香!

在互联网经济飞速发展的当下&#xff0c;寻找蓝海项目成为了许多创业者和投资者的首要任务。近期&#xff0c;知名网红老阳分享了一些他认为具有巨大潜力的蓝海项目&#xff0c;其中包括RPO人力资源、视频号带货和Temu跨境电商。下面我们将对这三个项目进行详细解析。 老阳分享…

照明灯十大知名品牌排行榜,前十名护眼台灯推荐

孩子们在学习时&#xff0c;良好的光线至关重要&#xff0c;因此&#xff0c;护眼台灯成为了许多家庭的首选。尽管人们对于护眼台灯的使用效果持有不同观点&#xff0c;但我多年来的使用体验证明&#xff0c;护眼台灯在保护视力、减轻眼睛疲劳方面发挥着不可或缺的作用&#xf…

C++ Qt学习——Qt启动流程

目录 1、点击文件&#xff0c;新建文件或项目 2、选择模板为Application&#xff08;Qt&#xff09;&#xff0c;Qt Qwidgets Application,再点击Choose ​编辑 3、给自己的文件起个名字 4、点击下一步 5、点击下一步 6、点击下一步 7、点击下一步 8、点击完成 9、直接…

应用方案 |安防摄像头(IPC)的步进马达及IR-CUT驱动芯片D6212

应用领域 安防摄像头&#xff08;IPC&#xff09;的步进马达及IR-CUT驱动。 02 功能介绍 D6212内置8路带有续流二极管的达林顿驱动管阵列和一个H桥驱动&#xff0c;单芯片即可实现2个步进电机和一个IR-CUT的直接驱动&#xff0c;使得电路应用非常简单。单个达林顿管在输入…

Verovio简介及在Windows10和Ubuntu 22.04上编译过程

Verovio是一个快速、便携、轻量级的开源库&#xff0c;用于将音乐编码倡议(Music Encoding Initiative(MEI))数字乐谱雕刻到SVG图像中。Verovio还包含即时转换器(on-the-fly converters)用于渲染Plaine & Easie Code、Humdrum、Musedata、MusicXML、EsAC和ABC数字乐谱。源代…

【Loss总结】适用与弱监督语义分割中的各类loss

【Loss总结】适用与弱监督语义分割中的各类loss 文章目录 【Loss总结】适用与弱监督语义分割中的各类loss交叉熵损失相对熵&#xff08;KL散度&#xff09;交叉熵 L1 LossL2LossSmoothL1LossDice loss梯度分析语义分割代码 交叉熵损失 交叉熵损失函数(CrossEntropy Loss)&…

大模型优化——重排序模型

检索增强生成(RAG)技术作为自大模型兴起后爆火的方向之一,已经广受研发者们追捧,大型语言模型(LLMs)如GPT系列和LLama系列在自然语言处理领域取得了显著的成功,但它们面临着幻觉、过时知识和不透明、不可追溯的推理过程等挑战。检索增强生成(RAG)通过整合外部数据库的…

VideoDubber时长可控的视频配音方法

本次分享由中国人民大学、微软亚洲研究院联合投稿于AAAI 2023的一篇专门为视频配音任务定制的机器翻译的工作《VideoDubber: Machine Translation with Speech-Aware Length Control for Video Dubbing》。这个工作将电影或电视节目中的原始语音翻译成目标语言。 论文地址&…

【Python】【Matplotlib】解决使用 plt.savefig() 保存的图片出现一片空白的问题

【Python】【Matplotlib】解决使用 plt.savefig() 保存的图片出现一片空白的问题 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#…

Leetcode : 1137. 高度检查器

学校打算为全体学生拍一张年度纪念照。根据要求&#xff0c;学生需要按照 非递减 的高度顺序排成一行。 排序后的高度情况用整数数组 expected 表示&#xff0c;其中 expected[i] 是预计排在这一行中第 i 位的学生的高度&#xff08;下标从 0 开始&#xff09;。 给你一个整数…

CV论文--2024.3.7

1、FAR: Flexible, Accurate and Robust 6DoF Relative Camera Pose Estimation 中文标题&#xff1a;FAR&#xff1a;灵活、准确和稳健的6DoF相机相对姿态估计 简介&#xff1a;在计算机视觉领域&#xff0c;估计图像之间的相对相机姿态一直是一个关键问题。通常&#xff0c;…

php导出excel文件

环境 php7.4hyperf3composer require phpoffice/phpspreadsheet代码 class IndexController extends AbstractController { /*** Inject* var Picture*/private $picture;public function index(){$res_data[]["robot" > 哈哈机器人,"order" > TES…

记录一下C++的学习之旅吧--C++基础

文章目录 前言using namespace std; 使用标准命名空间一、helloworld-输出表示1.1代码1.2 运行结果 二、变量2.1.1 普通变量代码2.1.2 运行结果2.2.1 常量和变量代码2.2.2 运行结果 三、sizeof---统计数据类型所占的内存大小3.1 代码3.2 运行结果 四、小数表示4.2 运行结果 总结…

02- 使用Docker安装RabbitMQ

使用Docker安装RabbitMQ 下载安装镜像 方式一: 启动docker服务,然后在线拉取 # 在线拉取镜像 docker pull rabbitmq:3-management# 使用docker images查看是否已经成功拉取方式二: 从本地加载 ,将RabbitMQ上传到虚拟机中后使用命令加载镜像即可 docker load -i mq.tar启动M…

mabatis 中

手动实现MaBatis底层机制 实现任务阶段一&#x1f34d;完成读取配置文件, 得到数据库连接&#x1f966;分析 代码实现&#x1f966;完成测试 实现任务阶段二&#x1f34d;编写执行器, 输入SQL语句, 完成操作&#x1f966;分析 代码实现&#x1f966;完成测试 实现任务阶段三&…

Redis缓存预热-缓存穿透-缓存雪崩-缓存击穿

什么叫缓存穿透? 模拟一个场景: 前端用户发送请求获取数据,后端首先会在缓存Redis中查询,如果能查到数据,则直接返回.如果缓存中查不到数据,则要去数据库查询,如果数据库有,将数据保存到Redis缓存中并且返回用户数据.如果数据库没有则返回null; 这个缓存穿透的问题就是这个…