设为首页 加入收藏

TOP

C++拷贝控制(一)
2017-06-22 10:23:10 】 浏览:391
Tags:拷贝 控制

C++拷贝控制

前言

c++作为高级语言,面向对象编程是其重要的语言特性。设计好的架构,其基础也是类的设计。我们之前已经将类本身的知识梳理了一遍。这一章着重介绍类控制,包括拷贝控制、重载、面向对象设计以及模板和泛型编程。这些非常非常重要,是实现工程必须要掌握的基础知识。要打起十分的精神来学习。

按照c++ primer的顺序,我们从拷贝控制说起。

这章看的时间有点久,有些东西很陌生。可能自己接触的实际工程太少,有些经验的东西还体会不到。不过也不影响学习。至少有了前车之鉴,以后走过的坑会少点。

c++是一门很细致的语言。对象的生命周期可能会经过初始化、拷贝、赋值、释放一系列的过程。c++统统可以对其进行控制。在类的基础知识那一章着重分析了类的初始化,也就是类的构造函数。这一章主要讨论的是如何控制对象拷贝、赋值、移动和销毁。

类通过特殊的成员函数来控制这些操作。

拷贝 —- 拷贝构造函数(初始化) 赋值 —- 拷贝赋值运算符 移动 —- 移动构造函数(初始化)、移动赋值运算符 销毁 —- 析构函数

注意:

拷贝构造函数和赋值是不一样的,前面提到过初始化和赋值的区别。拷贝构造函数是在用另一个对象创建本对象时调用,而赋值是在用右值替换左值过程调用。

这些统称为拷贝控制操作。需要指出的是,如果类没有定义这些操作,编译器会自动生成默认的操作。但是一些特殊的场景下,使用默认的操作会出现问题。因此,问题的关键在于认识在何时需要定义这些操作


拷贝、赋值、销毁

这三者操作是最基本的操作。移动操作是新的标准提供的性质,一会深入分析。

1.拷贝构造函数

拷贝构造函数使用场景有三个:

拷贝初始化 类类型按值传递 函数返回对象 使用花括号列表初始化一个数组的元素或聚合类成员

拷贝构造函数的第一个参数是自身类类型的引用,且任何其他参数都有默认值。

class Foo {
public: 
    Foo();
    Foo(const Foo &); // 拷贝构造函数
};

Foo a;
Foo B = a; // 拷贝初始化
Foo B(a); // 直接初始化

拷贝构造函数不应该是explicit,因为拷贝构造函数在几种情况下会被隐式的使用。

拷贝构造函数参数必须是引用,因为如果不是引用,按值传递就必须调用拷贝构造函数,如此无限循环。

2.拷贝赋值运算符

初始化和赋值是两个不同的操作,我们反复在强调这一点。这里是重载赋值运算符,以控制同类型之间的对象赋值。

场景

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

重载运算符本质上是函数。

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

Foo& Foo::operator= (const Foo &rhs) {
    data = rhs.data;
    return *this;
}

3.析构函数

析构函数是在对象销毁之前调用的函数。和构造函数执行相反的操作,这个相反在全方位。比如构造函数先执行初始化列表(按照数据成员定义顺序),然后执行函数体。析构过程先执行函数体,然后销毁数据成员(按照数据成员定义的逆序)。

析构函数不接受参数,不返回值。

class Foo {
public:
    ~Foo(); // 析构函数    
};

场景

变量离开作用域 对象被销毁时 容器(数组、vector等)被销毁时 delete显示销毁时 临时对象,创建的表达式结束时

注意:

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

4.三/五法则

何时定义拷贝控制操作,有几条原则可寻。

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

这是因为,需要析构函数,常常伴随动态内存管理。而使用编译器合成的操作,往往是浅拷贝。可能会析构多次,导致未定义的错误。因此,几乎肯定需要拷贝和赋值操作

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

5.使用合成版本

使用=default 显示地要求编译器生成合成的版本。

6.阻止拷贝

在某些场景下,需要禁止拷贝操作。比如iostream类,阻止拷贝,以避免多个对象读写同一个IO缓冲,导致数据不一致。

实现拷贝阻止有两种方式:

定义删除的函数(新标准)
struct NoCopy {
    NoCopy() = default;
    NoCopy(const NoCopy &) = delete; // 阻止拷贝
    NoCopy &operator=(const NoCopy &) = delete; // 阻止赋值
    ~NoCopy() = default;
};

注意:

析构函数不能是删除的成员,可以是删除,但是一旦定义为删除的,就不能定义这种类型的变量或成员,但是可以动态分配对象,却又无法释放对象。

private 拷贝控制

通过将拷贝构造函数和拷贝赋值函数声明成private 来阻止拷贝

class PrivateCopy {
    PrivateCopy(const PrivateCopy &); // class 默认为private ,阻止拷贝
    PrivateCopy &operator=(const PrivateCopy &); // 阻止赋值
public:
    PrivateCopy() = default;
    ~PrivateCopy() = default;
};

注意:

虽然声明成private,但是友元类和友元函数还是可以拷贝,因此,为了阻止友元类和友元函数拷贝,我们将这些拷贝成员声明为private,但是不定义它们。这样友元类和友元函数如果拷贝,会发生链接错误。


对象移动

拷贝的过程,是先分配新的空间,然后向新的空间里赋值。大部分情况下,对象拷贝之后会立即销毁。如果使用移动而非拷贝对象,性能将会大幅度的提升。新标准就提供了移动的新特性。

1.右值引用

为了支持移动操作,新标准引入了右值引用,非常triky。

我们之前提到的引用都是左值引用,即绑定到左值的引用。右值引用顾名思义,绑定到右值的引用。使用的方法是&&而非&。

右值引用一个非常重要的特性是—–只能绑定到一个将要销毁的对象(临时对象),因此,我们可以自由的将右值引用的资源”移动”到另一个对象中。好好体会一下。

int i = 42; int &r = i; // 左值引用 int &&rr = i; // 错误,不能将右值引用绑定到一个左值 int &r2 = i * 42; // 错误,不能将一个左值引用绑定到一个右值 const int &r3 = i * 42; // 正确,const引用绑定右值 int &&rr2 = i * 42; // 正确,右值引用绑定右值 

左值持久,右值短暂(字面值、临时对象)

但是我们说了,右值引用这个性质,可以让我们自由的接管所引用的对象的资源。

int &&rr1 = 42; // 正确,绑定字面值 int &&rr2 = rr1; // 错误,rr1表达式是一个左值

虽然我们不可以将一个右值直接绑定到左值上,但是我们可以显示的将一个左值转换为右值引用

#include 
     
       int &&rr3 = std
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇VS2010 c++编写的程序在别人的机.. 下一篇C中如何调用C++函数

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目