C/C++你必须了解的小知识(17)

161 阅读1分钟

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

1.3.12 简述一下 C++ 中的多态

由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。 多态分为静态多态和动态多态:

  1. 静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。

    比如一个简单的加法函数:

    include<iostream>
    using namespace std;
    
    int Add(int a,int b)//1
    {
        return a+b;
    }
    
    char Add(char a,char b)//2
    {
        return a+b;
    }
    
    int main()
    {
        cout<<Add(666,888)<<endl;//1
        cout<<Add('1','2');//2
        return 0;
    }
    

    显然,第一条语句会调用函数1,而第二条语句会调用函数2,这绝不是因为函数的声明顺序,不信你可以将顺序调过来试试。

    1. 动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:

      1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
      2. 通过基类类型的指针或引用来调用虚函数。

      说到这,得插播一条概念:重写——也就是基类中有一个虚函数,而在派生类中也要重写一个原型(返回值、名字、参数)都相同的虚函数。不过协变例外。协变是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。

      //协变测试函数
      #include<iostream>
      using namespace std;
      
      class Base
      {
      public:
          virtual Base* FunTest()
          {
              cout << "victory" << endl;
              return this;
          }
      };
      
      class Derived :public Base
      {
      public:
          virtual Derived* FunTest()
          {
              cout << "yeah" << endl;
              return this;
          }
      };
      
      int main()
      {
          Base b;
          Derived d;
      
          b.FunTest();
          d.FunTest();
      
          return 0;
      }
      

1.3.13 说说为什么要虚析构,为什么不能虚构造

  1. 虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。

    1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
    2. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。

    C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

  2. 不能虚构造:

    1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用。(悖论)
    2. 从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
    3. 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

1.3.14 说说模板类是在什么时候实现的

  1. 模板实例化:模板的实例化分为显示实例化和隐式实例化,前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的

  2. 模板具体化:当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。

  3. 代码示例:

    #include <iostream>
    using namespace std;
    
    // #1 模板定义
    template<class T>
    struct TemplateStruct
    {
        TemplateStruct()
        {
            cout << sizeof(T) << endl;
        }
    };
    
    // #2 模板显示实例化
    template struct TemplateStruct<int>;
    
    // #3 模板具体化
    template<> struct TemplateStruct<double>
    {
        TemplateStruct() {
            cout << "--8--" << endl;
        }
    };
    
    int main()
    {
        TemplateStruct<int> intStruct;
        TemplateStruct<double> doubleStruct;
    
        // #4 模板隐式实例化
        TemplateStruct<char> llStruct;
    }
    

    运行结果:

    4
    --8--
    1