[C++随想录] 继承

news2024/11/24 16:21:27

继承

  • 继承的引言
  • 基类和子类的赋值转换
  • 继承中的作用域
  • 派生类中的默认成员函数
  • 继承与友元
  • 继承与静态成员
  • 多继承的结构
  • 棱形继承的结构
  • 棱形虚拟继承的结构
  • 继承与组合

继承的引言

  1. 概念
    继承(inheritance)机制是面向对象程序设计使代码可以 复用的最重要的手段,它允许程序员在保
    持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
    程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
  2. 定义
class People
{
public:
	People(string name = "John", int age = 18)
	{
		_name = name;
		_age = age;
	}

	void print() 
	{
		cout << "姓名->" << _name << endl;
		cout << "年龄->" << _age << endl;

	}

protected:
	int _age ;
	string _name;
};

class Student : public People
{
public:
	Student(string name = "muyu", int age = 20, string id = "9210401227")
		:People(name, age)
	{
		_id = id;
	}

protected:
	string _id;
};

class Teacher : public People
{
public:
	Teacher()
	{
		People::_name = "mutong";
		People::_age = 22;
	}

protected:
	int _JobNumber;
};

int main()
{
	People p;
	p.print();
	cout << endl;

	Student s;
	s.print();
	cout << endl;

	Teacher t;
	t.print();

	return 0;
}

运行结果:

姓名->John
年龄->18

姓名->muyu
年龄->20

姓名->mutong
年龄->22
  1. 解释 class Student : public People

    People类是 父类/ 基类, Student类是 子类/ 派生类
    Student类继承People类的本质就是 复用Student 对象可以使用 People类里面的成员
    成员包括 成员变量 和 成员函数.
    成员变量是在对象空间内的, 而成员函数是不在对象空间内的, 属于整个类.
    成员的 访问限定符 有三种 public, protected, private
    继承方式不同 && 基类成员的访问限定符不同决定了基类的成员在派生类中的存在情况

  2. 继承的方式
    继承方式有三种: public, protected, private
    成员的 限定符 有三种 public, protected, private
    所以, 一共有 九种继承方式 👇👇👇

    1. 基类中的private成员, 在派生类中都是 不可见的
      • 不可见 和 private成员是不一样的, private成员是 类里面可以访问, 类外面不可访问, 不可见是 类里面看不见/ 不可访问, 类外面不可访问
    2. 其余继承方式, 派生类中的情况是 继承方式 和 类成员访问限定符中 权限小的那一个
      • 权限的大小: public > protected > private
    3. 父类如果是 class, 默认继承是 私有继承, 父类如果是 struct, 默认继承是 公有继承. 不过建议显示继承方式
    4. 常用的继承方式为 图中绿色的区域 ⇐ 继承的本质是 复用, 私有继承 和 基类中的私有成员在继承中是没有复用的意义的.
  3. 为什么 派生类没有 print函数 , 但能调用 print函数?
    我们可以认为 子类对象里面包含两个部分: 父类对象成员变量 + 子类本身成员变量

    子类对象中的 成员变量 = 自己本身的成员变量 + 父类的成员变量 (受访问限定符 和 继承方式共同限制)
    子类对象中的 成员函数 = 自己本身的成员函数 + 父类的成员函数 (受访问限定符 和 继承方式共同限制)
    print函数 是公有继承 && 访问限定符是公有 ⇒ 子类对象可以调用

  4. 为什么在 Teacher类中 可以People::_name = "mutong";
    我们已经知道了 派生类对象的基本结构了.
    那么派生类对象在 初始化阶段, 即调用默认构造 是先父类还是先子类呢?
    通过调试, 我们发现: 子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数
    Person类中有默认构造函数, 但是我们想改变一下 Teacher类对象中的 关于父类对象的那一部分, 那我们该怎么做呢?
    首先, 我们不能直接写

_name = "mutong";
_age = 22;

因为受 的影响, 域是编译器在编译阶段查找变量的规则.
虽然, 我们可以认为子类对象中有 父类对象成员 + 子类对象成员, 但彼此是 独立的.
调用默认构造函数还是去 Person类中去调用
编译器在 编译阶段默认查找的顺序是 局部域 , 子类域, 父类域, 全局域
我们在子类中去给父类对象成员赋值 ⇒ 我们应该告诉编译器, 这个变量直接去父类中去查找就OK
即, 这个时候我们要用 Person(父类)::

  1. 为什么在 Student类中 可以 :People(name, age)
    子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数.
    那么如果 父类对象没有默认构造函数呢?
    我们就需要 在子类的初始化列表处 显示调用父类的构造

基类和子类的赋值转换

int main()
{
	People p;
	Student st;

	st = p; // error
	p = st; // 可以进行转换

	return 0;
}
  • 父类对象 不能 赋值给子类对象, 而子类对象 可以 赋值给父类对象
    可以这样想: 子类对象的成员 > 父类对象的成员可以 变小一点, 但不能变大一点

父类对象 = 子类对象, 属于不同类型之间的赋值 ⇒ 一般都会发生 类型转换 ⇒ 类型转换, 那就意味着要产生 临时常量拷贝. 但结果真的如我们想的这般吗?

  • 验证 父类对象 = 子类对象 是否有临时常量拷贝
    拷贝是 常量的 ⇒ 要进行区分, 我们可以使用 引用 &
    如果生成了临时拷贝, 我们用普通引用 就会导致 权限的放大 , 就会报错
    如果没有生成临时拷贝, 我们用普通引用, 就是 权限的平移, 就不会报错
int main()
{
	// 类型转换
	int i = 0;
	double d = i;
	// double& dd = i // error
	const double& dd = i;

	// 赋值兼容转换
	Student st;
	People ps = st;
	People& p = st;

	return 0;
}

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

🗨️那么这个切片是怎样完成的呢?

继承中的作用域

🗨️在继承过程中, 可能会出现 父类中的成员名 和 子类中的成员名相同的情况, 那么派生类对象调用该成员会是怎样的情况呢?

  • 先看下面的代码:
class People
{
public:
	People(string name = "John", int age = 18)
	{
		_name = name;
		_age = age;
	}

	void print() 
	{
		cout << "class People" << endl;
	}

protected:
	int _age ;
	string _name;
};

class Student : public People
{
public:
	Student(string name = "muyu", int age = 20, string id = "9210401227")
		:People(name, age)
	{
		_id = id;
	}

	void print()
	{
		cout << "class Student : public People" << endl;
	}

protected:
	string _id;
};

void test1()
{
	Student st;

	st.print();
}

int main()
{
	test1();

	return 0;
}

运行结果:

class Student : public People

父类和子类中都有 print函数, 通过结果显示 派生类内部的print函数
这是因为 , 跟上面的People::_name = "muyu";是一样的道理
那么, 如果我们非要通过派生类对象 调用基类中的print函数呢?👇👇👇

void test1()
{
	Student st;
	
	st.People::print();
}

总结:

  1. 子类和父类中的成员尽量不同名!
  2. 上面的例子, 子类和父类有同名的成员, 子类隐藏父类的成员, 这种关系叫做 隐藏/ 重定义
    • 注意: 隐藏 != 重载
      重载的前提条件是 同一作用域, 而隐藏是 父类和子类成员同名
    • 隐藏 != 重写
      隐藏是 子类中同名成员隐藏父类中同名成员, 而重写是 子类中重写父类有关函数的实现

派生类中的默认成员函数

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

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
    保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
    解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
    virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
class Person
{
public:

	Person(string name = "muyu", int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& tem)
	{
		_name = tem._name;
		_age = tem._age;

		cout << "Person(const Person& tem)" << endl;
	}

	Person& operator=(const Person& tem)
	{
		_name = tem._name;
		_age = tem._age;

		return *this;

		cout << "Person& operator=(Person& tem)" << endl;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student(const string name,const int age, const int num)
		: Person(name,age)
		, _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)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);
			_num = s._num;
		}
		return *this;
	}

	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};

void test2()
{
	Student st1("牧童", 20, 20230101);
	Student st2(st1);

	Student st3("沐雨", 18, 20230102);
	st3 = st1;

}

int main()
{
	// test1();

	test2();

	return 0;
}

运行结果:

Person()
Student()
Person(const Person& tem)
Student(const Student& s)
Person()
Student()
Student& operator= (const Student& s)
~Student()
~Person()
~Student()
~Person()
~Student()
~Person()

🗨️其他函数都是 先父类, 后子类, 唯独 析构函数 先子类后父类?

  • 首先, 构造函数是 先父类, 后子类
    , 先进后出 ⇒ 析构的时候, 先子类, 后父类.
    其次, 父类可以调用子类的成员, 而子类不能调用父类的成员
    如果先析构父类, 如果子类对象还想调用父类的成员,那就完蛋了!

🗨️在子类的析构函数中, 调用父类的析构函数

  • 首先,
	~Student()
	{
		~Person(); // 提示有一个重载

		cout << "~Student()" << endl;
	}

纳闷? 这个还能有重载?
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor().
那么子类和父类中的 析构函数名 都是 destruction ⇒ 那么就构成了隐藏关系
那么我们在子类中调用父类的析构函数应该如下:

	~Student()
	{
		Person::~Person();

		cout << "~Student()" << endl;
	}

结果如下:

  • 编译器默认帮我们 先调用了父类的析构函数不信任我们用户, 由编译器自己完成

继承与友元

基类的友元, 派生类不会继承, 即基类的友元不能访问 子类中的 私有和保护成员

// 类的声明
class Student;

class Person
{
public:

	friend void Display(const Person& p, const Student& s);

protected:
	string _name; // 姓名

};

class Student : public Person
{
public:
	// friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号

};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl; // error: “Student::_stuNum”: 无法访问 protected 成员(在“Student”类中声明)
}

void test3()
{
	Person p;
	Student s;
	Display(p, s);
}

int main()
{
	test3();

	return 0;
}

解决方法就是: 让 Display函数也充当 子类的友元👇👇👇

class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号

};

继承与静态成员

基类中定义了一个静态成员, 则在整个继承体系中, 仅此一份. 子类不会单独拷贝一份, 继承的是使用权

🗨️只创建子类对象, 问一共创建了多少个子类对象?

  • 1. 可以在子类的默认构造中创建一个静态成员变量.
class A
{
public:
	A(){}
};

class B :public A
{
public:
	B()
	{
		_count++;
	}
public:
	static int _count;
};

int B::_count = 0;

void test4()
{
	B b1;
	B b2;
	B b3;
	B b4;
	B b5;
	B b6;


	cout << "子类中的个数->" << B::_count << endl;

}


int main()
{
	test4();

	return 0;
}

运行结果:

子类中的个数->6
  1. 可以在父类的默认构造中创建一个静态成员变量.
class A
{
public:
	A()
	{
		++_count;
	}
public:
	static int _count;
};

int A::_count = 0;

class B :public A
{

};

void test4()
{
	B b1;
	B b2;
	B b3;
	B b4;
	B b5;
	B b6;


	cout << "子类的个数->" << A::_count << endl;

}


int main()
{
	test4();

	return 0;
}

运行结果:

子类中的个数->6

多继承的结构

一个子类只有一个直接父类, 叫做 单继承

一个子类有两个及以上的父类, 叫做 多继承

  1. 单继承的结构

    其实在 内存中不是这样 "点开" 的关系, 而是连续的空间

  2. 多继承的机构

    当然, 也是连续的空间

棱形继承的结构

多继承会有一种情况是 棱形继承

D继承B和C, B和C又同时继承A ⇒ 就会导致D对象中有两个A对象成员
在这里插入图片描述

这样就会导致 冗余性和二义性

其实, 解决 访问不明确/ 二义性 可以使用 基类::

但是 内存中D还是存储了两份 A类对象 造成的数据冗余性问题还没解决呢?

棱形虚拟继承的结构

棱形虚拟继承解决的就是 数据冗余性 和 二义性的问题


通过 内存窗口, 我们发现:

  1. 把A从B 和 C中抽出来了, 让A既不属于B, 也不属于C
  2. B和C类中多了一个位置出来
  • B和C类中多了一个位置的用处是什么?

    我们发现: 地址指向的空间第一个位置是 0, 第二个位置分别是 20(十六进制转二进制) 和 12(十六进制转二进制)

    虽然, 把A类单独放在一个空间, 但 A类中的成员还是B和C类得一部分 =>
    这里是通过了B和C的两个指针,指向的一张表。这两个指针叫 虚基表指针,这两个表叫 虚基表。虚基表中存的 偏移量 。通过偏移量可以找到下面的A。

那么这个时候, 我们修改A类的对象, 就不会有 冗余性和二义性的问题了👇👇👇

继承与组合

继承是一种 is-a的关系, 是一种 白箱复用, 子类跟父类之间的 耦合度高
对象组合是一种 has-a的关系, 是一种 黑箱复用, 耦合度低

  • is-a 和 has-a
    is-a,就表示 子类是一个特殊的父类
    has-a, 就表示 A对象中有B对象

  • 白箱复用 和 黑箱复用
    白箱复用, 透明的, 即 子类知道父类内部的细节, 方法的实现
    黑箱复用, 不透明的, 即 对象之间不知道彼此的内部的细节

  • 耦合度
    打个比方:
    父类A中的成员 有20个是public, 80个是protected的; 派生类是public继承
    那么在派生类B中, A的成员都是可见的 ⇒ 耦合度是 100%
    同样的,
    A对象中的成员, 有20个是public, 80个是protected的;
    那么B对象想用A对象里面的成员, 只能使用 20个public的成员 ⇒ 耦合度是 20%

🗨️那对象组合这么好, 我们就用对象组合, 不用继承了是吧?

  • 首先, 存在即合理 ⇒ 全部都这样, 或者全部都那样的想法就是错误的
    1. 合理使用: 符合is-a 关系的就使用 继承; 符合 has-a关系就使用 对象组合; 如果 既符合has-a, 又符合 is-a关系使用 对象组合
    2. 实现 多态 , 必须使用继承

学后反思:

  1. 什么是菱形继承?菱形继承的问题是什么?
  2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
  3. 继承和组合的区别?什么时候用继承?什么时候用组合?

与朋友论学,须委曲谦下,宽以居之。 — — 王阳明
译文:与朋友谈论学问,必须婉转曲从谦虚下问,与之宽和相处

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

反弹Shell方法论

反弹Shell方法论 1.bash反弹shell2.Python 脚本反弹shell3.php反弹shell4.Java反弹shell5.perl 反弹shell6.Ruby脚本反弹shell7.利用nc反弹shell8.powershell反弹shell9.Socat反弹shell10.使用OpenSSL反弹加密shell11.反弹shell的本质 反弹shell命令查询 如果可以&#xff0c;尽…

2023年中国玉米淀粉糖市场现状及行业需求前景分析[图]

玉米淀粉糖是一种优良的可生物降解的天然高分子材料&#xff0c;也是近年来发展最快的淀粉深加工产品&#xff0c;淀粉糖是利用含淀粉质的农产品为原料&#xff0c;经过酸法、酸酶法或者双酶法水解制取的糖的统称&#xff0c;玉米淀粉糖的产品形式多种多样&#xff0c;目前&…

智能警用装备管理系统-科技赋能警务

警用物资装备管理系统&#xff08;智装备DW-S304&#xff09;是依托互云计算、大数据、RFID技术、数据库技术、AI、视频分析技术对警用装备进行统一管理、分析的信息化、智能化、规范化的系统。 &#xff08;1&#xff09;感知智能化 装备感知是整个方案的基础&#xff0c;本方…

python基于django的留学生服务管理平台

留学服务管理平台的用户是系统最根本使用者&#xff0c;按需要分析系统包括三类用户&#xff1a;学生、教师、管理员。这三类用户对系统的需求简要如下。技术栈 后端&#xff1a;pythondjango 前端&#xff1a;vueCSSJavaScriptjQueryelementui 开发语言&#xff1a;Python 框架…

向量数据库与传统数据库向量检索插件的区别?

向量数据库与传统数据库向量检索插件的区别 越来越多的传统关系型数据库和检索系统(如 Clickhouse、Elasticsearch等)开始提供内置的向量检索插件。 例如,Elasticsearch 8.0 支持通过 Restful API 来插入向量和开展 ANN 检索。但是,向量检索插件的问题显而易见——无法提供…

资源共享共赢系统应用

1.访问地址 http://www.gxcode.top/code 2.收益功能说明 上传共享收益资源信息&#xff0c;审核通过后获取收益。 3.具体操作如下图

更健康的不粘锅,用料扎实疏油疏水,帝伯朗零氟系列氧吧锅上手

做饭时&#xff0c;平底锅、汤锅、炒锅等锅具都经常用到&#xff0c;为了方便清理&#xff0c;很多人会选择不粘锅。不过在市面上的不粘锅质量良莠不齐&#xff0c;实际做饭的时候&#xff0c;因为要长时间高温使用&#xff0c;所以劣质的不粘锅很容易析出重金属、特氟龙等有害…

量子风险现在是真实存在的:如何应对不断变化的数据收集威胁

在数据安全至关重要的时代&#xff0c;清楚地提醒人们不断变化的威胁形势。黑客正在渗透路由器以及联网设备&#xff0c;以获得对国家网络的不可检测的后门访问权限。 这一事件凸显了数字基础设施中的漏洞&#xff0c;特别是数据在未知且通常是敌对的网络上传输时所面临的风险…

Vue鼠标右键画矩形和Ctrl按键多选组件

效果图 说明 下面会贴出组件代码以及一个Demo&#xff0c;上面的效果图即为Demo的效果&#xff0c;建议直接将两份代码拷贝到自己的开发环境直接运行调试。 组件代码 <template><!-- 鼠标画矩形选择对象 --><div class"objects" ref"objectsR…

【AI视野·今日NLP 自然语言处理论文速览 第五十二期】Wed, 11 Oct 2023

AI视野今日CS.NLP 自然语言处理论文速览 Wed, 11 Oct 2023 Totally 81 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers LongLLMLingua: Accelerating and Enhancing LLMs in Long Context Scenarios via Prompt Compression Author…

Web3 招聘 | Bitget、MyShell、imToken、Arweave 多项目招聘中

「Web3 招聘」是 TinTinLand 为 Web3 项目和求职者创建的一个招聘信息汇集专栏。本栏目将持续更新区块链行业招聘信息&#xff0c;满足不同求职者与项目方的多样需求。欢迎各项目方联系 TinTinLand 发布职位需求&#xff0c;欢迎求职者关注 TinTinLand 获取最新招聘信息。 此外…

python(自5)scrapy下载安装 基本使用

一&#xff0c;安装下载 (1)安装步骤 ​ //安装包下载&#xff1a;Archived: Python Extension Packages for Windows - Christoph Gohlke (uci.edu) ​// 先下载对应的 twisted 然后 pip install 拖进twisted//例如&#xff1a; twisted_iocpsupport‑1.0.2‑cp311‑cp31…

人机交互中的信息数量与信息质量

在人机交互中&#xff0c;信息数量和信息质量是影响人机交互效果的两个重要因素。信息数量指的是系统向用户提供的信息总量&#xff0c;包括输入信息、反馈信息、展示信息、错误信息等&#xff0c;在合适的情况下越少越好&#xff1b;信息质量则是指信息的准确性、有效性、清晰…

十九、【减淡工具组】

文章目录 减淡工具加深工具海绵工具 减淡工具 减淡工具的作用就是把画笔涂抹过后的区域的颜色减淡&#xff0c;让这部分区域的颜色看起来更加白更加亮&#xff1a; 也可以采用新建空白图层&#xff0c;然后采用画笔工具&#xff0c;用涂抹中性灰的方式让其变亮&#xff0c;采…

将字符串转换为小写形式字符串.casefold()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 将字符串转换为小写形式 字符串.casefold() 请问题目中的代码输出什么&#xff1f; s "Hello World" print("【显示】s ",s) print("【执行】s.casefold()"…

Python学习-----Day06——排序

冒泡排序 冒泡排序&#xff08;Bubble Sort&#xff09;也是一种简单直观的排序算法。它重复地走访过要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换&#xff0c;也就是说该数列已经…

C# InformativeDrawings 生成素描画

效果 项目 下载 可执行程序exe下载 源码下载

【Python-Django】基于TF-IDF算法的医疗推荐系统复现过程

复现步骤 step1&#xff1a; 修改原templates路径&#xff0c;删除&#xff0c;将setting.py中的路径置空 step2&#xff1a; 注册app python manage.py startapp [app名称]在app目录下创建static和templates目录 step3&#xff1a; 将项目中的资源文化进行拷贝 step4&#…

7 使用Docker容器管理的tomcat容器中的项目连接mysql数据库

1、查看容器的IP 1&#xff09;进入容器 docker exec -it mysql-test /bin/bash 2&#xff09;显示hosts文件内容 cat /etc/hosts 这里容器的ip为172.17.0.2 除了上面的方法外&#xff0c;也可以在容器外使用docker inspect查看容器的IP docker inspect mysql-test 以下为…

python openai宠物名字生成器

文章目录 OpenAICompletion宠物名字生成器提示词工程 prompt enginering 构建应用程序 OpenAI OpenAI 已经训练了非常擅长理解和生成文本的领先的语言模型。我们的 API 提供对这些模型的访问&#xff0c;可用于处理几乎任何涉及”语言处理“的任务。 Completion 补全&#x…