成员变量
data_ : 指向一个’\0’结尾的字符串
length_: 占用空间长度(包含’\0’)
成员函数
实现了:默认构造函数、拷贝构造函数、拷贝赋值操作符、析构函数、移动拷贝构造函数、移动赋值操作符。最后总结了copy and swap
技术。
对于拷贝赋值操作符注意:
0. 保证异常安全:new可能抛出异常时,保证原对象仍然有效
1. 处理自赋值:先new一个副本,再delete自己。能够正确处理自赋值,也能保证异常安全
2. 返回引用:遵循这一习惯,与标准库行为保持一致
#include <sys/types.h>
#include <string.h>
#include <iostream>
#include <vector>
#include <algorithm>
class MyString
{
friend std::ostream& operator<< (std::ostream& os, const MyString& ms);
public:
MyString() : length_(1), data_(new char[1])
{
std::cout << "ctor(default)" << std::endl;
data_[0] = '\0';
}
MyString(const char* str) : length_(strlen(str) + 1), data_(new char[length_])
{
std::cout << "ctor(const char*)" << std::endl;
memcpy(data_, str, length_);
}
~MyString()
{
std::cout << "dtor" << std::endl;
delete[] data_;
}
MyString(const MyString& rhs)
: length_(rhs.length_), data_(new char[length_])
{
std::cout << "copy ctor" << std::endl;
memcpy(data_, rhs.data_, length_) ;
}
MyString& operator= (const MyString& rhs)
{
std::cout << "copy assign" << std::endl;
size_t newlength = rhs.length_;
char* newdata = new char[newlength];
memcpy(newdata, rhs.data_, newlength);
delete[] data_;
data_ = newdata;
length_ = newlength;
return *this;
}
const char* c_str() const {
return const_cast<const char*>(data_);
}
size_t length() const {
return length_;
}
private:
size_t length_;
char* data_;
};
std::ostream& operator<< (std::ostream& os, const MyString& ms)
{
os << "data: \"" << ms.c_str() << "\" "
<< "length: " << ms.length() << std::endl;
return os;
}
int main()
{
MyString ms; // 默认构造函数
std::cout << ms ;
ms = "hello world"; // 先通过隐式类型转换生成一个临时的对象,然后进行copy assign,最后临时对象被析构
std::cout << ms ;
MyString ms2(ms); // copy ctor
std::cout << "--------------" << std::endl;
std::vector<MyString> vec; // 空的vector
vec.push_back(ms); // vector先分配空间,大小为1,然后通过copy ctor放入ms的副本
vec.push_back(ms2); // vector 发现空间不足,重新分配空间(扩大一倍,变为2),然后拷贝原始空间的对象到新空间,接着将原始空间对象析构,最后将ms2拷贝到vector中。
vec.push_back("network"); // 先是隐式类型转换构造一个临时变量,vector发现空间不足,进行空间扩容(变为4,伴随着旧空间对象的拷贝和释放),最后将临时对象拷贝到vector,然后临时对象析构。
std::cout << "--------------" << std::endl;
for_each(vec.begin(), vec.end(), [](const MyString &s) { std::cout << s; });
//std::vector<MyString>::const_iterator it;
//for(it = vec.begin(); it != vec.end(); it++) {
// std::cout << *it;
//}
return 0; // 最后 ms、ms2、vector中的对象被析构
}
输出:
ctor(default)
data: "" length: 1
ctor(const char*)
copy assign
dtor
data: "hello world" length: 12
copy ctor
--------------
copy ctor
copy ctor
copy ctor
dtor
ctor(const char*)
copy ctor
copy ctor
copy ctor
dtor
dtor
dtor
--------------
data: "hello world" length: 12
data: "hello world" length: 12
data: "network" length: 8
dtor
dtor
dtor
dtor
dtor
可能对于输出一眼看不出来,和注释相结合着看应该没什么问题。
为了避免临时对象的创建,可以对operator=进行重载:
MyString& operator= (const char* s)
{
std::cout << "copy assign(const char*)" << std::endl;
delete[] data_;
length_ = strlen(s) + 1;
data_ = new char[length_];
memcpy(data_, s, length_);
return *this;
}
定义了该函数后,对const char*的赋值将通过精确匹配到该函数,而不必经过临时对象的创建。
移动构造函数
在某些情况下,对象拷贝完后就立即被销毁了,那么移动代替拷贝将提升性能。c++11引入了右值引用的概念,即绑定到右值的引用。这里给出移动构造函数的实现,移动操作如果不抛出异常需要加上noexcept
:
/* move ctor */
MyString(MyString&& s) noexcept : length_(s.length_), data_(s.data_)
{
std::cout << "move ctor" << std::endl;
s.length_ = 0;
s.data_ = nullptr;
}
测试代码:
MyString ms("move me");
MyString ms2(ms); // copy ctor
MyString ms3(std::move(ms)); // move ctor
std::cout << ms;
std::cout << ms2;
std::cout << ms3;
这里使用了std::move获取了一个右值,因此会调用移动构造函数,直接“接管”资源。
有了移动构造函数之后,在将一个临时对象push到vector中的时候将调用移动构造函数,而不是拷贝构造函数,效率得到提升。
vec.push_back("network"); // 先创建一个临时对象,然后使用移动构造函数直接接管资源,省去了复制的过程。
移动拷贝赋值运算符
/* move copy assign */
MyString& operator= (MyString&& rhs) noexcept
{
std::cout << "move copy assign" << std::endl;
if(this != &rhs) { // 检测自赋值
delete[] data_; // 释放原来的资源
length_ = rhs.length_; // 接管
data_ = rhs.data_;
rhs.length_ = 0; // 将rhs置于可析构的状态
rhs.data_ = nullptr;
}
return *this; // 返回引用
}
copy and swap
在编写拷贝赋值运算符时需要考虑异常安全(new可能抛出异常bad_alloc,这时需要保证this的数据不被污染保持原样)、自赋值(如果先释放资源那么自赋值时会出现问题)等问题。
在为类编写移动拷贝构造函数时,可以使用copy and swap
技术,使用一个函数整合copy assign 和 move copy assign:
/* copy and swap */
MyString& operator= (MyString rhs)
{
swap(rhs);
return *this;
}
void swap(MyString& rhs)
{
std::swap(data_, rhs.data_);
}
MyString只重载了一个operator=,它的参数是pass-by-value
的。what?这么做不是会进行拷贝吗?
惊奇过后,你会发现,operator=的老版本中也进行了拷贝,既然拷贝是不可避免的,那么将参数设计成pass-by-value
也是没有问题的,反而简化了代码。
当参数是左值时,将使用拷贝构造函数创建rhs;如果是右值,则使用移动拷贝构造函数。然后使用swap交换数据即可。这么做是异常安全的,因为new操作是在参数列表中进行的,抛出异常时,原始数据不会被污染;而且这么做也能处理“自赋值”,因为先做了一份拷贝嘛。
此外,要注意这么做对于右值引用会多一对儿ctor和dtor,但是右值引用的移动构造函数做的是“接管”资源,开销不是很大。
关于std::move
关于“引用折叠”和std::move见《c++ primer 5th》16.2.5和16.2.6
Return by value 会不会有额外的copy等开销?
https://isocpp.org/wiki/faq/ctors#return-by-value-optimization
C++ FAQ 指出,经过编译器的优化,不会有额外的开销。