《c++ primer》 第13章 拷贝控制 学习笔记

第 13 章 拷贝控制


1.拷贝,赋值与销毁

对初学c++的程序员来说,必须定义对象拷贝,移动,赋值或销毁时做什么

<1.拷贝

<<1.

拷贝构造函数:如果一个构造函数的参数是本身的引用,且其他的参数都有默认值,此构造函数是拷贝构造函数。

合成拷贝构造函数:在我们没有为类定义时,编译器会帮我们定义一个。编译器从给定对象中一次将每个非static成员拷贝到正在创建的对象中

例子:

#include <iostream>
#include <string>

using namespace std;

class A
{
    friend std::ostream& print(std::ostream &os, A &a);
    public:
        A() = default;
        A(int a,string b):i(a),s(b) { c++; }
    private:
        int i;
        string s;
        static int c;                        //所有类的对象都能访问,只能在类外初始化
        //const static int c = 10;           //const int 可以在类内初始化
        //const static double c = 10.1;      //error:初始化只能在类外初始化, 在类外加上const double A::c = 10.1就对了
};
int A::c = 10;
std::ostream& print(std::ostream &os, A &a)
{
    os << a.i << " " << a.s << " " << a.c << endl;
}
int main()
{
    A a1(10,"123");
    A a2(a1);                                //没有定义拷贝构造函数,编译器自己合成了一个,叫合成拷贝构造函数。不拷贝static成员。
    print(std::cout, a2);
    A a3(10,"123");
    print(std::cout, a3);
}

拷贝构造函数:参数是自身类类型的常量引用,如果不是引用会无限递归


#include <iostream>
#include <string>

class A 
{
    public:
        A() = default;
        A(int a, std::string b):i(a), s(b) { }                 //直接初始化
        A(const A&a):i(a.i),s(a.s) { std::cout << "调用拷贝构造函数" << std::endl; }   //拷贝初始化

     private:
        int i;
        std::string s;
};

int main()
{
    A a1(1,"123");
    A a2(a1);        //拷贝初始化
    A a3 = a1;       //拷贝初始化
}

拷贝操作不仅会在=发生还会在

向函数传递一个非引用的形参时

从一个返回类型为非引用类型的函数返回一个对象。

用花括号列表初始化一个数组或聚合类。

课后题13.4

指出哪些地方调用了构造函数

Point global;
Point foo_bar(Point arg)         //调用1次
{
    Point local = arg, *heap = new Point(global);//调用两次
    *heap = local;               //不调用
    Point pa[4] = {local, *heap};//调用2次
    return *heap;                //调用1次
}

<2.拷贝赋值运算符

Sales_data s1,s2;

s1 = s2;

和拷贝构造函数一样,如果类未定义拷贝赋值运算符,编译器会默认合成一个。合成拷贝赋值运算符会将右侧的除了非static的成员全部拷贝过去

某些运算符必须定义成成员函数,包括赋值运算符,如果一个运算符是一个成员函数,其左侧运算对象就绑定到this指针上。

赋值运算符重载要返回类的引用是*this,因为赋值有可能是a= b = c;这种赋值,所以必须返回它自己的指针,那么也就是*this。所以必须定义成成员函数,和输出函数返回流的引用是一样的,可能os << a << b;


<3.析构函数

析构函数执行的于构造函数执行的相反,析构函数释放所使用的资源,并释放非static数据成员。

析构函数不接受参数,不能被重载,对于一个类只有一个析构函数。

析构函数销毁的顺序是构造函数构造顺寻的逆序。

在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何首位工作。

隐式销毁一个内置指针类型的成员不会delete它所指向的对象

什么时候会调用析构函数

变量在离开其作用域的时候

当一个对象被销毁时,其成员被销毁

容器被销毁时

当对指向它的指针应用delete运算符时被销毁

对于临时对象,当创建它的完整表达式结束时被销毁

当指向一个对象的引用或指针离开作用域时,析构函数不会执行。

合成析构函数

编译器会默认合成一个合成析构函数,析构函数是在合成析构函数的补充,比如堆上申请的对象,必须我们手动释放,就写在析构函数里面。

知道析构函数体自身并不直接销毁成员是非常重要的,成员是在析构函数体之后隐含的析构阶段被销毁的。


使用智能指针不能指向栈上的内存,栈上的内存本来就会自动清空,所以会出现error:free double.


#include <iostream>
#include <string>
#include <memory>

class foo
{
    public:
        foo():i(0),s(" "),p(new std::string) { }
        foo(int i1, std::string s1):
            i(i1), s(s1), p(new std::string())
            { std::cout << "调用构造函数" << std::endl; }
        foo(const foo &f)
        {
            i = f.i;
            s = f.s;
            p = new std::string(*f.p);
            std::cout << "调用拷贝构造函数" << std::endl;
        }
        //foo(const foo &f):
        //    i(f.i), s(f.s) { }
        foo& operator=(const foo&f)
        {
            i = f.i;
            s = f.s;
            p = new std::string(*f.p);
            std::cout << "调用赋值构造函数" << std::endl;
            return *this;
        }
        ~foo() 
        { 
            delete p;
            std::cout << "析构函数" << std::endl;
        }

    private:
        int i;
        std::string s;
        std::string *p;
};

foo fun1(foo f)
{
    foo f2(f);
    foo f3;
    f3 = f2;
    return f2;
}

int main()
{
    foo f1(1,"123");
    fun1(f1);

}




<4.三五法则。拷贝,赋值和销毁应该看作一个整体。

1.需要析构函数的类也需要拷贝和赋值操作

2.需要拷贝操作的类也需要赋值操作,反之亦然

很好理解,默认合成的构造函数是不会帮我们new一个对象或者是其他有关于分配内存的,指针它只会拷贝指针,
如果只定义了析构函数那么就会new了一次但是析构了多次。
如果一个类需要自定义析构函数,那么可以肯定它也需要定义拷贝赋值运算符和拷贝构造函数。和上面的例子一样

总之定义的时候三个同时定义好。

<在参数后面5.使用=default
我们可以通过=default让编译器来为我们生成默认版本

阻止拷贝:加上=delete

新标准下,我们可以通过将拷贝构造函数和赋值运算符定义为删除的函数来阻止拷贝和赋值。

=delete必须在函数第一次声明的时候,意味着定义必须写到类的外面。

在类内声明在类外定义的不是内联函数,必须显示指定

与=delete不同的是我们可以指定任何函数为=delete,(只能对编译器可以默认合成的使用=default)

虽然删除函数主要用途是禁止拷贝控制成员,但当我们希望引导函数匹配的时候,删除函数也是有用的。

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

删除一个类的析构函数或者类的某个成员的类型删除了析构函数,我们都不能定义该类的变量或临时对象

但可以动态分配这种类型,不能释放



<5.行为像值的类和行为像指针的类

行为像值的类:在类中包含需要动态分配的对象时,每次我们都new一个新值,那么在拷贝构造函数和赋值操作符也是要new一个新值。

对类资源的管理,每个类都有自己的一份

行为像指针的类:所有的类共享一份动态分配的对象,在c++11引入了智能指针shared_ptr , 所以我们可以很方便的来操作这些,但是如果我们想自己管理资源的话(不通过智能指针计数为0时释放资源)就要引入引用计数来辅助。

三个例子:

行为像值的类,行为像指针的类(智能指针指针),行为像指针的类(引用计数)

<<1.行为像值的类

#include <iostream>
#include <string>

class HasPtr
{
    friend std::ostream& print(std::ostream &os, const HasPtr &hp);
    public:
        //HasPtr():p(nullptr),i(0) { }//加上会有二义性ambiguous
        HasPtr(const std::string s = std::string()):
            p(new std::string(s)), i(0) { std::cout << "调用单参或无参构造函数" << std::endl; }
        HasPtr(const std::string s, int t):
            p(new std::string(s)), i(t) { std::cout << "调用两参构造函数" << std::endl; }
        HasPtr(const HasPtr&hp)
        {
            std::cout << "调用拷贝构造函数" << std::endl;
            p = new std::string(*hp.p);
            i = hp.i;
        }
        HasPtr& operator=(const HasPtr &hp)
        {
            std::cout << "调用赋值运算符" << std::endl;
            auto temp = new std::string(*hp.p);//考给临时变量,万一=左值是自己就释放了。
            if(p != nullptr)
                delete p;
            p = temp;
            i = hp.i;
            return *this;
        }
        ~HasPtr()
        {
            delete p;
        }

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

std::ostream& print(std::ostream &os, const HasPtr &hp)
{
    std::cout << "string:" << *hp.p << " int:" << hp.i << std::endl;
    return os;
}

int main()
{
    HasPtr p1;
    HasPtr p2("hehe");
    print(std::cout, p1);
    print(std::cout, p2);
    p1 = p2;
    print(std::cout, p1);
    HasPtr p4 = p2;
}


<<2.行为像指针的类(智能指针)

#include <iostream>
#include <memory>
#include <string>

class HasPtr
{
    friend std::ostream& print(std::ostream &os, const HasPtr &hp);
    public:
        HasPtr(const std::string &s = std::string()):
            sp(std::make_shared<std::string>(s)), i(0) { }
        HasPtr(const HasPtr& hp)
        {
            sp = hp.sp;
            i = hp.i;
        }
        HasPtr& operator=(const HasPtr& hp)
        {
            sp = hp.sp;
            i = hp.i;
            return *this;
        }

    private:
        std::shared_ptr<std::string>sp;
        int i;
};

std::ostream& print(std::ostream &os, const HasPtr&hp)
{
    os << "string:" << *hp.sp << " i:" << hp.i << "\n";
    os << "usecout:" << hp.sp.use_count();//hp.sp->use_count() error use_count()是智能指针的成员
}

int main()
{
    HasPtr p1;
    HasPtr p2("hehe");
    print(std::cout, p1) << std::endl;
    print(std::cout, p2) << std::endl;
    HasPtr p3 = p2;
    print(std::cout, p3) << std::endl;
    HasPtr p4;
    print(std::cout, p4) << std::endl;
    p4 = p2;
    print(std::cout, p4) << std::endl;
    
}



<<3.行为像指针的类(引用计数)

#include <iostream>
#include <string>

class HasPtr
{
    friend std::ostream& print(std::ostream &os, const HasPtr &ps);
    public:
        HasPtr(const std::string &s = std::string()):
            p(new std::string(s)), i(0), use(new std::size_t(1)) { }

        //HasPtr(const std::string &s = std::string(), std::size_t t):
        //error: 列表中带有默认初始值的要写在后面
        HasPtr(std::size_t t, const std::string &s = std::string()):
            p(new std::string(s)), i(t), use(new std::size_t(1)) { }
        HasPtr(const HasPtr& hp)
        {
            use = hp.use;
            p = hp.p;
            ++*use;
            i = hp.i;
        }
        HasPtr& operator=(HasPtr &ps)
        {
            //*ps.use++  error: ++和*优先级相同,从右向左进行,先加了地址在解引用。
            if(--*use == 0)
            {
                delete use;
                delete p;
            }
            p = ps.p;
            use = ps.use;
            i = ps.i;
            ++*ps.use;

            return *this;
        }
        ~HasPtr()
        {
            if(--*use == 0)
            {
                delete use;
                delete p;
            }
        }

    private:
        std::string *p;
        int i;
        std::size_t *use; //控制
};

//流要输出不能是const类型
std::ostream& print(std::ostream& os, const HasPtr &ps)
{
    os << "string:" << *ps.p << " int:" << ps.i << " use:" << *ps.use;
    return os;
}

int main()
{
    HasPtr p1;
    HasPtr p2(99, "hehe");
    print(std::cout, p1) << std::endl;
    print(std::cout, p2) << std::endl;
    HasPtr p3 = p2;
    print(std::cout, p3) << std::endl;
    HasPtr p4;
    print(std::cout, p4) << std::endl;
    p4 = p3;
    print(std::cout, p4) << std::endl;

}

根据我们的需要选取使用哪一种。


<6.swap交换操作

和拷贝控制成员不同,swap并不是必要的手段,但是对于分配了资源的类,定义swap可能是一种很重要的优化手段

因为我们对类使用系统的版本可能效率会非常的低,交换类对象时可能会是

void swap(A &a1, A &a2)
{
        A temp = a1;   //调用拷贝构造函数
        a1 = a2;       //调用赋值运算符
        a2 = temp;     //调用赋值运算符
}
在这段代码里使用了一次拷贝两次赋值,可见效率是比较低的,尤其对复杂的类

我们可以定义我们自己的swap,提升效率

且在赋值运算符中也可以使用swap来提升效率,

且使用拷贝和交换的赋值运算符自动就是异常安全的,能处理自动赋值

看HasPtr的改版,并使用vector排序

在这个例子中如果交换我们之用交换指针和字符串p,效率较高,但是如果我们用系统的版本就会一次拷贝两次赋值。

<span style="color:#000000;">#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

class HasPtr
{
    friend bool compare(HasPtr &hp1, HasPtr &hp2);      //compare二元谓词函数,在sort中使用。也可使使用lambda
    friend void swap(HasPtr &hp1, HasPtr &hp2);         //自定义版本的swap
    friend std::ostream& print(std::ostream &os, const HasPtr &hp);
    public:
        //HasPtr():p(nullptr),i(0) { }//加上会有二义性ambiguous
        HasPtr(const std::string s = std::string()):
            p(new std::string(s)), i(0) { std::cout << "调用单参或无参构造函数" << std::endl; }
        HasPtr(const std::string s, int t):
            p(new std::string(s)), i(t) { std::cout << "调用两参构造函数" << std::endl; }
        HasPtr(const HasPtr&hp)
        {
            std::cout << "调用拷贝构造函数" << std::endl;
            p = new std::string(*hp.p);
            i = hp.i;
        }
        //!!在赋值运算符中使用swap
        //!!拷贝并交技术
        HasPtr& operator=(HasPtr hp)
        {
            swap(*this, hp);//!!使用我们自定义的版本。  这里是个小技巧,传参时传的是一份拷贝,然后我们交换*this和拷贝,此时已经“赋值”完成,*this已经改变
            return *this;   //!!而且在作用域结束的时候,hp就会销毁,且hp的任务已经完成。
        }
        ~HasPtr()
        {
            delete p;
        }

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

bool compare(HasPtr &hp1, HasPtr &hp2)        //<来排序
{
    return hp1.i < hp2.i;
}

void swap(HasPtr &hp1, HasPtr &hp2)
{
    std::cout << "调用自定义版本swap" << std::endl;
    using std::swap;     //为了避免使用成我们自己的版本从而引起无限递归。
    swap(hp1.i, hp2.i);  //系统版本
    swap(hp1.p, hp2.p);  //系统版本
}

std::ostream& print(std::ostream &os, const HasPtr &hp)
{
    std::cout << "string:" << *hp.p << " int:" << hp.i << std::endl;
    return os;
}

int main()
{
    std::vector<HasPtr>vec_hp;
    HasPtr hp1("wang", 2);
    HasPtr hp2("wei", 3);
    HasPtr hp3("hao", 1);
    vec_hp.push_back(hp1);
    vec_hp.push_back(hp2);
    vec_hp.push_back(hp3);
    std::sort(vec_hp.begin(), vec_hp.end(), compare);
    for(const HasPtr&hp : vec_hp)
    {
        print(std::cout, hp);
    }

}</span>

运行结果可以看出使用了多次自定义swap,使用次数越多效率越高。

构造函数和拷贝构造调用比较多,例子中让保存到vector中且排序了。



C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。

在C++中,类的构造函数可以省略不写,这时C++会为它自动创建一个隐式默认构造函数;也可以由用户定义带参数的构造函数,构造函数也是一个成员函数,他可以被重载;当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。



<7.动态内存管理类

某些类需要自己进行内存分配,这些类要自己定义拷贝拷贝控制成员。

在某些操作中,动态内存管理类的性能会好的多,因为部分操作不会自己调用默认构造函数来初始化了,都是我们来直接拷贝赋值。



<8.对象移动

是c++11的新特性,也是非常重大的改变之一

很多情况下都会发生拷贝,而拷贝后就立即被销毁了,在这种情况下移动而非拷贝对象会大幅度提升效率。

IO类和unique_ptr 类可以移动但不能拷贝。


<<2.为了支持移动操作,新标准引入了右值引用&&

右值引用只能绑定在将要销毁的对象上

返回左值引用的函数,连同赋值,下标,解引用和前置递增递减运算符,都是返回左值的表达式,我们可以将这些值绑定到左值表达式上。

返回非引用类型的函数,连同算数,关系,位以及后置递增递减运算符(可以发现这些值都是临时的,即将销毁的),都生成右值。 我们不能将左值引用绑定到这类运算符上,但是可以将const的左值引用或者一个右值引用绑定到这类表达式上。

简单的说左值引用生存期长,右值引用生存期短

右值引用只能绑定到临时对象,该对象没有其他用户。使用右值引用的代码可以自由的接管所引用对象的资源。

不能将变量绑定到右值上,即使变量是右值引用类型。


标准库move函数

虽然不能将右值绑定到左值上面,但是可以显示的将左值转化为右值类型。

move告诉编译器:我们有一个左值但是我们希望像右值一样处理它,调用move后我们只能销毁对象或者赋值

使用move的代码应该使用std::move



<<3.移动构造函数和移动赋值运算符

要求:类支持拷贝和赋值

移动构造函数的第一个参数是该类类型的一个右值引用,和拷贝构造函数一样,其他任何参数都必须有默认的实参

移动构造函数确保移动后源对象处于这样一个状态,销毁它是无害的,一旦资源完成移动,源对象就必须不再指向

被移动的资源,这些资源的所有权已经归属新创建的对象

和构造函数不同,移动构造函数不创建任何新的资源。

可以这样理解,上面所说的算数,关系位和后置运算符都产生的是一个临时的对象,即将被销毁的,此时我们调用移动构造函数,将它的

所有权交给一个新创建的对象,也就是即将被销毁的不销毁了交给了别人保管。

移动操作不应该抛出异常

所以我们应该用noexcept来显示的指定

看StrVec的移动构造函数

StrVec::StrVec(StrVec &&s) noexcept                 //noexcept指名不抛出异常
:elements(s.elements), first_free(s.first_free),cap(s.cap)
{
            s.elements = s.first_free = s.cap = nullptr;  //销毁地动源后的对象
}
注意:

noexcept是C++11引入的,在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间

不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept

之所以不允许抛出异常是因为在移动过程中抛出异常会产生问题。移动源改变了,新创建的对象也改变了,源对象不能保证在失败的情况下自身不变的要求


移动赋值运算符

StrVec& StrVec::operator=(StrVec &&s) noexcept      //不抛出任何异常
{
        if(this != &s)                              //检测是否是自己给自己赋值,
        {
                free();                             //释放this本身资源
                elements = s.elements;
                first_free = s.first_free;
                cap = s.cap;
                s.elements = s.cap = s.first_free = nullptr;
        }
        return *this;
}
注意:

当我们编写一个移动赋值运算符时,必须保证移后源对象必须保持有效的,可析构的状态,但是我们不能对它的值进行任何假想。


对象移动这块内容比较多,也比较重要,《c++ primer》用了14页来讲述,而且std::move对编写程序效率影响很大

So,这里就简单叙述下,下面专门写一篇关于对象移动的博客。


这章我感觉比第四版的变化大了太多,新知识很多,而且不仅仅是语法,思想性的东西也很多。温故而知新

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏天的技术博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值