• C++primeplus P392-P415


    1.基类和派生类

    (1)基类
    C++中,可以由一个类派生出另一个类,原始的类叫做基类,派生出的类叫做派生类。

    webtown 俱乐部决定跟踪乒乓球会的会员。 于是设计一个简单的TableTennisPlayer类作为基类。

    class TableTennisPlayer   //类名
    {
    private:
    	string firstname;    //名字都是字符串形式
    	string lastname;
    	bool hasTable;    //会员判断标志
    public:
    	TableTennisPlayer(const string& fn = "none", const string& ln = "none", bool ht = false);  //默认构造函数
    	TableTennisPlayer(const char* fn, const char* ln, bool ht);  //普通构造函数
    	void Name()const;
    	bool HasTable()const { return hasTable; };  
    	void ResetTable(bool v) { hasTable = v; };
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    下面是函数的实现细节

    #include"tabtenn0.h"
    #include
    #include
    //#pragma warning(disable:4996)
    #pragma warning(disable:4996)
    TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht):firstname(fn),lastname(ln),hasTable(ht){}   //参数是const string的构造函数
    
    
    
    void TableTennisPlayer::Name()const
    {
    	std::cout << this->lastname.c_str() << ", " << this->firstname.c_str();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    下面是主函数

    #include"tabtenn0.h"
    #include
    
    int main(void)
    {
    	using std::cout;
    	TableTennisPlayer player1("Chuck", "Blizzard", true);   //这句话直接调用复制构造函数
    	TableTennisPlayer player2("Tare", "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\n";
    	else
    		cout << ": hasn't a table\n";
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    注意到该程序实例化对象时将C风格字符串作为参数:
    TableTennisPlayer player(“Chuck”, “Blizzard” , true);
    TableTennisPlayer player(“Tara”, “Boomdea” , false);

    但构造函数的形参类型被声明为const string &。这导致类型,但是string类有一个将const char *作为参数的构造函数,使用C风格字符串的时候,将自动调用这个构造函数。也就是说,无论参数是const string&还是const char *都有相应的构造函数。
    再一个,成员初始化列表直接调用string的复制构造函数。如果不使用初始化列表,那么将会先调用string的默认构造函数,再调用string的赋值运算符。

    (2)派生类
    webtnow俱乐部的一些成员曾经参加过当地的乒乓球竞标赛,需要这样一个类,它能包括成员在比赛中的得分。由此从TableTennisPlayer类中派生出一个类。我们叫他RatedPlayer类。

    声明RatedPlayer类从TableTennisPlayer类派生来:

    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(unsinged int r) {rating=r;}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 冒号代表,RatedPlayer类是由TableTennisPlayer派生而来
    • public表示:基类的公有成员将成为派生类的公有成员。基类的私有成员也成为派生类的一部分,但是只能通过基类的公有和保护方法访问(间接访问)
    • 派生类对象存储了基类的数据成员(派生类继承了基类的实现)
    • 派生类对象可以使用基类的方法
    • 派生类需要自己的构造函数
    • 派生类可以根据需要添加额外的数据成员和成员函数

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

    派生类不能直接访问基类的私有成员,需要通过基类的方法进行访问
    举个例子:儿子可以进爸爸的房间,但是不能动爸爸的保险柜,只有爸爸才能开保险柜。但是儿子可以通过爸爸去开保险柜。所以实际上儿子是不能开保险柜的,根本上还是爸爸开的。即使结果都是开了保险柜。

    派生类构造函数必须使用基类构造函数,创建派生类对象的时候,首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。那么就需要使用成员初始化列表语法来完成这个工作。

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

    如果没有显示使用基类构造函数,将会调用默认基类构造函数,像下面这样:

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

    除此之外,还可以这样使用构造函数

    RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer &tp):TableTennisPlayer(tp)
    {
    	Rating=r;
    }
    
    • 1
    • 2
    • 3
    • 4

    由于tp是引用,所以将调用基类的复制构造函数。
    有关派生类构造函数的要点如下:

    • 首先创建基类对象
    • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
    • 派生类构造函数应初始化派生类新增的数据成员
    • 如果初始化列表语法没有指明要使用的基类构造函数,将使用默认的基类构造函数
    • 派生类对象过期时,将首先调用派生类析构函数,然后调用基类析构函数

    3.使用派生类

    要使用派生类,程序必须要能够访问基类声明。
    由于两个类相关联,所以将他们的类声明放在一起

    #ifndef  TABTENN0_H_
    #define  TABTENN0_H_
    
    #include
    using std::string;    //使用名称空间std里面声明的string
    
    class TableTennisPlayer
    {
    private:
    	string firstname;
    	string lastname;
    	bool hasTable;
    public:
    	TableTennisPlayer(const string& fn = "none", const string& ln = "none", bool ht = false);  //默认构造函数
    	TableTennisPlayer(const char* fn, const char* ln, bool ht);
    	void Name()const;
    	bool HasTable()const { return hasTable; };
    	void ResetTable(bool v) { hasTable = v; };
    };
    
    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 // ! TABTENN)_H_
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    具体实现方法:

    #include"标头.h"
    #include
    #include
    //#pragma warning(disable:4996)
    #pragma warning(disable:4996)
    TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht):firstname(fn),lastname(ln),hasTable(ht){}   //参数是const string的构造函数
    
    TableTennisPlayer::TableTennisPlayer(const char* fn, const char* ln, bool ht):hasTable(ht)   //参数是const char*的构造函数
    {
    	
    	firstname = fn;
    	lastname = ln;
    }
    
    void TableTennisPlayer::Name()const
    {
    	std::cout << this->lastname.c_str() << ", " << this->firstname.c_str();
    }
    
    RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp) :TableTennisPlayer(tp), rating(r)
    {
    
    }
    
    RatedPlayer::RatedPlayer(unsigned int r, const string& fn, const string& ln, bool ht) : TableTennisPlayer(fn, ln, ht)
    {
    	rating = r;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    注意这两个类对象如何使用Table类的Name()和Hastable()方法的。

    #include"标头.h"
    #include
    
    int main(void)
    {
    	using std::cout;
    	using std::endl;
    	TableTennisPlayer player1("Chuck", "Blizzard", true);   //这句话直接调用复制构造函数
    	//TableTennisPlayer player2("Tare", "Boomdea", false);
    	RatedPlayer rplayer1(1140, "Mallory", "Duck", true);   //直接调用复制构造函数
    
    	rplayer1.Name();
    	if (rplayer1.HasTable())
    		cout << ":has a table\n";
    	else
    		cout << ":hasn't a table\n";
    
    	player1.Name();
    	if (player1.HasTable())
    		cout << ":has a table\n";
    	else
    		cout << ":hasn't a table\n";
    
    	rplayer1.Name();
    	cout << "; Rating: " << rplayer1.Rating() << endl;
    
    	RatedPlayer rplayer2(1212, player1);
    	cout << "Name :";
    	rplayer2.Name();
    	cout << "; Rating: " << rplayer2.Rating() << endl;
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    4.派生类和基类的关系

    1. 派生类对象可以使用基类的方法,但是不能使用基类的私有方法
    2. 基类指针可以在不进行显示类型转换的情况下指向派生类的ResetRanking方法。基类引用可以在不进行显示类型转换的情况下引用派生类对象
    3. 然而,基类指针或引用只能用于调用基类方法,而不能调用派生类方法。
    4. 不能将基类对象和地址赋给派生类引用和指针
    5. 对于形参为指向基类的指针的函数,可以选择基类对象的地址或者派生类对象的地址作为实参。

    引用兼容性规则
    这个规则能够让你将基类对象初始化为派生类对象。

    5.is—a关系

    C++有三种继承方式:公有继承,保护继承,私有继承。
    公有继承是最常用的方式,他建立一种is-a关系。即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。
    注意,is-a关系是不可逆的,比如,苹果是水果,但是水果不是苹果。

    6.多态公有继承

    如果希望基类和派生类的同名函数实现不同的功能,就需要知道多态的概念。------即一个方法有多种实现。
    有两种重要的机制可用于实现多态公有继承:

    1. 在派生类中重新定义基类的方法
    2. 使用虚方法

    如果要使同名方法的实现不同,可以这样:

    1. 使用限定名。具体操作为:类型+‘:’+方法
    2. 使用virtual。这一点很关键
      如果没有使用virtual,程序将根据引用类型1或者指针类型选择方法,如果使用了virtual,程序将根据引用或者指针指向的对象的类型来选择方法,这两者是有很大区别的。

    在基类定义的方法,在派生类又再次定义,这叫虚方法。方法在基类被声明为虚方法后,在派生类将自动成为虚方法。然而,在派生类声明中使用关键字virtual也不失为一种好方法。 注意,基类声明了一个虚析构函数,这样做是为了确保释放对象的时候,按照正确的顺序调用析构函数。

    非构造函数不能使用成员初始化列表语法,但是派生类方法可以调用公有的基类方法。

    虚析构函数可以确保正确的析构函数序列被调用。如果析构函数不是虚的,则将只调用对应于指针类型的析构函数(析构函数是公有成员,会被派生类继承)

    7.静态联编和动态联编

    首先知道什么是联编:
    将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在编译过程中进行联编被称为静态联编,又称为早期联编。在程序运行时联编叫做动态联编,也叫晚期联编。

    在C中,这很简单,因为C中的函数名是唯一的。
    但是C++不一样,因为C++可以重载函数,这使这项工作复杂一点。

    (1)指针和引用类型的兼容性

    指向基类的引用或指针可以引用派生类对象,而不必进行显示类型转换。将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显示类型转换。
    将基类指针或者引用转换为派生类指针或者引用被称为向下强制转换。但是这两个转换有区别,如果不使用显示转换类型,则向下强制转换是不允许的。
    隐式向上强制类型转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚函数来满足这种需求.

    (2)虚成员函数和动态联编

    BrassPlus ophelia;
    brass *bp;
    bp=&ophelia;
    bp->ViewAcct();
    
    • 1
    • 2
    • 3
    • 4

    如果基类Brass(派生类是BrassPlus)没有将ViewAcct()声明为虚的,则bp->ViewAcct()将根据指针类型(Brass*)调用Brass::ViewAcct()。由于指针类型在编译的时候就是已知的,因此编译器在编译的时候就可以完成联编,将ViewAcct()转换为Brass::ViewAcct()。编译器对非虚方法使用静态联编。

    相反,如果将基类成员函数ViewAcct()声明为虚的,则bp->ViewAcct()根据对象类型调用BrassPlus::ViewAcct()。很明显,只有在运行程序时才能确定对象的类型,所以编译器生成的代码在程序执行时,根据对象类型将ViewAcct()关联到Brass::ViewAcct()或BrassPlus::ViewAcct()。总之,编译器对虚方法使用动态联编。

    (3)虚函数的工作原理

    编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这个数组叫虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。例如:基类对象包含一个指针,该指针指向基类中所有虚函数的地址表(这说明这个表概念上是一个连续的空间,一个指针指向这个空间的首地址)。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。如果派生类没有重新定义虚函数,则虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则将该函数的地址添加到表中。无论类中包含的虚函数是一个还是10个,都只需要在对象中添加一个指针,只是表的大小不一样而已。

    (4)使用虚函数的成本

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

    1. 每个对象所占内存都会增大,因为需要存储‘存储地址的空间’。
    2. 对于每个类,编译器都将创建一个虚函数地址表(数组)
    3. 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
    4. 虽然非虚函数的效率比虚函数高,但不具备动态联编功能。

    8.有关虚函数的注意事项

    (1)重新定义将隐藏方法

    假设创建如下所示代码:

    class Dwelling
    {
    	public:
    	virtual void showperks(int a)const;
    	...
    }
    
    class Hovel:public  Dwelling
    {
    	public:
    	virtual void showperks()const;
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如果这样使用,会出现问题:
    Hovel trump;
    trump.showperks() ; //可以
    trump.showperks(2); //不可以

    为什么会这样呢?
    因为实际上trump对象的虚函数表将基类的同名函数隐藏了(该函数不在函数表中),各玩各的。重新定义继承的方法不是重载。如果重新定义派生类的函数,将覆盖所有基类同名的函数。

    所以,为了不出现错误,我们应该规范使用:

    1. 如果重新定义继承的方法,应确保与原来的原型完全相同。
    2. 但是如果返回类型是基类引用或者指针,则可以修改为指向派生类的引用或者指针。这叫返回类型协变。因为允许返回类型随类类型的变化而变化。举个例子:

    class Dewlling
    {
    public:
    virtual Dewlling &bulit(int n);

    };
    class Hovel:public Dewlling
    {
    public:
    virtual Hovel&bulit(int n);

    }

    3.如果基类声明被重载了,则应该在派生类中重新定义所有的基类版本。

    class Dewlling 
    {
    	public:
    	virtual void showperks(int a)const;
    	virtual void showperks(double x)const;
    	virtual void showperks()const;
    }
    
    
    class Hovel:public Dewlling
    {
    public:
    	virtual void showperks(int a)const;
    	virtual void showperks(double x)const;
    	virtual void showperks()const;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果只重新定义一个版本,则其它两个被隐藏,派生类对象将无法使用他们。注意,如果不需要修改,则新定义可以只调用基类版本:

    void Hovel::showperks()const{Dewlling ::showperks();}
    
    
    
    • 1
    • 2
    • 3

    9.访问控制:protected

    关键字protected与private 相似,在类外只能用公有类成员来访问protected中的成员。但是两者还是有区别的,只是这区别只有来基类派生的类中才会表现出来。

    • 对于类外来说,protected和private相似;
    • 对于派生类来说,protected和public相似;
  • 相关阅读:
    linux共享文件问题
    基于单片机的导盲拐杖设计
    Hostlink通讯协议解析【串行 C-Mode和Fins】
    练习实践:ubuntu18.04安装、配置Nginx+PHP环境,两种配置方式,多站点
    laravel系列(三) Dcat admin框架工具表单以及普通表单的使用
    运维监控的发展前景与挑战
    给新入坑的小伙伴们的郑氏Java上路指南
    对强缓存和协商缓存的理解
    java基础新
    【无标题】
  • 原文地址:https://blog.csdn.net/m0_60343477/article/details/126681636