cpp notes for class

CPP notes for class

这个文档主要记录在使用CPP进行面向对象开发时所使用到的CPP中类与继承的相关概念,主要是为了理清几个CPP类与继承中几个比较常见的概念,比如成员初始化列表,访问权限尤其是派生类和基类之间的访问关系,抽象基类和虚函数。

这里也会放上一些项目实战中的代码。

场景描述

因为这个文档主要是记录在我实际的项目开发中的遇到的问题,所以这里有必要对项目开发的场景进行一个简单的介绍。

项目场景是在Memory trace-driven的缓存仿真器中开发一个预取器。但是这个预取器有多种不同的类型,比如顺序预取器以及步幅预取器。而预取器最终会在缓存模型中进行实例化,所以为了利用C++的类多态特性,很容易想到先开发一个基类: HardwarePrefetcher,然后再由这个基类派生出两个不同的派生类,StridePrefetcher, SequentialPrefetcher,然后再在缓存模型中创建一个基类指针:

1
HardwarePrefetcher *prefetcher;

因为基类指针能够在不进行类型转换的情况下指向派生类的对象,所以能够让缓存模型在初始化的时候根据配置文件的信息实例化不同的派生类,然后再利用基类指针来调用派生类中的定义的方法。

成员初始化列表

基类的代码相对比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class HardwarePrefetcher
{
public:
unsigned int block_size;

// the below is some open interfaces
// <latency, prefetch_addr>
std::queue< std::pair<int, long> > prefetch_queue;

HardwarePrefetcher() : block_size(64) {}
HardwarePrefetcher(unsigned int blcksz) : block_size(blcksz) {}

void set_base(long req_addr, long pc)
{
req_addr = req_addr;
pc = pc;
}

// main mothod of prefetcher
virtual void tick()=0;
protected:
long req_addr;
long pc;
};

成员初始化列表只能用于类的构造函数,通用格式如下:

Constructor(parameter-list) : member1(parameter1), member2(parameter2), … { }

关于成员初始化列表需要注意的是,成员初始化列表只能用于构造函数,并且构造函数中的参数要么全都有默认值,要么全都没有默认值。此外,如果是从基类中继承的保护成员,那么将无法使用成员初始化列表进行初始化

类的继承与访问权限

这里主要讨论公有继承:

1
2
3
4
class StridePrefetcher : public HardwarePrefetcher
{
...
};

基类的公有成员将成为派生类的公有成员;基类的私有成员也会成为派生类的一部分,但是只能通过基类提供的接口进行访问。

此外,还有一个比较重要的关系是,基类的指针或者引用可以在不进行显示类型转换的情况下,指向派生类的对象。然而,基类的指针或者引用只能调用基类的方法。并且这种关系是单向的,也就是说不能使用派生类的指针或者引用指向基类的对象。

CPP类的访问权限限定符:

Access public protected private
member of the same class yes yes yes
member of the derived class yes yes no
not members yes no no

注意上面的同类的对象也只能访问自己的私有成员。

虚函数和抽象基类

前面提到过,在C++中可以在不进行显式类型转换的前提下使用基类的指针或者引用指向派生类的对象,并且能够使用基类指针或引用调用基类中的方法。而另一方面,使用公有继承得到的派生类会继承基类中的公有方法,派生类能够对这些方法进行重写,这就是C++中的类多态(class polymorphism)。

所以在使用基类指针或引用指向派生类的对象时,如果想要通过基类指针指向派生类中重写的方法,就需要使用虚函数。

在一个成员函数的声明语句前面使用关键字virtual能够使这个函数称为虚函数。如果没有使用关键字virtual,程序将根据指针类型或者引用类型选择方法,也就是使用基类指针或引用将会直接调用基类中的方法;如果使用了关键字virtual,程序将根据指针或引用所指向的对象的类型来选择方法,所以如果使用基类指针指向一个派生类对象,将会调用派生类中的方法。

对于虚函数,需要注意的是,只有函数被重新定义的时候才有必要将其声明为虚函数。注意重新定义并不是指重载,换句话说,虚函数提供了一种重新定义基类函数的方法,重新定义将不只是使用相同参数列表函数声明覆盖基类中的函数声明,无论参数列表是否相同,虚函数都会直接隐藏基类中的方法。不过如果修改了参数列表后,基类指针将无法识别到派生类中重新定义的方法。所以派生类在重新定义虚函数的时候尽量保持参数列表的一致,但是虚函数的返回值可以不必和基类中的保持一致。

虚函数表

如果没有将方法声明为虚函数,那么程序可以直接根据指针类型将函数调用绑定到对应的代码块,这个过程可以在编译时完成,称为静态绑定,或者早期绑定。

如果将方法声明为虚函数,那么只有在运行时才能够知道指针指向的对象类型,所以只能够在程序运行时进行绑定,这个过程为称为动态绑定,或者晚期绑定。

虽然动态绑定能够让程序在运行时进行决策,当时这样的方法需要采取额外的机制来跟踪基类指针指向的对象的类型。

虚函数表(Virtual Function Table, vtable)就是用于跟踪基类指针指向的对象类型的机制,vtable可以看作是多态类中的一个隐藏静态数据成员(hidden static data member)。每一个多态类的对象都会与一个vtable相关联,即多态类对象中都包含一个隐藏的指针指向vtable。所以程序能够在运行时通过这个指向vtable的指针来判定指针指向的对象的类型。

如果派生类中对虚函数进行了重新定义,那么该虚函数表中将会保存新函数的地址;如果派生类中没有重新定义虚函数,那么虚函数表中将会保存基类中的定义的函数的地址。调用虚函数时,程序将查看对象中的虚函数表,如果跳转到对应的虚函数表条目。

纯虚函数和抽象基类

在虚函数声明语句后面加上"=0"可以将其声明为纯虚函数(pure virtual function)。

1
virtual void tick()=0;

如果类声明中包含纯虚函数,则无法创建这种类型的对象,这样的类只能用作基类,也就是抽象基类(Abstract Bass Class)。可以在方法文件中给出纯虚函数的定义,也可以不给出纯虚函数的定义。

可以将抽象基类看作是一种必须实施的接口,抽象基类要求具体派生类重新定义其纯虚函数,从而迫使派生类遵循抽象基类设置的接口规则。


cpp notes for class
https://2hyan9.github.io/2022/11/13/cpp-notes-for-class/
作者
2hYan9
发布于
2022年11月13日
许可协议