引入
C++
语言不支持栈Stack
(此处出现的堆栈为操作系统的堆栈) 上的可变长度数组:
1 | int fun1(unsigned n) |
还有一种办法则是在堆Heap
上构建:
1 | int fun2(unsigned n) |
但如果频繁调用fun2
则会出现由于堆分配带来的效率问题。
C++11
虽然引入了std::array
用作传统构建在栈上的C
数组的替代。但std::array
依旧是定长数组。
QVarLengthArray
试图解决这一C++
难题。他在栈上分配一定数量元素,当试图调整其内部元素数量为更大时,它会自动使用在堆上分配。
详解
简而言之,QVarLengthArray
是一个栈数组与堆数组的结合:
- 初始给定大小,创建栈数组;
- 当添加元素大于栈数组大小时,创建堆数组,并把栈数组内容复制到堆数组上;
- 加快创建速度,适用于高频率使用数组的代码中;
示例
1 | int myfunc(int n) |
在上面的例子中,QVarLengthArray
将在堆栈上预分配1024
个元素,并使用它们,除非n + 1
大于1024
。如果省略第二个模板参数,则使用QVarLengthArray的默认值256。
源码解析
QVarLengthArray
的接口更为底层,缺少一些QList
的功能,这里先看一下私有成员:
1 | class QVarLengthArray |
首先成员变量:
-
a
为容量; -
s
为数组大小; -
ptr
为创建在堆上的数组指针; - 联合体中
array
为创建在栈上的数组,另外两个则是为了内存对齐锁创建的对象;
这里着重看一下realloc
函数(此处为Qt5.7.1
源码),此函数为在栈及堆上可变长度数组的实现关键:
1 | template <class T, int Prealloc> |
首先,函数声明:
- 第一行:
template <class T, int Prealloc>
为模板参数,T
为数组元素类型,Prealloc
为初始分配在栈上的数组的大小。 - 第二行:
Q_OUTOFLINE_TEMPLATE
为针对编译器的内联优化建议,asize
为数组大小,aalloc
为容量。
其次,函数部分:
-
Q_ASSERT(aalloc >= asize)
判断参数是否传入出错; const int copySize = qMin(asize, osize)
获取需要复制的元素长度;
-Q_ASSUME(copySize >= 0)
为断言和建议编译器优化宏;if (aalloc != a)
则是判断是否与之前的容量相等,不相等则重新创建数组;进入if
作用域:-
if (aalloc > Prealloc)
判断新容量是否大于之前分配在栈上的容量,是则重新在堆上创建aalloc*sizeof(T)
大小的数组,否则直接将Prealloc
设置为aalloc
; - 之后设置数组大小为
0
,拷贝旧元素到新数组中,完成一个拷贝后将数组大小加一; -
if (QTypeInfo<T>::isStatic)
此处的QTypeInfo<T>::isStatic
是一个编译时常量,如果需要使用复制构造函数重新迁移T
类型的对象,则该常量的值为true
,如果可以改用memcpy
,则该值为false
。之后则是进行复制,如果需要使用构造函数则调用构造函数进行新元素的拷贝(新版Qt为移动)构造并调用旧元素的析构函数进行析构,当出现错误时清理所有依旧完成拷贝的旧元素和释放旧指针并继续向外层抛出异常;如果支持memcpy
则直接进行内存的拷贝。
-
-
s = copySize
设置数组大小为拷贝的元素数量; -
if (QTypeInfo<T>::isComplex)
此处的QTypeInfo<T>::isComplex
也是一个编译时常量,如果需要运行T
类型的构造函数和析构函数,它的计算结果为true
,如果不需要,则为false
。即此处if
语句作用域继续析构剩余的旧元素; -
if (oldPtr != reinterpret_cast<T *>(array) && oldPtr != ptr)
判断指针是否为同一个,不是则释放旧指针; - 最后一个
if (QTypeInfo<T>::isComplex)
则是与之前同理,如果需要运行构造函数则调用新元素的构造函数进行构造或直接设置数组大小为asize
。