面向对象核心:
- 继承
- 通过继承联系在一起的类构成一种层次关系,基类定义所有类共同拥有的成员,派生类定义自己特有的成员。
- 基类将成员函数分成两类
- 普通函数:基类希望派生继承而不要改变的函数。
- 虚函数:基类希望派生类各自进行覆盖的函数,自己定义合适自己的版本。
- 封装(数据抽象)
- 多态(动态绑定)
// 基类
class Base {
……
}
// // Derived 以public方式继承自Base,可多继承。 // // :号后是类派生列表。 // 派生访问控制符:每个基类前面都有一个访问控制符,如果没有,则采用默认权限(class是private,struct是public) // 作用是决定基类成员在派生类中的private、public or protected。 class Derived : public Base { …… }
此时的Base对象中含有两部分成员:
- 继承自Base的成员
- 更准确地讲,含有直接/间接基类的所有成员。
- 自定义的成员
C++标准并没有规定派生类对象在内存中的分布,所以这两部分不一定是连续存储的。
<a name="NnNyu"></a>
## 父子类型转换
基类和派生类之间存在一种特殊的隐式转换情况,那就是**派生类指针、引用转换成基类指针、引用**,这还有一个前提条件,就是在类型转换代码处,基类的公有成员可访问。关于成员访问权限,看下一节的学习。<br />因为派生类对象包含基类对象部分,因此可以看成是基类对象,所以编译器会隐式执行派生类到基类的转换,注意仅限于引用、指针类型上的转换,对象不能隐式转换。
```cpp
Base base; // 基类对象
Derived derived; // 派生类对象
Base *pBase = base; // 基类类型指针
Base &rBase = base; // 基类类型指针
pBase = derived; // 正确,隐式转换成派生类类型。
// 为什么可以这样?因为派生类对象包含基类对象部分,基类类型指针、引用
// 肯定绑定到该基类部分上。
rBase = derived; // 正确
// 我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
// 静态类型是编译阶段确定,动态类型时是只有到执行时,才能确定。
// pBase、rBase的静态类型是Base*、Base&
// pBase、rBase的动态类型是Derived*、Derived&
Derived *pDerived = derived;
pDerived = pBase; // 错误,不存在基类向派生类的隐式转换。
pDerived = dynamic_cast<Derived*>(pBase); // 动态转换,运行时检查
// 如果pBase实际指向的不是Derived对象,
// 就失败,返回空指针。
pDerived = static_cast<Derived*>(pBase); // 不管三七二十一,直接转,必定成功,
// 但是合不合法就不知道了。
// 派生类向基类的隐式转换,只发生在指针、引用上,在对象中不存在隐式转换。
// 对象的赋值,触发的时候赋值运算符的运算。
Base b;
Derived d;
b = d; // 触发拷贝赋值运算,Base::operator=(const Base&)
// 只拷贝了d中的Base部分数据。
构造函数(成员初始化)
构成函数就设计到成员初始化,有一个原则:自己初始化自己的成员。
class Base{
public:
Base();
private:
int b_mem1;
int b_mem2;
}
class Derived : Base {
public:
Derived();
private:
int d_mem1;
int d_mem2;
}
// 隐式调用Base的默认构造函数初始化Base部分的成员
Derived::Derived()
: d_mem1(0)
, d_mem2(0){
}
Derived::Derived()
: Base() // 显式调用,意义同上,派生类可以选择使用基类构造函数进行初始化
, d_mem1(0)
, d_mem2(0){
}
注意,成员初始化顺序:
class Base { public: static void statfunc(); // static静态方法在整个继承体系当中都只会有一份定义。
public: static int m_istatic; // 所有Base对象共享。它属于这个类,而不是对象。 }
class Derived : public Base { public: void f(const Derived &obj){ Base::statfunc(); // 正确 Derived::statfunc(); // 正确 obj.statfunc(); // 正确 statfunc(); // 正确,this.statfunc(); } }
<a name="JOln3"></a>
## 阻止继承
```cpp
// 类将无法被继承。
class NoDerived final { // final关键字
}
class Derived : public NoDerived { // 错误,类NoDerived是final的。
}
名字查找
继承体系中的名字查找。如派生类中调用某一个成员,它的查找过程如下:
- 先在派生类自己的作用域中查找
- 没找到,往上在基类作用域中查找。
- 还没找到,则沿着继承链继续往上查找每个基类作用域中的成员。
- 最后还是没找到,则编译报错。
一个对象的静态类型决定了该对象有哪些成员可以访问,即使动态类型和静态类型不一致。
在派生类中如果定义和基类(包括间接基类)相同的成员,将掩盖基类的成员。通过显式的调用::作用域运算符我们可以使用指定作用域的成员,也就是说我们可以跳过派生类的同名成员,直接使用基类的该成员。
// 类对象成员调用的过程,总规则依然是先查找名字,后检查。
// 即先确定使用哪个名字,再检查名字是否匹配(函数签名是否匹配)。
//
// p->fuck()或者obj.fuck()
// 1、确定p、obj的静态类型
// 2、在静态类型的类作用域中查找fuck成员,
// 3、如果没找到则在静态类型的往上的继承链中基类查找。如果还没找到就报错。
// 4、找到,则进行检查:
// a、如果fuck是虚函数,且指针调用(p->fuck())或obj是引用类型。则启用“多态”逻辑,
// 即在运行时根据实际所引用对象来决定调用哪个的虚函数。
// b、其他情况,常规函数调用(函数匹配规则)。
struct Base {
int memfcn(};
};
struct Derived : Base (
int memfcn(int}; // 隐藏了基类的memfcn
};
Derived d;
Base b;
b.memfcn(); // 调用Base::memfcn
d.memfcn(10); // 调用Derived::memfcn
d.memfcn(); // 错误:参数列表为空的memfcn被隐藏了
ct.Base::memfcn(); // 正确:调用Base::memfcn
覆盖重载函数
struct Base {
void func() {}
void func(int) {}
void func(string) {}
}
struct Derived : public Base {
void func(int){}; // 将覆盖基类所有同名成员
// 此时,Derived只有一个func成员了,隐藏了基类的所有成员
}
int main(){
Derived d;
d.func(); // 错误,没有找到函数
d.func(1); // 正确,调用Derived::func(int)
d.func("asdf"); // 错误,没有找到函数
}
如果Derived需要沿用Base的全部重载版本,但是又想自己重载其中的部分版本,该怎么做?我们只能在基类中显式地将所有版本全部重载,这非常的笨拙,有一种更好的办法解决,使用using 声明。
struct Base {
void func() {}
void func(int) {}
void func(string) {}
}
struct Derived : public Base {
// 沿用base的全部重载版本。
using Base::func;
// 派生类只覆盖fuck(int)重载版本。其他重载版本在派生类中依然可见。
void func(int){};
}
int main(){
Derived d;
d.func(); // 正确,调用Base::func()
d.func(1); // 正确,调用Derived::func(int)
d.func("asdf"); // 正确,调用Base::func(string)
}
二、虚函数
class Base {
public:
// 普通函数:派生类直接继承这个函数,不会改变。
void func() const;
// 虚函数:基类希望派生类都各自定义一个适合自己的版本。
// 出构造函数以外的非static成员函数都可以是虚函数。
virtual void vfunc() const;
virtual void vfunc1() const final; // 阻止派生类覆盖此虚函数。
}
class Derived : public Base{
// 派生类重新声明基类的虚函数,这表示派生类要重新定义该虚函数(覆盖),
// 如果没有声明就使用基类版本。
//
// virtual可以省略,基类虚函数在派生类中都是虚函数。
// 子类声明了虚函数就必须定义该虚函数,因为编译阶段并不能确定此函数会不会被使用到
// 安全起见,就必须要定义。而普通函数就非必须,因为编译器可以确定当前代码会不会用到这个函数。
//
// virtual:派生类中可有可无,因为隐式声明为virtual
// override:显式标记派生类重写基类虚函数,方便区分。
virtual void vfunc() const override;
void vfunc1() const { // 错误,vfunc1是final的。
}
}
// 错误:virtual不能在类外函数定义前。
virtual void Derived::vfunc(){
}
虚函数覆盖
派生类覆盖基类虚函数时,对派生类中的虚函数声明有如下要求:
- 形参列表必须和基类虚函数完全一致。
- 返回值类型必须和基类虚函数完全一致
- 除了特殊情况,基类虚函数返回基类类型的指针、引用时,派生类虚函数可以返回派生类类型的指针、引用,且这种类型转换是可访问的。 ```cpp
class Base{ virtual Base* vfunc1(){}; }
class Derived : public Base{
// 正确,派生类类型指针、引用可转换到基类类型指针、引用。
Derived* vfunc1() override{};
}
<a name="Iah7b"></a>
## 默认实参
派生类虚函数的默认实参请保持与基类一致,因为默认实参的选择由虚函数的静态版本决定。见如下代码:
```cpp
class Base{
virtual void vfunc5(int a = 1){};
}
class Derived : public Base{
void vfunc5(int a = 2) override{};
}
int main(){
Derived d;
Base* p = &d;
p->vfunc5(); // a的只为1,因为p的静态类型是基类类型,因此选择基类的默认实参。
}
回避虚函数机制
有时候,我们需要回避虚函数的动态绑定机制,或者说派生类需要调用虚函数的基类版本时,我们可以这么做:
Derived d;
Base* p = &d;
p->Base::vfunc(); // 调用基类的vfunc版本。这种需求一般发生在成员函数代码中。
抽象基类
含有纯虚函数的类是抽象基类,抽象基类不能被创建。
class Base{ // 有纯虚函数的类就是抽象类(abstract class)
......
// = 0必须在类内部声明,不能有函数体,但是在类外部可以有函数体。
virtual afunc() = 0; // 纯虚函数,=0,
......
}
// 设计DerivedRefactor类,就是常见的重构。
class DerivedRefactor : public Base{};
class Derived2 : public DerivedRefactor{};
Base p; // 抽象类不能创建对象。
构造、析构
在构造析构中调用虚函数是一种特殊情况,对象的类将和所在构造函数、析构函数的类类型相同,这是C++标准的特殊情况,是为了避免多态调用导致的问题。
假设基类构造函数中调用虚函数也是正常的多态调用,则可能触发的是派生类虚函数版本,此时派生类对象还未初始化,很可能出错!析构函数同理。
class Base {
public:
Base(){
this->fuck(); // 调用Base::fuck,即使this是指向派生类对象
}
~Base(){
this->fuck(); // 调用Base::fuck,即使this是指向派生类对象
}
public:
virtual void fuck();
}
class Derived : public Base{
public:
void fuck();
}
三、访问控制
“很多访问者”都可能会访问(调用)类的成员,这些访问者可分为以下几类:
- 类内部:类内部实现的其他成员很可能会访问该成员。
- 类友元
- 类用户:创建该类对象的代码处。
- 派生类内部:包含间接派生类。
- 派生类友元
- 派生类用户
类的成员访问权限受两个因素控制:
- 因素一:声明成员时的访问说明符。
- 因素二:若成员派生自基类,则还要考虑类派生列表中该基类前面的访问说明符。 ```cpp
class Base { protected: // 因素一 int prot_mem; }
class Derived : public Base { // 因素二 …… // prot_mem的访问权限是protected类型。 }
访问说明符有三种:public、protected、private。它们用在上述两处意义有所不同:
- 当用于修饰声明的成员时:
- public:公共成员,所有访问者都可以访问。
- protected:受保护成员,除了类用户、派生类用户,都可以访问(派生类及其友元可访问)。此处要注意一种特殊情况,派生类及其友元只能通过派生类对象(基类类型对象不行)访问基类protected成员,见下面代码。
- private:私有成员,只有类内部、类友元可访问。
- 当用在类派生列表中时:
- 用于批量控制基类成员在派生类中的访问权限,具体情况见下面代码。
```cpp
// protected特殊情况:
// 派生类成员、友元只能通过派生类对象访问基类protected成员,不能通过基类类型对象
class Base {
protected:
int prot_mem;
};
class Derived : public Base {
friend void fuck(Derived &);
friend void fuck(Base &);
}
void fuck(Derived& d){
std::cout << d.prot_mem << std::endl; // 正确
}
void fuck(Base& b){
std::cout << b.prot_mem << std::endl; // 错误,必须通过派生类类型对象访问。
}
访问说明符用于类派生列表时,对基类成员在派生类中的访问权限影响如下:
// ***********************************************************************
// **********派生访问说明符影响的是基类成员在子类中的访问说明符类型。总结如下:
// ***********************************************************************
// public继承,让基类成员在子类中保持原有访问权限
//
// 基类public成员 + public继承 = 在子类中是public成员
// 基类protected成员 + public继承 = 在子类中是protected成员
// 基类private成员 + public继承 = 在子类中是private成员
//
// protected继承,在派生类中保护所有的基类成员,即,使基类成员的原有访问权限都提升到protected
// 以上,如果已经不低于protected,就保持不变,如protected、private
//
// 基类public成员 + protected继承 = 子类protected成员
// 基类protected成员 + protected继承 = 子类protected成员
// 基类private成员 + protected继承 = 子类private成员
//
// private继承,基类成员在子类中都变成private访问权限
//
// 基类public成员 + private继承 = 子类private成员
// 基类protected成员 + private继承 = 子类private成员
// 基类private成员 + private继承 = 子类private成员
//
类型转换可访问性
前面已经讲了,派生类可以隐式转换成基类指针、引用类型。但这也受派生访问说明符的影响。总结一点就是:
在代码中的某个给定节点来说,如果基类的公有成员是可访问的,则这种类型转换也是可访问的,否则不行。
只有当public继承时,派生类用户才能进行这种转换。
而派生类内部和派生类友元,永远都能进行这种转换。
友元
友元关系不能传递,友元关系同样也不能继承。
class Base{
friend class shit;
protected:
void base();
};
class Derived : public Base{
protected:
void derived();
};
class shit{
int f(Base b) { return b.base(); } // 正确:shit是base的友元
int f2(Derived d) { return d.derived(); } // 错误:shit不是Derived的友元
int f3(Derived s) { return s.base(); } // 正确:shit是Base的友元
};
class newshit : public shit{
int f(Base b) { return b.base(); } // 错误:newshit不是base的友元
int f2(Derived d) { return d.derived(); } // 错误:newshit不是Derived的友元
int f3(Derived s) { return s.base(); } // 错误:newshit不是Base的友元
}
修改访问权限
通过using来修改指定成员在基类中的访问权限。当然前提是这些成员在派生类中本身可以访问。因此不可能修改基类的private成员的访问权限。
class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived: private Base {
public:
using Base::size; // size原本应该是private的
protected:
using Base::n; // n原本应该是private的
}
class、struct区别
class和struct并没有什么深层次的区别,唯一的区别就在默认访问权限上不同,class默认private,struct默认public。
- 默认成员访问说明符不同
- class:private
- struct:public
- 默认派生访问说明符不同
Base *bp = new Derived; delete bp; // 将触发析构函数,若析构函数不是虚函数,则不是多态调用,而是普通函数调用 // bp的静态类型是Base,则调用Base的析构函数,显然这是不正确的。 // 因此如果基类的析构函数不是虚函数,则delete 子类对象将很可能出错!
定义析构函数,会阻止编译器合成移动操作。
<a name="7CXUY"></a>
## 合成的拷贝控制
基类、派生类的合成拷贝控制成员和其他合成拷贝控制成员是一样的功能,此外,这些合成的拷贝控制成员还负责使用**直接基类**对应的操作对派生类对象的对应基类部分进行拷贝控制操作(构造、赋值、析构)。<br />定义基类的方式可能会导致派生类的合成版本是删除的(deleting):
- 基类的默认构造、拷贝构造、拷贝赋值、析构函数是删除的、不可访问的(编译器不会合成删除的移动操作)。因为派生类不能使用基类的对应操作。
- 基类的析构是不可访问或删除的,则派生类合成的默认、拷贝构造是删除的,因为无法销毁派生类对象的基类部分。
- 基类的析构是不可访问或删除的,则派生类的移动构造是删除的。
- 当使用=default请求移动操作时,若基类对应操作是不可访问、删除的,则派生类对应也是删除的。
```cpp
class B {
public:
B();
B(const B&) = delete;
...... // 其他成员,不含有移动构造函数
// 因为定义了拷贝构造函数,因此不会合成移动构造函数
};
class D : public B {
// D(); // 合成的默认构造函数,调用B的默认构造函数
// D(const D&) = delete; // 合成的删除的拷贝构造函数,因为基类是删除的
// D没有移动构造函数,因为基类没有合成移动构造函数
}
D d; // 正确,D的合成的默认构造函数。
D d2(d); // 错误,D的拷贝构造函数是delete的。
D d3(std::move(d)); // 错误,D的拷贝构造函数是删除的。
// 没有移动构造函数,因此匹配的是拷贝构造函数。
因为基类一般会定义虚析构函数,因此基类不会有合成的移动操作。所以如果需要移动操作,我们需要显式地进行定义。
class Base {
public:
Base() = default;
Base(const Base&) = default;
Base(Base&&) = default; // 显式定义合成的移动构造
Base& operator=(const Base&) = default;
Base& operator=(Base&&) = default; // 显式定义合成的移动赋值
virtual ~Base() = default; // 有析构,不会合成移动操作。
}
派生类的拷贝控制
除了析构函数,派生类的拷贝控制成员需要完成(派生类+直接基类)的拷贝控制操作。其实也就是显式调用基类对应的拷贝控制操作。
析构函数只负责销毁所属类自己分配的资源。
class Base {
......
};
class Derived : public Base {
public:
D(const D& d) // 派生类的拷贝构造函数
: Base(d) { // 拷贝构造基类部分,显式调用基类的拷贝构造函数
...... // 拷贝构造派生类部分
}
D(D&& d) // 派生类的移动构造函数
: Base(std::move(d)){ // 移动构造基类部分,显式调用基类的移动构造函数
...... // 移动构造派生类部分
}
D& operator=(const D& d){ // 派生类移动拷贝赋值运算符
Base::operator=(d); // 拷贝赋值基类部分,显式调用基类拷贝赋值运算符。
...... // 拷贝赋值派生类部分
}
~D()
:~Base() // 错误,Base::~Base()会自动(隐式)执行
{
Base::~Base(); // 错误,Base::~Base()会自动(隐式)执行
...... // 销毁D自己分配的资源。
// 析构函数的执行顺序
// 1、派生类析构函数
// a、D的析构函数体
// b、成员析构,按类内成员声明的逆序。
// 2、基类析构函数
}
}
五、继承的构造函数
派生类会“继承”直接基类的构造函数(除了继承默认、拷贝、移动构造函数)。
using声明的方式继承构造函数。一般using声明是使名字在当前作用域可见,但作用域构造函数时,编译器将产生代码,会在派生类中生成与所有基类构造函数对应的派生类版本。换句话,对于基类的每个构造函数,编译器都在派生类中生成一个形参完全相同的构造函数。
class Base{
Base(const string& str);
Base(const int i);
Base(const float f);
}
class Derived : public Base{
public:
using Base::Base; // "继承"基类所有构造函数(除了默认、拷贝、赋值构造函数)
// 编译器将生成以下代码:
Derived(const string& str);
Derived(const int i);
Derived(const float f);
}
和普通的using声明还有不同,就是无法改变构造函数的访问权限(基类是什么样派生类中就是什么样),即使在派生类中显式声明,也不能改变explicit、constexpr。
如果基类构造函数含有默认实参,则在派生类中对应的会有多个构造函数,每个对应省掉一个有默认实参的形参。
class Base {
public:
Base(int a, int b = 1);
}
class Derived : public Base {
public:
using Base::Base; // “继承”基类构造函数,编译器产生的代码如下:
Derived(int a){
}
Derived(int a, int b){
}
}
显式自定义的构造函数会覆盖继承来的构造函数(如果形参列表完全相同)。
注意,默认构造、拷贝、赋值构造函数不会继承,但会按规则合成。
且继承的构造函数不会当做是用户自定义,因此如果只有继承的构造函数,还会合成一个默认构造函数。
六、容器与继承
当使用容器保存继承体系中的对象时,我们可能采用以下方法:
// Base: 基类
// Derived: 派生类
// vector<Base>来保存
vector<Base> vec;
vec.push_back(Base("motherfucker")); // 正确
vec.push_back(Derived("asdf", 1)); // 编译通过,但只会保存Derived的Base部分。
vector<shared_ptr<Base>> shit;
shit.push_back(make_pair<Base>("fuck")); // 保存Base对象
// 派生类的智能指针可以隐式转换成基类的指针指针类型。
shit.push_back(make_shared<Derived>("fuck1", 1)); // 保存Derived对象