C++学习(五)

C++学习(五),第1张

C++学习(五) 目录 一、引言 二、类继承 ------> 2.1、基类 ------> 2.2、派生类 ------> 2.3、继承:is-a关系 ------> 2.4、多态公有继承 ------> 2.5、派生类的实现 ------> 2.6、静态/动态联编 ------> 2.7、访问控制:protected ------> 2.8、抽象基类 ------> 2.7、访问控制:protected ------> 2.8、类作用域 ------> 2.9、继承和动态内存分配 ------> 2.8、类作用域 三、整体回顾 ------> 3.1、基本方面 ------> 3.2、继承 ------> 3.3、基类方法的使用 一、引言

本章来学习C++的第三个部分,类的继承

二、类继承

针对库文件,C++类提供了更高层次的重用性。很多厂家都会提供自己的类库,类库由类声明和实现(定义)构成,所以类库都会提供源码,意味着我们可以对类库的源码进行修改。
C++提供了比修改源码更加好的方法来扩展和修改类,叫做类继承。它能够从已有的类派生出新的类,派生类继承了基类的特征,我们就可以再派生类中添加功能、数据、修改基类的方法。
派生类甚至可以不看源码,就可以派生类,在类中添加新特性

1、基类

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类,所以继承的前提需要一个基类。
我们先写一个简单的基类

//声明
class Table_player
{
private:
  string firstname;
  string lastname;
  bool hasTable;
public:
  Table_player(const string & fn = "none",const string & ln = "none",bool ht = false);
  void name() const;
  bool HanTable() const { return hasTable;};
  void ResetTable(bool v) {hasTable = v;};
};
//定义
Table_player::Table_player(const string & fn,const string & ln,bool ht): firstname(fn),lastname(ln),hasTable(ht)
{
  std::cout << "Table_player creat successn";
}

void Table_player::name() const
{
  std::cout << lastname << "-" << firstname << ": " << hasTable << std::endl;
}

2、派生类 派生一个类

接下来我们在上面基类的基础上派生出一个类

class Rated_player : public Table_player
{
......
}

冒号指出Rated_player的基类是Table_player;pubilc声明Table_player是一个公有基类,这被称为公有派生。派生类对象包含基类对象。
使用公有派生,基类的公有成员将会称为派生类的公有成员,基类的私有成员部分也将转换成派生类的一部分,但只能通过基类的公有和保护方法访问。
Rated_player对象将具有以下特征
1、存储了基类的数据成员(继承了基类的实现)
2、可以使用基类的方法(继承了基类的接口)

派生类需要添加的部分

1、需要添加自己的构造函数
2、可以根据需要添加额外的数据成员和成员函数

访问权限

派生类不能直接访问基类的私有成员,必须通过基类方法进行访问。所以,派生类的构造函数必须使用基类构造函数

//声明
Rated_player(unsigned int r,const string & fn = "none",const string & ln = "none",bool ht = false);
//定义
Rated_player::Rated_player(unsigned int r,const string & fn,const string & ln,bool ht): Table_player(fn,ln,ht)
{
  rating = r;
}

Rated_player将实参传递给Table_player的构造函数,后者创建一个Table_player对象,并将实参数据存储在该对象中,之后进入Rated_player的函数中给Rated_player私有成员赋值
如果派生类的构造函数不添加基类的构造函数,系统会使用默认的基类构造函数,所以每次都应该显示的调用正确的基类构造函数
派生类的构造函数还有一种写法

Rated_player::Rated_player(unsigned int r,const Table_player & tp): Table_player(tp),rating(r)
{
  
}

这里调用了基类的复制构造函数,如果没有动态分配内存,是可以直接用默认函数的。通常将基类初始化完后,直接调用下面这个会方便一些

派生类要点总结

总结一下派生类的要点
1、创建派生类之前首先要创建基类
2、派生类构造函数应该通过成员初始化列表将基类信息传递给基类构造函数
3、派生类应该初始化新增的成员
4、创建派生类是,先调用基类的构造函数,再调用派生类的构造函数。派生类对象过期时,程序将先调用派生类的析构函数,在调用基类的析构函数,因为派生类是基于基类之上的

使用派生类
int main()
{
  Table_player play1("huang","xiaom",1);
  Rated_player play2(20,play1);
  Rated_player play3(20,"zhou","jiel",1);
  play1.name();
  play2.name();
  play3.name();
  return 0;
}

如下打印

Table_player creat success
Rated_player creat success
Table_player creat success
Rated_player creat success
xiaom-huang: 1
xiaom-huang: 1
jiel-zhou: 1

可以看到,创建了两个基类对象,两个派生类对象,并且都可以使用基类的成员函数

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

1、派生类可以使用基类的方法,前提是方法不是私有的
2、基类指针可以在不进行显示类型转换的情况下指向派生类对象
3、基类引用可以在不显示类型转换的情况下引用派生类对象
4、形参指向基类的指针的函数,也可以使用基类对象的地址或派生类对象的地址作为实参
5、派生类对象可以赋给基类对象
但基类指针只能调用基类方法,不可以调用派生类的方法和访问派生类数据

3、继承:is-a关系

派生类和基类之间的特殊关系是基于C++继承的底层模型,实际上,有三种模型
公有继承:
是最常用的,建立一种is-a关系,即派生类对象也是一个基类对象,对基类对象执行的 *** 作,也可以对派生类执行。这就是is-a(is a king of)
保护继承:之后会介绍
私有继承:之后会介绍

4、多态公有继承

我们目前派生类使用基类的方法,并没有做任何修改,但如果我们希望同一个方法在派生类和基类中的行为是不同的,方法的行为取决于调用该方法的对象(非引用或指针的类型) ,这种行为就成为多态,有两种方法可以实现多态公有继承

1、在派生类中重新定义基类的方法

发现这块书里没有写,其实就是对基类方法的重载,经过查找资料与实验发现
当定义与基类中方法同名的非虚方法时,它会在派生类中隐藏基类的该方法与所有该方法的重载方法,所以我们一般不会使用重载,否则基类中的对应方法就无法使用了

2、使用虚方法

下面会说到

虚函数

1、我们经常会基类中将派生类要重新定义的方法声明为虚方法,这种方法被声明为虚之后,它在派生类中也将自动称为虚方法,我们在派生类中可以使用virtual来指出那些函数是虚函数。注意virtual和friend一样,只在声明中出现,定义中不能添加
定义成虚函数后,程序将根据对象类型而不是引用或指针的类型来选择方法版本。
2、我们经常会在基类中声明一个虚析构函数,这是为了确保释放派生对象时能按照正确的调用顺序调用析构函数,之后会详细讲解

区别
使用虚函数:程序将根据引用或指针指向的对象类型选择方法
不使用虚函数:程序将根据引用类型或者指针类型选择方法

5、派生类的实现

上面我们说过,如果要访问基类的私有数据,必须通过基类的公有成员函数,这里就说一下派生类成员函数访问基类公有方法的方式,主要分为两类
1、如果派生类中没有重新定义,可以直接访问
2、如果派生类中重新定义了该方法,则需要加上作用域解析符(:

这里分享一个我写的demo,没啥具体含义,只做测试
类定义

#ifndef PASS_H_
#define PASS_H_
#include 
#include 

using std::string;
//基类
class Table_player
{
private:
  string firstname;
  string lastname;
  int years;
  bool hasTable;
public:
  Table_player(const string & fn = "none",const string & ln = "none",bool ht = false,int ye = 0);
  void name() const;
  virtual bool HanTable() const { std::cout << "Table_player hasTable : " << hasTable << std::endl;return hasTable;};
  int Show_years() const { std::cout << "Table_player years : " << years << std::endl; return years;};
  void ResetTable(bool v) {hasTable = v;};
};

class Rated_player : public Table_player
{
private:
  unsigned int rating;s
  unsigned int Table_num;
public:
  Rated_player(unsigned int r,unsigned int t,const string & fn = "none",const string & ln = "none",bool ht = false,int ye = 0);
  Rated_player(unsigned int r,unsigned int t,const Table_player & tp);
  unsigned int Rating() const { return rating;};    //新增成员
  void ResetRating(unsigned int r) {rating = r;};   //新增成员
  virtual bool HanTable();                          //虚函数
};
#endif

类实现

#include "pass_class.h"

using std::string;

//Table_player
Table_player::Table_player(const string & fn,const string & ln,bool ht,int ye):
                                                                firstname(fn),lastname(ln),hasTable(ht),years(ye)
{
  std::cout << "Table_player creat successn";
}

void Table_player::name() const
{
  std::cout << lastname << "-" << firstname << ": " << hasTable << std::endl;
}



//Rated_player
Rated_player::Rated_player(unsigned int r,unsigned int t,const string & fn,const string & ln,bool ht,int ye): Table_player(fn,ln,ht,ye)
{
  std::cout << "Rated_player creat successn";
  rating = r;
  Table_num = t;
}

Rated_player::Rated_player(unsigned int r,unsigned int t,const Table_player & tp): Table_player(tp),rating(r),Table_num(t)
{
  std::cout << "Rated_player creat successn";
}

bool Rated_player::HanTable()
{
  int age = Show_years();
  std::cout << "age = " << age << std::endl;
  if (Table_player::HanTable())
  {
    Table_num++;
    std::cout << "has " << Table_num << " tables n";
    return true;
  }
  else
    std::cout << "has no tables...n";
  return false;
}

使用demo

#include "pass_class.h"
using namespace std;

int main()
{
  Table_player play1("huang","xiaom",1,20);
  Rated_player play2(20,17,play1);
  Rated_player play3(50,15,"zhou","jiel",1,30); 

  play1.name();
  play2.name();
  play3.name();

  play1.Show_years();
  play1.HanTable();

  play2.HanTable();
  return 0;
}

显示结果

Table_player creat success
Rated_player creat success
Table_player creat success
Rated_player creat success
xiaom-huang: 1
xiaom-huang: 1
jiel-zhou: 1
Table_player years : 20
Table_player hasTable : 1
Table_player years : 20
age = 20
Table_player hasTable : 1
has 18 tables 

这里着重看一下virtual函数的多态,派生类通过基类的公有函数访问基类私有数据即可。

为何需要虚析构函数

我们知道,对于使用了动态内存的类来说,析构十分重要,因为要在析构中加上动态内存的释放代码(delete)
我们又知道,基类指针和引用可以在不进行显示类型转换的情况下指向派生类对象,派生类对象可以赋给基类对象
如果有一个基类类型的指针指向了一个派生类对象,而析构函数不用虚函数的方法,就会调用基类的析构,如果在派生类中使用了new,基类中没有使用,就会造成派生类的虚构一直没有被调用,这块内存一直没有被释放。
所以基类的析构函数通常都会使用虚函数

6、静态/动态联编

将源代码中的函数调用解释为指向特定的函数代码快,被称为函数名联编。在C语言中非常简单,因为每个函数名都对应一个不同的函数。

静态联编

而在C++中,由于函数重载的缘故,使得这项任务更加复杂,编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程中完成这种联编。在编译过程中完成联编被称为静态联编,又叫做早期联编。

动态联编

但是,虚函数的出现让静态联编变得十分困难,因为使用哪一个函数要在运行时确定,所以编译器必须生成能够在程序运行时选择正确需办法的代码,这就是动态联编

指针和引用类型的兼容性

C++中,动态联编与通过指针和引用调用方法有关。
通常,C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型,但之前我们说过,指向基类的引用或指针可以指向派生类对象,这也是公有继承is-a关系的一个特征。

将派生类的指针或引用转换为基类的引用或指针称为向上强制转换,是不需要显示类型转换。
相反,指向基类的引用或指针转换为派生类的指针或引用称为向下强制转换,必须要进行显示的类型转换

对于非虚函数,即在编译阶段就能确定调用哪个类的成员函数,编译器会使用静态联编
而对于虚函数,即在编译阶段不能确定使用哪个类的成员函数,就会使用动态联编

静态/动态联编的优劣

动态联编能够让我们重新定义类方法,而静态联编不行,但目前仍旧保留静态联编的原因有两点
1、效率:静态联编的效率相比动态联编更高,而且应用场合也比较广,所以此被设置为C++的默认选择
2、概念模型:指出不需要被重新定义的函数

7、访问控制:protected

目前我们已经介绍了两种访问控制的关键字:private、pubilc
除此之外还存在一种访问类型:protected。protected与private类似,在类外只有通过公有类成员访问,而区别在于派生类中。
派生类的成员可以直接访问基类的protected成员,但不能直接访问私有成员。
对于外界来说,保护成员和私有成员类似,对于派生类来说,基类保护成员与公有成员类似

通常我们对类的数据成员会采用私有访问控制,不会使用保护访问,而对成员函数使用保护访问控制很有用,可以放派生类访问公众不能使用的内部函数

8、抽象基类

抽象基类的标志就是拥有纯虚函数,纯虚函数也可以叫抽象函数,一般来说它只有函数名、参数和返回值类型,不需要函数体。这意味着它没有函数的实现,需要让派生类去实现
在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
在派生类中,必须对纯虚函数予以重写以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。
纯虚函数如下

virtual void Hasmoney() const = 0;

纯虚函数的结尾处为"=0"

9、继承和动态内存分配 1、基类只用new,派生类不使用

通过前面的介绍,我们知道了在类中如果使用了动态内存分配,就需要定义析构、复制构造函数和重载赋值运算符。
如果在派生类中不使用动态内存分配,则不需要显示定义析构、复制构造函数及重载赋值运算符
析构:首先调用派生类的析构,因为不使用new,所以没有额外 *** 作。之后调用基类的析构
复制构造函数:成员赋值将根据数据类型采用相应的复制方式,所以派生类采用派生类的默认构造函数,基类采用定义的复制构造函数

2、基类只用new,派生类使用new

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

总结一下:使用基类的方法处理基类元素,使用派生类的方法处理派生类元素

3、派生类访问基类的友元

想访问基类的友元时,必须使用基类类型,这是在派生类中可以使用强制类型转换,让编译器在匹配原型时能选择正确的函数,比如在基类和派生类中都重载了"<<"运算符的友元,在派生类中可以这么写

os << (const base & )hs;

这样就能使用基类的友元

三、整体回顾

下面我们回顾一下之前讲过的东西,整体总结一下

1、基本方面

1、编译器生成的函数,都为默认函数,没有定义时使用:默认构造、默认赋值函数
2、赋值运算符:使用语句创建新的对象使用初始化,语句修改已有对象的值为赋值,默认赋值是成员赋值,如果成员为类对象,则使用对应类类型中定义的赋值运算符。通常需要显式定义复制构造函数时,也需要显式定义赋值运算符。其中还有转换函数
3、构造函数不被继承,派生类需要定义直接的构造,并且在构造函数中使用初始化成员表的方式调用基类构造
4、如果使用new,必须显式定义析构,对于基类,需要将其变为虚函数
5、转换函数:使用一个参数就可以调用的构造定义了转换函数,带有explicit关键字的构造函数禁止隐式转换,能显式转换

6、按值传递与按引用传递参数:使用对象作为参数时,应该使用引用而不是值,一是为了提高效率,不同再生成临时拷贝,即调用复制构造函数,结束时调用析构函数,如果不修改时还应加上const限定符。还有一个原因就是在集成使用虚函数时,接受基类引用参数的函数可以接受派生类对象。

7、返回对象和引用:返回对象和按值传递参数比较相似,涉及到返回对象的临时副本,同样也需要调用复制构造函数和析构函数,返回引用可以节约时间和内存。返回引用和按对象传递都是对同一对象 *** 作。返回对象会在调用函数中生成一个临时副本,再根据我们的代码将这个副本赋值给某个变量
但返回对象时不能返回在该函数中创建的,因为其结束时临时对象就会消失。这种情况就需要返回对象,以生成一个调用程序可以使用的副本。
原则就是:函数返回在函数中创建的临时对象,不使用引用。如果函数返回通过引用或指针传递给它的对象,使用引用。

8、const:使用const时需要确认改参数不会被修改,也可以在函数返回类型前加const,确保返回的值不能修改。在成员函数的声明后加const,可以保护改函数隐式访问的类对象不被修改(*this)

2、继承

1、要遵循is-a的关系,如果两个类没有此关系,不应该使用公有派生,这个情况最好的办法就是创建一个抽象类,在由这个抽象类派生出其他的类。
2、is-a的关系定义了,基类指针或引用无需显示类型转换就能指向派生类对象或指针,反过来则需要进行显示转换(向下强制转换)
3、不能被继承的成员:构造、析构、赋值运算符(赋值运算符的特征表与派生类不同),派生类继承的方法特征标需要与基类完全相同
4、如果派生类使用了new,需要提供显式的赋值运算符。
赋值运算符将被转换成左边对象的调用方法,如将派生类赋值给基类,会调用基类的赋值运算符,只处理基类成员,忽略派生类成员,所以可以将派生类对象直接赋值给基类对象,无需进行显示转换。相反将基类赋值给派生类,因为指向派生类的引用不能自动转换成基类引用,所以无法执行,除非有转换构造函数,将右侧的基类转换成派生类对象
5、派生类的转换函数可以接受一个类型为基类的参数和其他参数,前提是其他参数有默认值
6、私有成员:私有成员对于外部和派生类都无法访问,只能通过公有方法
保护成员:对于外部和私有成员一样无法访问,对于派生类可以访问,常用于成员函数的保护
公有成员:外部与派生类都可访问
7、虚方法:如果希望派生类能重新定义该方法,则应该定义为虚函数。如果没有定义为虚函数而在派生类中重写,则在该派生类中会覆盖调基类的所有同名函数
8、析构函数必须要为虚方法
9、友元函数:友元函数并非类成员,所以不能继承,但能访问该类的私有成员。如果希望在派生类的友元中访问基类的友元,可以通过强制类型转换,将派生类引用转换为基类引用

3、基类方法的使用

1、派生类自动继承基类方法,前提没有重新定义该方法,否则会覆盖基类中所有的同名方法
2、派生类的构造/析构自动调用基类的构造/析构
3、派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数,通常需要显式调用成员初始化列表来指定基类构造函数,并且确保执行的顺序,先执行基类的构造,再执行派生类的构造
4、派生类的友元可以通过强制类型转换来使用基类的友元

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5690881.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存