面向对象程序设计
1.oop:核心思想(数据抽象、继承、动态绑定)
- 数据抽象:将类的接口与实现分离
- 继承:我们可以定义与其他类相似但完全不相同的新类
- 动态绑定(又称运行时绑定):在运行时选择函数的版本; 当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定
2.基类与派生类:
定义基类:
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作
- 基类必须将它的两种成员函数区分开来:一种是基类希望派生类进行覆盖(override)的函数,对于该类函数,基类通常将其定义为虚函数(virtual),当我们使用指针或引用调用虚函数时,该调用将被动态绑定;另外一种是基类希望派生类直接继承而不要改变的函数
- 基类通过在成员函数的声明语句之前加上virtual使得该函数执行动态绑定; 关键字virtual只能出现在类内部的声明语句之前而不能用于内外部的函数定义; 一个函数在基类中被声明成虚函数,那么在派生类中隐式的也是虚函数
- 成员函数如果没被声明成虚函数,则其解析过程发生在编译时而非运行时
定义派生类:
- 派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明
- 大多数类都只继承自一个类,这种形式的继承被称作 “单继承”
- c++ 11允许派生类显示地注明它使用某个成员函数覆盖了它继承的虚函数(添加关键字override)
class Bulk_quote : public Quote {
public:
double net_price(size_t) const override;
//
};
- 在一个对象中,继承自基类地部分和派生类自定义的部分不一定是连续存储的
- 因为在派生类对象中含有与基类对应的组成部分,所有能够把派生类的对象当成基类对象来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分上(这种转换称为派生类到基类的类型转换)
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //p指向Quote对象
p = &bulk; //p指向bulk的Quote部分
Quote &r = bulk; //r绑定到bulk的Quote部分
- 派生类的声明中包含类名但是不包含它的派生列表
- 防止继承的发生:在类名后接一个关键字final
3.类型转换与继承:
- 静态类型:变量声明时的类型或表达式生成的类型; 动态类型:变量或表达式表示的内存中的对象的类型
- 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致
- 不存在从基类向派生类的隐式类型转换:因为一个基类的对象可能是派生类对象的一部分,也可能不是,所有不存在从基类向派生类的自动类型转换
- 在对象之间不存在类型转换:派生类向基类的自动类型转换只对指针或引用有效,在派生类类型和基类类型之间不存在这样的转换
- 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉
4.虚函数:
- oop的核心思想是多态性。我们把具有继承关系的多个类型称为多态类型; 引用或指针的静态类型与动态类型不同这一事实正是c++语言支持多态性的根本存在
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同
- 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致,且派生类中虚函数的返回类型也必须与基类函数匹配(当类的虚函数返回类型是类的本身的指针或引用时,上述规则无效)
- 派生类如果定义了一个与基类虚函数同名函数,但参数列表不相同的话,仍然是合法行为,编译器会认为该函数与基类虚函数是相互独立的,但这往往是把形参列表弄错了的错误,编译器发现不了,所以C++11有一个好东西,在其后加上override表示其要对基类的函数进行覆盖,若未覆盖,编译器报错,我们可以发现自己的错误
- 虚函数可以有默认实参,若函数调用了默认实参,则实参值由静态类型决定,所以基类和派生类中定义的默认实参最好一致
- 某些情况下,我们不希望进行动态绑定,我们可使用作用域运算符强行指定其执行哪个版本,进行回避虚函数—一般情况下是成员函数中的代码才需要以防止自己调用自身造成无限循环
//强行调用基类中定义的函数版本而不管baseP的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);
5.抽象基类:
- 含有纯虚函数的类是抽象基类
- 我们不能创建抽象基类的对象
6.访问控制与继承:
- protected成员:类和对象不可访问,派生类的成员和友元可以访问
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员;派生类对于一个基类对象中的受保护成员没有任何访问特权(不能通过基类对象来访问基类中的受保护成员)
- 派生类向基类转换的可访问性:
-
/*1.只有公有继承时,用户代码才能使用派生类向基类的转换;否则不能使用该转换 2.不论以什么方式继承,派生类的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的 3.如果继承方式是公有的或受保护的,则派生类的成员和友元可以使用派生类向基类的类型转换;反之则不能使用*/
友元和继承:
- 不能继承友元关系;每个类负责控制各自成员的访问权限
- 通过在类的内部使用using声明语句,可以将该类的直接或间接基类中的任何可访问成员标记出来(派生类只能为那些可以访问的名字提供using声明)
- struct默认公有继承, class默认私有继承(一个私有派生的类最好显示声明)
7.继承中的类作用域:
- 当存在继承关系时,派生类地作用域嵌套在其基类地作用域之内;如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义
- 派生类的成员将隐藏同名的基类成员
struct Base{
protected:
int mem;
};
struct Derived : Base{
Derived(int i) : mem(i) {}
int get_mem() {return mem;} //返回Derived::mem
protected:
int mem; //隐藏基类中的mem
};
- 通过作用域运算符来使用隐藏的成员
struct Derived : Base {
int get_base_mem() {return Base::mem; }
};
- 理解函数调用的解析过程:
//假定调用p->mem()
1.首先确定p的静态类型,必定是一个类类型
2.在p的静态类型中查找mem,如果找不到,则依次在直接基类中查找直到继承链的顶端;若找不到则报错
3.一旦找到了mem,就进行常规的类型检查
名字查找优先:
- 如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员(即使两者的形参列表不相同)
struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int); //隐藏基类的memfcn
};
- 基类和派生类的虚函数参数列表必须相同,并且可以通过基类的作用域运算符调用基类的虚函数,若派生类的与基类虚函同名的成员参数列表与虚函数不同,那么派生类中的成员并没有覆盖基类的相应虚函数,因为形参列表不一致,派生类将拥有两个同名成员。(若要覆盖,则需要形参列表一致)
8.构造函数和拷贝控制:
虚析构函数:
- 通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本
- 如果基类中的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为
- 虚析构函数将阻止合成移动操作
合成拷贝控制和继承:
- 如果基类中的默认构造函数、拷贝构造函数或是析构函数被删除或是不可访问,则其派生类中的相应成员是被删除的
- 大多数基类都会有一个虚析构函数,因此基类通常不会含有合成的移动操作,派生类中也如此,如果我们确实需要移动的操作,我们需要自行首先在基类中进行定义
派生类的拷贝控制成员:
- 派生类的构造函数不仅要初始化自己的成员,还负责初始化派生类对象的基类部分,而派生类对象的基类部分是自动销毁的,派生类的析构函数只负责销毁自身派生类的成员,但是拷贝和移动操作,都会包含基类的部分
- 定义派生类的拷贝和移动构造函数,需要在其初始值列表中显式的调用其基类的拷贝或移动构造函数,否则的话,派生类对象的基类部分会被默认初始化(派生类中定义拷贝和移动赋值运算符也是一样,需要在函数体中调用基类的相应成员)
- 如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属对应的虚函数版本
继承的构造函数:
- 一个类只继承直接基类的构造函数,而不能继承默认、拷贝和移动构造函数,如果派生类没有定义他们,则编译器将为派生类合成它们
- 提供注明了基类名的using声明语句(继承基类构造函数):
class Bulk_quote : public Disc_quote {
public:
using Disc_quote::Disc_quote; //继承Disc_quote的构造函数
//...
};
- 派生类继承基类的构造函数,其派生类的部分成员将被默认初始化
- 不管using声明语句出现在哪,其访问级别不会被改变
- 当一个基类构造函数含有默认实参时,这些实参并不会被继承;相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略一个含有默认实参的形参
9.容器与继承:
- 当使用容器存放继承体系的对象时,通常必须采用间接存储的方式,因为不允许在容器中存放不同类型的元素
- 当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容
- 在容器中存放指针而非对象