自定义vector和Sring的结合使用
代码如下:
class CMyString
{
public:
CMyString(const char* str = nullptr)
{
cout << "CMyString(const char*)" << endl;
if (str != nullptr)
{
mptr = new char[strlen(str) + 1];
strcpy(mptr, str);
}
else
{
mptr = new char[1];
*mptr = '\0';
}
}
~CMyString()
{
cout << "~CMyString" << endl;
delete[]mptr;
mptr = nullptr;
}
// 带左值引用参数的拷贝构造
CMyString(const CMyString& str)
{
cout << "CMyString(const CMyString&)" << endl;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
}
// 带右值引用参数的拷贝构造
CMyString(CMyString&& str) // str引用的就是一个临时对象
{
cout << "CMyString(CMyString&&)" << endl;
mptr = str.mptr;
str.mptr = nullptr;
}
// 带左值引用参数的赋值重载函数
CMyString& operator=(const CMyString& str)
{
cout << "operator=(const CMyString&)" << endl;
if (this == &str)
return *this;
delete[]mptr;
mptr = new char[strlen(str.mptr) + 1];
strcpy(mptr, str.mptr);
return *this;
}
// 带右值引用参数的赋值重载函数
CMyString& operator=(CMyString&& str) // 临时对象
{
cout << "operator=(CMyString&&)" << endl;
if (this == &str)
return *this;
delete[]mptr;
mptr = str.mptr;
str.mptr = nullptr;
return *this;
}
const char* c_str()const { return mptr; }
private:
char* mptr;
friend CMyString operator+(const CMyString& lhs, const CMyString& rhs);
friend ostream& operator<<(ostream& out, const CMyString& str);
};
CMyString operator+(const CMyString& lhs, const CMyString& rhs)
{
CMyString tmpStr;
tmpStr.mptr = new char[strlen(lhs.mptr) + strlen(rhs.mptr) + 1];
strcpy(tmpStr.mptr, lhs.mptr);
strcat(tmpStr.mptr, rhs.mptr);
return tmpStr;
}
ostream& operator<<(ostream& out, const CMyString& str)
{
out << str.mptr;
return out;
}
template<typename T>
struct Allocator
{
T* allocate(size_t size) // 负责内存开辟
{
return (T*)malloc(sizeof(T) * size);
}
void deallocate(void* p) // 负责内存释放
{
free(p);
}
// 带有左值引用的对象构造
void construct(T *p, const T &val) // 负责对象构造
{
new (p) T(val); // 定位new
}
// 带有右值引用的对象构造
void construct(T *p, T &&val) // 负责对象构造
{
new (p) T(val); // 定位new
}
void destroy(T* p) // 负责对象析构
{
p->~T(); // ~T()代表了T类型的析构函数
}
};
template<typename T, typename Alloc = Allocator<T>>
class vector
{
public:
vector(int size = 10)
{
// 需要把内存开辟和对象构造分开处理
_first = _allocator.allocate(size);
_last = _first;
_end = _first + size;
}
~vector()
{
// 析构容器有效的元素,然后释放_first指针指向的堆内存
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p); // 把_first指针指向的数组的有效元素进行析构操作
}
_allocator.deallocate(_first); // 释放堆上的数组内存
_first = _last = _end = nullptr;
}
vector(const vector<T>& rhs)
{
int size = rhs._end - rhs._first;
_first = _allocator.allocate(size);
int len = rhs._last - rhs._first;
for (int i = 0; i < len; ++i)
{
_allocator.construct(_first + i, rhs._first[i]);
}
_last = _first + len;
_end = _first + size;
}
vector<T>& operator=(const vector<T>& rhs)
{
if (this == &rhs)
return *this;
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p); // 把_first指针指向的数组的有效元素进行析构操作
}
_allocator.deallocate(_first);
int size = rhs._end - rhs._first;
_first = _allocator.allocate(size);
int len = rhs._last - rhs._first;
for (int i = 0; i < len; ++i)
{
_allocator.construct(_first + i, rhs._first[i]);
}
_last = _first + len;
_end = _first + size;
return *this;
}
void pop_back() // 从容器末尾删除元素
{
if (empty())
return;
// 不仅要把_last指针--,还需要析构删除的元素
--_last;
_allocator.destroy(_last);
}
T back()const // 返回容器末尾的元素的值
{
return *(_last - 1);
}
bool full()const { return _last == _end; }
bool empty()const { return _first == _last; }
int size()const { return _last - _first; }
void push_back(const T &val) // 接收左值
{
if (full())
expand();
_allocator.construct(_last, val);
_last++;
}
void push_back(T &&val) // 接收右值
{
if (full())
expand();
_allocator.construct(_last, val);
_last++;
}
private:
T* _first; // 指向数组起始的位置
T* _last; // 指向数组中有效元素的后继位置
T* _end; // 指向数组空间的后继位置
Alloc _allocator; // 定义容器的空间配置器对象
void expand() // 容器的二倍扩容
{
int size = _end - _first;
T* ptmp = _allocator.allocate(2 * size);
for (int i = 0; i < size; ++i)
{
_allocator.construct(ptmp + i, _first[i]);
}
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p);
}
_allocator.deallocate(_first);
_first = ptmp;
_last = _first + size;
_end = _first + 2 * size;
}
};
如此一来,会有什么问题产生吗?
我们来简单测试一下:
int main()
{
CMyString str1 = "aaa";
vector<CMyString> vec;
cout << "-----------------------" << endl;
vec.push_back(str1);
vec.push_back(CMyString("bbb"));
cout << "-----------------------" << endl;
return 0;
}
结果如下:
因此:我们使用自定义的vector存储自定义的类型CMyString
,在向容器里面添加push_back()
的时候,一个添加了正常对象(str1
),一个添加了临时对象(CMyString("bbb")
)。
在之前的学习中,我们已经意识到临时对象对于代码效率的影响,所以在上面的融合版本中,对于CMyString的构造函数、allocator的construct方法和vector的push_back方法都提供了带右值引用参数的版本。
那么正常来说,上述代码的运行结果应该是:
两个----------
线范围内的打印,分别是左值引用的拷贝构造和右值引用的拷贝构造。
但是结果并不是像我们想象的那样。
所以问题到底出在了哪里??
为什么临时对象去匹配了左值引用呢??
还是那句话,右值引用变量本身就是一个左值
.
我们对程序进行一个单步调试,试着完成探究一下:
这里,在接受了右值之后,val自身变成了一个左值。
于是他就会去调用带有左值引用的空间配置器中的construct去进行对象的构造:
后面的一步,我们也可以想到,他就会调用左值引用的拷贝构造:
我们也就明白了;
所以最后打印的还是CMyString(const CMyString&)!
解决上述问题的关键在于:
如何避免将一个右值在被右值引用接收后,不被转化成为一个左值。
我们需要将其转化为一个右值
!!!
我们想起了之前这种写法:我们之前也见过这种写法:
使用move
来实现:
将那一行代码修改为:
_allocator.construct(_last, std::move(val));
于是我们发现它调用了我们所期望的构造函数:
但是还是一样的道理,他又将这个左值转化成了一个右值,再次修改construct
函数:
new (p) T(std::move(val));
完成正常调用;
终于,我们调用的是带右值引用参数的拷贝构造函数了。
总结
move(移动语义)其实就是将一个左值变为右值;
前提是你要知道那个地方用的是右值!
forward(类型完美转发)
那么,既然move
可以做到将左值变为右值,为什么还需要forward呢?
其实原因上面也说到了,用move的前提是:
你需要知道那个地方的参数是左值,但是你想要匹配右值,这样你可以使用move(移动语义)来实现。
简单说就是你需要知道参数的类型!!!
但是现实情况是,这样的实现较为复杂,你往往不知道那个地方的参数类型。
那么有没有办法让编译器自动识别参数是个左值还是右值呢?
这也就是完美转发存在的意义
我们在上述改动的地方做出如下修改:
将两种类型的构造函数写成模板
construct
:
template<typename Ty>
void construct(T* p, Ty&& val)
{
new (p) T(std::forward<Ty>(val));
}
push_back()
:
template<typename Ty>
void push_back(Ty&& val)
{
if (full())
expand();
_allocator.construct(_last, std::forward<Ty>(val));
_last++;
}
我们可以发现,一般使用forward的时候会搭配模板来使用,并且定义的参数是: Ty&& val
。
这个是什么意思呢?
其实这个是模板函数的类型推演+引用折叠
。
类型推演很好理解,根据实际参数的类型来推演出Ty的类型;
那引用折叠是什么?
其实这个就是我们可以偷懒的一种方式:
如果没有应用折叠的话,我们在写代码的时候,需要提供两个版本:带左值引用参数和带右值引用参数的版本。代码非常的冗余。
有了引用折叠之后,短短一句Ty&& val
。,就有如下强大的功能:
- 左值 + 右值 = 左值:
&
+&&
=&
; - 右值 + 右值 = 右值:
&&
+&&
=&&
也就是说,模板的推演会推演出不同类型(可能是&也可能是&&),但是,经过引用折叠之后,推演出的还是类型本身。
之后std::forward(Ty)
会根据Ty的类型返回左值或者右值,从而达到完美转发的效果。
小总结
move(左值)
:移动语义,得到右值类型forward
:类型完美转发,能够识别左值或者右值类型。