C++ Primer笔记14---chapter13 拷贝控制1

Posted Ston.V

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer笔记14---chapter13 拷贝控制1相关的知识,希望对你有一定的参考价值。

1.当定义一个类时,会通过五种特殊的成员函数来控制此类型的对象的拷贝、移动、赋值和销毁:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数

2.拷贝构造函数

2.1 拷贝构造函数的第一个参数必须是自身类型的引用,此参数几乎总是一个const引用,且任何额外参数都有默认值

与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数

class Sales_data
{
    public:
        Sales_data();
        virtual ~Sales_data();
        //与合成的拷贝构造函数等价的拷贝构造函数的声明
        Sales_data(const Sales_data&);

    protected:

    private:
        std::string bookNo;
        int units_sold=0;
        double revenue=0.0;
};


//与Sales_data的合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig):
    bookNo(orig.bookNo),
    units_sold(orig.units_sold),
    revenue(orig.revenue)
    {
    }

2.2 拷贝初始化

此时我们知道了拷贝初始化和赋值初始化的区别

//直接初始化,实际上是要求编译器使用函数匹配来选择对应的构造函数
string dots(10,'.');

//拷贝初始化,实际上是要求编译器将右侧对象拷贝到正在创建的对象中,如果需要还要进行类型转化
//拷贝初始化通常是由拷贝构造函数完成,但如果一个类有一个移动构造函数,则拷贝初始化时可能使用后者
string nines = string(100,'9');

拷贝初始化不仅在使用=定义变量时会发生,下列情况也会发生:将一个对象作为实参传递给一个非引用类型的形参、从一个返回类型为非引用类型的函数返回一个对象、从花括号列表初始化一个数组中的元素或一个聚合类的成员

2.3 返回值

拷贝构造函数用来初始化费引用类型参数,如果其参数不是引用类型,则调用永远不会成功:为了调用拷贝构造函数,我们必须拷贝他的实参,但为了拷贝实参又必须调用拷贝构造函数,如此会无限循环。

3. 拷贝赋值运算符

如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。

3.1 重载赋值运算符

重载运算符本质是函数,函数名由operator加上符号组成,因此赋值运算符就是名为operator=的函数

赋值运算符通常应该返回一个指向其左侧运算对象的引用

class Foo {
public:
    Foo& operator=(const Foo&);    //赋值运算符

};

类似拷贝构造函数,若自己未定义,编译器会定义拷贝赋值运算符;同样的,对于某些类,合成拷贝赋值运算符用来禁止该类型的对象赋值,如果拷贝赋值运算符并非此目的,则会将右侧对象的每个非static成员赋予给左侧对象的对应成员

//等价于合成赋值拷贝运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs){
    bookNo=rhs.bookNo;
    units_sold=rhs.units_sold;
    revenue=rhs.revenue;
    return *this;
}

4.析构函数

析构函数释放对象使用资源,并销毁对象的非static数据成员

它没有返回值,也不接受参数,函数名前有~;由于没有参数因此不能被重载,一个类只会有唯一的析构函数

//等价于合成析构函数
class Sales_data{
public:
    //成员会自动销毁,除此之外不需要做其他的事
    ~Sales_data() {}
};

析构函数自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。

5.三/五法则

有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数;在新标准下还可以定义一个移动构造函数和一个移动赋值运算符;并不要求定义所有,但是只需要其中一个操作,而不需要定义所有操作的情况很少见。

当我们决定一个类是否要定义他自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数

如果一个类需要自定义析构函数,几乎可以肯定他也需要自定义拷贝赋值运算和拷贝构造函数

class HasPtr{
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)),i(0){}
    //错误:HasPtr需要一个拷贝构造函数和一个拷贝赋值运算符
    ~HasPtr(){delete ps}

private:
    std::string *ps;
    int i;
};


//这个版本类使用合成构造函数和合成赋值运算符,这些函数简单拷贝指针成员,这意味着对个HasPtr对象可能指向相同的内存

//错误case:连续delete两次
//当f返回时,hp和ret都会被销毁,但是二者包含相同指针值,最终导致此指针被delete两次
HasPtr f(HasPtr hp){
    HasPtr ret=hp;
    return ret;
}

//另外
HasPtr p("hello");
f(p);    //当f结束,p.ps指向内存会被释放
HasPtr q(p);    //现在p和q都指向无效内存

如果一个类需要自定义拷贝赋值运算符,那么他一定需要拷贝构造函数,反之亦然;但是不意味着需要析构函数

6.使用default

可以将拷贝控制成员定义为=default来显示要求编译器生成合成的版本

class Sales_data{
    public:
    //拷贝控制成员使用default
    Sales_data()=default;
    Sales_data(const Sales_data &)=default;
    Sales_data& operator=(const Sales_data&);
    ~Sales_data()=default;

};

    Sales_data& operator=(const Sales_data&) = default;

如果我们不希望合成成员是内联函数,应该只对成员的类外定义使用=default

只能对具有合成版本的成员函数使用=default(即默认构造函数或拷贝控制成员)

 7.阻止拷贝

有时,定义类时必须采用某种机制组织拷贝或者赋值,例如iostream阻止了拷贝以避免多个对象写入或者读取相同的io缓冲。即使不定义拷贝控制成员,编译器也会生成合成的。

7.1 定义删除的函数

在新标准下,我们可以使用=delete来声明我们不希望以任何方式使用此函数,用法类似=default,但是与=default有两点不同:=delete必须出现在函数第一次声明的时候;我们可以对任何函数指定=delete

但是析构函数不能是删除的成员

7.2 private拷贝控制

在新标准前,类是通过将拷贝构造函数和拷贝赋值运算符生命为private来组织拷贝的。但是友元和成员函数仍然可以拷贝对象,为了阻止他们,我们讲这些拷贝控制成员声明为private的,但是不定义他。

8. 拷贝控制和资源管理

通常,管理类外资源的类必须定义拷贝控制成员,可以这种类可以看起来像值,也可以看起来像指针。

8.1 行为像值的类

定义行为像值的类需要拷贝其底层指针指向的对象

编写赋值运算符时,注意两点:如果将一个对象赋予了自身,赋值运算符必须正常工作;大多赋值运算符组合了析构函数和拷贝构造函数的工作。

注意如果使用赋值运算符赋予自身,不能先delete,这样的话会导致自身指针指向的对象被释放,从而无从拷贝,只剩一个空指针了。正常做法是先将右侧值拷贝到局部变量,然后delete自身指向的对象,最后将局部变量拷贝给本对象

class HasPtr {
public :
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0){}
    //对ps指向的string,每个HasPtr对象都有自己的拷贝
    HasPtr(const HasPtr &p):
        ps(new std::string(*p.ps)),i(p.i){}
    HasPtr& operator=(const HasPtr &);
    ~HasPtr() {delete ps;}
        
private:
    std::string *ps;
    int i;
};

//赋值运算符通常组合了析构函数和构造函数的操作
HasPtr& HasPtr::operator=(const HasPtr &rhs){
    auto newp=new string(*rhs.ps);  //拷贝底层string
    delete ps;      //释放旧内存
    ps = newp;      //从右侧运算对象拷贝数据到本对象
    i = rhs.i;
    return *this;
}

8.2 行为像指针的类

定义行为像指针多的类需要拷贝指针成员本身,此时析构函数不能单方面的释放关联的string,只有当最后一个指向string的HasPtr销毁时,他才可以释放string (类似于shared_ptr),有时我们希望直接管理资源(即不使用shared_ptr),就可以使用引用计数。

引用计数不能是HasPtr对象的成员,否则,p1先后拷贝给p2,p3,当p3的计数更新后,无法更新p2的计数。可以将计数器保存在动态内存中,当拷贝或者赋值对象时,我们拷贝指向计数器的指针,这样副本和原对象都会指向相同的计数器。

class HasPtr {
public :
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
    //拷贝构造函数拷贝所有三个数据成员,并递增计数器
    HasPtr(const HasPtr &p):
        ps(p.ps),i(p.i),use(p.use){ ++*use;}
    HasPtr& operator=(const HasPtr &);
    ~HasPtr() {delete ps;}
        
private:
    std::string *ps;
    int i;
    std::size_t *use;   //用来记录有多少个对象共享*ps的成员
};

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

9. 交换操作

除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。

如果一个类定义了自己的swap,那就会使用自定义版本,否则使用标准库版本,我们希望swap交换指针,而非需要开辟临时对象来转存(开辟临时对象转存如:HasPtr tmp=v1;v1=v2;v2=tmp)。(swap并不是必须,但是可以通过定义swap进行代码优化),如果需要优化,此时swap函数应该调用自定义的swap,而非标准库的std::swap.

class HasPtr {
public :
    //定义swap为friend,以便能访问到HasPtr的peivate数据成员
    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,这种称作是拷贝并交换的计数,将左侧运算对象与右侧对象的副本进行交换。注意,其中的参数并不是一个引用,这个技术有趣在于自动处理里了自赋值情况且天然就是异常安全的(在改变左侧运算对象之前,先拷贝右侧运算对象从而保证自赋值的安全,实际上和前面的方法是一致的)。

HasPtr& HasPtr::operator=(HasPtr rhs){
    swap(*this,rhs);    //rhs现在指向本对象曾经使用的内存
    return *this;       //rhs被销毁,从而delete了rhs中的指针
}

以上是关于C++ Primer笔记14---chapter13 拷贝控制1的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer笔记16---chapter13 代码实例

C++ Primer笔记15---chapter13 拷贝控制2

C++ Primer笔记15---chapter13 拷贝控制2

C++ C++ Primer 基础知识笔记

C++ Primer Plus读书笔记

C++ Primer学习笔记