第15章学习笔记

15.1 OOP(object-oriented programming)概述

面向对象程序设计(OOP)核心思想是数据抽象、继承和动态绑定数据抽象将类的接口与类的实现分离;继承可以定义相似的类型,并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,以统一方式使用它们的对象。

数据抽象

数据抽象时一种依赖于接口(interface)和实现(implemention)分离的编程技术。类的接口包括用户所能执行的操作(类的头文件.h,此处的头文件表示只包含类内成员声明的类定义);类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数(类的源文件.cpp定义头文件声明的各个函数的函数体)。封装实现了类的接口和实现的分离(访问权限设置),封装后的类隐藏了它的实现细节,即,类的用户只能使用接口而无法访问实现部分。

继承

可以通过继承基类来定义其派生类。派生类既具有基类的成员(功能)也能定义其自己特有的成员(功能)。在c++中,派生类可以直接继承其基类所有成员,但是对于某些函数,派生类希望定义适合自身的版本。为了满足这个要求,可以在基类将这部分函数声明成虚函数virtual。以下为一个例子:

为了满足书店中不同书籍的定价策略,定义两个类,一个quote类,其对象表示按原价销售书籍,作为基类;一个bulk_quote类,其对象表示打折销售的书籍,作为派生类。它们包含两个成员函数:

  • isbn(),返回书籍的ISBN编号。该操作不涉及派生类的特殊性,因此只定义在quote类中。
  • net_price(size_t n),返回书籍的实际销售价格,前提时用户购买该书的数量达到一定标准。这个操作显然是类型相关的,quote和bulk_quote类都应该包含该函数,而且在两个类中,其函数体也有所不同,因此应该在基类中声明成virtual。

因此,可以将quote类定义为:

class quote //只定义了类接口,先不定义实现部分

{

public:

扫描二维码关注公众号,回复: 4872307 查看本文章

         std::string  isbn() const;

         virtual double net_price(size_t n) const;//virtual声明表示在其派生类中可以定义相同函数和形参列表的函数对基类函数进行覆盖

};

派生类必须通过使用类派生列表明确指出它从哪个基类继承而来。即:

class bulk_quote:public quote//类派生列表:首先是一个冒号,然后紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问说明符。

{

public:

           double net_price(size_t n) const override;//使用override表示对基类的虚函数进行覆盖

};

派生类必须在其内部对所有需要重新定义的虚函数进行声明,c++新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施时在该函数的形参列表之后增加一个override。如果没有声明,将继承基类的函数。

动态绑定

 通过使用动态绑定,我们能用同一段代码分别处理quote和bulk_quote对象。当使用基类的引用或指针调用一个虚函数时,将发生动态绑定。动态绑定在运行时选择函数的版本。

note:在c++语言中,当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定。

 15.2 定义基类和派生类

15.2.1定义基类

class quote

{

public:

       quote()=default;

       quote(const std::string  &book,double sales_price): bookno(book),price(sales_price){}

      std::string isbn() const{ return bookno;}

      virtual double net_price(std::size_t n) const    {return n*price;}

      virtual ~quote()=default;

private:

      std::string  bookno;

protected:

      double price=0.0;

};

任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明为虚函数,则该函数在派生类中隐式的也是虚函数,通常会用override来表示派生类的函数覆盖了基类的虚函数。

成员函数如果没被声明成虚函数,则其解析过程发生在编译时而非运行时,若声明为虚函数,则其解析过程发生在运行时而非编译时。

访问控制与继承

派生类能访问基类的公有成员和保护成员,但不能访问基类的私有成员。

15.2.2定义派生类

派生类必须通过派生列表明确指出从哪个基类继承而来。每个基类前面有三种访问说明符:public\protected\private。这些说明符的作用是用来控制派生类从基类继承而来的成员是否对派生类的用户(即派生类的对象)可见,对派生类没有任何影响。

我们能将公有派生的对象绑定到基类的引用或指针上。

派生类中的虚函数

派生类经常override它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,直接继承基类的版本。注意,override关键字一般位于const关键字后面、引用限定符的后面。

 派生类对象及派生类向基类的类型转换

将基类的指针或引用绑定到派生类对象中的基类部分,这种转换称为派生类到基类的类型转换。编译器会隐式地执行派生类到基类的转换,其意味着,可以把派生类对象的引用或指针用在需要基类引用或指针的地方

 note:在派生类对象中含有与基类对应的组成部分,这一事实是基类继承的关键所在。

 派生类构造函数

派生类必须使用基类的构造函数来初始化它的基类部分。派生类构造函数通过构造函数初始化列表将实参传递给基类构造函数。例如:

bulk_quote(const std::string &book,double p,std::size_t q,double disc):quote(book,p),min_qty(q),discount(disc) {}

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。静态成员遵循通用的访问规则,详情可见第七章。

派生类的声明

声明只包含类名,不包含派生列表,即

class bulk_quote:public quote;//是错误的,声明不包含派生列表

class bulk_quote;//正确

 被用作基类的类

如果想将某个类用作基类,则该类必须以及定义,而非仅仅声明!因为派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道它们是什么。因此这个规定隐含的意思就是,一个类不能派生它本身。

 最终的派生类将包含它的直接基类(派生类继承的类)的子对象以及每个间接基类(直接基类继承的类)的子对象。

防止继承的发生

c++11提供一种防止继承发生的方法,即在类名后跟一个关键字final. 意思是其他类不能继承它,它不能作为基类。

class bulk_quote final:quote{/*...*/};//bulk_quote不能作为基类

class disc:bulk_qoute{/*...*/};//错误,不能继承bulk_qoute!

15.2.3类型转换与继承

 我们可以将基类的指针或引用绑定到派生类对象上,因此,当使用基类的引用或指针时,实际上我们并不清楚该引用或指针所绑定对象的真实类型。同时,智能指针和内置指针一样,也支持派生类向基类的类型转换,即可以将派生类指针存储在一个基类的智能指针内。

静态类型与动态类型

当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型该表达式表示对象动态类型区分开来。表达式的静态类型编译时总是已知,它是变量声明时的类型或表达式生成的类型。动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知。

 例如:

quote & item;//item的静态类型是quote

bulk_quote &i;//i的静态类型时bulk_quote

item=i;//item的动态类型时bulk_quote

如果表达式既不是引用也不是指针,则动态类型永远与静态类型一致。

不存在从基类到派生类的类型转换。之所以存在派生类向基类的转换是因为每个派生类都包含一个基类部分,但是反过来,一个基类的对象可能是派生类的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。即使一个基类的指针或引用绑定到一个派生类对象上,也不能利用此基类向派生类的转换。这是因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法!

warning:当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

15.3虚函数

当使用基类的引用或指针调用一个虚函数成员时,会执行动态绑定,隐含着使用基类的引用或指针可以进行派生类到基类的类型转换,如果在这个情况下调用虚函数,那么将无法得知调用的虚函数到底是派生类的函数还是基类的函数,所以会执行动态绑定,即在运行时解析函数的调用,因为只有在运行时,才能解析引用或指针绑定的对象是什么类型,并调用其类型下的函数。

因此,必须明白,动态绑定只有通过指针或引用调用虚函数时才会发生。当通过一个普通类型的表达式调用虚函数时,在编译时就会将调用的版本确定下来(即静态类型)。

派生类的虚函数

 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。

同样,派生类中虚函数的返回类型也必须与基类函数匹配。但有一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。

 如果派生类定义了一个函数与基类虚函数名字相同但形参不同,这也是合法行为,但编译器认为这是新定义的函数,并没有覆盖掉基类的版本,因此如果将这个新定义的函数名后加入override或final说明符,将发生编译错误。

虚函数与默认实参

如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定!也就是说,如果通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的时派生类中的函数版本也是如此。

best practice:如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

回避虚函数机制

在某些情况下,希望对虚函数的调用不进行动态绑定,而强迫执行虚函数的某个特定版本,使用作用域运算符。

double undiscounted=basep->quote::net_price(42);//强行调用基类中定义的函数版本而不管basep的动态类型到底是什么。

 

15.4抽象基类

纯虚函数

在类内部声明一个虚函数=0说明为纯虚函数,只能出现在类内部的虚函数声明语句处。如果要定义纯虚函数,必须在类的外部定义。

含有纯虚函数的类是抽象基类

含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,后续的其他类覆盖该接口。我们不能直接创建一个抽象基类的对象。

15.5访问控制与继承

受保护成员

  • 受保护成员对于类的对象来说是不可访问的
  • 受保护的成员对派生类的成员和友元是可访问的。
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问权限。

公有、私有、受保护继承

某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。

派生访问说明符对于派生类的成员和友元能否访问直接基类的成员没什么影响,对基类成员的访问权限只与基类中的访问说明符有关。

派生访问说明符是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。

例:

派生类向基类转换的可访问性

  • 只有当派生类公有继承基类时,用户代码才能使用派生类向基类转换
  • 不论派生类以什么方式继承基类,派生类的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远时可访问的
  • 如果派生类继承基类的方式时公有或受保护,则派生类的派生类的成员和友元可以使用派生类向基类的类型转换。

友元与继承

友元关系既不能传递也不能继承,因此基类的友元,不能访问派生类成员,但是可以访问派生类的基类部分成员。同样的,派生类的友元不能访问基类的成员,也不能访问派生类基类部分的成员。

例:

class base
{
protected:
int prot_mem;
friend class pal;
};
class sneaky:public base
{
int j;
};
class pal
{
public:
int f(base b) {return b.pub_mem();}//正确:palbase的友元
int f2(sneaky s){return s.j;}//错误:pal不是sneaky的友元
int f3(sneaky s){return s.prot_mem;}//正确:palbase的友元
};

改变个别成员的可访问性

有时需要改变派生类的某个名字的访问级别,使用using声明:

class base
{
protected:
size_t n;
public:
size_t size() const {return n;}
};
class derived: private base
{
public:
using base::size;
protected:
using base::n;
};

使用using改变继承成员size和n后,derived的用户可以使用size成员,derived的派生类可以使用n。注意,派生类只能为那些它可以访问的名字提供using 声明。

默认继承保护级别

使用class定义的派生类默认私有继承,使用struct定义的派生类默认公有继承。

15.6继承中的类作用域

当存在继承关系时,派生类的作用域嵌套在其基类的作用域内,如果一个名字在派生类的作用域无法解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致,但我们能使用哪些成员仍然是由静态类型决定的。

名字冲突与继承

和其他作用域一样,派生类可以重用定义在其直接基类或间接基类中的名字,此时,定义在内层作用域的名字将隐藏定义在外层作用域的名字。也就是说,派生的成员将隐藏同名的基类成员。

struct base
{
base():mem(0){}

protected:
int mem;
};
struct derived :base
{
derived(int i):mem(i){}//初始化derived::mem,同时base::mem会默认初始化
int get_mem() { return mem;}//返回derived::mem

protected:
int mem;//隐藏基类中的mem
};

但是,我们可以通过作用域运算符来使用一个被隐藏的基类成员:

struct derived :base
{
derived(int i):mem(i){}//初始化derived::mem,同时base::mem会默认初始化
int get_mem() { return base::mem;}//返回base::mem

protected:
int mem;//隐藏基类中的mem
};


关键概念:
理解函数调用的解析过程对于理解c++的继承至关重要,假定我们调用p->mem()(或者obj.mem()),则依次执行以下4个步骤:
  • 首先确定p(或obj)的静态类型。如果找不到,则依次在静态类型的直接基类中不断查找,直到到达继承链的顶端,如果找遍了该类及其基类仍然找不到,则编译器将报错。
  • 一旦找到了mem,就进行常规的类型检查,以确认对于当前找到的Mem,本次调用是否合法。
  • 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码!

 名字查找先于类型检查

 声明在内层作用域的函数并不会重载声明在外层作用域的函数,如果派生类的成员与基类成员同名,则派生类将其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员仍然被隐藏。同时,名字一旦找到,编译器就不再继续查找。

覆盖重载的函数

 成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。

有时一个类仅需要覆盖重载集合中的一些而非全部函数,则可以使用using声明,这样就无需覆盖基类中的每一个重载版本了。using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。此时,派生类只需要定义其特有的函数就可以了,而无需为继承而来的其他函数重新定义。基类函数的每个实例在派生类中该都必须是可访问的。

15.7构造函数与拷贝控制

15.7.1虚析构函数

继承关系对基类拷贝控制最直接的影响时基类通常应该定义一个虚析构函数,这样就能动态分配继承体系中的对象了。

只要基类的析构函数是虚函数,就能确保当我们delete基类指针时将运行正确的析构函数版本。

如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。

虚析构函数将阻止合成移动操作。如果一个类定义了析构函数,编译器不会为这个类合成移动操作。

15.7.2合成拷贝控制与继承

基类或派生类的合成拷贝控制成员,对类本身的成员依次进行初始化、赋值或销毁。此外,这些合成的成员还赋值使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作。比如,合成的派生类的默认构造函数运行直接基类的默认构造函数,后者有运行其直接基类的默认构造函数。值得注意的是,无论基类成员时合成的版本还是自定义的版本都没有太大影响,唯一的要求是相应的成员应该可访问并且不是一个被删除的函数。

派生类中删除的拷贝控制与基类的关系

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
  • 如果基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的。
  • 编译器将不会合成一个删除掉的移动操作。

15.7.3派生类的拷贝控制成员

和构造函数及运算符不同,派生类析构函数只负责销毁派生类自己分配的资源。当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。

派生类对象的基类部分首先被构建。当执行基类的构造函数时,该对象的派生类部分是未被初始化的状态。类似的,销毁派生类对象的次序正好相反

当执行基类的析构函数时,派生类部分已经被销毁掉了。

15.7.4继承的构造函数

派生类能改重用直接基类定义的构造函数。不能继承默认、拷贝、移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的Using 声明语句。

例:class bulk_quote:public disc_quote

{

public:

       using disc_quote::disc_quote;

};

一个构造函数的using声明不能改变其访问级别。当一个基类的构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。例:如果一个基类有一个接受两个形参的构造函数,其中第二个形参含有默认实参,则派生类将获得两个构造函数,一个构造函数接受两个形参(没有默认实参),一个构造函数只接受一个形参,对于与基类中没有默认至的哪个形参。

如果基类含有几个构造函数,除了两个例外情况,大多是时候,派生类会继承所有这些构造函数。第一个例外时派生类可以继承一部分构造函数而为其他构造函数定义自己的版本(需要具有相同的形参列表)第二个例外是默认、拷贝、移动构造函数不会被继承。

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自www.cnblogs.com/excellentlhw/p/10252795.html