读书笔记|| 类继承

一、一个简单的基类

面向对象编程的主要目的之一是提供可重用的代码。传统的C函数库通过预定义、预编译的函数提供了可重用性。C++类提供了更高层次的重用性,类库由类声明和实现构成。因为类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。类继承,C++提供了比修改代码更好的方法来扩展和修改类,能够从已有的类派生出新的类,而派生类继承了原有类的特征,包括方法。通过继承派生出的类通常比设计新类要容易得多,下面是可以通过继承完成的一些工作:

  • 可以在已有类的基础上添加功能;
  • 可以给类添加数据;
  • 可以修改类方法的行为。
    继承机制只需要提供新特性,甚至不需要访问源代码就可以派生出类。
    从一个类派生出另一个类时,原始类称为基类,继承类称为派生类,为了说明继承,首先需要一个基类。
#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using std::string;
// simple base class
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; };
};
#endif
#include "tabtenn0.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer (const string & fn, 
    const string & ln, bool ht) : firstname(fn),
	    lastname(ln), hasTable(ht) {}
    
void TableTennisPlayer::Name() const
{
    std::cout << lastname << ", " << firstname;
}
#include <iostream>
#include "tabtenn0.h"
int main ( void )
{
    using std::cout;
    TableTennisPlayer player1("Chuck", "Blizzard", true);
    TableTennisPlayer player2("Tara", "Boomdea", false);
    player1.Name();
    if (player1.HasTable())
        cout << ": has a table.\n";
    else
        cout << ": hasn't a table.\n";
    player2.Name();
    if (player2.HasTable())
        cout << ": has a table";
    else
        cout << ": hasn't a table.\n";
    // std::cin.get();
    return 0;
}

在这里插入图片描述
1.派生一个类

  • 派生类需要自己的构造函数
  • 派生类可以根据需要添加额外的数据成员和成员函数。
    构造函数必须给新成员和继承的成员提供数据。
    2.构造函数:访问权限的考虑
    派生类不能直接访问基类的私有成员,而必须通过基类的方法进行访问。派生类构造函数必须使用基类的构造函数。创建派生类对象时,程序首先创建基类对象。有关派生类构造函数的要点如下:
  • 首先创建基类对象;
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
  • 派生类构造函数应初始化派生类新增的数据成员。
    3.使用派生类
    要使用派生类,程序必须要能够访问基类声明。
#ifndef TABTENN1_H_
#define TABTENN1_H_
#include <string>
using std::string;
// simple base class
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; };
};
// 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;}
};
#endif
#include "tabtenn1.h"
#include <iostream>
TableTennisPlayer::TableTennisPlayer (const string & fn, 
    const string & ln, bool ht) : firstname(fn),
	    lastname(ln), hasTable(ht) {}
    
void TableTennisPlayer::Name() const
{
    std::cout << lastname << ", " << firstname;
}
// RatedPlayer methods
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
    rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
    : TableTennisPlayer(tp), rating(r)
{
}
#include <iostream>
#include "tabtenn1.h"
int main ( void )
{
    using std::cout;
    using std::endl;
    TableTennisPlayer player1("Tara", "Boomdea", false);
    RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
    rplayer1.Name();          // derived object uses base method
    if (rplayer1.HasTable())
        cout << ": has a table.\n";
    else
        cout << ": hasn't a table.\n";
    player1.Name();           // base object uses base method
    if (player1.HasTable())
        cout << ": has a table";
    else
        cout << ": hasn't a table.\n";
    cout << "Name: ";
    rplayer1.Name();
    cout << "; Rating: " << rplayer1.Rating() << endl;
// initialize RatedPlayer using TableTennisPlayer object
    RatedPlayer rplayer2(1212, player1);
    cout << "Name: ";
    rplayer2.Name();
    cout << "; Rating: " << rplayer2.Rating() << endl;
    // std::cin.get();
    return 0;
}

在这里插入图片描述
4.派生类和基类之间的特殊关系
派生类对象可以使用基类的方法,条件是方法不是私有的;另外两个重要的关系是:基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类的对象。

二、继承:is-a关系

派生类和基类之间的特殊关系是基于C++继承的底层模型的。实际上,C++有3种继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。新类将继承原始类的所有数据成员,派生类可以添加特性。这种关系的通常使用术语is-a。
公有继承不建立has-a关系,举例:午餐可能包括水果,但通常午餐并不是水果,在午餐中加入水果的正确方法是将其做为一种has-a关系:午餐有水果。
公有继承不能建立is-like-a关系,也就说,他不采用明喻。继承可以在基类的基础上添加属性,但不能删除基类的属性。在这些情况下,可以设计一个包含公有特性的类,然后以is-a或has-a关系,在这个类的基础上定义相关的类。
公有继承不建立is-implemented-as-a(作为……来实现),例如,可以使用数组来实现栈,但从Array类派生出Stack类是不合适的,因为栈不是数组。正确的做法是让栈包含一个私有的Array对象成员来隐藏数组实现。
公有继承不建立uses-a关系。例如:计算机可以使用激光打印机,但从Computer类派生出Printer类(或者反过来是没有意义的),然而可以使用友元函数或类来处理Printer对象和Computer对象之间的通信,

三、多态公有继承

派生类对象使用基类的方法而未做任何修改。有两种机制可用于实现多态公有继承:

  • 在派生类中重新定义基类的方法
  • 使用虚方法
    1.类实现
    关键字virtual只用于类声明的方法原型中。派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法,构造函数使用一种技术,而其他成员函数使用另一种技术,
    2.演示虚方法的行为
    方法是通过对象(而不是指针或引用)调用的,没有使用虚方法特性。
    3.为何需要虚析构函数
    如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。

四、静态联编和动态联编

程序调用函数时,将使用哪个可执行代码块?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C语言中,这非常简单,因为每个函数名都将对应一个不同的函数。在C++中,由于重载函数的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行来联编被称为静态联编,又称为早期联编。然而,虚函数使这项工作变得更困难。使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象,所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称为晚期联编。
1.指针和引用类型的兼容性
在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上来说,这是由继承控制的。公有继承建立is-a关系的一种方法是如何处理指向对象的指针和引用。通常C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型:

double x = 2.5;
int * pi = &x ; // invalid assignment 
long & rl = x ; 

指向基类的引用或指针可以引用派生类对象,而不必进行显示类型转换。

BrassPlus dilly ("Annie Dill" , 493222,2000);
Brass * pb = &dilly ; // ok
Brass & rl = dilly ; //ok

将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要及逆行显示类型转换,该规则使is-a关系的一部分。
相反的过程——将基类指针或引用转换为派生类的指针或引用——称为向下强制转换,如果不使用显示类型转换,则向下强制转换是不被允许的,原因是is-a关系通常是不可逆的。
2.虚成员函数和动态联编
如果动态联编让您能够重新定义类方法,而静态联编在这方面很差,但是依旧要默认静态联编。原因有两个——效率和概念模型
为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。
在设计类时,可能包含一些不在派生类重新定义的成员函数。不该将函数设置为虚函数,有两方面的好处:首先效率更高,其次,指出不要重新定义该函数,这表明,仅将那些预期将被重新定义的方法声明为虚的。
虚函数的工作原理:C++规定了虚函数的行为,但将实现方法留给了编译器作者,不需要知道实现方法就可以使用虚函数,但了解虚函数的工作原理有利于更好地理解概念。通常,编译器处理虚函数地方法是:给每一个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数地址。使用虚函数时,在内存和执行速度方面有一定的成本,包括:

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

五、访问控制:protected

关键字protected与private相似,在类外只能用共有类成员来访问protected部分中的类成员。private与protected之间的区别只有在基类派生的类中才会表现出来,派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。

六、抽象基类

圆是椭圆的一种特殊情况——长轴和短轴等长的椭圆。因此,所有的圆都是椭圆,可以从Ellipse类派生出Circle类。
考虑Ellipse类包含的内容,数据成员可以包括椭圆中心的坐标、半长轴、短半轴以及方向角。
另外,还可以包括一些移动椭圆、返回椭圆面积、旋转椭圆以及缩放长半轴和短半轴的方法:

class Ellipse
{
private:
    double x ;
    double y ;
    double a ;
    double b;
    double angle ;
    ……
public :
    ……
    void Move(int nx , ny ){x = nx;y = ny ;}
    virtual double Area() const {return 3.14159 * a * b ;}
    virtual void Rotate(double nang){angle += nang;}
    virtual void Scale(double sa , double sb)

现在从Ellipse类派生出一个Circle类:

class Circle : public Ellipse
{
    ……
}

虽然圆是椭圆的一种,但是这种派生是笨拙的。圆只需要一个半径就能描述大小和形状,并不需要那么多的量。所以,总的来说,不使用继承而直接定义Circle类更简单:

class Circle 
{
private :
    double x ;
    double y ;
    double r ;
    ……
public:
    ……
    void Move(int nx , ny){ x = nx; y = ny;}
    double Area() const{return 3.14159 * r * r;}
    void Scale {double sr } {r *= sr ;}
    ……
}

还有另外一种解决方法,就是将Ellipse和Circle类中抽象出它们的共性,将这些特性放到一个ABC中,然后从该ABC派生出Circle和Ellipse类。这样,便可以使用基类指针数组同时管理Circle和Ellipse对象,即可以使用多态方法。
当类声明中包含虚函数是,则不能创建该类的对象。包含纯虚函数的类只用作基类,要成为真正的ABC,必须至少包含一个纯虚函数,原型中的=0使虚函数成为纯虚函数。在原型中的=0指出类使一个抽象基类,在类中可以不定义该函数。

七、继承和动态内存分配

1.派生类不使用new
假设基类使用了动态内存分配:

class baseDMA
{
private :
    char * label;
    int rating'
public :
    baseDMA (const char * 1 = 'null'int r = 0);
    baseDMA (const baseDMA & rs);
    virture -bassDMA();
    bassDMA & operator =(const baseDMA & rs);
    ……
 }; 

声明中包含了构造函数使用new时需要的特殊方法;析构函数、复制构造函数和重载赋值运算符。
2.派生类使用new

class hasDMA : public baseDMA
{
private :
    char * style ;
public :
……
};

在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。
派生类析构函数将自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
水资源是人社会的宝贵财富,在生活、工农业生产中是不可缺少的。随着世界人口的增长及工农业生产的发展,需水量也在日益增长,水已经变得比以往任何时候都要珍贵。但是,由于人的生产和生活,导致水体的污染,水质恶化,使有限的水资源更加紧张。长期以来,油物质(石油物质和动植物油)一直是水和土壤中的重要污染源。它不仅对人的身体健康带来极大危害,而且使水质恶化,严重破坏水体生态平衡。因此各国都加强了油物质对水体和土壤的污染的治理。对于水中油含量的检测,我国处于落后阶段,与国际先进水平存在差距,所以难以满足当今技术水平的要求。为了取得具有代表性的正确数据,使分析数据具有与现代测试技术水平相应的准确性和先进性,不断提高分析成果的可比性和应用效果,检测的方法和仪器是非常重要的。只有保证了这两方面才能保证快速和准确地测量出水中油污染物含量,以达到保护和治理水污染的目的。开展水中油污染检测方法、技术和检测设备的研究,是提高水污染检测的一条重要措施。通过本课题的研究,探索出一套适合我国国情的水质污染现场检测技术和检测设备,具有广泛的应用前景和科学研究价值。 本课题针对我国水体的油污染,探索一套检测油污染的可行方案和方法,利用非分散红外光度法技术,开发研制具有自主知识产权的适合国情的适于野外便携式的测油仪。利用此仪器,可以检测出被测水样中亚甲基、甲基物质和动植物油脂的污染物含量,为我国众多的环境检测站点监测水体的油污染状况提供依据。
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值