写点什么

C++ primer -- 第十三章 类继承

用户头像
Dreamer
关注
发布于: 2020 年 11 月 02 日
C++ primer -- 第十三章 类继承

以下的内容是阅读《C++primer》第六版的十三章所作的笔记,仅供自己学习所用。

本章的重点内容如下:


  • is-a 关系的继承

  • 如何以公有方式从一个类派生出咯 ing 一个类

  • 保护访问

  • 构造函数陈谷元初始化列表

  • 向上和向下强制类型转换

  • 虚成员函数

  • 早期(静态)联编与晚期(动态)联编

  • 抽象基类

  • 纯虚基类

  • 何时以及如何使用公有继承




1 派生类的构造函数


  • 首先创建基类对象

  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数

  • 派生类构造函数应初始化派生类新增的数据成员


如果没有显示的提供基类的构造函数,会隐式调用基类的构造函数,释放的顺序和构造的顺序相反,即先执行派生类的析构函数,然后自动调用基类的析构函数。基类的构造函数负责初始化继承的数据成员,派生类的构造函数初始化新增的数据成员。


1.1 派生类与基类之间的特殊关系


派生类与基类之间的特殊关系:


  • 派生类的对象可以使用基类的方法,条件是方法不是私有的

  • 基类指针可以不进行显式转换的情况下指向派生类对象

  • 基类引用可以不进行显式类型转换的情况下引用派生类对象


注意,基类指针或引用只用用于调用基类的方法,不能用于调用派生类的方法。


通常情况下,C++中要求引用和指针的类型与赋给的类型匹配,但是在继承时时个例外。但是这种例外时单向的,不可以将睫类对象和地址赋值给派生类的引用和指针


2 继承: is-a 关系


C++有三种继承方式:公有继承,私有继承,保护继承。其中公有继承最为常见,且时 is-a 关系。对于其他的 has-a, uses- a, has implemented 理论上都可以实现,但是没有意义。


3 多态公有继承


当有一个需求时在派生类和在基类中的行为是不同的时,称为多态。有两种重要的机制可以实现多态。


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

  • 使用虚方法


对于使用虚函数来实现多态。由于前面提到可以使用基类的指针和引用来指向派生类的对象。如果的方法是通过引用或者指针调用的,而不是对象调用的,它将确定使用哪一种方法。此时,如果没有受用关键字 virtual,程序将根据引用类型或指针类型来选择方法,如果使用了 virtual 关键字,将根据引用或指针指向的对象的类型来选择方法。


常见的做法是在基类中将派生类中会重新定义的方法声明为虚方法。方法在基类中声明为虚后,在派生类中自动称为虚方法。


注意,对于虚函数,需要受用相应的虚析构函数来释放相应的呢困,如果,析构函数不是虚的,将调用指针指类型的析构函数。


4 静态联编和动态联编


将源代码中函数调用解释为执行待定的函数代码块被称为函数名联编(binding).在编译过程中 binding 称为静态联编编译器必须生成能够在程序运行时选择正确的虚函数的代码,称为动态联编(dynamic binding).


指针和引用类型的兼容性


C++中动态联编与通过指针和引用调用方法相关。从某种成程度来说,由继承控制。


将派生类引用或指针转换成鸡肋引用或指针称为向上强制类型转换(upcasting).这是由于继承的机制来实现。根据继承机制很容易得到一个性质,这种 upcasting 是可以向上传递的。


相反的过程,将积累的指针或引用转换成派生类的指针或引用(downcasting),如果不使用显式的类型转换,downcasting 是不允许的。原因是继承的 is-a 关系是不可逆的。


在大多数的情况下,动态联编很好,因为它让程序能够选择为特定类型设计的方法,但是两种不同的 binding 方式都是必要的。


首先,看效率。采用动态联编,编译器需要采用一些特定的方法来跟踪指针的变化。而静态联编不需要这些操作,静态联编的效率更高


然后是概念模型。设计类的时候,可能包含一些派生类重新定义的成员函数。不设置为虚函数,可以无需在派生类中再次定义。


虚函数的工作原理


C++规定了虚函数的行为,但是将实现方法留给了编译器作者。通常编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table, vtbl).vtbl 中存放了类对象进行声明的虚函数的地址。无论虚函数添加几个,都只在对象中添加一个地址成员。


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


  • 每个队形都将增大,增大量为存储地址的空间

  • 对于每个类,编译器都创建一个虚函数地址表

  • 对于每个函数的调用,都需要执行一项额外的操作,即到表中查找地址


虽然非虚函数的效率高,但是不具备动态联编的功能


虚函素注意事项


1.构造函数


构造函数不能说虚函数,创建派生类对象时,调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数


2.析构函数


析构函数应当时虚函数,除非类不用做基类。通常应给基类定义一个虚析构函数,即使它并不需要析构函数


3.友元


友元不是析构函数,只有类成员函数才能时虚成员函数。


4.没有重新定义


如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。


5.重新定义将隐藏方法


两条经验规则:

  • 如果重新定义继承的方法,应确保与原来的原型完全形同。但如果返回类型是基类的引用或指针,则应修改为指向派生类的引用或指针,这种特性被称为返回类型协同


class Dwelling

{

public:

// a base method

virtual Dwelling & build(int n);

...

}

class Hovel: public Dwelling

{

public:

virtual Hovel & build(int n); // same function signature

...

}

  • 如果基类被虫子啊类,则汀钙在派生类中重新定规所有的基类版本


5 访问控制: protected


关键字 protected 和 private 相似,在类外只能用公有类成员函数来访问 protected 中的类成员。其区别只有在基类派生的类中才会表现出来。派生类的成员可以访问 protected 的类成员,但不能访问基类的私有成员。对于外部来说,保护成员的行为与私有成员相似。单丝对于派生类,保护成员与公有成员相似。


6 抽象基类

包含纯虚函数的类就是抽象基类。定义为抽象基类的类无法创建对象。


7 继承和动态内存分配


  1. 情况一:派生类不使用 new

假设基类使用了动态内存分配,而派生类不使用 new。此时不需要为类显示定义析构函数,复制构造函数,和赋值构造函数


  1. 情况二:派生类使用 new

基类使用 new,派生类使用 new,必须显示调用析构函数,复制构造函数,和赋值构造函数


//base class using DMA

class baseDMA

{

private:

char * label;

int rating;

public:

baseDMA(const char * l = "null", int r = 0);

baseDMA(const baeDMA & rs);

virtual _baseDMA();

baseDMA & operator=(const baseDMA & rs);

...

};

//derived class with DMA

class hasDMA: public baseDMA

{

privite:

char * style; //use new in constructors

...

public:

...

};

析构函数的实现:


baseDMA::~baseDMA() // takes care of baseDMA stuff

{

delete [] label;

}

hasDMA::~hasDMA()

{

delete [] style;

}

复制构造函数的实现:


baseDMA::baseDMA(const baseDMA & rs)

{

label = new char[std::strlen(rs.label)+ 1];

std::strcpy(label, ts.label);

rating = rs.rating;

}

hasDMA::hasDMA(const hasDMA & hs)

:baseDMA(hs)

{

style = new char[std::strlen(hs.style)+1];

std::strcpy(style, hs.style);

}

复制构造函数的实现:


baseDMA::operator=(const baseDMA & rs)

{

if (this == rs)

return *this;

delete [] char;

label = new char[std::strlen(rs.label)+ 1];

std::strcpy(label, rs.label);

rating = rs.rating;

return *this;

}

hasDMA & hasDMA::operator=(const hasDMA & hs)

{

if(this == &rs)

return *this;

baseDMA::operator=(hs); // copy base portion

delete [] style; // prepare for new style

style = new char[std::strlen(hs.style)+1];

std::strcpy(style, hs,style);

return *this;

}

以上,针对三种不同的函数,使用不同的方法:

  • 析构函数: 自动完成

  • 构造函数:初始化成员列表

  • 运算符复制:作用域解析运算符


公有继承的考虑因素


在程序中使用继承时,有很多问题需要注意:


  1. is-a 关系

is-a 关系无需进行显示类型类型,基类指针就可以指向派生类对象,基类引用可以引用派生类对象

  1. 什么不能被继承

构造函数不能被继承,也就是说派生类创造类对象的时候,必须调用派生类的构造函数,而派生类的构造函数通常使用成员初始化列表来调用基类的构造函数,一创建派生类对象的基类部分。如果派生类没有使用成员初始化列表显示调用基类的构造函数,将使用基类的默认构造函数。


析构函数也是不能被继承的,释放对象的时候没程序首先调用派生类的析构函数,然后调用基类的析构函数。如果基类有默认的析构函数,编译器将为派生类生成默认析构函数。


  1. 赋值运算符


如果编译器将一个对象赋值给同一个类的另一个对象,它将自动为这个类提供一个复制运算符。这个运算符默认或隐式的采用成员赋值。如果说派生类对象,编译器将使用基类哦赋值运算符来处理派生类对象的基类部分的赋值。


如果派生类使用来 new,则必须提供显示赋值运算符的调用版本实现。


  1. 私有成员和保护成员


对派生类而言,保护成员类似公有成员,对外部而言,保护成员和私有成员类似,派生类可以直接访问基类的保护成员,但只能通过基类的成员函数来访问私有成员。


  1. 虚方法


如果希望在派生类中能够冲重新定义方法,则将其声明为虚函数,这样可以启动晚期联编。反之,则不需要定义为虚的。


  1. 析构函数


当指向对象的基类指针或引用来删除派生类对象的时候,程序将首先掉一哦那个派生类的析构函数,然后调用基类的析构函数,而不是仅仅调用基类的构造函数


  1. 友元函数


友元函数不是成员函数,不能继承,当希望派生类的友元函数能够使用基类的友元函数时,可以通过强制类型转换。


  1. 有关基类方法的说明


  • 派生类对象自动使用继承来的基类方法

  • 派生类对象构造函数自动调用基类的构造函数

  • 派生类对象自动调用基类的默认构造函数

  • 派生类构造函数显示调用成员初始化列表中指定的基类方法

  • 派生类方法可以使用作用域解析运算符来调用公有和受保护的基类方法

  • 派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换成基类的引用或指针


用户头像

Dreamer

关注

一个不想做搜索的NLPer不是一个好的CVer 2019.12.18 加入

还未添加个人简介

评论

发布
暂无评论
C++ primer -- 第十三章 类继承