目录
定义一个类时,我们显式地或隐式地指定在此类型的对象的拷贝、移动、赋值和销毁时做什么,一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数。
13.1 拷贝,赋值和销毁
13.1.1 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
如果没有为类定义拷贝构造函数,则编译器会自动为我们定义,且会从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
class Sales_data
{
private:
string bookNo;
int units_sold = 0;
double revenue = .0;
public:
Sales_data(const Sales_data& orig) :bookNo(orig.bookNo), units_sold(orig.units_sold), revenue(orig.revenue) {}
};
拷贝初始化在下述情况下也会发生
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
13.1.2 拷贝赋值运算符
即重载=运算符,拷贝赋值运算符接受一个与其所在类相同类型的参数,赋值运算符通常返回一个指向其左侧运算符对象的引用。
Sales_data& Sales_data::opertor=(const Sales_data &rhs)
{
bookNo=rhs.bookNo;
units_sold=rhs.units_sold;
revenue=rhs.revenue;
return *this;
}
13.1.3 析构函数
与构造函数不同,构造函数初始化对象的非static数据成员,析构函数释放对象使用的资源,并销毁对象的非static资源,析构函数没有返回值,也不接受参数,因此他不能被重载,对于一个给定的类,只有唯一一个析构函数。
13.1.4 阻止拷贝
(1)定义的删除函数
某些类中并不需要进行拷贝构造函数和拷贝赋值运算符,可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝,删除的函数:虽然我们是声明了他们,但不能以任何方式使用他们。在函数的参数列表后面加上=delete来指出我们希望将他定义为删除的。
struct NoCopy
{
NoCopy()=default; //使用合成的默认构造函数
NoCopy(const NoCopy&)=delete; //阻止拷贝
NoCopy &operator=(const NoCopy&)=delete; //阻止赋值
}
(2)析构函数不能是删除的成员
删除函数不能被定义为删除的
13.2 拷贝控制和资源管理
13.2.1 行为像值的类
(1)类值版本的HasPtr
class HasPtr
{
private:
string* ps;
int i;
public:
HasPtr(const string& s = string()) :ps(new string(s)), i(0) {}
HasPtr(const HasPtr& p) :ps(new string(*p.ps)), i(p.i) {}
HasPtr& operator=(const HasPtr&);
~HasPtr() { delete ps; }
};
(2)类值拷贝赋值运算符
在本例中,通过先拷贝右侧运算对象,我们可以处理自赋值情况,并能保证在异常发生时代码也是安全的,在完成拷贝后,我们释放左侧运算对象的资源,并更新指针指向新分配的string
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
auto newp=new string(*rhs.ps); //拷贝底层的string
delete ps; //释放旧内存
ps=newp; //从右侧运算对象拷贝数据到本对象
i=rhs.i;
return *this; //返回本对象
}
下述编写则会发生错误
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
delet ps;
ps=new string(*(rhs.ps));
i=rhs.i;
return *this;
}
若rhs和本对象是一个对象,delete ps会释放掉*this和rhs指向的string,则后续拷贝则会发生错误。
13.2.2 定义行为像指针的类
13.3 交换操作
管理资源类嗨通常定义一个swap函数,如果一个类定义了自己的swap,那么算法将使用类自定义版本,否则函数将使用标准库定义的swap。
(1)编写自己的swap函数
class HasPtr
{
friend void swap(HasPtr &,HasPtr&);
};
inline void swap(HasPtr &lhs,HasPtr &rhs)
{
using std::swap;
swap(lhs.ps,rhs.ps)
swap(lhs.i,rhs.i)
}
上述例子中,数据成员是内置类型,而内置类型不存在特定版本的swap,所以在本例中,对swap的调用会调用标准库的std::swap。
但是,若一个类的成员有自己类型特定的swap,调用std::swap则就是错误的。
例:若Foo的类中存在一个类型为HasPtr的成员h,若没有定义Foo版本的swap,则就会调用标准库版本的swap,则不正确。
下述定义Foo版本的swap函数不正确。
void swap(Foo &lhs,Foo &rhs)
{
std::swap(lhs.h,rhs.h);
//错误,上述使用了标准库的swap,而非HasPtr版本的
}
正确的swap函数
void swap(Foo &lhs,Foo &rhs)
{
using std::swap;
swap(lhs.h,rhs.h); //使用HasPtr版本的swap
}
13.3 动态内存管理类
下面将实现一个vector的简化版本
头文件
#pragma once
#ifndef STRVEC_H
#define STRVEC_H
#include<string>
#include<memory>
#include<utility>
#include<initializer_list>
#include<algorithm>
using std::string;
using std::allocator;
using std::pair;
class StrVec {
public:
//默认构造函数
StrVec() :elements(nullptr), first_free(nullptr), cap(nullptr) {};
StrVec(const StrVec&);
//通过列表初始化容器来构造
StrVec(std::initializer_list<string>);
StrVec& operator=(const StrVec&);
~StrVec();
//重载中括号,一边快速索引vector内的元素
string& operator[](size_t)const;
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; }
void reseve(size_t);//用来按需扩充内存空间
void resize(size_t, const string& s);//调整元素个数,少则删,多则添,不能减少原本就存在的内存空间,只能增加或者不变
private:
//因为所有对象公用一个内存分配器,所以需要一个静态的内存分配器
static allocator<string> alloc;
//传入两根内存指针,返回新创建的两根内存指针(拷贝赋值操作符使用)
pair<string*, string*> alloc_n_copy(const string*, const string*);//分配指定内存后拷贝,目的在于拷贝(创建)出来一个新的内存空间副本,主要在拷贝构造函数/拷贝赋值操作符内使用
//传入需要更新的内存大小(resize,
void alloc_n_move(size_t);
void chk_n_alloc();//检查内存空间是否满足插入需求,如果不满足插入需求,那么我们需要调用reallocate()重新分配一个新的空间,并且将原来的元素移动到新的内存空间
//使用移动构造
void reallocate();//扩充内存并且移动原来内存空间的元素
void free();
//成员属性
string* elements;//第一个元素
string* first_free;//指向最后一个实际元素之后的位置
string* cap;//指向分配内存末尾之后的位置
};
//拷贝构造函数
StrVec::StrVec(const StrVec& v) {
auto new_data = alloc_n_copy(v.begin(), v.end());
elements = new_data.first;
first_free = new_data.second;
}
StrVec::StrVec(std::initializer_list<string> ls)
{
auto new_data = alloc_n_copy(ls.begin(), ls.end());
elements = new_data.first;
first_free = cap = new_data.second;
}
//拷贝赋值函数
//特点:组合了拷贝构造和析构函数的功能
StrVec& StrVec::operator=(const StrVec& v) {
auto new_data = alloc_n_copy(v.begin(), v.end());
free();
elements = new_data.first;
first_free = cap = new_data.second;
return *this;
}
void StrVec::free() {
//不能传递给deallocate一个空指针,因为如果elements=0,那么函数什么也不做
if (elements) {//如果这段内存是存在的
//使用for循环destroy
//for (auto i = first_free; i != elements;) {
// //因为first_free一开始指向最后一个元素的下一个位置,所以是前置递减运算符
// alloc.destroy(--i);
//}
//使用for_each
for_each(elements, first_free, [this](string& rhs) { alloc.destroy(&rhs); });//捕获this指针
alloc.deallocate(elements, cap - elements);
}
}
pair<string*, string*>
StrVec::alloc_n_copy(const string* b, const string* e) {
string* data = alloc.allocate(e - b);//创建出(e-b)那么大的内存空间
//uninitialized_copy与uninitialized_fill的区别?
return { data,uninitialized_copy(b,e,data) };
}
void StrVec::alloc_n_move(size_t new_cap)//分配并且移动
{
auto new_data = alloc.allocate(new_cap);
auto elem = elements;
auto dest = new_data;
//移动原本存在的元素
for (size_t i = 0; i != size(); i++) {
alloc.construct(dest++, std::move(*elem++));
}
elements = new_data;
first_free = dest;
cap = elements + new_cap;
}
StrVec::~StrVec()
{
free();
}
string& StrVec::operator[](size_t i) const
{
return *(elements + i);
}
void StrVec::push_back(const string& str) {
chk_n_alloc();//检查内存是否符合要求,不符合要求就重新分配(重新分配内存加移动元素)
alloc.construct(first_free++, str);//在内存空间上构造str这个对象
}
void StrVec::chk_n_alloc() {
if (size() == capacity()) { reallocate(); }
}
void StrVec::reallocate()
{
auto new_capacity = size() ? 2 * size() : 1;//计算扩充后的内存大小,有两种情况,一种是原来就没有分配内存空间,另一种是分配了内存空间但是不够用
alloc_n_move(new_capacity);
}
void StrVec::reseve(size_t new_cap)
{
if (new_cap < capacity())//若重新分配的容量比原来还小,那么不允许分配,因为reseve是不容许改变容器内元素的数量的
return;
alloc_n_move(new_cap);//重新分配空间并且move
}
//resize可以扩充原本容器的空间,但无法减少容器预留的空间,resize本质是改变元素的数量
void StrVec::resize(size_t new_size, const string& s = "")
{
if (new_size > size()) {
if (new_size > capacity())reseve(new_size * 2);
for (size_t i = size(); i < new_size; i++) {
alloc.construct(first_free++, s);
}
}
else if (new_size < size()) {
for (size_t i = 0; i < new_size; i++) {
alloc.destroy(--first_free);//这里注意是前置递减
}
}
}
allocator<string> StrVec::alloc;
#endif
测试代码
#include"StrVec.h"
#include"String.h"
#include<string>
#include<iostream>
using namespace std;
int main(int argc, char** argv) {
StrVec v1;
v1.push_back("Harris");
v1.push_back("Jack");
v1.push_back("rommi");
v1.push_back("Tim");
v1.push_back("Tom");
v1.push_back("mike");
for (int i = 0; i < v1.size(); i++) {
cout << v1[i] << endl;
}
StrVec v2;
v2=v1
for (int i = 0; i < v2.size(); i++) {
cout << v2[i] << endl;
}
return 1;
}
13.4 对象移动
13.4.1 右值引用
右值引用就是必须绑定到右值的引用,通过&&获得右值引用,且,右值引用只能绑定到一个将要销毁的对象上。右值引用也不过是某个对象的另一个名字
int i=42;
int &r=i; //正确,r引用i
int &&r=i; //错误,不能将一个右值引用绑定到一个左值上
int &r2=i*42; //错误,i*42是一个右值
const int &r3=i*42; //正确,可以将一个const引用绑定到一个右值上
int &&rr2=i*42; //正确,将rr2绑定到乘法结果上
右值要么是字面常量,要么是在表达式求值过程中创建的临时对象,所以,右值引用引用的对象将要被销毁,或该对象没有其他用户。使用右值引用的代码可以自由地接管所引用的对象的资源。
13.4.2 移动构造函数和移动赋值运算符
(1)移动构造函数
移动构造函数的第一个参数是该类类型的一个引用,不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。除了完成资源移动,移动构造函数还必须确保移后源对象的销毁是无害的。且移动构造函数不分配任何新内存,且移动之,源对象必须可析构
例:
StrVec::StrVec(StrVec &&s) noexcept //移动操作不应抛出任何异常
::elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements=s.first_free=s.cap=nullptr;
}
(2)移动赋值运算符
StrVec &StrVec::operator=(StrVec &ths) noexcept
{
if(this!=&rhs)
{
free();
elements=rhs.elements;
first_free=rhs.first_free;
cap=rhs.cap;
}
return *this;
}
(2)移动右值,拷贝左值
如果一个类既有移动构造函数,又有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,例如对于StrVec,拷贝构造函数接受一个const StrVec的引用,他可以用于任何可以转换为StrVec的类型。而移动构造函数接受一个StrVec&&,因此只能用于实参是(非static)右值的情形。
StrVec v1v2;
v1=v2; //v2是左值,使用拷贝赋值
StrVec getVec(istream &)
v2=getVec(cin); //getVec(cin)是一个右值,采用移动赋值
如果没有移动构造函数,右值也被拷贝
class Foo
{
public:
Foo()=default;
Foo(const Foo&)
};
Foo x;
Foo y(x); //拷贝构造函数,x是一个左值
Foo z(std::move(x)) //拷贝构造函数,因为未定义移动构造函数
对z的初始化中,我们调用了move(x),他返回一个绑定到x的Foo &&,此时会被转换成const Foo &