C/C++你必须知道的小知识(19)

143 阅读4分钟

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


1.3.9 说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序

参考答案

  1. 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);

  2. 如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)

  3. 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;

  4. 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;

  5. 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)

  6. 综上可以得出,初始化顺序:

    父类构造函数–>成员类对象构造函数–>自身构造函数

    其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。

    析构顺序和构造顺序相反。

1.3.10 简述下向上转型和向下转型

  1. 子类转换为父类:向上转型,使用dynamic_cast(expression),这种转换相对来说比较安全不会有数据的丢失;
  2. 父类转换为子类:向下转型,可以使用强制转换,这种转换时不安全的,会导致数据的丢失,原因是父类的指针或者引用的内存中可能不包含子类的成员的内存。

1.3.11 简述下深拷贝和浅拷贝,如何实现深拷贝

  1. 浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子,你的小名叫西西,大名叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。

  2. 深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

  3. 深拷贝的实现:深拷贝的拷贝构造函数和赋值运算符的重载传统实现:

    STRING( const STRING& s )
    {
        //_str = s._str;
        _str = new char[strlen(s._str) + 1];
        strcpy_s( _str, strlen(s._str) + 1, s._str );
    }
    STRING& operator=(const STRING& s)
    {
        if (this != &s)
        {
            //this->_str = s._str;
            delete[] _str;
            this->_str = new char[strlen(s._str) + 1];
            strcpy_s(this->_str, strlen(s._str) + 1, s._str);
        }
        return *this;
    }
    

    这里的拷贝构造函数我们很容易理解,先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制到目标拷贝对象 , 那么这里的赋值运算符的重载是怎么样做的呢?

    这种方法解决了我们的指针悬挂问题,通过不断的开空间让不同的指针指向不同的内存,以防止同一块内存被释放两次的问题。

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