C Plus学习笔记之二

  • 基类与派生类的关系:
    • 派生类对象可以使用基类的方法,条件是方法不是私有的。
    • 基类指针可以在不进行显示类型转换的情况下指向派生类对象。
    • 基类引用可以在不进行显示类型转换的情况下引用派生类对象。
  • C++有三种继承关系:公有继承、保护继承和私有继承。

    • 公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行任何操作,也可以对派生类对象执行。注意:is-a关系通常是不可逆的,也就是说,水果不是香蕉。公有继承不能建立has-ais-like-ais-implemented-as-auses-a关系。
    • 多态公有继承:在派生类中重新定义基类方法。使用虚方法virtual,该关键字只出现在方法原型中。
      对于虚函数,程序将根据对象类型来确定使用哪个版本。对于两个对象中行为相同的方法,只在基类中声明。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
      方法在基类中声明为虚拟的后,它在派生类中将自动成为虚方法,一般也都在派生类中指出。为基类声明一个虚拟析构函数也是一种惯例,可以确保正确的析构函数被调用。一般先调用派生类的析构函数,再调用基类的析构函数。


      友元不能是虚函数,只有成员才能是虚函数,但可以让友元函数使用虚拟成员函数。
      可以使用数组来表示多种类型的对象,这就是多态性。
    • 访问控制protected:关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。protected与private的区别只有在基类派生的类中才能表现出来。派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      class TheOnlyInstance
      {
      public:
      static TheOnlyInstance * GetTheOnlyInstance();
      protected:
      TheOnlyInstance() {}
      }

      TheOnlyInstance* TheOnlyInstance::GetTheOnlyInstance()
      {
      static TheOnlyInstance objTheOnlyInstance;
      return &objTheOnlyInstance;
      }

      int main()
      {
      TheOnlyInstance noCanDo; //not allowed
      TheOnlyInstance * pTheOnlyInstance = TheOnlyInstance::GetTheOnlyInstance(); //以后调用,将返回同一个静态地址
      }
    • 使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生类公有接口的一部分,但可以在派生类的成员函数中使用它们。这种不完全继承是has-a关系的一部分,其特性与包含相同。
      包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员,这是两种方法的主要区别。
      成员初始化列表使用std::string(str),而不是name(str),这里string是基类。这是包含和私有继承之间的第二个主要区别。
      使用作用域解析操作符可以访问基类方法,但如果要使用基类对象本身,可以使用强制类型转换:

      1
      2
      3
      4
      const string & Student::Name() const
      {
      return (const string &) *this;
      }

    用类名显示地限定函数名不适合于友元函数,这是因为友元不属于类。不过可以显式的转换为基类来调用正确的函数。另一方面,如果不使用类型转换,由于使用的多重继承,编译器将无法确定转换成哪个基类。<例子>

    1
    2
    3
    4
    5
    ostream & operator(ostream & os, const Student & stu)
    {
    os << "Scores for " << (const string &) stu << ":\n";
    ...
    }

    私有继承所提供的特性比包含多,但会引发许多问题。
    私有继承可以重新定义虚函数,但也只能在类中使用。

    • 保护继承是私有继承的变体。基类的公有成员和保护成员都将成为派生类的保护成员,与私有不同,第三代的派生类能使用保护成员。
      <img src="/images/dif.png">
      
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。
    在C++中,动态联编与指针和引用调用的方法相关,从某中程度上说,这是由继承控制的。编译器对非虚函数采用静态编联。也就是说,当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。
    派生类的虚函数的返回类型形参类型必须与基类函数匹配,否则会隐藏同名的基类方法。只有一个例外,当类的虚函数的返回类型是类本身的指针或引用时,这称为返回类型协变。
    如果基类声明被重载了,则应在派生类中重新定义所有的基类版本,否则其他的版本都将被隐藏。
    将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这时公有继承不需要进行显式类型转换,这种转换是可以传递的。
    仅将那些预期将被重新定义的方法声明为虚拟的。构造函数不能是虚函数,析构函数应当是虚函数。

  • 编译器处理虚函数的方法:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表。
    虚函数的实现原理:
    虚函数的实现需要利用虚表(Virtual Table)和虚表指针(Virtual Table Pointer)。虚表是一个指针数组,它存储了类的虚函数的地址,虚表指针是一个指针,指向该对象所属的类的虚表首地址。在调用虚函数时,程序通过虚表指针找到虚表,再根据函数的索引(函数在虚表中的下标)找到对应的虚函数并执行。当一个类被继承时,派生类会继承父类的虚表地址,并在其中添加新的虚函数。如果派生类需要覆盖父类的虚函数,那么它会把新的虚函数地址写入虚表中对应的位置。这样就实现了运行时的多态性。
    虚函数表的创建:虚函数表在编译时由C++编译器自动生成。对于每个具有虚函数的类,编译器会生成一个虚函数表,其中包含了该类的虚函数指针。这个虚函数表通常位于类的内部,它是静态的,一旦创建就不会更改。
    虚函数表的指针:对于每个包含虚函数的类,编译器会在类的内部添加一个指向虚函数表的指针,通常位于对象的内存布局的最前面。这个指针被称为虚函数表指针(vptr)。
    运行时动态绑定:当你通过基类指针或引用调用虚函数时,实际执行的是派生类中的版本(如果派生类重新定义了虚函数)。这是因为虚函数表指针(vptr)指向了正确的虚函数表,允许在运行时根据对象的类型进行动态绑定。

    1
    2
    Base* ptr = new Derived;
    ptr->foo(); // 调用Derived类中的foo()

  • 虚函数指针会不会变,什么时候初始化,在析构里会不会变,析构函数能访问虚函数吗?
    初始化时创建:虚函数表指针在对象创建时就被初始化。当你创建一个类的实例(对象)时,其中包括一个指向该类虚函数表的虚函数表指针。这个指针是在对象构造过程中初始化的。
    不会在析构函数中改变:虚函数表指针通常在对象的整个生命周期内保持不变。这意味着即使在对象的析构函数中,虚函数表指针也不会改变。析构函数是用于对象销毁的,不负责改变虚函数表指针。
    虚函数的调用:虚函数表指针的主要作用是支持运行时多态性。通过这个指针,程序可以在运行时查找并调用正确的虚函数版本。在析构函数中,你可以调用虚函数,但需要注意的是析构函数本身不会改变虚函数表指针。在析构函数中调用虚函数时,通常执行的是对象的类型,而不是基类的类型。
    注意虚函数表指针变化的情况:在一些特殊情况下,虚函数表指针可能会变化。例如,当一个对象通过复制构造函数复制时,复制的对象将有自己的虚函数表指针。这通常发生在基类和派生类之间的复制。另外,如果你使用虚继承,虚函数表指针也可能会更改。
  • 抽象基类:当类声明中包含纯虚函数时,则不能创建该类的对象。要真正的成为抽象基类,则至少应包含一个纯虚函数。原型中的=0使虚函数成为纯虚函数。C++允许纯虚函数有定义,也可以不定义。纯虚方法是定义派生类的通用接口。
  • 如果基类派生类都使用动态内存分配,则必须为派生类定义显式析构函数、复制构造函数和赋值操作符,也就是说,必须使用相应的基类方法处理基类元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class hasDMA : public baseDMA
    {
    private:
    char * style;
    public:
    ...
    };

    hasDMA & hasDMA::operator =(const hasDMA & hs)
    {
    if (this == &hs)
    return *this;
    baseDMA::operator =(hs); // 重点!
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
    return *this;
    }
  • 派生类对象的友元函数可以通过基类的友元函数访问基类的成员,由于友元不是成员函数,友元函数不能继承,不能使用作用预解析符,所以可以相应类强制类型转换选择正确的函数。<例子>

    1
    2
    3
    4
    5
    6
    7
    std::ostream & operator <<(std::ostream & os, const hasDMA & hs)
    {
    os << (const baseDMA &)hs; // 相应类强制类型转换
    //也可以:os << dynamic_cast<const baseDMA &> (hs)
    os << "Style: " << hs.style << std::endl;
    return os;
    }
  • 通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。
  • 包含对象成员的类:使用公有继承时,类可以继承接口,可能还有实现。获得接口是is-a关系的组成部分,而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。

    对比私有继承:

    对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如,string类将+操作符重载为将两个字符串连接起来;但从概念上说,将两个Student对象串接起来是没有意义的。
    被包含对象的接口不是公有的,但可以在类方法中使用它。<例子>

    1
    2
    3
    4
    5
    6
    7
    double Student::Average() const
    {
    if (scores.size() > 0)
    return scores.sum() / scores.size();
    else
    return 0;
    }

    私有辅助方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #=> 位于private
    ostream & Student::arr_out(ostream & os) const
    {
    int i;
    int lim = scores.size();
    if (lim > 0)
    {
    for (i = 0; i < lim; i++)
    {
    os << scores[i] << " ";
    if (i % 5 != 0)
    os << endl;
    }
    }
    else
    os << " empty array ";
    return os;
    }
  • 使用using重新定义访问权限:

    • 方法一是定义一个使用该基类方法的派生类方法。

      1
      2
      3
      4
      double Student::sum() const
      {
      return std::valarray<double>::sum();
      }
    • 另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(将像名称空间那样)来指出派生类可以使用的特定的基类成员,即使使用的是私有派生。注意:using 声明只使用成员名——没有圆括号、函数特征标和返回类型。using声明只适合继承,而不适用于包含。

      1
      2
      3
      4
      5
      6
      7
      8
      class Student : private std::string, private std::valarray<double>
      {
      ...
      public:
      using std::valarray<double>::min;
      using std::valarray<double>::max;
      ...
      }
  • 虚基类:虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如:通过在类声明中使用关键字virtual, 可以使Worker被用作Singer和Waiter的虚基类

    1
    2
    class Singer : virtual public Worker { ... };
    class Waiter : public virtual Worker { ... };

    可以将SingingWaiter类定义为

    1
    class SingingWaiter : pulic Singer, public Waiter { ... };
  • 友元类:

    • 当一个类B成为了另外一个类A的“朋友”时,那么类A的私有和保护的数据成员就可以被类B访问。我们就把类B叫做类A的友元。
    • 友元类可以通过自己的方法来访问把它当做朋友的那个类的所有成员。但是我们应该注意的是,我们把类B设置成了类A的友元类,但是这并不会是类A成为类B的友元。说白了就是:甲愿意把甲的秘密告诉乙,但是乙不见得愿意把乙自己的秘密告诉甲。
    • 声明友元类的方法其实很简单,只要我们在类A的成员列表中写下语句:friend class B;这样一来,类B就被声明成了类A的友元。注意,类B虽然是类A的友元,但是两者之间不存在继承关系。这也就是说,友元类和原来那个类之间并没有什么继承关系,也不存在包含或者是被包含的关系。

core::common::Singleton 是一个模板类,通常用于实现单例模式。通过将 core::common::Singleton 声明为 GlobalConfigure 的友元类,core::common::Singleton 可以在实现单例模式时,访问 GlobalConfigure 中的私有构造函数、析构函数或其他私有成员,以确保单例模式的正确实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Forward declaration
namespace core::common {
template <typename T>
class Singleton;
}

class GlobalConfigure: public core::common::Singleton<GlobalConfigure> {
friend class core::common::Singleton<GlobalConfigure>;

private:
// 私有构造函数
GlobalConfigure() {
// 初始化逻辑...
}

public:
// 公有接口...
};

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!