之前写的没保存,以后在写。
5.动态内存管理类
StrVec类定义
class StrVec
{
public:
StrVec():elements(nullptr),first_free(nullptr),cap(nullptr){}
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
~StrVec();
void push_back(const string&);
size_t size()const{return first_free - elements;}
size_t capacity()const{return cap - elements;}
string* begin()const{return elements;}
string* end()const{return first_free;}
private:
static allocator<string> alloc;
void chk_n_allc(){if(size() == capacity() reallocate();}
pair<string*,string*> alloc_n_copy(const string*,const string*);
void free();
void reallocate();
string *elements;
string *first_free;
string *cap;
};
使用construct:用allocator分配内存,内存是未构造的,要使用原始内存,必须调用construct构造一个对象
void StrVec::push_back(const string& s)
{
chk_n_alloc(); //确保有空间容纳新元素
alloc.construct(first_free++,s); //在first_free指向的元素中构造s的副本
}
construct的第一个参数是一个指针,指向调用allocate所分配的未构造的内存空间。剩下参数确定用哪个构造函数来构造对象。
alloc_n_copy成员
类似vector,StrVec类有类值的行为,当拷贝或赋值StrVec时,必须分配独立的内存,并从原StrVec对象拷贝元素到新对象。
pair<string*,string*> StrVec::alloc_n_copy(const string* b,const string* e)
{
auto data = alloc.allocate(e - b); //e-b获得要分配的数量,allocate返回分配内存的第一个地址指针
return {data,uninitialized_copy(b,e,data)}; //pair返回新空间开始位置,拷贝的尾后的位置
}
free成员
free成员有两个责任:首先destroy元素,然后释放StrVec自己分配的内存空间。
void StrVec::free()
{
if(elements) //先检查指针释放为空
{
for(auto p = first_free; p!=elements;) //逆序销毁旧元素
alloc.destroy(--p);
alloc.deallocate(elements,cap - elements); //不能传给deallocate空指针
}
}
deallocate的指针必须是之前某次allocate调用所返回的指针。
拷贝控制成员
拷贝构造函数
StrVec::StrVec(const StrVec& s)
{
auto newdata = alloc_n_copy(s.begin(),s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
析构函数
StrVec::~StrVec(){free();}
拷贝赋值运算符
StrVec& StrVec::operator=(const StrVec& rhs)
{
auto data = alloc_n_copy(rhs.begin(),rhs.end()); //先拷贝元素,可解决自赋值的问题
free(); //释放自身数据
elements = data.first; //赋值
first_free = cap = data.second;
return *this;
}
在重新分配内存的过程中移动而不是拷贝元素
在编写reallocate成员函数之前,先思考一下此函数应该做什么:
1.为了一个新的,更大的string数组分配内存。
2.在内存空间的前一部分构造对象,保存现有元素。
3.销毁原内存空间中的元素,并释放这块内存。
从上可以看出,为一个StrVec重新分配内存空间会引起从旧内存空间到新内存空间逐个拷贝string。
由于string的行为类似值,每个string对构成它的所有字符都会保存自己的一份副本。拷贝一个string必须为这些字符分配内存空间
而销毁一个string必须释放所占用的内存。
因此,在本例中,拷贝这些string中的数据是多余的,在重新分配内存空间时,如果能避免分配和释放string的额外开销,StrVec的
性能会好很多。
移动构造函数和std::move
通过使用新标准库引入的两种机制,我们就可以避免string的拷贝。
首先,有一些标准库类,包括string,都定义了所谓的“移动构造函数”。
移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象。(说白了就是改变指针的指针而已)
第二个机制是一个名为move的标准库函数,定义在utility头文件中。
关于move需要了解两个关键点:
首先,当reallocate在新内存中构造string时,它必须调用move来表示希望使用string的移动构造函数(原因后面解释)。
如果它漏掉了move调用,将会使用string的拷贝构造函数。
其次,通常不为move提供一个using声明,当使用move时,直接调用std::move而不是move。
reallocate成员
现在可以编写reallocate函数了。首先调用allocate分配新的内存空间。新空间容量加倍,如果StrVec为空,分配容纳一个元素的空间。
void StrVec::reallocate()
{
auto newcapacity = size() ? 2*size() : 1; //准备更大的空间
auto newdata = alloc.allocate(newcapacity); //分配新内存
//将数据从旧内存移动到新内存
auto dest = newdata; //指向新数组中下一个空隙位置
auto elem = elements; //指向就数组中下一个元素
for(size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++)); //让新内存的对象指针 指向 旧内存指针指向的对象
free(); //移动完,释放旧内存
//更新数据结构,执行新元素
elements = newdata; //新对象中第一个元素
first_free = dest; //新对象中第一个可以用位置
cap = elements + newcapacity; //新对象的容量
}
6.对象移动
我们的StrVec类是这种不必要的拷贝的一个很好的例子。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的
更好的方式是移动元素。
使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类都包含不能共享的资源(如指针或IO缓冲),
因此这些类型的对象补鞥呢拷贝但可以移动。
在旧C++标准中,没有直接的方法移动对象。因此,即使不必拷贝对象的情况下,也不得不拷贝。
类似的,在旧版本的标准库中,容器中所保存的类型必须是可拷贝的。
但在新标准中,可以用容器保存不可拷贝的类型,只要它们能被移动即可。
6.1.右值引用
为了支持移动操作,新标准引入了一种新的引用类型:右值引用(必须绑定到右值的引用),通过&&来获得右值引用。
右值引用有一个重要性质:只能绑定到一个将要销毁的对象。
一般而言:一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
类似任何引用,一个右值引用是某个对象的另一个名字而已。常规引用又称为左值引用。
int i = 1;
int& r = i;
int&& rr = i; //错误:不能将一个右值引用绑定到一个左值上
int& r2 = i*2; //错误:i*2是一个右值
const int& r3 = i*2; //正确:const引用可以绑定一个右值
int&& rr2 = i*2; //正确
返回左值引用的函数,连同赋值、下标、解引用和前置递增递减运算符,都是返回左值的表达式的例子。
可以将一个左值引用绑定到这类表达式的结果上
返回非引用类型的函数,连同算术、关系、位以及后置递增递减运算符,都生成右值。
不能将一个左值引用绑定到这类表达式上,但可以将一个const的左值引用或一个右值引用绑定到这类表达式上。
左值持久,右值短暂
左值有持久的状态,右值要么是字面量,要么是在表达式求值过程中创建的临时对象。
右值引用只能绑定到临时对象,得知:
1.所引用的对象将要被销毁
2.该对象没有其他用户
意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
变量是左值
变量可以看做只有一个运算对象而没有运算符的表达式,类似其他表达式,变量表达式也有左值/右值属性。
变量表达式都是左值。带来的结果是,不能将一个右值引用绑定到右值引用类型的变量上:
int&& r1 = 4;
int&& r2 = r1; //错误:r1是左值
标准库move函数
虽然不能将一个右值引用直接绑定到一个左值上,但可以显示地将一个左值转换为对应的右值引用类型。
通过调用move的新标准库函数来获得绑定到左值上的右值引用。
int&& r3 = std::move(r1);
std::move调用之后,除了对r1赋值或销毁它外,将不再使用它。
6.2.移动构造函数和移动赋值运算符
为StrVec类定义移动构造函数,实现从一个StrVec到另一个StrVec的元素移动而非拷贝:
StrVec::StrVec(StrVec&& s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;
}
与拷贝构造函数不同,移动构造函数不分配任何新内存:它接管给定的StrVec中的内存。
接管内存后,将给定对象中的指针都置为nullptr。(这也是为什么移动之后不要再使用原对象了)
这样就完成了从给定对象的移动操作,此对象将继续存在。最终移后源对象会被销毁,意味着将在其上运行析构函数。
StrVec的析构函数在first_free上调用deallocate。如果忘记了改变s.first_free,则销毁移后源对象就会释放掉
我们刚刚移动的内存。
移动操作、标准库容器和异常
由于移动操作“窃取”资源,通常不分配任何资源。因此,移动操作通常不会抛出任何异常。
当编写一个不抛出异常的移动操作时,应该将此事通知标准库。除非标准库知道我们的移动构造函数不会抛出异常,否则
它会认为移动给我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。
一种通知标准库的方法是在构造函数中指明noexcept。noexcept是新标准引入的。
noexcept出现在参数列表和初始化列表开始的冒号之间:
class StrVec
{
public:
StrVec(StrVec&&) noexcept;
};
StrVec::StrVec(StrVec&& s) noexcept :/*成员初始化器*/
{/*构造函数体*/}
头文件的声明和定义中(如果定义在类外)都指定noexcept
移动赋值运算符
移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,
我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:
StrVec& StrVec::operator=(StrVec&& rhs) noexcept
{
if(this != &rhs) //直接检查自赋值
{
free(); //释放已有元素
elements = rhs.elements; //从rhs接管资源
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr; //将rhs置于可析构的状态
}
return *this;
}
我们进行检查的原因是此右值可能是move调用的返回结果。与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源
之前就释放左侧运算对象的资源(可能是相同的资源)。
移动源对象必须可析构
合成的移动操作:文字太多,上图
6.3.右值引用和成员函数
除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。
这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式:
拷贝:一个版本接受一个指向const的左值引用
移动:第二个版本接受一个指向非const的右值引用
例如,定义了push_back的标准库容器提供两个版本:
一个版本有一个右值引用参数,另一个版本有一个const左值引用。假定X是元素类型
void push_back(const X&); //拷贝:绑定到任意类型的X
void push_back(X&&); //移动:只能绑定到类型X的可修改的右值
一般来说不需要为函数操作定义接受一个const X&&或是一个(普通的)X&参数的版本。
当我们希望从实参“窃取”数据时,通常传递一个右值引用。为了达到这个目的,实参不能是const的。
类似的,从一个对象进行拷贝的操作不应该改变该对象。因此不需要定义接受一个普通的X&参数的版本。
作为一个更具体的例子,将StrVec类定义另一个版本的push_back:
class StrVec
{
public:
void push_back(const string&); //拷贝元素
void push_back(string&&); //移动元素
//其他成员的定义,如前
};
void StrVec::push_back(const string& s)
{
chk_n_alloc();
alloc.construct(first_free++,s);
}
void StrVec::push_back(string&& s)
{
chk_n_alloc();
alloc.construct(first_free++,std::move(s)); //右值引用版本调用std::move
}
右值引用版本调用move来将其参数传递给construct。如前所述,construct函数使用其第二个和随后的实参的类型来确定使用
哪个构造函数。由于move返回一个右值引用,传递给construct的实参类型是string&&。因此会使用string的移动构造函数构造
新元素。当我们调用push_back时,实参类型决定了新元素时拷贝还是移动到容器中:
StrVec vec;
string s = "some string";
vec.push_back(s); //调用push_back(const string&)
vec.push_back("oh!my god!"); //调用push_back(string&&), 从"oh!my god!"创建的string是右值
注意:"oh!my god!"是字符串字面值,类型是const char*字符数组,不是string,会隐式转换为string。
右值和左值引用成员函数
通常,我们在一个对象上调用成员函数,而不管该对象时一个左值还是右值。例如:
string s1="a",s2="b";
auto n = (s1+s2).find('a'); //(s1+s2)是一个右值
s1+s2 = "hi"; //对右值赋值
在旧标准中,我们没有办法阻止这种使用方式。为了维持向后兼容性,新标准库仍然允许向右值赋值。但是我们可能希望在自己
的类中阻止这种用法。在此情况下,我们希望强制左侧运算对象(即,this指向的对象)是一个左值
指出this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符:
class Foo
{
public:
Foo& operator=(const Foo&) &; //只能向可修改的左值赋值
};
Foo& Foo::operator=(const Foo& rhs) &
{
/*do some work*/
return *this;
}
引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。类似const的限定符,引用限定符只能用于(非static)成员函数,
且必须同时出现在函数的声明和定义中。
对于&限定的函数,只能将它用于左值;对于&&限定的函数,只能用于右值。例如:
Foo& retFoo(); //返回一个引用,retFoo调用时一个左值
Foo retVal(); //返回一个值,retVal调用是一个右值(假定是右值)
Foo i,j;
i = j;
retFoo() = j;
retVal() = j; //错误:retVal()返回一个右值
i = retVal();
一个函数可以同时用const和引用限定:在此情况下,引用限定必须跟在const限定符之后
class Foo
{
public:
Foo someMem() const &;
};
重载和引用计数
引用限定符也可以区分重载版本。可以综合引用限定符和const来区分一个成员函数的重载版本。
class Foo
{
public:
Foo sorted() &&; //可用于可改变的右值
Foo sorted() const &; //可用于任何类型的Foo
//其他成员的定义
private:
vector<int> data;
};
Foo Foo::sorted() && //本对象为右值,可以原址排序
{
sort(data.begin(),data.end());
return *this;
}
Foo Foo::sorted()const& //本对象是const或是一个左值,哪种情况都不能对其进行原址排序
{
Foo ret(*this); //拷贝一个副本
sort(ret.data.begin(),ret.data.end)); //排序副本
return ret; //返回副本
}
如果定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加:
class Foo
{
public:
Foo sorted() &&;
Foo sorted() const; //错误:必须加上引用限定符,或者把上面的引用限定符去掉
};
总结:如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。