群体的概念
- 群体是由多个数据元素组成的集合体
- 分为 线性群体 和 非线性群体 两大类,如数组与链表
- 线形群体中的元素按位置排列有序,可以区分为第一个元素,第二个元素等
- 非线性群体不用位置顺序来标识元素
直接访问的线性群体--数组类
- 静态数组是具有固定元素个数的群体,其中的元素可以通过下标直接访问
- 动态数组由一系列位置连续的,任意数量相同类型的元素组成
- vector 就是用类模板实现的动态数组
vector 容器
- 封装任意类型的动态数组,自动创建和删除
- 数组下标越界检查
- vector 对象的定义
vector <元素类型> 数组对象名(数组长度) vector<int> arr(5);
- vector 对象的使用
对数组元素的引用 vector 对象名 [下标表达式] 获取数组长度 size()
示例:
template <class T>
class Array {
private:
T* list; // 用于存放动态分配的数组内存首地址
int size;
public:
Array(int sz = 50); //构造函数
Array(const Array<T> &a); //复制构造函数,深层复制
~Array(); //析构函数
Array<T> & operator = (const Array<T> &rhs); // 重载 = ,如果是默认重载 =,则是浅层复制
T & operator [](int i); // 重载 []
cont T & operator [](int i) const; // 重载 [] 常函数,处理常对象(初始化后数组每个元素不能被修改)
operator T * (); // 重载到 T* 类型的转换
operator const T * () const; // const 版本
int getSize() const; // 取数组大小
void resize(int sz); // 修改数组大小
};
template <class T> Array<T>::Array(int sz) { // 构造函数
assert(sz >= 0); // sz 为数组大小(元素个数),应当非负,异常处理应用异常来抛出,后续章节讲异常
size = sz; // 将元素个数赋值给变量 size
list = new T[size]; // 动态分配 size 个 T 类型的元素空间
}
template <class T> Array<T>::~Array() { // 析构函数
delete [] list;
}
template <class T>
Array<T>::Array(const Array<T> &a) { // 复制构造函数,深层复制
size = a.size; // 从对象 x 取得数组大小,并赋值给当前对象的成员
list = new T[size]; // 动态分配 n 个 T 类型的元素空间
for(int i=0; i<size; i++) // 从对象 a 复制数组元素到本对象
list[i] = a.list[i];
}
// 重载 = 运算符,将对象 rhs 赋值给本对象,实现对象之间的整体赋值,同样是深层复制
template <class T>
Array<T> &Array<T>::operator = (const Array<T> &rhs) {
if(&rhs != this) {
// 如果本对象中数组大小与 rhs 不同,则删除数组原有内存,然后重新分配
if(size != rhs.size) {
delete [] list; // 删除数组原有内存
size = rhs.size; // 设置本对象的数组大小
list = new T[size]; // 重新分配 size 个元素的内存
}
// 从对象 X 复制数组元素到本对象
for(int i=0; i<size; i++)
list[i] = rhs.list[i];
}
return *this; // 返回当前对象的引用
}
复制构造函数与重载 = 运算符都是深层复制
区别在于:
复制构造函数对象生成之际就要分配同样大小的空间
重载 = 则要比较原有对象与新对象的空间大小先,还要避免自身复制
// 重载下标运算符,实现与普通数组一样通过下标访问元素,具有越界检查功能
template <class T>
T & Array<T>::operator[] (int n) {
assert(n >=0 && n <size); // 检查下标是否越界
return list[n]; // 返回下标为 n 的数组元素
}
template <class T>
const T & Array<T>::operator[] (int n) const{
assert(n >=0 && n <size); // 检查下标是否越界
return list[n]; // 返回下标为 n 的数组元素
}
// 重载指针转换运算符,将 Array 类的对象名转换为 T 类型的指针
template <class T>
Array<T>::operator T * () { // 参数与返回值都不能写
return list; // 返回当前对象中私有数组的首地址
}
// 取当前数组的大小
template <class T>
int Array<T>::getSize() const {
return size;
}
为什么需要重载指针转换运算符呢?
看一个示例:
void read(int *p, int n) {...}
int a[10];
read(a, 10); // ok, 数组名可以直接传给指针
Array<int> a(10); // 构造函数,希望也可以像数组一样用
read(a, 10); // 类型不一样的情况下,编译器尝试做隐含的类型转换,不能做 Array<int> 与 int * 的转换,所以需要自己要实现指针运算符转换,将 Array 类的对象名转换为 T 类型的指针
// 将数组大小修改为 sz
template <class T>
void Array<T>::resize(int sz) {
assert(sz >=0); // 检查 sz 是否非负
if(sz == size)
return;
T* newList = new T[sz]; // 申请新的数组内存
int n = (sz < size) ? sz : size; // 将 sz 与 size 中较小的一个赋值给 n
// 将原有数组中前 n 个元素复制到新数组中
for(int i=0; i<n; i++)
newList[I] = list[I];
delete [] list; // 删除原数组
list = newList; // 使 list 指向新数组
size = sz; // 更新 size
}
接着看看如何使用我们定义的 Array 类模板
Array<int> a(10);
...
后续直接把 a 当作数组来使用,譬如 : a[i]
STL 库里面的容器类型,实现的机理大致就是这样的,只是这里是比较简单的实现
通常情况下,我们都优先使用 STL 库里面的容器,除非不能满足需求才用自定义的容器