初探类继承0


前言

本章内容参考《C++ Primer Plus6th中文版》第13章,以及作者本人的理解,海贼星星老师的讲解.

  • 本章讨论公有继承,这种继承模拟了is-a关系,即派生对象是基对象的特例。(公有继承在什么情况下合适,什么情况下不合适)
  • 有些继承关系是多态的,意味着相同方法名可能导致依赖于对象类型的行为。要实现这种行为——虚函数
  • 有时,使用抽象基类是实现继承关系的最佳方式

面向对象编程的主要目的之一是提供可重用的代码。开发新项目, 尤其是当项目十分庞大时,重用经过测试的代码比重新编写代码要好得多。另外,必须考虑的细节越少, 便越能专注于程序的整体策略。

下面简述一下面向对象的三大特性:

  • 封装:我该有的和我该做的

  • 继承:叫爸爸,继承爸爸的财富(从抽象到具象的过程)

  • 多态:静态多态,动态多态。我就是我,不一样的烟火。


一、继承的语法结构

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类

定义方式:
class derived_c : 权限修饰 base_c

权限修饰

protected这里可以理解为家传的财产。就是外人访问不了,但是孙子,曾孙子,… …都可以访问

派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说, 保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

子类访问父类权限:
在这里插入图片描述

  • 子类继承父类的全部属性和方法,但是不一定能访问。
    子类想要访问父类的财产,只和父类中定义的权限相关。
  • 外人想要访问子类拿到的财产,则要有两个属性来取决定:

    1. 继承时的权限
    2. 父类成员原本在父类中自己的权限

外部访问权限:
在这里插入图片描述

规律:按照最小权限去做判断

二、派生一个类

基类

// tabtenn.h
#ifndef TABENN_H_
#define TABENN_H_
using std::string

class TableTennisPlayer{
private:
	string firstname;
	string lastname;
	bool hasTable;
public:
	TableTennisPlayer(const string &fn = 'none', const string &ln = 'none', bool ht = false);
	void Name() const;
	bool HasTable() const {return hasTable;}
	void ResetTable(bool v){ hasTable = v;}	
}; 

派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类 的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基 类的公有和保护方法访问

class RetedPlayer: public TableTennisPlayer {}

上述代码完成了哪些工作呢?Ratedplayer对象将具有以下特征:

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口)。

需要再继承特性中添加什么?

  • 派生类需要自己的构造函数。
  • 派生类可以根据需要添加额外的数据成员和成员函数。

构造函数必须给新成员(如果有的话)和继承的成员提供数据

在 第一个RatedPlayer构造函数中,每个成员对应一个形参;而第二个 Ratedplayer构造函数使用一个TableTennisPlayer参数,该参数包括 firstname、lastname和hasTable。

// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
    unsigned int rating;
public:
    RatedPlayer (unsigned int r = 0, const string & fn = "none",
                 const string & ln = "none", bool ht = false);
    RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
    unsigned int Rating() const { return rating; }
    void ResetRating (unsigned int r) {rating = r;}
};

2.1 构造函数:访问权限的考虑

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体地说,派生类构造函数需要使用基类构造函数。

== 创建派生类对象时,程序首先创建基类对象。 ==从概念上说,这意味 着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员 初始化列表语法来完成这种工作。例如,下面是第一个RatedPlayer构造 函数的代码:

// RatedPlayer methods
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
    rating = r;
}

其中: TableTennisPlayer(fn,ln,ht) 是成员初始化列表。它是可执行的 代码,调用TableTennisPlayer构造函数。

在这里插入图片描述
如果省略成员初始化列表,情况将如何呢?

RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) {
    rating = r;
}

首先必须创建基类对象,如果不调用基类构造函数,程序将使用默 认的基类构造函数,因此上述代码与下面等效:

RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) : TableTennisPlayer() {
    rating = r;
}

除非要使用默认构造函数,否则应显式调用正确的基类构造函数

第二个构造函数可以这样写:

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp) : TableTennisPlayer(tp) {
    rating = r;
}
//这里生成基类的构造函数 TableTennisPlayer(tp) 这是基类的复制构造函数,但是我没有没自己定义,所以编译器自动生成的

//还可以这样写
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp) : TableTennisPlayer(tp), rating(r) {}

有关派生类构造函数的要点如下:

  • 首先创建基类对象;
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
  • 派生类构造函数应初始化派生类新增的数据成员。

派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。(这个也可以理解,因为这和栈很像,先生成后销毁)

2.2 使用派生类

要使用派生类,程序必须要能够访问基类声明。既可以将两种类的声明置于同一个头文件中。也可以将每个类放在独立的头文件 中,如果两个类是相关的,把它们的类声明放在一起更合适。

类声明:
在这里插入图片描述

类定义:
在这里插入图片描述

2.3 派生类和基类之间的特殊关系

  1. 派生类对象可以使用基类的方法,条件是方法不是私有的;
  2. 基类指针可以在不进行显式类型转换的情况下指向派生类对象(基类指针可以直接指向派生类,神奇);
  3. 基类引用可以在不进行显式类型转换的情况下引用派生类对象(基类引用可以直接应用派生类,神奇)。

在这里插入图片描述

不过,基类指针或引用只能用于调用基类方法,因此,不能使用rtpt来调用派生类的ResetRanking方法。

通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对 继承来说是例外
然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针。

上述规则是有道理的。允许基类引用隐式地引用派生类对象,等于是可以使用基类引用为派生类对象调用基类的方法。因为派生类 继承了基类的方法,所以这样做不会出现问题。

而如果可以将基类对象赋 给派生类引用,将发生什么情况呢?派生类引用能够为基对象调用派生 类方法,这样做将出现问题。例如,将RatedPlayer::Rating( )方法用于 TableTennisPlayer对象是没有意义的,因为TableTennisPlayer对象没有 rating成员。

基类引用和指针可以指向派生类对象,将出现一些很有趣的结果。其中之一是基类中引用定义的函数或指针参数可用于派生类对象
例如这样一个函数:void Show(const TableTennnisPlayer & rt);
在这里插入图片描述

引用兼容属性能让我们能够将基类对象初始化为派生类对象。假设有这样一行代码:

RatedPlayer olaf1(234, "Olaf", "Loaf",true);
TableTennisPlayer olaf2(olaf1);

//要初始化olaf2,匹配的构造函数的原型如下:
TableTennisPlayer(const RatedPlayer &); // but does not exist

//类定义中没有这样的构造函数,但存在隐式复制构造函数(编译器给的默认):
// implicity copy constructor
TableTennisPlayer(const TableTennisPlayer &);

形参是基类引用,因此它可以引生派生类,这样,将olaf2初始化为olaf1时,将使用该隐式构造函数,它将复制firstname,lastname 和 hasTable成员。

同理,也可将派生类赋给基类

RatedPlayer olaf1(234, "Olaf", "Loaf",true);
TableTennisPlayer winner;
winner = olaf1;

//在这种条件下,程序将使用隐式重载赋值运算符
TableTennisPlayer & operator=(const TableTennisPlayer &)const

小节:
派生类可以当作参数赋值给基类中引用定义的函数或指针参数,隐式函数当然也可以咯!!!


三、继承:is-a关系

派生类和基类之间的特殊关系是基于C++继承的底层模型的。实际 上,C++有3种继承方式:公有继承、保护继承和私有继承。公有继承 是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,任何可以对基类对象执行的操作,也可以对派生类对象执行

因为派生类可以添加特性,所以,将这种关系称为 is-a-kind-of(是一种)关系可能 更准确,但是通常使用术语 is-a。


四、多态公有继承

派生类对象使用基类的方法,而未做 任何修改。然而,可能会遇到这样的情况,即希望同一个方法在派生类 和基类中的行为是不同的,这称为多态公有继承。有两种重要的机制可用于实现多态公有继承:

  • 在派生类中重新定义基类的方法;
  • 使用虚方法(关键字 virtual),然后在各自类中对该函数编写相关定义即可。

4.1 设计一个基类和其派生类

Brass Account(基类):

信息:

  • 客户姓名
  • 账号
  • 当前结余

可执行操作:

  • 创建账户
  • 存款
  • 取款
  • 显示账户信息

Brass Plus(派生类):
首先,其包含了Brass Account的所有信息及如下信息:

  • 透支上限
  • 透支贷款利率
  • 当前透支总额

不需要新的操作,但有两个操作的视线不同:

  • 对于取款操作,必须考虑透支保护
  • 显示操作必须显示Brass Plus账户的其他信息

设置透支上限(默认500)和利率(默认11.125%)

4.2 类的声明和实现

类声明:
在这里插入图片描述
在这里插入图片描述
说明:

  • Brass类和BrassPlus类都声明了ViewAcct( )和Withdraw( )方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的;
  • Brass类在声明ViewAcct( )和Withdraw( )时使用了新关键字virtual。这些方法被称为虚方法(virtual method);
  • Brass类还声明了一个虚析构函数,虽然该析构函数不执行任何操作。

虚函数是面向对象编程中的一个概念,主要用于实现多态性。当你在基类中声明一个函数为虚函数时,它允许派生类中具有相同签名的函数覆盖(override)基类中的实现。这意味着当你通过基类的指针或引用调用该虚函数时,将根据对象的实际类型来决定调用哪个版本的函数。
这种情况常见于基类引用指向一个派生类对象

实现动态绑定:动态绑定:当基类的指针或引用调用虚函数时,会根据对象的实际类型来动态地选择相应的函数实现。这使得在运行时可以根据对象的实际类型来调用适当的方法,而不是在编译时静态决定,增加了程序的灵活性。

举个例子:
如果不用虚函数,那么是什么类型的引用就用什么类型的方法
在这里插入图片描述
用了虚函数,那么虚函数会根据你实际是什么类型来调用相应的方法
在这里插入图片描述

基类声明了一个虚析构函数。这样做是为了确保释放派 生对象时,按正确的顺序调用析构函数。这个以下面会介绍。情况见于有指针类型的变量!!!

类实现:
见《C++ Primer Plus6th中文版》P403
注意:关键字virtual只用于类声明的方法原型,而没有用于定义中,当然这里有要注意的地方,那就是:

// 派生类重载函数的时候,要记得加上类的作用解析域(不管重载不重载都要加)
Brass::Withdraw(double amt){... ...}
Brass::ViewAcct() const{... ...}

// redefine
BrassPlus::Withdraw(double amt){
	... ...
	Brass::Withdraw();   //display base portion
	... ...
}
BrassPlus::ViewAcct() const{
	... ...
	Brass::Withdraw();   //display base portion
	... ...
}

在程序清单13.9中,方法是通过对象(而不是指针或者引用)调用的,没有使用虚方法特性

在程序清单13.10则举了一个虚方法的例子:

假设要同时管理Brass和BrassPlus账户,如果能使用同一个数组来保存Brsss和BrassPlus对象,将很有帮助,但这是不可能的。数组中所有元素的类型必须相同,而Brass和BrassPlus是不同的类型。然而,可以创建指向Brass的指针数组。这样,每个元素的类型都相同,但由于使用的是公有继承模型,因此Brass指针既可以指向Brass对象,也可以指向BrassPlus对象。因此,可以使用一个数组来表示多种类型的对象。这就是多态性。

在这里插入图片描述
上面代码描述了在数组中都是Brass指针,但是初始化用了两种不同的方式:基类,派生类。
多态性则是由下面代码提供:

for(i = 0; i < CLIENTS; i++){
	p_clients[i]->ViewAcct();
	cout << endl;
}

4.3 为何需要虚析构函数

如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。对于 程序清单13.10,这意味着只有Brass的析构函数被调用,即使指针指向 的是一个BrassPlus对象。如果析构函数是虚的,将调用相应对象类型的 析构函数。因此,如果指针指向的是BrassPlus对象,将调用BrassPlus的 析构函数,然后自动调用基类的析构函数。因此,使用虚析构函数可以 确保正确的析构函数序列被调用。

对于程序清单13.10,这种正确的行为并不是很重要,因为析构函数没有执行任何操作。然而,如果 BrassPlus包含一个执行某些操作的析构函数,则Brass必须有一个虚析构函数,即使该析构函数不执行任何操作。

举个例子:
比如你的派生类包含了指针成员,而你的基类析构函数没有销毁派生类指针成员的命令。由此可见,虚析构函数很重要!!!


五、静态联编和动态联编

程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这 个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。

在编译过程中进行联 编被称为静态联编(static binding),又称为早期联编(early binding)。

然而,虚函数使这项工作变得更困难。正如在程序清单 13.10所示的那样,使用哪一个函数是不能在编译时确定的,因为编译 器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程 序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。(会生成虚函数表

5.1 指针和引用类型的兼容性

将派生类引用或指针转换为基类引用或指针被称为向上强制转换 (upcasting),这使公有继承不需要进行显式类型转换。该规则是is-a 关系的一部分。

BrassPlus dilly{... ...};
Brass *pb = &dilly;  //OK
Brass & rb = dilly;  //OK

相反的过程——将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting),如果不使用显式类型转换,则向下强制转换是不允许的。原因是is-a关系通常是不可逆的。派生类可以新增 数据成员,因此使用这些数据成员的类成员函数不能应用于基类。

在这里插入图片描述

隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。

5.2虚成员函数和动态联编

编译器对虚方法使用动态联编。在大多数情况下,动态联编很好,因为它让程序能够选择为特定类 型设计的方法。

1.为什么有两种类型的联编以及为什么默认为静态联编

如果动态联编让您能够重新定义类方法,而静态联编在这方面很 差,为何不摒弃静态联编呢?原因有两个——效率和概念模型。

首先来看效率。为使程序能够在运行阶段进行决策,必须采取一些 方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销 (稍后将介绍一种动态联编方法)。

接下来看概念模型。在设计类时,可能包含一些不在派生类重新定 义的成员函数。例如,Brass::Balance( )函数返回账户结余,不应该重新 定义。不将该函数设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的 方法声明为虚的。

2.虚函数的工作原理

编译器处理虚函数的方法是:给每个对象添加一个隐藏成 员。隐藏成员中保存了一个指向函数地址数组的指针。== 这种数组称为虚函数表(virtual function table,vtbl)。==虚函数表中存储了为类对象进行 声明的虚函数的地址。

例如,基类对象包含一个指针,该指针指向基类 中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指 针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地 址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地 址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl 中(参见图13.5)。注意,无论类中包含的虚函数是1个还是10个,都 只需要在对象中添加1个地址成员,只是表的大小不同而已。
在这里插入图片描述
调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相 应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使 用数组中的第一个函数地址,并执行具有该地址的函数。如果使用类声 明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。

总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:

  • 每个对象都将增大,增大量为存储地址的空间;
  • 对于每个类,编译器都创建一个虚函数地址表(数组);
  • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址;

5.3 有关虚函数注意事项

  • 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有 的派生类(包括从派生类派生出来的类)中是虚的;
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为 动态联编。这种行为非常重要,因为这样基类指针或引 用可以指向派生类对象;
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的 类方法声明为虚的;
  • 构造函数不能是虚函数;
  • 析构函数应当是虚函数,除非类不用做基类。这意味着,即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使它不执行任何操作;

给类定义一个虚析构函数并非错误,即使这个类不用做基类;这只是一个效率方面的问题。

  • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函 数。
  • 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派 生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。
  • 如果派生类重新定义了虚函数,那么使用派生类将会隐藏基类的方法

六、抽象基类

至此,介绍了简单继承和较复杂的多态继承。接下来更为复杂的是抽象基类(abstract base class,ABC)。我们来看一些可使用ABC的编程情况。有时候,使用is-a规则并不是看上去的那样简单。

所有的圆都是椭圆,可以从Ellipse类派生出Circle类。但涉及到细节时,将发现很多问题。

首先考虑Ellipse类包含的内容。数据成员可以包括椭圆中心的坐标、半长轴(长轴的一半)、短半轴(短轴的一半)以及方向角(水平坐标轴与长轴之间的角度)。另外,还可以包括一些移动椭圆、返回椭圆面积、旋转椭圆以及缩放长半轴和短半轴的方法。
虽然圆是一种椭圆,但是这种派生是笨拙的。例如,圆只需要一个值(半径)就可以描述大小和形状,并不需要有长半轴(a)和短半轴(b)。
总的来说,不使用继承,直接定义Circle类更简单。但圆和椭圆有很多共性,分开定义类则忽略它们的共性,不符合实际情况。这时候就需要ABC了

还有一种解决方法,即从Ellipse和Circle类中抽象出它们的共性,将这些特性放到一个ABC中。然后从该ABC派生出Circle和Ellipse类。这样,便可以使用基类指针数组同时管理Circle和Ellipse对象,即可以使用多态方法)。在这个例子中,这两个类的共同点是中心坐标、Move( )方法(对于这两个类是相同的)和Area( )方法(对于这两个类来说,是不同的)。确实,甚至不能在ABC中实现Area( )方法,因为它没有包含必要的数据成员。

C++通过使用纯虚函数(pure virtual function) 提供未实现的函数。纯虚函数声明的结尾处为=0。

virtual double Area() const = 0; // a pure virtual function

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念 是,包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包含 一个纯虚函数。原型中的=0使虚函数成为纯虚函数。
在这里插入图片描述

总之,在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。

现在,可以从BaseEllipse类派生出Ellips类和Circle类,添加所需的成员来完成每个类。由于Circle和Ellipse对象的基类相同,因此可以用BaseEllipse指针数组同时管理这两种对象。像Circle和Ellipse这样的类有时被称为具体(concrete)类,这表示可以创建这些类型的对象。

总之,ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。

七、继承和动态内存分配

继承是怎样与动态内存分配(使用new和delete)进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?

7.1 第一种情况:派生类不使用new

假设基类使用了动态内存分配,基类中包含了构造函数使用new时需要的特殊方法:析构函数、复制构造函数和重载赋值运算符。如例子中的 baseDMA
在这里插入图片描述
现在,从 baseDMA 派生出 lackDMA 类,而后者不使用 new,也未包含其他一些不常用的、需要特殊处理的设计特性。

class lacksDMA: public baseDMA {
private:
    char color[40];
public:
    ...
}

先看结论:是否需要为lackDMA类定义显式析构函数、复制构造函数和赋值运算符呢?不需要。

  • 先看析构函数。如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数。实际上,派生类的默认构造 函数总是要进行一些操作:执行自身的代码后调用基类析构函数。因为 我们假设lackDMA成员不需执行任何特殊操作,所以默认析构函数是合适的。
  • 接着看复制构造函数。默认复制构造函数执行成 员复制,这对于动态内存分配来说是不合适的,但对于新的lacksDMA 成员来说是合适的。因此只需考虑继承的baseDMA对象。要知道,成员 复制将根据数据类型采用相应的复制方式,因此,将long复制到long中 是通过使用常规赋值完成的;但复制类成员或继承的类组件时,则是使 用该类的复制构造函数完成的。所以,lacksDMA类的默认复制构造函数使用显式baseDMA复制构造函数来复制lacksDMA对象的baseDMA部 分。因此,默认复制构造函数对于新的lacksDMA成员来说是合适的, 同时对于继承的baseDMA对象来说也是合适的。
  • 对于赋值运算来说,也是如此。类的默认赋值运算符将自动使用基类的 赋值运算符来对基类组件进行赋值。因此,默认赋值运算符也是合适的。

7.2 第二种情况:派生类使用new

在这种情况下,必须为派生类定义显式析构函数、复制构造函数和 赋值运算符。

这也很好理解,当派生类也有指针成员的时候,为了防止复制构造和赋值运算将派生类的指针成员给浅拷贝了。析构函数也要相应地释放派生类中的指针成员。

7.3派生类使用基类的友元函数

强制类型转换,见书《C++ Primer Plus6th中文版》P425


总结

.继承通过使用已有的类(基类)定义新的类(派生类),使得能够 根据需要修改编程代码。基类的析构函数通常应当是虚的。

如果要将类用作基类,则可以将成员声明为保护的,而不是私有 的,这样,派生类将可以直接访问这些成员。

可以考虑定义一个ABC:只定义接口,而不涉及实现。例如,可以 定义抽象类Shape,然后使用它派生出具体的形状类,如Circle和 Square。ABC必须至少包含一个纯虚方法,可以在声明中的分号前面加 上=0来声明纯虚方法。包含纯虚函数的类,不能用来创建对象。

参考文献

  1. 《C++ Primer Plus 6th中文版》
  2. 查漏补缺——C/C++(类0)
  3. https://github.com/ShujiaHuang/Cpp-Primer-Plus-6th
  4. 海贼宝藏星星老师
  • 45
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值