C++ Primer 笔记——拷贝控制

1.如果构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数的第一个参数必须是引用类型(否则会无限循环的调用拷贝构造函数)。

2.如果没有为一个类定义拷贝构造函数,编译器会为我们定义一个合成拷贝构造函数。与合成默认构造函数不同,即使我们定义了其他的构造函数,编译器也会为我们合成一个拷贝构造函数。

3.合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中(除了static成员)。对于类类型的成员,会使用其拷贝构造函数来拷贝,虽然我们不能拷贝一个数组,但会逐元素的拷贝一个数组类型的成员。

4.当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数,当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。

5.在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。但是拷贝/移动构造函数必须是存在且可访问的(例如,不能是private)。

std::string str("test");    // 编译器略过了拷贝构造函数

6.与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。或者我们可以自己重载。

class test
{
public:
    test& operator=(const test& t);
private:
    int m_id;
};

test& test::operator=(const test& t)
{
    m_id = t.m_id;
    return *this;    // 返回一个此对象的引用
}

7.析构函数释放对象使用的资源,并销毁对象的非static数据成员。由于析构函数不接受参数,所以不能被重载。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。

8.与普通指针不同,智能指针是类类型,所以具有析构函数,因此与普通指针不同,智能指针成员在析构阶段会被自动销毁。

9.我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default,在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的,如果不希望合成的成员是内联的,应该在类外定义使用=default。

10.与=defalut不同的是,我们可以对任何函数指定=delete。而且=dalete必须出现在函数第一次声明的时候。对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。

test t;    // 错误
test *p = new test();    // 正确
delete p;    // 错误

11.如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数将被定义为删除的,例如成员有引用类型或者无法默认构造的const成员等。

12.当你编写一个赋值运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中(因为可能将一个对象赋予它自身而且可能会出现异常)。当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了。

13.对于分配了资源的类,自定义swap可能是一种很重要的优化手段,因为我们可以只交换指针而不是交换整个动态分配的内存。而标准库的swap会进行一次拷贝两次赋值。

class test
{
public:
    test(int i) :m_p(new int(i)){}
    ~test() { if (m_p) delete m_p; m_p = nullptr; }

    int *m_p;
};

inline void swap(test& t1, test& t2)
{
    std::swap(t1.m_p, t2.m_p);    // 交换指针,而不是动态分配的int
}

test t1(1);
test t2(2);

std::cout << *t1.m_p << std::endl;    // 输出1
std::cout << *t2.m_p << std::endl;    // 输出2

using std::swap;
swap(t1, t2);    // 如果存在类型特定的swap版本,则不会调用std::swap

std::cout << *t1.m_p << std::endl; // 输出2
std::cout << *t2.m_p << std::endl; // 输出1

14.一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。所谓右值引用就是必须绑定到右值得引用,我们不能将一个右值引用绑定到一个左值上。

int i = 42;
int &r = i;    // 正确
int &&rr = i;    // 错误,右值引用不能绑定到一个左值上
int &r1 = i * 10; // 错误,i*10是一个右值
const int &r2 = i * 10;    // 正确,我们可以将一个const引用绑定到一个右值上
int &&rr1 = i * 10;        // 正确

15.由于右值引用只能绑定到临时对象,我们得知:所引用的对象将要被销毁;该对象没有其他用户。这两个特性意味着使用右值引用的代码可以自由地接管所引用的对象的资源。

16.变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。但是我们可以显示的将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。

int &&rr = 10;
int &&rr1 = rr;    // 错误,rr是左值
int &&rr2 = std::move(rr);    // 正确

move调用告诉编译器,我们有一个左值,但是我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。

17.类似拷贝构造函数。移动构造函数的第一个参数是该类类型的引用,但是是一个右值引用,任何额外的参数都必须有默认实参。除了完成资源移动,移动构造函数还必须确保移后源对象被销毁是无害的。特别的是,一旦资源完成移动,源对象必须不再指向被移动的资源。

class test
{
public:
    test(int i) :m_p(new int(i)){}
    test(test &&t)
    {
        if (m_p)
            delete m_p;        // 先释放自己的资源
        m_p = t.m_p;        // 指向t的资源
        t.m_p = nullptr;    // 保证t可以被正常析构
    }

    int *m_p;
};

test t1(1);
test t2(std::move(t1));

18.只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。与拷贝操作不同,移动操作永远不会隐式的定义为删除的函数,但是,如果我们显示的要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。

19.定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。

20.如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数:移动右值,拷贝左值。如果没有移动构造函数,则都调用拷贝构造函数,赋值预算符也一样。

21.如果我们为一个类添加一个移动构造函数,它实际上也会获得一个移动赋值运算符。

class test
{
public:
    test(int i):m_p(new int(i)) {}
    test(test &t) { m_p = t.m_p; }    // 因为定义了移动构造函数,这里必须显示定义
    test(test &&t)
    {
        if (m_p)
            delete m_p;
        std::swap(m_p, t.m_p);
    }

    test& operator=(test t)
    {
        std::swap(m_p, t.m_p);
        return *this;
    }

    int* m_p;
};

test t1(1);
test t2 = t1;    // 调用了拷贝构造函数
test t3 = std::move(t1);    // 调用了移动构造函数

22.移动迭代器解引用运算符生成一个右值引用。我们通过调用make_move_iterator将一个普通迭代器转换为一个移动迭代器。

std::vector<std::string> vec = { "1","2","3" };
std::allocator<std::string> alloc;
auto first = alloc.allocate(5);
auto last = std::uninitialized_copy(std::make_move_iterator(vec.begin()), std::make_move_iterator(vec.end()),first);    // 移动构造,vec里现在存的已经是空字串

23.我们可以在参数列表后放置一个引用限定符来阻止向右值赋值。

  • 类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在声明和定义中
  • 类似const,引用限定符也可以区分重载版本
  • 引用限定符必须跟随在const限定符之后
  • 如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加
std::string str1, str2;
(str1 + str2) = "test";    // 可以向右值赋值,但没有意义

class test
{
public:
    test(int i) { m_id = i; }

    test& operator=(const test& t) &    // 只能向可修改的左值赋值
    {
        m_id = t.m_id;
        return *this;
    }

    test add() const &     // 如果和const一起用,则const必须在前面
    {
        test t(*this);    // 不能改变this
        t.m_id++;
        return t;
    }

    test add() &&        // 本对象为右值
    {
        m_id++;
        return *this;
    }

public:
    int m_id;
};

test t1(1);
t1.add();        // t1是左值,调用test add() const &
test(2).add();    // 右值,调用test add() &&
class test
{
public:
    void add() && ;
    void add() const;    // 错误,必须加上引用限定符

    void sub();
    void sub() const;    // 正确,两个版本都没有引用限定符
};
时间: 2024-10-22 10:50:43

C++ Primer 笔记——拷贝控制的相关文章

【C++ Primer】拷贝控制

十三.复制控制 1. 复制构造函数 类中的成员函数都默觉得inline类型.所以即使在类定义体内的函数声明显示定义为inline类型,在进行函数定义时也可以将inline进行省略. // 复制构造函数应该为常量引用类型,假设同意传值參数会造成无限循环调用从而导致内存溢出. CopyConstruct(const CopyConstruct& a){value = a.value;} 复制构造函数可用于初始化顺序容器中的元素,如vector<string> svec(5); 这样的方式使

【足迹C++primer】54、继承类的范围,构造函数和拷贝控制

继承类的范围,构造函数和拷贝控制 当用派生类执行函数的时候,首先会在当前的类里面找 如果找不到就一级一级地往上找. Name Lookup Happens at Compile Time class Quote { public: Quote()=default; Quote(const string &book, double sales_price):bookNo(book), price(sales_price) {cout<<"Quote gouzhao functi

【足迹C++primer】45、拷贝控制示例

拷贝控制示例 那么接下来尽情欣赏这个案例吧!!! /** * 功能:拷贝控制示例 * 时间:2014年7月14日10:57:39 * 作者:cutter_point */ #include<iostream> #include<set> #include<vector> #include<string> using namespace std; class Folder; /** Message类 */ class Message { friend void

C++ Primer笔记9_构造函数_拷贝构造(深拷贝与浅拷贝)

1.构造函数: >构造函数是一个特殊的.与类同名的成员函数,用于给每一个成员设置适当的初始值. >构造函数不能有返回值,函数名与类名同样. >缺省构造函数时,系统将自己主动调用该缺省构造函数初始化对象,缺省构造函数会将全部数据成员都初始化为零或       空.缺省构造函数是不带參数的构造函数. >创建一个对象时,系统自己主动调用构造函数. 构造函数的特点: 1.构造函数能够重载,传入什么实參决定调用不同版本号的构造函数. 2.构造函数不能声明为const .也不能声明为virtu

【足迹C++primer】43、拷贝控制和资源管理

拷贝控制和资源管理 13.2.1行为像值的类 *定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针 *定义一个析构函数来释放string *定义一个拷贝赋值运算符来释放对象当前的string,并从右侧运算对象拷贝string class HasPtr { public: HasPtr(const string &s=string()):ps(new string(s)), i(0){} //对ps指向的string,每个HasPtr对象都有自己的拷贝 HasPtr(const HasP

《C++primer(第五版)》学习之路-第十三章:拷贝控制

[ 声明:版权所有,转载请标明出处,请勿用于商业用途.  联系信箱:[email protected]] 13.1 拷贝.赋值与销毁 1.当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝.移动.赋值和销毁时做什么.一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数. 2.在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化.在一个析构函数中,首先执行函数体,然后销毁成员.成员按

C++笔记(11):拷贝控制(拷贝移动,构造赋值,析构)

控制对象拷贝,赋值,析构 拷贝构造函数,移动构造函数 拷贝赋值运算符,移动赋值运算符 析构函数 ------------------------------------------------------------------------------------------------------------------------------------- 1. 拷贝构造函数:参数必须是引用类型&,一般是const的 拷贝构造函数的第1个参数指的是对于自身类类型的引用 2.拷贝赋值运算符:本

C++ Primer学习总结 第13章 拷贝控制

第13章 拷贝控制 1.    什么是拷贝构造函数? P440 如果一个类的构造函数的第一个参数是自己类类型的引用, 且所有其他参数都有默认值, 那么这就是一个拷贝构造函数. 2.    拷贝初始化和直接初始化.  P441 如果初始化的时候使用等号"="来初始化一个对象, 那么就是拷贝初始化. 相反, 如果初始化时, 没有用等号"=", 那么就是直接初始化. 如果是拷贝初始化, 那么用的一定是拷贝构造函数(也有可能用移动构造函数). 如果是直接初始化, 那么就用参

【C++ Primer 第13章】2. 拷贝控制和资源管理

拷贝控制和资源管理 • 类的行为像一个值.意味着它应该有自己的状态,当我们拷贝一个像值得对象时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响. • 行为像指针的类则共享状态.当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象. 13.2节练习 1 #include<iostream> 2 #include<string> 3 using namespace std; 4 5 class HasPtr { 6 public: 7 HasP