C++数组数组与对象生命周期(一)
今天学习类模板的时候,视频给了一个使用类模板实现数组的示例。原示例是直接存储对象的值,我希望实现一个类直接存储对象的引用,这样就能避免拷贝的开销。过程中踩了一些坑,这里记录发现问题到解决问题的过程。以下我们将内部使用指针存放数据的数组称作指针数组,而将内部直接拷贝对象存储数据的数组称作拷贝数组。
踩坑
#include <iostream>
using std::cout;
using std::endl;
template <typename T>
class TArray {
protected:
const int length;
T** element;
public:
explicit TArray(int length) : length(length), element(new T*[length]) {}
TArray(const TArray &array) : TArray(array.length) {
for(int i=0;i<length;i++) {
this -> element[i] = array.element[i];
}
}
TArray(T *element, int length) : TArray(length) {
for(int i=0;i<length;i++) {
this -> element[i] = &element[i];
}
}
~TArray() {
delete [] element;
}
int getLength() {
return this-> length;
}
T get(int index) {
return *element[index];
}
void set(int index, T element) {
this -> element[index] = &element;
}
};
int main() {
TArray<int> array(10);
for(int i=0;i<10;i++) {
array.set(i, i);
}
cout << "Element: ";
for(int i=0;i<10;i++) {
cout << array.get(i) << " ";
}
cout << endl;
cout << "Size: " << array.getLength() << endl;
}
输出:
Element: 11312508 11312508 11312508 11312508 11312508 11312508 11312508 11312508 11312508 11312508
Size: 10
竟然输出了一个随机值。
思考
回顾代码的过程中,发现这段代码存在问题:
void set(int index, T element) {
this -> element[index] = &element;
}
这里事实上是一个值传递,调用set
函数仍然要进行一次对象复制,于是做如下修改:
void set(int index, T & element) {
this -> element[index] = &element;
}
输出:
Element: 10 10 10 10 10 10 10 10 10 10
Size: 10
这是一个更离谱的输出。
在调试的过程中,我修改了main
函数:
int main() {
TArray<int> array(10);
for(int i=0;i<10;i++) {
array.set(i, i+1);
}
cout << "Element: ";
for(int i=0;i<10;i++) {
cout << array.get(i) << " ";
}
cout << endl;
cout << "Size: " << array.getLength() << endl;
}
发现无法通过编译,报错:
Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
这个问题是容易解决的,只需要修改set
函数:
void set(int index, const T & element) {
this -> element[index] = &element;
}
加上const
关键词,表明函数不会修改element
,出现新的报错:
Assigning to 'int ' from incompatible type 'const int '
添加const
约束,修改部分的代码如下:
protected:
const int length;
const T** element;
public:
TArray(int length) : length(length), element(new const T*[length]) {}
问题依旧。
解决
程序设计有一条重要的原则:谁创建,谁释放。在C++中,除了使用关键词new
创建的对象,对象在函数中创建的对象会被分配在栈区上,函数执行完毕后对象会被销毁。也就是说,在第一个for
语句块结束之后,变量i
的占用的内存被释放,为验证这一点,我们修改main()
函数:
int main() {
TArray<int> array(10);
cout << "Element: ";
for(int i=0;i<10;i++) {
array.set(i, i+1);
cout << array.get(i) << " ";
}
cout << endl;
cout << "Size: " << array.getLength() << endl;
}
输出:
Element: 1 2 3 4 5 6 7 8 9 10
Size: 10
结果符合预期,验证了我们的想法。
为了防止C++释放对象内存,我们可以用new
关键字构造一个对象并拷贝,修改set
方法如下:
void set(int index, const T & element) {
T* clone = new T(element);
this -> element[index] = clone;
}
这样就解决了问题。
反思
上面的解决方法并不是不完美的,因为我们使用指针数组初衷是希望避免拷贝对象的开销。如果通过这样的方式解决,其性能上并不会明显优于拷贝数组。用户如果希望保存指针可以显示地指定指针作为类模板的泛型参数,如TArray<int*>
。
但这种在内部使用指针存放对象的方法有它的优势,指针数组不必在创建数组的时候申请一块能完全容纳数组元素的内存空间,当实际放入数组的对象较少时,指针数组占用的内存小于拷贝数组占用的内存,尤其在对象较大,数组容量较大的情况。
必须指出的是,上述指针数组并不完善,首先它缺少了删除数组元素的方法,另外,它析构的方法还需要再斟酌,下一次作者会补充一个更加完善的版本。