C++ 学习笔记(十)(继承、抽象篇)

前言:主要是自己学习过程的积累笔记,所以跳跃性比较强,建议先自学后拿来作为复习用。

文章目录

  • 1 定义父类和子类
    • 1.1 定义父类
      • 访问说明符 protected
    • 1.2 定义子类
    • 1.3 子类向父类的转换
    • 1.4 转换的例外
    • 1.5 子类的构造函数
    • 1.6 静态成员不能继承
    • 1.7 防止继承的发生
  • 2 虚函数
    • 2.1 静态类型和动态类型
    • 2.2 调用虚函数
    • 2.3 子类中的虚函数
      • 2.3.1 返回类型不一致
      • 2.3.2 形参列表不一致
        • override 和 final 关键字
      • 2.3.3 回避动态绑定
  • 3 抽象基类
    • 重构
  • 4 访问控制与继承

1 定义父类和子类

面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类的区别,而以统一的方式使用它们。

建议读者在学习这一内容之前先将 C++ 类的知识理解透彻,具体可参考:C++ 学习笔记(八)(类篇一);如果想知道更多关于 C++ 类的知识,可以参考:C++ 学习笔记(九)(类篇二)。这两篇文章总结的非常详细。

1.1 定义父类

如果我们希望让一个类拥有另一个类的成员变量和成员函数,同时又能定义它自己的数据成员,就可以用到类的继承。通过继承联系在一起的类构成了一种层次关系。在层次关系根部的被称为基类,继承基类的类被称为派生类。当然,我们更多的会使用父类与子类来描述类之间的继承关系。

定义一个父类来表示某一书籍的销售情况:

读者可以把每一处的代码复制到一个文本中,就不需要重复的往前看了。

// Root 类表示所有书籍的销售情况
class Root
{
public:
	// 该语句使编译器为 Root 类生成一个默认构造函数
	Root() = default;
	// 该函数是 Root 类的构造函数
	Root(const string &book, double sales_price) : 
		bookNo(book), price(sales_price) {}
	// 该函数返回书籍的编号
	string get_number() { return bookNo; }
	// 该函数计算并返回该书籍的销售额
	virtual double net_price(int n) const { return n * price; }
	// 析构函数
	virtual ~Root() = default;
private:
	string bookNo;		// book number,书籍的编号
protected:
	double price = 0.0;	// 不打折时书本的售价
};	// 分号不要忘记

子类不仅可以继承父类的成员函数,还能对其进行修改。前提是在父类中,该函数用 virtual 关键字修饰过,这样的成员函数被称为虚函数。任何除了构造函数之外非静态函数都可以定义成虚函数。virtual 关键字只能出现在类的内部。C++ 把子类重写父类的虚函数这一操作称为覆盖(override)

访问说明符 protected

尽管子类可以继承父类的数据成员,但是默认情况下,子类只能访问父类中的公有成员,不能访问私有成员。如果父类希望它的子类有权访问某些成员,但又不想让其他的类访问该成员,就可以用受保护的(protected)访问说明符说明这些成员。

1.2 定义子类

子类必须通过类派生列表来说明它继承了哪个父类。类派生列表的形式是:class 子类名 : 访问说明符 父类名。不同的父类之间用逗号隔开。子类必须重新声明一遍父类的虚函数

定义一个子类来表示某一书籍的打折情况:

// 利用类派生列表说明 Son 类继承自 Root 类
// Son 类表示某一类书籍的销售情况
class Son : public Root
{
public:
	Son() = default;
	Son(const string&, double, int, double);
	// 该函数覆盖了父类的虚函数,用以实现打折后的销售额
	double net_price(int) const override;
private:
	// 子类也可以定义自己的成员变量
	int amount = 0;			// 当购买数量达到 amount 时予以折扣优惠
	double discount = 0.0;	// 折扣值
};

子类 Son 从父类那里继承了 get_number 函数,以及 bookNo 和 price 等成员变量。此外它也定义了自己的 net_price 函数,以及 amount 和 discount 成员变量。

子类可以选择不覆盖父类的虚函数,这样的话它会直接继承父类的虚函数版本。子类可以在覆盖虚函数时也使用 virtual 关键字,但更常用的做法,也是 C++ 新标准下的做法是:在形参列表之后、或者常量成员函数的 const 关键字之后(比如 Son 类中的做法)、或者在引用成员函数的引用限定符之后添加关键字 override

1.3 子类向父类的转换

一个 Son 类对象包含以下两个部分:
在这里插入图片描述
但是要清楚一点,继承自父类的部分和子类自定义的部分,在内存中不一定是连续存储的一个子类可以继承多个父类,其对象也就包含多个组成部分,这些部分也不一定是连续存储的。同时,类也可以多层次继承,比如 A 继承 B,B 又继承 C,C 又继承 D。

因为在子类中有父类的组成部分,所以可以把子类的对象当做父类的对象来使用。即可以让父类的指针指向子类对象,或者令父类的引用绑定到子类对象上

Root root_obj;			// Root 对象
Son son_obj;			// Son 对象
Root *p = &root_obj;	// p 指向 Root 对象
p = &son_obj;			// p 指向 Son 对象
Root &r = son_obj;		// r 绑定到 Son 对象的 Root 部分

这种转换通常称为子类到父类的类型转换,编译器会隐式地执行该转换。还有一种情况,当一个函数的形参是 Root 类的对象时,我们也能向其传递 Son 类的实参对象。

1.4 转换的例外

子类向父类的自动类型转换只对指针或者引用有效,在子类类型和父类类型之间则不存在这样的转换。举个栗子,如果有 Root 类对象 A,Son 类对象 B,那么执行 A = B 时,并不会把 B 强制转换为 Root 类型。这么做,只是将 B 中含有父类的部分拷贝给 了 A,而 B 中属于子类的部分则被切掉了

要理解在具有继承关系的类之间发生的类型转换,有四点非常重要:

  • 从子类向父类的类型转换只对指针或引用类型有效
  • 父类向子类不存在隐式地类型转换
  • 子类向父类的类型转换也可能会由于访问受限而变得不可行。
  • 将一个子类对象拷贝、移动或赋值给一个父类对象,只操作子类对象中的父类部分

1.5 子类的构造函数

尽管子类对象含有从父类继承而来的成员变量,但子类不能直接初始化它们,而必须使用父类的构造函数来初始化父类的部分。换句话说,每个类只负责它自己成员的初始化。定义 Son 类的构造函数如下:

Son(const string &book, double p, int amt, double disc) :
	Root(book, p), amount(amt), discount(disc) {}

该构造函数将其前两个参数 book 和 p 传递给 Root 的构造函数,而不是由它自己来执行初始化。如果我们不显示地指出父类成员的初始化方式,则它们会以父类的默认构造函数来执行默认初始化。

总之一点就是:每个类负责定义自己的接口。与类的对象交互必须通过该类的接口,即使是子类,也只能通过接口访问其父类的成员。这有助于我们更好地管理自己的代码,也能使其更加安全。

1.6 静态成员不能继承

如果父类定义了一个静态成员,则在整个继承体系中都只存在该成员的唯一定义。可以通过四种方式访问静态成员:

// 定义一个 Base 类
class Base
{
public:
	static void function();	// Base 类的静态成员
};

// 定义一个继承自 Base 的子类
class Derived : public Base
{
	void f(const Derived&);	// 子类中的一个成员函数
};

// 在子类外部定义 f 函数,接受一个 Derived 对象的引用作为实参
void Derived::f(const Derived& derived_obj)
{
	Base::function();			// 通过父类直接访问函数
	Derived::function();		// 通过子类直接访问函数
	derived_obj.function();		// 通过子类对象访问函数
	function();					// 通过 this 对象访问,相当于 this->function();
}

1.7 防止继承的发生

如果不希望某一个类被继承,可以在类名后加上 final 关键字:

class Base final { /* */ };	// Base 类不能作为父类

2 虚函数

2.1 静态类型和动态类型

静态类型指变量声明时的类型或者表达式生成的类型,它在程序编译的时候就确定了;动态类型是变量或表达式表示的内存中的对象的类型。比如 1.3 代码中的指针 p,它的静态类型是 Root,但后面绑定到了一个 Son 类的对象上,所以它的动态类型是 Son。

由此也可知道,如果表达式不是父类的指针或引用,那么它的静态类型和动态类型是永远保持一致的。

注意将静态类型和静态成员区分开来。前者指变量的类型,后者指变量的属性。

2.2 调用虚函数

通常情况下,如果我们不打算使用某个函数,就无须为该函数提供定义。但虚函数不管是否被用到都必须定义。当我们使用父类的引用或指针调用一个成员函数时会执行动态绑定,动态绑定的意思就是编译器会根据传入的实参类型,来决定使用哪个版本的虚函数,因此动态绑定也叫运行时绑定

由于上述原因,当某个虚函数通过指针或引用被调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本(先编译,后运行)。此处定义一个函数,当购买的书籍和数量都确定时,它负责打印销售额:

// 计算并打印售出指定数量的书籍所得到的销售额
double print_total(ostream &os, const Root &item, int n)
{
	// 根据传入 item 形参的对象的类型调用 Root::net_price 或者 Son::net_price
	double result = item.net_price(n);
	os << "ISBN: " << item.isbn()	// 调用 Root::isbn
		<< "sold: " << n << "total due: " << result << endl;
	return result;
}

该函数接受一个输出流和一个父类对象,以及该对象(书籍)的销量,在函数中通过对象调用 net_price 函数来计算销售额。此时我们定义父类和子类的对象来调用它:

// 调用父类的构造函数创建对象
Root base_obj("I am father.", 10);
// print_total 函数接受父类对象实参,调用的是父类虚函数,即 Root::net_price
print_total(cout, base_obj, 100);

// 调用子类的构造函数创建对象
Son derived_obj("I am son.", 10, 10, 0.2);
// print_total 函数接受子类对象实参,调用的是子类虚函数,即 Son::net_price
print_total(cout, derived_obj, 100);

以上便是动态绑定的的过程。必须要搞清楚,动态绑定只有当我们通过指针或引用调用虚函数时才会发生,也只有在这种情况下对象的动态类型与静态类型才有可能不一致。当我们通过一个非指针非引用的表达式调用虚函数时,程序在编译时就会确定好调用的版本。这也是 C++ 多态性的一个体现:具有继承关系的多个类型称为多态类型,我们能使用这些类型的“多种形式”而无须在意它们的差异。

2.3 子类中的虚函数

若子类想重写父类的虚函数,那么函数的返回类型、名字、参数列表全部都得和父类一样,仅仅是函数的具体实现不同(注意,这不同于函数的重载)。

2.3.1 返回类型不一致

如果返回类型不一样,编译就会出错。但是此处有一个例外,当返回类型是类本身的指针或引用时,父类子类虚函数的返回类型可以不一致。比如子类 A 继承自(派生自)父类 B,则类 A 的虚函数返回 A* 的同时类 B 的虚函数可以返回 B*,前提是从 A 到 B 的类型转换是可访问的。在后面会解释如何确定一个父类的可访问性。

2.3.2 形参列表不一致

override 和 final 关键字

如果参数列表不一致,编译器会认为子类新定义的这个函数和父类中的虚函数是相互独立的。从编程的角度而言,这意味着发生了错误,但实际编译时是可以编译通过的。要想调试发现这种错误很难,解决的办法就是,在重写的虚函数后加上 override 关键字,来告诉编译器该函数对父类中的虚函数进行覆盖。此时如果形参列表不一致就会报错;如果在父类没有 override 标记的虚函数,编译器也会报错。

同时,如果我们不希望某个函数被覆盖,可以在其后加上 final 关键字。之后任何尝试覆盖该函数的操作都将引发错误:

struct A
{
	virtual void f1(int) const;
	virtual void f2();
	void f3();
	void f5() final;
};

struct B : A
{
	void f1(int) const override;	// 正确:f1 与父类 A 中的 f1 匹配
	void f2(int) override;			// 错误:父类 A 没有 f2(int) 函数
	void f3() override;				// 错误:f3 不是虚函数
	void f4() override;				// 错误:父类 A 没有 f4 函数
	void f5() override;				// 错误:父类 A 已将 f5 声明成 final
};

2.3.3 回避动态绑定

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫执行虚函数的某个版本。比如当一个子类的虚函数需要调用父类的虚函数版本时,就可以通过使用作用域运算符来实现:

double result = object->Root::net_price(10);

该代码会强行调用 Root 类的 net_price 函数,而不管 object 实际指向的是 Root 对象还是 Son 对象。该调用将在编译时完成解析。通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。

3 抽象基类

在 1.2 中定义子类时,我们为子类添加了成员变量,一个是 amount,另一个是 discount。代表当购买数量达到一个值时给予折扣的优惠。如果我们希望将折扣策略改成:购买数量未达到一个值时才享受折扣的优惠。这两个策略都要求一个购买量的值 amount 和一个折扣值 discount。因此我们可以将它们两个抽象出来,放到一个类 Disc 中。来令其他类继承自 Disc 类,这样每个子类都能定义自己的 net_price 函数,来制定自己的折扣策略。

但是此处就出现一个问题: Disc 的作用仅仅是提供两个成员变量给子类,并不代表任一类书籍,也不实现特定的折扣策略,更不定义自己的 net_price 函数。为了防止出现不知名的意外,我们可以在 Disc 中将 net_price 函数定义成纯虚函数。纯虚函数无须定义,只需要在虚函数的函数体前加上一个“ = 0 ”即可,该标记只能出现在类的内部虚函数的声明处

// 用于保存折扣值和购买量的类,子类使用这些数据以实现不同的折扣策略
// Disc 类继承自 Root 类,所以有 net_price 函数
class Disc :: public Root
{
public:
	Disc() = default;
	Disc(const string &book, double price, int amt, double disc) : 
		Root(book, p), amount(amt), double disc) {}
	double net_price(int) const = 0;	// 加上 = 0 声明成纯虚函数
protected:
	// 将 Son 类的购买值和折扣值移到 Disc 类中
	int amount = 0;			// 购买值
	double discount = 0.0;	// 折扣值
};

含有纯虚函数的类被称为抽象基类我们不能定义这种类的对象,按理来说是用不上构造韩色儿,但 Disc 类仍然定义了一个默认构造函数和一个接受四个参数的构造函数。这是因为 Disc 类的子类的构造函数会用到 Disc 类的构造函数,来初始化它们的属于 Disc 类的部分。这里要补充一点,尽管没有意义,但我们仍然可以定义纯虚函数,只不过在类内只能声明纯虚函数,定义必须写到类外。

抽象基类只负责定义接口,后续的其他类可以覆盖这个接口。我们不能直接创建一个抽象基类的对象,但是可以创建抽象基类子类的对象,前提是这些子类覆盖了抽象基类的纯虚函数。

既然我们将购买值和折扣值移到了 Disc 类中, 就可以重写 Son 类了。这一次让它继承 Disc 而非 Root:

// Son 类表示某一类书籍的销售情况
class Son : public Disc
{
public:
	Son() = default;
	// Son 的构造函数会调用 Disc 的构造函数进行初始化
	Son(const string &book, double price, int amt, double disc) :
		Disc(book, price, amt, disc) {}
	// 该函数覆盖了父类的虚函数,用以实现打折后的销售额
	double net_price(int) const override;
};

这个时候 Son 的直接基类是 Disc,间接基类是 Root。每个 Son 对象包含三个子对象:一个空的 Son 部分,一个 Disc 子对象和一个 Root 子对象。在 C++ 中,每个类控制其对象的初始化过程。因此,即使 Son 类没有自己的数据成员,它也仍然需要提供一个接受四个参数的构造函数。该构造函数将它的实参传递给 Disc 的构造函数,随后 Disc 的构造函数继续调用 Root 的构造函数。Root 的构造函数首先初始化 bookNo 和 price 成员,够早结束后,运行 Disc 的构造函数并初始化 amount 和 discount 成员,最后运行 Son 的构造函数。

重构

在 Root 的继承体系中增加 Disc 类是重构的一个典型示例。重构负责重新设计类的体系以便将操作或数据从一个类移动到另一个类。对于面向对象的应用程序来说,重构是一种很普遍的现象。不过即使我们改变了整个继承体系,那些使用了 Son 或 Root 的代码也无须进行任何改动,不过一旦类被重构(或以其他方式被改变),就意味着我们必须重新编译含有这些类的代码了。

4 访问控制与继承

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

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

相关文章

clip精读

开头部分 1. 要点一 从文章题目来看-目的是&#xff1a;使用文本监督得到一个可以迁移的 视觉系统 2.要点二 之前是 fix-ed 的class 有诸多局限性&#xff0c;所以现在用大量不是精细标注的数据来学将更好&#xff0c;利用的语言多样性。——这个方法在 nlp其实广泛的存在&…

2023年ACM竞赛班 2023.3.20题解

目录 瞎编乱造第一题 瞎编乱造第二题 瞎编乱造第三题 瞎编乱造第四题 瞎编乱造第五题 不是很想编了但还是得编的第六题 不是很想编了但还是得编的第七题 还差三道题就编完了的第八题 还差两道题就编完了的第九题 太好啦终于编完了 为啥一周六天早八阿 瞎编乱造第一题…

【Matlab算法】粒子群算法求解一维线性函数问题(附MATLAB代码)

MATLAB求解一维线性函数问题前言正文函数实现可视化处理可视化结果前言 一维线性函数&#xff0c;也称为一次函数&#xff0c;是指只有一个自变量xxx的函数&#xff0c;且函数表达式可以写成yaxbyaxbyaxb的形式&#xff0c;其中aaa和bbb是常数。具体来说&#xff0c;aaa称为斜…

typedef uint8_t u8;(stm32数据类型)

在stm32单片机的库文件里有这么一段u8和u16的定义 typedef uint8_t u8; typedef uint16_t u16&#xff1b; 而uint8_t和uint16_t的定义是这样的 typedef unsigned char uint8_t; typedef unsigned short int uint16_t; 意味着u8就是就是指代的unsigned char …

linux简单入门

目录Linux简介Linux目录结构Linux文件命令文件处理命令文件查看命令常用文件查看命令Linux的用户和组介绍Linux权限管理Linux简介 Linux&#xff0c;全称GNU/Linux&#xff0c;是一种免费使用和自由传播的类UNIX操作系统&#xff0c;其内核由林纳斯本纳第克特托瓦兹&#xff0…

【Nginx二】——Nginx常用命令 配置文件

Nginx常用命令 配置文件常用命令启动和重启 Nginx配置文件maineventshttp常用命令 安装完成nginx后&#xff0c;输入 nginx -&#xff1f;查询nginx命令行参数 nginx version: nginx/1.22.1 Usage: nginx [-?hvVtTq] [-s signal] [-p prefix][-e filename] [-c filename] [-…

[数据结构]直接插入排序、希尔排序

文章目录排序的概念和运用排序的概念排序运用常见的排序算法常见的排序算法直接插入排序希尔排序性能对比排序的概念和运用 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操…

FastApi快速构建一个web项目

FastApi快速构建一个web项目 已经使用FastApi很久了。这个一个非常优秀的框架。和flask一样能够快速构建一个web服务。开发效率非常之高。今天我一个Demo来介绍一下这个框架的使用。供大家学习参考。 项目介绍 本项目主要介绍fastapi快速编写web服务&#xff0c;通过案例分别…

贪心算法(一)

一、概念 贪心算法的核心思想是&#xff0c;在处理一个大问题时&#xff0c;划分为多个局部并在每个局部选择最优解&#xff0c;并且认为在每个局部选择最优解&#xff0c;那么最后全局的问题得到的就是最优解。 贪心算法可以解决一些问题&#xff0c;但是不适用于所有问题&a…

音乐制作:Ableton Live 11 Suite Mac

Ableton Live 11 Suite Mac是一款非常专业的音乐制作软件&#xff0c;Live 是用于音乐创作和表演的快速、流畅和灵活的软件。它带有效果、乐器、声音和各种创意功能;制作任何类型的音乐所需的一切。以传统的线性排列方式进行创作&#xff0c;或者在 Live 的 Session 视图中不受…

MyBatisPlus的Wrapper使用示例

一、wapper介绍 1、Wrapper家族 在MP中我们可以使用通用Mapper&#xff08;BaseMapper&#xff09;实现基本查询&#xff0c;也可以使用自定义Mapper&#xff08;自定义XML&#xff09;来实现更高级的查询。当然你也可以结合条件构造器来方便的实现更多的高级查询。 Wrappe…

【Spring6】| Spring IoC注解式开发

目录 一&#xff1a;Spring IoC注解式开发 1. 回顾注解 2. 声明Bean的四个注解 3. Spring注解的使用 4. 选择性实例化Bean 5. 负责注入的注解&#xff08;重点&#xff09; 5.1 Value 5.2 Autowired与Qualifier 5.3 Resource 6. 全注解式开发 一&#xff1a;Spring I…

Springboot+vue开发的图书借阅管理系统项目源码下载-P0029

前言图书借阅管理系统项目是基于SpringBootVue技术开发而来&#xff0c;功能相对比较简单&#xff0c;分为两个角色即管理员和学生用户&#xff0c;核心业务功能就是图书的发布、借阅与归还&#xff0c;相比于一些复杂的系统&#xff0c;该项目具备简单易入手&#xff0c;便于二…

基于深度学习的车型识别系统(Python+清新界面+数据集)

摘要&#xff1a;基于深度学习的车型识别系统用于识别不同类型的车辆&#xff0c;应用YOLO V5算法根据不同尺寸大小区分和检测车辆&#xff0c;并统计各类型数量以辅助智能交通管理。本文详细介绍车型识别系统&#xff0c;在介绍算法原理的同时&#xff0c;给出Python的实现代码…

你掌握了吗?在PCB设计中,又快又准地放置元件

在印刷电路板设计中&#xff0c;设置电路板轮廓后&#xff0c;将零件(占地面积)调用到工作区。然后将零件重新放置到正确的位置&#xff0c;并在完成后进行接线。 组件放置是这项工作的第一步&#xff0c;对于之后的平滑布线工作是非常重要的工作。如果在接线工作期间模块不足…

MagicalCoder可视化开发平台:轻松搭建业务系统,为企业创造更多价值

让软件应用开发变得轻松起来&#xff0c;一起探索MagicalCoder可视化开发工具的魔力&#xff01;你是否为编程世界的各种挑战感到头痛&#xff1f;想要以更高效、简单的方式开发出专业级的项目&#xff1f;MagicalCoder低代码工具正是你苦心寻找的产品&#xff01;它是一款专为…

什么是Nginx

一.什么是nginxNginx (engine x) 是一个高性能的HTTP和反向代理web服务器&#xff0c;是一款由俄罗斯的程序设计师Igor Sysoev使用c语言开发的轻量级的Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器&#xff0c;官方测试nginx能够支支撑5万…

蓝桥杯冲刺 - week1

文章目录&#x1f4ac;前言&#x1f332;day192. 递归实现指数型枚举843. n-皇后问题&#x1f332;day2日志统计1209. 带分数&#x1f332;day3844. 走迷宫1101. 献给阿尔吉侬的花束&#x1f332;day41113. 红与黑&#x1f332;day51236. 递增三元组&#x1f332;day63491. 完全…

Java四种内部类(看这一篇就够了)

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

详细介绍less(css预处理语言)

详细介绍less&#xff08;css预处理语言&#xff09;什么是lessless解决什么问题less相比于css的优点如何使用less第一步&#xff1a;创建一个less文件第二步&#xff1a;引入less文件第三步&#xff0c;编写less文件&#xff08;和html一样的结构&#xff09;完整代码示例什么…