Chapter 4 Classes – A Tour of C++

4.1 Introduction

本章节中,我们把class分为以下三个种类:

  • Concrete class
  • Abstract class
  • Class层级中的class

4.2 Concrete Types

concrete class的定义就类似于c++内建的类型。例如,complex类型与拥有无限精度的整数类型,以及vector与string。

concrete类型最重要的特点就是,其representation(可以理解为该对象中的变量)只是该类型的定义的一部分。例如vector,在许多情况下,其包含了一个或者多个指向其他数据的指针。这也使得concrete类型的实现能在时间与空间上具有高效性。这也使得concrete类型具有以下特性:

  • 将concrete类型的对象存储在stack上,在静态分配的内存中,或者在其他对象内;
  • 直接引用对象(不单单通过指针或者引用);
  • 快速并完整地初始化对象(例如通过构造函数);
  • copy(拷贝)并且移动(move)对象。

4.3 An Arithmetic Type

class complex
{
private:
    double re, im; // representation: 两个double类型变量

public:
    complex(double r, double i) :re{r}, im{i} {}
    complex(double r) :re{r}, im{0} {}
    complex(): :re{0}, im{0} {}

    double real() const { return re; }
    void real(double d) { re = d; }
    double imag() const { return im; }
    void imag(double d) { im = d; }

    complex& operator+=(complex z)
    {
        re+=z.re;
        im+=z.im;
        return *this;
    }

    complex& operator-=(complex z)
    {
        re-=z.re;
        im-=z.im;
        return *this;
    }

    complex& operator*=(complex);
    complex& operator/=(complex);
};

上述代码是简化版的标准库complex类型。class的定义中,我们只需要读取complex自身,同时其只包含两个double类型的变量。此外,complex所包含的函数的实现都非常简单,其内部并不需要调用其他函数,因此这些函数都是inline的。class内定义的函数,默认都是inline的,不过我们也可以通过关键词inline来显示地将其声明为inline。

代码中,我们使用了const修饰成员函数,其表示这些函数并不会修改调用该函数的对象。此外,const成员函数能同时被const与非const对象调用,但是非const成员函数只能由非const对象调用。

4.3 Abstract Types

所谓abstract type指的就是用户并不知道代码的实现细节。我们需要将接口从representation中分离出来并且放弃实际的局部变量。由于我们并不知道abstract type的组成与实际尺寸,因此必须将其对象分配在heap中并且通过引用和指针读取这些对象。

class Container
{
public:
    virtual double& operator[](int) = 0; // 纯虚函数
    virtual int size() const = 0;
    virtual ~Container() {}
};

上述代码中的=0表示该函数为纯虚函数;也就是说继承Container的class必须定义该函数。因此,我们不能单纯定义一个Container对象。

Container c; // error!!! abstract class没有对象
Container* p = new Vector_container(10); // 运行ok,Container只是一个interface

一般来说,abstract class并不需要构造函数,因为其不需要对任何数据进行初始化。但是,其会拥有析构函数,而且析构函数为virtual,之后继承该abstract class的class能够提供相应的实现。

为了使上述Container能够被我们使用,我需要顶一个新的class能够实现其接口。此时,我们将使用concrete class Vector:

class Vector_container : public Container
{
public:
    Vector_container(int s) : v(s) {}
    ~Vector_container() {}

    double& operator[](int i) override { return v[i]; }
    int size() const override { return v.size(); }

private:
    Vector v;
};

需要注意的是Vector_container的析构函数将会重载基类Container的析构函数。此外,成员变量Vector的析构函数将在Vector_container的析构函数中被隐示地调用。

如果有一个函数use(Container&),Container作为引用参数,那么所有继承Container的对象都能作为参数传入。这样的话,即使我们改变了子类的实现模式,也不需要对use(Container&)进行重新编译,但是这么做的缺点是对象必须是指针或者引用。

4.4 Virtual Functions

虚函数的实现原理一般为编译器将虚函数的名称转为一个index索引并放入指向函数的指针列表。该列表通常被称为virtual function table虚函数表,也可以简写为vtbl。每一个拥有虚函数的类都有自己的vtbl,其定义了该类的虚函数。

即使调用者并不知道对象的尺寸与数据的排布,其通过vtbl仍然能正确地调用对应的函数。调用者只需要知道指向vtbl的指针的地址与相对应的虚函数的索引即可。这一虚函数的调用机制与普通的函数调用机制有着相似的效率(相差小于25%)。其空间上的额外开销是该类的每一个对象的一个额外函数指针以及每一个类对应一个额外的vtbl。

4.5.2 Hierarchy Navigation

假设基类Shape有多个子类Circle,Smiley等等,那么我们可以使用运算符dynamic_cast来检测,某一个Shape对象是不是一种Smiley。

Shape* ps {read_shape(cin)};

if(Smiley* p = dynamic_cast<Smiley*>(ps))
{
    // 指针ps指向Smiley
}
else
{
    // 指针ps并不是Smiley,那么将会返回nullptr
}

4.5.3 Avoiding Resource Leaks

为了防止资源或者说内存泄露,我们通常会使用标准库中的智能指针而不是“裸露指针”。

class Smiley : public Circle
{
private:
    vector<unique_ptr<Shape>> eyes;
    unique_ptr<Shape> mouth;
};

这么做的一个好处是我们不必再定义Smiley的析构函数。编译器将会隐示地生成一个析构函数,用来销毁vector中的unique_ptr。使用unique_ptr的代码与使用原生指针的代码有着几乎相同的效率。

unique_ptr<Shape> read_shape(istream& is)
{
    switch(k)
    {
        case Kind::circle:
            return unique_ptr<Shape>{new Circle{p, r}};
    }
}

void user()
{
    vector<unique_ptr<Shape>> v;
    while(cin)
        v.push_back(read_shape(cin));
    
    draw_all(v);
    rotate_all(v, 45);
} // 所有的shape将被隐示地销毁

4.6 Advice

[02] concrete类型是最为简单的class类型。其优于复杂的class类型也优于空白的数据struct类型。

[03] 只使用concrete class来表示最为简单的理念。

[10] 如果一个成员函数并不需要改变其对象的状态,那么将其声明为const。

[13] 如果一个class是container,那么赋予其initializer-list的构造函数。

[15] 通过指针与引用来读取多态对象。

[16] 一个abstract class并不需要构造函数。

[18] 如果一个class拥有虚函数,那么其应该拥有虚析构函数。

[21] 如果需要分辩子类的类型,那么使用dynamic_cast。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据