引言
在学习Go语言的过程中,感觉Go中的切片Slice挺好用的,就看了下实现的原理并自己用C++实现了一个类似的.
本文分为三部分,第一部分介绍Go中Slice的使用及原理,第二部分是笔者如何使用C++实现类似的功能
Go Slice 的使用及原理
使用
Go Slice的使用与原理在文章: Go Slices: usage and internals 中有更详细的介绍, 在此仅作简单的说明, 想要了解更多的内容可以查看该文章.
Slice 与数组类似,都可以存放多个相同类型的元素,不同的地方在于数组的长度是固定的,而Slice的长度是可变的. 在Go中可以通过调用make来创建一个Slice:
// make函数的声明
func make([]T, len, cap) []T
// 创建一个长度和容量都为5的切片,
s1 := make([]byte, 5, 5)
s2 := make([]byte, 5)
s1
是一个长度和容量均为5的切片, s2
只指定了长度,没有指定容量,没有指定时使用长度作为其容量
要获取其长度和容量可以使用len
和cap
函数:
len(s1) == 5
cap(s1) == 5
原理
一个Slice是数组的一个片段, 其中包含了指定数组的指针,片段的长度和容量
如Slices := s1[2:4]
表示为:
要增加slice的容量必须创建一个新的,更大的slice,并将旧的内容从原slice拷贝到新slice中. Go 提供了append
函数来完成这件事
func append(s []T, x ...T) []T
append
函数将元素x添加到切片的末尾,当切片增长到超过其容量时会自动扩大其容量.
C++中Slice的实现
分析
要实现Slice的容量增加,最直观的方式就是使用C++标准库中的vector
来实现, 再增加一个表示该切片的起始下标,和一个表示数据长度的值就可以实现类似的功能.
由于Slice与容器类似,因此使用模板类实现.由于语法上的不同,部分相同的功能在C++中的调用方式与Go中不同.
/*********************************************************************
* slice 模仿Go中的切片实现的切片
* 创建: 可以创建空的切片,也可以使用makeSlice()函数创建切片并指定切片长度
* slice目标在于尽可能提供与Go中的slice相同的功能,由于C++本身语法的限制
* 部分功能如[:]无法直接通过重载[]实现,改为重载()实现,
* 但总体上应该能与Go中的slice表现出一致的行为
* ******************************************************************/
template <typename T>
class slice
{
private:
std::shared_ptr<std::vector<T>> m_data;
size_t m_beginIndex;
size_t m_len;
}
每个slice
包含一个指向存储实际数据的std::vector<T>
的共享指针std::shared_ptr
, 可能有多个slice
指向同一份数据,因此使用shared_ptr
来管理; 数据需要连续存放,且空间可增长,故使用std::vector
m_beginIndex
表示切片的数据在底层数据中的起始位置
m_len
表示切片中数据的长度
构造函数
最简单的构造函数就是创建一个空的slice:
explicit slice()
: m_data(nullptr), m_beginIndex(0), m_len(0)
{}
创建空slice时使用空指针初始化m_data
, 再有数据时在分配空间. m_beginIndex
和m_len
均初始化为0,
处理直接创建一个空的切片,Go提供了根据数组创建切片的功能, 并直接使用数组的作为底层数据.但是C++中, 若创建的数组被释放,使用了该数组的slice
将访问已释放了的数据. 为此, C++中根据数组创建slice
需要复制数组中的数据.
/****************************************************************
* 使用数组初始化切片, 必须指定起始位置及长度
* 调用者需要保证数据的可用
* **************************************************************/
explicit slice(T data[], size_t beginIndex, size_t len)
: m_beginIndex(0), m_len(len)
{
if (len == 0)
{
return;
}
m_data = std::make_shared<std::vector<T>>();
m_data->reserve(len);
m_data->insert(m_data->end(), std::begin(data), std::end(data));
}
参数与Go中有所差别,其表现行为也有所差别:
在Go中,若时修改了数组,则相关的切片的数据也会发生变化, 如:
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
primes[1] = 0
fmt.Println(s)
}
输出结果为:
[3 5 7]
[0 5 7]
但是此时C++的表现行为则不同:
int main(int, char**) {
int data[4] = {1, 2, 3, 4};
slice<int> s(data, 0, 4);
cout << s << endl;
data[0] = 0;
cout << s << endl;
return 0;
}
输出结果为:
[1, 2, 3, 4]
[1, 2, 3, 4]
为此,提供另一个构造函数:
slice(std::initializer_list<T> list)
: m_data(std::make_shared<std::vector<T>>(list)),
m_beginIndex(0), m_len(list.size())
{
}
使用:
int main(int, char**) {
slice<int> s{1, 2, 3, 4};
cout << s << endl;
return 0;
}
因此,在C++中,需要一开始就从切片开始. 如:
int main(int, char**) {
slice<string> s{"Hello", "World"};
cout << s << endl;
auto s2 = s;
cout << s2 << endl;
s[0] = "";
cout << s2 << endl;
return 0;
}
输出:
[Hello, World]
[Hello, World]
[, World]
拷贝构造函数
在上面中使用了拷贝构造函数:
slice(const slice<T>& otherslice)
: m_data(otherslice.m_data), m_beginIndex(otherslice.m_beginIndex),
m_len(otherslice.m_len)
{
}
重载<<运算符
template <typename T>
class slice{
std::string toString() const
{
if (isEmpty())
{
return "[]";
}
std::stringstream ss;
ss << "[";
for (size_t i = 0; i < m_len - 1; i++)
{
ss << (*m_data)[i + m_beginIndex] << ", ";
}
ss << (*m_data)[m_beginIndex + m_len - 1];
ss << "]";
return ss.str();
}
};
template<typename T>
std::ostream& operator<<(std::ostream& out, const slice<T>& slice)
{
out << slice.toString();
return out;
}
append
Go中的slice的增长通过append实现,C++中类似功能的实现为:
template<typename T>
class slice{
slice<T> append(std::initializer_list<T> list)
{
if(m_beginIndex == 0 && m_len == m_data->size())
{
// 位于vector末尾,不需要拷贝数据,直接再末尾添加数据
m_data->reserve(m_data->size() + list.size());
m_data->insert(m_data->end(), list.begin(), list.end());
m_len = m_data->size();
}
else
{
auto tempData = std::make_shared<std::vector<T>>();
tempData->reserve(m_data->size() + list.size());
tempData->insert(tempData->end(), m_data->begin(), m_data->end());
tempData->insert(tempData->end(), list.begin(), list.end());
}
return slice<T>(*this);
}
};
/*****************************************************************
* 全局的append函数,与Go的append功能一致
* 由于使用变长参数时,若不添加一个值表示有多少个值则无法判断参数到哪里
* 结束,故改用std::initializer_list<T>,在调用时需要使用{}将值列表
* 包裹起来。如append(s {3, 2, 1});
* ***************************************************************/
template<typename T>
slice<T> append(const slice<T>& srcSlice, std::initializer_list<T> list)
{
slice<T> resSlice(srcSlice);
resSlice.append(list);
return resSlice;
}
总结
由于语言本身的限制, 个人能力有限, 无法在C++中实现功能与Go中的slice完全一致的slice, 但是实现了一个总体功能类似的slice
具体实现还有额外的几个函数,并没有全部放上来.
完整的实现代码: https://github.com/muzhy/furry-toy/blob/main/dataStruct/slice.hpp
以及相关的测试用例: https://github.com/muzhy/furry-toy/blob/main/test/dataStruct/slice_test.cpp
实现并没有经过完整的测试, 欢迎大家指出不足