面向对象核心:

  • 继承
    • 通过继承联系在一起的类构成一种层次关系,基类定义所有类共同拥有的成员,派生类定义自己特有的成员。
    • 基类将成员函数分成两类
      • 普通函数:基类希望派生继承而不要改变的函数。
      • 虚函数:基类希望派生类各自进行覆盖的函数,自己定义合适自己的版本。
  • 封装(数据抽象)
  • 多态(动态绑定)
    • 通过基类的引用或指针调用基类的一个虚函数,需在运行时才能确定函数的运行版本,也叫运行时绑定(run-time binding),虚函数是多态的基础

      一、继承

      ```cpp

// 基类 class Base {
…… }

// // Derived 以public方式继承自Base,可多继承。 // // :号后是类派生列表。 // 派生访问控制符:每个基类前面都有一个访问控制符,如果没有,则采用默认权限(class是private,struct是public) // 作用是决定基类成员在派生类中的private、public or protected。 class Derived : public Base { …… }

  1. 此时的Base对象中含有两部分成员:
  2. - 继承自Base的成员
  3. - 更准确地讲,含有直接/间接基类的所有成员。
  4. - 自定义的成员
  5. C++标准并没有规定派生类对象在内存中的分布,所以这两部分不一定是连续存储的。
  6. <a name="NnNyu"></a>
  7. ## 父子类型转换
  8. 基类和派生类之间存在一种特殊的隐式转换情况,那就是**派生类指针、引用转换成基类指针、引用**,这还有一个前提条件,就是在类型转换代码处,基类的公有成员可访问。关于成员访问权限,看下一节的学习。<br />因为派生类对象包含基类对象部分,因此可以看成是基类对象,所以编译器会隐式执行派生类到基类的转换,注意仅限于引用、指针类型上的转换,对象不能隐式转换。
  9. ```cpp
  10. Base base; // 基类对象
  11. Derived derived; // 派生类对象
  12. Base *pBase = base; // 基类类型指针
  13. Base &rBase = base; // 基类类型指针
  14. pBase = derived; // 正确,隐式转换成派生类类型。
  15. // 为什么可以这样?因为派生类对象包含基类对象部分,基类类型指针、引用
  16. // 肯定绑定到该基类部分上。
  17. rBase = derived; // 正确
  18. // 我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
  19. // 静态类型是编译阶段确定,动态类型时是只有到执行时,才能确定。
  20. // pBase、rBase的静态类型是Base*、Base&
  21. // pBase、rBase的动态类型是Derived*、Derived&
  22. Derived *pDerived = derived;
  23. pDerived = pBase; // 错误,不存在基类向派生类的隐式转换。
  24. pDerived = dynamic_cast<Derived*>(pBase); // 动态转换,运行时检查
  25. // 如果pBase实际指向的不是Derived对象,
  26. // 就失败,返回空指针。
  27. pDerived = static_cast<Derived*>(pBase); // 不管三七二十一,直接转,必定成功,
  28. // 但是合不合法就不知道了。
  29. // 派生类向基类的隐式转换,只发生在指针、引用上,在对象中不存在隐式转换。
  30. // 对象的赋值,触发的时候赋值运算符的运算。
  31. Base b;
  32. Derived d;
  33. b = d; // 触发拷贝赋值运算,Base::operator=(const Base&)
  34. // 只拷贝了d中的Base部分数据。

构造函数(成员初始化)

构成函数就设计到成员初始化,有一个原则:自己初始化自己的成员

  1. class Base{
  2. public:
  3. Base();
  4. private:
  5. int b_mem1;
  6. int b_mem2;
  7. }
  8. class Derived : Base {
  9. public:
  10. Derived();
  11. private:
  12. int d_mem1;
  13. int d_mem2;
  14. }
  15. // 隐式调用Base的默认构造函数初始化Base部分的成员
  16. Derived::Derived()
  17. : d_mem1(0)
  18. , d_mem2(0){
  19. }
  20. Derived::Derived()
  21. : Base() // 显式调用,意义同上,派生类可以选择使用基类构造函数进行初始化
  22. , d_mem1(0)
  23. , d_mem2(0){
  24. }

注意,成员初始化顺序:

  • 先初始化基类成员
    • 若有多基类,则按类派生列表顺序。
  • 后初始化自己成员,按声明顺序初始化。

    静态成员

    静态成员在整个继承体系中有且只有一份定义。 ```cpp

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(); } }

  1. <a name="JOln3"></a>
  2. ## 阻止继承
  3. ```cpp
  4. // 类将无法被继承。
  5. class NoDerived final { // final关键字
  6. }
  7. class Derived : public NoDerived { // 错误,类NoDerived是final的。
  8. }

名字查找

继承体系中的名字查找。如派生类中调用某一个成员,它的查找过程如下:

  1. 先在派生类自己的作用域中查找
  2. 没找到,往上在基类作用域中查找。
  3. 还没找到,则沿着继承链继续往上查找每个基类作用域中的成员。
  4. 最后还是没找到,则编译报错。

一个对象的静态类型决定了该对象有哪些成员可以访问,即使动态类型和静态类型不一致。
在派生类中如果定义和基类(包括间接基类)相同的成员,将掩盖基类的成员。通过显式的调用::作用域运算符我们可以使用指定作用域的成员,也就是说我们可以跳过派生类的同名成员,直接使用基类的该成员。

  1. // 类对象成员调用的过程,总规则依然是先查找名字,后检查。
  2. // 即先确定使用哪个名字,再检查名字是否匹配(函数签名是否匹配)。
  3. //
  4. // p->fuck()或者obj.fuck()
  5. // 1、确定p、obj的静态类型
  6. // 2、在静态类型的类作用域中查找fuck成员,
  7. // 3、如果没找到则在静态类型的往上的继承链中基类查找。如果还没找到就报错。
  8. // 4、找到,则进行检查:
  9. // a、如果fuck是虚函数,且指针调用(p->fuck())或obj是引用类型。则启用“多态”逻辑,
  10. // 即在运行时根据实际所引用对象来决定调用哪个的虚函数。
  11. // b、其他情况,常规函数调用(函数匹配规则)。
  12. struct Base {
  13. int memfcn(};
  14. };
  15. struct Derived : Base (
  16. int memfcn(int}; // 隐藏了基类的memfcn
  17. };
  18. Derived d;
  19. Base b;
  20. b.memfcn(); // 调用Base::memfcn
  21. d.memfcn(10); // 调用Derived::memfcn
  22. d.memfcn(); // 错误:参数列表为空的memfcn被隐藏了
  23. ct.Base::memfcn(); // 正确:调用Base::memfcn

覆盖重载函数

  1. struct Base {
  2. void func() {}
  3. void func(int) {}
  4. void func(string) {}
  5. }
  6. struct Derived : public Base {
  7. void func(int){}; // 将覆盖基类所有同名成员
  8. // 此时,Derived只有一个func成员了,隐藏了基类的所有成员
  9. }
  10. int main(){
  11. Derived d;
  12. d.func(); // 错误,没有找到函数
  13. d.func(1); // 正确,调用Derived::func(int)
  14. d.func("asdf"); // 错误,没有找到函数
  15. }

如果Derived需要沿用Base的全部重载版本,但是又想自己重载其中的部分版本,该怎么做?我们只能在基类中显式地将所有版本全部重载,这非常的笨拙,有一种更好的办法解决,使用using 声明。

  1. struct Base {
  2. void func() {}
  3. void func(int) {}
  4. void func(string) {}
  5. }
  6. struct Derived : public Base {
  7. // 沿用base的全部重载版本。
  8. using Base::func;
  9. // 派生类只覆盖fuck(int)重载版本。其他重载版本在派生类中依然可见。
  10. void func(int){};
  11. }
  12. int main(){
  13. Derived d;
  14. d.func(); // 正确,调用Base::func()
  15. d.func(1); // 正确,调用Derived::func(int)
  16. d.func("asdf"); // 正确,调用Base::func(string)
  17. }

二、虚函数

  1. class Base {
  2. public:
  3. // 普通函数:派生类直接继承这个函数,不会改变。
  4. void func() const;
  5. // 虚函数:基类希望派生类都各自定义一个适合自己的版本。
  6. // 出构造函数以外的非static成员函数都可以是虚函数。
  7. virtual void vfunc() const;
  8. virtual void vfunc1() const final; // 阻止派生类覆盖此虚函数。
  9. }
  10. class Derived : public Base{
  11. // 派生类重新声明基类的虚函数,这表示派生类要重新定义该虚函数(覆盖),
  12. // 如果没有声明就使用基类版本。
  13. //
  14. // virtual可以省略,基类虚函数在派生类中都是虚函数。
  15. // 子类声明了虚函数就必须定义该虚函数,因为编译阶段并不能确定此函数会不会被使用到
  16. // 安全起见,就必须要定义。而普通函数就非必须,因为编译器可以确定当前代码会不会用到这个函数。
  17. //
  18. // virtual:派生类中可有可无,因为隐式声明为virtual
  19. // override:显式标记派生类重写基类虚函数,方便区分。
  20. virtual void vfunc() const override;
  21. void vfunc1() const { // 错误,vfunc1是final的。
  22. }
  23. }
  24. // 错误:virtual不能在类外函数定义前。
  25. virtual void Derived::vfunc(){
  26. }

虚函数覆盖

派生类覆盖基类虚函数时,对派生类中的虚函数声明有如下要求:

  • 形参列表必须和基类虚函数完全一致。
  • 返回值类型必须和基类虚函数完全一致
    • 除了特殊情况,基类虚函数返回基类类型的指针、引用时,派生类虚函数可以返回派生类类型的指针、引用,且这种类型转换是可访问的。 ```cpp

class Base{ virtual Base* vfunc1(){}; }

class Derived : public Base{

  1. // 正确,派生类类型指针、引用可转换到基类类型指针、引用。
  2. Derived* vfunc1() override{};

}

  1. <a name="Iah7b"></a>
  2. ## 默认实参
  3. 派生类虚函数的默认实参请保持与基类一致,因为默认实参的选择由虚函数的静态版本决定。见如下代码:
  4. ```cpp
  5. class Base{
  6. virtual void vfunc5(int a = 1){};
  7. }
  8. class Derived : public Base{
  9. void vfunc5(int a = 2) override{};
  10. }
  11. int main(){
  12. Derived d;
  13. Base* p = &d;
  14. p->vfunc5(); // a的只为1,因为p的静态类型是基类类型,因此选择基类的默认实参。
  15. }

回避虚函数机制

有时候,我们需要回避虚函数的动态绑定机制,或者说派生类需要调用虚函数的基类版本时,我们可以这么做:

  1. Derived d;
  2. Base* p = &d;
  3. p->Base::vfunc(); // 调用基类的vfunc版本。这种需求一般发生在成员函数代码中。

抽象基类

含有纯虚函数的类是抽象基类,抽象基类不能被创建。

  1. class Base{ // 有纯虚函数的类就是抽象类(abstract class)
  2. ......
  3. // = 0必须在类内部声明,不能有函数体,但是在类外部可以有函数体。
  4. virtual afunc() = 0; // 纯虚函数,=0,
  5. ......
  6. }
  7. // 设计DerivedRefactor类,就是常见的重构。
  8. class DerivedRefactor : public Base{};
  9. class Derived2 : public DerivedRefactor{};
  10. Base p; // 抽象类不能创建对象。

构造、析构

在构造析构中调用虚函数是一种特殊情况,对象的类将和所在构造函数、析构函数的类类型相同,这是C++标准的特殊情况,是为了避免多态调用导致的问题。
假设基类构造函数中调用虚函数也是正常的多态调用,则可能触发的是派生类虚函数版本,此时派生类对象还未初始化,很可能出错!析构函数同理。

  1. class Base {
  2. public:
  3. Base(){
  4. this->fuck(); // 调用Base::fuck,即使this是指向派生类对象
  5. }
  6. ~Base(){
  7. this->fuck(); // 调用Base::fuck,即使this是指向派生类对象
  8. }
  9. public:
  10. virtual void fuck();
  11. }
  12. class Derived : public Base{
  13. public:
  14. void fuck();
  15. }

三、访问控制

“很多访问者”都可能会访问(调用)类的成员,这些访问者可分为以下几类:

  • 类内部:类内部实现的其他成员很可能会访问该成员。
  • 类友元
  • 类用户:创建该类对象的代码处。
  • 派生类内部:包含间接派生类。
  • 派生类友元
  • 派生类用户

类的成员访问权限受两个因素控制:

  • 因素一:声明成员时的访问说明符。
  • 因素二:若成员派生自基类,则还要考虑类派生列表中该基类前面的访问说明符。 ```cpp

class Base { protected: // 因素一 int prot_mem; }

class Derived : public Base { // 因素二 …… // prot_mem的访问权限是protected类型。 }

  1. 访问说明符有三种:publicprotectedprivate。它们用在上述两处意义有所不同:
  2. - 当用于修饰声明的成员时:
  3. - public:公共成员,所有访问者都可以访问。
  4. - protected:受保护成员,除了类用户、派生类用户,都可以访问(派生类及其友元可访问)。此处要注意一种特殊情况,派生类及其友元只能通过派生类对象(基类类型对象不行)访问基类protected成员,见下面代码。
  5. - private:私有成员,只有类内部、类友元可访问。
  6. - 当用在类派生列表中时:
  7. - 用于批量控制基类成员在派生类中的访问权限,具体情况见下面代码。
  8. ```cpp
  9. // protected特殊情况:
  10. // 派生类成员、友元只能通过派生类对象访问基类protected成员,不能通过基类类型对象
  11. class Base {
  12. protected:
  13. int prot_mem;
  14. };
  15. class Derived : public Base {
  16. friend void fuck(Derived &);
  17. friend void fuck(Base &);
  18. }
  19. void fuck(Derived& d){
  20. std::cout << d.prot_mem << std::endl; // 正确
  21. }
  22. void fuck(Base& b){
  23. std::cout << b.prot_mem << std::endl; // 错误,必须通过派生类类型对象访问。
  24. }

访问说明符用于类派生列表时,对基类成员在派生类中的访问权限影响如下:

  1. // ***********************************************************************
  2. // **********派生访问说明符影响的是基类成员在子类中的访问说明符类型。总结如下:
  3. // ***********************************************************************
  4. // public继承,让基类成员在子类中保持原有访问权限
  5. //
  6. // 基类public成员 + public继承 = 在子类中是public成员
  7. // 基类protected成员 + public继承 = 在子类中是protected成员
  8. // 基类private成员 + public继承 = 在子类中是private成员
  9. //
  10. // protected继承,在派生类中保护所有的基类成员,即,使基类成员的原有访问权限都提升到protected
  11. // 以上,如果已经不低于protected,就保持不变,如protected、private
  12. //
  13. // 基类public成员 + protected继承 = 子类protected成员
  14. // 基类protected成员 + protected继承 = 子类protected成员
  15. // 基类private成员 + protected继承 = 子类private成员
  16. //
  17. // private继承,基类成员在子类中都变成private访问权限
  18. //
  19. // 基类public成员 + private继承 = 子类private成员
  20. // 基类protected成员 + private继承 = 子类private成员
  21. // 基类private成员 + private继承 = 子类private成员
  22. //

类型转换可访问性

前面已经讲了,派生类可以隐式转换成基类指针、引用类型。但这也受派生访问说明符的影响。总结一点就是:

在代码中的某个给定节点来说,如果基类的公有成员是可访问的,则这种类型转换也是可访问的,否则不行。

只有当public继承时,派生类用户才能进行这种转换。
而派生类内部和派生类友元,永远都能进行这种转换。

友元

友元关系不能传递,友元关系同样也不能继承。

  1. class Base{
  2. friend class shit;
  3. protected:
  4. void base();
  5. };
  6. class Derived : public Base{
  7. protected:
  8. void derived();
  9. };
  10. class shit{
  11. int f(Base b) { return b.base(); } // 正确:shit是base的友元
  12. int f2(Derived d) { return d.derived(); } // 错误:shit不是Derived的友元
  13. int f3(Derived s) { return s.base(); } // 正确:shit是Base的友元
  14. };
  15. class newshit : public shit{
  16. int f(Base b) { return b.base(); } // 错误:newshit不是base的友元
  17. int f2(Derived d) { return d.derived(); } // 错误:newshit不是Derived的友元
  18. int f3(Derived s) { return s.base(); } // 错误:newshit不是Base的友元
  19. }

修改访问权限

通过using来修改指定成员在基类中的访问权限。当然前提是这些成员在派生类中本身可以访问。因此不可能修改基类的private成员的访问权限。

  1. class Base {
  2. public:
  3. std::size_t size() const { return n; }
  4. protected:
  5. std::size_t n;
  6. };
  7. class Derived: private Base {
  8. public:
  9. using Base::size; // size原本应该是private的
  10. protected:
  11. using Base::n; // n原本应该是private的
  12. }

class、struct区别

class和struct并没有什么深层次的区别,唯一的区别就在默认访问权限上不同,class默认private,struct默认public。

  • 默认成员访问说明符不同
    • class:private
    • struct:public
  • 默认派生访问说明符不同
    • class:private
    • struct:public

      四、继承的拷贝控制

      虚析构函数

      基类必须定义虚析构函数,这样才能动态分配继承体系中的对象。 ```cpp

Base *bp = new Derived; delete bp; // 将触发析构函数,若析构函数不是虚函数,则不是多态调用,而是普通函数调用 // bp的静态类型是Base,则调用Base的析构函数,显然这是不正确的。 // 因此如果基类的析构函数不是虚函数,则delete 子类对象将很可能出错!

  1. 定义析构函数,会阻止编译器合成移动操作。
  2. <a name="7CXUY"></a>
  3. ## 合成的拷贝控制
  4. 基类、派生类的合成拷贝控制成员和其他合成拷贝控制成员是一样的功能,此外,这些合成的拷贝控制成员还负责使用**直接基类**对应的操作对派生类对象的对应基类部分进行拷贝控制操作(构造、赋值、析构)。<br />定义基类的方式可能会导致派生类的合成版本是删除的(deleting):
  5. - 基类的默认构造、拷贝构造、拷贝赋值、析构函数是删除的、不可访问的(编译器不会合成删除的移动操作)。因为派生类不能使用基类的对应操作。
  6. - 基类的析构是不可访问或删除的,则派生类合成的默认、拷贝构造是删除的,因为无法销毁派生类对象的基类部分。
  7. - 基类的析构是不可访问或删除的,则派生类的移动构造是删除的。
  8. - 当使用=default请求移动操作时,若基类对应操作是不可访问、删除的,则派生类对应也是删除的。
  9. ```cpp
  10. class B {
  11. public:
  12. B();
  13. B(const B&) = delete;
  14. ...... // 其他成员,不含有移动构造函数
  15. // 因为定义了拷贝构造函数,因此不会合成移动构造函数
  16. };
  17. class D : public B {
  18. // D(); // 合成的默认构造函数,调用B的默认构造函数
  19. // D(const D&) = delete; // 合成的删除的拷贝构造函数,因为基类是删除的
  20. // D没有移动构造函数,因为基类没有合成移动构造函数
  21. }
  22. D d; // 正确,D的合成的默认构造函数。
  23. D d2(d); // 错误,D的拷贝构造函数是delete的。
  24. D d3(std::move(d)); // 错误,D的拷贝构造函数是删除的。
  25. // 没有移动构造函数,因此匹配的是拷贝构造函数。

因为基类一般会定义虚析构函数,因此基类不会有合成的移动操作。所以如果需要移动操作,我们需要显式地进行定义。

  1. class Base {
  2. public:
  3. Base() = default;
  4. Base(const Base&) = default;
  5. Base(Base&&) = default; // 显式定义合成的移动构造
  6. Base& operator=(const Base&) = default;
  7. Base& operator=(Base&&) = default; // 显式定义合成的移动赋值
  8. virtual ~Base() = default; // 有析构,不会合成移动操作。
  9. }

派生类的拷贝控制

除了析构函数,派生类的拷贝控制成员需要完成(派生类+直接基类)的拷贝控制操作。其实也就是显式调用基类对应的拷贝控制操作。
析构函数只负责销毁所属类自己分配的资源。

  1. class Base {
  2. ......
  3. };
  4. class Derived : public Base {
  5. public:
  6. D(const D& d) // 派生类的拷贝构造函数
  7. : Base(d) { // 拷贝构造基类部分,显式调用基类的拷贝构造函数
  8. ...... // 拷贝构造派生类部分
  9. }
  10. D(D&& d) // 派生类的移动构造函数
  11. : Base(std::move(d)){ // 移动构造基类部分,显式调用基类的移动构造函数
  12. ...... // 移动构造派生类部分
  13. }
  14. D& operator=(const D& d){ // 派生类移动拷贝赋值运算符
  15. Base::operator=(d); // 拷贝赋值基类部分,显式调用基类拷贝赋值运算符。
  16. ...... // 拷贝赋值派生类部分
  17. }
  18. ~D()
  19. :~Base() // 错误,Base::~Base()会自动(隐式)执行
  20. {
  21. Base::~Base(); // 错误,Base::~Base()会自动(隐式)执行
  22. ...... // 销毁D自己分配的资源。
  23. // 析构函数的执行顺序
  24. // 1、派生类析构函数
  25. // a、D的析构函数体
  26. // b、成员析构,按类内成员声明的逆序。
  27. // 2、基类析构函数
  28. }
  29. }

五、继承的构造函数

派生类会“继承”直接基类的构造函数(除了继承默认、拷贝、移动构造函数)。
using声明的方式继承构造函数。一般using声明是使名字在当前作用域可见,但作用域构造函数时,编译器将产生代码,会在派生类中生成与所有基类构造函数对应的派生类版本。换句话,对于基类的每个构造函数,编译器都在派生类中生成一个形参完全相同的构造函数

  1. class Base{
  2. Base(const string& str);
  3. Base(const int i);
  4. Base(const float f);
  5. }
  6. class Derived : public Base{
  7. public:
  8. using Base::Base; // "继承"基类所有构造函数(除了默认、拷贝、赋值构造函数)
  9. // 编译器将生成以下代码:
  10. Derived(const string& str);
  11. Derived(const int i);
  12. Derived(const float f);
  13. }

和普通的using声明还有不同,就是无法改变构造函数的访问权限(基类是什么样派生类中就是什么样),即使在派生类中显式声明,也不能改变explicit、constexpr。
如果基类构造函数含有默认实参,则在派生类中对应的会有多个构造函数,每个对应省掉一个有默认实参的形参。

  1. class Base {
  2. public:
  3. Base(int a, int b = 1);
  4. }
  5. class Derived : public Base {
  6. public:
  7. using Base::Base; // “继承”基类构造函数,编译器产生的代码如下:
  8. Derived(int a){
  9. }
  10. Derived(int a, int b){
  11. }
  12. }

显式自定义的构造函数会覆盖继承来的构造函数(如果形参列表完全相同)。
注意,默认构造、拷贝、赋值构造函数不会继承,但会按规则合成。
且继承的构造函数不会当做是用户自定义,因此如果只有继承的构造函数,还会合成一个默认构造函数。

六、容器与继承

当使用容器保存继承体系中的对象时,我们可能采用以下方法:

  1. // Base: 基类
  2. // Derived: 派生类
  3. // vector<Base>来保存
  4. vector<Base> vec;
  5. vec.push_back(Base("motherfucker")); // 正确
  6. vec.push_back(Derived("asdf", 1)); // 编译通过,但只会保存Derived的Base部分。
  1. vector<shared_ptr<Base>> shit
  2. shit.push_back(make_pair<Base>("fuck")); // 保存Base对象
  3. // 派生类的智能指针可以隐式转换成基类的指针指针类型。
  4. shit.push_back(make_shared<Derived>("fuck1", 1)); // 保存Derived对象