CHAPTER15-面向对象程序设计(C++ Primer笔记)
面向对象程序设计
object-oriented programming
15.1 OOP:概述
OOP核心思想:数据抽象、继承和动态绑定
数据抽象 将类的接口与实现分离
继承 定义相似类型并对其相似关系建模
动态绑定 一定程度上忽略相似类型的区别,以统一的方式使用他们的对象
继承
层次关系的根部为基类,继承自基类的其他类称为派生类。
派生类必须通过使用类派生列表指出其基类。
虚函数
基类希望它的派生类各自定义适合自身的版本,则声明为虚函数。
class A{
public:
virtual int virtualexample();
};
class B: public A{
int virtualexample();
};
动态绑定
函数的运行版本由实参决定,在函数运行时选择函数的版本,因此动态绑定也叫运行时绑定。
15.2 定义基类和派生类
派生类可以继承基类的成员,然而当遇到与类型相关的操作时,派生类必须重新定义,即派生类提供自己的新定义以覆盖(override)基类旧定义。
15.2.1 定义基类
基类的两种成员函数:
- 基类希望派生类进行覆盖的函数 :直接定义为虚函数virtual,当我们使用指针或引用调用虚函数时,该调用将会被动态绑定。(在基类声明一个函数为虚函数,在派生类中该函数也隐式的是虚函数)
- 基类希望派生类直接继承的函数 :解析过程发生在编译时而非运行时。
访问控制与继承:
protected访问运算符:派生类有权访问,禁止其他用户访问。
15.2.2 定义派生类
类派生列表 <访问符> <class_name>
public: 基类的公有成员也是派生类接口的组成部分;可以将公有派生类型的对象绑定到基类的引用或指针上。
大部分类为单继承,但也可以多于一个基类。
派生类中的虚函数
如果派生类没覆盖基类中的某个虚函数,则直接继承基类中的版本
可以不在覆盖的函数前加上virtual关键字,或者在函数后面添加override关键字显式覆盖(new in C++11)
派生类对象及派生类向基类的类型转换
A a; //基类对象
B b; //派生类对象
A *pa=&a; //pa指向A对象
pa=&b; //pa指向b的A部分
A &ra=b; //r绑定到b的A部分
派生类构造函数
B(int paramA1,int paramA2,int paramB1):A(paramA1,paramA2),Bmember(paramB1) {}//B类是A类的派生类
派生类使用基类的成员
派生类可以访问基类的公有成员和受保护成员:public 和 protected
派生类的作用域嵌套在基类的作用域之内。
继承与静态成员
基类的静态成员,整个继承体系只存在该成员的唯一定义。静态成员遵循通用的访问控制规则。
基类与派生类的声明
派生类的声明中包含类名但不包含派生列表。
被用作基类的类必须已经被定义而不是仅仅声明。
直接基类和间接基类
class Base{//...};
class D1:public Base{//...}; //Base是D1的直接基类
class D2:public D1{//...}; //Base 是D2的间接基类
final关键字
class NoDerived final{/* ... */}; //NoDerived 不可以作为基类
15.2.3 类型转换与继承
静态类型:在编译时总是已知的,变量声明时的类型或表达式生成的类型
动态类型:变量或表达式表示的内存中的对象的类型,直到运行时才可知
表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
注意:不存在从基类到派生类的隐式类型转换,且派生类向基类的自动转换只对指针或引用类型有用,在对象之间不存在类型转换。
15.3 虚函数
对虚函数的调用可能在运行时才被解析(决定调用哪个版本)
当某个虚函数通过指针或者引用调用时,被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
C++ 面向对象编程的核心思想——多态性
派生类中的虚函数
- 派生类中的虚函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与基类函数完全一致。
- 返回类型也必须基类函数匹配(当类的虚函数返回类型是类本身的指针或引用时,该规则无效)。
final和override说明符
final:不许后续的其他类覆盖该函数;
override:覆盖的函数参数列表必须完全一致,返回类型也应当完全相同或满足派生类向基类的自动转换规则(引用或指针);
虚函数与默认实参
如果调用虚函数时使用默认实参,选择的默认实参值由本次调用的静态类型决定。
回避虚函数的机制
使用作用域运算符实现运行虚函数版本的指定:
Derived D1;// Base->Derived
D1.Base::virtualexample();
15.4 抽象基类
纯虚函数
使用=0将虚函数说明为纯虚函数,纯虚函数为无须定义(可以在类外部对其进行提供定义,类的内部无法对纯虚函数提供函数体),后续的派生类可以对其进行覆盖。
class Derived:public Base{
int purevirtual() const =0;
};
抽象基类: 含有(或者未经覆盖直接继承)纯虚函数的类
注意
- 不可以定义抽象基类的对象,可以定义覆盖了纯虚函数的派生类的对象。
- 派生类构造函数只初始化它的直接基类。
15.5 访问控制与继承
class Base{
protected:
int prot_mem;
};
class Sneaky:public Base{
friend void clobber(Sneaky&);
friend void clobber(Base&);
int j;
}
受保护的成员 protected
- 受保护成员对类的用户是不可访问的;
- 受保护成员对派生类的成员和友元来说是可访问的;
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员,派生类对于一个基类对象中的受保护成员没有任何访特权。
公有,私有和受保护继承
某个类对其继承而来的成员的访问权限受到两个因素影响:
- 基类中该成员的访问说明符
- 在派生类中派生列表中的访问说明符
派生类B继承基类A
public: B继承来的成员访问权限不变
protected:继承来的成员访问权限 public降为protected,其他不变
private: 继承来的成员访问权限public,protected均变为private
外加派生类新添加的不同访问权限的成员,共同组成新的类B。
派生类向基类转换的可访问性
假定D继承自B:
- D公有地继承B,用户代码可以使用派生类向基类的转换;否则不可以;
- D的成员函数和友元都能使用派生类向基类的转换;
- 如果D公有地或受保护地继承自B,则D的派生类的成员和友元可以使用D到B的类型转换;否则不可以。
友元与继承
友元关系无法传递,也无法继承。基类的友元在访问派生类成员时不具备特殊性,类似的,派生类的友元也不能随意访问基类的成员。
改变个别成员的可访问性
在访问说明符下使用using关键字来将成员的可访问性改为对应权限:
class Base{
public:
int size() const{return n;}
protected:
int n;
};
class Derived:private Base{
public:
using Base::size;
protected:
using Base:n;
};
默认的继承保护级别
struct 默认public继承。
class默认private继承。
15.6 继承中的类作用域
派生类的作用域位于基类作用域之内,调用派生类型的对象的成员时,从内而外对作用域逐层进行名字查找。
在编译时进行名字查找
- 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。(无论静态类型和动态类型是否相同)
名字冲突与继承
派生类可以重用定义在其直接基类或间接基类中的名字(相当于隐藏同名的基类成员)
可以通过使用作用域运算符来使用被隐藏的基类成员。
注意 名字查找先于类型检查,即使基类成员函数的形参列表不一致,基类成员也仍然会被隐藏。
虚函数与作用域
class Base{
public:
virtual int fcn();
};
class D1:public Base{
public:
int fcn(int); //隐藏基类的fcn,非虚函数,但没有覆盖基类中的虚函数,因为形参列表不一致
virtual int f2(); //是一个基类中不存在的虚函数
};
class D2:public D1{
int fcn(int); //隐藏基类中的fcn,非虚函数
int fcn(); //覆盖了基类中的虚函数
void f2(); //覆盖了D1中的f2虚函数
};
//可以通过基类调用隐藏的虚函数
Base b;
D1 d1;
D2 d2;
Base *bp=&b, *bp1=d1,*bp2=d2;
bp->fcn(); //虚调用,运行时调用Base::fcn()
bp1->fcn(); //虚调用,运行时调用Base::fcn()
bp2->fcn(); //虚调用,运行时调用D2::fcn()
D1 *dp1=&d1,*dp2=&d2;
bp2->f2(); //错误,Base类型没有名为f2的成员
dp1->f2(); //虚调用,运行时调用D1::fcn()
dp2->f2(); //虚调用,运行时调用D2::fcn()
覆盖重载的函数
成员函数不论是否为虚函数都可以重载,派生类可以覆盖重载函数的0个或多个实例。
15.7 构造函数与拷贝控制
15.7.1 虚析构函数
基类通常应当定义一个虚析构函数,即可以动态分配继承体系中的对象。对继承体系中的虚析构函数分别定义对应的版本后,通过delete基类指针就可以运行正确的析构函数版本。
虚析构函数将阻止合成移动操作
如果一个类定义了析构函数,即使它通过default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
15.7.2 合成拷贝控制与继承
派生类中的合成的拷贝控制操作:
- 对类本身的成员依次进行初始化、赋值或销毁的操作;
- 使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或注销的操作。
派生类中删除的拷贝控制与基类的关系
- 基类中的默认构造函数、拷贝构造函数、拷贝赋值函数或析构函数是被删除的或不可访问的,则派生类中的对应成员将会是被删除的;
- 如果基类中有一个不可访问的或删除的析构函数,则派生类中的默认和拷贝构造函数将是被删除的;
- 编译器不会合成一个删除掉的 移动操作。
移动操作与继承
大多数基类都会定义一个虚析构函数。基类通常不含有合成的移动操作,且派生类中也没有合成的移动操作。
如果需要进行移动操作,则首先需要在基类中进行定义。
class Base{
public:
Base() = default; //对成员依次进行默认初始化
Base(const Base&) = default;//对成员依次拷贝
Base(Base&&) = default; //对成员依次移动
Base& operator=(const Base&) = default; //拷贝赋值
Base& operator=(Base&&) = default; //移动赋值
virtual ~Base() =default;
};
15.7.3 派生类的拷贝控制成员
拷贝以及移动构造函数和运算符 需要拷贝以及移动基类的成员以及自有成员;
析构函数只负责销毁派生类自己分配的资源。派生类的基类部分也是自动销毁的。
定义派生类的拷贝或移动构造函数
默认情况下,基类的默认构造函数初始化对象的基类部分,想要使用拷贝或移动构造函数,我们必须在构造函数初始值列表中显式地调用该构造函数。
class Base{//...//};
class D: public Base{
public:
D(const D& d):Base(d) //copy base members
/*D members' initial value*/ {/* ... */}
D(D&& d): Base(std::move(d)) //move base members
/*D member's initial value*/ {/* ... */}
};
派生类赋值运算符
派生类赋值运算符也必须显式地为其基类部分赋值。
//Base::operator=(const Base&)不会自动被调用
D& D::operator=(const D& rhs){
Base::operator=(rhs);
//为派生类的成员赋值
return *this;
}
派生类析构函数
析构函数体执行完后,对象的成员会被隐式销毁,因此对象的基类部分也自动隐式销毁;所以派生类析构函数只负责销毁由派生类自己分配的资源。
class D: public Base{
public:
//Base::~Base 被自动调用执行
~D(){/*自定义清除派生类分类的资源*/}
};
在构造函数和析构函数中调用虚函数
- 派生类对象的基类部分首先被构建。销毁派生类对象的次序正好相反。因此当我们执行上述基类成员的时候,该对象处于未完成的状态。
- 当构建一个对象时,需要把对象的类和构造函数的类看作同一个,析构函数同理。因此需要对虚函数的调用绑定来满足这种需求,且直接调用和间接调用都有效。
- 因此构造函数和析构函数调用了某个虚函数,应当执行与其所属类型相对应的虚函数版本。
15.7.4 继承的构造函数
new in C++11:派生类可以重用其直接基类定义的构造函数,可以乘称作“继承”;但类实际上不能继承默认、拷贝和移动构造函数,如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。
class D:public Base{
public:
using Base::Base; //继承Base的构造函数
// 类的剩余部分
};
当作用域构造函数时,using声明语句将为基类的每个构造函数生成一个与之对应的派生类构造函数(形参列表完全相同)。
生成的构造函数形如:
derived(params):base(args){ }
其中params时构造函数的形参列表,args将派生类构造函数的形参传递给基类的构造函数。派生类自有的数据成员将被默认初始化。
继承的构造函数的特点
- 构造函数的using声明不会改变该构造函数的访问级别。
- using语句不能指定explicit和constexpr。
- 基类构造函数的默认实参不会被继承。如果基类的构造函数存在默认实参,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
- 派生类通常会继承所有构造函数,但存在如下情况:派生类继承一部分构造函数,为其他的构造函数定义自己的版本(具有相同的参数列表的构造函数不会被继承);默认、拷贝和移动构造函数不会被继承。
15.8 容器与继承
使用容器存放继承体系中的对象时,必须采取间接存储的方式。容器中无法保存不同类型的元素,因此无法具有继承关系的多种类型的对象直接存放在容器当中。
在容器中放置(智能)指针而非对象
使用基类的指针(更好的选择是智能指针)存放在容器中。
vector<shared_ptr<Base>> basket;
basket.push_back(make_shared<Base>(args));//插入基类类型
basket.push_back(make_shared<Derived>(args)); //插入派生类动态指针
//调用
basket.back()->memberMethod();
Basket 类
class Basket{
public:
void add_item(const shared_ptr<Base> it){items.insert(it);}
private:
vector<shared_ptr<Base>> items;
};
Basket的用户必须处理动态内存,add_item需要接受一个shared_ptr参数。
Basket bsk;
bsk.add_item(make_shared<Base>(args));
bsk.add_item(make_shared<Derived>(args));
但可以通过重新定义add_item使得其接受一个Base对象而非shared_ptr:
void add_item(const Base & it); //拷贝
void add_item(Base && it); //移动
然而当我们进行分配内存时使用new Base(it)
时可能会出错,因为如果it是派生类型的对象,该对象将被切掉派生类多出的部分(new请求一个Base类型的内存,并拷贝it的Base部分)。
模拟虚拷贝
给Base类添加一个虚函数,将申请一份当前对象的拷贝。
class Base{
public:
virtual Base* clone() const & { return new Base(*this);}
virtual Base* clone() &&{
return new Base(std::move(*this));
}
};
class Derived:public Base{
public:
virtual Derived* clone() const & { return new Derived(*this);}
virtual Derived* clone() &&{
return new Derived(std::move(*this));
}
};
则add_item则可定义为:
class Basket{
public:
void add_item(const Base & it){
items.insert(shared_ptr<Base>(it.clone()));
} //拷贝
void add_item(Base && it){
items.insert(shared_ptr<Base>(std::move(it).clone()));
} //移动
};