拷贝控制是什么


C++ allows the programmer to define how objects are to be copied, moved, assigned and destroyed. Together these are known as copy control.

拷贝控制的基础

一、复制初始化

复制初始化 de 使用

Ref: C++的一大误区——深入解释直接初始化与复制初始化的区别

#include <iostream>
#include <cstring>
using namespace std; class ClassTest
{
public:
  ClassTest()
  {
    c[] = '\0';
    cout<<"ClassTest()"<<endl;
  }
  ClassTest& operator=(const ClassTest &ct)
  {
    strcpy(c, ct.c);
    cout<<"ClassTest& operator=(const ClassTest &ct)"<<endl;
    return *this;
  }
  ClassTest(const char *pc)
  {
    strcpy(c, pc);
    cout<<"ClassTest (const char *pc)"<<endl;
  }
  ClassTest(const ClassTest& ct)
  {
    strcpy(c, ct.c);
    cout<<"ClassTest(const ClassTest& ct)"<<endl;
  }
private:
  char c[];
};

1、ClassTest ct1("ab");
这条语句属于直接初始化,它不需要调用复制构造函数,直接调用构造函数ClassTest(const char *pc),所以当复制构造函数变为私有时,它还是能直接执行的。
 
2、ClassTest ct2 = "ab";
这条语句为复制初始化,它首先调用构造函数ClassTest(const char *pc)函数创建一个临时对象,然后调用复制构造函数,把这个临时对象作为参数,构造对象ct2;所以当复制构造函数变为私有时,该语句不能编译通过。
 
3、ClassTest ct3 = ct1;
这条语句为复制初始化,因为ct1本来已经存在,所以不需要调用相关的构造函数,而直接调用复制构造函数,把它的值复制给对象ct3;所以当复制构造函数变为私有时,该语句不能编译通过。
 
4、ClassTest ct4(ct1);
这条语句为复制初始化,因为ct1本来已经存在,直接调用复制构造函数,生成对象ct1的副本对象ct4。所以当复制构造函数变为私有时,该语句不能编译通过。
 
注:第4个对象ct4与第3个对象ct3的创建所调用的函数是一样的,但是本人却认为,调用复制函数的原因却有所不同。因为直接初始化是根据参数来调用构造函数的,如ClassTest ct4(ct1),它是根据括号中的参数(一个本类的对象),来直接确定为调用复制构造函数ClassTest(const ClassTest& ct),这跟函数重载时,会根据函数调用时的参数来调用相应的函数是一个道理;而对于ct3则不同,它的调用并不是像ct4时那样,根据参数来确定要调用复制构造函数的,它只是因为初始化必然要调用复制构造函数而已。它理应要创建一个临时对象,但只是这个对象却已经存在,所以就省去了这一步,然后直接调用复制构造函数,因为复制初始化必然要调用复制构造函数,所以ct3的创建仍是复制初始化。
 
5、ClassTest ct5 = ClassTest();
这条语句为复制初始化,首先调用默认构造函数产生一个临时对象,然后调用复制构造函数,把这个临时对象作为参数,构造对象ct5。所以当复制构造函数变为私有时,该语句不能编译通过。

编译器暗中优化

编译会帮你做很多你看不到,你也不知道的优化,

"你看到的结果,正是编译器做了优化后的代码的运行结果,并不是你的代码的真正运行结果。"

二、访问权限

复制构造函数是可以由编译默认合成的,而且是公有的(public),编译器就是根据这个特性来对代码进行优化的。

如果你自己定义这个复制构造函数,编译则不会自动生成,虽然编译不会自动生成,但是如果你自己定义的复制构造函数仍是公有的话,编译还是会为你做同样的优化。

当它是私有成员时,编译器就会有很不同的举动,因为你明确地告诉了编译器,你明确地拒绝了对象之间的复制操作,所以它也就不会帮你做之前所做的优化,你的代码的本来面目就出来了。

public 的 复制构造函数

 #include <iostream>
#include <algorithm>
#include <vector>
#include <string> using namespace std; class CExample
{
private:
int a; public:
CExample(int b) {
a=b;
printf("constructor is called\n");
} CExample(const CExample & c) {
a=c.a;
printf("copy constructor is called\n");
} ~CExample() {
cout<<"destructor is called\n";
} void Show()
{
cout<<a<<endl;
}
}; int main(void)
{
CExample A(); CExample B=A; CExample C=CExample(A); B.Show();
return ;
}

private 的 复制构造函数

编译器明确地拒绝了对象之间的复制操作。

首先,使用指定构造函数创建一个临时对象 (拷贝源)

然后,用复制构造函数将那个临时对象复制到正在创建的对象

所以,当复制构造函数被声明为私有时,所有的复制初始化都不能使用。

   class CExample
{
private:
int a; CExample(const CExample & c)
{
a=c.a;
printf("copy constructor is called\n");
} public:
CExample(int b)
{
a=b;
printf("constructor is called\n");
} ~CExample()
{
cout<<"destructor is called\n";
} void Show()
{
cout<<a<<endl;
}
}; int main(void)
{
CExample A(); >> CExample B=A; >> CExample C=CExample(A); B.Show();
return ;
}
 

三、 赋值与拷贝控制的区别

Copy Assignment Operator

以下是赋值,但不是初始化。初始化对应的是:copy contructor。

class A {
A& operator=(const A &a) { ... }
/* 类的内部变量一一对应复制 */
}

Copy constructor vs assignment operator

#include<iostream>
#include<stdio.h> using namespace std; class Test
{
public:
Test() {}
Test(const Test &t)
{
cout<<"Copy constructor called "<<endl;
} Test& operator = (const Test &t)
{
cout<<"Assignment operator called "<<endl;
return *this;
}
}; // Driver code
int main()
{
Test t1, t2;
t2 = t1; // assign (不是初始化的过程)
Test t3 = t1; // copy constructor (这是初始化的过程)
return ;
}

Output:

Assignment operator called
Copy constructor called

拷贝控制 and 资源管理

一、实例 Stack

头文件定义

其中的难点:

* move constructor, move assignment

* top函数为何有两个?

#ifndef UB_STACK_H
#define UB_STACK_H
#include <iostream> using namespace std; class UB_stack { public:
UB_stack();
UB_stack(const UB_stack &s);
UB_stack(UB_stack &&s);   // <-- move constructor
~UB_stack();   // destructor UB_stack& operator=(const UB_stack &s); // copy assignment
UB_stack& operator=(UB_stack &&s); // <-- move assignment void push(const int &item);
void pop();
int& top();
const int& top() const;
bool empty() const;
bool full() const; private:
class Node; // 节点定义
Node *head_; // 头指针
void reverse(Node *); friend void swap(UB_stack &s1, UB_stack &s2);
}; void swap(UB_stack &s1, UB_stack &s2);     // 在类外 依然要声明一次 #endif

类的实现

delete     ptr   代表用来释放内存,且只用来释放ptr指向的内存。
delete[] rg 用来释放rg指向的内存,!!还逐一调用数组中每个对象的destructor!!

对于像 int/char/long/int*/struct 等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的!但是如果是C++对象数组就不同了!

#include "UB_stack.h"

using namespace std;

class UB_stack::Node {
// allow UB_stack to access private data. 但看上去Node里面也没有private的内容呀!
friend class UB_stack; // methods and data in this class are private by default.
Node(int i, Node *n = nullptr) : item_{i}, next_{n} {}
~Node() {delete next_; } // destructor cleans up the memory int item_;
Node *next_;
};
//--------------------------------------------------------------------//
// constructor 但仍然应该是空
UB_stack::UB_stack() : head_{nullptr} {} // copy constructor
UB_stack::UB_stack(const UB_stack &s) : head_{nullptr} {
reverse(s.head_);
}

// move constructor
UB_stack::UB_stack(UB_stack &&s) : head_{s.head_} {
s.head_ = nullptr;
}
//--------------------------------------------------------------------// // destructor
UB_stack::~UB_stack() {
delete head_;
} //--------------------------------------------------------------------//
// return the top of the stack
int& UB_stack::top() {
return head_->item_;
} const int& UB_stack::top() const {
return head_->item_;
} bool UB_stack::empty() const {
return head_ == ;
} bool UB_stack::full() const {
return false;
} // method to work down a given stack
// and push items onto "this" stack correctly
void UB_stack::reverse(Node *h) {
if (h != nullptr) {
reverse(h->next_);
push(h->item_);
}
} // method to push an int onto the stack
void UB_stack::push(const int &item) {
head_ = new Node(item, head_);
}

// 支持了“等号”操作符
UB_stack& UB_stack::operator =(const UB_stack &s) {
// if not already the same stack.
if (this != &s) {
delete head_;
head_ = nullptr;
reverse(s.head_);
}
return *this;
} // Move Assignment.
UB_stack& UB_stack::operator =(UB_stack &&s) {
if (this != &s) {
delete head_;
head_ = s.head_;
s.head_ = nullptr;
}
return *this;
} // pop off the top of the stack.
void UB_stack::pop() {
Node *t = head_;
head_ = head_->next_;
t->next_ = nullptr;
delete t;
} void swap(UB_stack &s1, UB_stack &s2) {
// swap the pointers to the heads of the list only.
// much faster than swapping all the data.
swap(s1.head_, s2.head_);
}

二、内存释放

在main.cpp中,首先不使用指针去 new。

#include <iostream>
#include <vector>
#include "UB_stack.h" using namespace std; int main(void)
{
cout << "Hello world." << endl; UB_stack s;
s.push();
s.push();
s.push(); s.pop();
cout << s.top() << endl;
  // 虽然没有delete,但系统自动调用了~UB_stack()
return ;
}

改为 new后的指针方式,则调用 delete才会析构。

int main(void)
{
UB_stack *s = new UB_stack();
s->push();
s->push();
s->push();
s->pop();
cout << s->top() << endl;
delete s; return ;
}

三、拷贝构造、移动构造

深拷贝 & 浅拷贝

拷贝初始化、赋值过程的区别。

// 浅拷贝
UB_stack::UB_stack(const UB_stack &s) : head_{s.head_} { }
// 深拷贝
UB_stack::UB_stack(const UB_stack &s) : head_{nullptr} {
reverse(s.head_);
} UB_stack s2 {s1};

// 浅拷贝
UB_stack& UB_stack::operator=(const UB_stack &s) {
head_ = s.head_;
return *this;
}
// 深拷贝
UB_stack& UB_stack::operator=(const UB_stack &s) {
if (this != &s) {
delete head_;
head_ = nullptr;
reverse(s.head_);
}
return *this;
} s2 = s1

移动构造

Move Semantics.

可见,偷走了临时变量的内存空间,据为己用。节省了开辟空间的时间。

/* 思路:
* 新类截取旧类的指针head_
* 然后,迫使旧类放弃head_
*/ // move constructor.
UB_stack::UB_stack(UB_stack &&s) : head_{s.head_} {
s.head_ = nullptr;
} // move assignment.
UB_stack& UB_stack::operator =(UB_stack &&s) {
if (this != &s) {
delete head_;
head_ = s.head_;
s.head_ = nullptr;
}
return *this;
}

std:move()

一、基本原理

noexcept

void except_func() noexcept; 表示不抛出异常。

void except_func() noexcept (常量表达式);表达式为true不会抛出异常;flase抛出异常。

”右值“ 特性

Ref: https://www.zhihu.com/question/22111546/answer/30801982

右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。
以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。
对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。

二、移动语义的好处

没有移动语义

以表达式的值(例为函数调用)初始化对象或者给对象赋值是这样的: (重点:红色字体)

vector<string> str_split(const string& s);

vector<string> v = str_split("1,2,3");   // 返回的vector用以拷贝构造对象v。为v申请堆内存,复制数据,然后析构临时对象(释放堆内存)。
vector<string> v2;
v2 = str_split("1,2,3");           // 返回的vector被复制给对象v(拷贝赋值操作符)。需要先清理v2中原有数据,将临时对象中的数据复制给v2,然后析构临时对象。

注:v的拷贝构造调用有可能被优化掉,尽管如此在语义上仍然是有一次拷贝操作。

支持移动语义

同样的代码,在支持移动语义的世界里就变得更美好了,可以接收右值表达式。

vector<string> str_split(const string& s);

vector<string> v = str_split("1,2,3");   // 返回的vector用以移动构造对象v。v直接取走临时对象的堆上内存,无需新申请。之后临时对象成为空壳,不再拥有任何资源,析构时也无需释放堆内存。
vector<string> v2;
v2 = str_split("1,2,3");           // 返回的vector被移动给对象v(移动赋值操作符)。先释放v2原有数据,然后直接从返回值中取走数据,然后返回值被析构。

注:v的移动构造调用有可能被优化掉,尽管如此在语义上仍然是有一次移动操作。

不用多说也知道上面的形式是多么常用和自然。而且这里完全没有任何对右值引用的显式使用,性能提升却默默的实现了。

三、触发"移动构造"

Ref: c++ 之 std::move 原理实现与用法总结

std::move函数可以以非常简单的方式将左值引用转换为右值引用。Goto: (左值 右值 引用 左值引用) 概念

(1) C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建,本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。

(2) std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能。

(3) 对指针类型的标准库对象并不需要这么做。

按值传入参数

(1) 初始化

这里的序列容器,默认采用的是拷贝构建。这里通过move变为移动构造。

 #include <iostream>
#include <vector>
#include <list>
#include <chrono> using namespace std; int main()
{
std::vector<int> vecInt = {,,};   // 因为这里是左值
std::vector<int> vecCopy = std::move(vecInt);  // 所以这里本来也会是”左值“的角色,也就无法触发"move construct",要触发,就通过std::move后,左变右
vecCopy.at() = ; for (auto x: vecInt)
{
cout << x << endl;
} return ;
}

(2) 插入

void push_back( const T& value );  // (1) 左值调用,版本一
void push_back( T&& value );     // (2) 右值调用,版本二

如果你要往容器内放入超大对象,那么版本二自然是最佳选择。

vector<vector<string>> vv;

vector<string> v = {"", ""};
v.push_back("");   // 临时构造的string类型右值被移动进容器v
vv.push_back(std::move(v)); // 显式将v移动进vv, v中便没有了内容,move给了vv.

这里的参数类型通过 move,由 "左值调用" --> "右值调用",这叫做:Argument-Dependent Lookup (ADL) 参数依赖查找

它的规则就是当编译器对无限定域的函数调用进行名字查找时,除了当前名字空间域以外,也会把函数参数类型所处的名字空间 加入查找的范围。

(3) 构造实现

这里的自定义类,通过参数的”左右“类型,从而调用不同的构造函数。

class People {
public: People(string name) // 按值传入字符串,可接收左值、右值。接收左值时为复制,接收右值时为移动
: name_(std::move(name)) // 显式移动构造,将传入的字符串移入成员变量
{
}
string name_;
}; People a("Alice");   // 移动构造 name,这是个”右值“。 string bn = "Bob";
People b(bn);      // 拷贝构造 name,这是个”左值“

按值返回结果

vector<string> str_split(const string& s) {
vector<string> v;
// ...
return v; // v是左值,但优先移动,不支持移动时仍可复制。
}

四、类中实现

  • 容器的浅拷贝构造

使用:UB_stack s2 = std::move(s1);

UB_stack::UB_stack(UB_stack &&s) noexcept : head_{std::move(s.head_)} {

}

  • 容器的真搬运构造

使用:UB_stack s2 = std::move(s1);

UB_stack::UB_stack(UB_stack &&s) : head_{std::move(s.head_)} {
s.head = nullptr;
}

  • 容器的浅拷贝赋值

s2 = std::move(s1);

UB_stack& UB_stack::operator=(UB_stack &&s) noexcept {
head_ = std::move(s.head_);
return *this;
}

  • 容器的真搬运赋值

s2 = std::move(s1);

UB_stack& UB_stack::operator=(UB_stack &&s) {
if (this != &s) {
delete head_;
head_ = std::move(s.head_);
s.head_ = nullptr;
}
return *this;
}

End.

[c++] Copy Control的更多相关文章

  1. Copy Control settings

    Copy Control settings     Skip to end of metadata   Created by Rajesh Banka, last modified by Jyoti ...

  2. [C++] Copy Control (part 1)

    Copy, Assign, and Destroy When we define a class, we specify what happens when objects of the class ...

  3. C/C++:copy control (拷贝控制)

    前言:当定义一个类的时候,我们显示或者隐式地指定在此类型的对象拷贝,移动,赋值,销毁时做些什么,一个类通过定义五种特殊的成员函数来控制这些操作,包括拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值 ...

  4. C++之拷贝控制 (Copy Control)

    只有2种成员 值成员: 指针成员: 依实现可分为raw pointer / shared_ptr; 现在,仅考虑第③种:资源对象共享 角度来考虑拷贝控制 类的两种语义:值语义.似指针 编译器提供的de ...

  5. 【C++ 补习】Copy Control

    C++ Primer 5th edition, chapter 13. The Rule of Three If a class needs a destructor, it almost surel ...

  6. Bug 14143011 : ORA-19606: CANNOT COPY OR RESTORE TO SNAPSHOT CONTROL FILE

    Bug 14143011 : ORA-19606: CANNOT COPY OR RESTORE TO SNAPSHOT CONTROL FILE [oracle@test]$ tail -f rma ...

  7. C++-copy constructor、copy-assignment operator、destructor

    本文由@呆代待殆原创,转载请注明出处. 对于一个类来说,我们把copy constructor.copy-assignment operator.move constructor.move-assig ...

  8. [c++] Smart Pointers

    内存管理方面的知识 基础实例: #include <iostream> #include <stack> #include <memory> using names ...

  9. code of C/C++(3) - 从 《Accelerated C++》源码学习句柄类

    0  C++中多态的概念 多态是指通过基类的指针或者引用,利用虚函数机制,在运行时确定对象的类型,并且确定程序的编程策略,这是OOP思想的核心之一.多态使得一个对象具有多个对象的属性.class Co ...

随机推荐

  1. Qt for Mac 安装(包括PyQt)

    下载Qt: http://qt-project.org/downloads Qt5.2 for Mac:http://download.qt-project.org/official_releases ...

  2. 机器学习&深度学习资料

    机器学习(Machine Learning)&深度学习(Deep Learning)资料(Chapter 1) 机器学习(Machine Learning)&深度学习(Deep Lea ...

  3. call和bind改变的上下文环境

    这周自我学习的时间很宽裕.正巧一直对call和apply不甚理解(虽然bind还经常用到,仅仅是知道这么用有这个效果= =,而不知为何有这个效果),下午就自己写写demo,帮助自己理解. functi ...

  4. C++风格的回调对象方法. 采用template实现

    今天看了一篇文章,收藏一下代码.读一读很有激情 #include <iostream> #include <string> #include <vector> us ...

  5. LINQ 的使用

    [转]链接:cnblogs.com/liqingwen/p/5832322.html LINQ 简介 语言集成查询 (LINQ) 是 Visual Studio 2008 和 .NET Framewo ...

  6. ios视频播放器,代码和界面分离

    最近业余时间整理的一个ios播放器,界面采用storyboard,以前几乎都是用代码布局,但是用过一个项目storyboard后,就感觉storyboard很靠谱,团队合作版本控制的问题解决其实很简单 ...

  7. 使用EF取数据库返回的数据

    目录 一.取oracle自定义函数返回的自定义类型. 一.取oracle自定义函数返回的自定义类型. 1.首先创建一个函数返回自定义类型集合 --1.建立自定义类型 CREATE OR REPLACE ...

  8. 剑指Offer面试题:2.二维数组中的查找

    一.题目:二维数组中的查找 题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. ...

  9. Guava库介绍之集合(Collection)相关的API

    作者:Jack47 转载请保留作者和原文出处 欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 本文是我写的Google开源的Java编程库Guava系列之一,主要介 ...

  10. Got the Best Employee of the year 2015 Star Award

    Got "The Best Employee of the year 2015 Star Award" from the company, thanks to all that h ...