关于C++:STL容器模板类的内部实现-array

关于C++:STL容器模板类的内部实现-array

本小白最近在学习C++中STL的相关知识,特在此做个笔记
今天看到了array类的内部实现,特此来整理一下所学

关于using关键字的使用:

在洛谷刷题时常见的用法:

using namespace std;//标准命名空间

用以指定命名空间在程序中全局可见,这种做法想必很普遍了。
现在要说的用法是第二类用法:

今天新学到的用法:

using ll=long long;//ll作为long long类型的别名

作用与c中的typedef相同,不同的是,typedef无法高效的重定义模板类型。
在在 C++98/03 中往往不得不这样写:

template <typename Val>
struct str_map
{
    typedef std::map<std::string, Val> type;
};
// ...
str_map<int>::type map1;
// ...

而在C++11中出现了一个可以重定义模板的语法:

template 
using str_map_t = std::map;
// ...
str_map_t map1;

此种用法在今天的array实现中常有见到,另外using还有关于类继承方式的用法,但今日暂且不提。

array模板类的使用:

暂且赘述array容器的使用方法
首先是array类的声明:

    array<int, 10> arr;

这便声明了一个元素种类为int,元素个数为10,名为arr的array类对象。
我们可以像之前使用c语言的数组一样使用它:

    array<int,10> arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    // array容器声明方法:array<元素类型> 容器名{初始化可不写};
    //此时可以像普通数组一样使用vector,如下
    int *parr = &arr[5];
    cout << arr[0] << ' ' << *(parr + 1) << endl;

输出为:

1 7

我们还可以使用迭代器,所谓迭代器,是泛型编程的组成部分,用来遍历我们的数据结构,访问数据集合中的元素:

    array<int, 10>::iterator i;//这个i就是适用于array的迭代器
    cout << "Uninitialized:" << endl;
    for (i = arr.begin(); i < arr.end(); i++) {
        cout << *i << ' ';
    }
    arr[5] = 100;
    cout << endl;
    for (i = arr.begin(); i < arr.end(); i++) {
        cout << *i << ' ';
    }

输出为:

Uninitialized:
-734660048 32764 18 0 -1181673808 674 -1932214893 32764 -734660048 32764
-734660048 32764 18 0 -1181673808 100 -1932214893 32764 -734660048 32764

可见,迭代器的使用方式类似于指针,实际上,对于int型数据,迭代器的内部实现就是int*,也就是指向int类型的指针。

array类的内部:

在我们的IDE环境里,右击我们代码里array单词,选择查看定义,我们应该可以打开我们的环境变量目录里的array头文件(我试了vscode与visual studio 2022都可以,别的可以试一下),我们可以看见近千行的代码,别慌,现在的光标应该移动到了array类的定义上,我来带着你刨析它。

以下头文件的内容来源:

// array standard header

// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

首先是array类的定义(大小不为零的那种奥):

template <class _Ty, size_t _Size>
class array { // fixed size array of values(这句是官方注释,哈哈哈。意为固定大小的数据数组)
public:
    using value_type      = _Ty;
    using size_type       = size_t;
    using difference_type = ptrdiff_t;
    using pointer         = _Ty*;
    using const_pointer   = const _Ty*;
    using reference       = _Ty&;
    using const_reference = const _Ty&;

    using iterator       = _Array_iterator<_Ty, _Size>;
    using const_iterator = _Array_const_iterator<_Ty, _Size>;

    using reverse_iterator       = _STD reverse_iterator<iterator>;
    using const_reverse_iterator = _STD reverse_iterator<const_iterator>;
    ......
    _Ty _Elems[_Size];//这是个我们常用的基本数组。
};

在这里,[Size_t]即unsigned long long。

可以看到array是个模板类,根据我们定义对象的写法也可以看出,在这里[_Ty]是一个数据元素的类型,[_Size]是数据的数量(也就是array的大小),它们为占位符,由实例化时的输入指定。
记住这一点,我们接下来会涉及到C++模版的相关知识。

我们之前提到的using用法在这里看到了,我们知道它是用来给类型起别名的,而且对模板类适用。

可以看到[value_type]即值类型重定义为[_Ty],[pointer]即指针类型为[_Ty*](即为指向[_Ty]类型的指针类型),[iterator]即迭代器类型为[_Array_iterator<_Ty, _Size>],这是一个模版类的实例。

在我们之前写的迭代器使用小试的代码里,我们有这样的语句:

    array<int, 10>::iterator i;//这个i就是适用于array的迭代器
    for (i = arr.begin(); i < arr.end(); i++) {
        cout << *i << ' ';
    }

可以看到i的类型是array的迭代器,那么arr.begin()这个array类的成员函数的返回值类型应该也是一个迭代器。
那么我们就以begin()的实现来研究一下。

在array.hpp文件,我们可以通过现代编辑器的大纲功能看到文件的结构(或者搜索也行),以找到begin()的定义,应该在array类的内部:

    _NODISCARD _CONSTEXPR17 iterator begin() noexcept {
        return iterator(_Elems, 0);
    }

[_NODISCARD]和[_CONSTEXPR17]和[noexcept]都是和编译方式或异常处理有关的,我们暂且不要理他。
可以看到它的返回值是iterator(_Elems, 0),那么这玩意又是啥呢,诶等等,我们好像见过它,还记得array类内的一堆重定义语句嘛,就是我们之前那段代码;

    using iterator       = _Array_iterator<_Ty, _Size>;

继续查找[_Array_iterator]的定义,这又是一个模板类:

template <class _Ty, size_t _Size>
class _Array_iterator : public _Array_const_iterator<_Ty, _Size> {
public:
    using _Mybase = _Array_const_iterator<_Ty, _Size>;
//条件编译不用管
    using iterator_category = random_access_iterator_tag;
    using value_type        = _Ty;
    using difference_type   = ptrdiff_t;
    using pointer           = _Ty*;
    using reference         = _Ty&;
    ......

那么我们之前看见的begin()的返回值便是这个类,而且返回时还应调用了这个[_Array_iterator]类的构造函数,那我们就来看看。

PS:继续深入ing。

    _CONSTEXPR17 explicit _Array_iterator(pointer _Parg, size_t _Off = 0) noexcept : _Mybase(_Parg, _Off) {}

一样,不管[_NODISCARD]、[_CONSTEXPR17]和[noexcept],我们看到[_Array_iterator]类的构造函数唯一做的就是使用输入的[_Elems]和[0]初始化了[_Mybase],而[_Mybase]在上面见到,是[_Array_const_iterator<_Ty, _Size>]的重定义。

为啥又有了个常量的迭代器啊2333
唉,都走到这一步了。

来看:

template <class _Ty, size_t _Size>
class _Array_const_iterator
//中间条件编译不用管
{
public:
//中间条件编译不用管
    using iterator_category = random_access_iterator_tag;
    using value_type        = _Ty;
    using difference_type   = ptrdiff_t;
    using pointer           = const _Ty*;
    using reference         = const _Ty&;
    enum { _EEN_SIZE = _Size }; // helper for expression evaluator(又一个官方注释,可惜我不知道这是干啥的)
//中间条件编译不用管
    _CONSTEXPR17 _Array_const_iterator() noexcept : _Ptr(), _Idx(0) {}//这个我们没有用到

    _CONSTEXPR17 explicit _Array_const_iterator(pointer _Parg, size_t _Off = 0) noexcept : _Ptr(_Parg), _Idx(_Off) {}

    _NODISCARD _CONSTEXPR17 reference operator*() const noexcept {
        return *operator->();
    }
    _NODISCARD _CONSTEXPR17 pointer operator->() const noexcept {
        //中间异常相关不用管
        return _Ptr + _Idx;
    }
//中间别的功能实现不用管
private:
    pointer _Ptr; // beginning of array
    size_t _Idx; // offset into array
//中间条件编译结束不用管
};

可以看到[_Array_const_iterator<_Ty, _Size>]有两个成员:[_Ptr]即我们的指针、[_Idx]即元素的个数(用在迭代器上就是位置或者说索引)。
回顾一下begin()的定义:

    _NODISCARD _CONSTEXPR17 iterator begin() noexcept {
        return iterator(_Elems, 0);
    }

[_Idx]初始化为0(毕竟begin()嘛),而[_Ptr]初始化为[_Elems],也就是我们常用的基本数组。
_Array_iterator<_Ty, _Size>:

    _NODISCARD _CONSTEXPR17 reference operator*() const noexcept {
        return const_cast<reference>(_Mybase::operator*());
    }

    _NODISCARD _CONSTEXPR17 pointer operator->() const noexcept {
        return const_cast<pointer>(_Mybase::operator->());
    }

而[->]操作符重载为[_Ptr + _Idx],[*]重载为[*operator->()].

至此,我们终于能访问array类里的一个元素了。

我们来总结一下:

我们研究了一下array类里begin()函数的内部实现:

  1. 用array对象的数组(首地址)与0(索引的第开始嘛)生成一个具有对应两个成员变量的类,即[_Array_const_iterator< _Ty,_Size>]类型的[_Mybase]。
  2. 把这个类作为[_Array_iterator]类型的类的成员,返回整个类。
  3. 返回值就是我们所用的迭代器,基于所定义的操作符重载,我们便能想使用指针一样使用迭代器了。

这其中蕴含着的泛型编程思想值得我们讨论一下。

STL组件之间的关系:待处理数据容纳在容器中,经由算法的处理,将结果输出到另一个容器里。(完成这种操作的正是迭代器)

看起来这句话好像是废话,但是似乎应隐隐约约能感受到这种设计的精妙:这正是符合现实生活的,更贴近面向对象编程的用意。

至于STL为什么设计成如上的样子,请您试想,应用模版,我们不拘于数据类型的限制,应用重载操作符,我们能得到统一的语句编写方式,应用如上的嵌套定义、调用呢?我们可以使代码的重用率提升,毕竟STL里的操作有部分是一样的,而且可以确保我们在不同的系统得到的结果相同。

总之,STL是C++数据结构与泛型编程中的重要知识,我们应该深入的研究它。
STL万岁(≧▽≦)/
觉得还行的看官点个攒再走吧…

你可能感兴趣的:(c++)