您的位置:首页 > 编程语言 > C语言/C++

C++ 学习笔记(13)拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数、右值引用、引用限定符

2018-02-10 00:23 956 查看

C++ 学习笔记(13)拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数、右值引用、引用限定符

参考书籍:《C++ Primer 5th》

API:

13.1 拷贝、赋值与销毁

13.1.1 拷贝构造函数

拷贝构造函数的第一个参数必须是引用类型,通常还是const的引用。因为对于函数参数传递时,非引用类型的参数需要进行拷贝初始化,而如果拷贝构造函数的参数是非引用类型,就会陷入死循环。

除了第一个参数,其他参数都要有默认实参。

如果没有定义拷贝构造函数,编译器会自动合成一个。

合成拷贝构造函数:从给定对象中一次将每个非static成员拷贝到当前对象。

拷贝初始化发生情况:

=
初始化对象时。

非引用参数传递。

非引用函数返回值。

列表初始化数组元素或者聚合类的成员。

struct A
{
int n, i;
A(int n = 1) : n(n) { }
A(const A& a, int i = 3) : n(a.n), i(i) { }         // 第一个参数必须为引用,其他参数要有默认实参(隐式转换时的作用)
};

void main()
{
A a(2);         // 直接初始化
A b(a);         // 拷贝初始化
//A b = a;      // 等价上面
cout << b.n << "  " << b.i << endl;     // 输出 2 3
}


13.1.2 拷贝赋值运算符

如果运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。

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

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

13.1.3 析构函数

用于释放对象使用的资源,销毁对象的非static数据成员。

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

智能指针成员在析构阶段被自动销毁。

调用析构函数时间(对象被销毁):

变量离开作用域。

对象被销毁,成员也跟着销毁。

容器被销毁,元素也被销毁。

动态分配的对象,当对指向它的指针应用delete。

临时对象在创建完整表达式结束时。

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

13.1.4 三/五法则

如果一个类需要析构函数,可以肯定也需要拷贝构造函数和拷贝赋值运算符(如:析构要销毁指针,说明每个对象指针都是属于自己的,那拷贝的时候就不能直接复制指针,要新的指针)。

如果一个类需要拷贝运算符,也肯定需要拷贝构造函数,反之亦然。但不一定需要析构函数(如:拷贝时需要标明每个对象不同序号,需要重新拷贝,但和析构无关)。

13.1.5 使用=default

类似构造函数,
=default
可以要求编译器生成合成版本的拷贝控制成员。

默认是隐式内联的。对成员的类外定义就不是内联的。

13.1.5 阻止拷贝

定义删除的函数来阻止拷贝。在函数参数列表后面加上
=delete


不同于
=default
=delete
必须出现在函数第一次声明的时候。

可以对任何函数指定
=delete


指定了删除的析构函数,就无法销毁此类型的对象,也不能释放其变量和动态分配的对象的指针。

当不可能拷贝、赋值、销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。

声明private的拷贝构造函数和拷贝赋值运算符也可以阻止拷贝,但不建议。

13.2 拷贝控制和资源管理

13.2.1 行为像值的类

对于指针成员,应该是创建新的指针,而不是直接拷贝,否则指向的值还是同一个。

如果一个对象赋予它自身,赋值运算必须正确。

大多数赋值运算组合了析构函数和拷贝构造函数的工作。

struct Value
{
int * p;
Value(int n = 0) :p(new int(n)) {}
Value(const Value &value) :p(new int(*value.p)) {}      // 值类型的拷贝构造函数:指针要重新建
Value& operator=(const Value &);
~Value() { delete p; }
};

Value& Value::operator=(const Value &rv)
{
// 先拷贝值,再清除原有值,避免处理的对象是本身。如果先清除后拷贝,对于处理本身对象,就会出错
auto newp = new int(*rv.p); // 先拷贝传入的指针的值,指针需要重新创建
delete p;                   // 再删除原有值。
p = newp;                   // 最后再赋值
return *this;
}

void main()
{
Value v1(3);    // 构造函数
Value v2(v1);   // 拷贝构造函数
Value v3;
v3 = v1;        // 拷贝赋值运算符

cout << *v1.p << "  " << *v2.p << "  " << *v3.p << endl;    // 输出 3 3 3
v1 = v1;        // v1 赋值给自身,正确
*v2.p = 123;    // 修改v2和v3的指针指向的值
*v3.p = 666;
cout << *v1.p << "  " << *v2.p << "  " << *v3.p << endl;    // 输出 3 123 666
}


13.2.2 定义行为像指针的类

模仿shared_ptr,设计自己的引用计数。计数器为指针,动态分配,以共享。

创建一个对象时(构造函数),计数器初始化为1。

拷贝构造函数,共享计数器,计数器增加。

析构函数,计数器减小,为0时释放对象。

拷贝赋值运算符,增加右侧对象的计数器(类似拷贝构造函数),减小左侧的计数器(类似析构函数)。

class Pointer
{
public:
int *p;
Pointer(int n = 0) :p(new int(n)), count(new std::size_t(1)) {} // 构造函数,初始化计数器为1
Pointer(const Pointer &ptr) :p(ptr.p), count(ptr.count) { ++*count; }   // 拷贝构造函数,指针直接复制,计数器共享且递增
Pointer& operator=(const Pointer&);
~Pointer();
private:
std::size_t *count;
};

Pointer::~Pointer()
{
if (--*count == 0)      // 当析构最后一个对象
{
delete p;           // 销毁共享数据
delete count;       // 销毁计数器
}
}

Pointer & Pointer::operator=(const Pointer & rp)
{
++*rp.count;                    // 先递增右值,再递减左值。避免赋值本身时出错。
if (--*count == 0)      // 递减同析构函数
{
delete p;
delete count;
}
p = rp.p;               // 最后再复制指针和计数器(都指向同一个值)
count = rp.count;
return *this;
}

void main()
{
Pointer p1(8);  // 构造函数
Pointer p2(p1); // 拷贝构造函数
Pointer p3;
p3 = p1;        // 拷贝赋值运算符

cout << *p1.p << "  " << *p2.p << "  " << *p3.p << endl;    // 输出 8 8 8
*p2.p = 2;  // 修改任意一个对象(p1 p2 p3),共享数据成员p
cout << *p1.p << "  " << *p2.p << "  " << *p3.p << endl;    // 输出 2 2 2
}


13.3 交换操作

通常一次交换需要一次拷贝和两次赋值。

struct Value
{
...
//Value& operator=(const Value &);
Value& operator=(Value);            // 拷贝并交换,另一种拷贝赋值方法

friend void swap(Value&, Value&);   // 交换元素,标记友元以访问私有成员
}

inline void swap(Value &lv, Value &rv)
{
swap(lv.p, rv.p);       // 直接交换指针即可
}

// 参数是按值传递,所以会调用拷贝构造函数创建rv
Value& Value::operator=(Value rv)
{
swap(*this, rv);    // 将左侧运算对象和rv交换
return *this;       // 在函数结束后rv被销毁(即原来的值)。
}


13.5 动态内存管理

在重新分配内存过程中,移动元素而不是拷贝元素

创建一个新的内存

先构造一部分对象,将原来的元素直接移动过来

销毁原来内存的元素,释放空间

13.6 移动对象

标准库容器、string 和 shared_ptr 既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。

13.6.1 右值引用

常规引用称之为左值引用。右值引用和左值引用能绑定的值互补。

只能绑定到一个将要销毁的对象。可以接管所引用的对象的资源,“窃取”对象的状态。

int i = 123;
int &r = i;             // 正确
int &&rr1 = 123;        // 正确
int &&rr2 = i;          // 错误,右值引用不能绑定左值
int &r2 = i * 2;        // 错误,左值引用不能绑定右值
const int &r3 = i * 2;  // 正确,常引用可以绑定右值
int &&rr3 = i * 2;      // 正确,绑定到乘法结果


13.6.2 移动构造函数移动赋值运算符

第一个参赛是右值引用,额外参数必须有默认实参。

资源完成移动后,源对象必须不再指向被移动的资源(指针设置nullptr),因为这些资源所有权已经给新对象了。

移动通常不分配资源,所以不会抛出异常。标记noexcept。

标准库容器能对异常发生时,其自身的行为提供保证。

标记了noexcepte就会使用移动构造函数,否则会用拷贝构造函数。

在重新分配内存发生异常时:

移动构造函数已经修改了旧容器,而新容器元素也不存在。

拷贝构造函数的旧容器没有改变,新分配的容器可以直接释放。

不同于拷贝操作,编译器不会为某些类合成移动操作。

如果定义了自己的拷贝构造函数、拷贝赋值或析构函数,就不会合成移动构造函数和移动赋值运算符。

只有类没有定义任何版本的拷贝控制成员,且每个非static数据成员都可以移动时,编译器才会合成移动构造函数和移动赋值运算符。

不同于拷贝操作,移动操作永远不会隐式定义为删除。

显式要求生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除。

如果类定义了一个移动构造函数或一个移动赋值运算符,合成拷贝构造函数和拷贝赋值会被定义为删除的。

用拷贝构造函数代替移动构造函数一般是安全的。

struct Value
{
Value(Value &&v) noexcept : p(v.p) { v.p = nullptr; }       // 移动构造函数,移动完后,要将源指向置空
Value& operator=(Value);
};

// 既是移动赋值运算符(参数为右值时),也是拷贝赋值运算符(参数为左值时)
Value& Value::operator=(Value rv)
{
swap(*this, rv);
return *this;
}

void main()
{
Value v1(111);
Value v2(std::move(v1));    // 移动构造函数
cout << *v2.p << endl;      // 在移动完后,v1的所有权移交给v2,如果调用v1会产生异常

Value v3;
v3 = v2;                    // v2作为左值,调用拷贝,v2还是存在有效的
cout << *v3.p << endl;
v3 = std::move(v2);         // 使用std::move绑定右值,调用移动操作,v2失效
cout << *v3.p << endl;
}


移动迭代器:解引用运算符生成一个右值引用。通过标准库的make_move_iterator函数将普通迭代器转换成移动迭代器。

// 将begin()到end()之间元素复制到:first作为开始的未初始化内存。
uninitialized_copy(begin(),end(),first);

// 使用移动,原来的迭代器不可以再使用了。
uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first);


13.6.3 右值引用和成员函数

可以为成员函数提供移动和拷贝版本。

// 标准容器中push_back提供两个版本:
void push_back(const T&);   // 拷贝:绑定任意类型的T
void push_back( T&&);       // 移动:只绑定可修改的右值

int i = 5;
vector<int> vi;
vi.push_back(i);    // 调用push_back(const int &)
vi.push_back(2);    // 调用push_back(int &&)


引用限定符(reference qualifier):

& :说明只可以操作左值。

&&:说明只可以操作右值。

// 旧标准中,如果有右值引用操作,会产生如下问题:
string s1 = "aaa", s2 = "bbb";
s1 + s2 = "as";     // 正确,但没有意义。

// 使用引用限定符可以解决问题:
class A
{
public:
A sorted() &&;      // 参数列表之后,添加&&,对象为右值,即可以原址直接修改内容。
A sorted() const &; // 在const之后添加&,对象为const或左值,都不能直接对原址操作。
}


重载时:如果定义两个或两个以上具有同名和同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者都不加。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐