Posts C++primer-p13 拷贝控
Post
Cancel

C++primer-p13 拷贝控

第十三章 拷贝控制

拷贝控制操作(copy control):

  • 拷贝构造函数(copy constructor)
  • 拷贝赋值运算符(copy-assignment operator)
  • 移动构造函数(move constructor)
  • 移动赋值函数(move-assignement operator)
  • 析构函数(destructor)

拷贝、赋值和销毁

直接初始化与拷贝初始化的区别

拷贝初始化跟是不是拷贝没什么关系。

1
2
3
4
5
6
string dots(10,'.');    // 直接初始化
string s(dots);      // 直接初始化

string s2 = dots;       // 拷贝初始化
string bool = "0-00-000"    // 拷贝初始化
string nines = string(100,'9'); // 拷贝初始化
  1. 直接初始化要求编译器匹配普通的构造函数

  2. 拷贝初始化,要求编译器将右侧运算对象拷贝到正在创建的对象中。

  3. 如果一个类有移动构造函数,则拷贝初始化时候会使用移动构造函数而非拷贝构造函数。

  4. 拷贝构造函数还发生在一下情况:

    • 将对象作为一个实参传递给一个非引用类型的形参
    • 从一个返回类型为非引用类型的函数返回一个对象
    • 用花括号初始….

拷贝构造函数

  • 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数
  • class Foo{ public: Foo(const Foo&); }
  • 合成的拷贝构造函数(synthesized copy constructor):会将参数的成员逐个拷贝到正在创建的对象中。
  • 拷贝初始化
    • 将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
    • 通常使用拷贝构造函数完成。
    • string book = "9-99";
    • 出现场景:
      • =定义变量时。
      • 将一个对象作为实参传递给一个非引用类型的形参。
      • 从一个返回类型为非引用类型的函数返回一个对象。
      • 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1. 拷贝构造初探究
class User{
private:
    int age;
    string name;
public:
    const string &getName() const {
        return name;
    }

    explicit User(int age, const string &name) : age(age), name(name) {} // 明白的告诉编译器 该构造函数只能用户显示构造
    User(const User& user) :age(user.age),name(user.name){}  // 与默认拷贝构造类似 编译器默认拷贝构造
};
void test01(){
    User user(10,"jay");
    User u2 = user;
//    User u3 = {10,"jar"};  // 没加 explicit 是对的  加了就报错  不能走默认构造函数,只能走拷贝
    cout << u2.getName() << endl;
}

拷贝赋值运算符

  • 重载赋值运算符
    • 重写一个名为operator=的函数.
    • 通常返回一个指向其左侧运算对象的引用。
    • Foo& operator=(const Foo&);
  • 合成拷贝赋值运算符
    • 将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo1{
public:
    int age;
    Demo1& operator=(const Demo1& d){
        age += d.age;
        return *this;
    }
};
void test06(){
    Demo1 d1,d2;
    cout << d1.age << endl;
    d2.age = 10;
    d1 = d2;
    cout << d1.age << endl;
}

析构函数

  • 释放对象所使用的资源,并销毁对象的非static数据成员。
  • 名字由波浪号接类名构成。没有返回值,也不接受参数。
  • ~Foo();
  • 调用时机:
    • 变量在离开其作用域时。
    • 当一个对象被销毁时,其成员被销毁。
    • 容器被销毁时,其元素被销毁。
    • 动态分配的对象,当对指向它的指针应用delete运算符时。
    • 对于临时对象,当创建它的完整表达式结束时。
  • 合成析构函数
    • 空函数体执行完后,成员会被自动销毁。
    • 注意:析构函数体本身并不直接销毁成员。

三/五法则

  • 需要析构函数的类也需要拷贝和赋值操作。
  • 需要拷贝操作的类也需要赋值操作,反之亦然。

使用=default

  • 可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本。
  • 合成的函数将隐式地声明为内联的。
1
2
3
4
5
6
7
class User{
public:
    User() = default;
    User(const User &) = default;
    User& operator=(const User&) = default;
    ~User() = default;
};

阻止拷贝

  • 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
  • 定义删除的函数:=delete
  • 虽然声明了它们,但是不能以任何方式使用它们。
  • 析构函数不能是删除的成员。
  • 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
  • 老版本使用private声明来阻止拷贝。
1
2
3
4
5
6
7
8
9
10
class Demo3{
public:
    Demo3(const Demo3&) = delete;  // 禁止构造拷贝
    Demo3& operator =(const Demo3&) = delete;   // 禁止值拷贝
    Demo3() {};
};
void test07(){
    Demo3 d,d1;
//    d(d1); 
}

拷贝控制和资源管理

  • 类的行为可以像一个值,也可以像一个指针。
    • 行为像值:对象有自己的状态,副本和原对象是完全独立的。
    • 行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 行为像值的类
class HasPtr{
public:
    HasPtr(const string &str): ps(new string(str)),i(0)
    {};
    // 拷贝赋值
    HasPtr& operator=(const HasPtr &rhs){
        cout << "拷贝赋值" << endl;
        auto newp = new string(*rhs.ps);
        delete ps;
        ps = newp;
        i = rhs.i;
        return *this;
    }
    // 拷贝构造
    HasPtr(const HasPtr &p):ps(new string(*p.ps)),i(p.i){
        cout << "拷贝构造" << endl;
    }
    string *getPs() const {
        return ps;
    }
    // 析构函数
    ~HasPtr(){
        delete ps;
    }
private:
    string *ps;
    int i;
};

有空再看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 行为像指针的类
class HasPtr{
public:
    HasPtr(const string &str): ps(new string(str)),i(0),use(new size_t(1))
    {};
    // 拷贝赋值
    HasPtr& operator=(const HasPtr &rhs){
        ++ *rhs.use;
        if ( -- *use == 0){
            delete ps;
            delete use;
        }
        ps = rhs.ps;
        i = rhs.i;
        use = rhs.use;
        return *this;
    }
    // 拷贝构造
    HasPtr(const HasPtr &p):ps(new string(*p.ps)),i(p.i),use(p.use){
        ++ *use;
        cout << "拷贝构造" << endl;
    }
    string *getPs() const {
        return ps;
    }
    // 析构函数
    ~HasPtr(){
        if ( --*use == 0){
            cout << "调用析构函数" << endl;
            delete ps;
            delete use;
        }
    }
private:
    string *ps;
    int i;
    size_t *use;
};

拷贝构造与赋值构造

1
2
3
4
5
void test11(){
    HasPtr h1("xx");
    HasPtr h2 = h1;  // 拷贝构造
//    h2  =  h1;    // 赋值构造
}

交换操作

一次拷贝,两次赋值

  • 管理资源的类通常还定义一个名为swap的函数。
  • 经常用于重排元素顺序的算法。
  • swap而不是std::swap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Demo33{
private:
    int age;
    string name;
public:
    Demo33(int age, const string &name) : age(age), name(name) {}
    Demo33(Demo33 &d):age(d.age),name(d.name){
        cout << "调用拷贝构造" << endl;
    }
    Demo33& operator=(const Demo33& d){
        cout << "调用拷贝赋值" << endl;
        age = d.age;
        name = d.name;
    }
    friend void swap(Demo33&,Demo33&);
};

void swap(Demo33 &d1,Demo33 &d2){
    cout << "调用自己默认重载版本swap" << endl;
    using std::swap;
    swap(d1.name,d2.name);
    swap(d1.age,d2.age);
}

void test15(){
    Demo33 d1(1,"xx");
    Demo33 d2(2,"xxxx");
    using std::swap;
    swap(d1,d2);
}

自己动手写一个类似vector的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class SreVec{
public:
    SreVec() : elements(nullptr),first_free(nullptr),cap(nullptr) {}
    SreVec(const SreVec&);  // 拷贝构造
    SreVec& operator=(const SreVec&);  // 拷贝赋值运算符
    ~SreVec();  // 析构

    void push_back( const string&);  // 压栈操作
    size_t size() const {
        return first_free - elements;
    }
    size_t capacity() const{
        return cap - elements;
    }
    string* begin() const{
        return elements;
    }
    string* end() const{
        return first_free;
    }

private:
    allocator<string> alloc;
    // 保证有新元素容纳空间
    void chk_n_alloc(){
        if (size() == capacity())
            reallocate();
    }
    // 不太知道
    pair<string*,string*> alloc_n_copy(const string*,const string*);
    void free();  // 销毁元素并释放内存
    void reallocate();  // 获得更多内存并拷贝已有元素

    string *elements;  // 数组首元素指针
    string *first_free;  // 数组第一个空闲的指针
    string *cap;   // 数组最后位置指针
};
void SreVec::push_back(const string &s) {
    chk_n_alloc();
    alloc.construct(first_free++,s);
}
pair<string*, string*> SreVec::alloc_n_copy(const string *a, const string *b) {
    auto data = alloc.allocate(b - a);
    return {data, uninitialized_copy(a,b,data)};
}

void SreVec::free() {
    if (elements){
        for(auto p = first_free;p != elements;)    // 1. destory 元素
            alloc.destroy(--p);
        alloc.deallocate(elements,cap - elements);   // 2. 释放SreVec自己分配的空间
    }
}
SreVec::SreVec(const SreVec &s) {
    auto newData = alloc_n_copy(s.begin(),s.end());
    elements = newData.first;
    first_free = newData.second;
}
SreVec::~SreVec() {
    free();
}

SreVec &SreVec::operator=(const SreVec &s) {
    auto data = alloc_n_copy(s.begin(),s.end());
    free();
    elements = data.first;
    first_free = data.second;
    return *this;
}

void SreVec::reallocate() {
    auto newCapacity = size() ? 2 * size() : 1;
    auto newData = alloc.allocate(newCapacity);
    auto dest = newData;
    auto elem = elements;
    for(size_t i = 0;i != size(); ++ i){
        alloc.construct(dest++,*elem++);
    }
    free();
    elements = newData;
    first_free = dest;
    cap = elements + newCapacity;
}

void test19(){
    SreVec sreVec;
    sreVec.push_back("hello");
    sreVec.push_back("jar");

    SreVec s2 = sreVec;
    auto p = s2.begin();
    while (p != s2.end()){
        cout << *p ++ << endl;
    }
}

对象移动 (这是个大坑,先留着)

  • 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
  • 在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
  • 标准库容器、stringshared_ptr类既可以支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。

右值引用

  • 新标准引入右值引用以支持移动操作。
  • 通过&&获得右值引用。
  • 只能绑定到一个将要销毁的对象。
  • 常规引用可以称之为左值引用。
  • 左值持久,右值短暂。

move函数

  • int &&rr2 = std::move(rr1);
  • move告诉编译器,我们有一个左值,但我希望像右值一样处理它。
  • 调用move意味着:除了对rr1赋值或者销毁它外,我们将不再使用它。

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

  • 移动构造函数
    • 第一个参数是该类类型的一个引用,关键是,这个引用参数是一个右值引用
    • StrVec::StrVec(StrVec &&s) noexcept{}
    • 不分配任何新内存,只是接管给定的内存。
  • 移动赋值运算符
    • StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
  • 移动右值,拷贝左值。
  • 如果没有移动构造函数,右值也被拷贝。
  • 更新三/五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
  • 移动迭代器:
    • make_move_iterator函数讲一个普通迭代器转换为一个移动迭代器。
  • 建议:小心地使用移动操作,以获得性能提升。

右值引用和成员函数

  • 区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&
  • 引用限定符:
    • 在参数列表后面防止一个&,限定只能向可修改的左值赋值而不能向右值赋值。
This post is licensed under CC BY 4.0 by the author.

Contents

Trending Tags