本小白最近在学习C++中STL的相关知识,特在此做个笔记
今天看到了array类的内部实现,特此来整理一下所学
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<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类型的指针。
在我们的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
那么我们就以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()函数的内部实现:
这其中蕴含着的泛型编程思想值得我们讨论一下。
STL组件之间的关系:待处理数据容纳在容器中,经由算法的处理,将结果输出到另一个容器里。(完成这种操作的正是迭代器)
看起来这句话好像是废话,但是似乎应隐隐约约能感受到这种设计的精妙:这正是符合现实生活的,更贴近面向对象编程的用意。
至于STL为什么设计成如上的样子,请您试想,应用模版,我们不拘于数据类型的限制,应用重载操作符,我们能得到统一的语句编写方式,应用如上的嵌套定义、调用呢?我们可以使代码的重用率提升,毕竟STL里的操作有部分是一样的,而且可以确保我们在不同的系统得到的结果相同。
总之,STL是C++数据结构与泛型编程中的重要知识,我们应该深入的研究它。
STL万岁(≧▽≦)/。
觉得还行的看官点个攒再走吧…