C++primer-learning-notes-chapter13

224 阅读7分钟

拷贝控制

拷贝赋值和销毁

拷贝构造函数

  • 一个构造函数第一个参数是自身类型的引用,且任何额外参数都有默认值
class Foo{
public:
    Foo();//默认
    Foo(const Foo&);//拷贝  几乎总是const
}
  • 合成的拷贝构造函数用来阻止我们拷贝该类类型的对象,会将其参数的成员逐个拷贝到正在创建的对象中。数组类型,合成构造函数会逐元素地拷贝一个数组类型的成员。
class Sales_data{
public:
    Sales_data(const Sales_data&);
private:
    std::string bookNo;
    int units_sold = 0;
    double revenue = 0.0;
};
Sales_data:Sales_data(const Sales_data& orig):bookNo(orig.bookNo),units_sold(orig.units_sold),revenue(orig.revenue){}
  • 拷贝初始化何时发生,以及拷贝初始化是依靠拷贝构造函数或移动构造函数来完成的。何时发生:=,实参传递给一个非引用类型的形参,返回类型为非引用的函数返回一个对象,用花括号列表初始化一个数组中的元素或一个聚合类中的成员。初始化标准库容器调用其insert和push成员,容器会对其元素进行拷贝初始化,而用emplace成员创建的元素都进行直接初始化。
  • 参数和返回值:拷贝构造函数被用来初始化非引用类类型参数,所以拷贝构造函数的参数必须是引用类型,否则就会无限循环调用。
  • 拷贝初始化的限制:使用的初始化要求通过一个explicit的构造函数来进行类型转换,那么使用拷贝初始化还是直接初始化就不是无关紧要的了
vector<int> v1(10);
vector<int> v2 = 10;//wrong
void f(vector<int>);
f(10);//wrong
f(vector<int>(10));
  • 编译器可以绕过拷贝构造函数
string null_book = "";//拷贝初始化
string null_book("");//略过了拷贝构造函数,但拷贝移动构造函数必须是存在的且可被访问的。

拷贝赋值运算符

Sales_data trans,accum;
trans = accum;//使用Sales_data的拷贝赋值运算符

//重载
class Foo{
public:
    Foo& operator=(const Foo&);
};//返回一个指向左侧运算对象的引用

合成拷贝赋值运算符

Sales_data& Sales_data::operator=(const Sales_data &rhs){
    bookNo = rhs.bookNo;
    units_sold = rhs.units_sold;
    revenue = rhs.revenue;
    return *this;
}

析构函数

class Foo{
public:
    ~Foo();
};

三/五法则

  • 需要析构函数的类也需要拷贝和赋值操作。
  • 需要拷贝操作的类也需要赋值操作,反之亦然。

使用=default

  • 可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本。
class Sales_data{
public:
    Sales_data() = default;
    Sales_data(const Sales_data&) = default;
    Sales_data& operator=(const Sales_data&);
    ~Sales_data() = default;
};
Sales_data& Sales_data::operator=(const Sales_data&) = default;
  • 类内使用default,合成的函数将隐式声明为内联的,只对类外定义使用=default,则不是内联的。

阻止拷贝

  • 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
  • 定义删除的函数阻止拷贝,赋值
  • 虽然声明了它们,但是不能以任何方式使用它们。
  • 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
  • 老版本使用private声明来阻止拷贝。
struct NoCopy{
    NoCopy() = default;
    NoCopy(const NoCopy&) = delete; //阻止拷贝
    NoCopy& operator=(const NoCopy&) = delete;//阻止赋值
    ~NoCopy() = default;
}
  • 析构函数不能是删除的成员
struct NoDtor{
    NoDtor() = default;
    ~NoDtor() = default;
};
NoDtor nd; //错误,析构函数是删除的
NoDtor *p = new NoDtor();//正确,但不能delete p
delete p;//

拷贝控制和资源管理

  • 类的行为可以像一个值,也可以像一个指针。行为像值:对象有自己的状态,副本和原对象是完全独立的。行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
  • 行为像值的类
class HasPtr{
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;}
private:
    string *ps;
    int i;
};

HasPtr& HasPtr::operator=(const HasPtr &rhs){
  auto newp = new string(*rhs.ps);
  delete ps;
  ps = newp;
  i = rhs.i;
  return *this;
}
  • 定义行为像指针的类
//定义一个使用引用计数的类
class HasPtr{
public:
    HasPtr(const string& s = string()):ps(new string(s)),i(0),use(new size_t(1)){}
    HasPtr(const HasPtr& p):ps(p.ps),i(p.i),use(p.use){++*use;}
    HasPtr& operator=(const HasPtr&);
    ~HasPtr();
}

HasPtr::~HasPtr(){
    if(--*use == 0){
        delete ps;
        delete use;
    }
}

HasPtr& HasPtr::operator=(const HasPtr& rhs){
    ++*rhs.use;
    if(--*use == 0){
        delete ps;
        delete use;
    }
    ps = rhs.ps;
    i = rhs.i;
    use = rhs.use;
    return *this;
}

交换操作

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
void swap(Foo &lhs,Foo &rhs){
    std::swap(lhs.h,rhs.h); //使用标准库的swap
}
void swap(Foo &lhs,Foo &rhs){
    using std::swap;
    swap(lhs.h,rhs.h);  //使用HasPtr版本的swap
}

//赋值运算符中使用swap
HasPtr& HasPtr::operator=(HasPtr rhs){
    swap(*this,rhs);
    return *this; //rhs被销毁,从而delete了rhs中的指针
}

拷贝控制示例

class Message{
    friend class Folder;
public:
    explicit Message(const string& str = ""):contents(str){}
    Message(const Message&);
    Message& operator=(const Message&);
    ~Message();
    
    //Folder集合中添加删除本Message
    void save(Folder&);
    void remove(Folder&);
private:
    string contents;
    set<Floder*> folders;
    void add_to_Folders(const Message&);
    void remove_from_Folders();
};

void Message::save(Folder& f){
    folders.insert(&f);
    f.addMsg(this);
}
void Message::remove(Folder& f){
    folders.erase(&f);
    f.remMsg(this);
}

void Message::add_to_Folders(const Message& m){
    for(auto f : m.folders)
        f->addMsg(this);
}

Message:Message(const Message& m):contents(m.contents),folders(m.folders){
    add_to_Folders(m);
}

void Message::remove_from_Folders(){
    for(auto f:folders)
        f->remMsg(this);
}

Message::~Message(){
    remove_from_Folders();
}

Message& Message::operator=(const Message& rhs){
    remove_from_Folders();
    contents = rhs.contents;
    folders = rhs.folders;
    add_to_Folders(rhs);
    return *this;
}

void swap(Message& lhs,Message& rhs){
    using std::swap;
    for(auto f:lhs.folders)
        f->remMsg(&lhs);
    for(auto f:rhs.folders)
        f->remMsg(&rhs);
    swap(lhs.folders,rhs.folders);  //swap(set&,set&)
    swap(lhs.contents,rhs.contents); //swap(string&,string&)
    for(auto f:lhs.folders)
        f->addMsg(&lhs);
    for(auto f:rhs.folders)
        f->addMsg(&rhs);
}

动态内存管理类

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 std::allocator<string> alloc;
    void chk_n_alloc(){if(size()==capacity()) reallocate();}
    std::pair<string*,string*> alloc_n_copy(const string*,const string*);
    void free();
    void reallocate();
    string *elements;
    string *first_free;
    string *cap;
};

void StrVec::push_back(const string& s){
    chk_n_alloc();
    alloc.construct(first_free++,s);
}

pair<string*,string*> StrVec::alloc_n_copy(const string *b,const string *e){
    auto data = alloc.allocate(e - b);
    return {data,uninitialized_copy(b,e,data)};
}

void StrVec::free(){
    if(elements){
        for(auto p = first_free;p!=elements;)
            alloc.destroy(--p);
        alloc.deallocate(elements,cap-elements);
    }
}

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;
}

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;
}

对象移动

  • 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
  • 在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
  • 标准库容器、string和shared_ptr类既可以支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。

右值引用

  • 新标准引入右值引用以支持移动操作。通过&&获得右值引用。
  • 只能绑定到一个将要销毁的对象,该对象没有其他用户。常规引用可以称之为左值引用。左值持久,右值短暂。
  • 右值要么是字面值常量,要么是在表达式求值过程中创建的临时对象。使用右值引用的代码可以自由地接管所引用的对象的资源
int i =42;
int &r = i;
int &&rr = i;//F
int &r2 = i * 42//F i*42是左值
const int &r3 = i*42;//T  const引用可以绑定到右值上
int &&rr2 = i*42;//T
  • 变量是左值,右值引用不能绑定到一个右值引用类型的变量上。变量是持久的,直至离开作用域才被销毁。
int &&rr1 = 42;
int &&rr2 = rr1;

标准库move函数

  • 可显式将一个左值转换为对应的右值引用类型,move告诉编译器,我们有一个左值,但我希望像右值一样处理它。
  • 调用move意味着:除了对rr1赋值或者销毁它外,我们将不再使用它
int &&rr3 = std::move(rr1);

移动构造函数和移动赋值函数

StrVec::Strvec(StrVec &&s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap){
	s.elements = s.first_free = s.cap = nullptr;
}
  • 不分配任何新内存,只是接管给定的内存。