设为首页 加入收藏

TOP

C++ primer读书笔记第13章:拷贝控制(一)
2016-09-12 19:03:12 】 浏览:1187
Tags:primer 读书 笔记 拷贝 控制

  当定义一个类时,我们显式或者隐式地指定此类型的对象拷贝、移动、赋值和销毁时做什么,一个类通过定义五个特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。我们称这些操作为拷贝控制操作。通常实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作。

13.1拷贝、赋值与销毁

13.1.1拷贝构造函数

  如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此拷贝函数是拷贝构造函数。

    class Foo
    {
    public:
        Foo(const Foo&);            //拷贝构造函数
        Foo(const Foo&, int i = 0); //也是拷贝构造函数,但是要记住,自身类型的引用必须是构造函数的第一个参数,且其他参数要有默认值
    }

  拷贝构造函数的第一个参数必须是一个引用类型,而且这个参数几乎总是一个const引用。另外因为拷贝构造函数在很多情况下都会被隐式使用,因此,拷贝构造函数通常不应该是explicit的。如果我们将拷贝构造函数声明为explicit,那么我们必须显示调用此拷贝函数。

class Test
{
public:
    Test() = default;
    explicit Test(const Test&t){ _a = t._a; }
private:
    int _a = 10;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Test t1;
    Test t2 = t1;      //错误,拷贝构造函数为explicit,必须显示调用
    Test t3(t1);       //显示调用explicit拷贝构造函数,正确
}

合成拷贝构造函数

  如果我们没有为类定义一个拷贝构造函数,那么编译器会为我们自动合成一个。与合成默认构造函数不同的是,即使我们定义了其他构造函数,但是没有定义拷贝构造函数,那么编译器也会为我们合成一个默认拷贝构造函数。
合成的默认拷贝构造函数从给定对象中依次将没给非static成员拷贝到正在创建的对象中,其进行的只是简单的值拷贝。

拷贝初始化

  要注意区分直接初始化与拷贝初始化。直接初始化实际上是要求编译器使用普通的函数匹配来选择最匹配的构造函数创建一个对象,而拷贝初始化则是要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的化还要进行类型转换。所以拷贝初始化相较于直接初始化多了一步拷贝的工作,有时对于一个自定义类型,这将产生很大的额外开销。

string dots("zhang");       //直接初始化
string dots = "zhang"       //拷贝初始化

  拷贝初始化通常发生在一下情况:

将一个对象作为实参传递给一个非引用类型的形参时 从一个函数返回一个非引用类型的对象时

用花括号初始化一个数组中的元素或一个聚合类中的成员时

  现在我们可以解释为什么拷贝构造函数的参数必须是引用类型了,因为如果形参类型不是引用类型,那么调用将永远不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,而为了拷贝实参,我们又必须调用它的拷贝构造函数,如此无限循环。
  注意在标准库中,insert或者push成员时,容器会对成员进行拷贝初始化,而用emplace函数插入元素则会进行直接初始化,所以当元素类型是自定义类类型时,在效率上后者优于前者。
  在拷贝初始化过程中,编译器可以跳过拷贝构造函数,直接创建对象,这是编译器的一种优化手段,但是我们必须保证在此时拷贝构造函数是存在而且可访问的。

13.1.2 拷贝赋值运算符

  拷贝赋值运算符其实就是对=运算符的重载函数。如果一个类未定义自己的拷贝赋值运算符,那么编译器也会合成一个默认的拷贝赋值运算符,其作用与默认拷贝构造函数函数类似,都是依次拷贝右侧对象的每个非static成员给左侧对象的相应成员,不同的是它不是用在构造对象的时候。
赋值运算符通常返回一个指向左侧对象的引用,其形参通常是该类型的const引用。另外要注意的是,标准库通常要求其元素类型要具有一个拷贝赋值运算符。

class Test
{
public:
    Test() = default;        //默认构造函数
    Test(const Test&t)       //拷贝构造函数
    {
        _a = t._a; 
    }
    Test &operator=(const Test &t)       //拷贝赋值运算符,用*this返回引用,形参为const引用
    {
        _a = t._a;
        return *this;
    }

private:
    int _a = 10;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Test t1;                        
    Test t2 = t1;          //此处进行的是拷贝初始化,调用的是拷贝构造函数
    t1 = t2;               //此处进行的是赋值运算,调用的是拷贝赋值运算符,注意与上面的区别
}

要特别注意拷贝赋值运算符与拷贝构造函数的区别

13.1.3析构函数

  析构函数进行与构造函数相反的操作:构造函数初始化对象的非static数据成员,析构函数释放函数对象使用的资源,并且销毁对象的非static数据成员。
  析构函数是类的成员函数,其没有返回值,也不接受任何参数,所以其不可以被重载,对一个给定类,只会有一个唯一的析构函数。
  同样,当一个类未定义自己的析构函数时,编译器会为它合成一个默认析构函数,合成析构函数的函数体为空。
下列情况下,会自动调用类的析构函数:

变量在离开其作用域被销毁时 当一个对象被销毁时,其成员被销毁 容器被销毁时,其元素被销毁 对于一个动态分配的对象,当对指向它的指针使用delete运算时,对象会被销毁 对于临时对象,当创建它的完整表达式结束时被销毁

一个要十分注意的点是,析构函数并不直接销毁成员,在整个对象的销毁过程中,析构函数是作为销毁步骤之外的另一部分进行的。

13.1.4 三/五法则

  拷贝构造函数,拷贝赋值运算符、析构函数、(新标准中还有移动构造函数、移动赋值运算符)统称为一个类的拷贝控制函数。
  我们决定一个类是否需要定义自己版本的拷贝控制函数时,有一下两个基本原则:

如果一个类需要自定义析构函数,那么我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。

这其中其实主要是为了内存控制。

class Test
{
public:
    Test(){ _pa = new int(10);} //构造函数中申请了堆内存

    ~Test(){delete _pa;}  //必须自定义析构函数以释放动态分配的内存,否则会造成内存泄露

    //默认拷贝构造函数的行为
    //Test(const Test &t)
    //{
    //  _pa =t._pa;
    //}
    //这样会导致两个对象的指针成员指向同一块内存,然后在析构的时候会delete此指针两次,造成错误,所以必须自定义拷贝构造函数和拷贝赋值运算符

    Test(const Test &t)     //自定义拷贝构造函数
    {
        _pa = new int();
        *_pa = *(t._pa);
    }

    Test &operator= (const Test &t)   //自定义拷贝赋值运算符
    {
        *_pa = *(t._pa);
        return *
首页 上一页 1 2 3 4 5 下一页 尾页 1/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇c++转码基础(1):各种编码类型及un.. 下一篇Effective Modern C++ 条款32 对..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目