12. 面向对象程序设计

41 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

☔️15.1 OOP:概述

继承

通过继承,联系在一起的类构成一种层次关系

  • 基类:定义共同拥有的成员
  • 派生类:定义特有的成员(是从基类直接或间接的继承过来的)
  • 虚函数:基类希望派生类各自自定义自己合适的版本

派生列表

  • 首先是一个冒号,然后是以逗号分隔的基类列表,每个基类前面可以有访问说明符。
  • 派生类必须通过使用类派生列表明确指出基类,因为一个类可以有多个基类
class Student : public Class {};   //Class:基类,Student:派生类
  • 派生类必须在内部对所有重新定义的虚函数进行声明,声明时可以在前面加上 virtual,也可以不加。
  • C++11 允许使用 override 关键字显式地指明重新定义的虚函数,把 override 放到形参列表后面。(当派生类写错时,编译器会报错如果不写override的话,编译器以为派生类自己定义了一个新函数,所以不会报错)
class Quote{
public:
	std::string isbn() const;  //派生类完全继承基类,和基类的行为一样
	virtual double net_price(std::size_t n) const; //定义了虚函数,则派生类可以有自己的版本

};
class Bulk_quuote:public Quote{
public:
	double net_price(std::size_t) const override; //override是重写,需要修改基类的信息

}

动态绑定

  • 当使用基类的引用或指针来调用一个虚函数时将发生动态绑定。
  • 动态绑定根据传入的参数类型来选择函数版本(可能是基类中的该函数或派生类中的该函数),它发生在运行时,又称运行时绑定。 在这里插入图片描述

☔️15.2 定义基类和派生类

⛄️15.2.1 定义基类

  • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
  • 基类通过在其成员函数的声明语句前加上关键字virtual使得该函数执行动态绑定(解析过程发生在运行时),而成员函数没有被声明为虚函数。则解析过程发生在编译时而非运行时
  • 基类通过虚函数区分两种成员函数:
    1. 基类希望派生类进行覆盖的函数,为虚函数。构造函数与静态函数都不能定义成虚函数任何构造函数之外的非静态函数都可以定义为虚函数。
    2. 基类希望派生类直接继承不要改变的函数。

注意:

  • 使用指针或引用调用虚函数时,该调用将被动态绑定。
  • 如果基类把一个函数声明为虚函数,该函数在派生类中隐式地也是虚函数(c++11派生类的中不用写virtual,而是直接写override)
  • virtual只能出现在类内部声明语句之前,而不能用于类外部函数定义。

访问控制与继承

  • 派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
  • 派生类能访问基类的公有成员和受保护成员,不能访问私有成员。

在这里插入图片描述

⛄️15.2.2 定义派生类

  • 派生类需要派生列表明确指出它是从哪个基类继承来的
  • 类派生列表:首先是一个冒号,然后是以逗号分隔的基类列表,每个基类前面可以有访问说明符。
    • 访问说明符包括:public, protected, private

在这里插入图片描述

派生类中的虚函数

  • 派生类经常覆盖它继承的虚函数。如果没有覆盖,派生类会直接继承其在基类中的版本。
  • C++11 允许使用 override 关键字显式地指明重新定义的虚函数,把 override 放到形参列表后面、或 const 成员函数的 const 关键字后面、或引用成员函数的引用限定符后面。

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

  • 一个派生类对象有多个组成部分:

    • 一个含有派生类自己定义的成员的子对象,
    • 一个与该派生类继承的基类对应的子对象。
  • 因为派生类对象中含有与基类对应的组成部分,所以可以把派生类的对象当成基类对象来使用,也能把派生类的指针或引用用在需要基类指针的地方。

理解

  • 派生类是大队长覆盖面大,而基类是小队长覆盖面小,覆盖面大的可以转换为覆盖面小的(类似非const成员覆盖面大为大队长,而const覆盖面小为小队长,那么可以从非const转换为const,反之不行
Quote item;         //基类对象
Bulk_quote bulk;    //派生类对象
Quote *p = &item;   //定义基类指针
p = &bulk;          //指向派生类的基类指针(隐式转换)
Quote &r = bulk;    //绑定到Quote部分

在这里插入图片描述 派生类构造函数

  • 派生类不能直接初始化从基类继承来的成员,而是使用基类的构造函数来初始化它的基类部分。
  • 每个类控制自己的成员初始化过程。派生类构造函数通过构造函数初始化列表将实参传递给基类构造函数。
Bulk_quote(const std::string &book, double p, std::size_t qty,double disc)
:Quote(book, p), min_qty(qty), discount(disc)( )
//Quote就是从基类继承过来的,必须使用基类的构造函数进行初始化,剩下俩个是派生类独有的,直接调用派生类的构造函数
  • 默认情况下,派生类对象的基类部分会像数据成员一样默认初始化。如果要使用其他的基类构造函数,需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。(Quote(book, p))
  • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

派生类使用基类的成员 在这里插入图片描述

继承与静态成员

  • 基类的静态成员只存在唯一一个实例,不论有多少派生类。(类似太阳)
  • 如果静态成员是可访问的,派生类也能使用它,如果是private的,则派生类无权访问
Class Base{
public:
	static void statment();
};

Class Derived:public Base{
	void f(const Derived&);
}

void Derived::f(const Derived &derived_obj)
{
	Base::statment();     //直接通过基类访问,无须创建对象
	Derived::statment();  //直接通过派生类访问,无须创建对象
	derived_obj.statment(); //使用派生类对象
	statment();             //使用this指针
}

派生类的声明

  • 派生类的声明不包含派生列表(定义包含)。
class Bulk_quote:public Quote;   //错误
class Bulk_quote;                //正确

被用作基类的类

  • 如果想要某个类用作基类,`则该类必须已经定义而非仅仅声明
  • 继承可以多重继承,最终的派生类将包含它的直接基类的子对象和每一个间接基类的子对象。
  • 一个类的基类,同时也可以是一个派生类
class Base;       //错误声明但没有定义,不能作为基类
class Bulk_quote:public Qupte{...};  //正确

'多重继承'
class Base{ };
class D:public Base{ };
class D2:public D1 { };

防止继承

  • 如果定义了一个类并不希望它被其他类继承,可以在类名后跟一个关键字 final

在这里插入图片描述

⛄️15.2.3 类型转换与继承

  • 当使用基类的引用或指针时,实际上我们并不清楚它所绑定对象的真实类型,可能是基类对象也可能是派生类对象。

静态类型与动态类型

  • 形参的类型是静态的,实参的类型是动态的。只有当跑起来之后,在内存中调用相应的函数,此时内存中才会有一个对象,这个对象可能是基类可能是派生类 在这里插入图片描述

不存在从基类向派生类的隐式类型转换(但是可以强制转换)

  • 派生类可以向基类转换是因为派生类对象中包含基类部分,而基类的引用或指针可以绑定到该基类部分上,反过来是不行的。·
Bulk_quote bulk;       //派生类
Quote *p = &bulk;      //将基类的指针绑定到派生类
Bulk_quote *bulkp = p  //错误:不能将基类绑定到派生类上

在对象之间不存在类型转换

  • 在对象间不存在类型转换的,只是派生类的指针和引用可以隐式转换为基类
  • 当初始化或赋值一个类类型的对象时,实际是调用构造函数或赋值运算符。他们通常包含一个参数:该参数类型是类类型的 const 引用。此时是可以将派生类对象赋值给基类对象的,实际运行的是以引用作为参数的赋值运算符。 在这里插入图片描述
  • 当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉

☔️15.3 虚函数

  • 虚函数必须提供定义,即使没有被用到。因为编译器不知道哪个虚函数在运行时会被使用到。
  • 只有当通过指针或引用调用虚函数时才会发生动态绑定,正常调用虚函数时不会发生动态绑定,只是简单的赋值。
'通过指针或引用调用虚函数会发生动态绑定'
Quote base("2022", 50);
print_total(cout, base, 10);   //调用Quote::net_price
Bulk_quote derived("2021", 50);
print_total(cout, derived, 10);   //调用Bulk_quote::net_price

'正常调用虚函数不会发生动态绑定'
base = derived;     //把derived的Quote部分拷贝到base
base.net_price(20); //调用Quote::net_price

派生类中的虚函数

  • 当在派生类中覆盖了某个虚函数时,可以用 virtual 指明也可以不用。虚函数在所有的派生类中都是虚函数。(一般我们会加上override来说明该函数是虚函数,且是基类继承过来的。)
  • 如果派生类的函数覆盖了继承而来的虚函数,它的形参类型必须与被覆盖的基类函数完全一致。返回类型也必须相匹配。

final 和 override 说明符

  • 问题:
    • 如果派生类定义了一个函数与基类中虚函数名字相同但形参列表不同是合法的,不会报错。编译器会认为这个新定义的函数与基类中原有的函数时相互独立的,此时派生类的函数并没有覆盖掉基类中的版本。
  • c++ 11中可以使用override来指明派生类中的虚函数。这时如果该函数没有覆盖基类中的虚函数,编译器就会报错。
  • 可以把某个函数指定为 final,这样该函数就不能被派生类所覆盖
  • final 和 override 都写到形参列表和尾置返回类型之后。

虚函数与默认实参

  • 虚函数也可以有默认实参,实参值由调用的静态类型决定。即如果通过基类的指针或引用调用虚函数,则使用基类中定义的默认实参。
  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
B(1,2// B为基类,默认实参为1,2
A:public B(1,2,3,4)  //A为派生类,默认实参为1,2,3,4
B b;                 //b为B的对象
//b在进行初始化时,前面俩个参数是由基类的构造函数进行初始化的,因此如果实参不一样的话,例如(a,b,2,3),则通过基类初始化后为(1,2,3,4)发生错误

回避虚函数机制

  • 有时希望对虚函数的调用不进行动态绑定,而是强迫执行虚函数的某个特定版本,可以通过作用域运算符来实现。
double price = baseP->Quote::net_price(42);//net_price 是虚函数,这里指定调用基类 Quote 的虚函数版本。
  • 通常只有在成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的机制。或者当一个派生类的虚函数需要调用它的基类版本时。